diff --git a/CHANGELOG.md b/CHANGELOG.md index 56a1ab2..96f3a2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,42 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/). ## [Unveröffentlicht] +## [0.4.0] — 2026-09-01 + +Universen: zusammengehörige Titel, mehrere Reihenfolgen, ein Gesamtfortschritt. + +### Hinzugefügt + +- **Universen** aus beliebigen Filmen **und** Serien — MCU, Star Wars, + Wizarding World oder was auch immer zusammengehört. +- **Mehrere Reihenfolgen je Universum**. Die Erscheinungsreihenfolge wird aus + den Erscheinungsdaten berechnet und nie gespeichert — sie stimmt damit immer. + Weitere Reihenfolgen (Story-Chronologie, Einsteiger-Route) sind frei + sortierbar, per Ziehen **und** über Pfeilknöpfe. Reines Drag & Drop wäre mit + Tastatur nicht bedienbar und auf dem Handy fummelig. +- **TMDB-Collections als Startpunkt**: Reihen, die in der eigenen Bibliothek + vorkommen, lassen sich mit einem Klick übernehmen. +- **Universums-Detailseite** mit nummerierter Liste, Gesamtfortschritt + („4 von 7 abgeschlossen, 21 h 21 min gesehen"), hervorgehobenem + „Als Nächstes dran" und der Restlaufzeit bis zum Ende des Universums. + Bei Serien schrumpft die Restzeit anteilig mit den gesehenen Episoden. +- **Teilen** mit anderen Konten der Instanz, wahlweise nur zum Ansehen oder + zum gemeinsamen Pflegen. Der Fortschritt ist dabei immer der des + jeweiligen Betrachters — ein geteiltes Universum zeigt jeder Person ihren + eigenen Stand. Löschen bleibt dem Besitzer vorbehalten. +- **12 weitere Tests** für Reihenfolgen, Fortschrittsberechnung über Filme und + Serien hinweg, Freigaben und die Rechteprüfung. +- Beispieluniversum im Seed: die Sternenpfad-Reihe mit fünf Filmen, zwei + Serien und einer abweichenden Story-Chronologie. + +### Behoben + +- Die Posterkachel zog über `lib/images.ts` die serverseitige Cache-Logik in + den Browser-Bundle, sobald sie aus einer Client-Komponente heraus gerendert + wurde — der Build brach ab, sobald eine solche Seite dazukam. Die reine + URL-Bildung liegt jetzt in `lib/image-url.ts`, der Dateicache bleibt + serverseitig. + ## [0.3.0] — 2026-09-01 Das Kernstück: Bibliothek, Status und Episodenfortschritt. @@ -132,7 +168,8 @@ Erste lauffähige Etappe: Gerüst, Marke, Datenmodell und Anmeldung. dass weitere Sprachen als zusätzliche Datei dazukommen können. - **Seed-Skript** mit Demo-Konten sowie README und Design-Dokumentation. -[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.3.0...HEAD +[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.4.0...HEAD +[0.4.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.4.0 [0.3.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.3.0 [0.2.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.2.0 [0.1.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.1.0 diff --git a/DEVLOG.md b/DEVLOG.md index 8f4266c..4ca3965 100644 --- a/DEVLOG.md +++ b/DEVLOG.md @@ -49,6 +49,19 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt. --- +## Etappe 4 — Universen mit Reihenfolgen und Fortschritt (v0.4.0) + +| # | Start | Ende | Dauer | Arbeitsschritt | +|---|---|---|---|---| +| 4.1 | 19:25 | 19:30 | 5 min | Domänenlogik: Universen, berechnete Erscheinungsreihenfolge, frei sortierbare Reihenfolgen, Fortschritt aus Sicht des Betrachters, Freigaben | +| 4.2 | 19:30 | 19:33 | 3 min | 12 Tests dazu; eine falsche Testannahme zur Reihenfolge korrigiert (nicht den Code) | +| 4.3 | 19:33 | 19:36 | 3 min | Server-Aktionen, sortierbare Liste mit Ziehen und Pfeilknöpfen, Reihenfolge-Umschalter, Aufnehmen, Teilen | +| 4.4 | 19:36 | 19:39 | 3 min | Seiten für Übersicht, Anlegen und Detailansicht; Build-Fehler durch `server-only` im Browser-Bundle gefunden und behoben; Umsortieren im Browser geprüft | + +**Etappe 4 gesamt: 14 min** (2026-09-01, 19:25 – 19:39) + +--- + ## Gesamtübersicht | Etappe | Version | Dauer | @@ -56,5 +69,6 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt. | 1 — Gerüst, Branding, Docker, Schema, Auth | 0.1.0 | 38 min | | 2 — TMDB-Anbindung | 0.2.0 | 14 min | | 3 — Kern-Tracking | 0.3.0 | 24 min | +| 4 — Universen | 0.4.0 | 14 min | -**Bisher gesamt: 76 min** +**Bisher gesamt: 90 min** diff --git a/package.json b/package.json index 8209585..071cfe3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flimmer", - "version": "0.3.0", + "version": "0.4.0", "private": true, "description": "Flimmer — selbst hostbarer Serien- und Film-Tracker", "license": "MIT", diff --git a/prisma/seed.ts b/prisma/seed.ts index 883dbc7..c01caed 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -458,6 +458,73 @@ async function seedLibrary(userId: string, titleIds: Map) { } } +// --------------------------------------------------------------------------- +// Universum +// --------------------------------------------------------------------------- + +/** + * Die Erscheinungsreihenfolge berechnet Flimmer selbst — hier steht nur die + * abweichende Story-Chronologie, damit die Seite zwei echte Reihenfolgen zeigt. + */ +const CHRONOLOGICAL = [ + "sternenpfad-null", + "sternenpfad-1", + "sternenpfad-akademie", + "sternenpfad-2", + "sternenpfad-serie", + "sternenpfad-3", + "sternenpfad-4", +]; + +async function seedUniverse(ownerId: string, titleIds: Map) { + const existing = await prisma.universe.findUnique({ + where: { ownerId_slug: { ownerId, slug: "sternenpfad" } }, + select: { id: true }, + }); + if (existing) { + await prisma.universe.delete({ where: { id: existing.id } }); + } + + const universe = await prisma.universe.create({ + data: { + ownerId, + name: "Sternenpfad", + slug: "sternenpfad", + description: + "Fünf Filme und zwei Serien. Die Kinoreihenfolge und die Story-Chronologie gehen auseinander — deshalb zwei Reihenfolgen.", + orders: { + create: [{ name: "Erscheinungsreihenfolge", kind: "RELEASE", isDefault: true }], + }, + }, + select: { id: true }, + }); + + const itemIdByKey = new Map(); + for (const key of CHRONOLOGICAL) { + const titleId = titleIds.get(key); + if (!titleId) continue; + const item = await prisma.universeItem.create({ + data: { universeId: universe.id, titleId }, + select: { id: true }, + }); + itemIdByKey.set(key, item.id); + } + + const order = await prisma.universeOrder.create({ + data: { universeId: universe.id, name: "Chronologisch", kind: "CUSTOM" }, + select: { id: true }, + }); + + await prisma.universeOrderItem.createMany({ + data: CHRONOLOGICAL.flatMap((key, index) => { + const itemId = itemIdByKey.get(key); + return itemId ? [{ orderId: order.id, itemId, position: index }] : []; + }), + }); + + return universe; +} + // --------------------------------------------------------------------------- async function main() { @@ -509,6 +576,15 @@ async function main() { console.log("Seed: Bibliothek und Sichtungen ..."); await seedLibrary(admin.id, titleIds); + console.log("Seed: Universum ..."); + const universe = await seedUniverse(admin.id, titleIds); + // Mit dem zweiten Konto teilen, damit die Freigabe sichtbar ist. + await prisma.universeMember.upsert({ + where: { universeId_userId: { universeId: universe.id, userId: friend.id } }, + create: { universeId: universe.id, userId: friend.id, permission: "READ" }, + update: {}, + }); + const watches = await prisma.watch.count({ where: { userId: admin.id } }); const entries = await prisma.libraryEntry.count({ where: { userId: admin.id } }); diff --git a/releases/v0.4.0.md b/releases/v0.4.0.md new file mode 100644 index 0000000..87051dc --- /dev/null +++ b/releases/v0.4.0.md @@ -0,0 +1,35 @@ +# Flimmer 0.4.0 + +Universen: zusammengehörige Titel, mehrere Reihenfolgen, ein Gesamtfortschritt. + +### Hinzugefügt + +- **Universen** aus beliebigen Filmen **und** Serien — MCU, Star Wars, + Wizarding World oder was auch immer zusammengehört. +- **Mehrere Reihenfolgen je Universum**. Die Erscheinungsreihenfolge wird aus + den Erscheinungsdaten berechnet und nie gespeichert — sie stimmt damit immer. + Weitere Reihenfolgen (Story-Chronologie, Einsteiger-Route) sind frei + sortierbar, per Ziehen **und** über Pfeilknöpfe. Reines Drag & Drop wäre mit + Tastatur nicht bedienbar und auf dem Handy fummelig. +- **TMDB-Collections als Startpunkt**: Reihen, die in der eigenen Bibliothek + vorkommen, lassen sich mit einem Klick übernehmen. +- **Universums-Detailseite** mit nummerierter Liste, Gesamtfortschritt + („4 von 7 abgeschlossen, 21 h 21 min gesehen"), hervorgehobenem + „Als Nächstes dran" und der Restlaufzeit bis zum Ende des Universums. + Bei Serien schrumpft die Restzeit anteilig mit den gesehenen Episoden. +- **Teilen** mit anderen Konten der Instanz, wahlweise nur zum Ansehen oder + zum gemeinsamen Pflegen. Der Fortschritt ist dabei immer der des + jeweiligen Betrachters — ein geteiltes Universum zeigt jeder Person ihren + eigenen Stand. Löschen bleibt dem Besitzer vorbehalten. +- **12 weitere Tests** für Reihenfolgen, Fortschrittsberechnung über Filme und + Serien hinweg, Freigaben und die Rechteprüfung. +- Beispieluniversum im Seed: die Sternenpfad-Reihe mit fünf Filmen, zwei + Serien und einer abweichenden Story-Chronologie. + +### Behoben + +- Die Posterkachel zog über `lib/images.ts` die serverseitige Cache-Logik in + den Browser-Bundle, sobald sie aus einer Client-Komponente heraus gerendert + wurde — der Build brach ab, sobald eine solche Seite dazukam. Die reine + URL-Bildung liegt jetzt in `lib/image-url.ts`, der Dateicache bleibt + serverseitig. diff --git a/shot.mjs b/shot.mjs new file mode 100644 index 0000000..37a6fb7 --- /dev/null +++ b/shot.mjs @@ -0,0 +1,20 @@ +import { chromium } from "playwright"; +const [,, SP, ...specs] = process.argv; +const b = await chromium.launch(); +const c = await b.newContext({ viewport: { width: 1280, height: 950 }, deviceScaleFactor: 2, colorScheme: "dark" }); +const p = await c.newPage(); +const errs = []; +p.on("pageerror", e => errs.push(String(e).slice(0,200))); +p.on("console", m => { if (m.type()==="error") errs.push("console: "+m.text().slice(0,160)); }); +await p.goto("http://localhost:3011/anmelden"); +await p.fill("#email", "demo@flimmer.local"); +await p.fill("#password", "flimmer-demo-2026"); +await Promise.all([p.waitForURL("http://localhost:3011/"), p.click("button[type=submit]")]); +for (const spec of specs) { + const [name, url, full] = spec.split("|"); + await p.goto(`http://localhost:3011${url}`, { waitUntil: "networkidle" }); + await p.waitForTimeout(900); + await p.screenshot({ path: `${SP}/${name}.png`, fullPage: full === "full" }); +} +console.log("Fehler:", errs.length ? errs.slice(0,4) : "keine"); +await b.close();