- {$_(`dateConflict.${typeKey}.message`, { values: i18nValues })}
+ {#if existingDate === suggestedDate && typeKey !== 'startedAfterFinished'}
+ {$_(`dateConflict.${typeKey}.messageSame`, { values: { date: existingDate } })}
+ {:else}
+ {$_(`dateConflict.${typeKey}.message`, { values: i18nValues })}
+ {/if}
+
-
-
+ {#if existingDate === suggestedDate && typeKey !== 'startedAfterFinished'}
+
+ {:else}
+
+
+ {/if}
+ {#if rereadAvailable && onReread}
+
+ {/if}
{#if typeKey === 'startedAfterFinished'}
diff --git a/frontend/src/lib/i18n/locales/de.json b/frontend/src/lib/i18n/locales/de.json
index 989f8fc8..0ff8555a 100644
--- a/frontend/src/lib/i18n/locales/de.json
+++ b/frontend/src/lib/i18n/locales/de.json
@@ -74,7 +74,9 @@
"worstRated": "Am schlechtesten bewertet",
"showMore": "Mehr anzeigen",
"totalBooksAndAuthors": "Bücher insgesamt",
- "booksFromAuthors": "von {authors} {authors, plural, one {Autor} other {Autoren}}"
+ "booksFromAuthors": "von {authors} {authors, plural, one {Autor} other {Autoren}}",
+ "totalReads": "Lesungen gesamt",
+ "rereadBooks": "Erneut gelesene Bücher"
},
"dashboard": {
"title": "Lese-Dashboard",
@@ -213,7 +215,18 @@
"progressPromptSkip": "Überspringen",
"authorsPlaceholder": "Autor eingeben und Enter drücken",
"authorsHint": "Enter fügt einen Autor hinzu. Mit Rücktaste entfernt man den letzten.",
- "authorsLabel": "Autor(en)"
+ "authorsLabel": "Autor(en)",
+ "readAgain": "Erneut lesen",
+ "startAgain": "Erneut beginnen",
+ "readingHistory": "Leseverlauf",
+ "addPastReading": "Frühere Lektüre hinzufügen",
+ "sessionStarted": "Begonnen",
+ "sessionFinished": "Beendet",
+ "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",
+ "datesManagedBySessions": "Wird über Lesesitzungen verwaltet"
},
"addModal": {
"manual": "Manuell",
@@ -330,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",
@@ -345,7 +360,8 @@
"clearAndStart": "Beendigungsdatum löschen & heute starten",
"keepDesc": "Behält das Beendigungsdatum ({finishedDate}) und setzt kein Startdatum.",
"clearDesc": "Entfernt das Beendigungsdatum und setzt heute ({newStartDate}) als Startdatum."
- }
+ },
+ "startNewRead": "Neue Lektüre beginnen (behält frühere)"
},
"acquisition": {
"owned": "Im Besitz",
@@ -703,7 +719,8 @@
"subtitle": "Eine chronologische Ansicht der Bücher, die du gelesen hast",
"viewInLibrary": "Alle in der Bibliothek anzeigen",
"noReadBooks": "Noch keine gelesenen Bücher in deiner Bibliothek.",
- "goToLibrary": "Zur Bibliothek"
+ "goToLibrary": "Zur Bibliothek",
+ "reread": "Erneut gelesen"
},
"data": {
"title": "Datenverwaltung",
@@ -719,7 +736,8 @@
"books": "Bücher",
"progress": "Lesefortschritt",
"tags": "Tags",
- "covers": "Cover-Dateien"
+ "covers": "Cover-Dateien",
+ "sessions": "Lesesitzungen"
},
"button": "Daten exportieren",
"exporting": "Exportiere...",
diff --git a/frontend/src/lib/i18n/locales/en.json b/frontend/src/lib/i18n/locales/en.json
index ad6c5c2c..38b59c0c 100644
--- a/frontend/src/lib/i18n/locales/en.json
+++ b/frontend/src/lib/i18n/locales/en.json
@@ -74,7 +74,9 @@
"noRating": "No rating",
"topRated": "Top Rated",
"worstRated": "Worst Rated",
- "showMore": "Show more"
+ "showMore": "Show more",
+ "totalReads": "Total reads",
+ "rereadBooks": "Reread books"
},
"dashboard": {
"title": "Reading Dashboard",
@@ -213,7 +215,18 @@
"progressPromptSkip": "Skip",
"authorsPlaceholder": "Type an author and press Enter",
"authorsLabel": "Author(s)",
- "authorsHint": "Press Enter to add authors. Backspace removes the last author."
+ "authorsHint": "Press Enter to add authors. Backspace removes the last author.",
+ "readAgain": "Read again",
+ "startAgain": "Start again",
+ "readingHistory": "Reading history",
+ "addPastReading": "Add a past reading",
+ "sessionStarted": "Started",
+ "sessionFinished": "Finished",
+ "sessionReread": "Reread",
+ "sessionDeleteConfirm": "Delete this reading session and its progress entries?",
+ "sessionDelete": "Delete session",
+ "readCount": "Read {count, plural, one {1 time} other {# times}}",
+ "datesManagedBySessions": "Managed via reading sessions"
},
"addModal": {
"manual": "Manual",
@@ -330,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",
@@ -345,7 +360,8 @@
"clearAndStart": "Clear finish date & start today",
"keepDesc": "Keeps the finish date ({finishedDate}) and does not set a start date.",
"clearDesc": "Removes the finish date and sets today ({newStartDate}) as the start date."
- }
+ },
+ "startNewRead": "Start a new read (keeps your earlier reads)"
},
"acquisition": {
"owned": "Owned",
@@ -703,7 +719,8 @@
"subtitle": "A chronological view of books you've finished reading",
"viewInLibrary": "View all in library",
"noReadBooks": "No finished books in your library yet.",
- "goToLibrary": "Go to library"
+ "goToLibrary": "Go to library",
+ "reread": "Reread"
},
"data": {
"title": "Data Management",
@@ -719,7 +736,8 @@
"books": "Books",
"progress": "Reading progress",
"tags": "Tags",
- "covers": "Cover files"
+ "covers": "Cover files",
+ "sessions": "Reading sessions"
},
"button": "Export data",
"exporting": "Exporting...",
diff --git a/frontend/src/lib/i18n/locales/es.json b/frontend/src/lib/i18n/locales/es.json
index 990607b5..cb0bc044 100644
--- a/frontend/src/lib/i18n/locales/es.json
+++ b/frontend/src/lib/i18n/locales/es.json
@@ -74,7 +74,9 @@
"worstRated": "Peor valorados",
"showMore": "Mostrar más",
"totalBooksAndAuthors": "Libros en total",
- "booksFromAuthors": "de {authors} {authors, plural, one {autor} other {autores}}"
+ "booksFromAuthors": "de {authors} {authors, plural, one {autor} other {autores}}",
+ "totalReads": "Lecturas totales",
+ "rereadBooks": "Libros releídos"
},
"dashboard": {
"title": "Panel de lectura",
@@ -213,7 +215,18 @@
"progressPromptSkip": "Saltar",
"authorsPlaceholder": "Escribe un autor y pulsa Enter",
"authorsHint": "Pulsa Enter para añadir autores. Retroceso elimina el último.",
- "authorsLabel": "Autor(es)"
+ "authorsLabel": "Autor(es)",
+ "readAgain": "Volver a leer",
+ "startAgain": "Empezar de nuevo",
+ "readingHistory": "Historial de lectura",
+ "addPastReading": "Añadir lectura pasada",
+ "sessionStarted": "Iniciado",
+ "sessionFinished": "Terminado",
+ "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}}",
+ "datesManagedBySessions": "Gestionado mediante sesiones de lectura"
},
"addModal": {
"manual": "Manual",
@@ -330,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",
@@ -345,7 +360,8 @@
"clearAndStart": "Borrar fecha de finalización y empezar hoy",
"keepDesc": "Conserva la fecha de finalización ({finishedDate}) y no establece una fecha de inicio.",
"clearDesc": "Elimina la fecha de finalización y establece hoy ({newStartDate}) como fecha de inicio."
- }
+ },
+ "startNewRead": "Empezar una nueva lectura (conserva las anteriores)"
},
"acquisition": {
"owned": "En propiedad",
@@ -703,7 +719,8 @@
"subtitle": "Una vista cronológica de los libros que has terminado",
"viewInLibrary": "Ver todo en la biblioteca",
"noReadBooks": "Aún no hay libros terminados en tu biblioteca.",
- "goToLibrary": "Ir a la biblioteca"
+ "goToLibrary": "Ir a la biblioteca",
+ "reread": "Releído"
},
"data": {
"title": "Gestión de datos",
@@ -719,7 +736,8 @@
"books": "Libros",
"progress": "Progreso de lectura",
"tags": "Etiquetas",
- "covers": "Archivos de portada"
+ "covers": "Archivos de portada",
+ "sessions": "Sesiones de lectura"
},
"button": "Exportar datos",
"exporting": "Exportando...",
diff --git a/frontend/src/lib/i18n/locales/fr.json b/frontend/src/lib/i18n/locales/fr.json
index e198a71c..4caf232e 100644
--- a/frontend/src/lib/i18n/locales/fr.json
+++ b/frontend/src/lib/i18n/locales/fr.json
@@ -74,7 +74,9 @@
"worstRated": "Les moins bien notés",
"showMore": "Afficher plus",
"totalBooksAndAuthors": "Livres au total",
- "booksFromAuthors": "de {authors} {authors, plural, one {auteur} other {auteurs}}"
+ "booksFromAuthors": "de {authors} {authors, plural, one {auteur} other {auteurs}}",
+ "totalReads": "Lectures totales",
+ "rereadBooks": "Livres relus"
},
"dashboard": {
"title": "Tableau de bord de lecture",
@@ -213,7 +215,18 @@
"progressPromptSkip": "Passer",
"authorsPlaceholder": "Saisis un auteur et appuie sur Entrée",
"authorsHint": "Entrée pour ajouter des auteurs. Retour arrière supprime le dernier.",
- "authorsLabel": "Auteur(s)"
+ "authorsLabel": "Auteur(s)",
+ "readAgain": "Relire",
+ "startAgain": "Recommencer",
+ "readingHistory": "Historique de lecture",
+ "addPastReading": "Ajouter une lecture passée",
+ "sessionStarted": "Commencé",
+ "sessionFinished": "Terminé",
+ "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}}",
+ "datesManagedBySessions": "Géré via les sessions de lecture"
},
"addModal": {
"manual": "Manuel",
@@ -330,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é",
@@ -345,7 +360,8 @@
"clearAndStart": "Effacer la date de fin et commencer aujourd'hui",
"keepDesc": "Conserve la date de fin ({finishedDate}) et ne définit pas de date de début.",
"clearDesc": "Supprime la date de fin et définit aujourd'hui ({newStartDate}) comme date de début."
- }
+ },
+ "startNewRead": "Commencer une nouvelle lecture (conserve les précédentes)"
},
"acquisition": {
"owned": "Possédé",
@@ -703,7 +719,8 @@
"subtitle": "Une vue chronologique des livres que tu as terminés",
"viewInLibrary": "Voir tout dans la bibliothèque",
"noReadBooks": "Aucun livre terminé dans ta bibliothèque.",
- "goToLibrary": "Aller à la bibliothèque"
+ "goToLibrary": "Aller à la bibliothèque",
+ "reread": "Relu"
},
"data": {
"title": "Gestion des données",
@@ -719,7 +736,8 @@
"books": "Livres",
"progress": "Progression de lecture",
"tags": "Étiquettes",
- "covers": "Fichiers de couverture"
+ "covers": "Fichiers de couverture",
+ "sessions": "Sessions de lecture"
},
"button": "Exporter les données",
"exporting": "Exportation...",
diff --git a/frontend/src/lib/i18n/locales/zh.json b/frontend/src/lib/i18n/locales/zh.json
index 95793fd2..ad7d0c98 100644
--- a/frontend/src/lib/i18n/locales/zh.json
+++ b/frontend/src/lib/i18n/locales/zh.json
@@ -74,7 +74,9 @@
"worstRated": "评分最低",
"showMore": "显示更多",
"totalBooksAndAuthors": "书籍总数",
- "booksFromAuthors": "来自 {authors} 位不同的作者"
+ "booksFromAuthors": "来自 {authors} 位不同的作者",
+ "totalReads": "总阅读次数",
+ "rereadBooks": "重读书籍"
},
"dashboard": {
"title": "阅读仪表盘",
@@ -213,7 +215,18 @@
"progressPromptSkip": "跳过",
"authorsPlaceholder": "输入作者并按回车",
"authorsHint": "按回车添加作者。按退格键删除上一个。",
- "authorsLabel": "作者"
+ "authorsLabel": "作者",
+ "readAgain": "再次阅读",
+ "startAgain": "重新开始",
+ "readingHistory": "阅读历史",
+ "addPastReading": "添加过去的阅读",
+ "sessionStarted": "开始",
+ "sessionFinished": "结束",
+ "sessionReread": "重读",
+ "sessionDeleteConfirm": "删除此阅读会话及其进度记录?",
+ "sessionDelete": "删除会话",
+ "readCount": "阅读 {count} 次",
+ "datesManagedBySessions": "通过阅读会话管理"
},
"addModal": {
"manual": "手动添加",
@@ -330,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": "本书已完成",
@@ -345,7 +360,8 @@
"clearAndStart": "清除完成日期并从今天开始",
"keepDesc": "保留完成日期({finishedDate}),不设置开始日期。",
"clearDesc": "删除完成日期并将今天({newStartDate})设为开始日期。"
- }
+ },
+ "startNewRead": "开始新的阅读(保留之前的阅读)"
},
"acquisition": {
"owned": "已拥有",
@@ -703,7 +719,8 @@
"subtitle": "你已完成阅读的图书的时间线视图",
"viewInLibrary": "在书库中查看全部",
"noReadBooks": "你的书库中还没有已读完的图书。",
- "goToLibrary": "前往书库"
+ "goToLibrary": "前往书库",
+ "reread": "重读"
},
"data": {
"title": "数据管理",
@@ -719,7 +736,8 @@
"books": "图书",
"progress": "阅读进度",
"tags": "标签",
- "covers": "封面文件"
+ "covers": "封面文件",
+ "sessions": "阅读会话"
},
"button": "导出数据",
"exporting": "导出中...",
diff --git a/frontend/src/lib/publicProfile/sections.test.ts b/frontend/src/lib/publicProfile/sections.test.ts
index aff33cf6..52a7108f 100644
--- a/frontend/src/lib/publicProfile/sections.test.ts
+++ b/frontend/src/lib/publicProfile/sections.test.ts
@@ -44,7 +44,8 @@ const ALL_STATISTICS: PublicProfileStatisticsKey[] = [
'books_without_rating',
'average_rating',
'top_rated_books',
- 'worst_rated_books'
+ 'worst_rated_books',
+ 'total_reads'
];
function lookup(obj: unknown, path: string): unknown {
diff --git a/frontend/src/lib/publicProfile/sections.ts b/frontend/src/lib/publicProfile/sections.ts
index 57dee000..80573507 100644
--- a/frontend/src/lib/publicProfile/sections.ts
+++ b/frontend/src/lib/publicProfile/sections.ts
@@ -196,6 +196,12 @@ export const PUBLIC_PROFILE_STATISTICS: PublicProfileStatisticsDef[] = [
i18nKey: 'statistics.worstRated',
group: 'ratings',
defaultOn: false
+ },
+ {
+ key: 'total_reads',
+ i18nKey: 'statistics.totalReads',
+ group: 'summary',
+ defaultOn: false
}
];
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts
index 7cb25d39..5fd459f5 100644
--- a/frontend/src/lib/types.ts
+++ b/frontend/src/lib/types.ts
@@ -24,6 +24,8 @@ export interface Book {
date_added: string; // ISO datetime
date_started: string | null;
date_finished: string | null;
+ read_count?: number;
+ session_count?: number;
}
export interface BookListResponse {
@@ -212,6 +214,8 @@ export interface StatisticsResponse {
average_rating: number | null;
top_rated_books: TopRatedBook[];
worst_rated_books: TopRatedBook[];
+ total_reads?: number;
+ reread_books?: number;
}
export type StatisticsRange = 'alltime' | 'this_year' | 'last_year' | '3years' | 'custom';
@@ -348,6 +352,7 @@ export interface ReadingProgressEntry {
id: number;
book_id: number;
page: number;
+ session_id?: number | null;
created_at: string;
updated_at: string;
}
@@ -357,6 +362,52 @@ export interface BookProgress {
current_page: number;
}
+export interface ReadingSession {
+ id: number;
+ book_id: number;
+ status: ReadingStatus;
+ date_started: string | null;
+ date_finished: string | null;
+ is_current: boolean;
+ read_number: number | null;
+ is_reread: boolean;
+ progress_count: number;
+ last_page: number | null;
+ max_page: number | null;
+ created_at: string;
+ updated_at: string;
+}
+
+export interface ReadingSessionInput {
+ status: ReadingStatus;
+ date_started?: string | null;
+ date_finished?: string | null;
+}
+
+export interface SessionMutationResponse {
+ session: ReadingSession;
+ book: Book;
+}
+
+export interface TimelineSessionEntry {
+ session_id: number;
+ book_id: number;
+ title: string;
+ author: string | null;
+ authors: string[];
+ cover_url: string | null;
+ rating: number | null;
+ page_count: number | null;
+ date_finished: string | null;
+ read_number: number | null;
+ is_reread: boolean;
+}
+
+export interface TimelineSessionListResponse {
+ entries: TimelineSessionEntry[];
+ total: number;
+}
+
/** Search sources the backend can report progress for. */
export type ImportSource =
| 'open_library'
@@ -378,7 +429,7 @@ export type SearchStage =
| { stage: 'complete'; results: BookImportCandidate[] }
| { stage: 'error'; message: string };
-export type DataExportDataset = 'books' | 'progress' | 'tags' | 'covers';
+export type DataExportDataset = 'books' | 'progress' | 'tags' | 'covers' | 'sessions';
export type DataExportFormat = 'csv' | 'json';
export interface DataImportParseResponse {
@@ -511,7 +562,8 @@ export type PublicProfileStatisticsKey =
| 'books_without_rating'
| 'average_rating'
| 'top_rated_books'
- | 'worst_rated_books';
+ | 'worst_rated_books'
+ | 'total_reads';
export interface PublicProfileVisibilityConfig {
sections: PublicProfileSectionKey[];
diff --git a/frontend/src/routes/library/+page.svelte b/frontend/src/routes/library/+page.svelte
index d49b6809..e63859fb 100644
--- a/frontend/src/routes/library/+page.svelte
+++ b/frontend/src/routes/library/+page.svelte
@@ -513,6 +513,7 @@
onEdit={openEditFromDetail}
onDelete={handleDelete}
onProgress={handleProgressChange}
+ onSave={handleSave}
/>
{formatNumber(stats.total_books, 0)}
{$_('statistics.booksFromAuthors', { values: { authors: stats.total_authors } })}