import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react"; import { QortalGetMetadata, QortalMetadata, Service, } from "../../types/interfaces/resources"; import { alpha, Box, Button, ButtonBase, Card, CircularProgress, Dialog, DialogActions, DialogContent, DialogTitle, Divider, Fade, IconButton, Popover, Skeleton, Tab, Tabs, Typography, useTheme, } from "@mui/material"; import CheckIcon from "@mui/icons-material/Check"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import ArrowBackIosIcon from "@mui/icons-material/ArrowBackIos"; import ModeEditIcon from "@mui/icons-material/ModeEdit"; import CloseIcon from "@mui/icons-material/Close"; import { useListStore } from "../../state/lists"; import { Resource, useResources } from "../../hooks/useResources"; import { useGlobal } from "../../context/GlobalProvider"; import { ENTITY_SUBTITLE, SERVICE_SUBTITLE } from "./video-player-constants"; import ISO6391, { LanguageCode } from "iso-639-1"; import LanguageSelect from "./LanguageSelect"; import { useDropzone, DropzoneRootProps, DropzoneInputProps, } from "react-dropzone"; import { fileToBase64, objectToBase64 } from "../../utils/base64"; import { ResourceToPublish } from "../../types/qortalRequests/types"; import { useListReturn } from "../../hooks/useListData"; import { usePublish } from "../../hooks/usePublish"; import { Spacer } from "../../common/Spacer"; import { dismissToast, showError, showLoading, showSuccess, } from "../../utils/toast"; interface SubtitleManagerProps { qortalMetadata: QortalGetMetadata; close: () => void; open: boolean; onSelect: (subtitle: SubtitlePublishedData) => void; subtitleBtnRef: any; currentSubTrack: null | string; } export interface Subtitle { language: string | null; base64: string; type: string; filename: string; size: number; } export interface SubtitlePublishedData { language: string | null; subtitleData: string; type: string; filename: string; size: number; } export const languageOptions = ISO6391.getAllCodes().map((code) => ({ code, name: ISO6391.getName(code), nativeName: ISO6391.getNativeName(code), })); function a11yProps(index: number) { return { id: `subtitle-tab-${index}`, "aria-controls": `subtitle-tabpanel-${index}`, }; } const SubtitleManagerComponent = ({ qortalMetadata, open, close, onSelect, subtitleBtnRef, currentSubTrack, }: SubtitleManagerProps) => { const [mode, setMode] = useState(1); const [isOpenPublish, setIsOpenPublish] = useState(false); const { lists, identifierOperations, auth } = useGlobal(); const [isLoading, setIsLoading] = useState(false); const [showAll, setShowAll] = useState(false); const { fetchResources } = useResources(); // const [subtitles, setSubtitles] = useState([]) const subtitles = useListReturn( `subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}` ); console.log("subtitles222", subtitles); const mySubtitles = useMemo(() => { if (!auth?.name) return []; return subtitles?.filter((sub) => sub.name === auth?.name); }, [subtitles, auth?.name]); console.log("subtitles222", subtitles); const getPublishedSubtitles = useCallback(async () => { try { setIsLoading(true); const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; console.log("videoId", videoId); const postIdSearch = await identifierOperations.buildSearchPrefix( ENTITY_SUBTITLE, videoId ); let name: string | undefined = qortalMetadata?.name; if (showAll) { name = undefined; } const searchParams = { service: SERVICE_SUBTITLE, identifier: postIdSearch, name, limit: 0, }; const res = await lists.fetchResources( searchParams, `subs-${videoId}`, "BASE64" ); lists.addList(`subs-${videoId}`, res || []); console.log("resres2", res); } catch (error) { console.error(error); } finally { setIsLoading(false); } }, [showAll]); useEffect(() => { if ( !qortalMetadata?.identifier || !qortalMetadata?.name || !qortalMetadata?.service ) return; getPublishedSubtitles(); }, [ qortalMetadata?.identifier, qortalMetadata?.service, qortalMetadata?.name, getPublishedSubtitles, ]); const handleClose = () => { close(); setMode(1); // setTitle(""); // setDescription(""); // setHasMetadata(false); }; const publishHandler = async (subtitles: Subtitle[]) => { try { const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; const identifier = await identifierOperations.buildIdentifier( ENTITY_SUBTITLE, videoId ); const name = auth?.name; console.log("identifier2", identifier); if (!name) return; const resources: ResourceToPublish[] = []; const tempResources: { qortalMetadata: QortalMetadata; data: any }[] = []; for (const sub of subtitles) { const data = { subtitleData: sub.base64, language: sub.language, filename: sub.filename, type: sub.type, }; const base64Data = await objectToBase64(data); const resource = { name, identifier, service: SERVICE_SUBTITLE, base64: base64Data, filename: sub.filename, title: sub.language || undefined, }; resources.push(resource); tempResources.push({ qortalMetadata: { identifier, service: SERVICE_SUBTITLE, name, size: 100, created: Date.now(), }, data: data, }); } console.log("resources", resources); await qortalRequest({ action: "PUBLISH_MULTIPLE_QDN_RESOURCES", resources, }); lists.addNewResources( `subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`, tempResources ); } catch (error) {} }; const onBack = () => { if (mode === 1) close(); }; const onSelectHandler = (sub: SubtitlePublishedData) => { console.log("onSelectHandler"); onSelect(sub); close(); }; const theme = useTheme(); return ( <> Subtitles setIsOpenPublish(true)} > {isLoading && } {!isLoading && subtitles?.length === 0 && ( No subtitles )} {mode === 1 && !isLoading && subtitles?.length > 0 && ( )} {/* {[ 'Ambient mode', 'Annotations', 'Subtitles/CC', 'Sleep timer', 'Playback speed', 'Quality', ].map((label) => ( {label} ))} */} // // Subtitles // ({ // position: "absolute", // right: 8, // top: 8, // })} // > // // // // {mode === 1 && ( // // )} // {mode === 5 && } // {/* {mode === 2 && ( // // )} // {mode === 4 && ( // // )} */} // ); }; interface PublisherSubtitlesProps { publisherName: string; subtitles: any[]; setMode: (val: number) => void; onSelect: (subtitle: any) => void; onBack: () => void; currentSubTrack: string | null; } const PublisherSubtitles = ({ publisherName, subtitles, setMode, onSelect, onBack, currentSubTrack, }: PublisherSubtitlesProps) => { return ( <> onSelect(null)} sx={{ px: 2, py: 1, "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)", }, width: "100%", justifyContent: "space-between", }} > Off {!currentSubTrack ? : } {subtitles?.map((sub) => { return ( ); })} ); }; interface PublishSubtitlesProps { publishHandler: (subs: Subtitle[]) => Promise; isOpen: boolean; setIsOpen: (val: boolean) => void; mySubtitles: QortalGetMetadata[]; } const PublishSubtitles = ({ publishHandler, isOpen, setIsOpen, mySubtitles, }: PublishSubtitlesProps) => { const [language, setLanguage] = useState(null); const [subtitles, setSubtitles] = useState([]); const [isPublishing, setIsPublishing] = useState(false); const { lists } = useGlobal(); const theme = useTheme(); const onDrop = useCallback(async (acceptedFiles: File[]) => { const newSubtitles: Subtitle[] = []; for (const file of acceptedFiles) { try { const newSubtitle = { base64: await fileToBase64(file), language: null, type: file.type, filename: file.name, size: file.size, }; newSubtitles.push(newSubtitle); } catch (error) { console.error("Failed to parse audio file:", error); } } setSubtitles((prev) => [...newSubtitles, ...prev]); }, []); const { getRootProps, getInputProps, }: { getRootProps: () => DropzoneRootProps; getInputProps: () => DropzoneInputProps; isDragActive: boolean; } = useDropzone({ onDrop, accept: { "application/x-subrip": [".srt"], // SRT subtitles "text/vtt": [".vtt"], // WebVTT subtitles }, multiple: true, maxSize: 2 * 1024 * 1024, // 2MB }); const onChangeValue = (field: string, data: any, index: number) => { const sub = subtitles[index]; if (!sub) return; const copySub = { ...sub, [field]: data }; setSubtitles((prev) => { const copyPrev = [...prev]; copyPrev[index] = copySub; return copyPrev; }); }; console.log("subtitles", subtitles); const handleClose = () => { setIsOpen(false); setSubtitles([]); }; const [value, setValue] = useState(0); const handleChange = (event: React.SyntheticEvent, newValue: number) => { setValue(newValue); }; const onDelete = useCallback(async (sub: QortalGetMetadata) => { let loadId; try { setIsPublishing(true); loadId = showLoading("Deleting subtitle..."); await lists.deleteResource([sub]); showSuccess("Deleted subtitle"); } catch (error) { showError(error instanceof Error ? error.message : "Unable to delete"); } finally { setIsPublishing(false); dismissToast(loadId); } }, []); const publishHandlerLocal = async (subtitles: Subtitle[]) => { let loadId; try { setIsPublishing(true); loadId = showLoading("Publishing subtitles..."); await publishHandler(subtitles); showSuccess("Subtitles published"); setSubtitles([]); } catch (error) { showError(error instanceof Error ? error.message : "Unable to publish"); } finally { dismissToast(loadId); setIsPublishing(false); } }; const disableButton = !!subtitles.find((sub) => !sub?.language) || isPublishing; return ( My Subtitles ({ position: "absolute", right: 8, top: 8, })} > {value === 0 && ( {subtitles?.map((sub, i) => { return ( {sub.filename} onChangeValue("language", val, i) } /> ); })} )} {value === 1 && ( {mySubtitles?.map((sub, i) => { return ( ); })} )} {value === 0 && ( )} ); }; interface SubProps { sub: QortalGetMetadata; onSelect: (subtitle: Subtitle) => void; currentSubtrack: null | string; } const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { const { resource, isLoading, error } = usePublish(2, "JSON", sub); console.log("resource", resource); const isSelected = currentSubtrack === resource?.data?.language; return ( onSelect(isSelected ? null : resource?.data)} sx={{ px: 2, py: 1, "&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)", }, width: "100%", justifyContent: "space-between", }} > {isLoading && } {!isLoading && !error && ( <> {resource?.data?.language} {isSelected ? : } )} ); }; interface MySubtitleProps { sub: QortalGetMetadata; onDelete: (subtitle: QortalGetMetadata) => void; } const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => { const { resource, isLoading, error } = usePublish(2, "JSON", sub); console.log("resource", resource); return ( {resource?.data?.filename} {resource?.data?.language} ); }; export const SubtitleManager = React.memo(SubtitleManagerComponent);