mirror of
https://github.com/Andre-Bzk/twitch4steamdeck.git
synced 2026-11-15 15:43:08 +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 { useEffect, useRef } from 'react'
|
||||||
import type { FollowedChannelInfo } from '../types/t4sd'
|
import type { FollowedChannelInfo } from '../types/t4sd'
|
||||||
import LanguageBadge from './LanguageBadge'
|
import LanguageBadge from './LanguageBadge'
|
||||||
|
import { formatViewers } from '../lib/formatting'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
channel: FollowedChannelInfo
|
channel: FollowedChannelInfo
|
||||||
@@ -9,12 +10,6 @@ interface Props {
|
|||||||
onSelect: () => void
|
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 {
|
export default function FocusableCard({ channel, focused, onFocus, onSelect }: Props): JSX.Element {
|
||||||
const ref = useRef<HTMLButtonElement>(null)
|
const ref = useRef<HTMLButtonElement>(null)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
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'
|
||||||
|
|
||||||
interface PlaybackOverlayProps {
|
interface PlaybackOverlayProps {
|
||||||
playState: 'playing' | 'paused'
|
playState: 'playing' | 'paused'
|
||||||
@@ -28,20 +29,6 @@ interface PlaybackOverlayProps {
|
|||||||
onOpenChapters?: () => void
|
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 HIDE_DELAY_MS = 5_000
|
||||||
const DOUBLE_TAP_MS = 300
|
const DOUBLE_TAP_MS = 300
|
||||||
@@ -333,7 +320,7 @@ export function PlaybackOverlay({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="playback-overlay__progress-row">
|
<div className="playback-overlay__progress-row">
|
||||||
<span className="playback-overlay__time">{fmt(displayPosition)}</span>
|
<span className="playback-overlay__time">{formatTimestamp(displayPosition)}</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
ref={seekBarRef}
|
ref={seekBarRef}
|
||||||
@@ -355,7 +342,7 @@ export function PlaybackOverlay({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="playback-overlay__time">{fmt(durationSeconds)}</span>
|
<span className="playback-overlay__time">{formatTimestamp(durationSeconds)}</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : 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 { 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'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hasFocus: boolean
|
hasFocus: boolean
|
||||||
@@ -17,16 +18,6 @@ type FocusRegion = 'shelf' | 'grid'
|
|||||||
const STREAM_W = 220
|
const STREAM_W = 220
|
||||||
const STREAM_GAP = 14
|
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({
|
export default function BrowseScreen({
|
||||||
hasFocus,
|
hasFocus,
|
||||||
onRequestSidebar,
|
onRequestSidebar,
|
||||||
|
|||||||
@@ -5,6 +5,14 @@ import { PlaybackOverlay } from '../components/PlaybackOverlay'
|
|||||||
import { VideoPlayer } from '../components/VideoPlayer'
|
import { VideoPlayer } from '../components/VideoPlayer'
|
||||||
import type { FollowedChannelInfo, VodChapter, VodInfo, VodProgress } from '../types/t4sd'
|
import type { FollowedChannelInfo, VodChapter, VodInfo, VodProgress } from '../types/t4sd'
|
||||||
import { usePlaybackSession } from '../hooks/usePlaybackSession'
|
import { usePlaybackSession } from '../hooks/usePlaybackSession'
|
||||||
|
import {
|
||||||
|
formatCount,
|
||||||
|
formatDate,
|
||||||
|
formatDuration,
|
||||||
|
formatTimestamp,
|
||||||
|
formatViewers,
|
||||||
|
formatWatchedAt
|
||||||
|
} from '../lib/formatting'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
channel: FollowedChannelInfo
|
channel: FollowedChannelInfo
|
||||||
@@ -16,56 +24,10 @@ type FocusRegion = 'hero' | 'shelf' | 'chapters'
|
|||||||
const CARD_W = 260
|
const CARD_W = 260
|
||||||
const CARD_GAP = 16
|
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 {
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export default function ChannelScreen({ channel, onBack }: Props): JSX.Element {
|
||||||
const [vods, setVods] = useState<VodInfo[]>([])
|
const [vods, setVods] = useState<VodInfo[]>([])
|
||||||
const [vodsLoading, setVodsLoading] = useState(true)
|
const [vodsLoading, setVodsLoading] = useState(true)
|
||||||
|
|||||||
Reference in New Issue
Block a user