From 887dfa2ad52c9195109687f8791b56ec4d0e77ef Mon Sep 17 00:00:00 2001 From: Pas <74743263+Pasithea0@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:30:19 -0700 Subject: [PATCH] add pause overlay --- src/assets/locales/en.json | 8 +- src/backend/accounts/settings.ts | 2 + src/backend/metadata/getmeta.ts | 2 + src/backend/metadata/tmdb.ts | 15 +++- src/backend/metadata/types/mw.ts | 1 + src/backend/metadata/types/tmdb.ts | 1 + src/components/player/hooks/usePlayerMeta.ts | 4 + .../player/overlays/PauseOverlay.tsx | 87 +++++++++++++++++++ src/hooks/useSettingsState.ts | 14 +++ src/pages/Settings.tsx | 16 ++++ src/pages/parts/player/PlayerPart.tsx | 2 + src/pages/parts/settings/AppearancePart.tsx | 33 +++++++ src/stores/player/slices/source.ts | 2 + src/stores/preferences/index.tsx | 8 ++ 14 files changed, 190 insertions(+), 5 deletions(-) create mode 100644 src/components/player/overlays/PauseOverlay.tsx diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index a0560625..8586c410 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -1257,7 +1257,10 @@ "homeSectionOrder": "Home section order", "homeSectionOrderDescription": "Drag and drop to reorder the watching and bookmarks sections on your homepage. Group order can be editied from the home page.", "forceCompactEpisodeViewLabel": "Compact episodes", - "homeSectionOrderGroups": "Reorder bookmark groups" + "homeSectionOrderGroups": "Reorder bookmark groups", + "pauseOverlay": "Pause overlay", + "pauseOverlayDescription": "Show a title/logo and description overlay when the player is paused and idle.", + "pauseOverlayLabel": "Pause overlay" }, "sections": { "watching": "Currently Watching", @@ -1442,8 +1445,7 @@ "genreMovies": "{{genre}} Movies", "genreShows": "{{genre}} Shows", "categoryMovies": "{{category}} Movies", - "categoryShows": "{{category}} Shows", - "top10": "Top 10" + "categoryShows": "{{category}} Shows" }, "change": "Change", "more": "View more" diff --git a/src/backend/accounts/settings.ts b/src/backend/accounts/settings.ts index 592f19a2..8cafded2 100644 --- a/src/backend/accounts/settings.ts +++ b/src/backend/accounts/settings.ts @@ -44,6 +44,7 @@ export interface SettingsInput { manualSourceSelection?: boolean; enableDoubleClickToSeek?: boolean; enableAutoResumeOnPlaybackError?: boolean; + enablePauseOverlay?: boolean; enableNumberKeySeeking?: boolean; keyboardShortcuts?: KeyboardShortcuts; customTheme?: CustomThemeSettings; @@ -83,6 +84,7 @@ export interface SettingsResponse { manualSourceSelection?: boolean; enableDoubleClickToSeek?: boolean; enableAutoResumeOnPlaybackError?: boolean; + enablePauseOverlay?: boolean; enableNumberKeySeeking?: boolean; keyboardShortcuts?: KeyboardShortcuts; customTheme?: CustomThemeSettings; diff --git a/src/backend/metadata/getmeta.ts b/src/backend/metadata/getmeta.ts index a208bd1f..b83510e8 100644 --- a/src/backend/metadata/getmeta.ts +++ b/src/backend/metadata/getmeta.ts @@ -47,6 +47,7 @@ export function formatTMDBMetaResult( object_type: mediaTypeToTMDB(type), poster: getMediaPoster(movie.poster_path) ?? undefined, original_release_date: new Date(movie.release_date), + overview: movie.overview || undefined, }; } if (type === MWMediaType.SERIES) { @@ -62,6 +63,7 @@ export function formatTMDBMetaResult( })), poster: getMediaPoster(show.poster_path) ?? undefined, original_release_date: new Date(show.first_air_date), + overview: show.overview, }; } diff --git a/src/backend/metadata/tmdb.ts b/src/backend/metadata/tmdb.ts index 84b44d12..ff32ffc5 100644 --- a/src/backend/metadata/tmdb.ts +++ b/src/backend/metadata/tmdb.ts @@ -79,6 +79,7 @@ export function formatTMDBMeta( year: media.original_release_date?.getFullYear()?.toString(), poster: media.poster, type, + overview: media.overview, seasons: seasons as any, seasonData: season ? { @@ -408,8 +409,18 @@ export async function getMediaDetails< const item = seasonsQueue.shift(); if (!item) break; const { season, index } = item; - const episodes = await getSeasonDetails(id, season.season_number); - allEpisodesBySeason[index] = episodes; + const seasonData = await get( + `/tv/${id}/season/${season.season_number}`, + ); + allEpisodesBySeason[index] = seasonData.episodes.map((episode) => ({ + id: episode.id, + name: episode.name, + episode_number: episode.episode_number, + overview: episode.overview, + still_path: episode.still_path, + air_date: episode.air_date, + season_number: season.season_number, + })); } }, ); diff --git a/src/backend/metadata/types/mw.ts b/src/backend/metadata/types/mw.ts index 7202f352..d6abc38a 100644 --- a/src/backend/metadata/types/mw.ts +++ b/src/backend/metadata/types/mw.ts @@ -29,6 +29,7 @@ type MWMediaMetaBase = { id: string; year?: string; poster?: string; + overview?: string; }; type MWMediaMetaSpecific = diff --git a/src/backend/metadata/types/tmdb.ts b/src/backend/metadata/types/tmdb.ts index ace86197..71a6c089 100644 --- a/src/backend/metadata/types/tmdb.ts +++ b/src/backend/metadata/types/tmdb.ts @@ -25,6 +25,7 @@ export type TMDBMediaResult = { original_release_date?: Date; object_type: TMDBContentTypes; seasons?: TMDBSeasonShort[]; + overview?: string; }; export type TMDBSeasonMetaResult = { diff --git a/src/components/player/hooks/usePlayerMeta.ts b/src/components/player/hooks/usePlayerMeta.ts index 49982828..ea33cdd2 100644 --- a/src/components/player/hooks/usePlayerMeta.ts +++ b/src/components/player/hooks/usePlayerMeta.ts @@ -36,17 +36,20 @@ export function usePlayerMeta() { poster: m.meta.poster, tmdbId: m.tmdbId ?? "", imdbId: m.imdbId, + overview: m.meta.overview, episodes: m.meta.seasonData.episodes.map((v) => ({ number: v.number, title: v.title, tmdbId: v.id, air_date: v.air_date, + overview: v.overview, })), episode: { number: ep.number, title: ep.title, tmdbId: ep.id, air_date: ep.air_date, + overview: ep.overview, }, season: { number: m.meta.seasonData.number, @@ -62,6 +65,7 @@ export function usePlayerMeta() { poster: m.meta.poster, tmdbId: m.tmdbId ?? "", imdbId: m.imdbId, + overview: m.meta.overview, }; } setDirectMeta(playerMeta); diff --git a/src/components/player/overlays/PauseOverlay.tsx b/src/components/player/overlays/PauseOverlay.tsx new file mode 100644 index 00000000..4b292ac9 --- /dev/null +++ b/src/components/player/overlays/PauseOverlay.tsx @@ -0,0 +1,87 @@ +import { useEffect, useState } from "react"; +import { useIdle } from "react-use"; + +import { getMediaLogo } from "@/backend/metadata/tmdb"; +import { TMDBContentTypes } from "@/backend/metadata/types/tmdb"; +import { usePlayerStore } from "@/stores/player/store"; +import { usePreferencesStore } from "@/stores/preferences"; + +export function PauseOverlay() { + const isIdle = useIdle(10e3); // 10 seconds + const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused); + const meta = usePlayerStore((s) => s.meta); + const enablePauseOverlay = usePreferencesStore((s) => s.enablePauseOverlay); + const enableImageLogos = usePreferencesStore((s) => s.enableImageLogos); + const [logoUrl, setLogoUrl] = useState(null); + + const shouldShow = isPaused && isIdle && enablePauseOverlay; + + useEffect(() => { + let mounted = true; + const fetchLogo = async () => { + if (!meta?.tmdbId || !enableImageLogos) { + setLogoUrl(null); + return; + } + + try { + const type = + meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV; + const url = await getMediaLogo(meta.tmdbId, type); + if (mounted) setLogoUrl(url || null); + } catch { + if (mounted) setLogoUrl(null); + } + }; + + fetchLogo(); + return () => { + mounted = false; + }; + }, [meta?.tmdbId, meta?.type, enableImageLogos]); + + if (!meta) return null; + + const overview = + meta.type === "show" ? meta.episode?.overview : meta.overview; + + // Don't render anything if we don't have content, but keep structure for fade if valid + const hasContent = overview || logoUrl || meta.title; + if (!hasContent) return null; + + return ( +
+
+ {logoUrl ? ( + {meta.title} + ) : ( +

+ {meta.title} +

+ )} + + {meta.type === "show" && meta.episode && ( +

+ {meta.episode.title} +

+ )} + + {overview && ( +

+ {overview} +

+ )} +
+
+ ); +} diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts index 2a385615..422d9bcf 100644 --- a/src/hooks/useSettingsState.ts +++ b/src/hooks/useSettingsState.ts @@ -81,6 +81,7 @@ export function useSettingsState( manualSourceSelection: boolean, enableDoubleClickToSeek: boolean, enableAutoResumeOnPlaybackError: boolean, + enablePauseOverlay: boolean, customTheme: { primary: string; secondary: string; @@ -277,6 +278,12 @@ export function useSettingsState( resetEnableAutoResumeOnPlaybackError, enableAutoResumeOnPlaybackErrorChanged, ] = useDerived(enableAutoResumeOnPlaybackError); + const [ + enablePauseOverlayState, + setEnablePauseOverlayState, + resetEnablePauseOverlay, + enablePauseOverlayChanged, + ] = useDerived(enablePauseOverlay); const [ customThemeState, setCustomThemeState, @@ -323,6 +330,7 @@ export function useSettingsState( resetManualSourceSelection(); resetEnableDoubleClickToSeek(); resetEnableAutoResumeOnPlaybackError(); + resetEnablePauseOverlay(); resetCustomTheme(); } @@ -364,6 +372,7 @@ export function useSettingsState( manualSourceSelectionChanged || enableDoubleClickToSeekChanged || enableAutoResumeOnPlaybackErrorChanged || + enablePauseOverlayChanged || customThemeChanged; return { @@ -554,6 +563,11 @@ export function useSettingsState( set: setEnableAutoResumeOnPlaybackErrorState, changed: enableAutoResumeOnPlaybackErrorChanged, }, + enablePauseOverlay: { + state: enablePauseOverlayState, + set: setEnablePauseOverlayState, + changed: enablePauseOverlayChanged, + }, customTheme: { state: customThemeState, set: (v: { primary: string; secondary: string; tertiary: string }) => { diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx index f3bb1add..7bd1a728 100644 --- a/src/pages/Settings.tsx +++ b/src/pages/Settings.tsx @@ -531,6 +531,11 @@ export function SettingsPage() { (s) => s.setEnableAutoResumeOnPlaybackError, ); + const enablePauseOverlay = usePreferencesStore((s) => s.enablePauseOverlay); + const setEnablePauseOverlay = usePreferencesStore( + (s) => s.setEnablePauseOverlay, + ); + const account = useAuthStore((s) => s.account); const updateProfile = useAuthStore((s) => s.setAccountProfile); const updateDeviceName = useAuthStore((s) => s.updateDeviceName); @@ -647,6 +652,9 @@ export function SettingsPage() { settings.enableAutoResumeOnPlaybackError, ); } + if (settings.enablePauseOverlay !== undefined) { + setEnablePauseOverlay(settings.enablePauseOverlay); + } if (settings.customTheme) { setCustomTheme(settings.customTheme); setCustomThemeBaseline(settings.customTheme); @@ -687,6 +695,7 @@ export function SettingsPage() { setManualSourceSelection, setEnableDoubleClickToSeek, setEnableAutoResumeOnPlaybackError, + setEnablePauseOverlay, setCustomTheme, ]); @@ -728,6 +737,7 @@ export function SettingsPage() { manualSourceSelection, enableDoubleClickToSeek, enableAutoResumeOnPlaybackError, + enablePauseOverlay, customThemeBaseline ?? customTheme, ); @@ -797,6 +807,7 @@ export function SettingsPage() { state.manualSourceSelection.changed || state.enableDoubleClickToSeek.changed || state.enableAutoResumeOnPlaybackError.changed || + state.enablePauseOverlay.changed || state.customTheme.changed ) { await updateSettings(backendUrl, account, { @@ -829,6 +840,7 @@ export function SettingsPage() { enableDoubleClickToSeek: state.enableDoubleClickToSeek.state, enableAutoResumeOnPlaybackError: state.enableAutoResumeOnPlaybackError.state, + enablePauseOverlay: state.enablePauseOverlay.state, customTheme: state.customTheme.state, }); } @@ -889,6 +901,7 @@ export function SettingsPage() { setEnableAutoResumeOnPlaybackError( state.enableAutoResumeOnPlaybackError.state, ); + setEnablePauseOverlay(state.enablePauseOverlay.state); setCustomTheme(state.customTheme.state); setCustomThemeBaseline(state.customTheme.state); @@ -951,6 +964,7 @@ export function SettingsPage() { setManualSourceSelection, setEnableDoubleClickToSeek, setEnableAutoResumeOnPlaybackError, + setEnablePauseOverlay, setCustomTheme, ]); return ( @@ -1067,6 +1081,8 @@ export function SettingsPage() { homeSectionOrder={state.homeSectionOrder.state} setHomeSectionOrder={state.homeSectionOrder.set} enableLowPerformanceMode={state.enableLowPerformanceMode.state} + enablePauseOverlay={state.enablePauseOverlay.state} + setEnablePauseOverlay={state.enablePauseOverlay.set} customTheme={state.customTheme.state} setCustomTheme={state.customTheme.set} /> diff --git a/src/pages/parts/player/PlayerPart.tsx b/src/pages/parts/player/PlayerPart.tsx index bcabeccd..dfa1d3fb 100644 --- a/src/pages/parts/player/PlayerPart.tsx +++ b/src/pages/parts/player/PlayerPart.tsx @@ -11,6 +11,7 @@ import { SegmentData, useSkipTime, } from "@/components/player/hooks/useSkipTime"; +import { PauseOverlay } from "@/components/player/overlays/PauseOverlay"; import { useIsMobile } from "@/hooks/useIsMobile"; import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; import { usePlayerStore } from "@/stores/player/store"; @@ -99,6 +100,7 @@ export function PlayerPart(props: PlayerPartProps) { return ( {props.children} + diff --git a/src/pages/parts/settings/AppearancePart.tsx b/src/pages/parts/settings/AppearancePart.tsx index a3fdda1e..99a307de 100644 --- a/src/pages/parts/settings/AppearancePart.tsx +++ b/src/pages/parts/settings/AppearancePart.tsx @@ -292,6 +292,9 @@ export function AppearancePart(props: { enableImageLogos: boolean; setEnableImageLogos: (v: boolean) => void; + enablePauseOverlay: boolean; + setEnablePauseOverlay: (v: boolean) => void; + enableCarouselView: boolean; setEnableCarouselView: (v: boolean) => void; @@ -355,6 +358,7 @@ export function AppearancePart(props: { setEnableFeatured, setEnableDetailsModal, setEnableImageLogos, + setEnablePauseOverlay, setForceCompactEpisodeView, } = props; @@ -365,6 +369,7 @@ export function AppearancePart(props: { setEnableFeatured(false); setEnableDetailsModal(false); setEnableImageLogos(false); + setEnablePauseOverlay(false); setForceCompactEpisodeView(true); } }, [ @@ -373,6 +378,7 @@ export function AppearancePart(props: { setEnableFeatured, setEnableDetailsModal, setEnableImageLogos, + setEnablePauseOverlay, setForceCompactEpisodeView, ]); @@ -553,6 +559,33 @@ export function AppearancePart(props: { + {/* Pause Overlay */} +
+

+ {t("settings.appearance.options.pauseOverlay")} +

+

+ {t("settings.appearance.options.pauseOverlayDescription")} +

+
+ !props.enableLowPerformanceMode && + props.setEnablePauseOverlay(!props.enablePauseOverlay) + } + className={classNames( + "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", + props.enableLowPerformanceMode + ? "cursor-not-allowed opacity-50 pointer-events-none" + : "cursor-pointer opacity-100 pointer-events-auto", + )} + > + +

+ {t("settings.appearance.options.pauseOverlayLabel")} +

+
+
+ {/* Carousel View */}

diff --git a/src/stores/player/slices/source.ts b/src/stores/player/slices/source.ts index c369da35..3738796c 100644 --- a/src/stores/player/slices/source.ts +++ b/src/stores/player/slices/source.ts @@ -29,6 +29,7 @@ export interface PlayerMetaEpisode { tmdbId: string; title: string; air_date?: string; + overview?: string; } export interface PlayerMeta { @@ -38,6 +39,7 @@ export interface PlayerMeta { imdbId?: string; releaseYear: number; poster?: string; + overview?: string; episodes?: PlayerMetaEpisode[]; episode?: PlayerMetaEpisode; season?: { diff --git a/src/stores/preferences/index.tsx b/src/stores/preferences/index.tsx index 0e70d049..eef74d5e 100644 --- a/src/stores/preferences/index.tsx +++ b/src/stores/preferences/index.tsx @@ -39,6 +39,7 @@ export interface PreferencesStore { enableDoubleClickToSeek: boolean; enableAutoResumeOnPlaybackError: boolean; enableNumberKeySeeking: boolean; + enablePauseOverlay: boolean; keyboardShortcuts: KeyboardShortcuts; setEnableThumbnails(v: boolean): void; @@ -72,6 +73,7 @@ export interface PreferencesStore { setEnableDoubleClickToSeek(v: boolean): void; setEnableAutoResumeOnPlaybackError(v: boolean): void; setEnableNumberKeySeeking(v: boolean): void; + setEnablePauseOverlay(v: boolean): void; setKeyboardShortcuts(v: KeyboardShortcuts): void; } @@ -109,6 +111,7 @@ export const usePreferencesStore = create( enableDoubleClickToSeek: false, enableAutoResumeOnPlaybackError: true, enableNumberKeySeeking: true, + enablePauseOverlay: false, keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, setEnableThumbnails(v) { set((s) => { @@ -270,6 +273,11 @@ export const usePreferencesStore = create( s.enableNumberKeySeeking = v; }); }, + setEnablePauseOverlay(v) { + set((s) => { + s.enablePauseOverlay = v; + }); + }, setKeyboardShortcuts(v) { set((s) => { s.keyboardShortcuts = v;