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 && (