started on mutli-publish status

This commit is contained in:
PhilReact 2025-06-19 17:01:39 +03:00
parent e32f98e1b0
commit 979c6d4265
9 changed files with 306 additions and 12 deletions

View File

@ -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 (
<Dialog open={isPublishing} fullWidth maxWidth="sm">
<DialogTitle>Publishing Status</DialogTitle>
<DialogContent>
<Stack spacing={3}>
{resources.map((publish: ResourceToPublish) => {
const key = `${publish?.service}-${publish?.name}-${publish?.identifier}`;
const individualPublishStatus = publishStatus[key] || null
return (
<IndividualResourceComponent key={key} publishKey={key} publish={publish} publishStatus={individualPublishStatus} />
);
})}
</Stack>
</DialogContent>
</Dialog>
);
};
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<number | undefined>();
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 (
<Box p={1} border={1} borderColor="divider" borderRadius={2}>
<Typography variant="subtitle1" fontWeight="bold">
{publishKey}
</Typography>
<Box mt={2}>
<Typography variant="body2" gutterBottom>
File Chunk { (!publishStatus?.chunks || !publishStatus?.totalChunks) ? '' : publishStatus?.chunks}/{publishStatus?.totalChunks} ({(chunkPercent).toFixed(0)}%)
</Typography>
<LinearProgress variant="determinate" value={chunkPercent} />
</Box>
<Box mt={2}>
<Typography variant="body2" gutterBottom>
File Processing ({(!processingPercent || !processingStart) ? '0' : processingPercent.toFixed(0)}%)
</Typography>
<LinearProgress variant="determinate" value={processingPercent} />
</Box>
</Box>
)
}
export const MultiPublishDialog = React.memo(MultiPublishDialogComponent);

View File

