mirror of
https://github.com/p-stream/p-stream.git
synced 2026-03-30 06:08:45 +00:00
add modal stacking support
This commit is contained in:
parent
cbf1d678f2
commit
1d70f002e7
7 changed files with 83 additions and 31 deletions
|
|
@ -141,7 +141,8 @@
|
|||
"actions": {
|
||||
"copied": "Copied",
|
||||
"copy": "Copy",
|
||||
"cancel": "Cancel"
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm"
|
||||
},
|
||||
"auth": {
|
||||
"createAccount": "Don't have an account yet 😬 <0>Create an account.</0>",
|
||||
|
|
@ -333,7 +334,9 @@
|
|||
"show": "Show"
|
||||
},
|
||||
"episodeShort": "E",
|
||||
"seasonShort": "S"
|
||||
"seasonShort": "S",
|
||||
"seasonWatched": "Are you sure you want to mark the season as watched?",
|
||||
"seasonUnwatched": "Are you sure you want to mark the season as unwatched?"
|
||||
},
|
||||
"details": {
|
||||
"resume": "Resume",
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { mediaItemToId } from "@/backend/metadata/tmdb";
|
|||
import { DotList } from "@/components/text/DotList";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
|
|
@ -16,7 +17,6 @@ import { MediaBookmarkButton } from "./MediaBookmark";
|
|||
import { IconPatch } from "../buttons/IconPatch";
|
||||
import { Icon, Icons } from "../Icon";
|
||||
import { DetailsModal } from "../overlays/details/DetailsModal";
|
||||
import { useModal } from "../overlays/Modal";
|
||||
|
||||
export interface MediaCardProps {
|
||||
media: MediaItem;
|
||||
|
|
@ -223,7 +223,7 @@ export function MediaCard(props: MediaCardProps) {
|
|||
id: number;
|
||||
type: "movie" | "show";
|
||||
} | null>(null);
|
||||
const detailsModal = useModal("details");
|
||||
const { showModal } = useOverlayStack();
|
||||
const enableDetailsModal = usePreferencesStore(
|
||||
(state) => state.enableDetailsModal,
|
||||
);
|
||||
|
|
@ -258,8 +258,8 @@ export function MediaCard(props: MediaCardProps) {
|
|||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
detailsModal.show();
|
||||
}, [media, detailsModal, onShowDetails]);
|
||||
showModal("details");
|
||||
}, [media, showModal, onShowDetails]);
|
||||
|
||||
const handleCardClick = (e: React.MouseEvent) => {
|
||||
if (enableDetailsModal && canLink) {
|
||||
|
|
|
|||
|
|
@ -7,15 +7,15 @@ import { Icons } from "@/components/Icon";
|
|||
import { OverlayPortal } from "@/components/overlays/OverlayDisplay";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
import { Heading2 } from "@/components/utils/Text";
|
||||
import { useQueryParam } from "@/hooks/useQueryParams";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
|
||||
export function useModal(id: string) {
|
||||
const [currentModal, setCurrentModal] = useQueryParam("m");
|
||||
const show = useCallback(() => setCurrentModal(id), [id, setCurrentModal]);
|
||||
const hide = useCallback(() => setCurrentModal(null), [setCurrentModal]);
|
||||
const { showModal, hideModal, isModalVisible } = useOverlayStack();
|
||||
const show = useCallback(() => showModal(id), [id, showModal]);
|
||||
const hide = useCallback(() => hideModal(id), [id, hideModal]);
|
||||
return {
|
||||
id,
|
||||
isShown: currentModal === id,
|
||||
isShown: isModalVisible(id),
|
||||
show,
|
||||
hide,
|
||||
};
|
||||
|
|
@ -33,9 +33,17 @@ export function ModalCard(props: { children?: ReactNode }) {
|
|||
|
||||
export function Modal(props: { id: string; children?: ReactNode }) {
|
||||
const modal = useModal(props.id);
|
||||
const { modalStack } = useOverlayStack();
|
||||
const modalIndex = modalStack.indexOf(props.id);
|
||||
const zIndex = modalIndex >= 0 ? 1000 + modalIndex : 999;
|
||||
|
||||
return (
|
||||
<OverlayPortal darken close={modal.hide} show={modal.isShown}>
|
||||
<OverlayPortal
|
||||
darken
|
||||
close={modal.hide}
|
||||
show={modal.isShown}
|
||||
zIndex={zIndex}
|
||||
>
|
||||
<Helmet>
|
||||
<html data-no-scroll />
|
||||
</Helmet>
|
||||
|
|
|
|||
|
|
@ -77,10 +77,12 @@ export function OverlayPortal(props: {
|
|||
show?: boolean;
|
||||
close?: () => void;
|
||||
durationClass?: string;
|
||||
zIndex?: number;
|
||||
}) {
|
||||
const [portalElement, setPortalElement] = useState<Element | null>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const close = props.close;
|
||||
const zIndex = props.zIndex ?? 999;
|
||||
|
||||
useEffect(() => {
|
||||
const element = ref.current?.closest(".popout-location");
|
||||
|
|
@ -93,7 +95,10 @@ export function OverlayPortal(props: {
|
|||
? createPortal(
|
||||
<Transition show={props.show} animation="none">
|
||||
<FocusTrap>
|
||||
<div className="popout-wrapper fixed overflow-hidden pointer-events-auto inset-0 z-[999] select-none">
|
||||
<div
|
||||
className="popout-wrapper fixed overflow-hidden pointer-events-auto inset-0 select-none"
|
||||
style={{ zIndex }}
|
||||
>
|
||||
<Transition animation="fade" isChild>
|
||||
<div
|
||||
onClick={close}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import classNames from "classnames";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
|
||||
import {
|
||||
|
|
@ -16,18 +16,24 @@ import {
|
|||
import { IconPatch } from "@/components/buttons/IconPatch";
|
||||
import { Icons } from "@/components/Icon";
|
||||
import { Flare } from "@/components/utils/Flare";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
|
||||
import { useModal } from "../Modal";
|
||||
import { OverlayPortal } from "../OverlayDisplay";
|
||||
import { DetailsContent } from "./DetailsContent";
|
||||
import { DetailsSkeleton } from "./DetailsSkeleton";
|
||||
import { DetailsModalProps } from "./types";
|
||||
|
||||
export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
||||
const modal = useModal(id);
|
||||
const { hideModal, isModalVisible, modalStack } = useOverlayStack();
|
||||
const [detailsData, setDetailsData] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const modalIndex = modalStack.indexOf(id);
|
||||
const zIndex = modalIndex >= 0 ? 1000 + modalIndex : 999;
|
||||
|
||||
const hide = useCallback(() => hideModal(id), [hideModal, id]);
|
||||
const isShown = isModalVisible(id);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchDetails = async () => {
|
||||
if (!data?.id || !data?.type) return;
|
||||
|
|
@ -106,23 +112,24 @@ export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
|||
}
|
||||
};
|
||||
|
||||
if (modal.isShown && data?.id) {
|
||||
if (isShown && data?.id) {
|
||||
fetchDetails();
|
||||
}
|
||||
}, [modal.isShown, data]);
|
||||
}, [isShown, data]);
|
||||
|
||||
useEffect(() => {
|
||||
if (modal.isShown && !data?.id && !isLoading) {
|
||||
modal.hide();
|
||||
if (isShown && !data?.id && !isLoading) {
|
||||
hide();
|
||||
}
|
||||
}, [modal, data, isLoading]);
|
||||
}, [isShown, data, isLoading, hide]);
|
||||
|
||||
return (
|
||||
<OverlayPortal
|
||||
darken
|
||||
close={modal.hide}
|
||||
show={modal.isShown}
|
||||
close={hide}
|
||||
show={isShown}
|
||||
durationClass="duration-500"
|
||||
zIndex={zIndex}
|
||||
>
|
||||
<Helmet>
|
||||
<html data-no-scroll />
|
||||
|
|
@ -148,7 +155,7 @@ export function DetailsModal({ id, data, minimal }: DetailsModalProps) {
|
|||
<button
|
||||
type="button"
|
||||
className="text-s font-semibold text-type-secondary hover:text-white transition-transform hover:scale-95 select-none"
|
||||
onClick={modal.hide}
|
||||
onClick={hide}
|
||||
>
|
||||
<IconPatch icon={Icons.X} />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import { To, useNavigate } from "react-router-dom";
|
|||
|
||||
import { WideContainer } from "@/components/layout/WideContainer";
|
||||
import { DetailsModal } from "@/components/overlays/details/DetailsModal";
|
||||
import { useModal } from "@/components/overlays/Modal";
|
||||
import { useDebounce } from "@/hooks/useDebounce";
|
||||
import { useRandomTranslation } from "@/hooks/useRandomTranslation";
|
||||
import { useSearchQuery } from "@/hooks/useSearchQuery";
|
||||
|
|
@ -21,6 +20,7 @@ import { WatchingPart } from "@/pages/parts/home/WatchingPart";
|
|||
import { SearchListPart } from "@/pages/parts/search/SearchListPart";
|
||||
import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart";
|
||||
import { conf } from "@/setup/config";
|
||||
import { useOverlayStack } from "@/stores/interface/overlayStack";
|
||||
import { usePreferencesStore } from "@/stores/preferences";
|
||||
import { MediaItem } from "@/utils/mediaTypes";
|
||||
|
||||
|
|
@ -63,7 +63,7 @@ export function HomePage() {
|
|||
const [showBookmarks, setShowBookmarks] = useState(false);
|
||||
const [showWatching, setShowWatching] = useState(false);
|
||||
const [detailsData, setDetailsData] = useState<any>();
|
||||
const detailsModal = useModal("details");
|
||||
const { showModal } = useOverlayStack();
|
||||
const enableDiscover = usePreferencesStore((state) => state.enableDiscover);
|
||||
const enableFeatured = usePreferencesStore((state) => state.enableFeatured);
|
||||
const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
|
||||
|
|
@ -84,7 +84,7 @@ export function HomePage() {
|
|||
id: Number(media.id),
|
||||
type: media.type === "movie" ? "movie" : "show",
|
||||
});
|
||||
detailsModal.show();
|
||||
showModal("details");
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,13 +1,42 @@
|
|||
import { create } from "zustand";
|
||||
import { immer } from "zustand/middleware/immer";
|
||||
|
||||
type OverlayType = "volume" | "subtitle" | null;
|
||||
|
||||
interface OverlayStackStore {
|
||||
currentOverlay: OverlayType;
|
||||
modalStack: string[];
|
||||
setCurrentOverlay: (overlay: OverlayType) => void;
|
||||
showModal: (id: string) => void;
|
||||
hideModal: (id: string) => void;
|
||||
isModalVisible: (id: string) => boolean;
|
||||
getTopModal: () => string | null;
|
||||
}
|
||||
|
||||
export const useOverlayStack = create<OverlayStackStore>((set) => ({
|
||||
currentOverlay: null,
|
||||
setCurrentOverlay: (overlay) => set({ currentOverlay: overlay }),
|
||||
}));
|
||||
export const useOverlayStack = create<OverlayStackStore>()(
|
||||
immer((set, get) => ({
|
||||
currentOverlay: null,
|
||||
modalStack: [],
|
||||
setCurrentOverlay: (overlay) =>
|
||||
set((state) => {
|
||||
state.currentOverlay = overlay;
|
||||
}),
|
||||
showModal: (id: string) =>
|
||||
set((state) => {
|
||||
if (!state.modalStack.includes(id)) {
|
||||
state.modalStack.push(id);
|
||||
}
|
||||
}),
|
||||
hideModal: (id: string) =>
|
||||
set((state) => {
|
||||
state.modalStack = state.modalStack.filter((modalId) => modalId !== id);
|
||||
}),
|
||||
isModalVisible: (id: string) => {
|
||||
return get().modalStack.includes(id);
|
||||
},
|
||||
getTopModal: () => {
|
||||
const stack = get().modalStack;
|
||||
return stack.length > 0 ? stack[stack.length - 1] : null;
|
||||
},
|
||||
})),
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue