pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add pause overlay

Pas 887dfa2a a128a2cb

+190 -5
+5 -3
src/assets/locales/en.json
··· 1257 1257 "homeSectionOrder": "Home section order", 1258 1258 "homeSectionOrderDescription": "Drag and drop to reorder the watching and bookmarks sections on your homepage. Group order can be editied from the home page.", 1259 1259 "forceCompactEpisodeViewLabel": "Compact episodes", 1260 - "homeSectionOrderGroups": "Reorder bookmark groups" 1260 + "homeSectionOrderGroups": "Reorder bookmark groups", 1261 + "pauseOverlay": "Pause overlay", 1262 + "pauseOverlayDescription": "Show a title/logo and description overlay when the player is paused and idle.", 1263 + "pauseOverlayLabel": "Pause overlay" 1261 1264 }, 1262 1265 "sections": { 1263 1266 "watching": "Currently Watching", ··· 1442 1445 "genreMovies": "{{genre}} Movies", 1443 1446 "genreShows": "{{genre}} Shows", 1444 1447 "categoryMovies": "{{category}} Movies", 1445 - "categoryShows": "{{category}} Shows", 1446 - "top10": "Top 10" 1448 + "categoryShows": "{{category}} Shows" 1447 1449 }, 1448 1450 "change": "Change", 1449 1451 "more": "View more"
+2
src/backend/accounts/settings.ts
··· 44 44 manualSourceSelection?: boolean; 45 45 enableDoubleClickToSeek?: boolean; 46 46 enableAutoResumeOnPlaybackError?: boolean; 47 + enablePauseOverlay?: boolean; 47 48 enableNumberKeySeeking?: boolean; 48 49 keyboardShortcuts?: KeyboardShortcuts; 49 50 customTheme?: CustomThemeSettings; ··· 83 84 manualSourceSelection?: boolean; 84 85 enableDoubleClickToSeek?: boolean; 85 86 enableAutoResumeOnPlaybackError?: boolean; 87 + enablePauseOverlay?: boolean; 86 88 enableNumberKeySeeking?: boolean; 87 89 keyboardShortcuts?: KeyboardShortcuts; 88 90 customTheme?: CustomThemeSettings;
+2
src/backend/metadata/getmeta.ts
··· 47 47 object_type: mediaTypeToTMDB(type), 48 48 poster: getMediaPoster(movie.poster_path) ?? undefined, 49 49 original_release_date: new Date(movie.release_date), 50 + overview: movie.overview || undefined, 50 51 }; 51 52 } 52 53 if (type === MWMediaType.SERIES) { ··· 62 63 })), 63 64 poster: getMediaPoster(show.poster_path) ?? undefined, 64 65 original_release_date: new Date(show.first_air_date), 66 + overview: show.overview, 65 67 }; 66 68 } 67 69
+13 -2
src/backend/metadata/tmdb.ts
··· 79 79 year: media.original_release_date?.getFullYear()?.toString(), 80 80 poster: media.poster, 81 81 type, 82 + overview: media.overview, 82 83 seasons: seasons as any, 83 84 seasonData: season 84 85 ? { ··· 408 409 const item = seasonsQueue.shift(); 409 410 if (!item) break; 410 411 const { season, index } = item; 411 - const episodes = await getSeasonDetails(id, season.season_number); 412 - allEpisodesBySeason[index] = episodes; 412 + const seasonData = await get<TMDBSeason>( 413 + `/tv/${id}/season/${season.season_number}`, 414 + ); 415 + allEpisodesBySeason[index] = seasonData.episodes.map((episode) => ({ 416 + id: episode.id, 417 + name: episode.name, 418 + episode_number: episode.episode_number, 419 + overview: episode.overview, 420 + still_path: episode.still_path, 421 + air_date: episode.air_date, 422 + season_number: season.season_number, 423 + })); 413 424 } 414 425 }, 415 426 );
+1
src/backend/metadata/types/mw.ts
··· 29 29 id: string; 30 30 year?: string; 31 31 poster?: string; 32 + overview?: string; 32 33 }; 33 34 34 35 type MWMediaMetaSpecific =
+1
src/backend/metadata/types/tmdb.ts
··· 25 25 original_release_date?: Date; 26 26 object_type: TMDBContentTypes; 27 27 seasons?: TMDBSeasonShort[]; 28 + overview?: string; 28 29 }; 29 30 30 31 export type TMDBSeasonMetaResult = {
+4
src/components/player/hooks/usePlayerMeta.ts
··· 36 36 poster: m.meta.poster, 37 37 tmdbId: m.tmdbId ?? "", 38 38 imdbId: m.imdbId, 39 + overview: m.meta.overview, 39 40 episodes: m.meta.seasonData.episodes.map((v) => ({ 40 41 number: v.number, 41 42 title: v.title, 42 43 tmdbId: v.id, 43 44 air_date: v.air_date, 45 + overview: v.overview, 44 46 })), 45 47 episode: { 46 48 number: ep.number, 47 49 title: ep.title, 48 50 tmdbId: ep.id, 49 51 air_date: ep.air_date, 52 + overview: ep.overview, 50 53 }, 51 54 season: { 52 55 number: m.meta.seasonData.number, ··· 62 65 poster: m.meta.poster, 63 66 tmdbId: m.tmdbId ?? "", 64 67 imdbId: m.imdbId, 68 + overview: m.meta.overview, 65 69 }; 66 70 } 67 71 setDirectMeta(playerMeta);
+87
src/components/player/overlays/PauseOverlay.tsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { useIdle } from "react-use"; 3 + 4 + import { getMediaLogo } from "@/backend/metadata/tmdb"; 5 + import { TMDBContentTypes } from "@/backend/metadata/types/tmdb"; 6 + import { usePlayerStore } from "@/stores/player/store"; 7 + import { usePreferencesStore } from "@/stores/preferences"; 8 + 9 + export function PauseOverlay() { 10 + const isIdle = useIdle(10e3); // 10 seconds 11 + const isPaused = usePlayerStore((s) => s.mediaPlaying.isPaused); 12 + const meta = usePlayerStore((s) => s.meta); 13 + const enablePauseOverlay = usePreferencesStore((s) => s.enablePauseOverlay); 14 + const enableImageLogos = usePreferencesStore((s) => s.enableImageLogos); 15 + const [logoUrl, setLogoUrl] = useState<string | null>(null); 16 + 17 + const shouldShow = isPaused && isIdle && enablePauseOverlay; 18 + 19 + useEffect(() => { 20 + let mounted = true; 21 + const fetchLogo = async () => { 22 + if (!meta?.tmdbId || !enableImageLogos) { 23 + setLogoUrl(null); 24 + return; 25 + } 26 + 27 + try { 28 + const type = 29 + meta.type === "movie" ? TMDBContentTypes.MOVIE : TMDBContentTypes.TV; 30 + const url = await getMediaLogo(meta.tmdbId, type); 31 + if (mounted) setLogoUrl(url || null); 32 + } catch { 33 + if (mounted) setLogoUrl(null); 34 + } 35 + }; 36 + 37 + fetchLogo(); 38 + return () => { 39 + mounted = false; 40 + }; 41 + }, [meta?.tmdbId, meta?.type, enableImageLogos]); 42 + 43 + if (!meta) return null; 44 + 45 + const overview = 46 + meta.type === "show" ? meta.episode?.overview : meta.overview; 47 + 48 + // Don't render anything if we don't have content, but keep structure for fade if valid 49 + const hasContent = overview || logoUrl || meta.title; 50 + if (!hasContent) return null; 51 + 52 + return ( 53 + <div 54 + className={`absolute inset-0 z-[60] flex items-center bg-black/60 transition-opacity duration-500 ${ 55 + shouldShow 56 + ? "opacity-100 pointer-events-auto" 57 + : "opacity-0 pointer-events-none" 58 + }`} 59 + > 60 + <div className="ml-16 max-w-2xl p-8 pointer-events-auto"> 61 + {logoUrl ? ( 62 + <img 63 + src={logoUrl} 64 + alt={meta.title} 65 + className="mb-6 max-h-32 object-contain drop-shadow-lg" 66 + /> 67 + ) : ( 68 + <h1 className="mb-4 text-4xl font-bold text-white drop-shadow-lg"> 69 + {meta.title} 70 + </h1> 71 + )} 72 + 73 + {meta.type === "show" && meta.episode && ( 74 + <h2 className="mb-2 text-2xl font-semibold text-white/90 drop-shadow-md"> 75 + {meta.episode.title} 76 + </h2> 77 + )} 78 + 79 + {overview && ( 80 + <p className="text-lg text-white/80 drop-shadow-md line-clamp-6"> 81 + {overview} 82 + </p> 83 + )} 84 + </div> 85 + </div> 86 + ); 87 + }
+14
src/hooks/useSettingsState.ts
··· 81 81 manualSourceSelection: boolean, 82 82 enableDoubleClickToSeek: boolean, 83 83 enableAutoResumeOnPlaybackError: boolean, 84 + enablePauseOverlay: boolean, 84 85 customTheme: { 85 86 primary: string; 86 87 secondary: string; ··· 277 278 resetEnableAutoResumeOnPlaybackError, 278 279 enableAutoResumeOnPlaybackErrorChanged, 279 280 ] = useDerived(enableAutoResumeOnPlaybackError); 281 + const [ 282 + enablePauseOverlayState, 283 + setEnablePauseOverlayState, 284 + resetEnablePauseOverlay, 285 + enablePauseOverlayChanged, 286 + ] = useDerived(enablePauseOverlay); 280 287 const [ 281 288 customThemeState, 282 289 setCustomThemeState, ··· 323 330 resetManualSourceSelection(); 324 331 resetEnableDoubleClickToSeek(); 325 332 resetEnableAutoResumeOnPlaybackError(); 333 + resetEnablePauseOverlay(); 326 334 resetCustomTheme(); 327 335 } 328 336 ··· 364 372 manualSourceSelectionChanged || 365 373 enableDoubleClickToSeekChanged || 366 374 enableAutoResumeOnPlaybackErrorChanged || 375 + enablePauseOverlayChanged || 367 376 customThemeChanged; 368 377 369 378 return { ··· 553 562 state: enableAutoResumeOnPlaybackErrorState, 554 563 set: setEnableAutoResumeOnPlaybackErrorState, 555 564 changed: enableAutoResumeOnPlaybackErrorChanged, 565 + }, 566 + enablePauseOverlay: { 567 + state: enablePauseOverlayState, 568 + set: setEnablePauseOverlayState, 569 + changed: enablePauseOverlayChanged, 556 570 }, 557 571 customTheme: { 558 572 state: customThemeState,
+16
src/pages/Settings.tsx
··· 531 531 (s) => s.setEnableAutoResumeOnPlaybackError, 532 532 ); 533 533 534 + const enablePauseOverlay = usePreferencesStore((s) => s.enablePauseOverlay); 535 + const setEnablePauseOverlay = usePreferencesStore( 536 + (s) => s.setEnablePauseOverlay, 537 + ); 538 + 534 539 const account = useAuthStore((s) => s.account); 535 540 const updateProfile = useAuthStore((s) => s.setAccountProfile); 536 541 const updateDeviceName = useAuthStore((s) => s.updateDeviceName); ··· 646 651 setEnableAutoResumeOnPlaybackError( 647 652 settings.enableAutoResumeOnPlaybackError, 648 653 ); 654 + } 655 + if (settings.enablePauseOverlay !== undefined) { 656 + setEnablePauseOverlay(settings.enablePauseOverlay); 649 657 } 650 658 if (settings.customTheme) { 651 659 setCustomTheme(settings.customTheme); ··· 687 695 setManualSourceSelection, 688 696 setEnableDoubleClickToSeek, 689 697 setEnableAutoResumeOnPlaybackError, 698 + setEnablePauseOverlay, 690 699 setCustomTheme, 691 700 ]); 692 701 ··· 728 737 manualSourceSelection, 729 738 enableDoubleClickToSeek, 730 739 enableAutoResumeOnPlaybackError, 740 + enablePauseOverlay, 731 741 customThemeBaseline ?? customTheme, 732 742 ); 733 743 ··· 797 807 state.manualSourceSelection.changed || 798 808 state.enableDoubleClickToSeek.changed || 799 809 state.enableAutoResumeOnPlaybackError.changed || 810 + state.enablePauseOverlay.changed || 800 811 state.customTheme.changed 801 812 ) { 802 813 await updateSettings(backendUrl, account, { ··· 829 840 enableDoubleClickToSeek: state.enableDoubleClickToSeek.state, 830 841 enableAutoResumeOnPlaybackError: 831 842 state.enableAutoResumeOnPlaybackError.state, 843 + enablePauseOverlay: state.enablePauseOverlay.state, 832 844 customTheme: state.customTheme.state, 833 845 }); 834 846 } ··· 889 901 setEnableAutoResumeOnPlaybackError( 890 902 state.enableAutoResumeOnPlaybackError.state, 891 903 ); 904 + setEnablePauseOverlay(state.enablePauseOverlay.state); 892 905 setCustomTheme(state.customTheme.state); 893 906 setCustomThemeBaseline(state.customTheme.state); 894 907 ··· 951 964 setManualSourceSelection, 952 965 setEnableDoubleClickToSeek, 953 966 setEnableAutoResumeOnPlaybackError, 967 + setEnablePauseOverlay, 954 968 setCustomTheme, 955 969 ]); 956 970 return ( ··· 1067 1081 homeSectionOrder={state.homeSectionOrder.state} 1068 1082 setHomeSectionOrder={state.homeSectionOrder.set} 1069 1083 enableLowPerformanceMode={state.enableLowPerformanceMode.state} 1084 + enablePauseOverlay={state.enablePauseOverlay.state} 1085 + setEnablePauseOverlay={state.enablePauseOverlay.set} 1070 1086 customTheme={state.customTheme.state} 1071 1087 setCustomTheme={state.customTheme.set} 1072 1088 />
+2
src/pages/parts/player/PlayerPart.tsx
··· 11 11 SegmentData, 12 12 useSkipTime, 13 13 } from "@/components/player/hooks/useSkipTime"; 14 + import { PauseOverlay } from "@/components/player/overlays/PauseOverlay"; 14 15 import { useIsMobile } from "@/hooks/useIsMobile"; 15 16 import { PlayerMeta, playerStatus } from "@/stores/player/slices/source"; 16 17 import { usePlayerStore } from "@/stores/player/store"; ··· 99 100 return ( 100 101 <Player.Container onLoad={props.onLoad} showingControls={showTargets}> 101 102 {props.children} 103 + <PauseOverlay /> 102 104 <Player.BlackOverlay 103 105 show={showTargets && status === playerStatus.PLAYING} 104 106 />
+33
src/pages/parts/settings/AppearancePart.tsx
··· 292 292 enableImageLogos: boolean; 293 293 setEnableImageLogos: (v: boolean) => void; 294 294 295 + enablePauseOverlay: boolean; 296 + setEnablePauseOverlay: (v: boolean) => void; 297 + 295 298 enableCarouselView: boolean; 296 299 setEnableCarouselView: (v: boolean) => void; 297 300 ··· 355 358 setEnableFeatured, 356 359 setEnableDetailsModal, 357 360 setEnableImageLogos, 361 + setEnablePauseOverlay, 358 362 setForceCompactEpisodeView, 359 363 } = props; 360 364 ··· 365 369 setEnableFeatured(false); 366 370 setEnableDetailsModal(false); 367 371 setEnableImageLogos(false); 372 + setEnablePauseOverlay(false); 368 373 setForceCompactEpisodeView(true); 369 374 } 370 375 }, [ ··· 373 378 setEnableFeatured, 374 379 setEnableDetailsModal, 375 380 setEnableImageLogos, 381 + setEnablePauseOverlay, 376 382 setForceCompactEpisodeView, 377 383 ]); 378 384 ··· 549 555 <Toggle enabled={props.enableImageLogos} /> 550 556 <p className="flex-1 text-white font-bold"> 551 557 {t("settings.appearance.options.logosLabel")} 558 + </p> 559 + </div> 560 + </div> 561 + 562 + {/* Pause Overlay */} 563 + <div> 564 + <p className="text-white font-bold mb-3"> 565 + {t("settings.appearance.options.pauseOverlay")} 566 + </p> 567 + <p className="max-w-[25rem] font-medium"> 568 + {t("settings.appearance.options.pauseOverlayDescription")} 569 + </p> 570 + <div 571 + onClick={() => 572 + !props.enableLowPerformanceMode && 573 + props.setEnablePauseOverlay(!props.enablePauseOverlay) 574 + } 575 + className={classNames( 576 + "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", 577 + props.enableLowPerformanceMode 578 + ? "cursor-not-allowed opacity-50 pointer-events-none" 579 + : "cursor-pointer opacity-100 pointer-events-auto", 580 + )} 581 + > 582 + <Toggle enabled={props.enablePauseOverlay} /> 583 + <p className="flex-1 text-white font-bold"> 584 + {t("settings.appearance.options.pauseOverlayLabel")} 552 585 </p> 553 586 </div> 554 587 </div>
+2
src/stores/player/slices/source.ts
··· 29 29 tmdbId: string; 30 30 title: string; 31 31 air_date?: string; 32 + overview?: string; 32 33 } 33 34 34 35 export interface PlayerMeta { ··· 38 39 imdbId?: string; 39 40 releaseYear: number; 40 41 poster?: string; 42 + overview?: string; 41 43 episodes?: PlayerMetaEpisode[]; 42 44 episode?: PlayerMetaEpisode; 43 45 season?: {
+8
src/stores/preferences/index.tsx
··· 39 39 enableDoubleClickToSeek: boolean; 40 40 enableAutoResumeOnPlaybackError: boolean; 41 41 enableNumberKeySeeking: boolean; 42 + enablePauseOverlay: boolean; 42 43 keyboardShortcuts: KeyboardShortcuts; 43 44 44 45 setEnableThumbnails(v: boolean): void; ··· 72 73 setEnableDoubleClickToSeek(v: boolean): void; 73 74 setEnableAutoResumeOnPlaybackError(v: boolean): void; 74 75 setEnableNumberKeySeeking(v: boolean): void; 76 + setEnablePauseOverlay(v: boolean): void; 75 77 setKeyboardShortcuts(v: KeyboardShortcuts): void; 76 78 } 77 79 ··· 109 111 enableDoubleClickToSeek: false, 110 112 enableAutoResumeOnPlaybackError: true, 111 113 enableNumberKeySeeking: true, 114 + enablePauseOverlay: false, 112 115 keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS, 113 116 setEnableThumbnails(v) { 114 117 set((s) => { ··· 268 271 setEnableNumberKeySeeking(v) { 269 272 set((s) => { 270 273 s.enableNumberKeySeeking = v; 274 + }); 275 + }, 276 + setEnablePauseOverlay(v) { 277 + set((s) => { 278 + s.enablePauseOverlay = v; 271 279 }); 272 280 }, 273 281 setKeyboardShortcuts(v) {