pstream is dead; long live pstream taciturnaxolotl.github.io/pstream-ng/
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add modal stacking support

Pas 1d70f002 cbf1d678

+83 -31
+5 -2
src/assets/locales/en.json
··· 141 141 "actions": { 142 142 "copied": "Copied", 143 143 "copy": "Copy", 144 - "cancel": "Cancel" 144 + "cancel": "Cancel", 145 + "confirm": "Confirm" 145 146 }, 146 147 "auth": { 147 148 "createAccount": "Don't have an account yet 😬 <0>Create an account.</0>", ··· 333 334 "show": "Show" 334 335 }, 335 336 "episodeShort": "E", 336 - "seasonShort": "S" 337 + "seasonShort": "S", 338 + "seasonWatched": "Are you sure you want to mark the season as watched?", 339 + "seasonUnwatched": "Are you sure you want to mark the season as unwatched?" 337 340 }, 338 341 "details": { 339 342 "resume": "Resume",
+4 -4
src/components/media/MediaCard.tsx
··· 9 9 import { DotList } from "@/components/text/DotList"; 10 10 import { Flare } from "@/components/utils/Flare"; 11 11 import { useSearchQuery } from "@/hooks/useSearchQuery"; 12 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 12 13 import { usePreferencesStore } from "@/stores/preferences"; 13 14 import { MediaItem } from "@/utils/mediaTypes"; 14 15 ··· 16 17 import { IconPatch } from "../buttons/IconPatch"; 17 18 import { Icon, Icons } from "../Icon"; 18 19 import { DetailsModal } from "../overlays/details/DetailsModal"; 19 - import { useModal } from "../overlays/Modal"; 20 20 21 21 export interface MediaCardProps { 22 22 media: MediaItem; ··· 223 223 id: number; 224 224 type: "movie" | "show"; 225 225 } | null>(null); 226 - const detailsModal = useModal("details"); 226 + const { showModal } = useOverlayStack(); 227 227 const enableDetailsModal = usePreferencesStore( 228 228 (state) => state.enableDetailsModal, 229 229 ); ··· 258 258 id: Number(media.id), 259 259 type: media.type === "movie" ? "movie" : "show", 260 260 }); 261 - detailsModal.show(); 262 - }, [media, detailsModal, onShowDetails]); 261 + showModal("details"); 262 + }, [media, showModal, onShowDetails]); 263 263 264 264 const handleCardClick = (e: React.MouseEvent) => { 265 265 if (enableDetailsModal && canLink) {
+14 -6
src/components/overlays/Modal.tsx
··· 7 7 import { OverlayPortal } from "@/components/overlays/OverlayDisplay"; 8 8 import { Flare } from "@/components/utils/Flare"; 9 9 import { Heading2 } from "@/components/utils/Text"; 10 - import { useQueryParam } from "@/hooks/useQueryParams"; 10 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 11 11 12 12 export function useModal(id: string) { 13 - const [currentModal, setCurrentModal] = useQueryParam("m"); 14 - const show = useCallback(() => setCurrentModal(id), [id, setCurrentModal]); 15 - const hide = useCallback(() => setCurrentModal(null), [setCurrentModal]); 13 + const { showModal, hideModal, isModalVisible } = useOverlayStack(); 14 + const show = useCallback(() => showModal(id), [id, showModal]); 15 + const hide = useCallback(() => hideModal(id), [id, hideModal]); 16 16 return { 17 17 id, 18 - isShown: currentModal === id, 18 + isShown: isModalVisible(id), 19 19 show, 20 20 hide, 21 21 }; ··· 33 33 34 34 export function Modal(props: { id: string; children?: ReactNode }) { 35 35 const modal = useModal(props.id); 36 + const { modalStack } = useOverlayStack(); 37 + const modalIndex = modalStack.indexOf(props.id); 38 + const zIndex = modalIndex >= 0 ? 1000 + modalIndex : 999; 36 39 37 40 return ( 38 - <OverlayPortal darken close={modal.hide} show={modal.isShown}> 41 + <OverlayPortal 42 + darken 43 + close={modal.hide} 44 + show={modal.isShown} 45 + zIndex={zIndex} 46 + > 39 47 <Helmet> 40 48 <html data-no-scroll /> 41 49 </Helmet>
+6 -1
src/components/overlays/OverlayDisplay.tsx
··· 77 77 show?: boolean; 78 78 close?: () => void; 79 79 durationClass?: string; 80 + zIndex?: number; 80 81 }) { 81 82 const [portalElement, setPortalElement] = useState<Element | null>(null); 82 83 const ref = useRef<HTMLDivElement>(null); 83 84 const close = props.close; 85 + const zIndex = props.zIndex ?? 999; 84 86 85 87 useEffect(() => { 86 88 const element = ref.current?.closest(".popout-location"); ··· 93 95 ? createPortal( 94 96 <Transition show={props.show} animation="none"> 95 97 <FocusTrap> 96 - <div className="popout-wrapper fixed overflow-hidden pointer-events-auto inset-0 z-[999] select-none"> 98 + <div 99 + className="popout-wrapper fixed overflow-hidden pointer-events-auto inset-0 select-none" 100 + style={{ zIndex }} 101 + > 97 102 <Transition animation="fade" isChild> 98 103 <div 99 104 onClick={close}
+18 -11
src/components/overlays/details/DetailsModal.tsx
··· 1 1 import classNames from "classnames"; 2 - import { useEffect, useState } from "react"; 2 + import { useCallback, useEffect, useState } from "react"; 3 3 import { Helmet } from "react-helmet-async"; 4 4 5 5 import { ··· 16 16 import { IconPatch } from "@/components/buttons/IconPatch"; 17 17 import { Icons } from "@/components/Icon"; 18 18 import { Flare } from "@/components/utils/Flare"; 19 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 19 20 20 - import { useModal } from "../Modal"; 21 21 import { OverlayPortal } from "../OverlayDisplay"; 22 22 import { DetailsContent } from "./DetailsContent"; 23 23 import { DetailsSkeleton } from "./DetailsSkeleton"; 24 24 import { DetailsModalProps } from "./types"; 25 25 26 26 export function DetailsModal({ id, data, minimal }: DetailsModalProps) { 27 - const modal = useModal(id); 27 + const { hideModal, isModalVisible, modalStack } = useOverlayStack(); 28 28 const [detailsData, setDetailsData] = useState<any>(null); 29 29 const [isLoading, setIsLoading] = useState(false); 30 + 31 + const modalIndex = modalStack.indexOf(id); 32 + const zIndex = modalIndex >= 0 ? 1000 + modalIndex : 999; 33 + 34 + const hide = useCallback(() => hideModal(id), [hideModal, id]); 35 + const isShown = isModalVisible(id); 30 36 31 37 useEffect(() => { 32 38 const fetchDetails = async () => { ··· 106 112 } 107 113 }; 108 114 109 - if (modal.isShown && data?.id) { 115 + if (isShown && data?.id) { 110 116 fetchDetails(); 111 117 } 112 - }, [modal.isShown, data]); 118 + }, [isShown, data]); 113 119 114 120 useEffect(() => { 115 - if (modal.isShown && !data?.id && !isLoading) { 116 - modal.hide(); 121 + if (isShown && !data?.id && !isLoading) { 122 + hide(); 117 123 } 118 - }, [modal, data, isLoading]); 124 + }, [isShown, data, isLoading, hide]); 119 125 120 126 return ( 121 127 <OverlayPortal 122 128 darken 123 - close={modal.hide} 124 - show={modal.isShown} 129 + close={hide} 130 + show={isShown} 125 131 durationClass="duration-500" 132 + zIndex={zIndex} 126 133 > 127 134 <Helmet> 128 135 <html data-no-scroll /> ··· 148 155 <button 149 156 type="button" 150 157 className="text-s font-semibold text-type-secondary hover:text-white transition-transform hover:scale-95 select-none" 151 - onClick={modal.hide} 158 + onClick={hide} 152 159 > 153 160 <IconPatch icon={Icons.X} /> 154 161 </button>
+3 -3
src/pages/HomePage.tsx
··· 5 5 6 6 import { WideContainer } from "@/components/layout/WideContainer"; 7 7 import { DetailsModal } from "@/components/overlays/details/DetailsModal"; 8 - import { useModal } from "@/components/overlays/Modal"; 9 8 import { useDebounce } from "@/hooks/useDebounce"; 10 9 import { useRandomTranslation } from "@/hooks/useRandomTranslation"; 11 10 import { useSearchQuery } from "@/hooks/useSearchQuery"; ··· 21 20 import { SearchListPart } from "@/pages/parts/search/SearchListPart"; 22 21 import { SearchLoadingPart } from "@/pages/parts/search/SearchLoadingPart"; 23 22 import { conf } from "@/setup/config"; 23 + import { useOverlayStack } from "@/stores/interface/overlayStack"; 24 24 import { usePreferencesStore } from "@/stores/preferences"; 25 25 import { MediaItem } from "@/utils/mediaTypes"; 26 26 ··· 63 63 const [showBookmarks, setShowBookmarks] = useState(false); 64 64 const [showWatching, setShowWatching] = useState(false); 65 65 const [detailsData, setDetailsData] = useState<any>(); 66 - const detailsModal = useModal("details"); 66 + const { showModal } = useOverlayStack(); 67 67 const enableDiscover = usePreferencesStore((state) => state.enableDiscover); 68 68 const enableFeatured = usePreferencesStore((state) => state.enableFeatured); 69 69 const carouselRefs = useRef<{ [key: string]: HTMLDivElement | null }>({}); ··· 84 84 id: Number(media.id), 85 85 type: media.type === "movie" ? "movie" : "show", 86 86 }); 87 - detailsModal.show(); 87 + showModal("details"); 88 88 }; 89 89 90 90 return (
+33 -4
src/stores/interface/overlayStack.ts
··· 1 1 import { create } from "zustand"; 2 + import { immer } from "zustand/middleware/immer"; 2 3 3 4 type OverlayType = "volume" | "subtitle" | null; 4 5 5 6 interface OverlayStackStore { 6 7 currentOverlay: OverlayType; 8 + modalStack: string[]; 7 9 setCurrentOverlay: (overlay: OverlayType) => void; 10 + showModal: (id: string) => void; 11 + hideModal: (id: string) => void; 12 + isModalVisible: (id: string) => boolean; 13 + getTopModal: () => string | null; 8 14 } 9 15 10 - export const useOverlayStack = create<OverlayStackStore>((set) => ({ 11 - currentOverlay: null, 12 - setCurrentOverlay: (overlay) => set({ currentOverlay: overlay }), 13 - })); 16 + export const useOverlayStack = create<OverlayStackStore>()( 17 + immer((set, get) => ({ 18 + currentOverlay: null, 19 + modalStack: [], 20 + setCurrentOverlay: (overlay) => 21 + set((state) => { 22 + state.currentOverlay = overlay; 23 + }), 24 + showModal: (id: string) => 25 + set((state) => { 26 + if (!state.modalStack.includes(id)) { 27 + state.modalStack.push(id); 28 + } 29 + }), 30 + hideModal: (id: string) => 31 + set((state) => { 32 + state.modalStack = state.modalStack.filter((modalId) => modalId !== id); 33 + }), 34 + isModalVisible: (id: string) => { 35 + return get().modalStack.includes(id); 36 + }, 37 + getTopModal: () => { 38 + const stack = get().modalStack; 39 + return stack.length > 0 ? stack[stack.length - 1] : null; 40 + }, 41 + })), 42 + );