fix(clip): clip mode keeps its range, its focus and its keys through the player's own moves
- A mark on the very end seeks a moment short of it; while a clip is marked the end of the item neither closes the player nor steps the queue, and Plex's auto-skip waits. - Plex: Escape and Back peel what is over the picture (info, up-next, the countdown) before the clip; Back under a dialog is the dialog's; no range opens while the length is unknown. - YouTube: the focus goes back to the scissors the user can see, else the player; the panel keeps its name, Loop and focus when the view is maximised or restored under it. - A typed time stays in its field until taken, then shows where it landed; unreadable stays marked invalid; a decimal comma is read (ui/TimecodeField, the editor's fields too). - RangeTrack: a drag is its pointer's, a cancelled drag reports final, an adornment sits beside the slider, a caller-chosen focus ring (amber on the clip strip). CropFrame: a drag is its pointer's. - The editor's remove-cut is a real button at the cut's foot; a segment's keep toggle sits over the cut's hit area.
This commit is contained in:
@@ -50,7 +50,7 @@ import {
|
||||
} from "../lib/plexPlayerPrefs";
|
||||
import { Ctrl, PanelHead } from "./plexPlayerControls";
|
||||
import ClipPanel, { ClipCropFrame, type ClipCrop } from "./ClipPanel";
|
||||
import { DEFAULT_CLIP_S, markIn, markOut, openAt, type ClipRange } from "../lib/clipRange";
|
||||
import { markInAt, markOutAt, openAt, type ClipRange } from "../lib/clipRange";
|
||||
import { PlexPlayerSettings } from "./PlexPlayerSettings";
|
||||
import { PlexStatsOverlay } from "./PlexStatsOverlay";
|
||||
import { createFragMeter } from "../lib/plexStats";
|
||||
@@ -833,6 +833,9 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
seekTo,
|
||||
videoRef,
|
||||
reportWatchedError,
|
||||
// Marking a clip, the viewer is where they want to be: a skip would carry the player off the
|
||||
// range (an intro clip skipped past its Out, the credits' leaving for the next item).
|
||||
suspended: clip != null,
|
||||
});
|
||||
|
||||
// Download the ORIGINAL physical file (no re-encode/repackage). One user gesture, direct link.
|
||||
@@ -902,6 +905,8 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
// Only a genuine play-to-the-end counts. A next episode that failed to load never played
|
||||
// (absRef 0) — its stray 'ended' must not mark anything watched or cascade another advance.
|
||||
if (absRef.current <= 0) return;
|
||||
// Marking a clip, the end is a place, not a finish: closing or moving on would drop the clip.
|
||||
if (clipRef.current) return;
|
||||
const watchedWrite = api.plexSetState(id, "watched");
|
||||
watchedWrite.catch(reportWatchedError);
|
||||
// Whatever follows (the up-next card, the next item, closing) leaves a FINISHED item — unless
|
||||
@@ -918,7 +923,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
};
|
||||
video.addEventListener("ended", onEnded);
|
||||
return () => video.removeEventListener("ended", onEnded);
|
||||
}, [id, nextId, go, onClose, finishProgress, reportWatchedError, absRef, upNextPrefRef]);
|
||||
}, [id, nextId, go, onClose, finishProgress, reportWatchedError, absRef, upNextPrefRef, clipRef]);
|
||||
|
||||
// Up-next card actions: advance = go to the next item (it was already marked watched on 'ended');
|
||||
// cancel = dismiss and stay on the finished frame. Loading a new item closes any lingering card.
|
||||
@@ -937,21 +942,25 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
|
||||
// The ONE Back cascade for the player's own ways of saying "back": the on-screen arrow, ⌫ and
|
||||
// Escape. It peels off what is layered over the video — the auto-skip countdown, then the open
|
||||
// panel, then the stats readout — and only then leaves the player. The mouse/browser Back peels
|
||||
// the layers the viewer OPENED one per press, the one opened last first (each holds its own history
|
||||
// entry, see BackClose below); the ones that open by themselves have none, so Back leaves past
|
||||
// them. The shortcuts sheet is not in it: a Modal above us, with its own Escape layer and
|
||||
// Back-to-close. Leaving closes whatever is still up first (`useHistorySubview`'s back).
|
||||
// panel, then the stats readout, then the clip being marked (its crop first) — and only then leaves
|
||||
// the player. The mouse/browser Back peels the layers the viewer OPENED one per press, the one
|
||||
// opened last first (each holds its own history entry, see BackClose below); the ones that open by
|
||||
// themselves have none, so Back leaves past them. The shortcuts sheet is not in it: a Modal above
|
||||
// us, with its own Escape layer and Back-to-close. Leaving closes whatever is still up first
|
||||
// (`useHistorySubview`'s back).
|
||||
// - The on-screen arrow is a click on "leave", not a cancel, so it does not stop a countdown.
|
||||
// - Escape in fullscreen leaves fullscreen (the browser's own Escape) and not the player.
|
||||
// - The arrow leaves past a clip as it does past a panel; the keys step out of it.
|
||||
const back = useCallback(
|
||||
(from: "arrow" | "key" | "escape") => {
|
||||
if (from !== "arrow" && skipProgressRef.current != null) cancelAutoSkipRef.current();
|
||||
else if (panelRef.current !== "none") setPanel("none");
|
||||
else if (statsOpenRef.current) setStatsOpen(false);
|
||||
else if (from !== "escape" || !fullscreenElement()) onClose();
|
||||
else if (from === "escape" && fullscreenElement()) return;
|
||||
else if (from !== "arrow" && clipRef.current) stepOutOfClip();
|
||||
else onClose();
|
||||
},
|
||||
[onClose, panelRef, statsOpenRef, skipProgressRef, cancelAutoSkipRef],
|
||||
[onClose, panelRef, statsOpenRef, skipProgressRef, cancelAutoSkipRef, clipRef, stepOutOfClip],
|
||||
);
|
||||
// The mouse/browser Back closes the layer whose history entry it popped (see BackClose below).
|
||||
const closeStats = useCallback(() => setStatsOpen(false), []);
|
||||
@@ -1072,19 +1081,20 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
},
|
||||
info: () => togglePanel("info"),
|
||||
stats: () => setStatsOpen((v) => !v),
|
||||
// A range needs the item's length (unknown while its detail loads, or never given).
|
||||
clip: () => {
|
||||
if (clipRef.current) closeClip();
|
||||
else setClip(openAt(absRef.current, durationRef.current));
|
||||
else if (durationRef.current > 0) setClip(openAt(absRef.current, durationRef.current));
|
||||
},
|
||||
markIn: () => {
|
||||
const t = absRef.current;
|
||||
const d = durationRef.current;
|
||||
setClip((c) => markIn(c ?? openAt(t, d), t, d));
|
||||
if (d > 0) setClip((c) => markInAt(c, t, d));
|
||||
},
|
||||
markOut: () => {
|
||||
const t = absRef.current;
|
||||
const d = durationRef.current;
|
||||
setClip((c) => markOut(c ?? openAt(Math.max(0, t - DEFAULT_CLIP_S), d), t, d));
|
||||
if (d > 0) setClip((c) => markOutAt(c, t, d));
|
||||
},
|
||||
back: (ev) => {
|
||||
// HTPC-style "stop & go back" to wherever the player was opened from — mirrors the mouse
|
||||
@@ -1097,15 +1107,14 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
// ⌫ stopped closing them), then naming just the sheet and palette (the second fix, the third
|
||||
// bug — the app-wide ErrorDialog is neither, so ⌫ tore the player down under it).
|
||||
// The rule that holds: the player's own panels answer first, because `back`'s cascade is
|
||||
// what closes them; anything else layered above us takes the key.
|
||||
const ownPanelOpen =
|
||||
panelRef.current !== "none" || statsOpenRef.current || clipRef.current != null;
|
||||
// what closes them; anything else layered above us takes the key. The clip panel pushes no
|
||||
// layer, so with it open a layer above is never the player's own (a refused Cut opens the
|
||||
// ErrorDialog right over it): the clip is in the cascade, behind that layer.
|
||||
const ownPanelOpen = panelRef.current !== "none" || statsOpenRef.current;
|
||||
if (!ownPanelOpen && layerIdRef.current != null && anyLayerAbove(layerIdRef.current))
|
||||
return;
|
||||
ev.preventDefault();
|
||||
// The clip panel is the player's own too, and closes before the player does.
|
||||
if (clipRef.current) stepOutOfClip();
|
||||
else back("key");
|
||||
back("key");
|
||||
},
|
||||
escape: (ev) => {
|
||||
// Defer to any layer opened above the player (Modal or anchored popover) — let it take the
|
||||
@@ -1115,8 +1124,7 @@ export default function PlexPlayer({ itemId, onClose, queue, fromStart = false }
|
||||
// one and its close can pop it off the stack synchronously, so `anyLayerAbove` alone misses it.
|
||||
if (escapeHandled(ev)) return;
|
||||
if (layerIdRef.current != null && anyLayerAbove(layerIdRef.current)) return;
|
||||
if (clipRef.current) stepOutOfClip();
|
||||
else back("escape");
|
||||
back("escape");
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user