@ -259,7 +259,7 @@ const SubtitleManagerComponent = ({
}; };
const theme = useTheme(); const theme = useTheme();
if(!open) return if(!open) return null
return ( return (
<> <>
<Box <Box

View File

@ -3,6 +3,7 @@ import {
Ref, Ref,
RefObject, RefObject,
useCallback, useCallback,
useContext,
useEffect, useEffect,
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
@ -31,7 +32,7 @@ import convert from "srt-webvtt";
import { TimelineActionsComponent } from "./TimelineActionsComponent"; import { TimelineActionsComponent } from "./TimelineActionsComponent";
import { PlayBackMenu } from "./VideoControls"; import { PlayBackMenu } from "./VideoControls";
import { useGlobalPlayerStore } from "../../state/pip"; import { useGlobalPlayerStore } from "../../state/pip";
import { useLocation } from "react-router-dom"; import { LocationContext } from "../../context/GlobalProvider";
export async function srtBase64ToVttBlobUrl( export async function srtBase64ToVttBlobUrl(
base64Srt: string base64Srt: string
@ -212,7 +213,8 @@ export const VideoPlayer = ({
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false); const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false);
const subtitleBtnRef = useRef(null); const subtitleBtnRef = useRef(null);
const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null) const [currentSubTrack, setCurrentSubTrack] = useState<null | string>(null)
const location = useLocation(); const location = useContext(LocationContext)
const locationRef = useRef<string | null>(null) const locationRef = useRef<string | null>(null)
const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false) const [isOpenPlaybackMenu, setIsOpenPlaybackmenu] = useState(false)
const { const {

View File

@ -12,7 +12,6 @@ import { useProgressStore, useVideoStore } from "../../state/video";
import { QortalGetMetadata } from "../../types/interfaces/resources"; import { QortalGetMetadata } from "../../types/interfaces/resources";
import { useResourceStatus } from "../../hooks/useResourceStatus"; import { useResourceStatus } from "../../hooks/useResourceStatus";
import useIdleTimeout from "../../common/useIdleTimeout"; import useIdleTimeout from "../../common/useIdleTimeout";
import { useLocation, useNavigate } from "react-router-dom";
import { useGlobalPlayerStore } from "../../state/pip"; import { useGlobalPlayerStore } from "../../state/pip";
const controlsHeight = "42px"; const controlsHeight = "42px";
@ -42,7 +41,6 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
const [startPlay, setStartPlay] = useState(false); const [startPlay, setStartPlay] = useState(false);
const [startedFetch, setStartedFetch] = useState(false); const [startedFetch, setStartedFetch] = useState(false);
const startedFetchRef = useRef(false); const startedFetchRef = useRef(false);
const navigate = useNavigate()
const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore(); const { playbackSettings, setPlaybackRate, setVolume } = useVideoStore();
const { getProgress } = useProgressStore(); const { getProgress } = useProgressStore();

View File

@ -15,6 +15,8 @@ import { IndexManager } from "../components/IndexManager/IndexManager";
import { useIndexes } from "../hooks/useIndexes"; import { useIndexes } from "../hooks/useIndexes";
import { useProgressStore } from "../state/video"; import { useProgressStore } from "../state/video";
import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer"; import { GlobalPipPlayer } from "../hooks/useGlobalPipPlayer";
import { Location, NavigateFunction } from "react-router-dom";
import { MultiPublishDialog } from "../components/MultiPublish/MultiPublishDialog";
// ✅ Define Global Context Type // ✅ Define Global Context Type
interface GlobalContextType { interface GlobalContextType {
@ -24,6 +26,7 @@ interface GlobalContextType {
identifierOperations: ReturnType<typeof useIdentifiers>; identifierOperations: ReturnType<typeof useIdentifiers>;
persistentOperations: ReturnType<typeof usePersistentStore>; persistentOperations: ReturnType<typeof usePersistentStore>;
indexOperations: ReturnType<typeof useIndexes>; indexOperations: ReturnType<typeof useIndexes>;
navigate: NavigateFunction
} }
// ✅ Define Config Type for Hook Options // ✅ Define Config Type for Hook Options
@ -35,17 +38,25 @@ interface GlobalProviderProps {
appName: string; appName: string;
publicSalt: string; publicSalt: string;
}; };
navigate: NavigateFunction
location: Location
toastStyle?: CSSProperties; toastStyle?: CSSProperties;
} }
// ✅ Create Context with Proper Type // ✅ Create Context with Proper Type
const GlobalContext = createContext<GlobalContextType | null>(null); export const GlobalContext = createContext<GlobalContextType | null>(null);
export const LocationContext = createContext<Location | null>(null);
// 🔹 Global Provider (Handles Multiple Hooks) // 🔹 Global Provider (Handles Multiple Hooks)
export const GlobalProvider = ({ export const GlobalProvider = ({
children, children,
config, config,
toastStyle = {}, toastStyle = {},
navigate,
location
}: GlobalProviderProps) => { }: GlobalProviderProps) => {
// ✅ Call hooks and pass in options dynamically // ✅ Call hooks and pass in options dynamically
const auth = useAuth(config?.auth || {}); const auth = useAuth(config?.auth || {});
@ -70,8 +81,9 @@ export const GlobalProvider = ({
identifierOperations, identifierOperations,
persistentOperations, persistentOperations,
indexOperations, indexOperations,
navigate
}), }),
[auth, lists, appInfo, identifierOperations, persistentOperations] [auth, lists, appInfo, identifierOperations, persistentOperations, navigate]
); );
const { clearOldProgress } = useProgressStore(); const { clearOldProgress } = useProgressStore();
@ -80,8 +92,11 @@ export const GlobalProvider = ({
}, []); }, []);
return ( return (
<LocationContext.Provider value={location}>
<GlobalContext.Provider value={contextValue}> <GlobalContext.Provider value={contextValue}>
<GlobalPipPlayer /> <GlobalPipPlayer />
<MultiPublishDialog />
<Toaster <Toaster
position="top-center" position="top-center"
toastOptions={{ toastOptions={{
@ -94,6 +109,8 @@ export const GlobalProvider = ({
{children} {children}
</GlobalContext.Provider> </GlobalContext.Provider>
</LocationContext.Provider>
); );
}; };

View File

@ -1,7 +1,7 @@
// GlobalVideoPlayer.tsx // GlobalVideoPlayer.tsx
import videojs from 'video.js'; import videojs from 'video.js';
import { useGlobalPlayerStore } from '../state/pip'; 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 { Box, IconButton } from '@mui/material';
import { VideoContainer } from '../components/VideoPlayer/VideoPlayer-styles'; import { VideoContainer } from '../components/VideoPlayer/VideoPlayer-styles';
import { Rnd } from "react-rnd"; import { Rnd } from "react-rnd";
@ -10,13 +10,14 @@ import CloseIcon from '@mui/icons-material/Close';
import PlayArrowIcon from '@mui/icons-material/PlayArrow'; import PlayArrowIcon from '@mui/icons-material/PlayArrow';
import PauseIcon from '@mui/icons-material/Pause'; import PauseIcon from '@mui/icons-material/Pause';
import OpenInFullIcon from '@mui/icons-material/OpenInFull'; import OpenInFullIcon from '@mui/icons-material/OpenInFull';
import { useNavigate } from 'react-router-dom'; import { GlobalContext } from '../context/GlobalProvider';
export const GlobalPipPlayer = () => { export const GlobalPipPlayer = () => {
const { videoSrc, reset, isPlaying, location, type, currentTime, mode, videoId } = useGlobalPlayerStore(); const { videoSrc, reset, isPlaying, location, type, currentTime, mode, videoId } = useGlobalPlayerStore();
const [playing , setPlaying] = useState(false) const [playing , setPlaying] = useState(false)
const [hasStarted, setHasStarted] = useState(false) const [hasStarted, setHasStarted] = useState(false)
const playerRef = useRef<any>(null); const playerRef = useRef<any>(null);
const navigate = useNavigate() const {navigate} = useContext(GlobalContext)
const videoNode = useRef<HTMLVideoElement>(null); const videoNode = useRef<HTMLVideoElement>(null);
const { setProgress } = useProgressStore(); const { setProgress } = useProgressStore();
@ -279,7 +280,11 @@ const margin = 50;
zIndex: 2, zIndex: 2,
opacity: 1, opacity: 1,
}} onClick={()=> navigate(location)}><OpenInFullIcon /></IconButton> }} onClick={()=> {
if(navigate){
navigate(location)
}
}}><OpenInFullIcon /></IconButton>
)} )}
{playing && ( {playing && (
<IconButton sx={{ <IconButton sx={{

View File

@ -5,6 +5,8 @@ import { base64ToObject, retryTransaction } from "../utils/publish";
import { useGlobal } from "../context/GlobalProvider"; import { useGlobal } from "../context/GlobalProvider";
import { ReturnType } from "../components/ResourceList/ResourceListDisplay"; import { ReturnType } from "../components/ResourceList/ResourceListDisplay";
import { useCacheStore } from "../state/cache"; import { useCacheStore } from "../state/cache";
import { useMultiplePublishStore } from "../state/multiplePublish";
import { ResourceToPublish } from "../types/qortalRequests/types";
interface StoredPublish { interface StoredPublish {
qortalMetadata: QortalMetadata; qortalMetadata: QortalMetadata;
@ -29,6 +31,7 @@ interface StoredPublish {
}>; }>;
updatePublish: (publish: QortalGetMetadata, data: any) => Promise<void>; updatePublish: (publish: QortalGetMetadata, data: any) => Promise<void>;
deletePublish: (publish: QortalGetMetadata) => Promise<boolean | undefined>; deletePublish: (publish: QortalGetMetadata) => Promise<boolean | undefined>;
publishMultipleResources: (resources: ResourceToPublish[])=> void
}; };
type UsePublishWithoutMetadata = { type UsePublishWithoutMetadata = {
@ -39,6 +42,8 @@ interface StoredPublish {
}>; }>;
updatePublish: (publish: QortalGetMetadata, data: any) => Promise<void>; updatePublish: (publish: QortalGetMetadata, data: any) => Promise<void>;
deletePublish: (publish: QortalGetMetadata) => Promise<boolean | undefined>; deletePublish: (publish: QortalGetMetadata) => Promise<boolean | undefined>;
publishMultipleResources: (resources: ResourceToPublish[])=> void
}; };
export function usePublish( export function usePublish(
@ -73,6 +78,8 @@ interface StoredPublish {
const setResourceCache = useCacheStore((s) => s.setResourceCache); const setResourceCache = useCacheStore((s) => s.setResourceCache);
const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted); const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted);
const setPublishResources = useMultiplePublishStore((state) => state.setPublishResources);
const resetPublishResources = useMultiplePublishStore((state) => state.reset);
const [hasResource, setHasResource] = useState<boolean | null>(null); const [hasResource, setHasResource] = useState<boolean | null>(null);
const fetchRawData = useCallback(async (item: QortalGetMetadata) => { const fetchRawData = useCallback(async (item: QortalGetMetadata) => {
const url = `/arbitrary/${item?.service}/${encodeURIComponent(item?.name)}/${encodeURIComponent(item?.identifier)}?encoding=base64`; 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]); }, [getStorageKey, setPublish]);
if (!metadata) if (!metadata)
@ -269,6 +290,7 @@ interface StoredPublish {
fetchPublish, fetchPublish,
updatePublish, updatePublish,
deletePublish: deleteResource, deletePublish: deleteResource,
publishMultipleResources
}; };
return useMemo(() => ({ return useMemo(() => ({
@ -280,6 +302,7 @@ interface StoredPublish {
fetchPublish, fetchPublish,
updatePublish, updatePublish,
deletePublish: deleteResource, deletePublish: deleteResource,
publishMultipleResources
}), [ }), [
isLoading, isLoading,
error, error,
@ -289,6 +312,7 @@ interface StoredPublish {
fetchPublish, fetchPublish,
updatePublish, updatePublish,
deleteResource, deleteResource,
publishMultipleResources
]); ]);
}; };

View File

@ -8,7 +8,7 @@ interface PropsUseResourceStatus {
} }
export const useResourceStatus = ({ export const useResourceStatus = ({
resource, resource,
retryAttempts = 50, retryAttempts = 200,
}: PropsUseResourceStatus) => { }: PropsUseResourceStatus) => {
const resourceId = !resource ? null : `${resource.service}-${resource.name}-${resource.identifier}`; const resourceId = !resource ? null : `${resource.service}-${resource.name}-${resource.identifier}`;
const status = usePublishStore((state)=> state.getResourceStatus(resourceId)) || null const status = usePublishStore((state)=> state.getResourceStatus(resourceId)) || null

View File

@ -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<MultiplePublishState>((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<string, PublishStatus>;
getPublishStatusByKey: (key: string) => PublishStatus | undefined;
setPublishStatusByKey: (key: string, update: Partial<PublishStatus>) => void;
};
export const usePublishStatusStore = create<PublishStatusStore>((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,
},
});
},
}));