From 9e9139f43a13944d202e2a2f4582f5bef8dbf424 Mon Sep 17 00:00:00 2001 From: Nino Date: Sat, 31 Oct 2026 09:31:20 -0600 Subject: [PATCH] =?UTF-8?q?Ocultar=20de=20=C2=ABContinuar=20leyendo=C2=BB?= =?UTF-8?q?=20los=20mangas=20terminados=20salvo=20que=20haya=20cap=C3=ADtu?= =?UTF-8?q?los=20nuevos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- app/public/app.js | 7 +++++-- app/server.js | 21 ++++++++++++++++----- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/app/public/app.js b/app/public/app.js index fa156f8..a3ee451 100644 --- a/app/public/app.js +++ b/app/public/app.js @@ -179,10 +179,12 @@ function mangaCard(m) { } function continueCard(h) { const pct = h.total_pages ? Math.round(((h.page + 1) / h.total_pages) * 100) : 0 - return ` + // Terminado con capítulos nuevos: a la ficha, para elegir el siguiente. + return `
${esc(h.manga_title)}
${h.chapter_num ? `Cap. ${esc(h.chapter_num)}` : 'Oneshot'} · pág. ${h.page + 1}/${h.total_pages || '?'}
+ ${h.finished ? '
Terminado
' : h.hasNew ? '
Caps. nuevos
' : ''}
${timeAgo(h.updated_at)}
` } @@ -254,7 +256,7 @@ async function viewHome(params) { if (!q && !tag && !showCats) { Promise.all([api('/api/history'), api('/api/follows')]).then(([hist, fol]) => { if (!$('#cont')) return - $('#cont').innerHTML = `
${newsSection(fol)}
` + (hist.length ? `

Continuar leyendo

${hist.slice(0, 12).map(continueCard).join('')}
` : '') + $('#cont').innerHTML = `
${newsSection(fol)}
` + (hist = hist.filter(h => !h.finished), hist.length ? `

Continuar leyendo

${hist.slice(0, 12).map(continueCard).join('')}
` : '') bindNews(fol) }).catch(() => {}) } @@ -452,6 +454,7 @@ async function viewReader(mangaId, chapterId, startPage) { const payload = () => ({ mangaId, chapterId, mangaTitle: manga.title, cover: manga.cover, chapterNum: chapter.num ?? null, chapterTitle: chapter.title, lang: manga.source === 'lc' ? 'es' : chapter.lang || lang, page: lastShown(), totalPages: total, + isLast: idx >= 0 && !next, // al terminarlo, el manga sale de "Continuar leyendo" }) const saveNow = () => { clearTimeout(saveTimer); saveTimer = null; api('/api/progress', { method: 'POST', body: payload() }).catch(() => {}) } const scheduleSave = () => { clearTimeout(saveTimer); saveTimer = setTimeout(saveNow, 700) } diff --git a/app/server.js b/app/server.js index d0cb3e5..0b52a1d 100644 --- a/app/server.js +++ b/app/server.js @@ -107,6 +107,8 @@ CREATE TABLE IF NOT EXISTS downloaded_chapters ( CREATE INDEX IF NOT EXISTS idx_dlch_manga ON downloaded_chapters(manga_id); `) db.prepare('DELETE FROM sessions WHERE expires_at < ?').run(Date.now()) +// finished_at: cuándo se terminó la última página del último capítulo disponible (null = en curso). +if (!db.prepare('PRAGMA table_info(progress)').all().some(c => c.name === 'finished_at')) db.exec('ALTER TABLE progress ADD COLUMN finished_at INTEGER') // ---------- Contraseñas y sesiones ---------- function hashPassword(pw) { @@ -796,7 +798,14 @@ async function route(req, res) { // --- Historial --- if (p === '/api/history' && method === 'GET') { - return json(res, 200, db.prepare('SELECT * FROM progress WHERE user_id = ? ORDER BY updated_at DESC LIMIT 200').all(user.id)) + // Un manga terminado vuelve a "en curso" si después aparecieron capítulos nuevos. + const rows = db.prepare(`SELECT p.*, f.last_new_at FROM progress p + LEFT JOIN follows f ON f.user_id = p.user_id AND f.manga_id = p.manga_id AND f.active = 1 + WHERE p.user_id = ? ORDER BY p.updated_at DESC LIMIT 200`).all(user.id) + return json(res, 200, rows.map(({ last_new_at, ...r }) => { + const hasNew = !!(r.finished_at && last_new_at && last_new_at > r.finished_at) + return { ...r, finished: !!r.finished_at && !hasNew, hasNew } + })) } if (p === '/api/progress' && method === 'POST') { const b = await readBody(req) @@ -804,15 +813,17 @@ async function route(req, res) { const page = Math.max(0, Number(b.page) | 0) const total = Math.max(0, Number(b.totalPages) | 0) const now = Date.now() + const finishedAt = b.isLast === true && total && page >= total - 1 ? now : null const prevPage = db.prepare('SELECT page FROM chapter_reads WHERE user_id = ? AND chapter_id = ?').get(user.id, b.chapterId)?.page - db.prepare(`INSERT INTO progress (user_id, manga_id, manga_title, cover, chapter_id, chapter_num, chapter_title, lang, page, total_pages, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + db.prepare(`INSERT INTO progress (user_id, manga_id, manga_title, cover, chapter_id, chapter_num, chapter_title, lang, page, total_pages, updated_at, finished_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (user_id, manga_id) DO UPDATE SET manga_title = excluded.manga_title, cover = excluded.cover, chapter_id = excluded.chapter_id, chapter_num = excluded.chapter_num, chapter_title = excluded.chapter_title, - lang = excluded.lang, page = excluded.page, total_pages = excluded.total_pages, updated_at = excluded.updated_at`) + lang = excluded.lang, page = excluded.page, total_pages = excluded.total_pages, updated_at = excluded.updated_at, + finished_at = excluded.finished_at`) .run(user.id, b.mangaId, String(b.mangaTitle || '').slice(0, 300), b.cover ? String(b.cover).slice(0, 1000) : null, b.chapterId, b.chapterNum != null ? String(b.chapterNum) : null, String(b.chapterTitle || '').slice(0, 300), - LANG.test(b.lang || '') ? b.lang : null, page, total, now) + LANG.test(b.lang || '') ? b.lang : null, page, total, now, finishedAt) db.prepare(`INSERT INTO chapter_reads (user_id, chapter_id, manga_id, page, total_pages, updated_at) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (user_id, chapter_id) DO UPDATE SET page = excluded.page, total_pages = excluded.total_pages, updated_at = excluded.updated_at`) .run(user.id, b.chapterId, b.mangaId, page, total, now)