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)