add compact episodes view

This commit is contained in:
Pas 2025-06-11 19:43:52 -06:00
parent 7c89b2ffc1
commit d6f132b4c9
7 changed files with 117 additions and 9 deletions

View file

@ -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": {

View file

@ -18,6 +18,7 @@ export interface SettingsInput {
enableDetailsModal?: boolean;
enableImageLogos?: boolean;
enableCarouselView?: boolean;
forceCompactEpisodeView?: boolean;
sourceOrder?: string[];
enableSourceOrder?: boolean;
proxyTmdb?: boolean;

View file

@ -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}

View file

@ -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,
},
};
}

View file

@ -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">

View file

@ -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 */}

View file

@ -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;