mirror of
https://github.com/p-stream/p-stream.git
synced 2026-04-21 07:22:17 +00:00
add carousel view for watching and bookmarks
This commit is contained in:
parent
bcfed9b0b8
commit
3cc435332c
11 changed files with 566 additions and 64 deletions
|
|
@ -782,7 +782,10 @@
|
||||||
"logos": "Image logos",
|
"logos": "Image logos",
|
||||||
"logosDescription": "Show image logos instead of text titles in the details modal and featured section. Enabled by default.",
|
"logosDescription": "Show image logos instead of text titles in the details modal and featured section. Enabled by default.",
|
||||||
"logosNotice": "Most of the time, logos are English only. Other languages might want to disable this!",
|
"logosNotice": "Most of the time, logos are English only. Other languages might want to disable this!",
|
||||||
"logosLabel": "Image logos"
|
"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"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"connections": {
|
"connections": {
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,7 @@ export function useSettingsState(
|
||||||
proxyTmdb: boolean,
|
proxyTmdb: boolean,
|
||||||
enableSkipCredits: boolean,
|
enableSkipCredits: boolean,
|
||||||
enableImageLogos: boolean,
|
enableImageLogos: boolean,
|
||||||
|
enableCarouselView: boolean,
|
||||||
) {
|
) {
|
||||||
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
|
||||||
useDerived(proxyUrls);
|
useDerived(proxyUrls);
|
||||||
|
|
@ -150,6 +151,12 @@ export function useSettingsState(
|
||||||
] = useDerived(enableSourceOrder);
|
] = useDerived(enableSourceOrder);
|
||||||
const [proxyTmdbState, setProxyTmdbState, resetProxyTmdb, proxyTmdbChanged] =
|
const [proxyTmdbState, setProxyTmdbState, resetProxyTmdb, proxyTmdbChanged] =
|
||||||
useDerived(proxyTmdb);
|
useDerived(proxyTmdb);
|
||||||
|
const [
|
||||||
|
enableCarouselViewState,
|
||||||
|
setEnableCarouselViewState,
|
||||||
|
resetEnableCarouselView,
|
||||||
|
enableCarouselViewChanged,
|
||||||
|
] = useDerived(enableCarouselView);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
resetTheme();
|
resetTheme();
|
||||||
|
|
@ -171,6 +178,7 @@ export function useSettingsState(
|
||||||
resetSourceOrder();
|
resetSourceOrder();
|
||||||
resetEnableSourceOrder();
|
resetEnableSourceOrder();
|
||||||
resetProxyTmdb();
|
resetProxyTmdb();
|
||||||
|
resetEnableCarouselView();
|
||||||
}
|
}
|
||||||
|
|
||||||
const changed =
|
const changed =
|
||||||
|
|
@ -191,7 +199,8 @@ export function useSettingsState(
|
||||||
enableImageLogosChanged ||
|
enableImageLogosChanged ||
|
||||||
sourceOrderChanged ||
|
sourceOrderChanged ||
|
||||||
enableSourceOrderChanged ||
|
enableSourceOrderChanged ||
|
||||||
proxyTmdbChanged;
|
proxyTmdbChanged ||
|
||||||
|
enableCarouselViewChanged;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
reset,
|
reset,
|
||||||
|
|
@ -286,5 +295,10 @@ export function useSettingsState(
|
||||||
set: setProxyTmdbState,
|
set: setProxyTmdbState,
|
||||||
changed: proxyTmdbChanged,
|
changed: proxyTmdbChanged,
|
||||||
},
|
},
|
||||||
|
enableCarouselView: {
|
||||||
|
state: enableCarouselViewState,
|
||||||
|
set: setEnableCarouselViewState,
|
||||||
|
changed: enableCarouselViewChanged,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { To, useNavigate } from "react-router-dom";
|
import { To, useNavigate } from "react-router-dom";
|
||||||
|
|
@ -13,8 +13,10 @@ import { FeaturedCarousel } from "@/pages/discover/components/FeaturedCarousel";
|
||||||
import type { FeaturedMedia } from "@/pages/discover/components/FeaturedCarousel";
|
import type { FeaturedMedia } from "@/pages/discover/components/FeaturedCarousel";
|
||||||
import DiscoverContent from "@/pages/discover/discoverContent";
|
import DiscoverContent from "@/pages/discover/discoverContent";
|
||||||
import { HomeLayout } from "@/pages/layouts/HomeLayout";
|
import { HomeLayout } from "@/pages/layouts/HomeLayout";
|
||||||
|
import { BookmarksCarousel } from "@/pages/parts/home/BookmarksCarousel";
|
||||||
import { BookmarksPart } from "@/pages/parts/home/BookmarksPart";
|
import { BookmarksPart } from "@/pages/parts/home/BookmarksPart";
|
||||||
import { HeroPart } from "@/pages/parts/home/HeroPart";
|
import { HeroPart } from "@/pages/parts/home/HeroPart";
|
||||||
|
import { WatchingCarousel } from "@/pages/parts/home/WatchingCarousel";
|
||||||
import { WatchingPart } from "@/pages/parts/home/WatchingPart";
|
import { WatchingPart } from "@/pages/parts/home/WatchingPart";
|
||||||
import { SearchListPart } from "@/pages/parts/search/SearchListPart";
|
import { SearchListPart } from "@/pages/parts/search/SearchListPart";
|
||||||
import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart";
|
import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart";
|
||||||
|
|
@ -64,6 +66,11 @@ export function HomePage() {
|
||||||
const detailsModal = useModal("details");
|
const detailsModal = useModal("details");
|
||||||
const enableDiscover = usePreferencesStore((state) => state.enableDiscover);
|
const enableDiscover = usePreferencesStore((state) => state.enableDiscover);
|
||||||
const enableFeatured = usePreferencesStore((state) => state.enableFeatured);
|
const enableFeatured = usePreferencesStore((state) => state.enableFeatured);
|
||||||
|
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||||
|
const enableCarouselView = usePreferencesStore(
|
||||||
|
(state) => state.enableCarouselView,
|
||||||
|
);
|
||||||
|
const isMobile = window.innerWidth < 768;
|
||||||
|
|
||||||
const handleClick = (path: To) => {
|
const handleClick = (path: To) => {
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
|
@ -218,24 +225,50 @@ export function HomePage() {
|
||||||
)}
|
)}
|
||||||
{conf().SHOW_AD ? <AdsPart /> : null}
|
{conf().SHOW_AD ? <AdsPart /> : null}
|
||||||
</div>
|
</div>
|
||||||
<WideContainer>
|
<WideContainer ultraWide={enableCarouselView}>
|
||||||
{s.loading ? (
|
{s.loading ? (
|
||||||
<SearchLoadingPart />
|
<SearchLoadingPart />
|
||||||
) : s.searching ? (
|
) : s.searching ? (
|
||||||
<SearchListPart
|
enableCarouselView ? (
|
||||||
searchQuery={search}
|
<WideContainer>
|
||||||
onShowDetails={handleShowDetails}
|
<SearchListPart
|
||||||
/>
|
searchQuery={search}
|
||||||
|
onShowDetails={handleShowDetails}
|
||||||
|
/>
|
||||||
|
</WideContainer>
|
||||||
|
) : (
|
||||||
|
<SearchListPart
|
||||||
|
searchQuery={search}
|
||||||
|
onShowDetails={handleShowDetails}
|
||||||
|
/>
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<WatchingPart
|
{enableCarouselView ? (
|
||||||
onItemsChange={setShowWatching}
|
<>
|
||||||
onShowDetails={handleShowDetails}
|
<WatchingCarousel
|
||||||
/>
|
isMobile={isMobile}
|
||||||
<BookmarksPart
|
carouselRefs={carouselRefs}
|
||||||
onItemsChange={setShowBookmarks}
|
onShowDetails={handleShowDetails}
|
||||||
onShowDetails={handleShowDetails}
|
/>
|
||||||
/>
|
<BookmarksCarousel
|
||||||
|
isMobile={isMobile}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
onShowDetails={handleShowDetails}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<WatchingPart
|
||||||
|
onItemsChange={setShowWatching}
|
||||||
|
onShowDetails={handleShowDetails}
|
||||||
|
/>
|
||||||
|
<BookmarksPart
|
||||||
|
onItemsChange={setShowBookmarks}
|
||||||
|
onShowDetails={handleShowDetails}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!(showBookmarks || showWatching) && !enableDiscover ? (
|
{!(showBookmarks || showWatching) && !enableDiscover ? (
|
||||||
|
|
@ -252,25 +285,23 @@ export function HomePage() {
|
||||||
<div className="pb-20" />
|
<div className="pb-20" />
|
||||||
))}
|
))}
|
||||||
{/* there... perfect. */}
|
{/* there... perfect. */}
|
||||||
</WideContainer>
|
{enableDiscover && !search ? (
|
||||||
{enableDiscover && !search ? (
|
|
||||||
<div className="w-full max-w-[100dvw] justify-center items-center">
|
|
||||||
<DiscoverContent />
|
<DiscoverContent />
|
||||||
</div>
|
) : (
|
||||||
) : (
|
<div className="flex flex-col justify-center items-center h-40 space-y-4">
|
||||||
<div className="flex flex-col justify-center items-center h-40 space-y-4">
|
<div className="flex flex-col items-center justify-center">
|
||||||
<div className="flex flex-col items-center justify-center">
|
{!search && (
|
||||||
{!search && (
|
<Button
|
||||||
<Button
|
className="px-py p-[0.35em] mt-3 rounded-xl text-type-dimmed box-content text-[18px] bg-largeCard-background justify-center items-center"
|
||||||
className="px-py p-[0.35em] mt-3 rounded-xl text-type-dimmed box-content text-[18px] bg-largeCard-background justify-center items-center"
|
onClick={() => handleClick("/discover")}
|
||||||
onClick={() => handleClick("/discover")}
|
>
|
||||||
>
|
{t("home.search.discover")}
|
||||||
{t("home.search.discover")}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</WideContainer>
|
||||||
|
|
||||||
{detailsData && <DetailsModal id="details" data={detailsData} />}
|
{detailsData && <DetailsModal id="details" data={detailsData} />}
|
||||||
</HomeLayout>
|
</HomeLayout>
|
||||||
|
|
|
||||||
|
|
@ -170,6 +170,11 @@ export function SettingsPage() {
|
||||||
const proxyTmdb = usePreferencesStore((s) => s.proxyTmdb);
|
const proxyTmdb = usePreferencesStore((s) => s.proxyTmdb);
|
||||||
const setProxyTmdb = usePreferencesStore((s) => s.setProxyTmdb);
|
const setProxyTmdb = usePreferencesStore((s) => s.setProxyTmdb);
|
||||||
|
|
||||||
|
const enableCarouselView = usePreferencesStore((s) => s.enableCarouselView);
|
||||||
|
const setEnableCarouselView = usePreferencesStore(
|
||||||
|
(s) => s.setEnableCarouselView,
|
||||||
|
);
|
||||||
|
|
||||||
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);
|
||||||
|
|
@ -214,6 +219,7 @@ export function SettingsPage() {
|
||||||
proxyTmdb,
|
proxyTmdb,
|
||||||
enableSkipCredits,
|
enableSkipCredits,
|
||||||
enableImageLogos,
|
enableImageLogos,
|
||||||
|
enableCarouselView,
|
||||||
);
|
);
|
||||||
|
|
||||||
const availableSources = useMemo(() => {
|
const availableSources = useMemo(() => {
|
||||||
|
|
@ -298,6 +304,7 @@ export function SettingsPage() {
|
||||||
setEnableSourceOrder(state.enableSourceOrder.state);
|
setEnableSourceOrder(state.enableSourceOrder.state);
|
||||||
setFebboxToken(state.febboxToken.state);
|
setFebboxToken(state.febboxToken.state);
|
||||||
setProxyTmdb(state.proxyTmdb.state);
|
setProxyTmdb(state.proxyTmdb.state);
|
||||||
|
setEnableCarouselView(state.enableCarouselView.state);
|
||||||
|
|
||||||
if (state.profile.state) {
|
if (state.profile.state) {
|
||||||
updateProfile(state.profile.state);
|
updateProfile(state.profile.state);
|
||||||
|
|
@ -337,6 +344,7 @@ export function SettingsPage() {
|
||||||
setBackendUrl,
|
setBackendUrl,
|
||||||
setEnableSourceOrder,
|
setEnableSourceOrder,
|
||||||
setProxyTmdb,
|
setProxyTmdb,
|
||||||
|
setEnableCarouselView,
|
||||||
]);
|
]);
|
||||||
return (
|
return (
|
||||||
<SubPageLayout>
|
<SubPageLayout>
|
||||||
|
|
@ -400,6 +408,8 @@ export function SettingsPage() {
|
||||||
setEnableDetailsModal={state.enableDetailsModal.set}
|
setEnableDetailsModal={state.enableDetailsModal.set}
|
||||||
enableImageLogos={state.enableImageLogos.state}
|
enableImageLogos={state.enableImageLogos.state}
|
||||||
setEnableImageLogos={state.enableImageLogos.set}
|
setEnableImageLogos={state.enableImageLogos.set}
|
||||||
|
enableCarouselView={state.enableCarouselView.state}
|
||||||
|
setEnableCarouselView={state.enableCarouselView.set}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div id="settings-captions" className="mt-28">
|
<div id="settings-captions" className="mt-28">
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,7 @@ export function Discover() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="relative z-20">
|
<div className="relative z-20 px-10">
|
||||||
<DiscoverContent />
|
<DiscoverContent />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -120,11 +120,11 @@ export function FeaturedCarousel({
|
||||||
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
const { width: windowWidth, height: windowHeight } = useWindowSize();
|
||||||
|
|
||||||
const SLIDE_QUANTITY = 10;
|
const SLIDE_QUANTITY = 10;
|
||||||
|
const FETCH_QUANTITY = 20;
|
||||||
const SLIDE_QUANTITY_EDITOR_PICKS_MOVIES = 6;
|
const SLIDE_QUANTITY_EDITOR_PICKS_MOVIES = 6;
|
||||||
const SLIDE_QUANTITY_EDITOR_PICKS_TV_SHOWS = 4;
|
const SLIDE_QUANTITY_EDITOR_PICKS_TV_SHOWS = 4;
|
||||||
const SLIDE_DURATION = 8000;
|
const SLIDE_DURATION = 8000;
|
||||||
|
|
||||||
// Fetch featured media
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchFeaturedMedia = async () => {
|
const fetchFeaturedMedia = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
@ -138,41 +138,65 @@ export function FeaturedCarousel({
|
||||||
api_key: conf().TMDB_READ_API_KEY,
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
language: formattedLanguage,
|
language: formattedLanguage,
|
||||||
});
|
});
|
||||||
setMedia(
|
// Fetch movie items and randomly select
|
||||||
data.results.slice(0, SLIDE_QUANTITY).map((movie: any) => ({
|
const allMovies = data.results
|
||||||
|
.slice(0, FETCH_QUANTITY)
|
||||||
|
.map((movie: any) => ({
|
||||||
...movie,
|
...movie,
|
||||||
type: "movie" as const,
|
type: "movie" as const,
|
||||||
})),
|
}));
|
||||||
);
|
// Shuffle
|
||||||
|
const shuffledMovies = [...allMovies].sort(() => 0.5 - Math.random());
|
||||||
|
setMedia(shuffledMovies.slice(0, SLIDE_QUANTITY));
|
||||||
} else if (effectiveCategory === "tvshows") {
|
} else if (effectiveCategory === "tvshows") {
|
||||||
const data = await get<any>("/tv/popular", {
|
const data = await get<any>("/tv/popular", {
|
||||||
api_key: conf().TMDB_READ_API_KEY,
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
language: formattedLanguage,
|
language: formattedLanguage,
|
||||||
});
|
});
|
||||||
setMedia(
|
// Fetch show items
|
||||||
data.results.slice(0, SLIDE_QUANTITY).map((show: any) => ({
|
const allShows = data.results
|
||||||
|
.slice(0, FETCH_QUANTITY)
|
||||||
|
.map((show: any) => ({
|
||||||
...show,
|
...show,
|
||||||
type: "show" as const,
|
type: "show" as const,
|
||||||
})),
|
}));
|
||||||
);
|
// Shuffle
|
||||||
|
const shuffledShows = [...allShows].sort(() => 0.5 - Math.random());
|
||||||
|
setMedia(shuffledShows.slice(0, SLIDE_QUANTITY));
|
||||||
} else if (effectiveCategory === "editorpicks") {
|
} else if (effectiveCategory === "editorpicks") {
|
||||||
// Fetch editor picks movies
|
// Shuffle editor picks Ids
|
||||||
const moviePromises = EDITOR_PICKS_MOVIES.slice(
|
const allMovieIds = EDITOR_PICKS_MOVIES.map((item) => ({
|
||||||
0,
|
id: item.id,
|
||||||
SLIDE_QUANTITY_EDITOR_PICKS_MOVIES,
|
type: "movie" as const,
|
||||||
).map((item) =>
|
}));
|
||||||
get<any>(`/movie/${item.id}`, {
|
const allShowIds = EDITOR_PICKS_TV_SHOWS.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
type: "show" as const,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Combine and shuffle
|
||||||
|
const combinedIds = [...allMovieIds, ...allShowIds].sort(
|
||||||
|
() => 0.5 - Math.random(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Select the quantity
|
||||||
|
const selectedMovieIds = combinedIds
|
||||||
|
.filter((item) => item.type === "movie")
|
||||||
|
.slice(0, SLIDE_QUANTITY_EDITOR_PICKS_MOVIES);
|
||||||
|
const selectedShowIds = combinedIds
|
||||||
|
.filter((item) => item.type === "show")
|
||||||
|
.slice(0, SLIDE_QUANTITY_EDITOR_PICKS_TV_SHOWS);
|
||||||
|
|
||||||
|
// Fetch items
|
||||||
|
const moviePromises = selectedMovieIds.map(({ id }) =>
|
||||||
|
get<any>(`/movie/${id}`, {
|
||||||
api_key: conf().TMDB_READ_API_KEY,
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
language: formattedLanguage,
|
language: formattedLanguage,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fetch editor picks TV shows
|
const showPromises = selectedShowIds.map(({ id }) =>
|
||||||
const showPromises = EDITOR_PICKS_TV_SHOWS.slice(
|
get<any>(`/tv/${id}`, {
|
||||||
0,
|
|
||||||
SLIDE_QUANTITY_EDITOR_PICKS_TV_SHOWS,
|
|
||||||
).map((item) =>
|
|
||||||
get<any>(`/tv/${item.id}`, {
|
|
||||||
api_key: conf().TMDB_READ_API_KEY,
|
api_key: conf().TMDB_READ_API_KEY,
|
||||||
language: formattedLanguage,
|
language: formattedLanguage,
|
||||||
}),
|
}),
|
||||||
|
|
@ -192,11 +216,7 @@ export function FeaturedCarousel({
|
||||||
type: "show" as const,
|
type: "show" as const,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Combine and shuffle
|
setMedia([...movies, ...shows]);
|
||||||
const combined = [...movies, ...shows].sort(
|
|
||||||
() => 0.5 - Math.random(),
|
|
||||||
);
|
|
||||||
setMedia(combined);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching featured media:", error);
|
console.error("Error fetching featured media:", error);
|
||||||
|
|
@ -260,7 +280,7 @@ export function FeaturedCarousel({
|
||||||
setTouchEnd(null);
|
setTouchEnd(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch logo when current media changes
|
// Fetch clear logo when current media changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchLogo = async () => {
|
const fetchLogo = async () => {
|
||||||
// Cancel any in-progress logo fetch
|
// Cancel any in-progress logo fetch
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { get } from "@/backend/metadata/tmdb";
|
import { get } from "@/backend/metadata/tmdb";
|
||||||
|
import { WideContainer } from "@/components/layout/WideContainer";
|
||||||
import { DetailsModal } from "@/components/overlays/DetailsModal";
|
import { DetailsModal } from "@/components/overlays/DetailsModal";
|
||||||
import { useModal } from "@/components/overlays/Modal";
|
import { useModal } from "@/components/overlays/Modal";
|
||||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||||
|
|
@ -704,8 +705,8 @@ export function DiscoverContent() {
|
||||||
selectedCategory={selectedCategory}
|
selectedCategory={selectedCategory}
|
||||||
onCategoryChange={handleCategoryChange}
|
onCategoryChange={handleCategoryChange}
|
||||||
/>
|
/>
|
||||||
{/* Content Section with Lazy Loading Tabs */}
|
|
||||||
<div className="w-full md:w-[90%] max-w-[2400px] mx-auto">
|
<WideContainer ultraWide classNames="!px-0">
|
||||||
{/* Movies Tab */}
|
{/* Movies Tab */}
|
||||||
<LazyTabContent isActive={isMoviesTab}>
|
<LazyTabContent isActive={isMoviesTab}>
|
||||||
{renderMoviesContent()}
|
{renderMoviesContent()}
|
||||||
|
|
@ -720,7 +721,7 @@ export function DiscoverContent() {
|
||||||
<LazyTabContent isActive={isEditorPicksTab}>
|
<LazyTabContent isActive={isEditorPicksTab}>
|
||||||
{renderEditorPicksContent()}
|
{renderEditorPicksContent()}
|
||||||
</LazyTabContent>
|
</LazyTabContent>
|
||||||
</div>
|
</WideContainer>
|
||||||
|
|
||||||
<ScrollToTopButton />
|
<ScrollToTopButton />
|
||||||
|
|
||||||
|
|
|
||||||
198
src/pages/parts/home/BookmarksCarousel.tsx
Normal file
198
src/pages/parts/home/BookmarksCarousel.tsx
Normal file
|
|
@ -0,0 +1,198 @@
|
||||||
|
import React, { useMemo, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { EditButton } from "@/components/buttons/EditButton";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
|
import { MediaCard } from "@/components/media/MediaCard";
|
||||||
|
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||||
|
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
||||||
|
import { useBookmarkStore } from "@/stores/bookmarks";
|
||||||
|
import { useProgressStore } from "@/stores/progress";
|
||||||
|
import { MediaItem } from "@/utils/mediaTypes";
|
||||||
|
|
||||||
|
interface BookmarksCarouselProps {
|
||||||
|
isMobile: boolean;
|
||||||
|
carouselRefs: React.MutableRefObject<{
|
||||||
|
[key: string]: HTMLDivElement | null;
|
||||||
|
}>;
|
||||||
|
onShowDetails?: (media: MediaItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LONG_PRESS_DURATION = 500; // 0.5 seconds
|
||||||
|
|
||||||
|
function MediaCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="relative mt-4 group cursor-default user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="w-full aspect-[2/3] bg-mediaCard-hoverBackground rounded-lg" />
|
||||||
|
<div className="mt-2 h-4 bg-mediaCard-hoverBackground rounded w-3/4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BookmarksCarousel({
|
||||||
|
isMobile,
|
||||||
|
carouselRefs,
|
||||||
|
onShowDetails,
|
||||||
|
}: BookmarksCarouselProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const browser = !!window.chrome;
|
||||||
|
let isScrolling = false;
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const removeBookmark = useBookmarkStore((s) => s.removeBookmark);
|
||||||
|
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const bookmarksLength = useBookmarkStore(
|
||||||
|
(state) => Object.keys(state.bookmarks).length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const progressItems = useProgressStore((state) => state.items);
|
||||||
|
const bookmarks = useBookmarkStore((state) => state.bookmarks);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
let output: MediaItem[] = [];
|
||||||
|
Object.entries(bookmarks).forEach((entry) => {
|
||||||
|
output.push({
|
||||||
|
id: entry[0],
|
||||||
|
...entry[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
output = output.sort((a, b) => {
|
||||||
|
const bookmarkA = bookmarks[a.id];
|
||||||
|
const bookmarkB = bookmarks[b.id];
|
||||||
|
const progressA = progressItems[a.id];
|
||||||
|
const progressB = progressItems[b.id];
|
||||||
|
|
||||||
|
const dateA = Math.max(bookmarkA.updatedAt, progressA?.updatedAt ?? 0);
|
||||||
|
const dateB = Math.max(bookmarkB.updatedAt, progressB?.updatedAt ?? 0);
|
||||||
|
|
||||||
|
return dateB - dateA;
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}, [bookmarks, progressItems]);
|
||||||
|
|
||||||
|
const handleWheel = (e: React.WheelEvent) => {
|
||||||
|
if (isScrolling) return;
|
||||||
|
isScrolling = true;
|
||||||
|
|
||||||
|
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
setTimeout(() => {
|
||||||
|
isScrolling = false;
|
||||||
|
}, 345);
|
||||||
|
} else {
|
||||||
|
isScrolling = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLongPress = () => {
|
||||||
|
// Find the button by ID and simulate a click
|
||||||
|
const editButton = document.getElementById("edit-button-bookmark");
|
||||||
|
if (editButton) {
|
||||||
|
(editButton as HTMLButtonElement).click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault(); // Prevent default touch action
|
||||||
|
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
if (pressTimerRef.current) {
|
||||||
|
clearTimeout(pressTimerRef.current);
|
||||||
|
pressTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault(); // Prevent default mouse action
|
||||||
|
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
if (pressTimerRef.current) {
|
||||||
|
clearTimeout(pressTimerRef.current);
|
||||||
|
pressTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorySlug = "bookmarks";
|
||||||
|
const SKELETON_COUNT = 10;
|
||||||
|
|
||||||
|
if (bookmarksLength === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SectionHeading
|
||||||
|
title={t("home.bookmarks.sectionTitle") || "Bookmarks"}
|
||||||
|
icon={Icons.BOOKMARK}
|
||||||
|
className="ml-2 md:ml-8 mt-2 -mb-10"
|
||||||
|
>
|
||||||
|
<div className="mr-6">
|
||||||
|
<EditButton
|
||||||
|
editing={editing}
|
||||||
|
onEdit={setEditing}
|
||||||
|
id="edit-button-bookmark"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionHeading>
|
||||||
|
<div className="relative overflow-hidden carousel-container md:pb-4">
|
||||||
|
<div
|
||||||
|
id={`carousel-${categorySlug}`}
|
||||||
|
className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8"
|
||||||
|
ref={(el) => {
|
||||||
|
carouselRefs.current[categorySlug] = el;
|
||||||
|
}}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
>
|
||||||
|
<div className="md:w-12" />
|
||||||
|
|
||||||
|
{items.length > 0
|
||||||
|
? items.map((media) => (
|
||||||
|
<div
|
||||||
|
key={media.id}
|
||||||
|
style={{ userSelect: "none" }}
|
||||||
|
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||||
|
>
|
||||||
|
<WatchedMediaCard
|
||||||
|
key={media.id}
|
||||||
|
media={media}
|
||||||
|
onShowDetails={onShowDetails}
|
||||||
|
closable={editing}
|
||||||
|
onClose={() => removeBookmark(media.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: Array.from({ length: SKELETON_COUNT }).map(() => (
|
||||||
|
<MediaCardSkeleton
|
||||||
|
key={`skeleton-${categorySlug}-${Math.random().toString(36).substring(7)}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="md:w-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isMobile && (
|
||||||
|
<CarouselNavButtons
|
||||||
|
categorySlug={categorySlug}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
190
src/pages/parts/home/WatchingCarousel.tsx
Normal file
190
src/pages/parts/home/WatchingCarousel.tsx
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import React, { useMemo, useRef, useState } from "react";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import { EditButton } from "@/components/buttons/EditButton";
|
||||||
|
import { Icons } from "@/components/Icon";
|
||||||
|
import { SectionHeading } from "@/components/layout/SectionHeading";
|
||||||
|
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||||
|
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
||||||
|
import { useProgressStore } from "@/stores/progress";
|
||||||
|
import { shouldShowProgress } from "@/stores/progress/utils";
|
||||||
|
import { MediaItem } from "@/utils/mediaTypes";
|
||||||
|
|
||||||
|
interface WatchingCarouselProps {
|
||||||
|
isMobile: boolean;
|
||||||
|
carouselRefs: React.MutableRefObject<{
|
||||||
|
[key: string]: HTMLDivElement | null;
|
||||||
|
}>;
|
||||||
|
onShowDetails?: (media: MediaItem) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const LONG_PRESS_DURATION = 500; // 0.5 seconds
|
||||||
|
|
||||||
|
function MediaCardSkeleton() {
|
||||||
|
return (
|
||||||
|
<div className="relative mt-4 group cursor-default user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto">
|
||||||
|
<div className="animate-pulse">
|
||||||
|
<div className="w-full aspect-[2/3] bg-mediaCard-hoverBackground rounded-lg" />
|
||||||
|
<div className="mt-2 h-4 bg-mediaCard-hoverBackground rounded w-3/4" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function WatchingCarousel({
|
||||||
|
isMobile,
|
||||||
|
carouselRefs,
|
||||||
|
onShowDetails,
|
||||||
|
}: WatchingCarouselProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const browser = !!window.chrome;
|
||||||
|
let isScrolling = false;
|
||||||
|
const [editing, setEditing] = useState(false);
|
||||||
|
const removeItem = useProgressStore((s) => s.removeItem);
|
||||||
|
const pressTimerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
|
const itemsLength = useProgressStore((state) => {
|
||||||
|
return Object.entries(state.items).filter(
|
||||||
|
(entry) => shouldShowProgress(entry[1]).show,
|
||||||
|
).length;
|
||||||
|
});
|
||||||
|
|
||||||
|
const progressItems = useProgressStore((state) => state.items);
|
||||||
|
|
||||||
|
const items = useMemo(() => {
|
||||||
|
const output: MediaItem[] = [];
|
||||||
|
Object.entries(progressItems)
|
||||||
|
.filter((entry) => shouldShowProgress(entry[1]).show)
|
||||||
|
.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
|
||||||
|
.forEach((entry) => {
|
||||||
|
output.push({
|
||||||
|
id: entry[0],
|
||||||
|
...entry[1],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return output;
|
||||||
|
}, [progressItems]);
|
||||||
|
|
||||||
|
const handleWheel = (e: React.WheelEvent) => {
|
||||||
|
if (isScrolling) return;
|
||||||
|
isScrolling = true;
|
||||||
|
|
||||||
|
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (browser) {
|
||||||
|
setTimeout(() => {
|
||||||
|
isScrolling = false;
|
||||||
|
}, 345);
|
||||||
|
} else {
|
||||||
|
isScrolling = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const categorySlug = "continue-watching";
|
||||||
|
const SKELETON_COUNT = 10;
|
||||||
|
|
||||||
|
const handleLongPress = () => {
|
||||||
|
// Find the button by ID and simulate a click
|
||||||
|
const editButton = document.getElementById("edit-button-watching");
|
||||||
|
if (editButton) {
|
||||||
|
(editButton as HTMLButtonElement).click();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchStart = (e: React.TouchEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault(); // Prevent default touch action
|
||||||
|
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTouchEnd = () => {
|
||||||
|
if (pressTimerRef.current) {
|
||||||
|
clearTimeout(pressTimerRef.current);
|
||||||
|
pressTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
e.preventDefault(); // Prevent default mouse action
|
||||||
|
pressTimerRef.current = setTimeout(handleLongPress, LONG_PRESS_DURATION);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
if (pressTimerRef.current) {
|
||||||
|
clearTimeout(pressTimerRef.current);
|
||||||
|
pressTimerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (itemsLength === 0) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SectionHeading
|
||||||
|
title={t("home.continueWatching.sectionTitle")}
|
||||||
|
icon={Icons.CLOCK}
|
||||||
|
className="ml-2 md:ml-8 mt-2 -mb-10"
|
||||||
|
>
|
||||||
|
<div className="mr-6">
|
||||||
|
<EditButton
|
||||||
|
editing={editing}
|
||||||
|
onEdit={setEditing}
|
||||||
|
id="edit-button-watching"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SectionHeading>
|
||||||
|
<div className="relative overflow-hidden carousel-container md:pb-4">
|
||||||
|
<div
|
||||||
|
id={`carousel-${categorySlug}`}
|
||||||
|
className="grid grid-flow-col auto-cols-max gap-4 pt-0 overflow-x-scroll scrollbar-none rounded-xl overflow-y-hidden md:pl-8 md:pr-8"
|
||||||
|
ref={(el) => {
|
||||||
|
carouselRefs.current[categorySlug] = el;
|
||||||
|
}}
|
||||||
|
onWheel={handleWheel}
|
||||||
|
>
|
||||||
|
<div className="md:w-12" />
|
||||||
|
|
||||||
|
{items.length > 0
|
||||||
|
? items.map((media) => (
|
||||||
|
<div
|
||||||
|
key={media.id}
|
||||||
|
style={{ userSelect: "none" }}
|
||||||
|
onContextMenu={(e: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
onTouchStart={handleTouchStart}
|
||||||
|
onTouchEnd={handleTouchEnd}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onMouseUp={handleMouseUp}
|
||||||
|
className="relative mt-4 group cursor-pointer user-select-none rounded-xl p-2 bg-transparent transition-colors duration-300 w-[10rem] md:w-[11.5rem] h-auto"
|
||||||
|
>
|
||||||
|
<WatchedMediaCard
|
||||||
|
key={media.id}
|
||||||
|
media={media}
|
||||||
|
onShowDetails={onShowDetails}
|
||||||
|
closable={editing}
|
||||||
|
onClose={() => removeItem(media.id)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
: Array.from({ length: SKELETON_COUNT }).map(() => (
|
||||||
|
<MediaCardSkeleton
|
||||||
|
key={`skeleton-${categorySlug}-${Math.random().toString(36).substring(7)}`}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="md:w-12" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isMobile && (
|
||||||
|
<CarouselNavButtons
|
||||||
|
categorySlug={categorySlug}
|
||||||
|
carouselRefs={carouselRefs}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -213,6 +213,9 @@ export function AppearancePart(props: {
|
||||||
|
|
||||||
enableImageLogos: boolean;
|
enableImageLogos: boolean;
|
||||||
setEnableImageLogos: (v: boolean) => void;
|
setEnableImageLogos: (v: boolean) => void;
|
||||||
|
|
||||||
|
enableCarouselView: boolean;
|
||||||
|
setEnableCarouselView: (v: boolean) => void;
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
@ -362,6 +365,30 @@ export function AppearancePart(props: {
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Carousel View */}
|
||||||
|
<div>
|
||||||
|
<p className="text-white font-bold mb-3">
|
||||||
|
{t("settings.appearance.options.carouselView")}
|
||||||
|
</p>
|
||||||
|
<p className="max-w-[25rem] font-medium">
|
||||||
|
{t("settings.appearance.options.carouselViewDescription")}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
props.setEnableCarouselView(!props.enableCarouselView)
|
||||||
|
}
|
||||||
|
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.enableCarouselView} />
|
||||||
|
<p className="flex-1 text-white font-bold">
|
||||||
|
{t("settings.appearance.options.carouselViewLabel")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Second Column - Themes */}
|
{/* Second Column - Themes */}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ export interface PreferencesStore {
|
||||||
enableFeatured: boolean;
|
enableFeatured: boolean;
|
||||||
enableDetailsModal: boolean;
|
enableDetailsModal: boolean;
|
||||||
enableImageLogos: boolean;
|
enableImageLogos: boolean;
|
||||||
|
enableCarouselView: boolean;
|
||||||
sourceOrder: string[];
|
sourceOrder: string[];
|
||||||
enableSourceOrder: boolean;
|
enableSourceOrder: boolean;
|
||||||
proxyTmdb: boolean;
|
proxyTmdb: boolean;
|
||||||
|
|
@ -21,6 +22,7 @@ export interface PreferencesStore {
|
||||||
setEnableFeatured(v: boolean): void;
|
setEnableFeatured(v: boolean): void;
|
||||||
setEnableDetailsModal(v: boolean): void;
|
setEnableDetailsModal(v: boolean): void;
|
||||||
setEnableImageLogos(v: boolean): void;
|
setEnableImageLogos(v: boolean): void;
|
||||||
|
setEnableCarouselView(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;
|
||||||
|
|
@ -36,6 +38,7 @@ export const usePreferencesStore = create(
|
||||||
enableFeatured: true, // enabled for testing
|
enableFeatured: true, // enabled for testing
|
||||||
enableDetailsModal: false,
|
enableDetailsModal: false,
|
||||||
enableImageLogos: true,
|
enableImageLogos: true,
|
||||||
|
enableCarouselView: true, // enabled for testing
|
||||||
sourceOrder: [],
|
sourceOrder: [],
|
||||||
enableSourceOrder: false,
|
enableSourceOrder: false,
|
||||||
proxyTmdb: false,
|
proxyTmdb: false,
|
||||||
|
|
@ -74,6 +77,11 @@ export const usePreferencesStore = create(
|
||||||
s.enableImageLogos = v;
|
s.enableImageLogos = v;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setEnableCarouselView(v) {
|
||||||
|
set((s) => {
|
||||||
|
s.enableCarouselView = v;
|
||||||
|
});
|
||||||
|
},
|
||||||
setSourceOrder(v) {
|
setSourceOrder(v) {
|
||||||
set((s) => {
|
set((s) => {
|
||||||
s.sourceOrder = v;
|
s.sourceOrder = v;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue