mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
Phase 5.6 abgeschlossen
- Seitenmenu um Einstellungen ergänzt - Sprach-Badge und Flaggen anzeige im Thumbnail des Streams anzeigbar. - Sprach und Flaggen Einstellungen hinzugefügt.
This commit is contained in:
+52
-9
@@ -1,9 +1,9 @@
|
|||||||
# Twitch4SteamDeck — Fortschritts-Snapshot
|
# 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`.
|
> **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:
|
#### Neue/geänderte Dateien:
|
||||||
- `src/main/twitch/types.ts` — `HelixGame`, `GameInfo`
|
- `src/main/twitch/types.ts` — `HelixGame`, `GameInfo`, `game_id`+`language` in `HelixStream`
|
||||||
- `src/main/twitch/helixClient.ts` — `getTopGames()`, `getTopStreams(gameId?)`
|
- `src/main/twitch/helixClient.ts` — `getTopGames(limit, cursor?)`, `getTopStreams(opts?)`
|
||||||
- `src/main/ipc/handlers.ts` — `twitch:get-top-games`, `twitch:get-top-streams`
|
- `src/main/ipc/handlers.ts` — `twitch:get-top-games`, `twitch:get-top-streams`
|
||||||
- `src/preload/index.ts` — `GameInfo`-Typ + neue API-Methoden
|
- `src/preload/index.ts` — `GameInfo`-Typ + neue API-Methoden
|
||||||
- `src/renderer/src/types/t4sd.d.ts` — `GameInfo`, T4sdApi erweitert
|
- `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/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/BrowseScreen.tsx` — Shelf (Top Live) + Kategorien-Grid
|
||||||
- `src/renderer/src/screens/CategoryScreen.tsx` — neu
|
- `src/renderer/src/screens/CategoryScreen.tsx` — neu: Streams-Grid pro Kategorie
|
||||||
- `src/renderer/src/screens/AppShell.tsx` — `selectedCategory`-Routing
|
- `src/renderer/src/screens/AppShell.tsx` — `selectedCategory`-Routing
|
||||||
- `src/renderer/src/styles/global.css` — Browse/Game-Card-Styles
|
|
||||||
|
|
||||||
#### Key-Mapping Browse + Category:
|
#### Key-Mapping Browse + Category:
|
||||||
- **Enter / A**: Stream → direkt Live starten · Kategorie → Drilldown
|
- **Enter / A**: Stream → direkt Live starten · Kategorie → Drilldown
|
||||||
@@ -87,6 +86,49 @@ Verifiziert 2026-04-08.
|
|||||||
- **Y**: Refresh
|
- **Y**: Refresh
|
||||||
- **Escape / B**: Category → zurück zu Browse · Browse → Sidebar
|
- **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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Nächster Schritt: Phase 6 — Flatpak-Packaging
|
## 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)
|
- 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
|
- better-sqlite3 muss für Linux/ARM64 (Steam Deck) cross-compiliert werden
|
||||||
- Flatpak-Manifest: `flatpak/tv.twitch4steamdeck.App.yml`
|
- Flatpak-Manifest: `flatpak/tv.twitch4steamdeck.App.yml`
|
||||||
|
- **Flaggen-Emojis erst hier testbar** (Noto Color Emoji auf Linux)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ mit Resume. Verteilung als Flatpak.
|
|||||||
3. Live-Stream-Wiedergabe werbefrei (Streamlink → mpv)
|
3. Live-Stream-Wiedergabe werbefrei (Streamlink → mpv)
|
||||||
4. VOD-Browsing pro Kanal
|
4. VOD-Browsing pro Kanal
|
||||||
5. VOD-Wiedergabe werbefrei
|
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.
|
**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.
|
Wiedergabe läuft **außerhalb** Electron in einem mpv-Vollbildfenster.
|
||||||
Electron startet/stoppt mpv, sendet Befehle, lauscht auf `time-pos` für Verlaufsspeicherung.
|
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)
|
## Verzeichnisstruktur (Ist-Stand + Soll)
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -96,20 +111,25 @@ twitch4steamdeck/
|
|||||||
│ └── src/
|
│ └── src/
|
||||||
│ ├── main.tsx ✅
|
│ ├── main.tsx ✅
|
||||||
│ ├── App.tsx ✅ (Routing: LoginScreen / AppShell)
|
│ ├── App.tsx ✅ (Routing: LoginScreen / AppShell)
|
||||||
|
│ ├── context/
|
||||||
|
│ │ └── SettingsContext.tsx ✅ Phase 5.6
|
||||||
│ ├── screens/
|
│ ├── screens/
|
||||||
│ │ ├── LoginScreen.tsx ✅
|
│ │ ├── LoginScreen.tsx ✅
|
||||||
│ │ ├── AppShell.tsx ✅ (Sidebar + Main-Content, Region-Nav)
|
│ │ ├── AppShell.tsx ✅ (Sidebar + Main-Content, Region-Nav)
|
||||||
│ │ ├── FollowingScreen.tsx ✅ (ersetzt die ursprünglich geplante HomeScreen.tsx)
|
│ │ ├── FollowingScreen.tsx ✅
|
||||||
│ │ ├── BrowseScreen.tsx ✅ (Platzhalter)
|
│ │ ├── BrowseScreen.tsx ✅ Phase 5.5 (Shelf + Kategorien-Grid)
|
||||||
|
│ │ ├── CategoryScreen.tsx ✅ Phase 5.5 (Streams pro Kategorie)
|
||||||
│ │ ├── AccountScreen.tsx ✅ (Platzhalter + Logout)
|
│ │ ├── AccountScreen.tsx ✅ (Platzhalter + Logout)
|
||||||
|
│ │ ├── SettingsScreen.tsx ✅ Phase 5.6
|
||||||
│ │ ├── ChannelScreen.tsx ✅ Phase 3
|
│ │ ├── ChannelScreen.tsx ✅ Phase 3
|
||||||
│ │ └── PlayerLaunchScreen.tsx — entfällt (in ChannelScreen integriert)
|
│ │ └── PlayerLaunchScreen.tsx — entfällt (in ChannelScreen integriert)
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── Sidebar.tsx ✅
|
│ │ ├── Sidebar.tsx ✅ (4 Tabs inkl. Einstellungen)
|
||||||
│ │ ├── Icons.tsx ✅ (inline SVG: Heart/Compass/User)
|
│ │ ├── Icons.tsx ✅ (Heart/Compass/User/Settings)
|
||||||
│ │ └── FocusableCard.tsx ✅ (Block-Layout)
|
│ │ ├── FocusableCard.tsx ✅ (mit LanguageBadge)
|
||||||
|
│ │ └── LanguageBadge.tsx ✅ Phase 5.6
|
||||||
│ ├── input/
|
│ ├── input/
|
||||||
│ │ └── gamepad.ts ✅
|
│ │ └── gamepad.ts ✅ (A/B/X/Y + DPad gemappt)
|
||||||
│ ├── types/
|
│ ├── types/
|
||||||
│ │ └── t4sd.d.ts ✅
|
│ │ └── t4sd.d.ts ✅
|
||||||
│ └── styles/
|
│ └── 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 /channels/followed?user_id=…` (paginiert via `after`, safety-cap 500)
|
||||||
- `GET /streams?user_id=…` (batched, max 100 IDs/Call)
|
- `GET /streams?user_id=…` (batched, max 100 IDs/Call)
|
||||||
- `GET /videos?user_id=…&type=archive` — `getVideos()` ✅
|
- `GET /videos?user_id=…&type=archive` — `getVideos()` ✅
|
||||||
- `GET /games/top` — `getTopGames()` ✅ Phase 5.5
|
- `GET /games/top?first=40&after=<cursor>` — `getTopGames(limit, cursor?)` ✅ Phase 5.5
|
||||||
- `GET /streams` (global + game_id) — `getTopStreams()` ✅ 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>`.
|
Header: `Client-Id: <id>`, `Authorization: Bearer <access_token>`.
|
||||||
`getFollowedWithLiveStatus()` merged Followed + Live + Avatare zu `FollowedChannelInfo[]` und
|
`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.
|
- Schrift: min. 22px Body, 36px+ Headlines.
|
||||||
- Mapping: D-Pad/Stick = Navigation, A = Aktivieren, B = Zurück, Y = Refresh, Start = Menü, L1/R1 = Tab.
|
- 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)
|
### Flatpak (⏳ Phase 6)
|
||||||
- Runtime: `org.freedesktop.Platform//23.08`, `org.electronjs.Electron2.BaseApp`
|
- Runtime: `org.freedesktop.Platform//23.08`, `org.electronjs.Electron2.BaseApp`
|
||||||
- Module: app-bundle, `mpv`, `streamlink` (pip), evtl. `streamlink-ttvlol`
|
- 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)
|
## Offene Punkte (Post-MVP)
|
||||||
- Eigene `client_id` im Build vor Release.
|
- Eigene `client_id` im Build vor Release.
|
||||||
- Chat / Emotes (BTTV/FFZ/7TV).
|
- 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).
|
- README + ToS-Disclaimer (Twitch-Werbeumgehung kann gegen ToS verstoßen).
|
||||||
|
- Weitere Einstellungen (Qualität, Theme, ...) — Infrastruktur mit SettingsContext bereit.
|
||||||
|
|||||||
@@ -195,7 +195,8 @@ export class HelixClient {
|
|||||||
gameName: s.game_name,
|
gameName: s.game_name,
|
||||||
viewerCount: s.viewer_count,
|
viewerCount: s.viewer_count,
|
||||||
thumbnailUrl: s.thumbnail_url.replace('{width}', '440').replace('{height}', '248'),
|
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) ?? '',
|
profileImageUrl: avatarMap.get(f.broadcaster_id) ?? '',
|
||||||
isLive: !!stream,
|
isLive: !!stream,
|
||||||
streamTitle: stream?.title,
|
streamTitle: stream?.title,
|
||||||
|
gameId: stream?.game_id,
|
||||||
gameName: stream?.game_name,
|
gameName: stream?.game_name,
|
||||||
viewerCount: stream?.viewer_count,
|
viewerCount: stream?.viewer_count,
|
||||||
thumbnailUrl: stream
|
thumbnailUrl: stream
|
||||||
@@ -226,7 +228,8 @@ export class HelixClient {
|
|||||||
.replace('{width}', '440')
|
.replace('{width}', '440')
|
||||||
.replace('{height}', '248')
|
.replace('{height}', '248')
|
||||||
: undefined,
|
: undefined,
|
||||||
startedAt: stream?.started_at
|
startedAt: stream?.started_at,
|
||||||
|
language: stream?.language
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export interface HelixStream {
|
|||||||
viewer_count: number
|
viewer_count: number
|
||||||
thumbnail_url: string
|
thumbnail_url: string
|
||||||
started_at: string
|
started_at: string
|
||||||
|
language: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HelixPaginatedResponse<T> {
|
export interface HelixPaginatedResponse<T> {
|
||||||
@@ -81,4 +82,5 @@ export interface FollowedChannelInfo {
|
|||||||
/** 440×248 thumbnail URL (only set when live) */
|
/** 440×248 thumbnail URL (only set when live) */
|
||||||
thumbnailUrl?: string
|
thumbnailUrl?: string
|
||||||
startedAt?: string
|
startedAt?: string
|
||||||
|
language?: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface FollowedChannelInfo {
|
|||||||
viewerCount?: number
|
viewerCount?: number
|
||||||
thumbnailUrl?: string
|
thumbnailUrl?: string
|
||||||
startedAt?: string
|
startedAt?: string
|
||||||
|
language?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaybackEvent {
|
export interface PlaybackEvent {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
import { useEffect, useRef } from 'react'
|
||||||
import type { FollowedChannelInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo } from '../types/t4sd'
|
||||||
|
import LanguageBadge from './LanguageBadge'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
channel: FollowedChannelInfo
|
channel: FollowedChannelInfo
|
||||||
@@ -40,6 +41,7 @@ export default function FocusableCard({ channel, focused, onFocus, onSelect }: P
|
|||||||
{channel.viewerCount !== undefined && (
|
{channel.viewerCount !== undefined && (
|
||||||
<span className="card__viewers">{formatViewers(channel.viewerCount)}</span>
|
<span className="card__viewers">{formatViewers(channel.viewerCount)}</span>
|
||||||
)}
|
)}
|
||||||
|
<LanguageBadge language={channel.language} />
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="card__thumb-offline">
|
<div className="card__thumb-offline">
|
||||||
|
|||||||
@@ -36,3 +36,12 @@ export function UserIcon(props: IconProps): JSX.Element {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function SettingsIcon(props: IconProps): JSX.Element {
|
||||||
|
return (
|
||||||
|
<svg {...base} {...props}>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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<string, string> = {
|
||||||
|
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 <span className={className}>{content}</span>
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useEffect, useRef } from 'react'
|
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 {
|
interface SidebarItemDef {
|
||||||
key: TabKey
|
key: TabKey
|
||||||
@@ -12,7 +12,8 @@ interface SidebarItemDef {
|
|||||||
export const SIDEBAR_ITEMS: SidebarItemDef[] = [
|
export const SIDEBAR_ITEMS: SidebarItemDef[] = [
|
||||||
{ key: 'following', label: 'Du folgst', Icon: HeartIcon },
|
{ key: 'following', label: 'Du folgst', Icon: HeartIcon },
|
||||||
{ key: 'browse', label: 'Durchsuchen', Icon: CompassIcon },
|
{ 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 {
|
interface Props {
|
||||||
|
|||||||
@@ -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<AppSettings>
|
||||||
|
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<SettingsCtx | null>(null)
|
||||||
|
|
||||||
|
export function SettingsProvider({ children }: { children: React.ReactNode }): JSX.Element {
|
||||||
|
const [settings, setSettings] = useState<AppSettings>(loadSettings)
|
||||||
|
|
||||||
|
const setStreamBadgeMode = useCallback((mode: StreamBadgeMode) => {
|
||||||
|
setSettings((prev) => {
|
||||||
|
const next = { ...prev, streamBadgeMode: mode }
|
||||||
|
saveSettings(next)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsContext.Provider value={{ settings, setStreamBadgeMode }}>
|
||||||
|
{children}
|
||||||
|
</SettingsContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSettings(): SettingsCtx {
|
||||||
|
const ctx = useContext(SettingsContext)
|
||||||
|
if (!ctx) throw new Error('useSettings must be used inside SettingsProvider')
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
import { SettingsProvider } from './context/SettingsContext'
|
||||||
import './styles/global.css'
|
import './styles/global.css'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<SettingsProvider>
|
||||||
|
<App />
|
||||||
|
</SettingsProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import BrowseScreen from './BrowseScreen'
|
|||||||
import CategoryScreen from './CategoryScreen'
|
import CategoryScreen from './CategoryScreen'
|
||||||
import ChannelScreen from './ChannelScreen'
|
import ChannelScreen from './ChannelScreen'
|
||||||
import FollowingScreen from './FollowingScreen'
|
import FollowingScreen from './FollowingScreen'
|
||||||
|
import SettingsScreen from './SettingsScreen'
|
||||||
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
||||||
|
|
||||||
type Region = 'sidebar' | 'main'
|
type Region = 'sidebar' | 'main'
|
||||||
@@ -115,6 +116,9 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
onLogout={onLogout}
|
onLogout={onLogout}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{tab === 'settings' && (
|
||||||
|
<SettingsScreen hasFocus={mainFocus} onRequestSidebar={requestSidebar} />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
import LanguageBadge from '../components/LanguageBadge'
|
||||||
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -244,6 +245,7 @@ export default function BrowseScreen({
|
|||||||
{ch.viewerCount !== undefined && (
|
{ch.viewerCount !== undefined && (
|
||||||
<span className="card__viewers">{formatViewers(ch.viewerCount)}</span>
|
<span className="card__viewers">{formatViewers(ch.viewerCount)}</span>
|
||||||
)}
|
)}
|
||||||
|
<LanguageBadge language={ch.language} className="browse-stream-card__language" />
|
||||||
</div>
|
</div>
|
||||||
<span className="browse-stream-card__name">{ch.broadcasterName}</span>
|
<span className="browse-stream-card__name">{ch.broadcasterName}</span>
|
||||||
{ch.gameName && (
|
{ch.gameName && (
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div className="screen">
|
||||||
|
<header className="screen__header">
|
||||||
|
<h2 className="screen__title">Einstellungen</h2>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="settings-section">
|
||||||
|
<h3 className="settings-section__title">Sprach-Anzeige auf Stream-Karten</h3>
|
||||||
|
<p className="settings-section__hint">
|
||||||
|
Twitch liefert nur die Stream-Sprache — kein Land. Flaggen-Zuordnung
|
||||||
|
ist eine Annäherung basierend auf der dominanten Twitch-Nutzerbase
|
||||||
|
(z.B. Portugiesisch → 🇧🇷 Brasilien).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="settings-options">
|
||||||
|
{OPTIONS.map((opt, i) => {
|
||||||
|
const isActive = settings.streamBadgeMode === opt.mode
|
||||||
|
const isFocused = hasFocus && i === focusedIndex
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={opt.mode}
|
||||||
|
className={[
|
||||||
|
'settings-option',
|
||||||
|
isActive ? 'settings-option--active' : '',
|
||||||
|
isFocused ? 'settings-option--focused' : ''
|
||||||
|
].filter(Boolean).join(' ')}
|
||||||
|
onClick={() => {
|
||||||
|
setFocusedIndex(i)
|
||||||
|
setStreamBadgeMode(opt.mode)
|
||||||
|
}}
|
||||||
|
tabIndex={isFocused ? 0 : -1}
|
||||||
|
>
|
||||||
|
<span className="settings-option__radio" aria-hidden="true">
|
||||||
|
{isActive ? '●' : '○'}
|
||||||
|
</span>
|
||||||
|
<span className="settings-option__label">{opt.label}</span>
|
||||||
|
<span className="settings-option__preview">{opt.preview}</span>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!hasFocus && (
|
||||||
|
<div className="screen__state" style={{ flex: 0 }}>
|
||||||
|
<SettingsIcon style={{ width: 48, height: 48, opacity: 0.2 }} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -989,6 +989,115 @@ body,
|
|||||||
color: var(--color-fg-dim);
|
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 Screen ───────────────────────────────── */
|
||||||
.account {
|
.account {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|||||||
Vendored
+1
@@ -27,6 +27,7 @@ export interface FollowedChannelInfo {
|
|||||||
viewerCount?: number
|
viewerCount?: number
|
||||||
thumbnailUrl?: string
|
thumbnailUrl?: string
|
||||||
startedAt?: string
|
startedAt?: string
|
||||||
|
language?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GameInfo {
|
export interface GameInfo {
|
||||||
|
|||||||
@@ -101,7 +101,23 @@
|
|||||||
- [x] `CategoryScreen.tsx`: Streams-Grid pro Game
|
- [x] `CategoryScreen.tsx`: Streams-Grid pro Game
|
||||||
- [x] `AppShell.tsx`: `selectedCategory`-Routing
|
- [x] `AppShell.tsx`: `selectedCategory`-Routing
|
||||||
- [x] `global.css`: Browse/Game-Card-Styles
|
- [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`: `<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
|
## Phase 6 — Flatpak-Packaging
|
||||||
- [ ] `flatpak/tv.twitch4steamdeck.App.yml` Manifest (Runtime, Module, Permissions)
|
- [ ] `flatpak/tv.twitch4steamdeck.App.yml` Manifest (Runtime, Module, Permissions)
|
||||||
|
|||||||
Reference in New Issue
Block a user