mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23eeb9912c |
@@ -1,182 +0,0 @@
|
||||
# Twitch4SteamDeck — Fortschritts-Snapshot
|
||||
|
||||
> Stand: **2026-04-09 (Session 5)**. Diese Datei dient als Übergabe zwischen Sessions.
|
||||
> **Lies zuerst:** diese Datei + `todo.md` + `plan.md`.
|
||||
|
||||
**Aktueller Stand:** Phase 0–5.6 abgeschlossen & verifiziert. Phase 6 (Flatpak-Packaging): Manifest + Hilfs-Dateien geschrieben, wartet auf User-Verifikation in WSL2 + Steam Deck.
|
||||
|
||||
---
|
||||
|
||||
## Bereits abgeschlossen
|
||||
|
||||
### ✅ Phase 0 — Projekt-Skelett
|
||||
### ✅ Phase 1 — Twitch OAuth Device Code Flow
|
||||
### ✅ Phase 2 — Twitch Helix-Client + Sidebar-Layout
|
||||
Alle drei End-to-End verifiziert 2026-04-07.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 3 — Streamlink + mpv (Live-Wiedergabe)
|
||||
|
||||
Verifiziert 2026-04-08.
|
||||
|
||||
#### Architektur-Entscheidungen:
|
||||
- `--twitch-api-header` wird **nicht** übergeben — Twitch lehnt den Device-Code-Token für
|
||||
streamlinks interne GQL-API ab. Öffentliche Streams funktionieren ohne Auth-Header.
|
||||
- `resolveMpvBin()` löst den mpv-Pfad explizit auf (`C:\Program Files\MPV Player\mpv.exe`),
|
||||
da Electron den User-PATH nicht vollständig erbt.
|
||||
- `--twitch-disable-ads` ist von streamlink deaktiviert worden (deprecated) — kein Ad-Bypass
|
||||
im Moment. Post-MVP-Problem.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 4 — VOD-Browsing + -Wiedergabe
|
||||
|
||||
Verifiziert 2026-04-08.
|
||||
|
||||
#### Architektur-Entscheidungen:
|
||||
- **VODs:** streamlink `--stream-url` liefert HLS-URL → mpv wird **direkt** mit der URL
|
||||
gespawnt (nicht via `--player`-Flag). Grund: Piped-Streams sind nicht seekable.
|
||||
Direktes mpv-Spawnen ermöglicht Seeking + funktionierendes IPC.
|
||||
- **Live-Streams:** weiterhin via `--player mpv --player-args "--fullscreen"`. Kein IPC
|
||||
nötig (kein Seeking, kein Position-Tracking bei Live).
|
||||
- VOD-Shelf im `ChannelScreen` (untere Hälfte, horizontal, oberer Hero-Bereich fix).
|
||||
Navigation: ↓ → Shelf, ←/→ zwischen VODs, ↑ → zurück, Enter → abspielen.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 5 — Lokaler VOD-Verlauf (Resume)
|
||||
|
||||
Verifiziert 2026-04-08.
|
||||
|
||||
#### Neue Dateien:
|
||||
- `src/main/store/db.ts` — SQLite via better-sqlite3, WAL-Modus, Migration
|
||||
- `src/main/store/historyRepo.ts` — `upsertVod`, `updatePosition`, `markCompleted`,
|
||||
`getPosition`, `getProgressMap`
|
||||
|
||||
#### Architektur-Entscheidungen:
|
||||
- mpv IPC funktioniert jetzt zuverlässig (da mpv direkt gespawnt wird).
|
||||
`MpvController.observeTimePos()` parst eingehende JSON-Lines, Throttling auf 5s.
|
||||
- Resume-Position wird beim `startVod()`-Aufruf automatisch aus DB gelesen.
|
||||
- Auto-Complete bei >95% Fortschritt.
|
||||
- `„Continue Watching"-Reihe` wurde bewusst weggelassen (vom User nicht gewünscht).
|
||||
- VOD-Karte zeigt: Fortschrittsbalken (lila), `0:16 von 6:22`, `Vor X Min.`,
|
||||
Completed-Overlay mit Kreis-Checkmark.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 5.5 — Browse-Menü: Kategorien + Top Live-Streams
|
||||
|
||||
Verifiziert 2026-04-08.
|
||||
|
||||
#### Neue/geänderte Dateien:
|
||||
- `src/main/twitch/types.ts` — `HelixGame`, `GameInfo`, `game_id`+`language` in `HelixStream`
|
||||
- `src/main/twitch/helixClient.ts` — `getTopGames(limit, cursor?)`, `getTopStreams(opts?)`
|
||||
- `src/main/ipc/handlers.ts` — `twitch:get-top-games`, `twitch:get-top-streams`
|
||||
- `src/preload/index.ts` — `GameInfo`-Typ + neue API-Methoden
|
||||
- `src/renderer/src/types/t4sd.d.ts` — `GameInfo`, T4sdApi erweitert
|
||||
- `src/renderer/src/input/gamepad.ts` — Button 2 (X) → `'x'`, Button 3 (Y) → `'y'`
|
||||
- `src/renderer/src/screens/BrowseScreen.tsx` — Shelf (Top Live) + Kategorien-Grid
|
||||
- `src/renderer/src/screens/CategoryScreen.tsx` — neu: Streams-Grid pro Kategorie
|
||||
- `src/renderer/src/screens/AppShell.tsx` — `selectedCategory`-Routing
|
||||
|
||||
#### Key-Mapping Browse + Category:
|
||||
- **Enter / A**: Stream → direkt Live starten · Kategorie → Drilldown
|
||||
- **X**: Stream-Karte → ChannelScreen öffnen (VOD-Shelf + Live-Button)
|
||||
- **Y**: Refresh
|
||||
- **Escape / B**: Category → zurück zu Browse · Browse → Sidebar
|
||||
|
||||
#### Wichtige Implementierungsdetails:
|
||||
- `getTopGames()` macht 40 parallele `/streams?game_id=<id>&first=100` Calls für Zuschauerzahlen
|
||||
(Top-100-Streams pro Kategorie summiert → ~85-95% Genauigkeit vs. Twitch-Website).
|
||||
- Pagination: `getTopGames()` gibt `{ games, cursor }` zurück. BrowseScreen lädt automatisch
|
||||
nächste 40 Kategorien wenn letztes Grid-Element fokussiert wird.
|
||||
- `getTopStreams()` global mit `limit: 100` für den Shelf.
|
||||
- Zuschauerzahlen auf Kategorien zeigen `n.toLocaleString('de-DE')` Format.
|
||||
|
||||
---
|
||||
|
||||
### ✅ Phase 5.6 — Einstellungen-Menü + Sprach-Badge/Flagge
|
||||
|
||||
Verifiziert 2026-04-08.
|
||||
|
||||
#### Neue Dateien:
|
||||
- `src/renderer/src/context/SettingsContext.tsx` — `SettingsProvider`, `useSettings()` Hook,
|
||||
localStorage-Persistenz (`t4sd:settings`), Fallback auf Default bei korrupten Werten
|
||||
- `src/renderer/src/components/LanguageBadge.tsx` — rendert Badge/Flagge/Beides je nach Mode;
|
||||
38 Sprachen gemappt (pragmatische Twitch-Dominanz-Zuordnung)
|
||||
- `src/renderer/src/screens/SettingsScreen.tsx` — 4 Optionen, gamepad-navigierbar
|
||||
|
||||
#### Geänderte Dateien:
|
||||
- `src/main/twitch/types.ts` — `language` in `HelixStream` + `FollowedChannelInfo`
|
||||
- `src/main/twitch/helixClient.ts` — `language` in beiden Stream-Mappern
|
||||
- `src/preload/index.ts` + `t4sd.d.ts` — `language?: string` in `FollowedChannelInfo`
|
||||
- `src/renderer/src/components/Icons.tsx` — `SettingsIcon` (Zahnrad)
|
||||
- `src/renderer/src/components/Sidebar.tsx` — 4. Tab `'settings'`
|
||||
- `src/renderer/src/components/FocusableCard.tsx` — `<LanguageBadge>` unten-rechts im Thumb
|
||||
- `src/renderer/src/screens/BrowseScreen.tsx` — `<LanguageBadge>` in Shelf-Cards
|
||||
- `src/renderer/src/screens/AppShell.tsx` — `tab === 'settings'` → SettingsScreen
|
||||
- `src/renderer/src/main.tsx` — `<SettingsProvider>` um App
|
||||
|
||||
#### Architektur-Entscheidungen:
|
||||
- Settings in `localStorage` (nicht SQLite) — reine UI-Präferenz, kein IPC nötig,
|
||||
synchron lesbar (kein Flicker beim Start).
|
||||
- Default-Mode: `'language'` (Kürzel wie DE/EN) — funktioniert auf Windows Dev-Host sicher.
|
||||
Flaggen-Emojis (Unicode Regional Indicators) werden erst auf Steam Deck (Linux/Noto Emoji)
|
||||
korrekt gerendert — auf Windows Dev-Host ggf. Rechtecke statt Flaggen.
|
||||
- `StreamBadgeMode`: `'off' | 'language' | 'flag' | 'both'`
|
||||
- LanguageBadge Position: `bottom: 10px; right: 10px` im Thumbnail (gegenüber Viewer-Count
|
||||
unten-links). Font-Size 15px (FocusableCard) / 14px (Browse-Shelf) > 13px Viewer-Count.
|
||||
- Unbekannte Sprachen / `'other'` → kein Badge.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6 — Flatpak-Packaging (in Arbeit, wartet auf WSL2 + Steam Deck)
|
||||
|
||||
Ziel: App läuft auf dem Steam Deck als Flatpak.
|
||||
|
||||
#### Neue Dateien (Phase 6):
|
||||
- `flatpak/tv.twitch4steamdeck.App.yml` — Flatpak-Manifest (mpv + streamlink als Module)
|
||||
- `flatpak/twitch4steamdeck.sh` — Launcher mit `zypak-wrapper` (Electron-Sandbox)
|
||||
- `flatpak/tv.twitch4steamdeck.App.desktop` — Desktop-Eintrag
|
||||
- `flatpak/build-flatpak.sh` — Build-Skript für WSL2 (prüft deps, füllt SHA256, baut)
|
||||
- `resources/icons/icon.svg` — App-Icon (Platzhalter, konvertiert zu PNG beim Build)
|
||||
|
||||
#### Architektur-Entscheidungen:
|
||||
- **Pre-Build in WSL2:** `npm install` + `@electron/rebuild` + `npm run build` laufen
|
||||
AUSSERHALB von flatpak-builder in WSL2. Das Manifest kopiert nur die Artefakte.
|
||||
→ Vermeidet npm-im-Flatpak-Sandbox-Probleme.
|
||||
- **Electron-Binary aus npm:** Das Electron-Binary kommt aus `node_modules/electron/dist/electron`
|
||||
(npm install für Linux), NICHT aus dem BaseApp. BaseApp liefert nur Chrome-Runtime-Libs + zypak.
|
||||
- **zypak-wrapper:** Ersetzt Chrome-Setuid-Sandbox für Flatpak-Kompatibilität.
|
||||
- **mpv:** Aus Sources gebaut, libplacebo deaktiviert (entfernt Vulkan-Abhängigkeitskette),
|
||||
ffmpeg/libass/libva kommen aus freedesktop SDK 24.08.
|
||||
- **streamlink:** Via pip install ins Flatpak gebaut. Für sauberen Offline-Build:
|
||||
`flatpak-pip-generator` für Python-Dependencies verwenden (TODO beim echten Build).
|
||||
- **better-sqlite3:** Muss für Linux x64 rebuilt werden (Steam Deck = AMD Zen2 x86_64,
|
||||
NICHT ARM64 — die alte ARM64-Notiz war falsch).
|
||||
- **IPC-Socket:** `os.tmpdir()` → `/tmp` — beide Prozesse (Electron-Main + mpv-Child)
|
||||
teilen denselben Flatpak-Sandbox-Namespace. Kein Code-Fix nötig.
|
||||
- **VAAPI:** `--hwdec=vaapi` bereits implementiert für non-win32 in `streamlink.ts:73`.
|
||||
- **Flaggen-Emojis erst hier testbar** (Noto Color Emoji aus freedesktop Platform Runtime)
|
||||
|
||||
#### Noch ausstehend (User):
|
||||
1. WSL2-Setup: `sudo apt install flatpak flatpak-builder librsvg2-bin`
|
||||
2. `bash flatpak/build-flatpak.sh` (in WSL2, aus `/mnt/c/Projekte/twitch4steamdeck`)
|
||||
3. Verifikation auf Steam Deck: Login, Live, VOD, Resume, Gamepad, Gaming Mode
|
||||
|
||||
---
|
||||
|
||||
## Technische Baseline
|
||||
|
||||
- Electron 33, electron-vite, TypeScript 5.5, React 18
|
||||
- better-sqlite3 12.x (native, rebuilt für Electron via `@electron/rebuild`)
|
||||
- streamlink 8.x unter `%LOCALAPPDATA%\Programs\Streamlink\bin\streamlink.exe`
|
||||
- mpv v0.41.0 unter `C:\Program Files\MPV Player\mpv.exe`
|
||||
- Dev-Host: **Windows**. Target: **Steam Deck (Linux / Flatpak)**
|
||||
|
||||
### Wichtiges Projekt-Memo
|
||||
- Bei **allen Architekturänderungen** `plan.md` updaten.
|
||||
- Nach **jeder Subaufgabe** Checkbox in `todo.md` setzen und Update hier.
|
||||
- Dev-Host: **Windows**. Target: **Steam Deck (Linux / Flatpak)**.
|
||||
- **Nicht** mit Speculative Abstractions aufblähen.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twitch4steamdeck",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"description": "Twitch-Client für das Steam Deck (Live & VOD, Big-Screen-UI)",
|
||||
"main": "out/main/index.js",
|
||||
"author": "twitch4steamdeck",
|
||||
|
||||
@@ -1,289 +0,0 @@
|
||||
# Twitch4SteamDeck — Architekturplan
|
||||
|
||||
> Spiegel von `C:\Users\andre\.claude\plans\smooth-shimmying-parasol.md`.
|
||||
> Fortschritt steht in `todo.md`, aktueller Stand in `CLAUDE_PROGRESS.md`.
|
||||
|
||||
## Ziel
|
||||
|
||||
Steam-Deck-App zur **werbefreien** Wiedergabe von Twitch-Livestreams und -VODs.
|
||||
Big-Screen-/10-Foot-UI, vollständig per Gamepad bedienbar. Lokaler VOD-Verlauf
|
||||
mit Resume. Verteilung als Flatpak.
|
||||
|
||||
## Tech-Stack (entschieden)
|
||||
|
||||
| Bereich | Wahl |
|
||||
|---|---|
|
||||
| Sprache/Runtime | TypeScript + Electron (Main + Renderer) |
|
||||
| UI | React + CSS, Spatial-Navigation für Gamepad |
|
||||
| Wiedergabe | externer **mpv**-Prozess via JSON-IPC-Socket |
|
||||
| Stream-Quelle / Ad-Bypass | **Streamlink** (Subprozess) + `--twitch-disable-ads`, optional TTV.LOL |
|
||||
| Twitch-Auth | **OAuth 2.0 Device Code Flow** |
|
||||
| Twitch-Daten | Helix-API (`api.twitch.tv/helix`) |
|
||||
| Token-Storage | Electron `safeStorage` → `userData/twitch-tokens.bin` |
|
||||
| Lokale Persistenz | SQLite via `better-sqlite3` (VOD-Verlauf, Settings) |
|
||||
| Packaging | `electron-builder` → Flatpak |
|
||||
|
||||
## MVP-Scope
|
||||
|
||||
1. Twitch-Login via Device Code Flow
|
||||
2. Followed Channels (Liste, Live-Indicator)
|
||||
3. Live-Stream-Wiedergabe werbefrei (Streamlink → mpv)
|
||||
4. VOD-Browsing pro Kanal
|
||||
5. VOD-Wiedergabe werbefrei
|
||||
6. Lokaler VOD-Verlauf + Resume
|
||||
7. Browse: Top Live-Streams + Kategorien (mit Zuschauerzahlen, Infinite Scroll)
|
||||
8. Einstellungen: Sprach-Badge / Flagge auf Thumbnails
|
||||
|
||||
**Bewusst nicht im MVP:** Chat/Emotes, Suche (Text-Input), PiP, Themes.
|
||||
|
||||
## Architektur
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────┐
|
||||
│ Electron Main Process (Node) │
|
||||
│ - AuthService (Device Code Flow, Refresh) │
|
||||
│ - TwitchApiClient (Helix) │
|
||||
│ - StreamlinkRunner (spawn streamlink) │
|
||||
│ - MpvController (spawn mpv, JSON-IPC) │
|
||||
│ - HistoryStore (better-sqlite3) │
|
||||
│ - SettingsStore │
|
||||
└──────────────┬──────────────────────────────────┘
|
||||
│ IPC (contextBridge / preload)
|
||||
┌──────────────┴──────────────────────────────────┐
|
||||
│ Electron Renderer (React + TS) │
|
||||
│ - Screens: Login, Home, Channel, Player │
|
||||
│ - SpatialNavigation (Gamepad-Fokus) │
|
||||
│ - GamepadInputService │
|
||||
└─────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Wiedergabe läuft **außerhalb** Electron in einem mpv-Vollbildfenster.
|
||||
Electron startet/stoppt mpv, sendet Befehle, lauscht auf `time-pos` für Verlaufsspeicherung.
|
||||
|
||||
## Architektur-Abweichungen (aktuell)
|
||||
|
||||
Zusätzlich zu den ursprünglichen Abweichungen:
|
||||
- **Browse-Screen** (`BrowseScreen.tsx`) ist kein Platzhalter mehr, sondern
|
||||
ein vollständiger Screen mit Shelf (Top Live) + Kategorien-Grid.
|
||||
- **CategoryScreen** (`CategoryScreen.tsx`) neu: Drilldown aus Browse in
|
||||
Streams einer Kategorie. Key-Mapping: Enter=Live, X=ChannelScreen, B=zurück.
|
||||
- **SettingsContext** (`context/SettingsContext.tsx`): globaler React-Context
|
||||
mit localStorage-Persistenz für UI-Präferenzen. Kein IPC nötig.
|
||||
- **LanguageBadge** (`components/LanguageBadge.tsx`): rendert Sprachkürzel/Flagge
|
||||
auf Thumbnails. Flaggen = Unicode-Emoji, auf Steam Deck (Noto Emoji) korrekt.
|
||||
- **Gamepad Button 2 (X) + 3 (Y)** jetzt gemappt: `'x'` / `'y'`.
|
||||
|
||||
## Verzeichnisstruktur (Ist-Stand + Soll)
|
||||
|
||||
```
|
||||
twitch4steamdeck/
|
||||
├── package.json ✅
|
||||
├── electron.vite.config.ts ✅
|
||||
├── electron-builder.yml ✅
|
||||
├── .env.example ✅
|
||||
├── flatpak/
|
||||
│ ├── tv.twitch4steamdeck.App.yml ✅ Phase 6
|
||||
│ ├── twitch4steamdeck.sh ✅ Phase 6
|
||||
│ ├── tv.twitch4steamdeck.App.desktop ✅ Phase 6
|
||||
│ └── build-flatpak.sh ✅ Phase 6
|
||||
├── resources/
|
||||
│ └── icons/
|
||||
│ └── icon.svg ✅ Phase 6 (Platzhalter)
|
||||
├── src/
|
||||
│ ├── main/
|
||||
│ │ ├── index.ts ✅ (Auth + Helix instanziiert)
|
||||
│ │ ├── auth/
|
||||
│ │ │ ├── deviceCodeFlow.ts ✅
|
||||
│ │ │ ├── tokenStore.ts ✅
|
||||
│ │ │ └── authService.ts ✅
|
||||
│ │ ├── twitch/
|
||||
│ │ │ ├── helixClient.ts ✅
|
||||
│ │ │ └── types.ts ✅
|
||||
│ │ ├── playback/
|
||||
│ │ │ ├── types.ts ✅ Phase 3
|
||||
│ │ │ ├── streamlink.ts ✅ Phase 3
|
||||
│ │ │ ├── mpvController.ts ✅ Phase 3
|
||||
│ │ │ └── playbackService.ts ✅ Phase 3
|
||||
│ │ ├── store/
|
||||
│ │ │ ├── db.ts ⏳ Phase 5
|
||||
│ │ │ ├── historyRepo.ts ⏳ Phase 5
|
||||
│ │ │ └── settingsRepo.ts ⏳ Phase 5
|
||||
│ │ └── ipc/
|
||||
│ │ └── handlers.ts ✅ (Auth + twitch:get-followed + playback:*)
|
||||
│ ├── preload/
|
||||
│ │ └── index.ts ✅ (auth + twitch APIs)
|
||||
│ └── renderer/
|
||||
│ ├── index.html ✅
|
||||
│ └── src/
|
||||
│ ├── main.tsx ✅
|
||||
│ ├── App.tsx ✅ (Routing: LoginScreen / AppShell)
|
||||
│ ├── context/
|
||||
│ │ └── SettingsContext.tsx ✅ Phase 5.6
|
||||
│ ├── screens/
|
||||
│ │ ├── LoginScreen.tsx ✅
|
||||
│ │ ├── AppShell.tsx ✅ (Sidebar + Main-Content, Region-Nav)
|
||||
│ │ ├── FollowingScreen.tsx ✅
|
||||
│ │ ├── BrowseScreen.tsx ✅ Phase 5.5 (Shelf + Kategorien-Grid)
|
||||
│ │ ├── CategoryScreen.tsx ✅ Phase 5.5 (Streams pro Kategorie)
|
||||
│ │ ├── AccountScreen.tsx ✅ (Platzhalter + Logout)
|
||||
│ │ ├── SettingsScreen.tsx ✅ Phase 5.6
|
||||
│ │ ├── ChannelScreen.tsx ✅ Phase 3
|
||||
│ │ └── PlayerLaunchScreen.tsx — entfällt (in ChannelScreen integriert)
|
||||
│ ├── components/
|
||||
│ │ ├── Sidebar.tsx ✅ (4 Tabs inkl. Einstellungen)
|
||||
│ │ ├── Icons.tsx ✅ (Heart/Compass/User/Settings)
|
||||
│ │ ├── FocusableCard.tsx ✅ (mit LanguageBadge)
|
||||
│ │ └── LanguageBadge.tsx ✅ Phase 5.6
|
||||
│ ├── input/
|
||||
│ │ └── gamepad.ts ✅ (A/B/X/Y + DPad gemappt)
|
||||
│ ├── types/
|
||||
│ │ └── t4sd.d.ts ✅
|
||||
│ └── styles/
|
||||
│ └── global.css ✅
|
||||
└── resources/
|
||||
└── icons/ ⏳
|
||||
```
|
||||
|
||||
**Architektur-Abweichungen vom Original-Plan (bewusst):**
|
||||
- `HomeScreen.tsx` wurde durch `FollowingScreen.tsx` ersetzt und in ein `AppShell.tsx`-Layout
|
||||
mit linker `Sidebar.tsx` (3 Tabs: Du folgst / Durchsuchen / Mein Account) eingebettet.
|
||||
- Spatial-Navigation ist eine **eigene Implementierung** (Region-basiert: `sidebar` ↔ `main`),
|
||||
kein `@noriginmedia/norigin-spatial-navigation`. Grund: einfacher, keine externe Dependency,
|
||||
volle Kontrolle über das Region-Switch-Verhalten.
|
||||
- `FocusableCard` nutzt **Block-Layout** (nicht Flex), weil Flex im Zusammenspiel mit
|
||||
`aspect-ratio` und Grid-Zellen Schrumpf-Artefakte hatte.
|
||||
|
||||
## Kernkomponenten — Spezifikation
|
||||
|
||||
### Auth (✅ implementiert)
|
||||
- Endpoint `POST https://id.twitch.tv/oauth2/device` mit `client_id` + `scopes` (space-sep).
|
||||
- Anzeige `user_code` + `verification_uri` + QR.
|
||||
- Polling `POST https://id.twitch.tv/oauth2/token` mit
|
||||
`grant_type=urn:ietf:params:oauth:grant-type:device_code`.
|
||||
- Refresh-Token via Electron `safeStorage`.
|
||||
- Auto-Refresh wenn Helix 401.
|
||||
- Client-ID via `MAIN_VITE_TWITCH_CLIENT_ID` (`.env` im Projektroot, electron-vite lädt sie).
|
||||
- Scope MVP: `user:read:follows`.
|
||||
|
||||
### Helix-Client (✅ implementiert)
|
||||
Implementierte Endpoints (in `src/main/twitch/helixClient.ts`):
|
||||
- `GET /users` (eigener User → `cachedUserId`)
|
||||
- `GET /users?id=…` (batched, für Avatare der Followed-Kanäle)
|
||||
- `GET /channels/followed?user_id=…` (paginiert via `after`, safety-cap 500)
|
||||
- `GET /streams?user_id=…` (batched, max 100 IDs/Call)
|
||||
- `GET /videos?user_id=…&type=archive` — `getVideos()` ✅
|
||||
- `GET /games/top?first=40&after=<cursor>` — `getTopGames(limit, cursor?)` ✅ Phase 5.5
|
||||
- Gibt `{ games: GameInfo[], cursor?: string }` zurück (Pagination)
|
||||
- Macht 40 parallele `/streams?game_id=<id>&first=100` Calls für Zuschauerzahlen
|
||||
- `GET /streams` (global `limit:100` + optional `game_id`) — `getTopStreams(opts?)` ✅ Phase 5.5
|
||||
- Reichert mit Avataren an (`getProfileImages`, batched)
|
||||
- Gibt `FollowedChannelInfo[]` zurück (inkl. `gameId`, `language`)
|
||||
|
||||
Header: `Client-Id: <id>`, `Authorization: Bearer <access_token>`.
|
||||
`getFollowedWithLiveStatus()` merged Followed + Live + Avatare zu `FollowedChannelInfo[]` und
|
||||
sortiert live-zuerst, dann alphabetisch.
|
||||
|
||||
### Streamlink + mpv (✅ Phase 3 + 4)
|
||||
|
||||
**Zwei unterschiedliche Spawn-Strategien (implementiert):**
|
||||
|
||||
**Live-Streams** (`spawnStreamlink`):
|
||||
```
|
||||
streamlink --player <mpv-bin> --player-args "--fullscreen" twitch.tv/<channel> best
|
||||
```
|
||||
- streamlink spawnt mpv als Kind-Prozess, kein IPC
|
||||
- `--twitch-disable-ads` wurde von streamlink deaktiviert (deprecated) — kein Ad-Bypass
|
||||
- `--twitch-api-header=Authorization=OAuth` wird **nicht** übergeben: Twitch lehnt
|
||||
Device-Code-Flow-Token für streamlinks interne GQL-API ab
|
||||
|
||||
**VODs** (`getStreamUrl` + `spawnMpv`):
|
||||
```
|
||||
# Schritt 1: HLS-URL abrufen
|
||||
streamlink --stream-url https://www.twitch.tv/videos/<id> best
|
||||
# → gibt z.B. https://…/playlist.m3u8 zurück
|
||||
|
||||
# Schritt 2: mpv direkt mit HLS-URL starten
|
||||
mpv <hls-url> --input-ipc-server=<ipcPath> --fullscreen --hwdec=<auto|vaapi> --start=<resumePos>
|
||||
```
|
||||
- mpv wird direkt gespawnt (nicht via streamlink) → HLS-URL ist seekable → Seeking/Fortschrittsbalken funktioniert
|
||||
- IPC funktioniert zuverlässig (direkter mpv-Prozess, kein Pfad-Escaping durch streamlink)
|
||||
- `--hwdec=auto` auf Windows, `--hwdec=vaapi` auf Linux/Steam Deck
|
||||
|
||||
**IPC-Pfade:**
|
||||
- Windows: Named Pipe `\\.\pipe\twitch4sd-mpv`
|
||||
- Linux/Flatpak: Unix Socket `/tmp/twitch4sd-mpv.sock`
|
||||
|
||||
**mpvController** (`src/main/playback/mpvController.ts`):
|
||||
- `connect(retries, delayMs)` — verbindet nach Spawn
|
||||
- `observeTimePos(cb)` — registriert `observe_property 1 time-pos`, parst JSON-Lines
|
||||
- `quit()` — sendet `{"command":["quit"]}` via IPC
|
||||
- Wird bei Live-Streams instanziiert aber nie verbunden (harmlos)
|
||||
|
||||
**IPC zwischen Renderer und Main:**
|
||||
- `playback:start-live` → `(channelLogin, quality?)` → `PlaybackService.startLive()`
|
||||
- `playback:start-vod` → `(vodId, channelLogin, title, durationSeconds)` → `PlaybackService.startVod()`
|
||||
- `playback:stop` → `PlaybackService.stop()`
|
||||
- `playback:event` (main → renderer) → `{kind: 'started'|'stopped'|'error', message?}`
|
||||
|
||||
### VOD-History-DB (✅ Phase 5)
|
||||
```sql
|
||||
CREATE TABLE vod_history (
|
||||
vod_id TEXT PRIMARY KEY,
|
||||
channel_login TEXT NOT NULL,
|
||||
title TEXT,
|
||||
duration_seconds INTEGER,
|
||||
resume_position_seconds INTEGER NOT NULL DEFAULT 0,
|
||||
watched_at INTEGER NOT NULL,
|
||||
completed INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_history_watched ON vod_history(watched_at DESC);
|
||||
```
|
||||
- DB-Pfad: `app.getPath('userData')/history.db`, WAL-Modus
|
||||
- Complete-Threshold: `resume_position / duration > 0.95`
|
||||
- `„Continue Watching"-Reihe` bewusst weggelassen (nicht benötigt)
|
||||
- IPC: `history:get-progress` → `(vodIds: string[]) → Record<vodId, VodProgress>`
|
||||
- VOD-Karte zeigt: Fortschrittsbalken, `0:16 von 6:22`, `Vor X Min.`, Completed-Overlay
|
||||
|
||||
### Big-Screen-UI / Gamepad
|
||||
- Spatial-Navigation-Lib oder eigene Mini-Implementierung.
|
||||
- Fokus-Ring deutlich sichtbar.
|
||||
- Schrift: min. 22px Body, 36px+ Headlines.
|
||||
- Mapping: D-Pad/Stick = Navigation, A = Aktivieren, B = Zurück, Y = Refresh, Start = Menü, L1/R1 = Tab.
|
||||
|
||||
### Settings (✅ Phase 5.6)
|
||||
|
||||
- Persistenz: `localStorage`, Key `t4sd:settings`, synchron lesbar (kein Flicker)
|
||||
- Shape: `{ streamBadgeMode: 'off' | 'language' | 'flag' | 'both' }`
|
||||
- Default: `'language'`
|
||||
- React Context: `SettingsProvider` um `<App>`, `useSettings()` Hook
|
||||
- Flaggen: Unicode Regional Indicator Emojis — auf Steam Deck (Noto Color Emoji) korrekt,
|
||||
auf Windows Dev-Host ggf. Rechtecke
|
||||
- `LanguageBadge` rendert in Thumbnails: unten-rechts (gegenüber Viewer-Count unten-links)
|
||||
- 38 Sprachen gemappt (pragmatische Twitch-Dominanz: `pt→🇧🇷`, `en→🇺🇸`, `zh→🇹🇼`)
|
||||
|
||||
### Flatpak (✅ Phase 6 — Dateien erstellt, Verifikation auf Steam Deck ausstehend)
|
||||
- Runtime: `org.freedesktop.Platform//24.08`, `org.electronjs.Electron2.BaseApp//24.08`
|
||||
- Module: `mpv` (aus Sources, libplacebo disabled), `streamlink` (pip), App-Bundle
|
||||
- Permissions: `--share=network`, `--share=ipc`, `--socket=wayland`, `--socket=fallback-x11`,
|
||||
`--socket=pulseaudio`, `--device=dri`
|
||||
- Pre-Build-Strategie: `npm install` + `@electron/rebuild` + `npm run build` in WSL2,
|
||||
dann Artefakte per `type:dir` Source ins Flatpak kopieren
|
||||
- Launcher: `zypak-wrapper node_modules/electron/dist/electron out/main/index.js`
|
||||
- Build-Skript: `flatpak/build-flatpak.sh` (WSL2, prüft deps, füllt SHA256, baut)
|
||||
|
||||
## Verifikation
|
||||
|
||||
End-to-End auf Steam Deck oder Linux-Box:
|
||||
1. Build: `npm install && npm run build && flatpak-builder --user --install build-dir flatpak/…yml`
|
||||
2. Login-Flow mit Device-Code + Smartphone
|
||||
3. Live-Stream 5+ Min ohne Werbe-Unterbrechung (Streamlink-Debug-Logs gegenchecken)
|
||||
4. VOD ansehen, mpv beenden, neu starten → springt an gespeicherte Position
|
||||
5. Komplette Bedienung nur mit Steam-Deck-Controller
|
||||
6. Im Gaming Mode als Non-Steam-Game starten und alles wiederholen
|
||||
|
||||
## Offene Punkte (Post-MVP)
|
||||
- Eigene `client_id` im Build vor Release.
|
||||
- Chat / Emotes (BTTV/FFZ/7TV).
|
||||
- Suche (Text-Input am Gamepad — eigenes UX-Problem).
|
||||
- README + ToS-Disclaimer (Twitch-Werbeumgehung kann gegen ToS verstoßen).
|
||||
- Weitere Einstellungen (Qualität, Theme, ...) — Infrastruktur mit SettingsContext bereit.
|
||||
@@ -91,3 +91,34 @@ export function SettingsIcon(props: IconProps): JSX.Element {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function EyeIcon(props: IconProps): JSX.Element {
|
||||
return (
|
||||
<svg {...base} {...props}>
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function GamepadIcon(props: IconProps): JSX.Element {
|
||||
return (
|
||||
<svg {...base} {...props}>
|
||||
<rect x="2" y="6" width="20" height="12" rx="4" />
|
||||
<path d="M6 12h4" />
|
||||
<path d="M8 10v4" />
|
||||
<circle cx="15" cy="11" r="1" fill="currentColor" stroke="none" />
|
||||
<circle cx="17" cy="13" r="1" fill="currentColor" stroke="none" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function ClapperboardIcon(props: IconProps): JSX.Element {
|
||||
return (
|
||||
<svg {...base} {...props}>
|
||||
<path d="M4 11H20V20a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V11z" />
|
||||
<path d="M4 11V7a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v4" />
|
||||
<path d="M4 9l4-3 4 3 4-3" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { GamepadHintItem } from './GamepadPrompt'
|
||||
import { PauseIcon, PlayIcon, StopIcon } from './Icons'
|
||||
import { ClapperboardIcon, EyeIcon, GamepadIcon, PauseIcon, PlayIcon, StopIcon } from './Icons'
|
||||
|
||||
interface PlaybackOverlayProps {
|
||||
playState: 'playing' | 'paused'
|
||||
@@ -10,12 +10,21 @@ interface PlaybackOverlayProps {
|
||||
channelName: string
|
||||
channelAvatar: string
|
||||
title: string
|
||||
viewerCount?: number
|
||||
viewCount?: number
|
||||
gameName?: string
|
||||
onTogglePause: () => void
|
||||
onSeek: (seconds: number) => void
|
||||
onSeekTo: (seconds: number) => void
|
||||
onStop: () => void
|
||||
}
|
||||
|
||||
function formatCount(n: number): string {
|
||||
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
|
||||
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
|
||||
return String(n)
|
||||
}
|
||||
|
||||
/** Formats seconds as HH:MM:SS */
|
||||
function fmt(s: number): string {
|
||||
const total = Math.max(0, Math.floor(s))
|
||||
@@ -36,6 +45,9 @@ export function PlaybackOverlay({
|
||||
channelName,
|
||||
channelAvatar,
|
||||
title,
|
||||
viewerCount,
|
||||
viewCount,
|
||||
gameName,
|
||||
onTogglePause,
|
||||
onSeek,
|
||||
onSeekTo,
|
||||
@@ -253,8 +265,46 @@ export function PlaybackOverlay({
|
||||
{/* Video title */}
|
||||
{title && <p className="playback-overlay__title">{title}</p>}
|
||||
|
||||
{/* Progress row */}
|
||||
{!isLive && durationSeconds > 0 ? (
|
||||
{/* Progress row / status row */}
|
||||
{isLive ? (
|
||||
<div className="playback-overlay__meta-row">
|
||||
<div className="playback-overlay__live-indicator">
|
||||
<span className="playback-overlay__live-dot" />
|
||||
Live
|
||||
</div>
|
||||
{viewerCount !== undefined && viewerCount > 0 && (
|
||||
<span className="playback-overlay__meta-chip">
|
||||
<EyeIcon width={14} height={14} />
|
||||
{formatCount(viewerCount)}
|
||||
</span>
|
||||
)}
|
||||
{gameName && (
|
||||
<span className="playback-overlay__meta-chip">
|
||||
<GamepadIcon width={14} height={14} />
|
||||
{gameName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
) : durationSeconds > 0 ? (
|
||||
<>
|
||||
<div className="playback-overlay__meta-row">
|
||||
<div className="playback-overlay__vod-badge">
|
||||
<ClapperboardIcon width={13} height={13} />
|
||||
VOD
|
||||
</div>
|
||||
{viewCount !== undefined && viewCount > 0 && (
|
||||
<span className="playback-overlay__meta-chip">
|
||||
<EyeIcon width={14} height={14} />
|
||||
{formatCount(viewCount)}
|
||||
</span>
|
||||
)}
|
||||
{gameName && (
|
||||
<span className="playback-overlay__meta-chip">
|
||||
<GamepadIcon width={14} height={14} />
|
||||
{gameName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="playback-overlay__progress-row">
|
||||
<span className="playback-overlay__time">{fmt(displayPosition)}</span>
|
||||
|
||||
@@ -280,11 +330,7 @@ export function PlaybackOverlay({
|
||||
|
||||
<span className="playback-overlay__time">{fmt(durationSeconds)}</span>
|
||||
</div>
|
||||
) : isLive ? (
|
||||
<div className="playback-overlay__live-indicator">
|
||||
<span className="playback-overlay__live-dot" />
|
||||
Live
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
{/* Stop button */}
|
||||
|
||||
@@ -321,6 +321,8 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
||||
channelName={liveChannel?.broadcasterName ?? ''}
|
||||
channelAvatar={liveChannel?.profileImageUrl ?? ''}
|
||||
title={liveChannel?.streamTitle ?? ''}
|
||||
viewerCount={liveChannel?.viewerCount ?? undefined}
|
||||
gameName={liveChannel?.gameName ?? undefined}
|
||||
onTogglePause={() => {
|
||||
if (livePlayState === 'playing') {
|
||||
liveVideoRef.current?.pause()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { GamepadHintItem, GamepadPrompt } from '../components/GamepadPrompt'
|
||||
import { PlaybackOverlay } from '../components/PlaybackOverlay'
|
||||
import { VideoPlayer, type VideoPlayerHandle } from '../components/VideoPlayer'
|
||||
@@ -69,6 +69,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
||||
const [shelfIndex, setShelfIndex] = useState(0)
|
||||
const [chapterPanelVod, setChapterPanelVod] = useState<VodInfo | null>(null)
|
||||
const [chapters, setChapters] = useState<VodChapter[]>([])
|
||||
const [vodChaptersLoaded, setVodChaptersLoaded] = useState(false)
|
||||
const [chaptersLoading, setChaptersLoading] = useState(false)
|
||||
const [chapterIndex, setChapterIndex] = useState(0)
|
||||
const [wasPlayingBeforeChapters, setWasPlayingBeforeChapters] = useState(false)
|
||||
@@ -81,6 +82,16 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
||||
// HLS-Player state
|
||||
const [hlsPayload, setHlsPayload] = useState<HlsUrlPayload | null>(null)
|
||||
|
||||
// Derive active chapter name from chapters list + current playback position.
|
||||
// Three states: loading → undefined (chip hidden), loaded+empty → 'Unbekannt', loaded+chapters → active chapter.
|
||||
const currentChapterName = useMemo<string | undefined>(() => {
|
||||
if (isLivePlayback) return undefined
|
||||
if (!vodChaptersLoaded) return undefined
|
||||
if (chapters.length === 0) return 'Unbekannt'
|
||||
const active = [...chapters].reverse().find((ch) => ch.positionSeconds <= currentPosition)
|
||||
return active?.gameName ?? 'Unbekannt'
|
||||
}, [chapters, vodChaptersLoaded, currentPosition, isLivePlayback])
|
||||
|
||||
const watchBtnRef = useRef<HTMLButtonElement>(null)
|
||||
const chapterListRef = useRef<HTMLUListElement>(null)
|
||||
const videoRef = useRef<VideoPlayerHandle>(null)
|
||||
@@ -363,6 +374,10 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
||||
setWasPlayingBeforeChapters(false)
|
||||
setCurrentVod(vod)
|
||||
setPendingChapterVod(null)
|
||||
setVodChaptersLoaded(false)
|
||||
void loadChapters(vod.id)
|
||||
.then((loaded) => { setChapters(loaded); setVodChaptersLoaded(true) })
|
||||
.catch(() => { setVodChaptersLoaded(true) })
|
||||
try {
|
||||
await window.t4sd.playback.startVod(
|
||||
vod.id,
|
||||
@@ -403,6 +418,10 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
||||
setWasPlayingBeforeChapters(false)
|
||||
setCurrentVod(vod)
|
||||
setPendingChapterVod(null)
|
||||
setVodChaptersLoaded(false)
|
||||
void loadChapters(vod.id)
|
||||
.then((loaded) => { setChapters(loaded); setVodChaptersLoaded(true) })
|
||||
.catch(() => { setVodChaptersLoaded(true) })
|
||||
try {
|
||||
await window.t4sd.playback.startVod(
|
||||
vod.id,
|
||||
@@ -672,6 +691,9 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
||||
channelName={channel.broadcasterName}
|
||||
channelAvatar={channel.profileImageUrl}
|
||||
title={isLivePlayback ? (channel.streamTitle ?? '') : (currentVod?.title ?? '')}
|
||||
viewerCount={isLivePlayback ? (channel.viewerCount ?? undefined) : undefined}
|
||||
viewCount={!isLivePlayback ? (currentVod?.viewCount ?? undefined) : undefined}
|
||||
gameName={isLivePlayback ? (channel.gameName ?? undefined) : currentChapterName}
|
||||
onTogglePause={() => {
|
||||
if (playState === 'playing') {
|
||||
void window.t4sd.playback.pause()
|
||||
|
||||
@@ -1809,6 +1809,42 @@ body,
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Meta-Info-Zeile (Live-Indikator + Chips) */
|
||||
.playback-overlay__meta-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Info-Chip: Auge+Zahl / Gamepad+Name */
|
||||
.playback-overlay__meta-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 2px 7px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* VOD-Badge (analog zum LIVE-Badge, aber Twitch-Lila statt rot) */
|
||||
.playback-overlay__vod-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
color: #fff;
|
||||
background: var(--color-accent);
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* Stop button */
|
||||
.playback-overlay__stop-btn {
|
||||
align-self: flex-end;
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
# Twitch4SteamDeck — TODO
|
||||
|
||||
> Checkboxen-Tracker. Nach jedem erledigten Punkt sofort abhaken und kurzes
|
||||
> Update in `CLAUDE_PROGRESS.md` ergänzen.
|
||||
|
||||
## Phase 0 — Projekt-Skelett
|
||||
- [x] `package.json`, tsconfig (root/node/web)
|
||||
- [x] `electron.vite.config.ts`, `electron-builder.yml`, `.gitignore`
|
||||
- [x] Main-Stub mit BrowserWindow
|
||||
- [x] Preload-Stub mit `contextBridge`
|
||||
- [x] Renderer (React + Big-Screen-Baseline-CSS)
|
||||
- [x] Smoke-Test: `npm install && npm run dev` zeigt Fenster
|
||||
|
||||
## Phase 1 — Twitch OAuth Device Code Flow
|
||||
- [x] `deviceCodeFlow.ts` (request + polling + refresh)
|
||||
- [x] `tokenStore.ts` (safeStorage-Persistenz)
|
||||
- [x] `authService.ts` (Lifecycle + EventEmitter)
|
||||
- [x] IPC-Handler + Preload-Bridge
|
||||
- [x] `LoginScreen.tsx` (QR + Code + Countdown)
|
||||
- [x] `App.tsx` Routing nach Auth-Status
|
||||
- [x] `.env.example`
|
||||
- [x] **End-to-End-Verifikation durch User** (Login mit echter Twitch-App durchführen)
|
||||
- [x] Token-Persistenz prüfen: App-Neustart → bleibt eingeloggt
|
||||
- [ ] Logout testen: `userData/twitch-tokens.bin` wird gelöscht (später)
|
||||
|
||||
## Phase 2 — Twitch Helix-Client + Followed-Channels-Home-Screen
|
||||
- [x] `src/main/twitch/types.ts` (Helix-Response-Typen)
|
||||
- [x] `src/main/twitch/helixClient.ts` (fetch-Wrapper, Auth-Header, Client-ID-Header, Paginierung)
|
||||
- [x] Helix-Endpoints implementieren:
|
||||
- [x] `GET /users` (eigener User → user_id cachen)
|
||||
- [x] `GET /channels/followed?user_id=…` (paginiert)
|
||||
- [x] `GET /streams?user_id=…` (batched, max 100 IDs pro Call)
|
||||
- [x] IPC: `twitch:get-followed` registrieren (merged live-Status + Avatar)
|
||||
- [x] Preload-API erweitern: `window.t4sd.twitch.getFollowed`
|
||||
- [x] `FollowingScreen.tsx` — Karten-Grid mit Live-Indicator, Vorschaubild, Titel, Zuschauerzahl
|
||||
- [x] Spatial-Navigation: eigene Implementierung (Arrow-Keys + Region-Switch Sidebar ↔ Main)
|
||||
- [x] Gamepad-Service: `src/renderer/src/input/gamepad.ts` (Gamepad-API → Keyboard-Events)
|
||||
- [x] `FocusableCard.tsx` — Block-Layout, Avatar + Name + Titel + Spiel + Viewer-Badge
|
||||
- [x] `AppShell.tsx` + `Sidebar.tsx` — Layout mit linker Navigation (Du folgst / Durchsuchen / Mein Account)
|
||||
- [x] `Icons.tsx` — inline SVG-Icons (Heart, Compass, User)
|
||||
- [x] `BrowseScreen.tsx` + `AccountScreen.tsx` — Platzhalter-Screens mit Logout
|
||||
- [x] **End-to-End-Verifikation durch User** (2026-04-07: Karten + Tastatur-Nav bestätigt)
|
||||
|
||||
## Phase 3 — Streamlink + mpv (Live-Wiedergabe)
|
||||
|
||||
### Vorab (Dev-Setup, Windows)
|
||||
- [x] **User:** `mpv` installieren — `winget install shinchiro.mpv`, via `setx` zum PATH
|
||||
- [x] **User:** `streamlink` installieren — `winget install streamlink` (8.2.1)
|
||||
- [x] **User:** Baseline-Test erfolgreich:
|
||||
`streamlink --twitch-disable-ads twitch.tv/streamerhouse best --player mpv`
|
||||
|
||||
### Backend (Main-Prozess)
|
||||
- [x] `src/main/playback/types.ts`
|
||||
- [x] `src/main/playback/streamlink.ts` — `resolveStreamlinkBin()` (Win32-Pfad-Auflösung),
|
||||
`spawnStreamlink()` mit `--player mpv --player-args "..."` (Variante 2)
|
||||
- [x] `src/main/playback/mpvController.ts` — Named Pipe (Win32) / Unix Socket (Linux),
|
||||
connect mit Retry, `quit()`, `disconnect()`
|
||||
- [x] `src/main/playback/playbackService.ts`
|
||||
- [x] IPC-Kanäle: `playback:start-live`, `playback:stop`, `playback:event`
|
||||
|
||||
### Renderer (UI)
|
||||
- [x] Preload + `t4sd.d.ts` erweitern: `window.t4sd.playback.{startLive, stop, onEvent}`
|
||||
- [x] `src/renderer/src/screens/ChannelScreen.tsx` (Hero + Live-Button + Zustandsanzeige)
|
||||
- [x] Routing in `AppShell.tsx`: `selectedChannel`-State → `ChannelScreen`
|
||||
- [x] `FocusableCard.onSelect` → setzt `selectedChannel` via `onSelectChannel`-Prop
|
||||
- [x] Spatial-Nav: Escape/B im ChannelScreen → Stop oder Zurück
|
||||
|
||||
### Verifikation
|
||||
- [x] Stream startet: mpv-Fenster öffnet sich, Stream spielt ab (2026-04-08)
|
||||
_(Fix: --twitch-api-header entfernt, resolveMpvBin() ergänzt)_
|
||||
- [x] **User:** Stop-Verhalten: mpv-Fenster schließt sich, keine Zombie-Prozesse (2026-04-08)
|
||||
- [x] **User:** Logout → Login → Stream starten funktioniert ohne App-Neustart (2026-04-08)
|
||||
⚠️ `--twitch-disable-ads` ist von streamlink deaktiviert — Werbung wird ggf. abgespielt (Post-MVP)
|
||||
|
||||
## Phase 4 — VOD-Browsing + -Wiedergabe
|
||||
- [x] Helix `GET /videos?user_id=…&type=archive` → `getVideos()` in helixClient
|
||||
- [x] VOD-Shelf im ChannelScreen (horizontal, obere Hälfte fix)
|
||||
- [x] `playback:start-vod` IPC-Kanal + `PlaybackService.startVod()`
|
||||
- [x] mpv `--start=<seconds>` beim Spawn (Parameter vorbereitet für Phase 5)
|
||||
- [x] **User:** VOD-Wiedergabe + Stop-Verhalten verifiziert (2026-04-08)
|
||||
|
||||
## Phase 5 — Lokaler VOD-Verlauf (Resume)
|
||||
- [x] SQLite-Setup: `src/main/store/db.ts` (better-sqlite3, Migration)
|
||||
- [x] `historyRepo.ts` mit `vod_history`-Tabelle
|
||||
- [x] mpv `time-pos`-Observer schreibt alle ~5s in DB
|
||||
- [x] Auto-Resume: beim VOD-Start Position aus DB lesen → `--start=<s>`
|
||||
- [x] Auto-Complete bei >95% Wiedergabefortschritt
|
||||
- [x] Resume-Indikator auf VOD-Karte (Fortschrittsbalken + „0:16 von 6:22" + „Vor X Min.")
|
||||
- [x] Completed-Indikator (dunkles Overlay + Checkmark)
|
||||
- [x] **User:** Verifiziert (2026-04-08)
|
||||
- ~~„Continue Watching"-Reihe~~ — entfällt (nicht benötigt)
|
||||
|
||||
## Phase 5.5 — Browse-Menü: Kategorien + Top Live-Streams
|
||||
|
||||
- [x] `HelixGame`, `GameInfo` Typen in `types.ts`
|
||||
- [x] `helixClient.ts`: `getTopGames()` + `getTopStreams(gameId?)`
|
||||
- [x] IPC: `twitch:get-top-games`, `twitch:get-top-streams`
|
||||
- [x] Preload + `t4sd.d.ts`: Typen + API-Methoden
|
||||
- [x] `gamepad.ts`: Button 2 (X) → `'x'`, Button 3 (Y) → `'y'`
|
||||
- [x] `BrowseScreen.tsx`: Shelf (Top Live) + Grid (Kategorien), Key-Mapping
|
||||
- [x] `CategoryScreen.tsx`: Streams-Grid pro Game
|
||||
- [x] `AppShell.tsx`: `selectedCategory`-Routing
|
||||
- [x] `global.css`: Browse/Game-Card-Styles
|
||||
- [x] **User:** End-to-End-Verifikation (Browse laden, Live starten, Kategorie-Drilldown)
|
||||
|
||||
## Phase 5.6 — Einstellungen + Sprach-Badge/Flagge
|
||||
|
||||
- [x] `language` in `HelixStream` + `FollowedChannelInfo` (types.ts, preload, t4sd.d.ts)
|
||||
- [x] `helixClient.ts`: `language` in `getTopStreams` + `getFollowedWithLiveStatus`
|
||||
- [x] `SettingsContext.tsx`: Provider + `useSettings()` + localStorage-Persistenz
|
||||
- [x] `Icons.tsx`: `SettingsIcon` (Zahnrad)
|
||||
- [x] `Sidebar.tsx`: 4. Tab „Einstellungen"
|
||||
- [x] `LanguageBadge.tsx`: Badge/Flag/Both Rendering + 38-Sprachen-Map
|
||||
- [x] `SettingsScreen.tsx`: Radio-Liste mit 4 Optionen
|
||||
- [x] `AppShell.tsx`: Settings-Tab routen
|
||||
- [x] `main.tsx`: `<SettingsProvider>` um App
|
||||
- [x] `FocusableCard.tsx`: LanguageBadge unten-rechts
|
||||
- [x] `BrowseScreen.tsx`: LanguageBadge in Shelf-Cards
|
||||
- [x] `global.css`: Badge + Settings-Screen Styles
|
||||
- [x] **User:** End-to-End-Verifikation (Einstellungen + Badge funktionieren, Persistenz bestätigt; Flaggen erst auf Steam Deck testbar)
|
||||
|
||||
## Phase 6 — Flatpak-Packaging
|
||||
- [x] `flatpak/tv.twitch4steamdeck.App.yml` Manifest (Runtime, Module, Permissions)
|
||||
- [x] `flatpak/twitch4steamdeck.sh` Launcher (zypak-wrapper)
|
||||
- [x] `flatpak/tv.twitch4steamdeck.App.desktop` Desktop-Eintrag
|
||||
- [x] `flatpak/build-flatpak.sh` Build-Skript für WSL2 (inkl. SHA256-Auflösung)
|
||||
- [x] `resources/icons/icon.svg` App-Icon (Platzhalter)
|
||||
- [x] mpv + streamlink als Flatpak-Module im Manifest definiert
|
||||
- [x] **Fix (Phase 6.1):** streamlink Python-Deps via `flatpak-pip-generator` eingebunden
|
||||
(`build-flatpak.sh` Schritt 4b + `!include python3-streamlink.json` im Manifest)
|
||||
- [x] **Fix (Phase 6.1):** mpv-Patch `patches/mpv-ffmpeg7-avio-const.patch` ins Manifest eingebunden
|
||||
- [x] **User:** WSL2-Setup (flatpak, flatpak-builder, Runtimes) — einmalig
|
||||
- [x] **User:** `bash flatpak/build-flatpak.sh` in WSL2 neu ausführen (nach 6.1-Fix)
|
||||
- [x] **User:** Auf Steam Deck installieren und Login + Live + VOD durchspielen
|
||||
- [x] **User:** Als Non-Steam-Game in Gaming Mode hinzufügen, dort testen
|
||||
|
||||
## Post-MVP (bewusst nicht im ersten Release)
|
||||
- [ ] Chat (Live + VOD-Replay) inkl. BTTV/FFZ/7TV
|
||||
- [ ] Suche / Kategorien-Browsing
|
||||
- [ ] Eigene Twitch-Client-ID im Build verankert
|
||||
- [ ] README mit Disclaimer (ToS) + Setup-Anleitung
|
||||
- [ ] Flathub-Einreichung
|
||||
Reference in New Issue
Block a user