mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-31 00:58:44 +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",
|
||||
"carouselView": "Carousel view",
|
||||
"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": {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface SettingsInput {
|
|||
enableDetailsModal?: boolean;
|
||||
enableImageLogos?: boolean;
|
||||
enableCarouselView?: boolean;
|
||||
forceCompactEpisodeView?: boolean;
|
||||
sourceOrder?: string[];
|
||||
enableSourceOrder?: boolean;
|
||||
proxyTmdb?: boolean;
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { Menu } from "@/components/player/internals/ContextMenu";
|
|||
import { useOverlayRouter } from "@/hooks/useOverlayRouter";
|
||||
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
|
||||
import { hasAired } from "../utils/aired";
|
||||
|
|
@ -122,6 +123,9 @@ export function EpisodesView({
|
|||
const descriptionRefs = useRef<{
|
||||
[key: string]: HTMLParagraphElement | null;
|
||||
}>({});
|
||||
const forceCompactEpisodeView = usePreferencesStore(
|
||||
(s) => s.forceCompactEpisodeView,
|
||||
);
|
||||
|
||||
const isTextTruncated = (element: HTMLElement | null) => {
|
||||
if (!element) return false;
|
||||
|
|
@ -248,7 +252,12 @@ export function EpisodesView({
|
|||
content = (
|
||||
<div className="relative">
|
||||
{/* 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
|
||||
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"
|
||||
|
|
@ -261,8 +270,14 @@ export function EpisodesView({
|
|||
<div
|
||||
ref={carouselRef}
|
||||
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",
|
||||
{ "carousel-container": window.innerWidth >= 1024 },
|
||||
"flex pb-4 pt-2 scrollbar-hide",
|
||||
{
|
||||
"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={{
|
||||
scrollbarWidth: "none",
|
||||
|
|
@ -289,7 +304,12 @@ export function EpisodesView({
|
|||
return (
|
||||
<div key={ep.id} ref={isActive ? activeEpisodeRef : null}>
|
||||
{/* 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
|
||||
onClick={() => playEpisode(ep.id)}
|
||||
active={isActive}
|
||||
|
|
@ -327,6 +347,7 @@ export function EpisodesView({
|
|||
onClick={() => playEpisode(ep.id)}
|
||||
className={classNames(
|
||||
"hidden sm:flex lg:hidden w-full rounded-lg overflow-hidden transition-all duration-200 relative cursor-pointer",
|
||||
forceCompactEpisodeView ? "!hidden" : "",
|
||||
isActive
|
||||
? "bg-video-context-hoverColor/50"
|
||||
: "hover:bg-video-context-hoverColor/50",
|
||||
|
|
@ -430,6 +451,7 @@ export function EpisodesView({
|
|||
onClick={() => playEpisode(ep.id)}
|
||||
className={classNames(
|
||||
"hidden lg:block flex-shrink-0 transition-all duration-200 relative cursor-pointer rounded-lg overflow-hidden",
|
||||
forceCompactEpisodeView ? "!hidden" : "",
|
||||
isActive
|
||||
? "bg-video-context-hoverColor/50"
|
||||
: "hover:bg-video-context-hoverColor/50",
|
||||
|
|
@ -547,7 +569,12 @@ export function EpisodesView({
|
|||
</div>
|
||||
|
||||
{/* 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
|
||||
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"
|
||||
|
|
@ -597,13 +624,25 @@ function EpisodesOverlay({
|
|||
[router],
|
||||
);
|
||||
|
||||
const forceCompactEpisodeView = usePreferencesStore(
|
||||
(s) => s.forceCompactEpisodeView,
|
||||
);
|
||||
|
||||
return (
|
||||
<Overlay id={id}>
|
||||
<OverlayRouter id={id}>
|
||||
<OverlayPage id={id} path="/" width={343} height={431}>
|
||||
<SeasonsView setSeason={setSeason} selectedSeason={selectedSeason} />
|
||||
</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 ? (
|
||||
<EpisodesView
|
||||
selectedSeason={selectedSeason}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ export function useSettingsState(
|
|||
enableSkipCredits: boolean,
|
||||
enableImageLogos: boolean,
|
||||
enableCarouselView: boolean,
|
||||
forceCompactEpisodeView: boolean,
|
||||
) {
|
||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||
useDerived(proxyUrls);
|
||||
|
|
@ -160,6 +161,12 @@ export function useSettingsState(
|
|||
resetEnableCarouselView,
|
||||
enableCarouselViewChanged,
|
||||
] = useDerived(enableCarouselView);
|
||||
const [
|
||||
forceCompactEpisodeViewState,
|
||||
setForceCompactEpisodeViewState,
|
||||
resetForceCompactEpisodeView,
|
||||
forceCompactEpisodeViewChanged,
|
||||
] = useDerived(forceCompactEpisodeView);
|
||||
|
||||
function reset() {
|
||||
resetTheme();
|
||||
|
|
@ -183,6 +190,7 @@ export function useSettingsState(
|
|||
resetEnableSourceOrder();
|
||||
resetProxyTmdb();
|
||||
resetEnableCarouselView();
|
||||
resetForceCompactEpisodeView();
|
||||
}
|
||||
|
||||
const changed =
|
||||
|
|
@ -205,7 +213,8 @@ export function useSettingsState(
|
|||
sourceOrderChanged ||
|
||||
enableSourceOrderChanged ||
|
||||
proxyTmdbChanged ||
|
||||
enableCarouselViewChanged;
|
||||
enableCarouselViewChanged ||
|
||||
forceCompactEpisodeViewChanged;
|
||||
|
||||
return {
|
||||
reset,
|
||||
|
|
@ -310,5 +319,10 @@ export function useSettingsState(
|
|||
set: setEnableCarouselViewState,
|
||||
changed: enableCarouselViewChanged,
|
||||
},
|
||||
forceCompactEpisodeView: {
|
||||
state: forceCompactEpisodeViewState,
|
||||
set: setForceCompactEpisodeViewState,
|
||||
changed: forceCompactEpisodeViewChanged,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -178,6 +178,13 @@ export function SettingsPage() {
|
|||
(s) => s.setEnableCarouselView,
|
||||
);
|
||||
|
||||
const forceCompactEpisodeView = usePreferencesStore(
|
||||
(s) => s.forceCompactEpisodeView,
|
||||
);
|
||||
const setForceCompactEpisodeView = usePreferencesStore(
|
||||
(s) => s.setForceCompactEpisodeView,
|
||||
);
|
||||
|
||||
const account = useAuthStore((s) => s.account);
|
||||
const updateProfile = useAuthStore((s) => s.setAccountProfile);
|
||||
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
|
||||
|
|
@ -227,6 +234,7 @@ export function SettingsPage() {
|
|||
enableSkipCredits,
|
||||
enableImageLogos,
|
||||
enableCarouselView,
|
||||
forceCompactEpisodeView,
|
||||
);
|
||||
|
||||
const availableSources = useMemo(() => {
|
||||
|
|
@ -282,7 +290,8 @@ export function SettingsPage() {
|
|||
state.sourceOrder.changed ||
|
||||
state.enableSourceOrder.changed ||
|
||||
state.proxyTmdb.changed ||
|
||||
state.enableCarouselView.changed
|
||||
state.enableCarouselView.changed ||
|
||||
state.forceCompactEpisodeView.changed
|
||||
) {
|
||||
await updateSettings(backendUrl, account, {
|
||||
applicationLanguage: state.appLanguage.state,
|
||||
|
|
@ -301,6 +310,7 @@ export function SettingsPage() {
|
|||
enableSourceOrder: state.enableSourceOrder.state,
|
||||
proxyTmdb: state.proxyTmdb.state,
|
||||
enableCarouselView: state.enableCarouselView.state,
|
||||
forceCompactEpisodeView: state.forceCompactEpisodeView.state,
|
||||
});
|
||||
}
|
||||
if (state.deviceName.changed) {
|
||||
|
|
@ -337,6 +347,7 @@ export function SettingsPage() {
|
|||
setRealDebridKey(state.realDebridKey.state);
|
||||
setProxyTmdb(state.proxyTmdb.state);
|
||||
setEnableCarouselView(state.enableCarouselView.state);
|
||||
setForceCompactEpisodeView(state.forceCompactEpisodeView.state);
|
||||
|
||||
if (state.profile.state) {
|
||||
updateProfile(state.profile.state);
|
||||
|
|
@ -378,6 +389,7 @@ export function SettingsPage() {
|
|||
setEnableSourceOrder,
|
||||
setProxyTmdb,
|
||||
setEnableCarouselView,
|
||||
setForceCompactEpisodeView,
|
||||
]);
|
||||
return (
|
||||
<SubPageLayout>
|
||||
|
|
@ -443,6 +455,8 @@ export function SettingsPage() {
|
|||
setEnableImageLogos={state.enableImageLogos.set}
|
||||
enableCarouselView={state.enableCarouselView.state}
|
||||
setEnableCarouselView={state.enableCarouselView.set}
|
||||
forceCompactEpisodeView={state.forceCompactEpisodeView.state}
|
||||
setForceCompactEpisodeView={state.forceCompactEpisodeView.set}
|
||||
/>
|
||||
</div>
|
||||
<div id="settings-captions" className="mt-28">
|
||||
|
|
|
|||
|
|
@ -216,6 +216,9 @@ export function AppearancePart(props: {
|
|||
|
||||
enableCarouselView: boolean;
|
||||
setEnableCarouselView: (v: boolean) => void;
|
||||
|
||||
forceCompactEpisodeView: boolean;
|
||||
setForceCompactEpisodeView: (v: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
|
@ -389,6 +392,32 @@ export function AppearancePart(props: {
|
|||
</p>
|
||||
</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>
|
||||
|
||||
{/* Second Column - Themes */}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface PreferencesStore {
|
|||
enableDetailsModal: boolean;
|
||||
enableImageLogos: boolean;
|
||||
enableCarouselView: boolean;
|
||||
forceCompactEpisodeView: boolean;
|
||||
sourceOrder: string[];
|
||||
enableSourceOrder: boolean;
|
||||
proxyTmdb: boolean;
|
||||
|
|
@ -25,6 +26,7 @@ export interface PreferencesStore {
|
|||
setEnableDetailsModal(v: boolean): void;
|
||||
setEnableImageLogos(v: boolean): void;
|
||||
setEnableCarouselView(v: boolean): void;
|
||||
setForceCompactEpisodeView(v: boolean): void;
|
||||
setSourceOrder(v: string[]): void;
|
||||
setEnableSourceOrder(v: boolean): void;
|
||||
setProxyTmdb(v: boolean): void;
|
||||
|
|
@ -43,6 +45,7 @@ export const usePreferencesStore = create(
|
|||
enableDetailsModal: false,
|
||||
enableImageLogos: true,
|
||||
enableCarouselView: false,
|
||||
forceCompactEpisodeView: false,
|
||||
sourceOrder: [],
|
||||
enableSourceOrder: false,
|
||||
proxyTmdb: false,
|
||||
|
|
@ -88,6 +91,11 @@ export const usePreferencesStore = create(
|
|||
s.enableCarouselView = v;
|
||||
});
|
||||
},
|
||||
setForceCompactEpisodeView(v) {
|
||||
set((s) => {
|
||||
s.forceCompactEpisodeView = v;
|
||||
});
|
||||
},
|
||||
setSourceOrder(v) {
|
||||
set((s) => {
|
||||
s.sourceOrder = v;
|
||||
|
|
|
|||
Loading…
Reference in a new issue