Modo libro 3D, tomos con cubiertas reales y ocultar créditos

Libro 3D (app/public/book3d.js): lector WebGL2 con mesa de madera, cubierta,
hojas que giran, cámara real y giroscopio, ahora con más recorrido alrededor
del libro.

Tomos (app/volumes.js): agrupa los capítulos en un solo libro con portada y
contraportada reales. El reparto sale de Wikipedia, MangaDex, M440 y
MangaUpdates, contrastados entre sí. Lo que ninguna fuente sitúa se estima, de
modo que solo quedan sueltos los capítulos del final, y los fuera de orden se
recolocan. Manhwa y manhua no tienen tomos.

Ocultar créditos (app/credits.js): botón por obra en la ficha. Al leer por
tomos oculta, sin borrarlas, la página cero y las dos últimas de cada capítulo
si se repiten en otros capítulos de la obra.

HANDOFF-LIBRO3D.md recoge el estado, las decisiones y las pruebas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-06 09:28:15 -06:00
co-authored by Claude Opus 5.5
parent bb1f3ff8e4
commit 66502409fe
10 changed files with 2430 additions and 58 deletions
+27 -1
View File
@@ -12,6 +12,7 @@
// Todo se guarda en la base de datos, una vez por manga y para todos los usuarios: lo guardado se sirve sin
// consultar nada y, si es antiguo, se actualiza en segundo plano (si una fuente falla, se queda lo que había).
// Los mangas seguidos por alguien se mantienen siempre al día, aunque nadie abra su ficha.
import { parseMuVolumes, workTitle } from './volumes.js'
const DAY = 24 * 3600e3
const INFO_TTL = v => (v?.found ? 30 * DAY : 3 * DAY)
const SYNC_EVERY = 3600e3
@@ -119,6 +120,31 @@ export function setupMeta({ db, md, sourceOf, fetchMangaFull, HttpError, json, n
}, wait)
}
// Las entradas de MangaDex con el título de la obra, en el orden en que las devuelve (primero la más parecida).
function mdMatches(mangaId) {
return stored(`mdworks:${mangaId}`, v => (v.length ? 30 * DAY : 3 * DAY), async () => {
const { manga } = await fetchMangaFull(mangaId, 'es')
const title = workTitle(manga.title)
const r = await md('/manga', { title, limit: 10, 'contentRating[]': MD_RATINGS }, 6 * 3600e3)
return (r?.data || []).filter(m => mdTitles(m).some(t => similar(t, title))).map(m => m.id)
}, true)
}
// Capítulo -> tomo según las publicaciones de MangaUpdates. Sirve para las obras licenciadas, que MangaDex deja sin capítulos.
async function muVolumes(mangaId) {
const l = await links(mangaId, true)
if (!l.mu) return {}
return stored(`mumap:${mangaId}`, v => (Object.keys(v).length ? 7 * DAY : 3 * DAY), async () => {
const rows = []
for (let page = 1; page <= 20; page++) {
const r = await mu('/releases/search', { search: String(l.mu), search_type: 'series', perpage: 100, page })
rows.push(...(r?.results || []).map(x => x.record))
if (!r || page * 100 >= r.total_hits) break
}
return parseMuVolumes(rows)
}, true)
}
// ---------- Ficha ----------
const AL_FIELDS = `id idMal siteUrl title{romaji english native} synonyms description(asHtml:true) format status
startDate{year month day} endDate{year month day} chapters volumes countryOfOrigin isAdult
@@ -287,5 +313,5 @@ export function setupMeta({ db, md, sourceOf, fetchMangaFull, HttpError, json, n
return true
}
return { route, links, info, cachedInfo, dates }
return { route, links, mdMatches, muVolumes, similar, stored, info, cachedInfo, dates }
}