mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 05:12:23 +00:00
add compact episodes view
This commit is contained in:
parent
7c89b2ffc1
commit
d6f132b4c9
7 changed files with 117 additions and 9 deletions
|
|
@ -792,7 +792,10 @@
|
||||||
"logosLabel": "Image logos",
|
"logosLabel": "Image logos",
|
||||||
"carouselView": "Carousel view",
|
"carouselView": "Carousel view",
|
||||||
"carouselViewDescription": "Display your currently watching and bookmark sections as carousels instead of a grid. Disabled by default.",
|
"carouselViewDescription": "Display your currently watching and bookmark sections as carousels instead of a grid. Disabled by default.",
|
||||||
"carouselViewLabel": "Carousel view"
|
"carouselViewLabel": "Carousel view",
|
||||||
|
"forceCompactEpisodeView": "Force compact episode view",
|
||||||
|
"forceCompactEpisodeViewDescription": "Force the episode carousel in the player to use the \"classic\" compact vertical view. Disabled by default.",
|
||||||
|
"forceCompactEpisodeViewLabel": "Compact episodes"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connections": {
|
"connections": {
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ export interface SettingsInput {
|
||||||
enableDetailsModal?: boolean;
|
enableDetailsModal?: boolean;
|
||||||
enableImageLogos?: boolean;
|
enableImageLogos?: boolean;
|
||||||
enableCarouselView?: boolean;
|
enableCarouselView?: boolean;
|
||||||
|
forceCompactEpisodeView?: boolean;
|
||||||
sourceOrder?: string[];
|
sourceOrder?: string[];
|
||||||
enableSourceOrder?: boolean;
|
enableSourceOrder?: boolean;
|
||||||
proxyTmdb?: boolean;
|
proxyTmdb?: boolean;
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
|
||||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||||
import { usePlayerStore } from "@/stores/player/store";
|
import { usePlayerStore } from "@/stores/player/store";
|
||||||
|
import { usePreferencesStore } from "@/stores/preferences";
|
||||||
import { useProgressStore } from "@/stores/progress";
|
import { useProgressStore } from "@/stores/progress";
|
||||||
|
|
||||||
import { hasAired } from "../utils/aired";
|
import { hasAired } from "../utils/aired";
|
||||||
|
|
@ -122,6 +123,9 @@ export function EpisodesView({
|
||||||
const descriptionRefs = useRef<{
|
const descriptionRefs = useRef<{
|
||||||
[key: string]: HTMLParagraphElement | null;
|
[key: string]: HTMLParagraphElement | null;
|
||||||
}>({});
|
}>({});
|
||||||
|
const forceCompactEpisodeView = usePreferencesStore(
|
||||||
|
(s) => s.forceCompactEpisodeView,
|
||||||
|
);
|
||||||
|
|
||||||
const isTextTruncated = (element: HTMLElement | null) => {
|
const isTextTruncated = (element: HTMLElement | null) => {
|
||||||
if (!element) return false;
|
if (!element) return false;
|
||||||
|
|
@ -248,7 +252,12 @@ export function EpisodesView({
|
||||||
content = (
|
content = (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{/* Horizontal scroll buttons */}
|
{/* Horizontal scroll buttons */}
|
||||||
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"absolute left-0 top-1/2 transform -translate-y-1/2 z-10 px-4",
|
||||||
|
forceCompactEpisodeView ? "hidden" : "hidden lg:block",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||||
|
|
@ -261,8 +270,14 @@ export function EpisodesView({
|
||||||
<div
|
<div
|
||||||
ref={carouselRef}
|
ref={carouselRef}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"flex flex-col lg:flex-row lg:overflow-x-auto space-y-3 sm:space-y-4 lg:space-y-0 lg:space-x-4 pb-4 pt-2 lg:px-12 scrollbar-hide",
|
"flex pb-4 pt-2 scrollbar-hide",
|
||||||
{ "carousel-container": window.innerWidth >= 1024 },
|
{
|
||||||
|
"carousel-container":
|
||||||
|
window.innerWidth >= 1024 && !forceCompactEpisodeView,
|
||||||
|
},
|
||||||
|
forceCompactEpisodeView
|
||||||
|
? "flex-col space-y-3"
|
||||||
|
: "flex-col lg:flex-row lg:overflow-x-auto space-y-3 sm:space-y-4 lg:space-y-0 lg:space-x-4 lg:px-12 ",
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
scrollbarWidth: "none",
|
scrollbarWidth: "none",
|
||||||
|
|
@ -289,7 +304,12 @@ export function EpisodesView({
|
||||||
return (
|
return (
|
||||||
<div key={ep.id} ref={isActive ? activeEpisodeRef : null}>
|
<div key={ep.id} ref={isActive ? activeEpisodeRef : null}>
|
||||||
{/* Extra small screens - Simple vertical list with no thumbnails */}
|
{/* Extra small screens - Simple vertical list with no thumbnails */}
|
||||||
<div className="block sm:hidden w-full px-3">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"block w-full px-3",
|
||||||
|
forceCompactEpisodeView ? "" : "sm:hidden",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Menu.Link
|
<Menu.Link
|
||||||
onClick={() => playEpisode(ep.id)}
|
onClick={() => playEpisode(ep.id)}
|
||||||
active={isActive}
|
active={isActive}
|
||||||
|
|
@ -327,6 +347,7 @@ export function EpisodesView({
|
||||||
onClick={() => playEpisode(ep.id)}
|
onClick={() => playEpisode(ep.id)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"hidden sm:flex lg:hidden w-full rounded-lg overflow-hidden transition-all duration-200 relative cursor-pointer",
|
"hidden sm:flex lg:hidden w-full rounded-lg overflow-hidden transition-all duration-200 relative cursor-pointer",
|
||||||
|
forceCompactEpisodeView ? "!hidden" : "",
|
||||||
isActive
|
isActive
|
||||||
? "bg-video-context-hoverColor/50"
|
? "bg-video-context-hoverColor/50"
|
||||||
: "hover:bg-video-context-hoverColor/50",
|
: "hover:bg-video-context-hoverColor/50",
|
||||||
|
|
@ -430,6 +451,7 @@ export function EpisodesView({
|
||||||
onClick={() => playEpisode(ep.id)}
|
onClick={() => playEpisode(ep.id)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"hidden lg:block flex-shrink-0 transition-all duration-200 relative cursor-pointer rounded-lg overflow-hidden",
|
"hidden lg:block flex-shrink-0 transition-all duration-200 relative cursor-pointer rounded-lg overflow-hidden",
|
||||||
|
forceCompactEpisodeView ? "!hidden" : "",
|
||||||
isActive
|
isActive
|
||||||
? "bg-video-context-hoverColor/50"
|
? "bg-video-context-hoverColor/50"
|
||||||
: "hover:bg-video-context-hoverColor/50",
|
: "hover:bg-video-context-hoverColor/50",
|
||||||
|
|
@ -547,7 +569,12 @@ export function EpisodesView({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right scroll button */}
|
{/* Right scroll button */}
|
||||||
<div className="absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4 hidden lg:block">
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"absolute right-0 top-1/2 transform -translate-y-1/2 z-10 px-4",
|
||||||
|
forceCompactEpisodeView ? "hidden" : "hidden lg:block",
|
||||||
|
)}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
className="p-2 bg-black/80 hover:bg-video-context-hoverColor transition-colors rounded-full border border-video-context-border backdrop-blur-sm"
|
||||||
|
|
@ -597,13 +624,25 @@ function EpisodesOverlay({
|
||||||
[router],
|
[router],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const forceCompactEpisodeView = usePreferencesStore(
|
||||||
|
(s) => s.forceCompactEpisodeView,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Overlay id={id}>
|
<Overlay id={id}>
|
||||||
<OverlayRouter id={id}>
|
<OverlayRouter id={id}>
|
||||||
<OverlayPage id={id} path="/" width={343} height={431}>
|
<OverlayPage id={id} path="/" width={343} height={431}>
|
||||||
<SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} />
|
<SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} />
|
||||||
</OverlayPage>
|
</OverlayPage>
|
||||||
<OverlayPage id={id} path="/episodes" width={0} height={375} fullWidth>
|
<OverlayPage
|
||||||
|
id={id}
|
||||||
|
path="/episodes"
|
||||||
|
width={343}
|
||||||
|
height={
|
||||||
|
forceCompactEpisodeView || window.innerWidth < 1024 ? 431 : 375
|
||||||
|
}
|
||||||
|
fullWidth={!forceCompactEpisodeView}
|
||||||
|
>
|
||||||
{selectedSeason.length > 0 ? (
|
{selectedSeason.length > 0 ? (
|
||||||
<EpisodesView
|
<EpisodesView
|
||||||
selectedSeason={selectedSeason}
|
selectedSeason={selectedSeason}
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ export function useSettingsState(
|
||||||
enableSkipCredits: boolean,
|
enableSkipCredits: boolean,
|
||||||
enableImageLogos: boolean,
|
enableImageLogos: boolean,
|
||||||
enableCarouselView: boolean,
|
enableCarouselView: boolean,
|
||||||
|
forceCompactEpisodeView: boolean,
|
||||||
) {
|
) {
|
||||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||||
useDerived(proxyUrls);
|
useDerived(proxyUrls);
|
||||||
|
|
@ -160,6 +161,12 @@ export function useSettingsState(
|
||||||
resetEnableCarouselView,
|
resetEnableCarouselView,
|
||||||
enableCarouselViewChanged,
|
enableCarouselViewChanged,
|
||||||
] = useDerived(enableCarouselView);
|
] = useDerived(enableCarouselView);
|
||||||
|
const [
|
||||||
|
forceCompactEpisodeViewState,
|
||||||
|
setForceCompactEpisodeViewState,
|
||||||
|
resetForceCompactEpisodeView,
|
||||||
|
forceCompactEpisodeViewChanged,
|
||||||
|
] = useDerived(forceCompactEpisodeView);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
resetTheme();
|
resetTheme();
|
||||||
|
|
@ -183,6 +190,7 @@ export function useSettingsState(
|
||||||
resetEnableSourceOrder();
|
resetEnableSourceOrder();
|
||||||
resetProxyTmdb();
|
resetProxyTmdb();
|
||||||
resetEnableCarouselView();
|
resetEnableCarouselView();
|
||||||
|
resetForceCompactEpisodeView();
|
||||||
}
|
}
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
|
|
@ -205,7 +213,8 @@ export function useSettingsState(
|
||||||
sourceOrderChanged ||
|
sourceOrderChanged ||
|
||||||
enableSourceOrderChanged ||
|
enableSourceOrderChanged ||
|
||||||
proxyTmdbChanged ||
|
proxyTmdbChanged ||
|
||||||
enableCarouselViewChanged;
|
enableCarouselViewChanged ||
|
||||||
|
forceCompactEpisodeViewChanged;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reset,
|
reset,
|
||||||
|
|
@ -310,5 +319,10 @@ export function useSettingsState(
|
||||||
set: setEnableCarouselViewState,
|
set: setEnableCarouselViewState,
|
||||||
changed: enableCarouselViewChanged,
|
changed: enableCarouselViewChanged,
|
||||||
},
|
},
|
||||||
|
forceCompactEpisodeView: {
|
||||||
|
state: forceCompactEpisodeViewState,
|
||||||
|
set: setForceCompactEpisodeViewState,
|
||||||
|
changed: forceCompactEpisodeViewChanged,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,13 @@ export function SettingsPage() {
|
||||||
(s) => s.setEnableCarouselView,
|
(s) => s.setEnableCarouselView,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const forceCompactEpisodeView = usePreferencesStore(
|
||||||
|
(s) => s.forceCompactEpisodeView,
|
||||||
|
);
|
||||||
|
const setForceCompactEpisodeView = usePreferencesStore(
|
||||||
|
(s) => s.setForceCompactEpisodeView,
|
||||||
|
);
|
||||||
|
|
||||||
const account = useAuthStore((s) => s.account);
|
const account = useAuthStore((s) => s.account);
|
||||||
const updateProfile = useAuthStore((s) => s.setAccountProfile);
|
const updateProfile = useAuthStore((s) => s.setAccountProfile);
|
||||||
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
|
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
|
||||||
|
|
@ -227,6 +234,7 @@ export function SettingsPage() {
|
||||||
enableSkipCredits,
|
enableSkipCredits,
|
||||||
enableImageLogos,
|
enableImageLogos,
|
||||||
enableCarouselView,
|
enableCarouselView,
|
||||||
|
forceCompactEpisodeView,
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableSources = useMemo(() => {
|
const availableSources = useMemo(() => {
|
||||||
|
|
@ -282,7 +290,8 @@ export function SettingsPage() {
|
||||||
state.sourceOrder.changed ||
|
state.sourceOrder.changed ||
|
||||||
state.enableSourceOrder.changed ||
|
state.enableSourceOrder.changed ||
|
||||||
state.proxyTmdb.changed ||
|
state.proxyTmdb.changed ||
|
||||||
state.enableCarouselView.changed
|
state.enableCarouselView.changed ||
|
||||||
|
state.forceCompactEpisodeView.changed
|
||||||
) {
|
) {
|
||||||
await updateSettings(backendUrl, account, {
|
await updateSettings(backendUrl, account, {
|
||||||
applicationLanguage: state.appLanguage.state,
|
applicationLanguage: state.appLanguage.state,
|
||||||
|
|
@ -301,6 +310,7 @@ export function SettingsPage() {
|
||||||
enableSourceOrder: state.enableSourceOrder.state,
|
enableSourceOrder: state.enableSourceOrder.state,
|
||||||
proxyTmdb: state.proxyTmdb.state,
|
proxyTmdb: state.proxyTmdb.state,
|
||||||
enableCarouselView: state.enableCarouselView.state,
|
enableCarouselView: state.enableCarouselView.state,
|
||||||
|
forceCompactEpisodeView: state.forceCompactEpisodeView.state,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (state.deviceName.changed) {
|
if (state.deviceName.changed) {
|
||||||
|
|
@ -337,6 +347,7 @@ export function SettingsPage() {
|
||||||
setRealDebridKey(state.realDebridKey.state);
|
setRealDebridKey(state.realDebridKey.state);
|
||||||
setProxyTmdb(state.proxyTmdb.state);
|
setProxyTmdb(state.proxyTmdb.state);
|
||||||
setEnableCarouselView(state.enableCarouselView.state);
|
setEnableCarouselView(state.enableCarouselView.state);
|
||||||
|
setForceCompactEpisodeView(state.forceCompactEpisodeView.state);
|
||||||
|
|
||||||
if (state.profile.state) {
|
if (state.profile.state) {
|
||||||
updateProfile(state.profile.state);
|
updateProfile(state.profile.state);
|
||||||
|
|
@ -378,6 +389,7 @@ export function SettingsPage() {
|
||||||
setEnableSourceOrder,
|
setEnableSourceOrder,
|
||||||
setProxyTmdb,
|
setProxyTmdb,
|
||||||
setEnableCarouselView,
|
setEnableCarouselView,
|
||||||
|
setForceCompactEpisodeView,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
|
|
@ -443,6 +455,8 @@ export function SettingsPage() {
|
||||||
setEnableImageLogos={state.enableImageLogos.set}
|
setEnableImageLogos={state.enableImageLogos.set}
|
||||||
enableCarouselView={state.enableCarouselView.state}
|
enableCarouselView={state.enableCarouselView.state}
|
||||||
setEnableCarouselView={state.enableCarouselView.set}
|
setEnableCarouselView={state.enableCarouselView.set}
|
||||||
|
forceCompactEpisodeView={state.forceCompactEpisodeView.state}
|
||||||
|
setForceCompactEpisodeView={state.forceCompactEpisodeView.set}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="settings-captions" className="mt-28">
|
<div id="settings-captions" className="mt-28">
|
||||||
|
|
|
||||||
|
|
@ -216,6 +216,9 @@ export function AppearancePart(props: {
|
||||||
|
|
||||||
enableCarouselView: boolean;
|
enableCarouselView: boolean;
|
||||||
setEnableCarouselView: (v: boolean) => void;
|
setEnableCarouselView: (v: boolean) => void;
|
||||||
|
|
||||||
|
forceCompactEpisodeView: boolean;
|
||||||
|
setForceCompactEpisodeView: (v: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
@ -389,6 +392,32 @@ export function AppearancePart(props: {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Force Compact Episode View */}
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold mb-3">
|
||||||
|
{t("settings.appearance.options.forceCompactEpisodeView")}
|
||||||
|
</p>
|
||||||
|
<p className="max-w-[25rem] font-medium">
|
||||||
|
{t(
|
||||||
|
"settings.appearance.options.forceCompactEpisodeViewDescription",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
props.setForceCompactEpisodeView(!props.forceCompactEpisodeView)
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
"cursor-pointer opacity-100 pointer-events-auto",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Toggle enabled={props.forceCompactEpisodeView} />
|
||||||
|
<p className="flex-1 text-white font-bold">
|
||||||
|
{t("settings.appearance.options.forceCompactEpisodeViewLabel")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Second Column - Themes */}
|
{/* Second Column - Themes */}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ export interface PreferencesStore {
|
||||||
enableDetailsModal: boolean;
|
enableDetailsModal: boolean;
|
||||||
enableImageLogos: boolean;
|
enableImageLogos: boolean;
|
||||||
enableCarouselView: boolean;
|
enableCarouselView: boolean;
|
||||||
|
forceCompactEpisodeView: boolean;
|
||||||
sourceOrder: string[];
|
sourceOrder: string[];
|
||||||
enableSourceOrder: boolean;
|
enableSourceOrder: boolean;
|
||||||
proxyTmdb: boolean;
|
proxyTmdb: boolean;
|
||||||
|
|
@ -25,6 +26,7 @@ export interface PreferencesStore {
|
||||||
setEnableDetailsModal(v: boolean): void;
|
setEnableDetailsModal(v: boolean): void;
|
||||||
setEnableImageLogos(v: boolean): void;
|
setEnableImageLogos(v: boolean): void;
|
||||||
setEnableCarouselView(v: boolean): void;
|
setEnableCarouselView(v: boolean): void;
|
||||||
|
setForceCompactEpisodeView(v: boolean): void;
|
||||||
setSourceOrder(v: string[]): void;
|
setSourceOrder(v: string[]): void;
|
||||||
setEnableSourceOrder(v: boolean): void;
|
setEnableSourceOrder(v: boolean): void;
|
||||||
setProxyTmdb(v: boolean): void;
|
setProxyTmdb(v: boolean): void;
|
||||||
|
|
@ -43,6 +45,7 @@ export const usePreferencesStore = create(
|
||||||
enableDetailsModal: false,
|
enableDetailsModal: false,
|
||||||
enableImageLogos: true,
|
enableImageLogos: true,
|
||||||
enableCarouselView: false,
|
enableCarouselView: false,
|
||||||
|
forceCompactEpisodeView: false,
|
||||||
sourceOrder: [],
|
sourceOrder: [],
|
||||||
enableSourceOrder: false,
|
enableSourceOrder: false,
|
||||||
proxyTmdb: false,
|
proxyTmdb: false,
|
||||||
|
|
@ -88,6 +91,11 @@ export const usePreferencesStore = create(
|
||||||
s.enableCarouselView = v;
|
s.enableCarouselView = v;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setForceCompactEpisodeView(v) {
|
||||||
|
set((s) => {
|
||||||
|
s.forceCompactEpisodeView = v;
|
||||||
|
});
|
||||||
|
},
|
||||||
setSourceOrder(v) {
|
setSourceOrder(v) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.sourceOrder = v;
|
s.sourceOrder = v;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue