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 (
+
+
+ );
+};
+
+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