diff --git a/src/main/auth/authService.ts b/src/main/auth/authService.ts index 67db128..65b7dc2 100644 --- a/src/main/auth/authService.ts +++ b/src/main/auth/authService.ts @@ -1,4 +1,4 @@ -// Orchestriert den kompletten Twitch-Login-Lifecycle und hält den aktuellen Token-Status. +// Orchestrates the full Twitch login lifecycle and tracks current token status. import { EventEmitter } from 'node:events' import { diff --git a/src/main/auth/deviceCodeFlow.ts b/src/main/auth/deviceCodeFlow.ts index 1284e7d..ffae0f1 100644 --- a/src/main/auth/deviceCodeFlow.ts +++ b/src/main/auth/deviceCodeFlow.ts @@ -92,7 +92,7 @@ export async function pollForToken(opts: PollOptions): Promise { return { kind: 'authorized', token } } - // Twitch liefert bei Pending einen 400 mit message-Feld. + // Twitch returns a 400 with a message field while the code is pending. let payload: ErrorResponse = {} try { const json = (await res.json()) as unknown diff --git a/src/main/auth/tokenStore.ts b/src/main/auth/tokenStore.ts index 1d3cf63..4adee04 100644 --- a/src/main/auth/tokenStore.ts +++ b/src/main/auth/tokenStore.ts @@ -1,5 +1,5 @@ -// Verschlüsselte Persistenz für Twitch-Tokens. -// Nutzt Electron safeStorage (OS-Keystore unter der Haube) — keine native Module nötig. +// Encrypted persistence for Twitch tokens. +// Uses Electron safeStorage (OS keystore under the hood) — no native modules required. import { app, safeStorage } from 'electron' import { promises as fs } from 'node:fs' @@ -27,8 +27,8 @@ export async function saveTokens(tokens: StoredTokens): Promise { const encrypted = safeStorage.encryptString(json) await fs.writeFile(path, encrypted) } else { - // Fallback: unverschlüsselt (z. B. headless Linux ohne Keyring). - // Datei nur für aktuellen User lesbar machen. + // Fallback: plaintext (e.g. headless Linux without a keyring). + // Restrict file permissions to the current user. await fs.writeFile(path, json, { mode: 0o600 }) } } @@ -48,7 +48,7 @@ export async function loadTokens(): Promise { try { json = safeStorage.decryptString(raw) } catch { - // Datei evtl. mit anderem Key geschrieben — als ungültig behandeln. + // File may have been written with a different key — treat as invalid. return null } } else { diff --git a/src/main/index.ts b/src/main/index.ts index aa3f8f9..089d0b3 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -9,11 +9,12 @@ import { getHlsCacheEnabled } from './prefs/hlsCachePref' const isDev = !app.isPackaged -// Erlaubt video.play() ohne vorangehende User-Geste (nötig, weil play() in hls.js-Callbacks -// aufgerufen wird, die asynchron weit nach dem ursprünglichen Click-Event stattfinden). +// Allows video.play() without a prior user gesture — play() is called from hls.js callbacks +// that fire asynchronously long after the original click event. app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required') -// Explizite Cache-Größe verhindert Chromiums dynamische Größenberechnung, die bei -// wechselndem freiem Speicherplatz zu "Invalid cache (current) size"-Fehlern führt. +// Explicit cache size prevents Chromium's dynamic sizing, which causes "Invalid cache (current) +// size" errors when available disk space fluctuates. + app.commandLine.appendSwitch('disk-cache-size', String(500 * 1024 * 1024)) function createWindow(): BrowserWindow { diff --git a/src/main/input/gamepadReader.ts b/src/main/input/gamepadReader.ts index 3deeecc..666c3cc 100644 --- a/src/main/input/gamepadReader.ts +++ b/src/main/input/gamepadReader.ts @@ -41,7 +41,7 @@ const INPUT_EVENT_SIZE = 24 const EV_KEY = 0x01 const EV_ABS = 0x03 -// Button-Codes (BTN_GAMEPAD-Bereich) – konsistent über alle Controller und Treiber +// Button codes (BTN_GAMEPAD range) — consistent across all controllers and drivers const BTN_SOUTH = 0x130 const BTN_EAST = 0x131 const BTN_NORTH = 0x133 @@ -70,10 +70,10 @@ const BUTTON_MAP: Record = { [BTN_TR2]: 'r2', } -const STICK_THRESHOLD = 16384 // ~50% von 32767 +const STICK_THRESHOLD = 16384 // ~50% of 32767 const REPEAT_INITIAL_MS = 400 const REPEAT_INTERVAL_MS = 150 -// Trigger-Achsen sollen kein Auto-Repeat auslösen (300s-Sprünge nicht wiederholen) +// Trigger axes must not fire auto-repeat (avoid repeating 300 s jumps) const NO_REPEAT_ABS_CODES = new Set([ABS_Z, ABS_RZ]) interface AxisState { @@ -96,7 +96,7 @@ function findEventDevice(jsDevicePath: string): string | null { const eventEntry = entries.find((e) => /^event\d+$/.test(e)) if (eventEntry) return `/dev/input/${eventEntry}` } catch { - // Kein /sys-Zugriff oder Device verschwunden + // No /sys access or device has disappeared } return null } @@ -400,7 +400,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () => console.log(`[gamepad] geöffnet (evdev): ${jsPath} → ${eventPath}`) } } else { - // Fallback auf altes Joystick-API wenn kein /sys-Mapping gefunden + // Fallback to legacy joystick API when no /sys mapping is found reader = new JoystickFallbackReader(jsPath, onKey) if (reader.start()) { readers.set(jsPath, reader) @@ -417,7 +417,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () => } } } catch { - // /dev/input nicht zugänglich + // /dev/input not accessible } } diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 8d5738b..3537b2c 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -85,7 +85,7 @@ export function registerIpcHandlers( ) ipcMain.handle(IPC.playbackStop, () => playback.stop()) ipcMain.handle(IPC.playbackPause, () => { - // Fokus auf Electron-Fenster zurückholen (Windows: Win Gamepad API braucht Fokus) + // Bring Electron window back into focus (Windows: Gamepad API requires window focus) for (const win of BrowserWindow.getAllWindows()) win.focus() }) ipcMain.handle( diff --git a/src/main/playback/streamlink.ts b/src/main/playback/streamlink.ts index 71d1ef0..b095c0d 100644 --- a/src/main/playback/streamlink.ts +++ b/src/main/playback/streamlink.ts @@ -68,7 +68,7 @@ export function getStreamUrl(url: string, quality: string): Promise { proc.stdout?.on('data', (d: Buffer) => { out += d.toString() }) proc.on('error', reject) proc.on('exit', (code) => { - // Nimm die letzte Zeile die mit https:// beginnt — ignoriert Info-/Warn-Zeilen + // Take the last line starting with https:// — skips info/warn lines from streamlink const hlsUrl = out.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('https://')).at(-1) ?? '' if (code === 0 && hlsUrl) { resolve(hlsUrl) diff --git a/src/main/twitch/helixClient.ts b/src/main/twitch/helixClient.ts index 913fc87..a366f6e 100644 --- a/src/main/twitch/helixClient.ts +++ b/src/main/twitch/helixClient.ts @@ -308,7 +308,7 @@ export class HelixClient { } }) - // Live-Kanäle zuerst, dann alphabetisch + // Live channels first, then alphabetical return channels.sort((a, b) => { if (a.isLive !== b.isLive) return a.isLive ? -1 : 1 return a.broadcasterName.localeCompare(b.broadcasterName) diff --git a/src/renderer/src/components/VideoPlayer.tsx b/src/renderer/src/components/VideoPlayer.tsx index 7f5d7d0..b566145 100644 --- a/src/renderer/src/components/VideoPlayer.tsx +++ b/src/renderer/src/components/VideoPlayer.tsx @@ -70,12 +70,12 @@ export const VideoPlayer = forwardRef( } })) - // HLS-Player initialisieren und URL laden + // Initialize hls.js and load the URL useEffect(() => { const video = videoRef.current if (!video) return - // Bestehende hls.js-Instanz zerstören + // Tear down any existing hls.js instance if (hlsRef.current) { hlsRef.current.destroy() hlsRef.current = null @@ -87,7 +87,7 @@ export const VideoPlayer = forwardRef( const hls = new Hls({ enableWorker: false }) hlsRef.current = hls - // Erst Media attachieren, dann Source laden (zuverlässiger in Electron/Chromium) + // Attach media first, then load the source — more reliable in Electron/Chromium hls.attachMedia(video) console.log('[VideoPlayer] attachMedia aufgerufen') @@ -101,9 +101,8 @@ export const VideoPlayer = forwardRef( if (startPosition > 0 && !isLive) { video.currentTime = startPosition } - // Mit der korrekten Reihenfolge (attachMedia → MEDIA_ATTACHED → loadSource) - // ist die MediaSource beim MANIFEST_PARSED-Event bereits am Video-Element — - // play() kann direkt aufgerufen werden. + // With the correct order (attachMedia → MEDIA_ATTACHED → loadSource) + // the MediaSource is already attached at MANIFEST_PARSED — play() is safe to call. video.play().then(() => { console.log('[VideoPlayer] play() erfolgreich') }).catch((err: unknown) => { @@ -119,7 +118,7 @@ export const VideoPlayer = forwardRef( } }) } else if (video.canPlayType('application/vnd.apple.mpegurl')) { - // Native HLS (nicht Electron, aber als Fallback) + // Native HLS (not Electron, but as a fallback) video.src = hlsUrl if (startPosition > 0 && !isLive) { video.currentTime = startPosition @@ -138,7 +137,7 @@ export const VideoPlayer = forwardRef( // eslint-disable-next-line react-hooks/exhaustive-deps }, [hlsUrl]) - // VOD-Fortschritt alle 5s an Main-Prozess melden (für SQLite History) + // Report VOD progress every 5 s to the main process (SQLite history) useEffect(() => { if (!vodId || !durationSeconds || isLive) return const id = setInterval(() => { diff --git a/src/renderer/src/context/SettingsContext.tsx b/src/renderer/src/context/SettingsContext.tsx index c78e1ce..a396757 100644 --- a/src/renderer/src/context/SettingsContext.tsx +++ b/src/renderer/src/context/SettingsContext.tsx @@ -50,7 +50,7 @@ function saveSettings(s: AppSettings): void { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(s)) } catch { - // localStorage nicht verfügbar — ignorieren + // localStorage not available — ignore } } diff --git a/src/renderer/src/screens/AppShell.tsx b/src/renderer/src/screens/AppShell.tsx index c917290..67e81b0 100644 --- a/src/renderer/src/screens/AppShell.tsx +++ b/src/renderer/src/screens/AppShell.tsx @@ -29,11 +29,11 @@ export default function AppShell({ onLogout }: Props): JSX.Element { const [quitDialogOpen, setQuitDialogOpen] = useState(false) const [quitDialogChoice, setQuitDialogChoice] = useState('no') - // Kanal-Info für den globalen Overlay — AppShell-spezifisch, kein Playback-State + // Channel info for the global overlay — AppShell-specific, not part of playback state const [liveChannel, setLiveChannel] = useState(null) const [livePosition, setLivePosition] = useState(0) - // Globale Playback-Session — aktiv wenn kein ChannelScreen offen ist + // Global playback session — active when no ChannelScreen is open const globalSession = usePlaybackSession({ active: !selectedChannel, onStopped: () => { setLiveChannel(null); setLivePosition(0) }, @@ -84,7 +84,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element { setSelectedChannel(ch) }, []) - // Direkt-Start: kein ChannelScreen — globaler Overlay startet sofort + // Direct start: no ChannelScreen — global overlay starts immediately const handleStartLive = useCallback((ch: FollowedChannelInfo) => { setLiveChannel(ch) void startGlobalLive(ch.broadcasterLogin) @@ -108,11 +108,11 @@ export default function AppShell({ onLogout }: Props): JSX.Element { setSelectedCategory(game) }, []) - // Key-Handler für den globalen Playback-Overlay (B=Stop, A=Pause, Pfeile=Seek) + // Key handler for the global playback overlay (B=Stop, A=Pause, arrows=Seek) useEffect(() => { if (!isGlobalPlaying) return const onKey = (e: KeyboardEvent): void => { - // Quality-Panel hat Vorrang wenn geöffnet + // Quality panel takes priority when open if (liveQualityPanelOpen) { if (e.key === 'ArrowUp') { e.preventDefault() @@ -177,7 +177,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element { return () => window.removeEventListener('keydown', onKey) }, [isGlobalPlaying, livePlayState, handleStopLive, liveQualityPanelOpen, liveAvailableQualities, liveQualityFocusedIndex, liveCurrentQuality, liveChannel, handleLiveQualityChange]) - // Key-Handler für die Sidebar (wenn sie den Fokus hat) + // Key handler for the sidebar (when it has focus) useEffect(() => { if (region !== 'sidebar') return const onKey = (e: KeyboardEvent): void => { diff --git a/src/renderer/src/screens/BrowseScreen.tsx b/src/renderer/src/screens/BrowseScreen.tsx index cf147bc..f045a18 100644 --- a/src/renderer/src/screens/BrowseScreen.tsx +++ b/src/renderer/src/screens/BrowseScreen.tsx @@ -73,7 +73,7 @@ export default function BrowseScreen({ void load() }, [load]) - // Nächste Seite automatisch nachladen wenn letztes Element fokussiert wird + // Auto-load next page when the last item gets focus useEffect(() => { if (focusRegion !== 'grid' || loadState !== 'ok') return if (gridIndex === topGames.length - 1 && nextCursor && !isLoadingMore) { @@ -179,7 +179,7 @@ export default function BrowseScreen({ return () => window.removeEventListener('keydown', onKey) }, [hasFocus, loadState, focusRegion, shelfIndex, gridIndex, topStreams, topGames, getGridColumns, onRequestSidebar, onSelectChannel, onStartLive, onSelectCategory, load]) - // Grid-Karte in den Viewport scrollen wenn fokussiert + // Scroll the focused grid card into the viewport useEffect(() => { if (focusRegion !== 'grid') return const grid = gridRef.current diff --git a/src/renderer/src/screens/ChannelScreen.tsx b/src/renderer/src/screens/ChannelScreen.tsx index 688c748..2479e14 100644 --- a/src/renderer/src/screens/ChannelScreen.tsx +++ b/src/renderer/src/screens/ChannelScreen.tsx @@ -62,7 +62,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { .finally(() => setChaptersLoading(false)) } - // Playback-Session — ChannelScreen ist immer der aktive Eigentümer wenn gemountet + // Playback session — ChannelScreen is always the active owner while mounted const session = usePlaybackSession({ active: true, onStarted: (ev) => { @@ -134,7 +134,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { const onKey = (e: KeyboardEvent): void => { const isPlaying = playState === 'playing' || playState === 'starting' || playState === 'paused' - // Kapitel-Panel hat oberste Priorität (gilt auch während Wiedergabe) + // Chapter panel has top priority (also applies during playback) if (focusRegion === 'chapters' && chapterPanelVod) { const duringPlayback = isPlaying if (e.key === 'Escape') { @@ -175,9 +175,9 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { return } - // Während der Wiedergabe: nur Playback-Steuerung erlauben, keine Navigation + // During playback: allow only playback controls, block screen navigation if (isPlaying) { - // Quality-Panel hat Vorrang wenn geöffnet + // Quality panel takes priority when open if (qualityPanelOpen) { if (e.key === 'ArrowUp') { e.preventDefault() diff --git a/src/renderer/src/screens/FollowingScreen.tsx b/src/renderer/src/screens/FollowingScreen.tsx index 5279326..8eb0310 100644 --- a/src/renderer/src/screens/FollowingScreen.tsx +++ b/src/renderer/src/screens/FollowingScreen.tsx @@ -50,13 +50,13 @@ export default function FollowingScreen({ if (!hasFocus) return const onKey = (e: KeyboardEvent): void => { - // Refresh über [Y] funktioniert immer + // [Y] refresh always works regardless of focus region if (e.key === 'y' || e.key === 'Y') { void load() return } - // Bei leeren / Fehler-Zuständen nur Left zum Sidebar zulassen + // In empty / error states only allow Left to reach the sidebar if (loadState !== 'ok' || channels.length === 0) { if (e.key === 'ArrowLeft') { e.preventDefault() diff --git a/src/renderer/src/screens/LoginScreen.tsx b/src/renderer/src/screens/LoginScreen.tsx index ed783c3..26520cc 100644 --- a/src/renderer/src/screens/LoginScreen.tsx +++ b/src/renderer/src/screens/LoginScreen.tsx @@ -17,7 +17,7 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element { const [phase, setPhase] = useState({ kind: 'idle' }) const startBtnRef = useRef(null) - // Initial: prüfen ob Client-ID konfiguriert ist. + // On mount: check whether the client ID is configured. useEffect(() => { void window.t4sd.auth.isConfigured().then((ok) => { if (!ok) setPhase({ kind: 'not-configured' }) @@ -35,7 +35,7 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element { return () => window.removeEventListener('keydown', onKey) }, [phase]) - // Auth-Events aus Main abonnieren. + // Subscribe to auth events from the main process. useEffect(() => { const off = window.t4sd.auth.onEvent((event) => { switch (event.kind) { @@ -59,7 +59,7 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element { return off }, [onAuthorized]) - // Countdown für die aktive Phase. + // Countdown tick for the awaiting phase. useEffect(() => { if (phase.kind !== 'awaiting') return if (phase.remainingSec <= 0) return