fix hdr playback, lint warnings

This commit is contained in:
2026-06-04 22:50:46 +03:00
parent 2fce5dfb59
commit 6756bf9d40
10 changed files with 144 additions and 72 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ export default function DuplicatesPage() {
sortOrder: ['Ascending'],
limit: 5000,
})
const items = data?.Items || []
const items = useMemo(() => data?.Items || [], [data?.Items])
const duplicates = useMemo(() => {
// Group by TMDB ID first (most reliable)
+16 -11
View File
@@ -347,17 +347,19 @@ export default function PlayerPage() {
* the resume condition once per item, not re-trigger when item data
* refreshes or prefs change mid-playback. */
const resumePromptShownRef = useRef<string | null>(null)
const resumeItemId = item?.Id
const resumePositionTicks = item?.UserData?.PlaybackPositionTicks
useEffect(() => {
if (!item || resumePromptShownRef.current === item.Id) return
const pos = Number(item.UserData?.PlaybackPositionTicks ?? 0)
if (!resumeItemId || resumePromptShownRef.current === resumeItemId) return
const pos = Number(resumePositionTicks ?? 0)
const thresholdSec = usePreferencesStore.getState().resumeThresholdSec ?? 5
const threshold = thresholdSec * 10_000_000
const fromQueue = searchParams.get('resume') === 'true'
if (showResumePromptPref && !fromQueue && pos > threshold) {
setResumePromptOpen(true)
resumePromptShownRef.current = item.Id ?? null
resumePromptShownRef.current = resumeItemId
}
}, [item?.Id, searchParams, showResumePromptPref])
}, [resumeItemId, resumePositionTicks, searchParams, showResumePromptPref])
/* Auto-rewatch counter: when an already-played item starts playing
* again, record the rewatch. We trip it at most once per item-mount
@@ -645,23 +647,26 @@ export default function PlayerPage() {
const currentMarker: Marker | undefined = markers.find(
m => currentTime >= m.startSec && currentTime < m.endSec - 0.5,
)
const currentMarkerType = currentMarker?.type
const currentMarkerStartSec = currentMarker?.startSec
const currentMarkerEndSec = currentMarker?.endSec
/* Auto-skip when entering a marker AND the corresponding pref is on.
* Also accumulate the skipped seconds into the per-series tally so the
* detail page can surface a "you saved Xh Ym" badge. */
useEffect(() => {
if (!currentMarker || !playerRef.current) return
if (!currentMarkerType || currentMarkerEndSec == null || !playerRef.current) return
const p = playerRef.current
const from = p.currentTime
if (currentMarker.type === 'intro' && skipIntros) {
p.currentTime = currentMarker.endSec
if (seriesId) recordSkippedSeconds(seriesId, 'intro', currentMarker.endSec - from)
} else if (currentMarker.type === 'credits' && skipCredits && duration > 0) {
const target = Math.max(currentMarker.endSec, duration - 0.5)
if (currentMarkerType === 'intro' && skipIntros) {
p.currentTime = currentMarkerEndSec
if (seriesId) recordSkippedSeconds(seriesId, 'intro', currentMarkerEndSec - from)
} else if (currentMarkerType === 'credits' && skipCredits && duration > 0) {
const target = Math.max(currentMarkerEndSec, duration - 0.5)
p.currentTime = target
if (seriesId) recordSkippedSeconds(seriesId, 'credits', target - from)
}
}, [currentMarker?.type, currentMarker?.startSec, skipIntros, skipCredits, duration, seriesId])
}, [currentMarkerType, currentMarkerStartSec, currentMarkerEndSec, skipIntros, skipCredits, duration, seriesId])
/* Imperatively switch the active subtitle track. We use 'hidden' rather
* than 'showing' so the browser doesn't paint its own caption UI over our
@@ -1,4 +1,4 @@
import { useMemo } from 'react'
import { useEffect, useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Activity, MonitorPlay, Users, Server as ServerIcon, Clock } from '../../../lib/icons'
import { jellyfinClient, getSystemApi, getSessionApi } from '../../../api/jellyfin'
@@ -10,6 +10,15 @@ function useApi() {
export function ServerDashboardSection() {
const api = useApi()
const [now, setNow] = useState(0)
useEffect(() => {
const update = () => setNow(Date.now())
update()
const id = window.setInterval(update, 60_000)
return () => window.clearInterval(id)
}, [])
const serverInfo = useQuery({
queryKey: ['jellyfin', 'system-info'],
queryFn: async () => {
@@ -37,13 +46,14 @@ export function ServerDashboardSection() {
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
const uptime = useMemo(() => {
if (!now) return null
const start = (info as { ServerStartTime?: string } | null | undefined)?.ServerStartTime
if (!start) return null
const ms = Date.now() - new Date(start).getTime()
const ms = now - new Date(start).getTime()
const days = Math.floor(ms / 86_400_000)
const hrs = Math.floor((ms % 86_400_000) / 3_600_000)
return days > 0 ? `${days}d ${hrs}h` : `${hrs}h`
}, [info])
}, [info, now])
return (
<Section id="server-dashboard" title="Server dashboard" description="Live stats and activity">
+19 -20
View File
@@ -69,24 +69,6 @@ export function ShortcutsSection() {
const setPreference = usePreferencesStore(s => s.setPreference)
const [capturingId, setCapturingId] = useState<string | null>(null)
function commit(id: string, binding: string) {
const conflict = Object.entries({ ...overrides })
.filter(([k]) => k !== id)
.find(([, keys]) => (keys as string[]).some(k => normalizeBinding(k) === binding))
const defaultConflict = SHORTCUTS.find(sc => sc.id !== id && sc.keys.some(k => normalizeBinding(k) === binding) && !overrides[sc.id])
if (conflict) {
const other = SHORTCUTS.find(s => s.id === conflict[0])
toast(`That combo is bound to "${other?.description || conflict[0]}"`, 'error')
return
}
if (defaultConflict) {
toast(`That combo is bound to "${defaultConflict.description}"`, 'error')
return
}
setPreference('keyboardShortcuts', { ...overrides, [id]: [binding] })
toast('Shortcut updated', 'success')
}
function reset(id: string) {
const next = { ...overrides }
delete next[id]
@@ -101,6 +83,23 @@ export function ShortcutsSection() {
useEffect(() => {
if (!capturingId) return
const id = capturingId
function commit(binding: string) {
const conflict = Object.entries({ ...overrides })
.filter(([k]) => k !== id)
.find(([, keys]) => (keys as string[]).some(k => normalizeBinding(k) === binding))
const defaultConflict = SHORTCUTS.find(sc => sc.id !== id && sc.keys.some(k => normalizeBinding(k) === binding) && !overrides[sc.id])
if (conflict) {
const other = SHORTCUTS.find(s => s.id === conflict[0])
toast(`That combo is bound to "${other?.description || conflict[0]}"`, 'error')
return
}
if (defaultConflict) {
toast(`That combo is bound to "${defaultConflict.description}"`, 'error')
return
}
setPreference('keyboardShortcuts', { ...overrides, [id]: [binding] })
toast('Shortcut updated', 'success')
}
function onKey(e: KeyboardEvent) {
e.preventDefault()
e.stopPropagation()
@@ -113,11 +112,11 @@ export function ShortcutsSection() {
}
const binding = eventToBinding(e)
setCapturingId(null)
commit(id, binding)
commit(binding)
}
window.addEventListener('keydown', onKey, true)
return () => window.removeEventListener('keydown', onKey, true)
}, [capturingId, overrides, commit])
}, [capturingId, overrides, setPreference])
return (
<Section