mirror of
https://github.com/p-stream/p-stream.git
synced 2026-01-11 20:10:32 +00:00
refactor overlay stack and modals to allow multiple and better navigation
overlays will not close previous ones so that they don't conflict and there can essentially be unlimited modal navigations. Done by passing the modal data to each modal using a global hook instead of relying on local data for each. When navigating to a new path, it closes all modals. On CollectionOverlay, when opening a new details modal overlay, it closes the previous using a custom ShowDetails handler. This isn't the cleanest approach, but offers the greatest flexibility in the future
This commit is contained in:
parent
61593e7fe5
commit
ba59405612
12 changed files with 108 additions and 106 deletions
|
|
@ -16,7 +16,6 @@ import { MediaItem } from "@/utils/mediaTypes";
|
|||
import { MediaBookmarkButton } from "./MediaBookmark";
|
||||
import { IconPatch } from "../buttons/IconPatch";
|
||||
import { Icon, Icons } from "../Icon";
|
||||
import { DetailsModal } from "../overlays/detailsModal";
|
||||
|
||||
// Intersection Observer Hook
|
||||
function useIntersectionObserver(options: IntersectionObserverInit = {}) {
|
||||
|
|
@ -300,10 +299,6 @@ function MediaCardContent({
|
|||
|
||||
export function MediaCard(props: MediaCardProps) {
|
||||
const { media, onShowDetails, forceSkeleton } = props;
|
||||
const [detailsData, setDetailsData] = useState<{
|
||||
id: number;
|
||||
type: "movie" | "show";
|
||||
} | null>(null);
|
||||
const { showModal } = useOverlayStack();
|
||||
const enableDetailsModal = usePreferencesStore(
|
||||
(state) => state.enableDetailsModal,
|
||||
|
|
@ -335,11 +330,11 @@ export function MediaCard(props: MediaCardProps) {
|
|||
return;
|
||||
}
|
||||
|
||||
setDetailsData({
|
||||
// Show modal with data through overlayStack
|
||||
showModal("details", {
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
showModal("details");
|
||||
}, [media, showModal, onShowDetails]);
|
||||
|
||||
const handleCardClick = (e: React.MouseEvent) => {
|
||||
|
|
@ -355,14 +350,11 @@ export function MediaCard(props: MediaCardProps) {
|
|||
};
|
||||
|
||||
const content = (
|
||||
<>
|
||||
<MediaCardContent
|
||||
{...props}
|
||||
onShowDetails={handleShowDetails}
|
||||
forceSkeleton={forceSkeleton}
|
||||
/>
|
||||
{detailsData && <DetailsModal id="details" data={detailsData} />}
|
||||
</>
|
||||
<MediaCardContent
|
||||
{...props}
|
||||
onShowDetails={handleShowDetails}
|
||||
forceSkeleton={forceSkeleton}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!canLink) {
|
||||
|
|
|
|||
|
|
@ -23,8 +23,9 @@ import { DetailsSkeleton } from "./DetailsSkeleton";
|
|||
import { OverlayPortal } from "../../../OverlayDisplay";
|
||||
import { DetailsModalProps } from "../../types";
|
||||
|
||||
export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
||||
const { hideModal, isModalVisible, modalStack } = useOverlayStack();
|
||||
export function DetailsModal({ id, data: _data, minimal }: DetailsModalProps) {
|
||||
const { hideModal, isModalVisible, modalStack, getModalData } =
|
||||
useOverlayStack();
|
||||
const [detailsData, setDetailsData] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
|
|
@ -33,9 +34,15 @@ export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
|||
|
||||
const hide = useCallback(() => hideModal(id), [hideModal, id]);
|
||||
const isShown = isModalVisible(id);
|
||||
const modalData = getModalData(id);
|
||||
|
||||
// Only show modal if there's data to display
|
||||
const shouldShow = Boolean(isShown && (modalData?.id || _data?.id));
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDetails = async () => {
|
||||
// Use data from overlayStack or fallback to props for backward compatibility
|
||||
const data = modalData || _data;
|
||||
if (!data?.id || !data?.type) return;
|
||||
|
||||
setIsLoading(true);
|
||||
|
|
@ -113,22 +120,22 @@ export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
|||
}
|
||||
};
|
||||
|
||||
if (isShown && data?.id) {
|
||||
if (shouldShow) {
|
||||
fetchDetails();
|
||||
}
|
||||
}, [isShown, data]);
|
||||
}, [shouldShow, modalData, _data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isShown && !data?.id && !isLoading) {
|
||||
if (isShown && !modalData?.id && !_data?.id && !isLoading) {
|
||||
hide();
|
||||
}
|
||||
}, [isShown, data, isLoading, hide]);
|
||||
}, [isShown, modalData, _data, isLoading, hide]);
|
||||
|
||||
return (
|
||||
<OverlayPortal
|
||||
darken
|
||||
close={hide}
|
||||
show={isShown}
|
||||
show={shouldShow}
|
||||
durationClass="duration-500"
|
||||
zIndex={zIndex}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -9,18 +9,19 @@ import { MediaCard } from "@/components/media/MediaCard";
|
|||
import { Flare } from "@/components/utils/Flare";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
// Simple carousel component for collection overlay
|
||||
interface SimpleCarouselProps {
|
||||
mediaItems: MediaItem[];
|
||||
onShowDetails: (movieId: number) => void;
|
||||
onShowDetails?: (media: MediaItem) => void;
|
||||
categorySlug?: string;
|
||||
}
|
||||
|
||||
function SimpleCarousel({
|
||||
mediaItems,
|
||||
onShowDetails,
|
||||
onShowDetails: _onShowDetails,
|
||||
categorySlug = "collection",
|
||||
}: SimpleCarouselProps) {
|
||||
const { isMobile } = useIsMobile();
|
||||
|
|
@ -56,11 +57,7 @@ function SimpleCarousel({
|
|||
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"
|
||||
style={{ scrollSnapAlign: "start" }}
|
||||
>
|
||||
<MediaCard
|
||||
media={media}
|
||||
onShowDetails={() => onShowDetails(Number(media.id))}
|
||||
linkable
|
||||
/>
|
||||
<MediaCard media={media} linkable onShowDetails={_onShowDetails} />
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
|
@ -108,9 +105,10 @@ export function CollectionOverlay({
|
|||
collectionId,
|
||||
collectionName,
|
||||
onClose,
|
||||
onMovieClick,
|
||||
onMovieClick: _onMovieClick,
|
||||
}: CollectionOverlayProps) {
|
||||
const { t } = useTranslation();
|
||||
const { showModal } = useOverlayStack();
|
||||
const [collection, setCollection] = useState<CollectionData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -163,6 +161,15 @@ export function CollectionOverlay({
|
|||
};
|
||||
};
|
||||
|
||||
const handleShowDetails = (media: MediaItem) => {
|
||||
// Show details modal and close collection overlay
|
||||
showModal("details", {
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black/90 backdrop-blur-sm z-50 flex items-center justify-center p-4 sm:p-6 lg:p-8 transition-opacity duration-300"
|
||||
|
|
@ -311,7 +318,7 @@ export function CollectionOverlay({
|
|||
{!loading && !error && sortedMovies.length > 0 && (
|
||||
<SimpleCarousel
|
||||
mediaItems={sortedMovies.map(movieToMediaItem)}
|
||||
onShowDetails={onMovieClick}
|
||||
onShowDetails={handleShowDetails}
|
||||
categorySlug="collection"
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { usePlayerStore } from "@/stores/player/store";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
|
||||
|
|
@ -10,25 +9,20 @@ import { VideoPlayerButton } from "./Button";
|
|||
|
||||
export function InfoButton() {
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
const modal = useModal("player-details");
|
||||
const { showModal, isModalVisible } = useOverlayStack();
|
||||
const setHasOpenOverlay = usePlayerStore((s) => s.setHasOpenOverlay);
|
||||
const [detailsData, setDetailsData] = useState<{
|
||||
id: number;
|
||||
type: "movie" | "show";
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
setHasOpenOverlay(modal.isShown);
|
||||
}, [setHasOpenOverlay, modal.isShown]);
|
||||
setHasOpenOverlay(isModalVisible("player-details"));
|
||||
}, [setHasOpenOverlay, isModalVisible]);
|
||||
|
||||
const handleClick = async () => {
|
||||
if (!meta?.tmdbId) return;
|
||||
|
||||
setDetailsData({
|
||||
showModal("player-details", {
|
||||
id: Number(meta.tmdbId),
|
||||
type: meta.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
modal.show();
|
||||
};
|
||||
|
||||
const enableLowPerformanceMode = usePreferencesStore(
|
||||
|
|
@ -40,16 +34,11 @@ export function InfoButton() {
|
|||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<VideoPlayerButton
|
||||
icon={Icons.CIRCLE_QUESTION}
|
||||
iconSizeClass="text-base"
|
||||
className="p-2 !-mr-2"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
{detailsData && (
|
||||
<DetailsModal id="player-details" data={detailsData} minimal />
|
||||
)}
|
||||
</>
|
||||
<VideoPlayerButton
|
||||
icon={Icons.CIRCLE_QUESTION}
|
||||
iconSizeClass="text-base"
|
||||
className="p-2 !-mr-2"
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import { useTranslation } from "react-i18next";
|
|||
import { To, useNavigate } from "react-router-dom";
|
||||
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { useRandomTranslation } from "@/hooks/useRandomTranslation";
|
||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||
|
|
@ -62,7 +61,6 @@ export function HomePage() {
|
|||
const s = useSearch(search);
|
||||
const [showBookmarks, setShowBookmarks] = useState(false);
|
||||
const [showWatching, setShowWatching] = useState(false);
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const { showModal } = useOverlayStack();
|
||||
const enableDiscover = usePreferencesStore((state) => state.enableDiscover);
|
||||
const enableFeatured = usePreferencesStore((state) => state.enableFeatured);
|
||||
|
|
@ -83,11 +81,10 @@ export function HomePage() {
|
|||
};
|
||||
|
||||
const handleShowDetails = async (media: MediaItem | FeaturedMedia) => {
|
||||
setDetailsData({
|
||||
showModal("details", {
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
showModal("details");
|
||||
};
|
||||
|
||||
const renderHomeSections = () => {
|
||||
|
|
@ -235,8 +232,6 @@ export function HomePage() {
|
|||
</div>
|
||||
)}
|
||||
</WideContainer>
|
||||
|
||||
{detailsData && <DetailsModal id="details" data={detailsData} />}
|
||||
</HomeLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { SectionHeading } from "@/components/layout/SectionHeading";
|
|||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { EditGroupOrderModal } from "@/components/overlays/EditGroupOrderModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { UserIcon, UserIcons } from "@/components/UserIcon";
|
||||
|
|
@ -58,18 +57,16 @@ export function AllBookmarks({ onShowDetails }: AllBookmarksProps) {
|
|||
const [tempGroupOrder, setTempGroupOrder] = useState<string[]>([]);
|
||||
const backendUrl = useBackendUrl();
|
||||
const account = useAuthStore((s) => s.account);
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const { showModal } = useOverlayStack();
|
||||
|
||||
const handleShowDetails = async (media: MediaItem) => {
|
||||
if (onShowDetails) {
|
||||
onShowDetails(media);
|
||||
} else {
|
||||
setDetailsData({
|
||||
showModal("details", {
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
showModal("details");
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -431,8 +428,6 @@ export function AllBookmarks({ onShowDetails }: AllBookmarksProps) {
|
|||
setTempGroupOrder(newOrder);
|
||||
}}
|
||||
/>
|
||||
|
||||
{detailsData && <DetailsModal id="details" data={detailsData} />}
|
||||
</WideContainer>
|
||||
</SubPageLayout>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,24 +11,22 @@ import { TMDBMovieData } from "@/backend/metadata/types/tmdb";
|
|||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { MediaCard } from "@/components/media/MediaCard";
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { CarouselNavButtons } from "@/pages/discover/components/CarouselNavButtons";
|
||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||
import { useDiscoverStore } from "@/stores/discover";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
import { MediaCarousel } from "./components/MediaCarousel";
|
||||
|
||||
export function DiscoverMore() {
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const [curatedLists, setCuratedLists] = useState<CuratedMovieList[]>([]);
|
||||
const [movieDetails, setMovieDetails] = useState<{
|
||||
[listSlug: string]: TMDBMovieData[];
|
||||
}>({});
|
||||
const detailsModal = useModal("discover-details");
|
||||
const { showModal } = useOverlayStack();
|
||||
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||
const navigate = useNavigate();
|
||||
const { lastView } = useDiscoverStore();
|
||||
|
|
@ -65,11 +63,10 @@ export function DiscoverMore() {
|
|||
}, []);
|
||||
|
||||
const handleShowDetails = async (media: MediaItem) => {
|
||||
setDetailsData({
|
||||
showModal("discover-details", {
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
detailsModal.show();
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
|
|
@ -183,7 +180,6 @@ export function DiscoverMore() {
|
|||
</div>
|
||||
))}
|
||||
</WideContainer>
|
||||
{detailsData && <DetailsModal id="discover-details" data={detailsData} />}
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
|
||||
import { SubPageLayout } from "../layouts/SubPageLayout";
|
||||
import { FeaturedCarousel } from "./components/FeaturedCarousel";
|
||||
|
|
@ -11,22 +9,13 @@ import DiscoverContent from "./discoverContent";
|
|||
import { PageTitle } from "../parts/util/PageTitle";
|
||||
|
||||
export function Discover() {
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const detailsModal = useModal("discover-details");
|
||||
|
||||
// Clear details data when modal is closed
|
||||
useEffect(() => {
|
||||
if (!detailsModal.isShown) {
|
||||
setDetailsData(undefined);
|
||||
}
|
||||
}, [detailsModal.isShown]);
|
||||
const { showModal } = useOverlayStack();
|
||||
|
||||
const handleShowDetails = (media: FeaturedMedia) => {
|
||||
setDetailsData({
|
||||
showModal("discover-details", {
|
||||
id: Number(media.id),
|
||||
type: media.type,
|
||||
});
|
||||
detailsModal.show();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -52,8 +41,6 @@ export function Discover() {
|
|||
<div className="relative z-20 px-4 md:px-10">
|
||||
<DiscoverContent />
|
||||
</div>
|
||||
|
||||
{detailsData && <DetailsModal id="discover-details" data={detailsData} />}
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import { Icon, Icons } from "@/components/Icon";
|
|||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { MediaCard } from "@/components/media/MediaCard";
|
||||
import { MediaGrid } from "@/components/media/MediaGrid";
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { Heading1 } from "@/components/utils/Text";
|
||||
import {
|
||||
DiscoverContentType,
|
||||
|
|
@ -20,6 +18,7 @@ import {
|
|||
} from "@/pages/discover/hooks/useDiscoverMedia";
|
||||
import { SubPageLayout } from "@/pages/layouts/SubPageLayout";
|
||||
import { useDiscoverStore } from "@/stores/discover";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
|
|
@ -30,7 +29,6 @@ interface MoreContentProps {
|
|||
export function MoreContent({ onShowDetails }: MoreContentProps) {
|
||||
const { mediaType = "movie", contentType, id, category } = useParams();
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const [selectedProvider, setSelectedProvider] = useState<OptionItem | null>(
|
||||
null,
|
||||
);
|
||||
|
|
@ -40,7 +38,7 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
const [isContentVisible, setIsContentVisible] = useState(false);
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const detailsModal = useModal("discover-details");
|
||||
const { showModal } = useOverlayStack();
|
||||
const { lastView } = useDiscoverStore();
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
const progressStore = useProgressStore();
|
||||
|
|
@ -113,11 +111,10 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
onShowDetails(media);
|
||||
return;
|
||||
}
|
||||
setDetailsData({
|
||||
showModal("discover-details", {
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
detailsModal.show();
|
||||
};
|
||||
|
||||
const handleLoadMore = async () => {
|
||||
|
|
@ -386,7 +383,6 @@ export function MoreContent({ onShowDetails }: MoreContentProps) {
|
|||
)}
|
||||
</div>
|
||||
</WideContainer>
|
||||
{detailsData && <DetailsModal id="discover-details" data={detailsData} />}
|
||||
</SubPageLayout>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,12 @@
|
|||
import classNames from "classnames";
|
||||
import { t } from "i18next";
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { Button } from "@/components/buttons/Button";
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { useDiscoverStore } from "@/stores/discover";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { useProgressStore } from "@/stores/progress";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
|
|
@ -18,9 +17,8 @@ import { ScrollToTopButton } from "./components/ScrollToTopButton";
|
|||
|
||||
export function DiscoverContent() {
|
||||
const { selectedCategory, setSelectedCategory } = useDiscoverStore();
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const navigate = useNavigate();
|
||||
const detailsModal = useModal("discover-details");
|
||||
const { showModal } = useOverlayStack();
|
||||
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||
const progressItems = useProgressStore((state) => state.items);
|
||||
|
||||
|
|
@ -34,11 +32,10 @@ export function DiscoverContent() {
|
|||
};
|
||||
|
||||
const handleShowDetails = async (media: MediaItem | FeaturedMedia) => {
|
||||
setDetailsData({
|
||||
showModal("discover-details", {
|
||||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
detailsModal.show();
|
||||
};
|
||||
|
||||
const movieProgressItems = Object.entries(progressItems || {}).filter(
|
||||
|
|
@ -240,7 +237,7 @@ export function DiscoverContent() {
|
|||
|
||||
<ScrollToTopButton />
|
||||
|
||||
{detailsData && <DetailsModal id="discover-details" data={detailsData} />}
|
||||
{/* DetailsModal is now managed by overlayStack */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
|
||||
import { convertLegacyUrl, isLegacyUrl } from "@/backend/metadata/getmeta";
|
||||
import { generateQuickSearchMediaUrl } from "@/backend/metadata/tmdb";
|
||||
import { DetailsModal } from "@/components/overlays/detailsModal";
|
||||
import { KeyboardCommandsModal } from "@/components/overlays/KeyboardCommandsModal";
|
||||
import { NotificationModal } from "@/components/overlays/notificationsModal";
|
||||
import { useGlobalKeyboardEvents } from "@/hooks/useGlobalKeyboardEvents";
|
||||
|
|
@ -39,6 +40,7 @@ import { RegisterPage } from "@/pages/Register";
|
|||
import { SupportPage } from "@/pages/Support";
|
||||
import { Layout } from "@/setup/Layout";
|
||||
import { useHistoryListener } from "@/stores/history";
|
||||
import { useClearModalsOnNavigation } from "@/stores/interface/overlayStack";
|
||||
import { LanguageProvider } from "@/stores/language";
|
||||
|
||||
const DeveloperPage = lazy(() => import("@/pages/DeveloperPage"));
|
||||
|
|
@ -103,6 +105,7 @@ function App() {
|
|||
useHistoryListener();
|
||||
useOnlineListener();
|
||||
useGlobalKeyboardEvents();
|
||||
useClearModalsOnNavigation();
|
||||
const maintenance = false; // Shows maintance page
|
||||
const [showDowntime, setShowDowntime] = useState(maintenance);
|
||||
|
||||
|
|
@ -123,6 +126,9 @@ function App() {
|
|||
<LanguageProvider />
|
||||
<NotificationModal id="notifications" />
|
||||
<KeyboardCommandsModal id="keyboard-commands" />
|
||||
<DetailsModal id="details" />
|
||||
<DetailsModal id="discover-details" />
|
||||
<DetailsModal id="player-details" />
|
||||
{!showDowntime && (
|
||||
<Routes>
|
||||
{/* functional routes */}
|
||||
|
|
|
|||
|
|
@ -1,35 +1,51 @@
|
|||
import { useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
type OverlayType = "volume" | "subtitle" | "speed" | null;
|
||||
|
||||
interface ModalData {
|
||||
id: number;
|
||||
type: "movie" | "show";
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface OverlayStackStore {
|
||||
currentOverlay: OverlayType;
|
||||
modalStack: string[];
|
||||
modalData: Record<string, ModalData | undefined>;
|
||||
setCurrentOverlay: (overlay: OverlayType) => void;
|
||||
showModal: (id: string) => void;
|
||||
showModal: (id: string, data?: ModalData) => void;
|
||||
hideModal: (id: string) => void;
|
||||
isModalVisible: (id: string) => boolean;
|
||||
getTopModal: () => string | null;
|
||||
getModalData: (id: string) => ModalData | undefined;
|
||||
clearAllModals: () => void;
|
||||
}
|
||||
|
||||
export const useOverlayStack = create<OverlayStackStore>()(
|
||||
immer((set, get) => ({
|
||||
currentOverlay: null,
|
||||
modalStack: [],
|
||||
modalData: {},
|
||||
setCurrentOverlay: (overlay) =>
|
||||
set((state) => {
|
||||
state.currentOverlay = overlay;
|
||||
}),
|
||||
showModal: (id: string) =>
|
||||
showModal: (id: string, data?: ModalData) =>
|
||||
set((state) => {
|
||||
if (!state.modalStack.includes(id)) {
|
||||
state.modalStack.push(id);
|
||||
}
|
||||
if (data) {
|
||||
state.modalData[id] = data;
|
||||
}
|
||||
}),
|
||||
hideModal: (id: string) =>
|
||||
set((state) => {
|
||||
state.modalStack = state.modalStack.filter((modalId) => modalId !== id);
|
||||
delete state.modalData[id];
|
||||
}),
|
||||
isModalVisible: (id: string) => {
|
||||
return get().modalStack.includes(id);
|
||||
|
|
@ -38,5 +54,24 @@ export const useOverlayStack = create<OverlayStackStore>()(
|
|||
const stack = get().modalStack;
|
||||
return stack.length > 0 ? stack[stack.length - 1] : null;
|
||||
},
|
||||
getModalData: (id: string) => {
|
||||
return get().modalData[id];
|
||||
},
|
||||
clearAllModals: () =>
|
||||
set((state) => {
|
||||
state.modalStack = [];
|
||||
state.modalData = {};
|
||||
state.currentOverlay = null;
|
||||
}),
|
||||
})),
|
||||
);
|
||||
|
||||
// Hook to clear modals on navigation
|
||||
export function useClearModalsOnNavigation() {
|
||||
const location = useLocation();
|
||||
const clearAllModals = useOverlayStack((state) => state.clearAllModals);
|
||||
|
||||
useEffect(() => {
|
||||
clearAllModals();
|
||||
}, [location.pathname, clearAllModals]);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue