From 897f6ab7277e69420999db35a99310bc7aad5ead Mon Sep 17 00:00:00 2001 From: Andre <200930841+Andre-Bzk@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:37:20 +0200 Subject: [PATCH] =?UTF-8?q?Phase=205.6=20abgeschlossen=20-=20Seitenmenu=20?= =?UTF-8?q?um=20Einstellungen=20erg=C3=A4nzt=20-=20Sprach-Badge=20und=20Fl?= =?UTF-8?q?aggen=20anzeige=20im=20Thumbnail=20des=20Streams=20anzeigbar.?= =?UTF-8?q?=20-=20=20Sprach=20und=20Flaggen=20Einstellungen=20hinzugef?= =?UTF-8?q?=C3=BCgt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE_PROGRESS.md | 61 ++++++++-- plan.md | 56 +++++++-- src/main/twitch/helixClient.ts | 7 +- src/main/twitch/types.ts | 2 + src/preload/index.ts | 1 + src/renderer/src/components/FocusableCard.tsx | 2 + src/renderer/src/components/Icons.tsx | 9 ++ src/renderer/src/components/LanguageBadge.tsx | 74 ++++++++++++ src/renderer/src/components/Sidebar.tsx | 7 +- src/renderer/src/context/SettingsContext.tsx | 65 +++++++++++ src/renderer/src/main.tsx | 5 +- src/renderer/src/screens/AppShell.tsx | 4 + src/renderer/src/screens/BrowseScreen.tsx | 2 + src/renderer/src/screens/SettingsScreen.tsx | 107 +++++++++++++++++ src/renderer/src/styles/global.css | 109 ++++++++++++++++++ src/renderer/src/types/t4sd.d.ts | 1 + todo.md | 18 ++- 17 files changed, 504 insertions(+), 26 deletions(-) create mode 100644 src/renderer/src/components/LanguageBadge.tsx create mode 100644 src/renderer/src/context/SettingsContext.tsx create mode 100644 src/renderer/src/screens/SettingsScreen.tsx diff --git a/CLAUDE_PROGRESS.md b/CLAUDE_PROGRESS.md index 3928362..002c41c 100644 --- a/CLAUDE_PROGRESS.md +++ b/CLAUDE_PROGRESS.md @@ -1,9 +1,9 @@ # Twitch4SteamDeck — Fortschritts-Snapshot -> Stand: **2026-04-08 (Session 3)**. Diese Datei dient als Übergabe zwischen Sessions. +> Stand: **2026-04-08 (Session 4)**. Diese Datei dient als Übergabe zwischen Sessions. > **Lies zuerst:** diese Datei + `todo.md` + `plan.md`. -**Aktueller Stand:** Phase 0–5 abgeschlossen & verifiziert. Phase 5.5 (Browse) implementiert, wartet auf User-Verifikation. Danach: Phase 6 (Flatpak-Packaging). +**Aktueller Stand:** Phase 0–5.6 abgeschlossen & verifiziert. Als nächstes: Phase 6 (Flatpak-Packaging). --- @@ -65,21 +65,20 @@ Verifiziert 2026-04-08. --- ---- +### ✅ Phase 5.5 — Browse-Menü: Kategorien + Top Live-Streams -### ⏳ Phase 5.5 — Browse-Menü (implementiert, Verifikation ausstehend) +Verifiziert 2026-04-08. #### Neue/geänderte Dateien: -- `src/main/twitch/types.ts` — `HelixGame`, `GameInfo` -- `src/main/twitch/helixClient.ts` — `getTopGames()`, `getTopStreams(gameId?)` +- `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 + Grid (komplett neu) -- `src/renderer/src/screens/CategoryScreen.tsx` — neu +- `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 -- `src/renderer/src/styles/global.css` — Browse/Game-Card-Styles #### Key-Mapping Browse + Category: - **Enter / A**: Stream → direkt Live starten · Kategorie → Drilldown @@ -87,6 +86,49 @@ Verifiziert 2026-04-08. - **Y**: Refresh - **Escape / B**: Category → zurück zu Browse · Browse → Sidebar +#### Wichtige Implementierungsdetails: +- `getTopGames()` macht 40 parallele `/streams?game_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` — `` unten-rechts im Thumb +- `src/renderer/src/screens/BrowseScreen.tsx` — `` in Shelf-Cards +- `src/renderer/src/screens/AppShell.tsx` — `tab === 'settings'` → SettingsScreen +- `src/renderer/src/main.tsx` — `` 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. + --- ## Nächster Schritt: Phase 6 — Flatpak-Packaging @@ -99,6 +141,7 @@ Wichtige offene Punkte: - IPC-Pfad: Unix Socket `/tmp/twitch4sd-mpv.sock` (bereits implementiert für non-win32) - better-sqlite3 muss für Linux/ARM64 (Steam Deck) cross-compiliert werden - Flatpak-Manifest: `flatpak/tv.twitch4steamdeck.App.yml` +- **Flaggen-Emojis erst hier testbar** (Noto Color Emoji auf Linux) --- diff --git a/plan.md b/plan.md index 113c605..60681f4 100644 --- a/plan.md +++ b/plan.md @@ -30,7 +30,9 @@ mit Resume. Verteilung als Flatpak. 3. Live-Stream-Wiedergabe werbefrei (Streamlink → mpv) 4. VOD-Browsing pro Kanal 5. VOD-Wiedergabe werbefrei -6. Lokaler VOD-Verlauf + Resume + „Continue Watching"-Reihe +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. @@ -58,6 +60,19 @@ mit Resume. Verteilung als Flatpak. 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) ``` @@ -96,20 +111,25 @@ twitch4steamdeck/ │ └── 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 ✅ (ersetzt die ursprünglich geplante HomeScreen.tsx) -│ │ ├── BrowseScreen.tsx ✅ (Platzhalter) +│ │ ├── 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 ✅ -│ │ ├── Icons.tsx ✅ (inline SVG: Heart/Compass/User) -│ │ └── FocusableCard.tsx ✅ (Block-Layout) +│ │ ├── Sidebar.tsx ✅ (4 Tabs inkl. Einstellungen) +│ │ ├── Icons.tsx ✅ (Heart/Compass/User/Settings) +│ │ ├── FocusableCard.tsx ✅ (mit LanguageBadge) +│ │ └── LanguageBadge.tsx ✅ Phase 5.6 │ ├── input/ -│ │ └── gamepad.ts ✅ +│ │ └── gamepad.ts ✅ (A/B/X/Y + DPad gemappt) │ ├── types/ │ │ └── t4sd.d.ts ✅ │ └── styles/ @@ -146,8 +166,12 @@ Implementierte Endpoints (in `src/main/twitch/helixClient.ts`): - `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` — `getTopGames()` ✅ Phase 5.5 -- `GET /streams` (global + game_id) — `getTopStreams()` ✅ Phase 5.5 +- `GET /games/top?first=40&after=` — `getTopGames(limit, cursor?)` ✅ Phase 5.5 + - Gibt `{ games: GameInfo[], cursor?: string }` zurück (Pagination) + - Macht 40 parallele `/streams?game_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: `, `Authorization: Bearer `. `getFollowedWithLiveStatus()` merged Followed + Live + Avatare zu `FollowedChannelInfo[]` und @@ -220,6 +244,17 @@ CREATE INDEX IF NOT EXISTS idx_history_watched ON vod_history(watched_at DESC); - 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 ``, `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) - Runtime: `org.freedesktop.Platform//23.08`, `org.electronjs.Electron2.BaseApp` - Module: app-bundle, `mpv`, `streamlink` (pip), evtl. `streamlink-ttvlol` @@ -239,5 +274,6 @@ End-to-End auf Steam Deck oder Linux-Box: ## Offene Punkte (Post-MVP) - Eigene `client_id` im Build vor Release. - Chat / Emotes (BTTV/FFZ/7TV). -- Suche, Kategorien, Top-Streams. +- 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. diff --git a/src/main/twitch/helixClient.ts b/src/main/twitch/helixClient.ts index 17f2679..da24f7b 100644 --- a/src/main/twitch/helixClient.ts +++ b/src/main/twitch/helixClient.ts @@ -195,7 +195,8 @@ export class HelixClient { gameName: s.game_name, viewerCount: s.viewer_count, thumbnailUrl: s.thumbnail_url.replace('{width}', '440').replace('{height}', '248'), - startedAt: s.started_at + startedAt: s.started_at, + language: s.language })) } @@ -219,6 +220,7 @@ export class HelixClient { profileImageUrl: avatarMap.get(f.broadcaster_id) ?? '', isLive: !!stream, streamTitle: stream?.title, + gameId: stream?.game_id, gameName: stream?.game_name, viewerCount: stream?.viewer_count, thumbnailUrl: stream @@ -226,7 +228,8 @@ export class HelixClient { .replace('{width}', '440') .replace('{height}', '248') : undefined, - startedAt: stream?.started_at + startedAt: stream?.started_at, + language: stream?.language } }) diff --git a/src/main/twitch/types.ts b/src/main/twitch/types.ts index b114897..6e0bb4d 100644 --- a/src/main/twitch/types.ts +++ b/src/main/twitch/types.ts @@ -22,6 +22,7 @@ export interface HelixStream { viewer_count: number thumbnail_url: string started_at: string + language: string } export interface HelixPaginatedResponse { @@ -81,4 +82,5 @@ export interface FollowedChannelInfo { /** 440×248 thumbnail URL (only set when live) */ thumbnailUrl?: string startedAt?: string + language?: string } diff --git a/src/preload/index.ts b/src/preload/index.ts index 734c125..72cd61a 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -47,6 +47,7 @@ export interface FollowedChannelInfo { viewerCount?: number thumbnailUrl?: string startedAt?: string + language?: string } export interface PlaybackEvent { diff --git a/src/renderer/src/components/FocusableCard.tsx b/src/renderer/src/components/FocusableCard.tsx index fe38dd5..32747f8 100644 --- a/src/renderer/src/components/FocusableCard.tsx +++ b/src/renderer/src/components/FocusableCard.tsx @@ -1,5 +1,6 @@ import { useEffect, useRef } from 'react' import type { FollowedChannelInfo } from '../types/t4sd' +import LanguageBadge from './LanguageBadge' interface Props { channel: FollowedChannelInfo @@ -40,6 +41,7 @@ export default function FocusableCard({ channel, focused, onFocus, onSelect }: P {channel.viewerCount !== undefined && ( {formatViewers(channel.viewerCount)} )} + ) : (
diff --git a/src/renderer/src/components/Icons.tsx b/src/renderer/src/components/Icons.tsx index a0ddea8..089f29d 100644 --- a/src/renderer/src/components/Icons.tsx +++ b/src/renderer/src/components/Icons.tsx @@ -36,3 +36,12 @@ export function UserIcon(props: IconProps): JSX.Element { ) } + +export function SettingsIcon(props: IconProps): JSX.Element { + return ( + + + + + ) +} diff --git a/src/renderer/src/components/LanguageBadge.tsx b/src/renderer/src/components/LanguageBadge.tsx new file mode 100644 index 0000000..ff64e79 --- /dev/null +++ b/src/renderer/src/components/LanguageBadge.tsx @@ -0,0 +1,74 @@ +import { useSettings } from '../context/SettingsContext' + +// Pragmatische Sprache→Flagge-Zuordnung. +// Grundsatz: dominante Twitch-Nutzerbase pro Sprache, +// nicht die politisch "erste" Assoziation. +const LANG_FLAG: Record = { + en: '🇺🇸', + de: '🇩🇪', + es: '🇪🇸', + fr: '🇫🇷', + pt: '🇧🇷', // Brasilien dominiert pt auf Twitch + ja: '🇯🇵', + ko: '🇰🇷', + zh: '🇹🇼', // Taiwan dominiert zh auf Twitch + ru: '🇷🇺', + it: '🇮🇹', + pl: '🇵🇱', + tr: '🇹🇷', + nl: '🇳🇱', + sv: '🇸🇪', + fi: '🇫🇮', + da: '🇩🇰', + no: '🇳🇴', + cs: '🇨🇿', + hu: '🇭🇺', + th: '🇹🇭', + ar: '🇸🇦', + id: '🇮🇩', + el: '🇬🇷', + ro: '🇷🇴', + bg: '🇧🇬', + uk: '🇺🇦', + vi: '🇻🇳', + he: '🇮🇱', + sk: '🇸🇰', + ms: '🇲🇾', + tl: '🇵🇭', + hi: '🇮🇳', + ca: '🇪🇸', + hr: '🇭🇷', + sr: '🇷🇸', + sl: '🇸🇮', + lt: '🇱🇹', + lv: '🇱🇻', + et: '🇪🇪' +} + +interface Props { + language?: string + /** Optional: andere CSS-Klasse für kleinere Karten */ + className?: string +} + +export default function LanguageBadge({ language, className = 'card__language' }: Props): JSX.Element | null { + const { settings } = useSettings() + const mode = settings.streamBadgeMode + + if (mode === 'off' || !language || language === 'other' || language === '') return null + + const upper = language.toUpperCase() + const flag = LANG_FLAG[language.toLowerCase()] + + let content: string + if (mode === 'language') { + content = upper + } else if (mode === 'flag') { + content = flag ?? upper // Fallback auf Kürzel wenn keine Flagge bekannt + } else { + // 'both' + content = flag ? `${flag} ${upper}` : upper + } + + return {content} +} diff --git a/src/renderer/src/components/Sidebar.tsx b/src/renderer/src/components/Sidebar.tsx index 67bca50..eaf6d73 100644 --- a/src/renderer/src/components/Sidebar.tsx +++ b/src/renderer/src/components/Sidebar.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef } from 'react' -import { CompassIcon, HeartIcon, UserIcon } from './Icons' +import { CompassIcon, HeartIcon, SettingsIcon, UserIcon } from './Icons' -export type TabKey = 'following' | 'browse' | 'account' +export type TabKey = 'following' | 'browse' | 'account' | 'settings' interface SidebarItemDef { key: TabKey @@ -12,7 +12,8 @@ interface SidebarItemDef { export const SIDEBAR_ITEMS: SidebarItemDef[] = [ { key: 'following', label: 'Du folgst', Icon: HeartIcon }, { key: 'browse', label: 'Durchsuchen', Icon: CompassIcon }, - { key: 'account', label: 'Mein Account', Icon: UserIcon } + { key: 'account', label: 'Mein Account', Icon: UserIcon }, + { key: 'settings', label: 'Einstellungen', Icon: SettingsIcon } ] interface Props { diff --git a/src/renderer/src/context/SettingsContext.tsx b/src/renderer/src/context/SettingsContext.tsx new file mode 100644 index 0000000..6b4b358 --- /dev/null +++ b/src/renderer/src/context/SettingsContext.tsx @@ -0,0 +1,65 @@ +import { createContext, useCallback, useContext, useState } from 'react' + +export type StreamBadgeMode = 'off' | 'language' | 'flag' | 'both' + +export interface AppSettings { + streamBadgeMode: StreamBadgeMode +} + +const STORAGE_KEY = 't4sd:settings' +const DEFAULTS: AppSettings = { streamBadgeMode: 'language' } + +function loadSettings(): AppSettings { + try { + const raw = localStorage.getItem(STORAGE_KEY) + if (!raw) return DEFAULTS + const parsed = JSON.parse(raw) as Partial + const modes: StreamBadgeMode[] = ['off', 'language', 'flag', 'both'] + return { + streamBadgeMode: modes.includes(parsed.streamBadgeMode as StreamBadgeMode) + ? (parsed.streamBadgeMode as StreamBadgeMode) + : DEFAULTS.streamBadgeMode + } + } catch { + return DEFAULTS + } +} + +function saveSettings(s: AppSettings): void { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(s)) + } catch { + // localStorage nicht verfügbar — ignorieren + } +} + +interface SettingsCtx { + settings: AppSettings + setStreamBadgeMode: (mode: StreamBadgeMode) => void +} + +const SettingsContext = createContext(null) + +export function SettingsProvider({ children }: { children: React.ReactNode }): JSX.Element { + const [settings, setSettings] = useState(loadSettings) + + const setStreamBadgeMode = useCallback((mode: StreamBadgeMode) => { + setSettings((prev) => { + const next = { ...prev, streamBadgeMode: mode } + saveSettings(next) + return next + }) + }, []) + + return ( + + {children} + + ) +} + +export function useSettings(): SettingsCtx { + const ctx = useContext(SettingsContext) + if (!ctx) throw new Error('useSettings must be used inside SettingsProvider') + return ctx +} diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index 81c4c6a..6f4a890 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -1,10 +1,13 @@ import React from 'react' import ReactDOM from 'react-dom/client' import App from './App' +import { SettingsProvider } from './context/SettingsContext' import './styles/global.css' ReactDOM.createRoot(document.getElementById('root')!).render( - + + + ) diff --git a/src/renderer/src/screens/AppShell.tsx b/src/renderer/src/screens/AppShell.tsx index 69e6fbd..2d78662 100644 --- a/src/renderer/src/screens/AppShell.tsx +++ b/src/renderer/src/screens/AppShell.tsx @@ -5,6 +5,7 @@ import BrowseScreen from './BrowseScreen' import CategoryScreen from './CategoryScreen' import ChannelScreen from './ChannelScreen' import FollowingScreen from './FollowingScreen' +import SettingsScreen from './SettingsScreen' import type { FollowedChannelInfo, GameInfo } from '../types/t4sd' type Region = 'sidebar' | 'main' @@ -115,6 +116,9 @@ export default function AppShell({ onLogout }: Props): JSX.Element { onLogout={onLogout} /> )} + {tab === 'settings' && ( + + )} )}
diff --git a/src/renderer/src/screens/BrowseScreen.tsx b/src/renderer/src/screens/BrowseScreen.tsx index 8dd8916..4ea4bca 100644 --- a/src/renderer/src/screens/BrowseScreen.tsx +++ b/src/renderer/src/screens/BrowseScreen.tsx @@ -1,4 +1,5 @@ import { useCallback, useEffect, useRef, useState } from 'react' +import LanguageBadge from '../components/LanguageBadge' import type { FollowedChannelInfo, GameInfo } from '../types/t4sd' interface Props { @@ -244,6 +245,7 @@ export default function BrowseScreen({ {ch.viewerCount !== undefined && ( {formatViewers(ch.viewerCount)} )} + {ch.broadcasterName} {ch.gameName && ( diff --git a/src/renderer/src/screens/SettingsScreen.tsx b/src/renderer/src/screens/SettingsScreen.tsx new file mode 100644 index 0000000..cba847a --- /dev/null +++ b/src/renderer/src/screens/SettingsScreen.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from 'react' +import { SettingsIcon } from '../components/Icons' +import { type StreamBadgeMode, useSettings } from '../context/SettingsContext' + +interface Props { + hasFocus: boolean + onRequestSidebar: () => void +} + +interface Option { + mode: StreamBadgeMode + label: string + preview: string +} + +const OPTIONS: Option[] = [ + { mode: 'off', label: 'Aus', preview: '— kein Badge' }, + { mode: 'language', label: 'Nur Sprach-Kürzel', preview: 'z.B. DE EN PT' }, + { mode: 'flag', label: 'Nur Flagge', preview: 'z.B. 🇩🇪 🇺🇸 🇧🇷' }, + { mode: 'both', label: 'Beides (Flagge + Kürzel)', preview: 'z.B. 🇩🇪 DE 🇺🇸 EN' } +] + +export default function SettingsScreen({ hasFocus, onRequestSidebar }: Props): JSX.Element { + const { settings, setStreamBadgeMode } = useSettings() + const [focusedIndex, setFocusedIndex] = useState(() => + Math.max(0, OPTIONS.findIndex((o) => o.mode === settings.streamBadgeMode)) + ) + + useEffect(() => { + if (!hasFocus) return + const onKey = (e: KeyboardEvent): void => { + switch (e.key) { + case 'ArrowUp': + e.preventDefault() + setFocusedIndex((i) => Math.max(0, i - 1)) + break + case 'ArrowDown': + e.preventDefault() + setFocusedIndex((i) => Math.min(OPTIONS.length - 1, i + 1)) + break + case 'ArrowLeft': + e.preventDefault() + onRequestSidebar() + break + case 'Enter': { + e.preventDefault() + const opt = OPTIONS[focusedIndex] + if (opt) setStreamBadgeMode(opt.mode) + break + } + } + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [hasFocus, focusedIndex, onRequestSidebar, setStreamBadgeMode]) + + return ( +
+
+

Einstellungen

+
+ +
+

Sprach-Anzeige auf Stream-Karten

+

+ Twitch liefert nur die Stream-Sprache — kein Land. Flaggen-Zuordnung + ist eine Annäherung basierend auf der dominanten Twitch-Nutzerbase + (z.B. Portugiesisch → 🇧🇷 Brasilien). +

+ +
+ {OPTIONS.map((opt, i) => { + const isActive = settings.streamBadgeMode === opt.mode + const isFocused = hasFocus && i === focusedIndex + return ( + + ) + })} +
+
+ + {!hasFocus && ( +
+ +
+ )} +
+ ) +} diff --git a/src/renderer/src/styles/global.css b/src/renderer/src/styles/global.css index cd94807..9bb4da2 100644 --- a/src/renderer/src/styles/global.css +++ b/src/renderer/src/styles/global.css @@ -989,6 +989,115 @@ body, color: var(--color-fg-dim); } +/* ── Language Badge (unten-rechts im Thumbnail) ───── */ +.card__language { + position: absolute; + bottom: 10px; + right: 10px; + background: rgba(0, 0, 0, 0.72); + color: #fff; + font-size: 15px; + font-weight: 700; + padding: 3px 7px; + border-radius: 4px; + letter-spacing: 0.5px; + line-height: 1.2; +} + +.browse-stream-card__language { + position: absolute; + bottom: 6px; + right: 7px; + background: rgba(0, 0, 0, 0.72); + color: #fff; + font-size: 14px; + font-weight: 700; + padding: 2px 6px; + border-radius: 4px; + letter-spacing: 0.5px; + line-height: 1.2; +} + +/* ── Settings Screen ──────────────────────────────── */ +.settings-section { + display: flex; + flex-direction: column; + gap: 12px; + max-width: 680px; +} + +.settings-section__title { + margin: 0; + font-size: 24px; + font-weight: 700; +} + +.settings-section__hint { + margin: 0; + font-size: 16px; + color: var(--color-fg-dim); + line-height: 1.5; +} + +.settings-options { + display: flex; + flex-direction: column; + gap: 8px; + margin-top: 8px; +} + +.settings-option { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + border-radius: var(--radius); + background: var(--color-bg-elev); + border: 2px solid transparent; + color: var(--color-fg); + font: inherit; + font-size: 20px; + cursor: pointer; + text-align: left; + transition: background 0.1s, border-color 0.1s; +} + +.settings-option:hover { + background: var(--color-bg-elev2); +} + +.settings-option--active { + background: rgba(145, 71, 255, 0.12); +} + +.settings-option--active .settings-option__radio { + color: var(--color-accent); +} + +.settings-option--focused { + border-color: var(--color-focus); + box-shadow: 0 0 0 2px rgba(191, 148, 255, 0.25); +} + +.settings-option__radio { + font-size: 20px; + color: var(--color-fg-dim); + flex-shrink: 0; + width: 24px; +} + +.settings-option__label { + flex: 1; + font-weight: 600; +} + +.settings-option__preview { + font-size: 17px; + color: var(--color-fg-dim); + font-weight: 400; + white-space: nowrap; +} + /* ── Account Screen ───────────────────────────────── */ .account { flex: 1; diff --git a/src/renderer/src/types/t4sd.d.ts b/src/renderer/src/types/t4sd.d.ts index 1a7178a..390cb57 100644 --- a/src/renderer/src/types/t4sd.d.ts +++ b/src/renderer/src/types/t4sd.d.ts @@ -27,6 +27,7 @@ export interface FollowedChannelInfo { viewerCount?: number thumbnailUrl?: string startedAt?: string + language?: string } export interface GameInfo { diff --git a/todo.md b/todo.md index a44cec8..fbf5a91 100644 --- a/todo.md +++ b/todo.md @@ -101,7 +101,23 @@ - [x] `CategoryScreen.tsx`: Streams-Grid pro Game - [x] `AppShell.tsx`: `selectedCategory`-Routing - [x] `global.css`: Browse/Game-Card-Styles -- [ ] **User:** End-to-End-Verifikation (Browse laden, Live starten, Kategorie-Drilldown) +- [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`: `` 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 - [ ] `flatpak/tv.twitch4steamdeck.App.yml` Manifest (Runtime, Module, Permissions)