Add live introdb key reload and recap playback markers
- reload introdb API key on setting updates - support recap/preview markers in playback and next-episode flow - add profile defaults for recap and preview auto-play settings
This commit is contained in:
@@ -128,8 +128,8 @@ export interface Profile {
|
||||
show_forced_subtitles?: boolean;
|
||||
auto_skip_intro: boolean;
|
||||
auto_skip_credits: boolean;
|
||||
auto_skip_recap: boolean;
|
||||
auto_play_next_preview: boolean;
|
||||
auto_skip_recap?: boolean;
|
||||
auto_play_next_preview?: boolean;
|
||||
library_restrictions_enabled: boolean;
|
||||
allowed_library_ids: number[] | null;
|
||||
max_playback_quality: string;
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
interface IntroSkipButtonProps {
|
||||
onSkip: () => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* "Skip Intro" button visible during the intro time range.
|
||||
*/
|
||||
export function IntroSkipButton({ onSkip }: IntroSkipButtonProps) {
|
||||
export function IntroSkipButton({ onSkip, label = "Skip Intro" }: IntroSkipButtonProps) {
|
||||
return (
|
||||
<button
|
||||
onClick={onSkip}
|
||||
type="button"
|
||||
className="absolute right-6 bottom-24 z-50 rounded border border-white/40 bg-black/70 px-6 py-2 text-sm font-medium text-white transition-colors hover:bg-white/20"
|
||||
>
|
||||
Skip Intro
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -72,6 +72,10 @@ interface VideoPlayerProps {
|
||||
intro: PlayerTimeRange | null;
|
||||
autoSkipIntro?: boolean;
|
||||
credits: PlayerTimeRange | null;
|
||||
recap?: PlayerTimeRange | null;
|
||||
autoSkipRecap?: boolean;
|
||||
preview?: PlayerTimeRange | null;
|
||||
autoPlayNextPreview?: boolean;
|
||||
duration?: number;
|
||||
seriesContext?: SeriesContext;
|
||||
onNavigateEpisode?: (contentId: string) => void;
|
||||
@@ -154,6 +158,10 @@ export function VideoPlayer({
|
||||
intro,
|
||||
autoSkipIntro = false,
|
||||
credits,
|
||||
recap = null,
|
||||
autoSkipRecap = false,
|
||||
preview = null,
|
||||
autoPlayNextPreview = false,
|
||||
duration: propDuration,
|
||||
seriesContext,
|
||||
onNavigateEpisode,
|
||||
@@ -190,6 +198,7 @@ export function VideoPlayer({
|
||||
const backendDurationRef = useRef(propDuration ?? 0);
|
||||
const autoEnterPictureInPictureAttemptedRef = useRef(false);
|
||||
const autoSkippedIntroKeyRef = useRef<string | null>(null);
|
||||
const autoSkippedRecapKeyRef = useRef<string | null>(null);
|
||||
const endedFiredRef = useRef(false);
|
||||
const [hasEnded, setHasEnded] = useState(false);
|
||||
const onEndedRef = useRef(onEnded);
|
||||
@@ -822,7 +831,7 @@ export function VideoPlayer({
|
||||
);
|
||||
|
||||
const nextEpisode = useNextEpisode(
|
||||
roomPlaybackActive ? null : credits,
|
||||
roomPlaybackActive ? null : autoPlayNextPreview && preview ? preview : credits,
|
||||
roomPlaybackActive ? undefined : seriesContext,
|
||||
currentTime,
|
||||
handleNavigate,
|
||||
@@ -886,11 +895,16 @@ export function VideoPlayer({
|
||||
|
||||
// -- Intro skip --
|
||||
const showIntroSkip = intro != null && currentTime >= intro.start && currentTime < intro.end;
|
||||
const showRecapSkip = recap != null && currentTime >= recap.start && currentTime < recap.end;
|
||||
|
||||
const skipIntro = useCallback(() => {
|
||||
if (intro) handlePlayerSeek(intro.end);
|
||||
}, [intro, handlePlayerSeek]);
|
||||
|
||||
const skipRecap = useCallback(() => {
|
||||
if (recap) handlePlayerSeek(recap.end);
|
||||
}, [recap, handlePlayerSeek]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoSkipIntro || !intro || !isPlayerReady || awaitingFirstFrame) {
|
||||
return;
|
||||
@@ -926,6 +940,41 @@ export function VideoPlayer({
|
||||
watchTogetherSync.attachedSessionId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoSkipRecap || !recap || !isPlayerReady || awaitingFirstFrame) {
|
||||
return;
|
||||
}
|
||||
if (currentTime < recap.start || currentTime >= recap.end) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
roomPlaybackActive &&
|
||||
(!watchTogether.room?.self_can_manage_room ||
|
||||
watchTogetherSync.attachedSessionId !== sessionId)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const recapKey = `${sessionId}:${activeFileId ?? "unknown"}:${recap.start}:${recap.end}`;
|
||||
if (autoSkippedRecapKeyRef.current === recapKey) {
|
||||
return;
|
||||
}
|
||||
autoSkippedRecapKeyRef.current = recapKey;
|
||||
handlePlayerSeek(recap.end);
|
||||
}, [
|
||||
activeFileId,
|
||||
autoSkipRecap,
|
||||
awaitingFirstFrame,
|
||||
currentTime,
|
||||
handlePlayerSeek,
|
||||
isPlayerReady,
|
||||
recap,
|
||||
roomPlaybackActive,
|
||||
sessionId,
|
||||
watchTogether.room?.self_can_manage_room,
|
||||
watchTogetherSync.attachedSessionId,
|
||||
]);
|
||||
|
||||
// Stabilize the dependency – only the bitrate matters for buffer sizing.
|
||||
const selectedVersionBitrate = transcodeQuality.effectiveVersion?.bitrate ?? 0;
|
||||
|
||||
@@ -1972,6 +2021,7 @@ export function VideoPlayer({
|
||||
|
||||
{/* Intro skip button */}
|
||||
{!isDetached && showIntroSkip && <IntroSkipButton onSkip={skipIntro} />}
|
||||
{!isDetached && showRecapSkip && <IntroSkipButton onSkip={skipRecap} label="Skip Recap" />}
|
||||
|
||||
{/* Next episode overlay */}
|
||||
{!isDetached && nextEpisode.showCountdown && nextEpisode.nextEpisode && (
|
||||
|
||||
@@ -77,6 +77,8 @@ export function WatchPage({
|
||||
showForcedSubtitles,
|
||||
profileLanguage,
|
||||
autoSkipIntro,
|
||||
autoSkipRecap,
|
||||
autoPlayNextPreview,
|
||||
seriesContext,
|
||||
onNavigateEpisode,
|
||||
onEnded,
|
||||
@@ -411,6 +413,10 @@ export function WatchPage({
|
||||
intro={activeMarkers.intro}
|
||||
autoSkipIntro={autoSkipIntro}
|
||||
credits={activeMarkers.credits}
|
||||
recap={activeMarkers.recap}
|
||||
autoSkipRecap={autoSkipRecap}
|
||||
preview={activeMarkers.preview}
|
||||
autoPlayNextPreview={autoPlayNextPreview}
|
||||
duration={selectedDuration}
|
||||
qualityPreference={qualityPreference}
|
||||
seriesContext={seriesContext}
|
||||
|
||||
@@ -10,11 +10,11 @@ interface NextEpisodeState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects when the current playback position enters the credits region,
|
||||
* Detects when the current playback position enters the configured trigger region,
|
||||
* starts a 10-second countdown, and provides the next episode reference.
|
||||
*/
|
||||
export function useNextEpisode(
|
||||
credits: PlayerTimeRange | null,
|
||||
triggerRegion: PlayerTimeRange | null,
|
||||
seriesContext: SeriesContext | undefined,
|
||||
currentTime: number,
|
||||
onNavigate: (contentId: string) => void,
|
||||
@@ -41,11 +41,11 @@ export function useNextEpisode(
|
||||
}
|
||||
}, [currentEpisodeKey]);
|
||||
|
||||
// Detect entry into credits region.
|
||||
// Detect entry into the configured trigger region.
|
||||
useEffect(() => {
|
||||
if (!credits || !nextEpisode || cancelledRef.current) return;
|
||||
if (!triggerRegion || !nextEpisode || cancelledRef.current) return;
|
||||
|
||||
if (currentTime >= credits.start && !showCountdown) {
|
||||
if (currentTime >= triggerRegion.start && !showCountdown) {
|
||||
setShowCountdown(true);
|
||||
setSecondsRemaining(10);
|
||||
|
||||
@@ -60,7 +60,7 @@ export function useNextEpisode(
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
}, [currentTime, credits, nextEpisode, showCountdown, onNavigate]);
|
||||
}, [currentTime, triggerRegion, nextEpisode, showCountdown, onNavigate]);
|
||||
|
||||
// Clean up interval on unmount.
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user