diff --git a/src/components/media/MediaCard.tsx b/src/components/media/MediaCard.tsx index 94cba547..85140103 100644 --- a/src/components/media/MediaCard.tsx +++ b/src/components/media/MediaCard.tsx @@ -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 = ( - <> - - {detailsData && } - + ); if (!canLink) { diff --git a/src/components/overlays/detailsModal/components/layout/DetailsModal.tsx b/src/components/overlays/detailsModal/components/layout/DetailsModal.tsx index 3eb7a5cb..c1a4b1a1 100644 --- a/src/components/overlays/detailsModal/components/layout/DetailsModal.tsx +++ b/src/components/overlays/detailsModal/components/layout/DetailsModal.tsx @@ -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(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 ( diff --git a/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx b/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx index f6c8c3fe..b9ce9758 100644 --- a/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx +++ b/src/components/overlays/detailsModal/components/overlays/CollectionOverlay.tsx @@ -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" }} > - onShowDetails(Number(media.id))} - linkable - /> + ))} @@ -108,9 +105,10 @@ export function CollectionOverlay({ collectionId, collectionName, onClose, - onMovieClick, + onMovieClick: _onMovieClick, }: CollectionOverlayProps) { const { t } = useTranslation(); + const { showModal } = useOverlayStack(); const [collection, setCollection] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(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 (
0 && ( )} diff --git a/src/components/player/internals/InfoButton.tsx b/src/components/player/internals/InfoButton.tsx index 093c08fa..0ca76d04 100644 --- a/src/components/player/internals/InfoButton.tsx +++ b/src/components/player/internals/InfoButton.tsx @@ -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 ( - <> - - {detailsData && ( - - )} - + ); } diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index f9b0c520..88a97ee0 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -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(); 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() {
)} - - {detailsData && } ); } diff --git a/src/pages/bookmarks/AllBookmarks.tsx b/src/pages/bookmarks/AllBookmarks.tsx index c71e4f02..028e4126 100644 --- a/src/pages/bookmarks/AllBookmarks.tsx +++ b/src/pages/bookmarks/AllBookmarks.tsx @@ -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([]); const backendUrl = useBackendUrl(); const account = useAuthStore((s) => s.account); - const [detailsData, setDetailsData] = useState(); 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 && } ); diff --git a/src/pages/discover/AllMovieLists.tsx b/src/pages/discover/AllMovieLists.tsx index c2ff5c87..7d663267 100644 --- a/src/pages/discover/AllMovieLists.tsx +++ b/src/pages/discover/AllMovieLists.tsx @@ -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(); const [curatedLists, setCuratedLists] = useState([]); 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() { ))} - {detailsData && } ); } diff --git a/src/pages/discover/Discover.tsx b/src/pages/discover/Discover.tsx index 0c3a8b81..07da54ca 100644 --- a/src/pages/discover/Discover.tsx +++ b/src/pages/discover/Discover.tsx @@ -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(); - 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() {
- - {detailsData && } ); } diff --git a/src/pages/discover/MoreContent.tsx b/src/pages/discover/MoreContent.tsx index 5869b854..29f86efb 100644 --- a/src/pages/discover/MoreContent.tsx +++ b/src/pages/discover/MoreContent.tsx @@ -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(); const [selectedProvider, setSelectedProvider] = useState( 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) { )} - {detailsData && } ); } diff --git a/src/pages/discover/discoverContent.tsx b/src/pages/discover/discoverContent.tsx index fee88691..b060bb9c 100644 --- a/src/pages/discover/discoverContent.tsx +++ b/src/pages/discover/discoverContent.tsx @@ -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(); 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() { - {detailsData && } + {/* DetailsModal is now managed by overlayStack */} ); } diff --git a/src/setup/App.tsx b/src/setup/App.tsx index 0876f79f..cfc49df3 100644 --- a/src/setup/App.tsx +++ b/src/setup/App.tsx @@ -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() { + + + {!showDowntime && ( {/* functional routes */} diff --git a/src/stores/interface/overlayStack.ts b/src/stores/interface/overlayStack.ts index 22d9c280..c84a9f90 100644 --- a/src/stores/interface/overlayStack.ts +++ b/src/stores/interface/overlayStack.ts @@ -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; 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()( 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()( 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]); +}