diff --git a/apps/mana/apps/web/src/lib/modules/library/components/ProgressControls.svelte b/apps/mana/apps/web/src/lib/modules/library/components/ProgressControls.svelte
new file mode 100644
index 000000000..7b1cca4eb
--- /dev/null
+++ b/apps/mana/apps/web/src/lib/modules/library/components/ProgressControls.svelte
@@ -0,0 +1,354 @@
+
+
+
+{#if entry.details.kind === 'book'}
+
+{:else if entry.details.kind === 'series'}
+
+
+
Episoden-Tracker
+ {seriesTotals.watched} / {seriesTotals.total || '?'}
+
+ {#if seasonList.length === 0}
+
+ Trage Staffeln + Episoden in den Details ein, um die einzelnen Folgen abzuhaken.
+
+ {:else}
+
+ {#each seasonList as s (s.season)}
+ {@const watched = entry.details.kind === 'series' ? (entry.details.watched ?? []) : []}
+ {@const seasonWatched = countWatchedInSeason(watched, s.season)}
+
{
+ const target = e.currentTarget as HTMLDetailsElement;
+ if (target.open) openSeason = s.season;
+ else if (openSeason === s.season) openSeason = null;
+ }}
+ >
+
+ Staffel {s.season}
+ {seasonWatched} / {s.episodeCount}
+
+
+ {#each Array.from({ length: s.episodeCount }, (_, i) => i + 1) as ep (ep)}
+
+ {/each}
+
+
+ {/each}
+
+ {/if}
+
+{:else if entry.details.kind === 'comic'}
+
+
+
Ausgaben-Fortschritt
+
+ {entry.details.currentIssue ?? 0}
+ {#if entry.details.issueCount}/ {entry.details.issueCount}{/if}
+
+
+
+
+ #{entry.details.currentIssue ?? 0}
+
+
+
+{/if}
+
+
diff --git a/apps/mana/apps/web/src/lib/modules/library/stores/entries.svelte.ts b/apps/mana/apps/web/src/lib/modules/library/stores/entries.svelte.ts
index 0463cb415..d680458fb 100644
--- a/apps/mana/apps/web/src/lib/modules/library/stores/entries.svelte.ts
+++ b/apps/mana/apps/web/src/lib/modules/library/stores/entries.svelte.ts
@@ -145,6 +145,26 @@ export const libraryEntriesStore = {
}
},
+ /**
+ * "Nochmal" — re-start a completed entry. Leaves `times` alone (it was
+ * already incremented when the entry was marked complete); resets
+ * `startedAt` to today, clears `completedAt`, flips status back to
+ * 'active'. For series, the per-episode watched list is preserved so
+ * the user has a record of the previous run-through (and can reset
+ * individual episodes via the tracker if they want).
+ */
+ async restartEntry(id: string) {
+ const existing = await libraryEntryTable.get(id);
+ if (!existing) return;
+ const nowDate = new Date().toISOString().slice(0, 10);
+ await libraryEntryTable.update(id, {
+ status: 'active',
+ startedAt: nowDate,
+ completedAt: null,
+ updatedAt: new Date().toISOString(),
+ });
+ },
+
async rate(id: string, rating: number | null) {
await libraryEntryTable.update(id, {
rating,
diff --git a/apps/mana/apps/web/src/lib/modules/library/views/DetailView.svelte b/apps/mana/apps/web/src/lib/modules/library/views/DetailView.svelte
index 266083201..dfa11ec0d 100644
--- a/apps/mana/apps/web/src/lib/modules/library/views/DetailView.svelte
+++ b/apps/mana/apps/web/src/lib/modules/library/views/DetailView.svelte
@@ -1,9 +1,9 @@
@@ -91,10 +105,20 @@
{/each}
- {#if entry.times > 0}
-
- {entry.kind === 'book' || entry.kind === 'comic' ? 'Gelesen' : 'Gesehen'}: {entry.times}×
-
+ {#if entry.times > 0 || entry.status === 'completed'}
+
+ {#if entry.times > 0}
+
+ {entry.kind === 'book' || entry.kind === 'comic' ? 'Gelesen' : 'Gesehen'}:
+ {entry.times}×
+
+ {/if}
+ {#if entry.status === 'completed'}
+
+ {/if}
+
{/if}
{#if entry.genres.length > 0 || entry.tags.length > 0}
@@ -168,6 +192,8 @@
{/if}
+
+
{#if entry.review}
Review
@@ -296,10 +322,29 @@
color: #a855f7;
border-color: #a855f7;
}
+ .times-row {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ margin-top: 0.2rem;
+ }
.times {
font-size: 0.85rem;
color: var(--color-text-muted, #64748b);
}
+ .restart {
+ padding: 0.3rem 0.75rem;
+ border-radius: 0.4rem;
+ border: 1px solid #a855f7;
+ background: transparent;
+ color: #a855f7;
+ cursor: pointer;
+ font: inherit;
+ font-size: 0.8rem;
+ }
+ .restart:hover {
+ background: color-mix(in srgb, #a855f7 10%, transparent);
+ }
.tag-row {
display: flex;
gap: 0.3rem;
diff --git a/docs/plans/library-module.md b/docs/plans/library-module.md
index 22bd684b1..f7597e296 100644
--- a/docs/plans/library-module.md
+++ b/docs/plans/library-module.md
@@ -2,10 +2,13 @@
## Status (2026-04-17)
-**M1 Skelett: DONE.** Modul registriert, Dexie v26 angelegt, Encryption-Registry
-eingetragen, Route `/library` mountet einen minimalen Listen-View. Guest-Seed mit
-je einem Eintrag pro `kind` (Dune, Arrival, Severance, Saga) vorhanden.
-Nächster Schritt: M2 (volles CRUD + Form + Grid + DetailView).
+**M1 Skelett: DONE** (commit 8c6502d0f) — Modul registriert, Dexie v26, Encryption-Registry, Route mountet, Guest-Seed (Dune, Arrival, Severance, Saga).
+
+**M2 CRUD / Grid / Detail: DONE** (commit 364178496) — KindTabs, StatusFilter, RatingStars, EntryCard, EntryForm (Create + Edit, typ-spezifische Details-Accordion), GridView, DetailView, `/library/entry/[id]`-Route.
+
+**M3 Fortschritt: DONE** — `ProgressControls.svelte`: Seiten-Slider für Bücher (auto-completes bei 100%), Episode-Tracker für Serien (abhakbare Folgen pro Staffel), Issue-Bumper für Comics. `libraryEntriesStore.restartEntry` + "Nochmal"-Button im DetailView für abgeschlossene Einträge.
+
+Nächster Schritt: M4 (Cover-Upload via `uload`) oder M6 (AI-Tools).
Vor M2 entschieden:
- Audiobooks: `kind='book'` mit `details.format='audio'` (nicht eigener `kind`).
@@ -212,8 +215,8 @@ Missionen wie *"Trage meine letzten 5 gesehenen Filme ein"* können dann über d
## Reihenfolge (Milestones)
1. **M1 — Skelett [DONE 2026-04-17]**: types, collections, module.config, Registry-Eintrag, Dexie-Migration (v26), leere Route. *Ziel: App zeigt leeres Modul an, nichts crasht.*
-2. **M2 — CRUD**: entries-Store, EntryForm, GridView, DetailView. Manuelles Anlegen/Editieren funktioniert für alle 4 `kind`s. Cover via URL.
-3. **M3 — Fortschritt**: Status-Wechsel, Rating, times-Counter, Episode-Tracker für Serien, Seiten-Slider für Bücher. Guest-Seed komplett.
+2. **M2 — CRUD [DONE 2026-04-17]**: entries-Store, EntryForm, GridView, DetailView. Manuelles Anlegen/Editieren funktioniert für alle 4 `kind`s. Cover via URL.
+3. **M3 — Fortschritt [DONE 2026-04-17]**: Status-Wechsel, Rating, times-Counter, Episode-Tracker für Serien, Seiten-Slider für Bücher, Issue-Bumper für Comics, Nochmal-Button.
4. **M4 — Cover-Upload**: Integration mit `uload`. Encryption-Registry final.
5. **M5 — Stats + Dashboard-Widget**: useStats, kleiner Widget für Dashboard.
6. **M6 — AI-Tools**: list/create/complete/rate Tools, Shared-AI-Policy.