refactor: extract magic values into named constant modules

Scattered numeric literals had no names at their use sites, making it
impossible to know whether two identical values were intentionally the
same or coincidentally equal. Moving them to typed constants makes the
relationship explicit and ensures a single edit propagates everywhere.
This commit is contained in:
Andre
2026-05-14 12:17:47 +02:00
parent 0e38f930e7
commit bb635366b2
10 changed files with 23 additions and 23 deletions
+3
View File
@@ -0,0 +1,3 @@
export const TRIGGER_THRESHOLD = 8
export const DEDUP_WINDOW_MS = 40
export const HOTPLUG_SCAN_INTERVAL_MS = 3_000
+3 -8
View File
@@ -32,6 +32,7 @@
import * as fs from 'node:fs'
import * as path from 'node:path'
import type { BrowserWindow } from 'electron'
import { DEDUP_WINDOW_MS, HOTPLUG_SCAN_INTERVAL_MS, TRIGGER_THRESHOLD } from '../constants/input'
// evdev input_event auf 64-bit Linux:
// { u64 tv_sec (8 Bytes), u64 tv_usec (8 Bytes), u16 type (2), u16 code (2), s32 value (4) } = 24 Bytes
@@ -70,16 +71,10 @@ const BUTTON_MAP: Record<number, string> = {
}
const STICK_THRESHOLD = 16384 // ~50% von 32767
// Trigger-Schwellwert: Achsenwert > 0 (nicht gedrückt) bis ~255–1023 je Controller
const TRIGGER_THRESHOLD = 8
const REPEAT_INITIAL_MS = 400
const REPEAT_INTERVAL_MS = 150
// Trigger-Achsen sollen kein Auto-Repeat auslösen (300s-Sprünge nicht wiederholen)
const NO_REPEAT_ABS_CODES = new Set([ABS_Z, ABS_RZ])
const SCAN_INTERVAL_MS = 3000
// Bluetooth-/Steam-Setups können dieselbe Eingabe über mehrere Devices liefern.
// Nahezu zeitgleiche identische Events werden global entdoppelt.
const DUPLICATE_EVENT_WINDOW_MS = 40
interface AxisState {
key: string | null
@@ -367,7 +362,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () =>
const onKey = (key: string): void => {
const now = Date.now()
const lastAt = lastKeyAt.get(key) ?? 0
if (now - lastAt < DUPLICATE_EVENT_WINDOW_MS) return
if (now - lastAt < DEDUP_WINDOW_MS) return
lastKeyAt.set(key, now)
const win = getWindow()
@@ -427,7 +422,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () =>
}
scanDevices()
const timer = setInterval(scanDevices, SCAN_INTERVAL_MS)
const timer = setInterval(scanDevices, HOTPLUG_SCAN_INTERVAL_MS)
return () => {
clearInterval(timer)
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { GamepadHintItem } from './GamepadPrompt'
import { ClapperboardIcon, EyeIcon, GamepadIcon, PauseIcon, PlayIcon, QualityIcon, StopIcon } from './Icons'
import { formatCount, formatTimestamp } from '../lib/formatting'
import { DOUBLE_TAP_MS, OVERLAY_HIDE_DELAY_MS } from '../constants/playback'
interface PlaybackOverlayProps {
playState: 'playing' | 'paused'
@@ -30,9 +31,6 @@ interface PlaybackOverlayProps {
}
const HIDE_DELAY_MS = 5_000
const DOUBLE_TAP_MS = 300
export function PlaybackOverlay({
playState,
durationSeconds,
@@ -81,7 +79,7 @@ export function PlaybackOverlay({
const scheduleHide = useCallback(() => {
clearHideTimer()
hideTimerRef.current = setTimeout(() => setVisible(false), HIDE_DELAY_MS)
hideTimerRef.current = setTimeout(() => setVisible(false), OVERLAY_HIDE_DELAY_MS)
}, [clearHideTimer])
/** Call on any user interaction to show overlay and reset auto-hide timer. */
+1 -2
View File
@@ -1,5 +1,6 @@
import Hls from 'hls.js'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { POSITION_REPORT_INTERVAL_MS } from '../constants/playback'
export interface VideoPlayerHandle {
seek(delta: number): void
@@ -24,8 +25,6 @@ interface Props {
onTimeUpdate?: (seconds: number) => void
}
const POSITION_REPORT_INTERVAL_MS = 5_000
export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
({ hlsUrl, startPosition, isLive, vodId, durationSeconds, onPlaying, onPaused, onEnded, onError, onTimeUpdate }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null)
+3
View File
@@ -0,0 +1,3 @@
export const AXIS_THRESHOLD = 0.5
export const REPEAT_INITIAL_MS = 400
export const REPEAT_INTERVAL_MS = 150
+3
View File
@@ -0,0 +1,3 @@
export const OVERLAY_HIDE_DELAY_MS = 5_000
export const DOUBLE_TAP_MS = 300
export const POSITION_REPORT_INTERVAL_MS = 5_000
+4
View File
@@ -0,0 +1,4 @@
export const CARD_W = 260
export const CARD_GAP = 16
export const STREAM_W = 220
export const STREAM_GAP = 14
+2 -4
View File
@@ -20,6 +20,8 @@
* Axis 0 (left stick X) → Left/Right
*/
import { AXIS_THRESHOLD, REPEAT_INITIAL_MS, REPEAT_INTERVAL_MS } from '../constants/input'
const BUTTON_MAP: Record<number, string> = {
0: 'Enter', // A
1: 'Escape', // B
@@ -35,10 +37,6 @@ const BUTTON_MAP: Record<number, string> = {
15: 'ArrowRight'
}
const AXIS_THRESHOLD = 0.5
const REPEAT_INITIAL_MS = 400
const REPEAT_INTERVAL_MS = 150
interface AxisRepeat {
key: string | null
heldSince: number
+1 -2
View File
@@ -3,6 +3,7 @@ import { GamepadHintItem } from '../components/GamepadPrompt'
import LanguageBadge from '../components/LanguageBadge'
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
import { formatViewers, formatViewersFull } from '../lib/formatting'
import { STREAM_GAP, STREAM_W } from '../constants/ui'
interface Props {
hasFocus: boolean
@@ -15,8 +16,6 @@ interface Props {
type LoadState = 'loading' | 'ok' | 'error'
type FocusRegion = 'shelf' | 'grid'
const STREAM_W = 220
const STREAM_GAP = 14
export default function BrowseScreen({
hasFocus,
+1 -3
View File
@@ -13,6 +13,7 @@ import {
formatViewers,
formatWatchedAt
} from '../lib/formatting'
import { CARD_GAP, CARD_W } from '../constants/ui'
interface Props {
channel: FollowedChannelInfo
@@ -21,9 +22,6 @@ interface Props {
type FocusRegion = 'hero' | 'shelf' | 'chapters'
const CARD_W = 260
const CARD_GAP = 16
function resolveThumbnail(url: string | undefined, w = 1280, h = 720): string | undefined {
return url?.replace('{width}', String(w)).replace('{height}', String(h))
}