mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +01:00
style: translate all source code comments to English
German inline comments alongside English identifiers create a language mismatch that makes the code harder to read for anyone not fluent in German. All 28 affected comments across 14 files are now in English, consistent with the rest of the project's written artifacts.
This commit is contained in:
@@ -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 { EventEmitter } from 'node:events'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export async function pollForToken(opts: PollOptions): Promise<PollOutcome> {
|
|||||||
return { kind: 'authorized', token }
|
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 = {}
|
let payload: ErrorResponse = {}
|
||||||
try {
|
try {
|
||||||
const json = (await res.json()) as unknown
|
const json = (await res.json()) as unknown
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// Verschlüsselte Persistenz für Twitch-Tokens.
|
// Encrypted persistence for Twitch tokens.
|
||||||
// Nutzt Electron safeStorage (OS-Keystore unter der Haube) — keine native Module nötig.
|
// Uses Electron safeStorage (OS keystore under the hood) — no native modules required.
|
||||||
|
|
||||||
import { app, safeStorage } from 'electron'
|
import { app, safeStorage } from 'electron'
|
||||||
import { promises as fs } from 'node:fs'
|
import { promises as fs } from 'node:fs'
|
||||||
@@ -27,8 +27,8 @@ export async function saveTokens(tokens: StoredTokens): Promise<void> {
|
|||||||
const encrypted = safeStorage.encryptString(json)
|
const encrypted = safeStorage.encryptString(json)
|
||||||
await fs.writeFile(path, encrypted)
|
await fs.writeFile(path, encrypted)
|
||||||
} else {
|
} else {
|
||||||
// Fallback: unverschlüsselt (z. B. headless Linux ohne Keyring).
|
// Fallback: plaintext (e.g. headless Linux without a keyring).
|
||||||
// Datei nur für aktuellen User lesbar machen.
|
// Restrict file permissions to the current user.
|
||||||
await fs.writeFile(path, json, { mode: 0o600 })
|
await fs.writeFile(path, json, { mode: 0o600 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ export async function loadTokens(): Promise<StoredTokens | null> {
|
|||||||
try {
|
try {
|
||||||
json = safeStorage.decryptString(raw)
|
json = safeStorage.decryptString(raw)
|
||||||
} catch {
|
} 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
|
return null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+5
-4
@@ -9,11 +9,12 @@ import { getHlsCacheEnabled } from './prefs/hlsCachePref'
|
|||||||
|
|
||||||
const isDev = !app.isPackaged
|
const isDev = !app.isPackaged
|
||||||
|
|
||||||
// Erlaubt video.play() ohne vorangehende User-Geste (nötig, weil play() in hls.js-Callbacks
|
// Allows video.play() without a prior user gesture — play() is called from hls.js callbacks
|
||||||
// aufgerufen wird, die asynchron weit nach dem ursprünglichen Click-Event stattfinden).
|
// that fire asynchronously long after the original click event.
|
||||||
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
||||||
// Explizite Cache-Größe verhindert Chromiums dynamische Größenberechnung, die bei
|
// Explicit cache size prevents Chromium's dynamic sizing, which causes "Invalid cache (current)
|
||||||
// wechselndem freiem Speicherplatz zu "Invalid cache (current) size"-Fehlern führt.
|
// size" errors when available disk space fluctuates.
|
||||||
|
|
||||||
app.commandLine.appendSwitch('disk-cache-size', String(500 * 1024 * 1024))
|
app.commandLine.appendSwitch('disk-cache-size', String(500 * 1024 * 1024))
|
||||||
|
|
||||||
function createWindow(): BrowserWindow {
|
function createWindow(): BrowserWindow {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ const INPUT_EVENT_SIZE = 24
|
|||||||
const EV_KEY = 0x01
|
const EV_KEY = 0x01
|
||||||
const EV_ABS = 0x03
|
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_SOUTH = 0x130
|
||||||
const BTN_EAST = 0x131
|
const BTN_EAST = 0x131
|
||||||
const BTN_NORTH = 0x133
|
const BTN_NORTH = 0x133
|
||||||
@@ -70,10 +70,10 @@ const BUTTON_MAP: Record<number, string> = {
|
|||||||
[BTN_TR2]: 'r2',
|
[BTN_TR2]: 'r2',
|
||||||
}
|
}
|
||||||
|
|
||||||
const STICK_THRESHOLD = 16384 // ~50% von 32767
|
const STICK_THRESHOLD = 16384 // ~50% of 32767
|
||||||
const REPEAT_INITIAL_MS = 400
|
const REPEAT_INITIAL_MS = 400
|
||||||
const REPEAT_INTERVAL_MS = 150
|
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])
|
const NO_REPEAT_ABS_CODES = new Set([ABS_Z, ABS_RZ])
|
||||||
|
|
||||||
interface AxisState {
|
interface AxisState {
|
||||||
@@ -96,7 +96,7 @@ function findEventDevice(jsDevicePath: string): string | null {
|
|||||||
const eventEntry = entries.find((e) => /^event\d+$/.test(e))
|
const eventEntry = entries.find((e) => /^event\d+$/.test(e))
|
||||||
if (eventEntry) return `/dev/input/${eventEntry}`
|
if (eventEntry) return `/dev/input/${eventEntry}`
|
||||||
} catch {
|
} catch {
|
||||||
// Kein /sys-Zugriff oder Device verschwunden
|
// No /sys access or device has disappeared
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -400,7 +400,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () =>
|
|||||||
console.log(`[gamepad] geöffnet (evdev): ${jsPath} → ${eventPath}`)
|
console.log(`[gamepad] geöffnet (evdev): ${jsPath} → ${eventPath}`)
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
reader = new JoystickFallbackReader(jsPath, onKey)
|
||||||
if (reader.start()) {
|
if (reader.start()) {
|
||||||
readers.set(jsPath, reader)
|
readers.set(jsPath, reader)
|
||||||
@@ -417,7 +417,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// /dev/input nicht zugänglich
|
// /dev/input not accessible
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ export function registerIpcHandlers(
|
|||||||
)
|
)
|
||||||
ipcMain.handle(IPC.playbackStop, () => playback.stop())
|
ipcMain.handle(IPC.playbackStop, () => playback.stop())
|
||||||
ipcMain.handle(IPC.playbackPause, () => {
|
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()
|
for (const win of BrowserWindow.getAllWindows()) win.focus()
|
||||||
})
|
})
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function getStreamUrl(url: string, quality: string): Promise<string> {
|
|||||||
proc.stdout?.on('data', (d: Buffer) => { out += d.toString() })
|
proc.stdout?.on('data', (d: Buffer) => { out += d.toString() })
|
||||||
proc.on('error', reject)
|
proc.on('error', reject)
|
||||||
proc.on('exit', (code) => {
|
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) ?? ''
|
const hlsUrl = out.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('https://')).at(-1) ?? ''
|
||||||
if (code === 0 && hlsUrl) {
|
if (code === 0 && hlsUrl) {
|
||||||
resolve(hlsUrl)
|
resolve(hlsUrl)
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ export class HelixClient {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Live-Kanäle zuerst, dann alphabetisch
|
// Live channels first, then alphabetical
|
||||||
return channels.sort((a, b) => {
|
return channels.sort((a, b) => {
|
||||||
if (a.isLive !== b.isLive) return a.isLive ? -1 : 1
|
if (a.isLive !== b.isLive) return a.isLive ? -1 : 1
|
||||||
return a.broadcasterName.localeCompare(b.broadcasterName)
|
return a.broadcasterName.localeCompare(b.broadcasterName)
|
||||||
|
|||||||
@@ -70,12 +70,12 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// HLS-Player initialisieren und URL laden
|
// Initialize hls.js and load the URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const video = videoRef.current
|
const video = videoRef.current
|
||||||
if (!video) return
|
if (!video) return
|
||||||
|
|
||||||
// Bestehende hls.js-Instanz zerstören
|
// Tear down any existing hls.js instance
|
||||||
if (hlsRef.current) {
|
if (hlsRef.current) {
|
||||||
hlsRef.current.destroy()
|
hlsRef.current.destroy()
|
||||||
hlsRef.current = null
|
hlsRef.current = null
|
||||||
@@ -87,7 +87,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
|
|||||||
const hls = new Hls({ enableWorker: false })
|
const hls = new Hls({ enableWorker: false })
|
||||||
hlsRef.current = hls
|
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)
|
hls.attachMedia(video)
|
||||||
console.log('[VideoPlayer] attachMedia aufgerufen')
|
console.log('[VideoPlayer] attachMedia aufgerufen')
|
||||||
|
|
||||||
@@ -101,9 +101,8 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
|
|||||||
if (startPosition > 0 && !isLive) {
|
if (startPosition > 0 && !isLive) {
|
||||||
video.currentTime = startPosition
|
video.currentTime = startPosition
|
||||||
}
|
}
|
||||||
// Mit der korrekten Reihenfolge (attachMedia → MEDIA_ATTACHED → loadSource)
|
// With the correct order (attachMedia → MEDIA_ATTACHED → loadSource)
|
||||||
// ist die MediaSource beim MANIFEST_PARSED-Event bereits am Video-Element —
|
// the MediaSource is already attached at MANIFEST_PARSED — play() is safe to call.
|
||||||
// play() kann direkt aufgerufen werden.
|
|
||||||
video.play().then(() => {
|
video.play().then(() => {
|
||||||
console.log('[VideoPlayer] play() erfolgreich')
|
console.log('[VideoPlayer] play() erfolgreich')
|
||||||
}).catch((err: unknown) => {
|
}).catch((err: unknown) => {
|
||||||
@@ -119,7 +118,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if (video.canPlayType('application/vnd.apple.mpegurl')) {
|
} 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
|
video.src = hlsUrl
|
||||||
if (startPosition > 0 && !isLive) {
|
if (startPosition > 0 && !isLive) {
|
||||||
video.currentTime = startPosition
|
video.currentTime = startPosition
|
||||||
@@ -138,7 +137,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [hlsUrl])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (!vodId || !durationSeconds || isLive) return
|
if (!vodId || !durationSeconds || isLive) return
|
||||||
const id = setInterval(() => {
|
const id = setInterval(() => {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ function saveSettings(s: AppSettings): void {
|
|||||||
try {
|
try {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(s))
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(s))
|
||||||
} catch {
|
} catch {
|
||||||
// localStorage nicht verfügbar — ignorieren
|
// localStorage not available — ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,11 +29,11 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
const [quitDialogOpen, setQuitDialogOpen] = useState(false)
|
const [quitDialogOpen, setQuitDialogOpen] = useState(false)
|
||||||
const [quitDialogChoice, setQuitDialogChoice] = useState<QuitChoice>('no')
|
const [quitDialogChoice, setQuitDialogChoice] = useState<QuitChoice>('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<FollowedChannelInfo | null>(null)
|
const [liveChannel, setLiveChannel] = useState<FollowedChannelInfo | null>(null)
|
||||||
const [livePosition, setLivePosition] = useState(0)
|
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({
|
const globalSession = usePlaybackSession({
|
||||||
active: !selectedChannel,
|
active: !selectedChannel,
|
||||||
onStopped: () => { setLiveChannel(null); setLivePosition(0) },
|
onStopped: () => { setLiveChannel(null); setLivePosition(0) },
|
||||||
@@ -84,7 +84,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
setSelectedChannel(ch)
|
setSelectedChannel(ch)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Direkt-Start: kein ChannelScreen — globaler Overlay startet sofort
|
// Direct start: no ChannelScreen — global overlay starts immediately
|
||||||
const handleStartLive = useCallback((ch: FollowedChannelInfo) => {
|
const handleStartLive = useCallback((ch: FollowedChannelInfo) => {
|
||||||
setLiveChannel(ch)
|
setLiveChannel(ch)
|
||||||
void startGlobalLive(ch.broadcasterLogin)
|
void startGlobalLive(ch.broadcasterLogin)
|
||||||
@@ -108,11 +108,11 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
setSelectedCategory(game)
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isGlobalPlaying) return
|
if (!isGlobalPlaying) return
|
||||||
const onKey = (e: KeyboardEvent): void => {
|
const onKey = (e: KeyboardEvent): void => {
|
||||||
// Quality-Panel hat Vorrang wenn geöffnet
|
// Quality panel takes priority when open
|
||||||
if (liveQualityPanelOpen) {
|
if (liveQualityPanelOpen) {
|
||||||
if (e.key === 'ArrowUp') {
|
if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
@@ -177,7 +177,7 @@ export default function AppShell({ onLogout }: Props): JSX.Element {
|
|||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [isGlobalPlaying, livePlayState, handleStopLive, liveQualityPanelOpen, liveAvailableQualities, liveQualityFocusedIndex, liveCurrentQuality, liveChannel, handleLiveQualityChange])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (region !== 'sidebar') return
|
if (region !== 'sidebar') return
|
||||||
const onKey = (e: KeyboardEvent): void => {
|
const onKey = (e: KeyboardEvent): void => {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export default function BrowseScreen({
|
|||||||
void load()
|
void load()
|
||||||
}, [load])
|
}, [load])
|
||||||
|
|
||||||
// Nächste Seite automatisch nachladen wenn letztes Element fokussiert wird
|
// Auto-load next page when the last item gets focus
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (focusRegion !== 'grid' || loadState !== 'ok') return
|
if (focusRegion !== 'grid' || loadState !== 'ok') return
|
||||||
if (gridIndex === topGames.length - 1 && nextCursor && !isLoadingMore) {
|
if (gridIndex === topGames.length - 1 && nextCursor && !isLoadingMore) {
|
||||||
@@ -179,7 +179,7 @@ export default function BrowseScreen({
|
|||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [hasFocus, loadState, focusRegion, shelfIndex, gridIndex, topStreams, topGames, getGridColumns, onRequestSidebar, onSelectChannel, onStartLive, onSelectCategory, load])
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (focusRegion !== 'grid') return
|
if (focusRegion !== 'grid') return
|
||||||
const grid = gridRef.current
|
const grid = gridRef.current
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
|||||||
.finally(() => setChaptersLoading(false))
|
.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({
|
const session = usePlaybackSession({
|
||||||
active: true,
|
active: true,
|
||||||
onStarted: (ev) => {
|
onStarted: (ev) => {
|
||||||
@@ -134,7 +134,7 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
|||||||
const onKey = (e: KeyboardEvent): void => {
|
const onKey = (e: KeyboardEvent): void => {
|
||||||
const isPlaying = playState === 'playing' || playState === 'starting' || playState === 'paused'
|
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) {
|
if (focusRegion === 'chapters' && chapterPanelVod) {
|
||||||
const duringPlayback = isPlaying
|
const duringPlayback = isPlaying
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
@@ -175,9 +175,9 @@ export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Während der Wiedergabe: nur Playback-Steuerung erlauben, keine Navigation
|
// During playback: allow only playback controls, block screen navigation
|
||||||
if (isPlaying) {
|
if (isPlaying) {
|
||||||
// Quality-Panel hat Vorrang wenn geöffnet
|
// Quality panel takes priority when open
|
||||||
if (qualityPanelOpen) {
|
if (qualityPanelOpen) {
|
||||||
if (e.key === 'ArrowUp') {
|
if (e.key === 'ArrowUp') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@@ -50,13 +50,13 @@ export default function FollowingScreen({
|
|||||||
if (!hasFocus) return
|
if (!hasFocus) return
|
||||||
|
|
||||||
const onKey = (e: KeyboardEvent): void => {
|
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') {
|
if (e.key === 'y' || e.key === 'Y') {
|
||||||
void load()
|
void load()
|
||||||
return
|
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 (loadState !== 'ok' || channels.length === 0) {
|
||||||
if (e.key === 'ArrowLeft') {
|
if (e.key === 'ArrowLeft') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element {
|
|||||||
const [phase, setPhase] = useState<Phase>({ kind: 'idle' })
|
const [phase, setPhase] = useState<Phase>({ kind: 'idle' })
|
||||||
const startBtnRef = useRef<HTMLButtonElement>(null)
|
const startBtnRef = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
// Initial: prüfen ob Client-ID konfiguriert ist.
|
// On mount: check whether the client ID is configured.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void window.t4sd.auth.isConfigured().then((ok) => {
|
void window.t4sd.auth.isConfigured().then((ok) => {
|
||||||
if (!ok) setPhase({ kind: 'not-configured' })
|
if (!ok) setPhase({ kind: 'not-configured' })
|
||||||
@@ -35,7 +35,7 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element {
|
|||||||
return () => window.removeEventListener('keydown', onKey)
|
return () => window.removeEventListener('keydown', onKey)
|
||||||
}, [phase])
|
}, [phase])
|
||||||
|
|
||||||
// Auth-Events aus Main abonnieren.
|
// Subscribe to auth events from the main process.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const off = window.t4sd.auth.onEvent((event) => {
|
const off = window.t4sd.auth.onEvent((event) => {
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
@@ -59,7 +59,7 @@ export default function LoginScreen({ onAuthorized }: Props): JSX.Element {
|
|||||||
return off
|
return off
|
||||||
}, [onAuthorized])
|
}, [onAuthorized])
|
||||||
|
|
||||||
// Countdown für die aktive Phase.
|
// Countdown tick for the awaiting phase.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (phase.kind !== 'awaiting') return
|
if (phase.kind !== 'awaiting') return
|
||||||
if (phase.remainingSec <= 0) return
|
if (phase.remainingSec <= 0) return
|
||||||
|
|||||||
Reference in New Issue
Block a user