diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index eaa5ef13..d0e66a16 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -1330,6 +1330,9 @@ "skipCredits": "Skip End Credits", "skipCreditsDescription": "When enabled, automatically play the next episode at 99% completion to skip end credits. When disabled, wait until the episode is fully completed.", "skipCreditsLabel": "Skip end credits", + "autoSkipSegments": "Auto Skip Segments", + "autoSkipSegmentsDescription": "When enabled, automatically skip intro, recap, and preview segments while watching.", + "autoSkipSegmentsLabel": "Auto skip segments", "lowPerformanceMode": "Low performance/bandwidth mode", "lowPerformanceModeDescription": "Optimizes the application for slower connections and devices by disabling bandwidth-heavy features. This mode reduces data usage and improves performance while keeping the core search and watch functionality intact. ", "lowPerformanceModeLabel": "Low performance mode", diff --git a/src/backend/accounts/settings.ts b/src/backend/accounts/settings.ts index 83993b0f..304f360a 100644 --- a/src/backend/accounts/settings.ts +++ b/src/backend/accounts/settings.ts @@ -15,6 +15,7 @@ export interface SettingsInput { enableThumbnails?: boolean; enableAutoplay?: boolean; enableSkipCredits?: boolean; + enableAutoSkipSegments?: boolean; enableDiscover?: boolean; enableFeatured?: boolean; enableDetailsModal?: boolean; @@ -50,6 +51,7 @@ export interface SettingsResponse { enableThumbnails?: boolean; enableAutoplay?: boolean; enableSkipCredits?: boolean; + enableAutoSkipSegments?: boolean; enableDiscover?: boolean; enableFeatured?: boolean; enableDetailsModal?: boolean; diff --git a/src/components/player/base/Container.tsx b/src/components/player/base/Container.tsx index 9f37b87d..27bac29d 100644 --- a/src/components/player/base/Container.tsx +++ b/src/components/player/base/Container.tsx @@ -1,6 +1,7 @@ import { ReactNode, RefObject, useEffect, useRef } from "react"; import { OverlayDisplay } from "@/components/overlays/OverlayDisplay"; +import { AutoSkipSegments } from "@/components/player/internals/AutoSkipSegments"; import { SkipTracker } from "@/components/player/internals/Backend/SkipTracker"; import { CastingInternal } from "@/components/player/internals/CastingInternal"; import { HeadUpdater } from "@/components/player/internals/HeadUpdater"; @@ -100,6 +101,7 @@ export function Container(props: PlayerProps) { +
diff --git a/src/components/player/internals/AutoSkipSegments.tsx b/src/components/player/internals/AutoSkipSegments.tsx new file mode 100644 index 00000000..9d296826 --- /dev/null +++ b/src/components/player/internals/AutoSkipSegments.tsx @@ -0,0 +1,87 @@ +import { useEffect, useRef } from "react"; + +import { useSkipTime } from "@/components/player/hooks/useSkipTime"; +import { usePlayerStore } from "@/stores/player/store"; +import { usePreferencesStore } from "@/stores/preferences"; + +interface SegmentSkipState { + segmentId: string; + hasSkipped: boolean; +} + +/** + * Component that automatically skips segments (intro, recap, preview, credits) + * when the enableAutoSkipSegments preference is enabled. + * For credits segments, only skips if end_ms is null (end of video). + */ +export function AutoSkipSegments() { + const enableAutoSkipSegments = usePreferencesStore( + (s) => s.enableAutoSkipSegments, + ); + const skipCredits = usePreferencesStore((s) => s.enableSkipCredits); + const display = usePlayerStore((s) => s.display); + const time = usePlayerStore((s) => s.progress.time); + const meta = usePlayerStore((s) => s.meta); + const segments = useSkipTime(); + + // Track which segments we've already skipped to avoid re-skipping + const skippedSegmentsRef = useRef>(new Map()); + + // Reset skip state when media changes + useEffect(() => { + skippedSegmentsRef.current.clear(); + }, [meta?.tmdbId, meta?.season?.number, meta?.episode?.number]); + + useEffect(() => { + if (!enableAutoSkipSegments || !display) return; + + const currentSeconds = time; + + for (const segment of segments) { + // For credits, only skip if skipCredits is enabled and end_ms is null (end of video) + const isCreditsSegment = segment.type === "credits"; + if (isCreditsSegment) { + if (!skipCredits) continue; + // Check if credits go to end of video (end_ms is null) + if (segment.end_ms !== null) continue; + } else if (segment.end_ms === null) { + // For intro, recap, preview - skip if enabled and end time is defined + continue; + } + + const startSeconds = (segment.start_ms ?? 0) / 1000; + const endSeconds = segment.end_ms ? segment.end_ms / 1000 : Infinity; + const segmentId = `${segment.type}-${startSeconds}-${endSeconds}`; + + // Check if we're inside the segment + if (currentSeconds >= startSeconds && currentSeconds < endSeconds) { + const skipState = skippedSegmentsRef.current.get(segmentId); + + // Only skip if we haven't skipped this segment yet + if (!skipState || !skipState.hasSkipped) { + // Skip to the end of the segment + display.setTime( + endSeconds === Infinity ? currentSeconds + 10 : endSeconds, + ); + + // Mark this segment as skipped + skippedSegmentsRef.current.set(segmentId, { + segmentId, + hasSkipped: true, + }); + } + } + } + }, [ + enableAutoSkipSegments, + skipCredits, + display, + time, + segments, + meta?.tmdbId, + meta?.season?.number, + meta?.episode?.number, + ]); + + return null; +} diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index a88609cf..101a476b 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -58,6 +58,8 @@ export function useSettingsState( | undefined, enableThumbnails: boolean, enableAutoplay: boolean, + enableSkipCredits: boolean, + enableAutoSkipSegments: boolean, enableDiscover: boolean, enableFeatured: boolean, enableDetailsModal: boolean, @@ -68,7 +70,6 @@ export function useSettingsState( embedOrder: string[], enableEmbedOrder: boolean, proxyTmdb: boolean, - enableSkipCredits: boolean, enableImageLogos: boolean, enableCarouselView: boolean, enableMinimalCards: boolean, @@ -143,6 +144,12 @@ export function useSettingsState( resetEnableSkipCredits, enableSkipCreditsChanged, ] = useDerived(enableSkipCredits); + const [ + enableAutoSkipSegmentsState, + setEnableAutoSkipSegmentsState, + resetEnableAutoSkipSegments, + enableAutoSkipSegmentsChanged, + ] = useDerived(enableAutoSkipSegments); const [ enableDiscoverState, setEnableDiscoverState, @@ -282,6 +289,7 @@ export function useSettingsState( resetEnableThumbnails(); resetEnableAutoplay(); resetEnableSkipCredits(); + resetEnableAutoSkipSegments(); resetEnableDiscover(); resetEnableFeatured(); resetEnableDetailsModal(); @@ -321,6 +329,7 @@ export function useSettingsState( enableThumbnailsChanged || enableAutoplayChanged || enableSkipCreditsChanged || + enableAutoSkipSegmentsChanged || enableDiscoverChanged || enableFeaturedChanged || enableDetailsModalChanged || @@ -421,6 +430,11 @@ export function useSettingsState( set: setEnableSkipCreditsState, changed: enableSkipCreditsChanged, }, + enableAutoSkipSegments: { + state: enableAutoSkipSegmentsState, + set: setEnableAutoSkipSegmentsState, + changed: enableAutoSkipSegmentsChanged, + }, enableDiscover: { state: enableDiscoverState, set: setEnableDiscoverState, diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index 9caa891e..6d00bd06 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -399,6 +399,13 @@ export function SettingsPage() { (s) => s.setEnableSkipCredits, ); + const enableAutoSkipSegments = usePreferencesStore( + (s) => s.enableAutoSkipSegments, + ); + const setEnableAutoSkipSegments = usePreferencesStore( + (s) => s.setEnableAutoSkipSegments, + ); + const sourceOrder = usePreferencesStore((s) => s.sourceOrder); const setSourceOrder = usePreferencesStore((s) => s.setSourceOrder); @@ -558,6 +565,8 @@ export function SettingsPage() { account ? account.profile : undefined, enableThumbnails, enableAutoplay, + enableSkipCredits, + enableAutoSkipSegments, enableDiscover, enableFeatured, enableDetailsModal, @@ -568,7 +577,6 @@ export function SettingsPage() { embedOrder, enableEmbedOrder, proxyTmdb, - enableSkipCredits, enableImageLogos, enableCarouselView, enableMinimalCards, @@ -629,6 +637,7 @@ export function SettingsPage() { state.enableThumbnails.changed || state.enableAutoplay.changed || state.enableSkipCredits.changed || + state.enableAutoSkipSegments.changed || state.enableDiscover.changed || state.enableFeatured.changed || state.enableDetailsModal.changed || @@ -658,6 +667,7 @@ export function SettingsPage() { enableThumbnails: state.enableThumbnails.state, enableAutoplay: state.enableAutoplay.state, enableSkipCredits: state.enableSkipCredits.state, + enableAutoSkipSegments: state.enableAutoSkipSegments.state, enableDiscover: state.enableDiscover.state, enableFeatured: state.enableFeatured.state, enableDetailsModal: state.enableDetailsModal.state, @@ -706,6 +716,7 @@ export function SettingsPage() { setEnableThumbnails(state.enableThumbnails.state); setEnableAutoplay(state.enableAutoplay.state); setEnableSkipCredits(state.enableSkipCredits.state); + setEnableAutoSkipSegments(state.enableAutoSkipSegments.state); setEnableDiscover(state.enableDiscover.state); setEnableFeatured(state.enableFeatured.state); setEnableDetailsModal(state.enableDetailsModal.state); @@ -769,6 +780,7 @@ export function SettingsPage() { setTIDBKey, setEnableAutoplay, setEnableSkipCredits, + setEnableAutoSkipSegments, setEnableDiscover, setEnableFeatured, setEnableDetailsModal, @@ -855,6 +867,8 @@ export function SettingsPage() { setEnableAutoplay={state.enableAutoplay.set} enableSkipCredits={state.enableSkipCredits.state} setEnableSkipCredits={state.enableSkipCredits.set} + enableAutoSkipSegments={state.enableAutoSkipSegments.state} + setEnableAutoSkipSegments={state.enableAutoSkipSegments.set} sourceOrder={availableSources} setSourceOrder={state.sourceOrder.set} enableSourceOrder={state.enableSourceOrder.state} diff --git a/src/pages/parts/settings/PreferencesPart.tsx b/src/pages/parts/settings/PreferencesPart.tsx index cb387d9d..e68a5bdc 100644 --- a/src/pages/parts/settings/PreferencesPart.tsx +++ b/src/pages/parts/settings/PreferencesPart.tsx @@ -24,6 +24,8 @@ export function PreferencesPart(props: { setEnableAutoplay: (v: boolean) => void; enableSkipCredits: boolean; setEnableSkipCredits: (v: boolean) => void; + enableAutoSkipSegments: boolean; + setEnableAutoSkipSegments: (v: boolean) => void; sourceOrder: string[]; setSourceOrder: (v: string[]) => void; enableSourceOrder: boolean; @@ -175,6 +177,29 @@ export function PreferencesPart(props: { {t("settings.preferences.skipCreditsLabel")}

+ + {/* Auto Skip Segments Preference */} +
+

+ {t("settings.preferences.autoSkipSegments")} +

+

+ {t("settings.preferences.autoSkipSegmentsDescription")} +

+
+ props.setEnableAutoSkipSegments( + !props.enableAutoSkipSegments, + ) + } + className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg" + > + +

+ {t("settings.preferences.autoSkipSegmentsLabel")} +

+
+
)} diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx index fb4d53c0..c9dcb3ab 100644 --- a/src/stores/preferences/index.tsx +++ b/src/stores/preferences/index.tsx @@ -11,6 +11,7 @@ export interface PreferencesStore { enableThumbnails: boolean; enableAutoplay: boolean; enableSkipCredits: boolean; + enableAutoSkipSegments: boolean; enableDiscover: boolean; enableFeatured: boolean; enableDetailsModal: boolean; @@ -42,6 +43,7 @@ export interface PreferencesStore { setEnableThumbnails(v: boolean): void; setEnableAutoplay(v: boolean): void; setEnableSkipCredits(v: boolean): void; + setEnableAutoSkipSegments(v: boolean): void; setEnableDiscover(v: boolean): void; setEnableFeatured(v: boolean): void; setEnableDetailsModal(v: boolean): void; @@ -77,6 +79,7 @@ export const usePreferencesStore = create( enableThumbnails: false, enableAutoplay: true, enableSkipCredits: true, + enableAutoSkipSegments: false, enableDiscover: true, enableFeatured: false, enableDetailsModal: false, @@ -119,6 +122,11 @@ export const usePreferencesStore = create( s.enableSkipCredits = v; }); }, + setEnableAutoSkipSegments(v) { + set((s) => { + s.enableAutoSkipSegments = v; + }); + }, setEnableDiscover(v) { set((s) => { s.enableDiscover = v;