mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-11 17:55:33 +00:00
add pause overlay
This commit is contained in:
parent
a128a2cb19
commit
887dfa2ad5
14 changed files with 190 additions and 5 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<TMDBSeason>(
|
||||
`/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,
|
||||
}));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ type MWMediaMetaBase = {
|
|||
id: string;
|
||||
year?: string;
|
||||
poster?: string;
|
||||
overview?: string;
|
||||
};
|
||||
|
||||
type MWMediaMetaSpecific =
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ export type TMDBMediaResult = {
|
|||
original_release_date?: Date;
|
||||
object_type: TMDBContentTypes;
|
||||
seasons?: TMDBSeasonShort[];
|
||||
overview?: string;
|
||||
};
|
||||
|
||||
export type TMDBSeasonMetaResult = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
87
src/components/player/overlays/PauseOverlay.tsx
Normal file
87
src/components/player/overlays/PauseOverlay.tsx
Normal file
|
|
@ -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<string | null>(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 (
|
||||
<div
|
||||
className={`absolute inset-0 z-[60] flex items-center bg-black/60 transition-opacity duration-500 ${
|
||||
shouldShow
|
||||
? "opacity-100 pointer-events-auto"
|
||||
: "opacity-0 pointer-events-none"
|
||||
}`}
|
||||
>
|
||||
<div className="ml-16 max-w-2xl p-8 pointer-events-auto">
|
||||
{logoUrl ? (
|
||||
<img
|
||||
src={logoUrl}
|
||||
alt={meta.title}
|
||||
className="mb-6 max-h-32 object-contain drop-shadow-lg"
|
||||
/>
|
||||
) : (
|
||||
<h1 className="mb-4 text-4xl font-bold text-white drop-shadow-lg">
|
||||
{meta.title}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
{meta.type === "show" && meta.episode && (
|
||||
<h2 className="mb-2 text-2xl font-semibold text-white/90 drop-shadow-md">
|
||||
{meta.episode.title}
|
||||
</h2>
|
||||
)}
|
||||
|
||||
{overview && (
|
||||
<p className="text-lg text-white/80 drop-shadow-md line-clamp-6">
|
||||
{overview}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Player.Container onLoad={props.onLoad} showingControls={showTargets}>
|
||||
{props.children}
|
||||
<PauseOverlay />
|
||||
<Player.BlackOverlay
|
||||
show={showTargets && status === playerStatus.PLAYING}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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: {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pause Overlay */}
|
||||
<div>
|
||||
<p className="text-white font-bold mb-3">
|
||||
{t("settings.appearance.options.pauseOverlay")}
|
||||
</p>
|
||||
<p className="max-w-[25rem] font-medium">
|
||||
{t("settings.appearance.options.pauseOverlayDescription")}
|
||||
</p>
|
||||
<div
|
||||
onClick={() =>
|
||||
!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",
|
||||
)}
|
||||
>
|
||||
<Toggle enabled={props.enablePauseOverlay} />
|
||||
<p className="flex-1 text-white font-bold">
|
||||
{t("settings.appearance.options.pauseOverlayLabel")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Carousel View */}
|
||||
<div>
|
||||
<p className="text-white font-bold mb-3">
|
||||
|
|
|
|||
|
|
@ -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?: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue