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 fs from 'node:fs'
import * as path from 'node:path' import * as path from 'node:path'
import type { BrowserWindow } from 'electron' 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: // 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 // { 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 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_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-Achsen sollen kein Auto-Repeat auslösen (300s-Sprünge nicht wiederholen)
const NO_REPEAT_ABS_CODES = new Set([ABS_Z, ABS_RZ]) 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 { interface AxisState {
key: string | null key: string | null
@@ -367,7 +362,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () =>
const onKey = (key: string): void => { const onKey = (key: string): void => {
const now = Date.now() const now = Date.now()
const lastAt = lastKeyAt.get(key) ?? 0 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) lastKeyAt.set(key, now)
const win = getWindow() const win = getWindow()
@@ -427,7 +422,7 @@ export function startGamepadReader(getWindow: () => BrowserWindow | null): () =>
} }
scanDevices() scanDevices()
const timer = setInterval(scanDevices, SCAN_INTERVAL_MS) const timer = setInterval(scanDevices, HOTPLUG_SCAN_INTERVAL_MS)
return () => { return () => {
clearInterval(timer) clearInterval(timer)
@@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { GamepadHintItem } from './GamepadPrompt' import { GamepadHintItem } from './GamepadPrompt'
import { ClapperboardIcon, EyeIcon, GamepadIcon, PauseIcon, PlayIcon, QualityIcon, StopIcon } from './Icons' import { ClapperboardIcon, EyeIcon, GamepadIcon, PauseIcon, PlayIcon, QualityIcon, StopIcon } from './Icons'
import { formatCount, formatTimestamp } from '../lib/formatting' import { formatCount, formatTimestamp } from '../lib/formatting'
import { DOUBLE_TAP_MS, OVERLAY_HIDE_DELAY_MS } from '../constants/playback'
interface PlaybackOverlayProps { interface PlaybackOverlayProps {
playState: 'playing' | 'paused' playState: 'playing' | 'paused'
@@ -30,9 +31,6 @@ interface PlaybackOverlayProps {
} }
const HIDE_DELAY_MS = 5_000
const DOUBLE_TAP_MS = 300
export function PlaybackOverlay({ export function PlaybackOverlay({
playState, playState,
durationSeconds, durationSeconds,
@@ -81,7 +79,7 @@ export function PlaybackOverlay({
const scheduleHide = useCallback(() => { const scheduleHide = useCallback(() => {
clearHideTimer() clearHideTimer()
hideTimerRef.current = setTimeout(() => setVisible(false), HIDE_DELAY_MS) hideTimerRef.current = setTimeout(() => setVisible(false), OVERLAY_HIDE_DELAY_MS)
}, [clearHideTimer]) }, [clearHideTimer])
/** Call on any user interaction to show overlay and reset auto-hide timer. */ /** 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 Hls from 'hls.js'
import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react'
import { POSITION_REPORT_INTERVAL_MS } from '../constants/playback'
export interface VideoPlayerHandle { export interface VideoPlayerHandle {
seek(delta: number): void seek(delta: number): void
@@ -24,8 +25,6 @@ interface Props {
onTimeUpdate?: (seconds: number) => void onTimeUpdate?: (seconds: number) => void
} }
const POSITION_REPORT_INTERVAL_MS = 5_000
export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>( export const VideoPlayer = forwardRef<VideoPlayerHandle, Props>(
({ hlsUrl, startPosition, isLive, vodId, durationSeconds, onPlaying, onPaused, onEnded, onError, onTimeUpdate }, ref) => { ({ hlsUrl, startPosition, isLive, vodId, durationSeconds, onPlaying, onPaused, onEnded, onError, onTimeUpdate }, ref) => {
const videoRef = useRef<HTMLVideoElement>(null) 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 * 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> = { const BUTTON_MAP: Record<number, string> = {
0: 'Enter', // A 0: 'Enter', // A
1: 'Escape', // B 1: 'Escape', // B
@@ -35,10 +37,6 @@ const BUTTON_MAP: Record<number, string> = {
15: 'ArrowRight' 15: 'ArrowRight'
} }
const AXIS_THRESHOLD = 0.5
const REPEAT_INITIAL_MS = 400
const REPEAT_INTERVAL_MS = 150
interface AxisRepeat { interface AxisRepeat {
key: string | null key: string | null
heldSince: number heldSince: number
+1 -2
View File
@@ -3,6 +3,7 @@ import { GamepadHintItem } from '../components/GamepadPrompt'
import LanguageBadge from '../components/LanguageBadge' import LanguageBadge from '../components/LanguageBadge'
import type { FollowedChannelInfo, GameInfo } from '../types/t4sd' import type { FollowedChannelInfo, GameInfo } from '../types/t4sd'
import { formatViewers, formatViewersFull } from '../lib/formatting' import { formatViewers, formatViewersFull } from '../lib/formatting'
import { STREAM_GAP, STREAM_W } from '../constants/ui'
interface Props { interface Props {
hasFocus: boolean hasFocus: boolean
@@ -15,8 +16,6 @@ interface Props {
type LoadState = 'loading' | 'ok' | 'error' type LoadState = 'loading' | 'ok' | 'error'
type FocusRegion = 'shelf' | 'grid' type FocusRegion = 'shelf' | 'grid'
const STREAM_W = 220
const STREAM_GAP = 14
export default function BrowseScreen({ export default function BrowseScreen({
hasFocus, hasFocus,
+1 -3
View File
@@ -13,6 +13,7 @@ import {
formatViewers, formatViewers,
formatWatchedAt formatWatchedAt
} from '../lib/formatting' } from '../lib/formatting'
import { CARD_GAP, CARD_W } from '../constants/ui'
interface Props { interface Props {
channel: FollowedChannelInfo channel: FollowedChannelInfo
@@ -21,9 +22,6 @@ interface Props {
type FocusRegion = 'hero' | 'shelf' | 'chapters' type FocusRegion = 'hero' | 'shelf' | 'chapters'
const CARD_W = 260
const CARD_GAP = 16
function resolveThumbnail(url: string | undefined, w = 1280, h = 720): string | undefined { function resolveThumbnail(url: string | undefined, w = 1280, h = 720): string | undefined {
return url?.replace('{width}', String(w)).replace('{height}', String(h)) return url?.replace('{width}', String(w)).replace('{height}', String(h))
} }