fix(player): short-video auto-watch + player cleanups
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.
This commit is contained in:
parent
c0e941bc0c
commit
31c1284eb7
3 changed files with 26 additions and 26 deletions
|
|
@ -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<string, unknown> }>(["me"])
|
||||
?.preferences ?? {}) as { playerAutoAdvance?: AutoMode; playerLoop?: LoopMode };
|
||||
const savedPrefs = useMemo(
|
||||
() =>
|
||||
(qc.getQueryData<{ preferences?: Record<string, unknown> }>(["me"])?.preferences ?? {}) as {
|
||||
playerAutoAdvance?: AutoMode;
|
||||
playerLoop?: LoopMode;
|
||||
},
|
||||
[qc]
|
||||
);
|
||||
const [autoMode, setAutoMode] = useState<AutoMode>(savedPrefs.playerAutoAdvance ?? "off");
|
||||
const [loopMode, setLoopMode] = useState<LoopMode>(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 && (
|
||||
<div className="flex flex-wrap items-center justify-center gap-x-4 gap-y-1.5 px-4 py-2 border-b border-border text-sm">
|
||||
<button
|
||||
onClick={() => index > 0 && setPlayingId(queue![index - 1].id)}
|
||||
onClick={goPrev}
|
||||
disabled={index === 0}
|
||||
title={`${t("player.previous")} · Shift+←`}
|
||||
className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
|
||||
|
|
@ -657,7 +659,7 @@ export default function PlayerModal({
|
|||
{index + 1} / {queue!.length}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => index < queue!.length - 1 && setPlayingId(queue![index + 1].id)}
|
||||
onClick={goNext}
|
||||
disabled={index === queue!.length - 1}
|
||||
title={`${t("player.next")} · Shift+→`}
|
||||
className="inline-flex items-center gap-1.5 text-muted enabled:hover:text-fg disabled:opacity-40 transition"
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import AddToPlaylist from "./AddToPlaylist";
|
|||
import DownloadButton from "./DownloadButton";
|
||||
import clsx from "clsx";
|
||||
import type { Video } from "../lib/api";
|
||||
import { formatDuration, formatViews, relativeTime } from "../lib/format";
|
||||
import { formatDate, formatDuration, formatViews, relativeTime } from "../lib/format";
|
||||
|
||||
function Actions({
|
||||
video,
|
||||
|
|
@ -257,16 +257,7 @@ function VideoCard({
|
|||
</>
|
||||
)}
|
||||
{relativeTime(video.published_at)}
|
||||
{video.published_at && (
|
||||
<>
|
||||
{" · "}
|
||||
{new Date(video.published_at).toLocaleDateString(i18n.language, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
{video.published_at && <>{" · "}{formatDate(video.published_at, i18n.language)}</>}
|
||||
</>
|
||||
);
|
||||
// Show the full title in a native tooltip only when it's clamped (overflows its 2 lines).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue