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