From 979c6d426559a0a4bf47c81943dd7097c1e858fe Mon Sep 17 00:00:00 2001 From: PhilReact Date: Thu, 19 Jun 2025 17:01:39 +0300 Subject: [PATCH] started on mutli-publish status --- .../MultiPublish/MultiPublishDialog.tsx | 169 ++++++++++++++++++ .../VideoPlayer/SubtitleManager.tsx | 2 +- src/components/VideoPlayer/VideoPlayer.tsx | 6 +- .../VideoPlayer/useVideoPlayerController.tsx | 2 - src/context/GlobalProvider.tsx | 21 ++- src/hooks/useGlobalPipPlayer.tsx | 13 +- src/hooks/usePublish.tsx | 24 +++ src/hooks/useResourceStatus.tsx | 2 +- src/state/multiplePublish.ts | 79 ++++++++ 9 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 src/components/MultiPublish/MultiPublishDialog.tsx create mode 100644 src/state/multiplePublish.ts diff --git a/src/components/MultiPublish/MultiPublishDialog.tsx b/src/components/MultiPublish/MultiPublishDialog.tsx new file mode 100644 index 0000000..40eb47d --- /dev/null +++ b/src/components/MultiPublish/MultiPublishDialog.tsx @@ -0,0 +1,169 @@ +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; + +import { + Dialog, + DialogTitle, + DialogContent, + Typography, + Box, + LinearProgress, + Stack +} from '@mui/material'; +import { PublishStatus, useMultiplePublishStore, usePublishStatusStore } from "../../state/multiplePublish"; +import { ResourceToPublish } from "../../types/qortalRequests/types"; + + + + +const MultiPublishDialogComponent = () => { + const { resources, isPublishing } = useMultiplePublishStore((state) => ({ + resources: state.resources, + isPublishing: state.isPublishing +})); +const { publishStatus, setPublishStatusByKey, getPublishStatusByKey } = usePublishStatusStore(); + + + useEffect(() => { + function handleNavigation(event: any) { + if (event.data?.action === 'PUBLISH_STATUS') { + const data = event.data; + console.log('datadata', data) + // Validate required structure before continuing + if ( + !data.publishLocation || + typeof data.publishLocation.name !== 'string' || + typeof data.publishLocation.identifier !== 'string' || + typeof data.publishLocation.service !== 'string' + ) { + console.warn('Invalid PUBLISH_STATUS data, skipping:', data); + return; + } + + const { publishLocation, chunks, totalChunks } = data; + + const key = `${publishLocation?.service}-${publishLocation?.name}-${publishLocation?.identifier}`; + + try { + const dataToBeSent: any = {}; + if (chunks !== undefined && chunks !== null) { + dataToBeSent.chunks = chunks; + } + if (totalChunks !== undefined && totalChunks !== null) { + dataToBeSent.totalChunks = totalChunks; + } + + setPublishStatusByKey(key, { + publishLocation, + ...dataToBeSent, + processed: data?.processed || false +}); + } catch (err) { + console.error('Failed to set publish status:', err); + } + } + } + + window.addEventListener("message", handleNavigation); + + return () => { + window.removeEventListener("message", handleNavigation); + }; + }, []); + if(!isPublishing) return null + return ( + + Publishing Status + + + {resources.map((publish: ResourceToPublish) => { + const key = `${publish?.service}-${publish?.name}-${publish?.identifier}`; + const individualPublishStatus = publishStatus[key] || null + return ( + + ); + })} + + + + + ); +}; + +interface IndividualResourceComponentProps { + publish: ResourceToPublish + publishStatus: PublishStatus + publishKey: string +} +const ESTIMATED_PROCESSING_MS = 5 * 60 * 1000; // 5 minutes + +const IndividualResourceComponent = ({publish, publishKey, publishStatus}: IndividualResourceComponentProps)=> { + const [now, setNow] = useState(Date.now()); +console.log('key500',publishKey, publishStatus) + + const chunkPercent = useMemo(()=> { + if(!publishStatus?.chunks || !publishStatus?.totalChunks) return 0 + return (publishStatus?.chunks / publishStatus?.totalChunks) * 100 + }, [publishStatus]) + console.log('chunkPercent', chunkPercent) + const chunkDone = useMemo(()=> { + if(!publishStatus?.chunks || !publishStatus?.totalChunks) return false + return publishStatus?.chunks === publishStatus?.totalChunks + }, [publishStatus]) + + useEffect(() => { + if(!chunkDone) return + const interval = setInterval(() => { + setNow(Date.now()); + }, 1000); + return () => clearInterval(interval); + }, [chunkDone]); + + const [processingStart, setProcessingStart] = useState(); + +useEffect(() => { + if (chunkDone && !processingStart) { + setProcessingStart(Date.now()); + } +}, [chunkDone, processingStart]); + + const processingPercent = useMemo(() => { + if (!chunkDone || !processingStart || !publishStatus?.totalChunks || !now) return 0; + + const totalMB = publishStatus.totalChunks * 5; + const estimatedProcessingMs = (300_000 / 2048) * totalMB; + + const elapsed = now - processingStart; + if(!elapsed || elapsed < 0) return 0 + return Math.min((elapsed / estimatedProcessingMs) * 100, 100); +}, [chunkDone, processingStart, now, publishStatus?.totalChunks]); + + return ( + + + {publishKey} + + + + + File Chunk { (!publishStatus?.chunks || !publishStatus?.totalChunks) ? '' : publishStatus?.chunks}/{publishStatus?.totalChunks} ({(chunkPercent).toFixed(0)}%) + + + + + + + File Processing ({(!processingPercent || !processingStart) ? '0' : processingPercent.toFixed(0)}%) + + + + + ) +} + +export const MultiPublishDialog = React.memo(MultiPublishDialogComponent); diff --git a/src/components/VideoPlayer/SubtitleManager.tsx b/src/components/VideoPlayer/SubtitleManager.tsx index bc3cfc2..4bfaf7e 100644 --- a/src/components/VideoPlayer/SubtitleManager.tsx +++ b/src/components/VideoPlayer/SubtitleManager.tsx @@ -259,7 +259,7 @@ const SubtitleManagerComponent = ({ }; const theme = useTheme(); - if(!open) return + if(!open) return null return ( <> (null) - const location = useLocation(); + const location = useContext(LocationContext) + const locationRef = useRef(null) const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false) const { diff --git a/src/components/VideoPlayer/useVideoPlayerController.tsx b/src/components/VideoPlayer/useVideoPlayerController.tsx index 3571738..6ae896f 100644 --- a/src/components/VideoPlayer/useVideoPlayerController.tsx +++ b/src/components/VideoPlayer/useVideoPlayerController.tsx @@ -12,7 +12,6 @@ import { useProgressStore, useVideoStore } from "../../state/video"; import { QortalGetMetadata } from "../../types/interfaces/resources"; import { useResourceStatus } from "../../hooks/useResourceStatus"; import useIdleTimeout from "../../common/useIdleTimeout"; -import { useLocation, useNavigate } from "react-router-dom"; import { useGlobalPlayerStore } from "../../state/pip"; const controlsHeight = "42px"; @@ -42,7 +41,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => { const [startPlay, setStartPlay] = useState(false); const [startedFetch, setStartedFetch] = useState(false); const startedFetchRef = useRef(false); - const navigate = useNavigate() const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore(); const { getProgress } = useProgressStore(); diff --git a/src/context/GlobalProvider.tsx b/src/context/GlobalProvider.tsx index a799961..5273250 100644 --- a/src/context/GlobalProvider.tsx +++ b/src/context/GlobalProvider.tsx @@ -15,6 +15,8 @@ import { IndexManager } from "../components/IndexManager/IndexManager"; import { useIndexes } from "../hooks/useIndexes"; import { useProgressStore } from "../state/video"; import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer"; +import { Location, NavigateFunction } from "react-router-dom"; +import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog"; // ✅ Define Global Context Type interface GlobalContextType { @@ -24,6 +26,7 @@ interface GlobalContextType { identifierOperations: ReturnType; persistentOperations: ReturnType; indexOperations: ReturnType; + navigate: NavigateFunction } // ✅ Define Config Type for Hook Options @@ -35,17 +38,25 @@ interface GlobalProviderProps { appName: string; publicSalt: string; }; + navigate: NavigateFunction + location: Location toastStyle?: CSSProperties; } // ✅ Create Context with Proper Type -const GlobalContext = createContext(null); +export const GlobalContext = createContext(null); + +export const LocationContext = createContext(null); + + // 🔹 Global Provider (Handles Multiple Hooks) export const GlobalProvider = ({ children, config, toastStyle = {}, + navigate, + location }: GlobalProviderProps) => { // ✅ Call hooks and pass in options dynamically const auth = useAuth(config?.auth || {}); @@ -70,8 +81,9 @@ export const GlobalProvider = ({ identifierOperations, persistentOperations, indexOperations, + navigate }), - [auth, lists, appInfo, identifierOperations, persistentOperations] + [auth, lists, appInfo, identifierOperations, persistentOperations, navigate] ); const { clearOldProgress } = useProgressStore(); @@ -80,8 +92,11 @@ export const GlobalProvider = ({ }, []); return ( + + + + + ); }; diff --git a/src/hooks/useGlobalPipPlayer.tsx b/src/hooks/useGlobalPipPlayer.tsx index 20a3bb9..d15089e 100644 --- a/src/hooks/useGlobalPipPlayer.tsx +++ b/src/hooks/useGlobalPipPlayer.tsx @@ -1,7 +1,7 @@ // GlobalVideoPlayer.tsx import videojs from 'video.js'; import { useGlobalPlayerStore } from '../state/pip'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { Box, IconButton } from '@mui/material'; import { VideoContainer } from '../components/VideoPlayer/VideoPlayer-styles'; import { Rnd } from "react-rnd"; @@ -10,13 +10,14 @@ import CloseIcon from '@mui/icons-material/Close'; import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PauseIcon from '@mui/icons-material/Pause'; import OpenInFullIcon from '@mui/icons-material/OpenInFull'; -import { useNavigate } from 'react-router-dom'; +import { GlobalContext } from '../context/GlobalProvider'; export const GlobalPipPlayer = () => { const { videoSrc, reset, isPlaying, location, type, currentTime, mode, videoId } = useGlobalPlayerStore(); const [playing , setPlaying] = useState(false) const [hasStarted, setHasStarted] = useState(false) const playerRef = useRef(null); - const navigate = useNavigate() + const {navigate} = useContext(GlobalContext) + const videoNode = useRef(null); const { setProgress } = useProgressStore(); @@ -279,7 +280,11 @@ const margin = 50; zIndex: 2, opacity: 1, - }} onClick={()=> navigate(location)}> + }} onClick={()=> { + if(navigate){ + navigate(location) + } + }}> )} {playing && ( ; updatePublish: (publish: QortalGetMetadata, data: any) => Promise; deletePublish: (publish: QortalGetMetadata) => Promise; + publishMultipleResources: (resources: ResourceToPublish[])=> void }; type UsePublishWithoutMetadata = { @@ -39,6 +42,8 @@ interface StoredPublish { }>; updatePublish: (publish: QortalGetMetadata, data: any) => Promise; deletePublish: (publish: QortalGetMetadata) => Promise; + publishMultipleResources: (resources: ResourceToPublish[])=> void + }; export function usePublish( @@ -73,6 +78,8 @@ interface StoredPublish { const setResourceCache = useCacheStore((s) => s.setResourceCache); const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted); + const setPublishResources = useMultiplePublishStore((state) => state.setPublishResources); + const resetPublishResources = useMultiplePublishStore((state) => state.reset); const [hasResource, setHasResource] = useState(null); const fetchRawData = useCallback(async (item: QortalGetMetadata) => { const url = `/arbitrary/${item?.service}/${encodeURIComponent(item?.name)}/${encodeURIComponent(item?.identifier)}?encoding=base64`; @@ -262,6 +269,20 @@ interface StoredPublish { ); + }, [getStorageKey, setPublish]); + + const publishMultipleResources = useCallback(async (resources: ResourceToPublish[]) => { + try { + setPublishResources(resources) + const lengthOfResources = resources?.length; + const lengthOfTimeout = lengthOfResources * 1200000; // Time out in QR, Seconds = 20 Minutes + return await qortalRequestWithTimeout({ + action: "PUBLISH_MULTIPLE_QDN_RESOURCES", + resources + }, lengthOfTimeout); + } catch (error) { + + } }, [getStorageKey, setPublish]); if (!metadata) @@ -269,6 +290,7 @@ interface StoredPublish { fetchPublish, updatePublish, deletePublish: deleteResource, + publishMultipleResources }; return useMemo(() => ({ @@ -280,6 +302,7 @@ interface StoredPublish { fetchPublish, updatePublish, deletePublish: deleteResource, + publishMultipleResources }), [ isLoading, error, @@ -289,6 +312,7 @@ interface StoredPublish { fetchPublish, updatePublish, deleteResource, + publishMultipleResources ]); }; diff --git a/src/hooks/useResourceStatus.tsx b/src/hooks/useResourceStatus.tsx index a8c861f..8059edd 100644 --- a/src/hooks/useResourceStatus.tsx +++ b/src/hooks/useResourceStatus.tsx @@ -8,7 +8,7 @@ interface PropsUseResourceStatus { } export const useResourceStatus = ({ resource, - retryAttempts = 50, + retryAttempts = 200, }: PropsUseResourceStatus) => { const resourceId = !resource ? null : `${resource.service}-${resource.name}-${resource.identifier}`; const status = usePublishStore((state)=> state.getResourceStatus(resourceId)) || null diff --git a/src/state/multiplePublish.ts b/src/state/multiplePublish.ts new file mode 100644 index 0000000..da8c048 --- /dev/null +++ b/src/state/multiplePublish.ts @@ -0,0 +1,79 @@ +import { create } from 'zustand'; +import { ResourceToPublish } from '../types/qortalRequests/types'; +import { Service } from '../types/interfaces/resources'; + + +interface MultiplePublishState { + resources: ResourceToPublish[]; + setPublishResources: (resources: ResourceToPublish[])=> void + reset: ()=> void + isPublishing: boolean +} + const initialState = { + resources: [], + isPublishing: false + }; +export const useMultiplePublishStore = create((set) => ({ + ...initialState, + setPublishResources: (resources: ResourceToPublish[]) => set(() => ({ resources, isPublishing: true })), + reset: () => set(initialState), + +})); + +export type PublishLocation = { + name: string; + identifier: string; + service: Service; +}; + +export type PublishStatus = { + publishLocation: PublishLocation; + chunks: number; + totalChunks: number; + processed: boolean; +}; + +type PublishStatusStore = { + publishStatus: Record; + getPublishStatusByKey: (key: string) => PublishStatus | undefined; + setPublishStatusByKey: (key: string, update: Partial) => void; +}; + + +export const usePublishStatusStore = create((set, get) => ({ + publishStatus: {}, + + getPublishStatusByKey: (key) => get().publishStatus[key], + + setPublishStatusByKey: (key, update) => { + const current = get().publishStatus; + + const prev: PublishStatus = current[key] ?? { + publishLocation: { + name: '', + identifier: '', + service: 'DOCUMENT', + processed: false, + }, + chunks: 0, + totalChunks: 0, + processed: false, + }; + + const newStatus: PublishStatus = { + ...prev, + ...update, + publishLocation: { + ...prev.publishLocation, + ...(update.publishLocation ?? {}), + }, + }; + + set({ + publishStatus: { + ...current, + [key]: newStatus, + }, + }); + }, +})); \ No newline at end of file