From 31c1284eb709a3ca38a6e3fe637d80f507dea371 Mon Sep 17 00:00:00 2001 From: npeter83 Date: Sat, 11 Jul 2026 18:49:56 +0200 Subject: [PATCH] fix(player): short-video auto-watch + player cleanups MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BUG (YB1): the auto-watch checkpoint marked a video watched once position crossed `duration - FINISH_MARGIN` (10s). For clips shorter than ~10s that threshold is negative, and for ~11-20s clips it's only a few seconds in, so the 5s checkpoint tick marked short videos watched almost immediately (clearing their resume position). Cap the margin at half the clip: Math.min(FINISH_MARGIN, dur/2). Cleanups (behavior-neutral): - Extract format.formatDate(iso, lang) — the day/month/year toLocaleDateString option object was duplicated verbatim in PlayerModal (fullDate) and VideoCard. (ChannelPage's "joined" is month/year only, so it's left as-is.) - The in-bar prev/next buttons now reuse goPrev/goNext instead of re-implementing the step + bounds inline (they already exist for the side arrows + keyboard). - Memoize savedPrefs so the ['me'] cache read + spread runs once, not on every render (it only seeds the initial autoMode/loopMode state). tsc green, knip clean, localdev boots healthy. --- frontend/src/components/PlayerModal.tsx | 32 +++++++++++++------------ frontend/src/components/VideoCard.tsx | 13 ++-------- frontend/src/lib/format.ts | 7 ++++++ 3 files changed, 26 insertions(+), 26 deletions(-) diff --git a/frontend/src/components/PlayerModal.tsx b/frontend/src/components/PlayerModal.tsx index 88568a5..1f663ae 100644 --- a/frontend/src/components/PlayerModal.tsx +++ b/frontend/src/components/PlayerModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { createPortal } from "react-dom"; import { useQuery, useQueryClient } from "@tanstack/react-query"; @@ -7,7 +7,7 @@ import Avatar from "./Avatar"; import AddToPlaylist from "./AddToPlaylist"; import DownloadButton from "./DownloadButton"; import { api, type Video } from "../lib/api"; -import { channelYouTubeUrl, formatDuration, formatViews, relativeTime } from "../lib/format"; +import { channelYouTubeUrl, formatDate, formatDuration, formatViews, relativeTime } from "../lib/format"; import { renderDescription } from "../lib/descriptionLinks"; import { useBackToClose } from "../lib/history"; @@ -84,14 +84,7 @@ export default function PlayerModal({ // non-embeddable still surface the existing playback-error overlay when reached. const [queue] = useState(() => queueProp); // Precise upload date shown inline after the relative time (e.g. "9 yr ago · 12 Jan 2017"). - const fullDate = (d: string | null | undefined) => - d - ? new Date(d).toLocaleDateString(i18n.language, { - year: "numeric", - month: "short", - day: "numeric", - }) - : ""; + const fullDate = (d: string | null | undefined) => formatDate(d, i18n.language); // Track the playing item by id (not a frozen index) so it survives the queue changing // under us — e.g. an item removed in another tab. The index is derived from the *live* // queue, so the N / M counter and prev/next neighbours stay correct without disrupting @@ -235,8 +228,14 @@ export default function PlayerModal({ // Auto-advance + loop are persistent per-user settings. Read the current values from the cached // `me`, mirror them in local state for instant UI, and write both to the server + cache on change // so they apply to every player and survive reloads. A ref feeds the (once-bound) end handler. - const savedPrefs = (qc.getQueryData<{ preferences?: Record }>(["me"]) - ?.preferences ?? {}) as { playerAutoAdvance?: AutoMode; playerLoop?: LoopMode }; + const savedPrefs = useMemo( + () => + (qc.getQueryData<{ preferences?: Record }>(["me"])?.preferences ?? {}) as { + playerAutoAdvance?: AutoMode; + playerLoop?: LoopMode; + }, + [qc] + ); const [autoMode, setAutoMode] = useState(savedPrefs.playerAutoAdvance ?? "off"); const [loopMode, setLoopMode] = useState(savedPrefs.playerLoop ?? "off"); const modeRef = useRef({ autoMode, loopMode }); @@ -446,7 +445,10 @@ export default function PlayerModal({ // navigated to via description links. const maybeAutoWatch = (current: number, duration: number): boolean => { if (autoMarkedRef.current || duration <= 0 || currentIdRef.current !== id) return false; - if (current > duration - FINISH_MARGIN) { + // Cap the finish margin at half the clip, so a video shorter than FINISH_MARGIN isn't + // auto-marked watched almost immediately (its threshold would otherwise go negative). + const margin = Math.min(FINISH_MARGIN, duration / 2); + if (current > duration - margin) { autoMarkedRef.current = true; setWatched(true); return true; @@ -646,7 +648,7 @@ export default function PlayerModal({ {hasQueue && (