mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 09:33:09 +01:00
refactor: centralize format utilities in lib/formatting.ts
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.
This commit is contained in:
@@ -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<HTMLButtonElement>(null)
|
||||
|
||||
|
||||
@@ -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({
|
||||
)}
|
||||
</div>
|
||||
<div className="playback-overlay__progress-row">
|
||||
<span className="playback-overlay__time">{fmt(displayPosition)}</span>
|
||||
<span className="playback-overlay__time">{formatTimestamp(displayPosition)}</span>
|
||||
|
||||
<div
|
||||
ref={seekBarRef}
|
||||
@@ -355,7 +342,7 @@ export function PlaybackOverlay({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span className="playback-overlay__time">{fmt(durationSeconds)}</span>
|
||||
<span className="playback-overlay__time">{formatTimestamp(durationSeconds)}</span>
|
||||
</div>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
@@ -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')}`
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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<VodInfo[]>([])
|
||||
const [vodsLoading, setVodsLoading] = useState(true)
|
||||
|
||||
Reference in New Issue
Block a user