From 0e38f930e78ab497e2715e39f2d5c17fdb0cf225 Mon Sep 17 00:00:00 2001 From: Andre <200930841+Andre-Bzk@users.noreply.github.com> Date: Thu, 14 May 2026 12:13:08 +0200 Subject: [PATCH] refactor: centralize format utilities in lib/formatting.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit formatViewers, formatCount, formatDuration, formatDate, formatWatchedAt, formatTimestamp, and formatViewersFull were duplicated across four files. Divergence was silent — fixing a rounding edge case or locale string in one copy left the others stale. --- src/renderer/src/components/FocusableCard.tsx | 7 +-- .../src/components/PlaybackOverlay.tsx | 19 ++----- src/renderer/src/lib/formatting.ts | 50 +++++++++++++++++ src/renderer/src/screens/BrowseScreen.tsx | 11 +--- src/renderer/src/screens/ChannelScreen.tsx | 54 +++---------------- 5 files changed, 63 insertions(+), 78 deletions(-) create mode 100644 src/renderer/src/lib/formatting.ts diff --git a/src/renderer/src/components/FocusableCard.tsx b/src/renderer/src/components/FocusableCard.tsx index 32747f8..93267df 100644 --- a/src/renderer/src/components/FocusableCard.tsx +++ b/src/renderer/src/components/FocusableCard.tsx @@ -1,6 +1,7 @@ import { useEffect, useRef } from 'react' import type { FollowedChannelInfo } from '../types/t4sd' import LanguageBadge from './LanguageBadge' +import { formatViewers } from '../lib/formatting' interface Props { channel: FollowedChannelInfo @@ -9,12 +10,6 @@ interface Props { onSelect: () => void } -function formatViewers(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.` - if (n >= 1000) return `${(n / 1000).toFixed(1)}K` - return String(n) -} - export default function FocusableCard({ channel, focused, onFocus, onSelect }: Props): JSX.Element { const ref = useRef(null) diff --git a/src/renderer/src/components/PlaybackOverlay.tsx b/src/renderer/src/components/PlaybackOverlay.tsx index 22122e4..c085ce4 100644 --- a/src/renderer/src/components/PlaybackOverlay.tsx +++ b/src/renderer/src/components/PlaybackOverlay.tsx @@ -1,6 +1,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' interface PlaybackOverlayProps { playState: 'playing' | 'paused' @@ -28,20 +29,6 @@ interface PlaybackOverlayProps { onOpenChapters?: () => void } -function formatCount(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` - return String(n) -} - -/** Formats seconds as HH:MM:SS */ -function fmt(s: number): string { - const total = Math.max(0, Math.floor(s)) - const h = Math.floor(total / 3600) - const m = Math.floor((total % 3600) / 60) - const sec = total % 60 - return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}` -} const HIDE_DELAY_MS = 5_000 const DOUBLE_TAP_MS = 300 @@ -333,7 +320,7 @@ export function PlaybackOverlay({ )}
- {fmt(displayPosition)} + {formatTimestamp(displayPosition)}
- {fmt(durationSeconds)} + {formatTimestamp(durationSeconds)}
) : null} diff --git a/src/renderer/src/lib/formatting.ts b/src/renderer/src/lib/formatting.ts new file mode 100644 index 0000000..99b3bf3 --- /dev/null +++ b/src/renderer/src/lib/formatting.ts @@ -0,0 +1,50 @@ +export function formatViewers(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.` + if (n >= 1000) return `${(n / 1000).toFixed(1)}K` + return String(n) +} + +export function formatViewersFull(n: number): string { + return n.toLocaleString('de-DE') +} + +export function formatCount(n: number): string { + if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` + if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` + return String(n) +} + +export function formatDuration(s: number): string { + const h = Math.floor(s / 3600) + const m = Math.floor((s % 3600) / 60) + return `${h}:${String(m).padStart(2, '0')}` +} + +export function formatDate(iso: string): string { + return new Date(iso).toLocaleDateString('de-DE', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }) +} + +export function formatWatchedAt(ms: number): string { + const diff = Date.now() - ms + const mins = Math.floor(diff / 60_000) + const hours = Math.floor(diff / 3_600_000) + const days = Math.floor(diff / 86_400_000) + if (mins < 2) return 'Gerade eben' + if (mins < 60) return `Vor ${mins} Min.` + if (hours < 24) return `Vor ${hours} Std.` + if (days === 1) return 'Gestern' + if (days < 7) return `Vor ${days} Tagen` + return new Date(ms).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) +} + +export function formatTimestamp(s: number): string { + const total = Math.max(0, Math.floor(s)) + const h = Math.floor(total / 3600) + const m = Math.floor((total % 3600) / 60) + const sec = total % 60 + return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}` +} diff --git a/src/renderer/src/screens/BrowseScreen.tsx b/src/renderer/src/screens/BrowseScreen.tsx index d62b5ef..99c5541 100644 --- a/src/renderer/src/screens/BrowseScreen.tsx +++ b/src/renderer/src/screens/BrowseScreen.tsx @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' import { GamepadHintItem } from '../components/GamepadPrompt' import LanguageBadge from '../components/LanguageBadge' import type { FollowedChannelInfo, GameInfo } from '../types/t4sd' +import { formatViewers, formatViewersFull } from '../lib/formatting' interface Props { hasFocus: boolean @@ -17,16 +18,6 @@ type FocusRegion = 'shelf' | 'grid' const STREAM_W = 220 const STREAM_GAP = 14 -function formatViewers(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.` - if (n >= 1000) return `${(n / 1000).toFixed(1)}K` - return String(n) -} - -function formatViewersFull(n: number): string { - return n.toLocaleString('de-DE') -} - export default function BrowseScreen({ hasFocus, onRequestSidebar, diff --git a/src/renderer/src/screens/ChannelScreen.tsx b/src/renderer/src/screens/ChannelScreen.tsx index 5386dc0..4efd2c7 100644 --- a/src/renderer/src/screens/ChannelScreen.tsx +++ b/src/renderer/src/screens/ChannelScreen.tsx @@ -5,6 +5,14 @@ import { PlaybackOverlay } from '../components/PlaybackOverlay' import { VideoPlayer } from '../components/VideoPlayer' import type { FollowedChannelInfo, VodChapter, VodInfo, VodProgress } from '../types/t4sd' import { usePlaybackSession } from '../hooks/usePlaybackSession' +import { + formatCount, + formatDate, + formatDuration, + formatTimestamp, + formatViewers, + formatWatchedAt +} from '../lib/formatting' interface Props { channel: FollowedChannelInfo @@ -16,56 +24,10 @@ type FocusRegion = 'hero' | 'shelf' | 'chapters' const CARD_W = 260 const CARD_GAP = 16 -function formatViewers(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)} Mio.` - if (n >= 1000) return `${(n / 1000).toFixed(1)}K` - return String(n) -} - -function formatCount(n: number): string { - if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M` - if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K` - return String(n) -} - -function formatDuration(s: number): string { - const h = Math.floor(s / 3600) - const m = Math.floor((s % 3600) / 60) - return `${h}:${String(m).padStart(2, '0')}` -} - -function formatDate(iso: string): string { - return new Date(iso).toLocaleDateString('de-DE', { - day: '2-digit', - month: '2-digit', - year: 'numeric' - }) -} - -function formatWatchedAt(ms: number): string { - const diff = Date.now() - ms - const mins = Math.floor(diff / 60_000) - const hours = Math.floor(diff / 3_600_000) - const days = Math.floor(diff / 86_400_000) - if (mins < 2) return 'Gerade eben' - if (mins < 60) return `Vor ${mins} Min.` - if (hours < 24) return `Vor ${hours} Std.` - if (days === 1) return 'Gestern' - if (days < 7) return `Vor ${days} Tagen` - return new Date(ms).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) -} - function resolveThumbnail(url: string | undefined, w = 1280, h = 720): string | undefined { return url?.replace('{width}', String(w)).replace('{height}', String(h)) } -function formatTimestamp(s: number): string { - const h = Math.floor(s / 3600) - const m = Math.floor((s % 3600) / 60) - const sec = s % 60 - return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(sec).padStart(2, '0')}` -} - export default function ChannelScreen({ channel, onBack }: Props): JSX.Element { const [vods, setVods] = useState([]) const [vodsLoading, setVodsLoading] = useState(true)