diff --git a/docs/guide/using-librislog/progress.md b/docs/guide/using-librislog/progress.md index 6e8f103d..046c7165 100644 --- a/docs/guide/using-librislog/progress.md +++ b/docs/guide/using-librislog/progress.md @@ -33,3 +33,12 @@ Click the delete button next to an entry to remove it from the log. A confirmati ## Reading Percentage For books with a page count, the detail view shows your current page, total pages, reading percentage, and a visual progress bar. + +## Reading Sessions & Re-reads + +Books can be read more than once. Each reading is tracked as its own session with independent dates and progress. + +- **Read again** (book detail dialog, for finished books): starts a fresh reading today, keeping all earlier sessions and progress untouched. Use this for a genuine re-read. +- **Changing status from "read" to "currently reading"** (edit drawer): the legacy correction path. It can keep the previous finish date or clear it, and may leave the book in a "currently reading" state that still carries the old finish date. Use this only to correct a book's status, not to re-read it. + +For books that already have reading sessions, the start and finish dates are mirrors of the session and are read-only in the edit drawer; edit them via the reading history instead. diff --git a/docs/releases.md b/docs/releases.md index 8e6d70f1..16944eed 100644 --- a/docs/releases.md +++ b/docs/releases.md @@ -45,7 +45,7 @@ LibrisLog v1.10.0 adds four more metadata sources to the import search (BnF, Inv **Features** - 📖 **Multiple reading sessions per book.** Each book keeps a list of reading periods (start and finish dates) so re-reads never overwrite earlier dates. A new `reading_session` table stores them; `Book.date_started` / `Book.date_finished` remain a mirror of the current session, so every existing consumer (sorting, timeline, statistics, import/export) keeps working unchanged -- 🔁 **"Read again" action.** A ghost button in the book detail dialog starts a new reading session for a finished book. Earlier sessions and their progress stay untouched, and the progress bar resets to 0 % +- 🔁 **"Read again" action.** A ghost button in the book detail dialog starts a new reading session for a finished book. Earlier sessions and their progress stay untouched, and the progress bar resets to 0 %. This is the recommended way to re-read; changing a book's status from "read" to "currently reading" via the edit drawer is the legacy correction path and can keep the previous finish date - 📜 **Reading history.** A collapsible block in the book detail dialog lists every session (#, started, finished, status, pages) with per-row edit and delete, plus "Add a past reading" for back-filling handwritten history - 🏷️ **"read ×N" badge.** Cards, list rows and the book detail dialog show a small badge once a book has been read at least twice - 📊 **Statistics distinguish unique books from total completions.** A new "Total reads" card shows the number of completed reads (with a "N re-read books" sub-line). `books_finished_per_month/year`, the `books_per_month/year` goals, and `pages_read` now count every completion; "Pages read" in the bucket still counts each book once, so no single-session user sees any change diff --git a/frontend/src/lib/components/BookCard.svelte b/frontend/src/lib/components/BookCard.svelte index 458fa47b..cc4c3ce3 100644 --- a/frontend/src/lib/components/BookCard.svelte +++ b/frontend/src/lib/components/BookCard.svelte @@ -104,10 +104,9 @@ {$_(STATUS_LABEL_KEYS[book.reading_status])} {#if (book.read_count ?? 0) >= 2} - ×{book.read_count} + + ×{book.read_count} + {/if} diff --git a/frontend/src/lib/components/BookDetailDialog.svelte b/frontend/src/lib/components/BookDetailDialog.svelte index 070d473a..c071253a 100644 --- a/frontend/src/lib/components/BookDetailDialog.svelte +++ b/frontend/src/lib/components/BookDetailDialog.svelte @@ -5,7 +5,9 @@ import { formatDate, formatDateTime, + fromDateInputValue, fromDateTimeInputValue, + toDateInputValue, toDateTimeInputValue } from '#lib/date.js'; import { getTimezone } from '#lib/stores/timezone.js'; @@ -13,6 +15,7 @@ import { toasts } from '#lib/toasts.js'; import { formatLanguageCode } from '#lib/utils/language.js'; import { formatAuthors } from '#lib/utils/authors.js'; + import AdaptiveDateInput from './AdaptiveDateInput.svelte'; import StarRating from './StarRating.svelte'; import { Line } from 'svelte-chartjs'; import { X } from '@lucide/svelte'; @@ -175,19 +178,22 @@ try { const payload: { status: ReadingStatus; date_started: string | null; date_finished: string | null } = { status: addSessionStatus, - date_started: addSessionStarted ? fromDateTimeInputValue(addSessionStarted, tz) : null, - date_finished: addSessionFinished ? fromDateTimeInputValue(addSessionFinished, tz) : null + date_started: addSessionStarted ? fromDateInputValue(addSessionStarted, tz) : null, + date_finished: addSessionFinished ? fromDateInputValue(addSessionFinished, tz) : null }; + let result; if (editingSessionId !== null) { - await api.books.sessions.update(book.id, editingSessionId, payload); + result = await api.books.sessions.update(book.id, editingSessionId, payload); } else { - await api.books.sessions.create(book.id, payload); + result = await api.books.sessions.create(book.id, payload); } + book = result.book; + onSave?.(result.book); addSessionOpen = false; addSessionStarted = ''; addSessionFinished = ''; editingSessionId = null; - await loadSessions(); + await loadSessionsAndProgress(); } catch (e: unknown) { toasts.add( e instanceof Error ? e.message : $_('common.actionFailed', { values: { action: $_('book.addPastReading') } }), @@ -234,8 +240,8 @@ function openEditSession(s: ReadingSession) { addSessionStatus = s.status === 'currently_reading' ? 'read' : s.status; - addSessionStarted = s.date_started ? toDateTimeInputValue(s.date_started, tz) : ''; - addSessionFinished = s.date_finished ? toDateTimeInputValue(s.date_finished, tz) : ''; + addSessionStarted = s.date_started ? toDateInputValue(s.date_started, tz) : ''; + addSessionFinished = s.date_finished ? toDateInputValue(s.date_finished, tz) : ''; editingSessionId = s.id; addSessionOpen = true; } @@ -584,9 +590,11 @@
- {$_(`dateConflict.${typeKey}.message`, { values: i18nValues })} + {#if existingDate === suggestedDate && typeKey !== 'startedAfterFinished'} + {$_(`dateConflict.${typeKey}.messageSame`, { values: { date: existingDate } })} + {:else} + {$_(`dateConflict.${typeKey}.message`, { values: i18nValues })} + {/if}
-1 {$_(`dateConflict.${typeKey}.keepDesc`, { values: i18nValues })}
2 {$_(`dateConflict.${typeKey}.clearDesc`, { values: i18nValues })}
diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json index 06e59c0c..0ff8555a 100644 --- a/frontend/src/lib/i18n/locales/de.json +++ b/frontend/src/lib/i18n/locales/de.json @@ -225,7 +225,8 @@ "sessionReread": "Erneut gelesen", "sessionDeleteConfirm": "Diese Lesesitzung und ihre Fortschrittseinträge löschen?", "sessionDelete": "Sitzung löschen", - "readCount": "{count, plural, one {1 Mal} other {# Mal}} gelesen" + "readCount": "{count, plural, one {1 Mal} other {# Mal}} gelesen", + "datesManagedBySessions": "Wird über Lesesitzungen verwaltet" }, "addModal": { "manual": "Manuell", @@ -342,13 +343,15 @@ "title": "Startdatum bereits gesetzt", "message": "Startdatum ist bereits gesetzt. Möchtest du {oldDate} behalten oder {newDate} als neues Startdatum setzen?", "keepOld": "{oldDate} behalten", - "useNew": "{newDate} verwenden" + "useNew": "{newDate} verwenden", + "messageSame": "Startdatum ist bereits auf {date} gesetzt." }, "finished": { "title": "Beendigungsdatum bereits gesetzt", "message": "Beendigungsdatum ist bereits gesetzt. Möchtest du {oldDate} behalten oder {newDate} als neues Beendigungsdatum setzen?", "keepOld": "{oldDate} behalten", - "useNew": "{newDate} verwenden" + "useNew": "{newDate} verwenden", + "messageSame": "Enddatum ist bereits auf {date} gesetzt." }, "startedAfterFinished": { "title": "Buch bereits beendet", diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json index 0937ef6f..38b59c0c 100644 --- a/frontend/src/lib/i18n/locales/en.json +++ b/frontend/src/lib/i18n/locales/en.json @@ -225,7 +225,8 @@ "sessionReread": "Reread", "sessionDeleteConfirm": "Delete this reading session and its progress entries?", "sessionDelete": "Delete session", - "readCount": "Read {count, plural, one {1 time} other {# times}}" + "readCount": "Read {count, plural, one {1 time} other {# times}}", + "datesManagedBySessions": "Managed via reading sessions" }, "addModal": { "manual": "Manual", @@ -342,13 +343,15 @@ "title": "Start date already set", "message": "Date started is already set. Do you want to keep {oldDate} or set {newDate} as the new date started?", "keepOld": "Keep {oldDate}", - "useNew": "Use {newDate}" + "useNew": "Use {newDate}", + "messageSame": "Date started is already set to {date}." }, "finished": { "title": "Finish date already set", "message": "Date finished is already set. Do you want to keep {oldDate} or set {newDate} as the new date finished?", "keepOld": "Keep {oldDate}", - "useNew": "Use {newDate}" + "useNew": "Use {newDate}", + "messageSame": "Date finished is already set to {date}." }, "startedAfterFinished": { "title": "Book was already finished", diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json index 15114de8..cb0bc044 100644 --- a/frontend/src/lib/i18n/locales/es.json +++ b/frontend/src/lib/i18n/locales/es.json @@ -225,7 +225,8 @@ "sessionReread": "Releído", "sessionDeleteConfirm": "¿Eliminar esta sesión de lectura y sus entradas de progreso?", "sessionDelete": "Eliminar sesión", - "readCount": "Leído {count, plural, one {1 vez} other {# veces}}" + "readCount": "Leído {count, plural, one {1 vez} other {# veces}}", + "datesManagedBySessions": "Gestionado mediante sesiones de lectura" }, "addModal": { "manual": "Manual", @@ -342,13 +343,15 @@ "title": "Fecha de inicio ya establecida", "message": "La fecha de inicio ya está establecida. ¿Quieres conservar {oldDate} o establecer {newDate} como nueva fecha de inicio?", "keepOld": "Conservar {oldDate}", - "useNew": "Usar {newDate}" + "useNew": "Usar {newDate}", + "messageSame": "La fecha de inicio ya está establecida en {date}." }, "finished": { "title": "Fecha de finalización ya establecida", "message": "La fecha de finalización ya está establecida. ¿Quieres conservar {oldDate} o establecer {newDate} como nueva fecha de finalización?", "keepOld": "Conservar {oldDate}", - "useNew": "Usar {newDate}" + "useNew": "Usar {newDate}", + "messageSame": "La fecha de fin ya está establecida en {date}." }, "startedAfterFinished": { "title": "El libro ya estaba terminado", diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json index 47036cf8..4caf232e 100644 --- a/frontend/src/lib/i18n/locales/fr.json +++ b/frontend/src/lib/i18n/locales/fr.json @@ -225,7 +225,8 @@ "sessionReread": "Relu", "sessionDeleteConfirm": "Supprimer cette session de lecture et ses entrées de progression ?", "sessionDelete": "Supprimer la session", - "readCount": "Lu {count, plural, one {1 fois} other {# fois}}" + "readCount": "Lu {count, plural, one {1 fois} other {# fois}}", + "datesManagedBySessions": "Géré via les sessions de lecture" }, "addModal": { "manual": "Manuel", @@ -342,13 +343,15 @@ "title": "Date de début déjà définie", "message": "La date de début est déjà définie. Veux-tu conserver {oldDate} ou définir {newDate} comme nouvelle date de début ?", "keepOld": "Conserver {oldDate}", - "useNew": "Utiliser {newDate}" + "useNew": "Utiliser {newDate}", + "messageSame": "La date de début est déjà définie sur {date}." }, "finished": { "title": "Date de fin déjà définie", "message": "La date de fin est déjà définie. Veux-tu conserver {oldDate} ou définir {newDate} comme nouvelle date de fin ?", "keepOld": "Conserver {oldDate}", - "useNew": "Utiliser {newDate}" + "useNew": "Utiliser {newDate}", + "messageSame": "La date de fin est déjà définie sur {date}." }, "startedAfterFinished": { "title": "Livre déjà terminé", diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json index 90b8ee6e..ad7d0c98 100644 --- a/frontend/src/lib/i18n/locales/zh.json +++ b/frontend/src/lib/i18n/locales/zh.json @@ -225,7 +225,8 @@ "sessionReread": "重读", "sessionDeleteConfirm": "删除此阅读会话及其进度记录?", "sessionDelete": "删除会话", - "readCount": "阅读 {count} 次" + "readCount": "阅读 {count} 次", + "datesManagedBySessions": "通过阅读会话管理" }, "addModal": { "manual": "手动添加", @@ -342,13 +343,15 @@ "title": "开始日期已设置", "message": "开始日期已设置。你想保留 {oldDate} 还是将 {newDate} 设为新的开始日期?", "keepOld": "保留 {oldDate}", - "useNew": "使用 {newDate}" + "useNew": "使用 {newDate}", + "messageSame": "开始日期已设置为 {date}。" }, "finished": { "title": "完成日期已设置", "message": "完成日期已设置。你想保留 {oldDate} 还是将 {newDate} 设为新的完成日期?", "keepOld": "保留 {oldDate}", - "useNew": "使用 {newDate}" + "useNew": "使用 {newDate}", + "messageSame": "结束日期已设置为 {date}。" }, "startedAfterFinished": { "title": "本书已完成",