mirror of
https://github.com/Qortal/qapp-core.git
synced 2025-06-16 10:21:20 +00:00
applied subtitles vtt and srt
This commit is contained in:
parent
b48b47ae1c
commit
d3c4a4713e
10
package-lock.json
generated
10
package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
|
"iso-639-1": "^3.1.5",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-idle-timer": "^5.7.2",
|
"react-idle-timer": "^5.7.2",
|
||||||
@ -2566,6 +2567,15 @@
|
|||||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/iso-639-1": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/iso-639-1/-/iso-639-1-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-gXkz5+KN7HrG0Q5UGqSMO2qB9AsbEeyLP54kF1YrMsIxmu+g4BdB7rflReZTSTZGpfj8wywu6pfPBCylPIzGQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jackspeak": {
|
"node_modules/jackspeak": {
|
||||||
"version": "3.4.3",
|
"version": "3.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
|
||||||
|
@ -32,6 +32,7 @@
|
|||||||
"dexie": "^4.0.11",
|
"dexie": "^4.0.11",
|
||||||
"dompurify": "^3.2.4",
|
"dompurify": "^3.2.4",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
|
"iso-639-1": "^3.1.5",
|
||||||
"react-dropzone": "^14.3.8",
|
"react-dropzone": "^14.3.8",
|
||||||
"react-hot-toast": "^2.5.2",
|
"react-hot-toast": "^2.5.2",
|
||||||
"react-idle-timer": "^5.7.2",
|
"react-idle-timer": "^5.7.2",
|
||||||
|
33
src/components/VideoPlayer/LanguageSelect.tsx
Normal file
33
src/components/VideoPlayer/LanguageSelect.tsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
// components/LanguageSelector.tsx
|
||||||
|
import React from 'react';
|
||||||
|
import { Autocomplete, TextField } from '@mui/material';
|
||||||
|
import { languageOptions } from './SubtitleManager';
|
||||||
|
|
||||||
|
|
||||||
|
export default function LanguageSelector({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
}: {
|
||||||
|
value: string | null;
|
||||||
|
onChange: (value: string | null) => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Autocomplete
|
||||||
|
options={languageOptions}
|
||||||
|
getOptionLabel={(option) => `${option.name} (${option.code})`}
|
||||||
|
value={languageOptions.find((opt) => opt.code === value) || null}
|
||||||
|
onChange={(event, newValue) => onChange(newValue?.code || null)}
|
||||||
|
renderInput={(params) => <TextField {...params} label="Subtitle Language" />}
|
||||||
|
isOptionEqualToValue={(option, val) => option.code === val.code}
|
||||||
|
sx={{ width: 300 }}
|
||||||
|
slotProps={{
|
||||||
|
popper: {
|
||||||
|
sx: {
|
||||||
|
zIndex: 999991, // Must be higher than Dialog's default zIndex (1300)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -1,117 +1,350 @@
|
|||||||
import { useCallback, useEffect, useState } from "react"
|
import React, { useCallback, useEffect, useState } from "react";
|
||||||
import { QortalGetMetadata } from "../../types/interfaces/resources"
|
import { QortalGetMetadata, QortalMetadata, Service } from "../../types/interfaces/resources";
|
||||||
import { Box, ButtonBase, Dialog, DialogContent, DialogTitle, IconButton, Typography } from "@mui/material"
|
import {
|
||||||
|
alpha,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonBase,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogTitle,
|
||||||
|
Divider,
|
||||||
|
Fade,
|
||||||
|
IconButton,
|
||||||
|
Popover,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||||
|
import ModeEditIcon from '@mui/icons-material/ModeEdit';
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { useListStore } from "../../state/lists";
|
import { useListStore } from "../../state/lists";
|
||||||
import { useResources } from "../../hooks/useResources";
|
import { Resource, useResources } from "../../hooks/useResources";
|
||||||
import { useGlobal } from "../../context/GlobalProvider";
|
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";
|
||||||
interface SubtitleManagerProps {
|
interface SubtitleManagerProps {
|
||||||
qortalMetadata: QortalGetMetadata
|
qortalMetadata: QortalGetMetadata;
|
||||||
close: ()=> void
|
close: () => void;
|
||||||
open: boolean
|
open: boolean;
|
||||||
|
onSelect: (subtitle: SubtitlePublishedData)=> void;
|
||||||
|
subtitleBtnRef: any
|
||||||
}
|
}
|
||||||
export const SubtitleManager = ({qortalMetadata, open, close}: SubtitleManagerProps) => {
|
export interface Subtitle {
|
||||||
const [mode, setMode] = useState(1)
|
language: string | null;
|
||||||
const {lists} = useGlobal()
|
base64: string;
|
||||||
const {fetchResources} = useResources()
|
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),
|
||||||
|
}));
|
||||||
|
const SubtitleManagerComponent = ({
|
||||||
|
qortalMetadata,
|
||||||
|
open,
|
||||||
|
close,
|
||||||
|
onSelect,
|
||||||
|
subtitleBtnRef
|
||||||
|
}: SubtitleManagerProps) => {
|
||||||
|
const [mode, setMode] = useState(1);
|
||||||
|
const { lists, identifierOperations, auth } = useGlobal();
|
||||||
|
const { fetchResources } = useResources();
|
||||||
// const [subtitles, setSubtitles] = useState([])
|
// const [subtitles, setSubtitles] = useState([])
|
||||||
const subtitles = useListStore(
|
const subtitles = useListReturn(`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`)
|
||||||
(state) => state.lists[`${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`]?.items || []
|
|
||||||
);
|
|
||||||
const getPublishedSubtitles = useCallback(async ()=> {
|
console.log('subtitles222', subtitles)
|
||||||
|
const getPublishedSubtitles = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
await fetchResources(qortalMetadata, `${qortalMetadata?.service}- ${qortalMetadata?.name}-${qortalMetadata?.identifier}`, "BASE64");
|
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
|
||||||
|
console.log('videoId', videoId)
|
||||||
|
const postIdSearch = await identifierOperations.buildSearchPrefix(
|
||||||
|
ENTITY_SUBTITLE,
|
||||||
|
videoId,
|
||||||
|
);
|
||||||
|
const searchParams = {
|
||||||
|
service: SERVICE_SUBTITLE,
|
||||||
|
identifier: postIdSearch,
|
||||||
|
limit: 0
|
||||||
|
};
|
||||||
|
const res = await lists.fetchResources(searchParams, `subs-${videoId}`, "BASE64");
|
||||||
|
lists.addList(`subs-${videoId}`, res || []);
|
||||||
|
console.log('resres2', res)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error);
|
||||||
}
|
}
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
useEffect(()=> {
|
useEffect(() => {
|
||||||
if(!qortalMetadata?.identifier || !qortalMetadata?.name || !qortalMetadata?.service) return
|
if (
|
||||||
|
!qortalMetadata?.identifier ||
|
||||||
|
!qortalMetadata?.name ||
|
||||||
|
!qortalMetadata?.service
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
// getPublishedSubtitles()
|
getPublishedSubtitles()
|
||||||
}, [qortalMetadata?.identifier, qortalMetadata?.service, qortalMetadata?.name, getPublishedSubtitles])
|
}, [
|
||||||
|
qortalMetadata?.identifier,
|
||||||
|
qortalMetadata?.service,
|
||||||
|
qortalMetadata?.name,
|
||||||
|
getPublishedSubtitles,
|
||||||
|
]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
close()
|
close();
|
||||||
setMode(1);
|
setMode(1);
|
||||||
// setTitle("");
|
// setTitle("");
|
||||||
// setDescription("");
|
// setDescription("");
|
||||||
// setHasMetadata(false);
|
// setHasMetadata(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelect = ()=> {
|
|
||||||
|
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)=> {
|
||||||
|
onSelect(sub)
|
||||||
|
close()
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Popover
|
||||||
open={!!open}
|
open={!!open}
|
||||||
fullWidth={true}
|
anchorEl={subtitleBtnRef.current}
|
||||||
maxWidth={"md"}
|
onClose={handleClose}
|
||||||
sx={{
|
slots={{
|
||||||
zIndex: 999990,
|
transition: Fade,
|
||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
|
transition: {
|
||||||
|
timeout: 200,
|
||||||
|
},
|
||||||
paper: {
|
paper: {
|
||||||
elevation: 0,
|
sx: {
|
||||||
|
bgcolor: alpha('#181818', 0.98),
|
||||||
|
color: 'white',
|
||||||
|
opacity: 0.9,
|
||||||
|
borderRadius: 2,
|
||||||
|
boxShadow: 5,
|
||||||
|
p: 1,
|
||||||
|
minWidth: 200,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
anchorOrigin={{
|
||||||
|
vertical: 'top',
|
||||||
|
horizontal: 'center',
|
||||||
|
}}
|
||||||
|
transformOrigin={{
|
||||||
|
vertical: 'bottom',
|
||||||
|
horizontal: 'center',
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<DialogTitle>Subtitles</DialogTitle>
|
<Box sx={{
|
||||||
<IconButton
|
padding: '5px 0px 10px 0px',
|
||||||
aria-label="close"
|
display: 'flex',
|
||||||
onClick={handleClose}
|
gap:'10px',
|
||||||
sx={(theme) => ({
|
width: '100%'
|
||||||
position: "absolute",
|
}}>
|
||||||
right: 8,
|
<ButtonBase onClick={onBack}>
|
||||||
top: 8,
|
<ArrowBackIosIcon sx={{
|
||||||
})}
|
fontSize: '1.15em'
|
||||||
>
|
}}/>
|
||||||
<CloseIcon />
|
</ButtonBase>
|
||||||
</IconButton>
|
<ButtonBase>
|
||||||
|
<Typography onClick={onBack} sx={{
|
||||||
|
fontSize: '0.85rem'
|
||||||
|
}}>Subtitles</Typography>
|
||||||
|
|
||||||
|
</ButtonBase>
|
||||||
|
<ButtonBase sx={{
|
||||||
|
marginLeft: 'auto',
|
||||||
|
|
||||||
|
}}>
|
||||||
|
<ModeEditIcon sx={{
|
||||||
|
fontSize: '1.15rem'
|
||||||
|
}} />
|
||||||
|
</ButtonBase>
|
||||||
|
</Box>
|
||||||
|
<Divider />
|
||||||
{mode === 1 && (
|
{mode === 1 && (
|
||||||
<PublisherSubtitles
|
<PublisherSubtitles
|
||||||
subtitles={subtitles}
|
subtitles={subtitles}
|
||||||
publisherName={qortalMetadata.name}
|
publisherName={qortalMetadata.name}
|
||||||
setMode={setMode}
|
setMode={setMode}
|
||||||
onSelect={onSelect}
|
onSelect={onSelectHandler}
|
||||||
/>
|
onBack={onBack}
|
||||||
)}
|
|
||||||
{/* {mode === 2 && (
|
|
||||||
<CommunitySubtitles
|
|
||||||
link={open?.link}
|
|
||||||
name={open?.name}
|
|
||||||
mode={mode}
|
|
||||||
setMode={setMode}
|
|
||||||
username={username}
|
|
||||||
category={open?.category}
|
|
||||||
rootName={open?.rootName}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{/* <Box>
|
||||||
|
{[
|
||||||
|
'Ambient mode',
|
||||||
|
'Annotations',
|
||||||
|
'Subtitles/CC',
|
||||||
|
'Sleep timer',
|
||||||
|
'Playback speed',
|
||||||
|
'Quality',
|
||||||
|
].map((label) => (
|
||||||
|
<Typography
|
||||||
|
key={label}
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
))}
|
||||||
|
</Box> */}
|
||||||
|
</Popover>
|
||||||
|
// <Dialog
|
||||||
|
// open={!!open}
|
||||||
|
// fullWidth={true}
|
||||||
|
// maxWidth={"md"}
|
||||||
|
// sx={{
|
||||||
|
// zIndex: 999990,
|
||||||
|
// }}
|
||||||
|
// slotProps={{
|
||||||
|
// paper: {
|
||||||
|
// elevation: 0,
|
||||||
|
// },
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <DialogTitle>Subtitles</DialogTitle>
|
||||||
|
// <IconButton
|
||||||
|
// aria-label="close"
|
||||||
|
// onClick={handleClose}
|
||||||
|
// sx={(theme) => ({
|
||||||
|
// position: "absolute",
|
||||||
|
// right: 8,
|
||||||
|
// top: 8,
|
||||||
|
// })}
|
||||||
|
// >
|
||||||
|
// <CloseIcon />
|
||||||
|
// </IconButton>
|
||||||
|
// <Button onClick={() => setMode(5)}>New subtitles</Button>
|
||||||
|
// {mode === 1 && (
|
||||||
|
// <PublisherSubtitles
|
||||||
|
// subtitles={subtitles}
|
||||||
|
// publisherName={qortalMetadata.name}
|
||||||
|
// setMode={setMode}
|
||||||
|
// onSelect={onSelect}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
// {mode === 5 && <PublishSubtitles publishHandler={publishHandler} />}
|
||||||
|
// {/* {mode === 2 && (
|
||||||
|
// <CommunitySubtitles
|
||||||
|
// link={open?.link}
|
||||||
|
// name={open?.name}
|
||||||
|
// mode={mode}
|
||||||
|
// setMode={setMode}
|
||||||
|
// username={username}
|
||||||
|
// category={open?.category}
|
||||||
|
// rootName={open?.rootName}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
|
||||||
{mode === 4 && (
|
// {mode === 4 && (
|
||||||
<MySubtitles
|
// <MySubtitles
|
||||||
link={open?.link}
|
// link={open?.link}
|
||||||
name={open?.name}
|
// name={open?.name}
|
||||||
mode={mode}
|
// mode={mode}
|
||||||
setMode={setMode}
|
// setMode={setMode}
|
||||||
username={username}
|
// username={username}
|
||||||
title={title}
|
// title={title}
|
||||||
description={description}
|
// description={description}
|
||||||
setDescription={setDescription}
|
// setDescription={setDescription}
|
||||||
setTitle={setTitle}
|
// setTitle={setTitle}
|
||||||
/>
|
// />
|
||||||
)} */}
|
// )} */}
|
||||||
</Dialog>
|
// </Dialog>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
interface PublisherSubtitlesProps {
|
interface PublisherSubtitlesProps {
|
||||||
publisherName: string
|
publisherName: string;
|
||||||
subtitles: any[]
|
subtitles: any[];
|
||||||
setMode: (val: number)=> void
|
setMode: (val: number) => void;
|
||||||
onSelect: (subtitle: any)=> void
|
onSelect: (subtitle: any) => void;
|
||||||
|
onBack: ()=> void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PublisherSubtitles = ({
|
const PublisherSubtitles = ({
|
||||||
@ -119,8 +352,80 @@ const PublisherSubtitles = ({
|
|||||||
subtitles,
|
subtitles,
|
||||||
setMode,
|
setMode,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
onBack
|
||||||
}: PublisherSubtitlesProps) => {
|
}: PublisherSubtitlesProps) => {
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
|
||||||
|
{subtitles?.map((sub)=> {
|
||||||
|
return <Subtitle onSelect={onSelect} sub={sub} key={`${sub?.qortalMetadata?.service}-${sub?.qortalMetadata?.name}-${sub?.qortalMetadata?.identifier}`}/>
|
||||||
|
})}
|
||||||
|
|
||||||
|
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface PublishSubtitlesProps {
|
||||||
|
publishHandler: (subs: Subtitle[])=> void
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const PublishSubtitles = ({ publishHandler }: PublishSubtitlesProps) => {
|
||||||
|
const [language, setLanguage] = useState<null | string>(null);
|
||||||
|
const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
|
||||||
|
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)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@ -133,27 +438,64 @@ const PublisherSubtitles = ({
|
|||||||
alignItems: "flex-start",
|
alignItems: "flex-start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ButtonBase
|
<Box {...getRootProps()}>
|
||||||
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
display: 'flex',
|
||||||
|
gap: '10px',
|
||||||
}}
|
}}
|
||||||
onClick={() => setMode(2)}
|
variant="contained"
|
||||||
>
|
>
|
||||||
<Box
|
<input {...getInputProps()} />
|
||||||
sx={{
|
Import subtitles
|
||||||
p: 2,
|
</Button>
|
||||||
border: "2px solid",
|
|
||||||
borderRadius: 2,
|
|
||||||
width: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography>Create new index</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</ButtonBase>
|
{subtitles?.map((sub, i) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<LanguageSelect
|
||||||
|
value={sub.language}
|
||||||
|
onChange={(val: string | null) => onChangeValue('language',val, i)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={()=> publishHandler(subtitles)}
|
||||||
|
// disabled={disableButton}
|
||||||
|
variant="contained"
|
||||||
|
>
|
||||||
|
Publish index
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface SubProps {
|
||||||
|
sub: QortalGetMetadata
|
||||||
|
onSelect: (subtitle: Subtitle)=> void;
|
||||||
|
}
|
||||||
|
const Subtitle = ({sub, onSelect}: SubProps)=> {
|
||||||
|
const {resource, isLoading } = usePublish(2, 'JSON', sub)
|
||||||
|
console.log('resource', resource)
|
||||||
|
return <Typography
|
||||||
|
onClick={()=> onSelect(resource?.data)}
|
||||||
|
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
'&:hover': {
|
||||||
|
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||||
|
cursor: 'pointer',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{resource?.data?.language}
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SubtitleManager = React.memo(SubtitleManagerComponent);
|
||||||
|
@ -35,9 +35,10 @@ interface VideoControlsBarProps {
|
|||||||
decreaseSpeed: ()=> void
|
decreaseSpeed: ()=> void
|
||||||
playbackRate: number
|
playbackRate: number
|
||||||
openSubtitleManager: ()=> void
|
openSubtitleManager: ()=> void
|
||||||
|
subtitleBtnRef: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export const VideoControlsBar = ({showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => {
|
export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, increaseSpeed,decreaseSpeed, isFullScreen, showControlsFullScreen, reloadVideo, onVolumeChange, volume, isPlaying, canPlay, isScreenSmall, controlsHeight, playerRef, duration, progress, togglePlay, toggleFullscreen, extractFrames, openSubtitleManager}: VideoControlsBarProps) => {
|
||||||
|
|
||||||
const showMobileControls = isScreenSmall && canPlay;
|
const showMobileControls = isScreenSmall && canPlay;
|
||||||
|
|
||||||
@ -96,7 +97,7 @@ export const VideoControlsBar = ({showControls, playbackRate, increaseSpeed,decr
|
|||||||
<Box sx={controlGroupSX}>
|
<Box sx={controlGroupSX}>
|
||||||
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
|
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
|
||||||
<ObjectFitButton />
|
<ObjectFitButton />
|
||||||
<IconButton onClick={openSubtitleManager}>
|
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>
|
||||||
sub
|
sub
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<PictureInPictureButton />
|
<PictureInPictureButton />
|
||||||
|
@ -10,9 +10,30 @@ import videojs from 'video.js';
|
|||||||
import 'video.js/dist/video-js.css';
|
import 'video.js/dist/video-js.css';
|
||||||
|
|
||||||
import Player from "video.js/dist/types/player";
|
import Player from "video.js/dist/types/player";
|
||||||
import { SubtitleManager } from "./SubtitleManager";
|
import { Subtitle, SubtitleManager, SubtitlePublishedData } from "./SubtitleManager";
|
||||||
|
import { base64ToBlobUrl } from "../../utils/base64";
|
||||||
|
import convert from 'srt-webvtt';
|
||||||
|
|
||||||
|
export async function srtBase64ToVttBlobUrl(base64Srt: string): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
// Step 1: Convert base64 string to a Uint8Array
|
||||||
|
const binary = atob(base64Srt);
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
for (let i = 0; i < binary.length; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: Create a Blob from the Uint8Array with correct MIME type
|
||||||
|
const srtBlob = new Blob([bytes], { type: 'application/x-subrip' });
|
||||||
|
console.log('srtBlob', srtBlob)
|
||||||
|
// Step 3: Use convert() with the Blob
|
||||||
|
const vttBlobUrl: string = await convert(srtBlob);
|
||||||
|
return vttBlobUrl
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to convert SRT to VTT:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
type StretchVideoType = "contain" | "fill" | "cover" | "none" | "scale-down";
|
||||||
|
|
||||||
|
|
||||||
@ -142,6 +163,7 @@ export const VideoPlayer = ({
|
|||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [showControls, setShowControls] = useState(false)
|
const [showControls, setShowControls] = useState(false)
|
||||||
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false)
|
const [isOpenSubtitleManage, setIsOpenSubtitleManage] = useState(false)
|
||||||
|
const subtitleBtnRef = useRef(null)
|
||||||
const {
|
const {
|
||||||
reloadVideo,
|
reloadVideo,
|
||||||
togglePlay,
|
togglePlay,
|
||||||
@ -219,12 +241,10 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
useVideoPlayerHotKeys(hotkeyHandlers);
|
useVideoPlayerHotKeys(hotkeyHandlers);
|
||||||
|
|
||||||
const updateProgress = useCallback(() => {
|
const updateProgress = useCallback(() => {
|
||||||
console.log('currentTime2')
|
|
||||||
const player = playerRef?.current;
|
const player = playerRef?.current;
|
||||||
if (!player || typeof player?.currentTime !== 'function') return;
|
if (!player || typeof player?.currentTime !== 'function') return;
|
||||||
|
|
||||||
const currentTime = player.currentTime();
|
const currentTime = player.currentTime();
|
||||||
console.log('currentTime3', currentTime)
|
|
||||||
if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) {
|
if (typeof currentTime === 'number' && videoLocation && currentTime > 0.1) {
|
||||||
setProgress(videoLocation, currentTime);
|
setProgress(videoLocation, currentTime);
|
||||||
setLocalProgress(currentTime);
|
setLocalProgress(currentTime);
|
||||||
@ -251,7 +271,6 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
|
(e: React.SyntheticEvent<HTMLVideoElement, Event>) => {
|
||||||
try {
|
try {
|
||||||
const video = e.currentTarget;
|
const video = e.currentTarget;
|
||||||
console.log('onVolumeChangeHandler')
|
|
||||||
setVolume(video.volume);
|
setVolume(video.volume);
|
||||||
setIsMuted(video.muted);
|
setIsMuted(video.muted);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -271,7 +290,6 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
};
|
};
|
||||||
}, [showControls]);
|
}, [showControls]);
|
||||||
|
|
||||||
console.log('isFullscreen', isFullscreen, showControlsFullScreen)
|
|
||||||
|
|
||||||
const videoStylesVideo = useMemo(() => {
|
const videoStylesVideo = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
@ -318,11 +336,9 @@ const closeSubtitleManager = useCallback(()=> {
|
|||||||
|
|
||||||
const enterFullscreen = () => {
|
const enterFullscreen = () => {
|
||||||
const ref = containerRef?.current as any;
|
const ref = containerRef?.current as any;
|
||||||
console.log('refffff', ref)
|
|
||||||
if (!ref) return;
|
if (!ref) return;
|
||||||
|
|
||||||
if (ref.requestFullscreen && !isFullscreen) {
|
if (ref.requestFullscreen && !isFullscreen) {
|
||||||
console.log('requset ')
|
|
||||||
ref.requestFullscreen();
|
ref.requestFullscreen();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -399,7 +415,102 @@ useEffect(() => {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const previousSubtitleUrlRef = useRef<string | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
// Component unmount cleanup
|
||||||
|
if (previousSubtitleUrlRef.current) {
|
||||||
|
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
||||||
|
previousSubtitleUrlRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onSelectSubtitle = useCallback(async (subtitle: SubtitlePublishedData)=> {
|
||||||
|
console.log('onSelectSubtitle', subtitle)
|
||||||
|
const player = playerRef.current;
|
||||||
|
if (!player || !subtitle.subtitleData || !subtitle.type) return;
|
||||||
|
|
||||||
|
// Cleanup: revoke previous Blob URL
|
||||||
|
if (previousSubtitleUrlRef.current) {
|
||||||
|
URL.revokeObjectURL(previousSubtitleUrlRef.current);
|
||||||
|
previousSubtitleUrlRef.current = null;
|
||||||
|
}
|
||||||
|
let blobUrl
|
||||||
|
if(subtitle?.type === "application/x-subrip"){
|
||||||
|
blobUrl = await srtBase64ToVttBlobUrl(subtitle.subtitleData)
|
||||||
|
} else {
|
||||||
|
blobUrl = base64ToBlobUrl(subtitle.subtitleData, subtitle.type)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
previousSubtitleUrlRef.current = blobUrl;
|
||||||
|
|
||||||
|
const remoteTracksList = playerRef.current?.remoteTextTracks();
|
||||||
|
|
||||||
|
if (remoteTracksList) {
|
||||||
|
const toRemove: TextTrack[] = [];
|
||||||
|
|
||||||
|
// Bypass TS restrictions safely
|
||||||
|
const list = remoteTracksList as unknown as { length: number; [index: number]: TextTrack };
|
||||||
|
|
||||||
|
for (let i = 0; i < list.length; i++) {
|
||||||
|
const track = list[i];
|
||||||
|
if (track) toRemove.push(track);
|
||||||
|
}
|
||||||
|
|
||||||
|
toRemove.forEach((track) => {
|
||||||
|
playerRef.current?.removeRemoteTextTrack(track);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
playerRef.current?.addRemoteTextTrack({
|
||||||
|
kind: 'subtitles',
|
||||||
|
src: blobUrl,
|
||||||
|
srclang: 'en',
|
||||||
|
label: 'English',
|
||||||
|
default: true
|
||||||
|
}, true);
|
||||||
|
|
||||||
|
// Remove all existing remote text tracks
|
||||||
|
// try {
|
||||||
|
// const remoteTracks = playerRef.current?.remoteTextTracks()?.tracks_
|
||||||
|
// if (remoteTracks && remoteTracks?.length) {
|
||||||
|
// const toRemove: TextTrack[] = [];
|
||||||
|
// for (let i = 0; i < remoteTracks.length; i++) {
|
||||||
|
// const track = remoteTracks[i];
|
||||||
|
// toRemove.push(track);
|
||||||
|
// }
|
||||||
|
// toRemove.forEach((track) => {
|
||||||
|
// console.log('removing track')
|
||||||
|
// playerRef.current?.removeRemoteTextTrack(track);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// } catch (error) {
|
||||||
|
// console.log('error2', error)
|
||||||
|
// }
|
||||||
|
|
||||||
|
await new Promise((res)=> {
|
||||||
|
setTimeout(() => {
|
||||||
|
res(null)
|
||||||
|
}, 1000);
|
||||||
|
})
|
||||||
|
const tracksInfo = playerRef.current?.textTracks();
|
||||||
|
console.log('tracksInfo', tracksInfo)
|
||||||
|
if (!tracksInfo) return;
|
||||||
|
|
||||||
|
const tracks = Array.from({ length: (tracksInfo as any).length }, (_, i) => (tracksInfo as any)[i]);
|
||||||
|
console.log('tracks', tracks)
|
||||||
|
for (const track of tracks) {
|
||||||
|
console.log('track', track)
|
||||||
|
|
||||||
|
if (track.kind === 'subtitles') {
|
||||||
|
track.mode = 'showing'; // force display
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
},[])
|
||||||
|
|
||||||
const handleMouseLeave = useCallback(() => {
|
const handleMouseLeave = useCallback(() => {
|
||||||
setShowControls(false);
|
setShowControls(false);
|
||||||
@ -413,7 +524,6 @@ useEffect(() => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return;
|
if (!resourceUrl || !isReady || !videoLocactionStringified || !startPlay) return;
|
||||||
console.log("EFFECT TRIGGERED", { isReady, resourceUrl, startPlay, poster, videoLocactionStringified });
|
|
||||||
|
|
||||||
const resource = JSON.parse(videoLocactionStringified)
|
const resource = JSON.parse(videoLocactionStringified)
|
||||||
let canceled = false;
|
let canceled = false;
|
||||||
@ -437,7 +547,6 @@ useEffect(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
console.log('options', options)
|
|
||||||
const ref = videoRef as any;
|
const ref = videoRef as any;
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
@ -448,13 +557,7 @@ useEffect(() => {
|
|||||||
playerRef.current?.playbackRate(playbackRate)
|
playerRef.current?.playbackRate(playbackRate)
|
||||||
playerRef.current?.volume(volume);
|
playerRef.current?.volume(volume);
|
||||||
|
|
||||||
playerRef.current?.addRemoteTextTrack({
|
|
||||||
kind: 'subtitles',
|
|
||||||
src: 'http://127.0.0.1:22393/arbitrary/DOCUMENT/a-test/test-identifier',
|
|
||||||
srclang: 'en',
|
|
||||||
label: 'English',
|
|
||||||
default: true
|
|
||||||
}, true);
|
|
||||||
playerRef.current?.play();
|
playerRef.current?.play();
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -472,7 +575,6 @@ useEffect(() => {
|
|||||||
console.error('useEffect start player', error)
|
console.error('useEffect start player', error)
|
||||||
}
|
}
|
||||||
return () => {
|
return () => {
|
||||||
console.log('canceled')
|
|
||||||
canceled = true;
|
canceled = true;
|
||||||
const player = playerRef.current;
|
const player = playerRef.current;
|
||||||
|
|
||||||
@ -490,12 +592,10 @@ useEffect(() => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if(!isPlayerInitialized) return
|
if(!isPlayerInitialized) return
|
||||||
const player = playerRef?.current;
|
const player = playerRef?.current;
|
||||||
console.log('player rate', player)
|
|
||||||
if (!player) return;
|
if (!player) return;
|
||||||
|
|
||||||
const handleRateChange = () => {
|
const handleRateChange = () => {
|
||||||
const newRate = player?.playbackRate();
|
const newRate = player?.playbackRate();
|
||||||
console.log('Playback rate changed:', newRate);
|
|
||||||
if(newRate){
|
if(newRate){
|
||||||
setPlaybackRate(newRate); // or any other state/action
|
setPlaybackRate(newRate); // or any other state/action
|
||||||
}
|
}
|
||||||
@ -544,11 +644,11 @@ useEffect(() => {
|
|||||||
|
|
||||||
|
|
||||||
{isReady && (
|
{isReady && (
|
||||||
<VideoControlsBar playbackRate={playbackRate} increaseSpeed={hotkeyHandlers.increaseSpeed}
|
<VideoControlsBar subtitleBtnRef={subtitleBtnRef} playbackRate={playbackRate} increaseSpeed={hotkeyHandlers.increaseSpeed}
|
||||||
decreaseSpeed={hotkeyHandlers.decreaseSpeed} playerRef={playerRef} isFullScreen={isFullscreen} showControlsFullScreen={showControlsFullScreen} showControls={showControls} extractFrames={extractFrames} toggleFullscreen={toggleFullscreen} onVolumeChange={onVolumeChange} volume={volume} togglePlay={togglePlay} reloadVideo={hotkeyHandlers.reloadVideo} isPlaying={isPlaying} canPlay={true} isScreenSmall={false} controlsHeight={controlsHeight} duration={duration} progress={localProgress} openSubtitleManager={openSubtitleManager} />
|
decreaseSpeed={hotkeyHandlers.decreaseSpeed} playerRef={playerRef} isFullScreen={isFullscreen} showControlsFullScreen={showControlsFullScreen} showControls={showControls} extractFrames={extractFrames} toggleFullscreen={toggleFullscreen} onVolumeChange={onVolumeChange} volume={volume} togglePlay={togglePlay} reloadVideo={hotkeyHandlers.reloadVideo} isPlaying={isPlaying} canPlay={true} isScreenSmall={false} controlsHeight={controlsHeight} duration={duration} progress={localProgress} openSubtitleManager={openSubtitleManager} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SubtitleManager close={closeSubtitleManager} open={isOpenSubtitleManage} qortalMetadata={qortalVideoResource} />
|
<SubtitleManager subtitleBtnRef={subtitleBtnRef} close={closeSubtitleManager} open={isOpenSubtitleManage} qortalMetadata={qortalVideoResource} onSelect={onSelectSubtitle} />
|
||||||
</VideoContainer>
|
</VideoContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -62,13 +62,11 @@ export const useVideoPlayerController = (props: UseVideoControls) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoLocation && isPlayerInitialized) {
|
if (videoLocation && isPlayerInitialized) {
|
||||||
console.log('hellohhhh5')
|
|
||||||
try {
|
try {
|
||||||
const ref = playerRef as any;
|
const ref = playerRef as any;
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
|
|
||||||
const savedProgress = getProgress(videoLocation);
|
const savedProgress = getProgress(videoLocation);
|
||||||
console.log('savedProgress', savedProgress)
|
|
||||||
if (typeof savedProgress === "number") {
|
if (typeof savedProgress === "number") {
|
||||||
playerRef.current?.currentTime(savedProgress);
|
playerRef.current?.currentTime(savedProgress);
|
||||||
|
|
||||||
|
4
src/components/VideoPlayer/video-player-constants.ts
Normal file
4
src/components/VideoPlayer/video-player-constants.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Service } from "../../types/interfaces/resources";
|
||||||
|
|
||||||
|
export const ENTITY_SUBTITLE = "ENTITY_SUBTITLE";
|
||||||
|
export const SERVICE_SUBTITLE: Service = "FILE"
|
9
src/hooks/useListData.tsx
Normal file
9
src/hooks/useListData.tsx
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useListStore } from "../state/lists";
|
||||||
|
import { useCacheStore } from "../state/cache"; // Assuming you export getResourceCache
|
||||||
|
import { QortalGetMetadata } from "../types/interfaces/resources";
|
||||||
|
|
||||||
|
export function useListReturn(listName: string): QortalGetMetadata[] {
|
||||||
|
const list = useListStore((state) => state.lists[listName]?.items) || [];
|
||||||
|
return list
|
||||||
|
}
|
@ -28,6 +28,7 @@ export const useResources = (retryAttempts: number = 2) => {
|
|||||||
const addTemporaryResource = useCacheStore((s) => s.addTemporaryResource);
|
const addTemporaryResource = useCacheStore((s) => s.addTemporaryResource);
|
||||||
const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted);
|
const markResourceAsDeleted = useCacheStore((s) => s.markResourceAsDeleted);
|
||||||
const setSearchParamsForList = useCacheStore((s) => s.setSearchParamsForList);
|
const setSearchParamsForList = useCacheStore((s) => s.setSearchParamsForList);
|
||||||
|
const addList = useListStore((s) => s.addList);
|
||||||
|
|
||||||
const deleteList = useListStore(state => state.deleteList)
|
const deleteList = useListStore(state => state.deleteList)
|
||||||
const requestControllers = new Map<string, AbortController>();
|
const requestControllers = new Map<string, AbortController>();
|
||||||
@ -204,9 +205,10 @@ export const useResources = (retryAttempts: number = 2) => {
|
|||||||
if (cancelRequests) {
|
if (cancelRequests) {
|
||||||
cancelAllRequests();
|
cancelAllRequests();
|
||||||
}
|
}
|
||||||
|
console.log('listName', listName)
|
||||||
const cacheKey = generateCacheKey(params);
|
const cacheKey = generateCacheKey(params);
|
||||||
const searchCache = getSearchCache(listName, cacheKey);
|
const searchCache = getSearchCache(listName, cacheKey);
|
||||||
|
console.log('searchCache', searchCache)
|
||||||
if (searchCache) {
|
if (searchCache) {
|
||||||
const copyParams = {...params}
|
const copyParams = {...params}
|
||||||
delete copyParams.after
|
delete copyParams.after
|
||||||
@ -219,9 +221,12 @@ export const useResources = (retryAttempts: number = 2) => {
|
|||||||
let responseData: QortalMetadata[] = [];
|
let responseData: QortalMetadata[] = [];
|
||||||
let filteredResults: QortalMetadata[] = [];
|
let filteredResults: QortalMetadata[] = [];
|
||||||
let lastCreated = params.before || undefined;
|
let lastCreated = params.before || undefined;
|
||||||
|
console.log('lastCreated', lastCreated)
|
||||||
const targetLimit = params.limit ?? 20; // Use `params.limit` if provided, else default to 20
|
const targetLimit = params.limit ?? 20; // Use `params.limit` if provided, else default to 20
|
||||||
|
const isUnlimited = params.limit === 0;
|
||||||
|
|
||||||
while (filteredResults.length < targetLimit) {
|
while (isUnlimited || filteredResults.length < targetLimit) {
|
||||||
|
console.log('beforebefore')
|
||||||
const response = await qortalRequest({
|
const response = await qortalRequest({
|
||||||
action: "SEARCH_QDN_RESOURCES",
|
action: "SEARCH_QDN_RESOURCES",
|
||||||
mode: "ALL",
|
mode: "ALL",
|
||||||
@ -229,27 +234,31 @@ export const useResources = (retryAttempts: number = 2) => {
|
|||||||
limit: targetLimit - filteredResults.length, // Adjust limit dynamically
|
limit: targetLimit - filteredResults.length, // Adjust limit dynamically
|
||||||
before: lastCreated,
|
before: lastCreated,
|
||||||
});
|
});
|
||||||
|
console.log('responseresponse', response)
|
||||||
if (!response || response.length === 0) {
|
if (!response || response.length === 0) {
|
||||||
break; // No more data available
|
break; // No more data available
|
||||||
}
|
}
|
||||||
|
|
||||||
responseData = response;
|
responseData = response;
|
||||||
const validResults = responseData.filter((item) => item.size !== 32);
|
const validResults = responseData.filter((item) => item.size !== 32);
|
||||||
|
console.log('validResults', validResults)
|
||||||
filteredResults = [...filteredResults, ...validResults];
|
filteredResults = [...filteredResults, ...validResults];
|
||||||
|
|
||||||
if (filteredResults.length >= targetLimit) {
|
if (filteredResults.length >= targetLimit && !isUnlimited) {
|
||||||
filteredResults = filteredResults.slice(0, targetLimit);
|
filteredResults = filteredResults.slice(0, targetLimit);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastCreated = responseData[responseData.length - 1]?.created;
|
lastCreated = responseData[responseData.length - 1]?.created;
|
||||||
|
if (isUnlimited) break;
|
||||||
|
|
||||||
if (!lastCreated) break;
|
if (!lastCreated) break;
|
||||||
}
|
}
|
||||||
const copyParams = {...params}
|
const copyParams = {...params}
|
||||||
delete copyParams.after
|
delete copyParams.after
|
||||||
delete copyParams.before
|
delete copyParams.before
|
||||||
delete copyParams.offset
|
delete copyParams.offset
|
||||||
|
console.log('listName2', listName, filteredResults)
|
||||||
setSearchCache(listName, cacheKey, filteredResults, cancelRequests ? JSON.stringify(copyParams) : null);
|
setSearchCache(listName, cacheKey, filteredResults, cancelRequests ? JSON.stringify(copyParams) : null);
|
||||||
fetchDataFromResults(filteredResults, returnType);
|
fetchDataFromResults(filteredResults, returnType);
|
||||||
|
|
||||||
@ -350,15 +359,15 @@ export const useResources = (retryAttempts: number = 2) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return useMemo(() => ({
|
return useMemo(() => ({
|
||||||
fetchResources,
|
fetchResources,
|
||||||
addNewResources,
|
addNewResources,
|
||||||
updateNewResources,
|
updateNewResources,
|
||||||
deleteResource,
|
deleteResource,
|
||||||
deleteList,
|
deleteList,
|
||||||
|
addList,
|
||||||
fetchResourcesResultsOnly
|
fetchResourcesResultsOnly
|
||||||
}), [fetchResources, addNewResources, updateNewResources, deleteResource, deleteList, fetchResourcesResultsOnly]);
|
}), [fetchResources, addNewResources, updateNewResources, deleteResource, deleteList, fetchResourcesResultsOnly, addList]);
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ export { useModal } from './hooks/useModal';
|
|||||||
export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls';
|
export { AudioPlayerControls , OnTrackChangeMeta, AudioPlayerProps, AudioPlayerHandle} from './components/AudioPlayer/AudioPlayerControls';
|
||||||
export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys';
|
export { useAudioPlayerHotkeys } from './components/AudioPlayer/useAudioPlayerHotkeys';
|
||||||
export { VideoPlayer } from './components/VideoPlayer/VideoPlayer';
|
export { VideoPlayer } from './components/VideoPlayer/VideoPlayer';
|
||||||
|
export { useListReturn } from './hooks/useListData';
|
||||||
import './index.css'
|
import './index.css'
|
||||||
export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events';
|
export { executeEvent, subscribeToEvent, unsubscribeFromEvent } from './utils/events';
|
||||||
export { formatBytes, formatDuration } from './utils/numbers';
|
export { formatBytes, formatDuration } from './utils/numbers';
|
||||||
|
@ -51,7 +51,7 @@ export type Service =
|
|||||||
| "VOICE_PRIVATE"
|
| "VOICE_PRIVATE"
|
||||||
| "DOCUMENT_PRIVATE"
|
| "DOCUMENT_PRIVATE"
|
||||||
| "MAIL_PRIVATE"
|
| "MAIL_PRIVATE"
|
||||||
| "MESSAGE_PRIVATE";
|
| "MESSAGE_PRIVATE" | 'AUTO_UPDATE';
|
||||||
|
|
||||||
|
|
||||||
export interface QortalMetadata {
|
export interface QortalMetadata {
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { Service } from "../interfaces/resources";
|
||||||
import {
|
import {
|
||||||
Coin,
|
Coin,
|
||||||
ConfirmationStatus,
|
ConfirmationStatus,
|
||||||
@ -8,7 +9,6 @@ import {
|
|||||||
ForeignCoin,
|
ForeignCoin,
|
||||||
ResourcePointer,
|
ResourcePointer,
|
||||||
ResourceToPublish,
|
ResourceToPublish,
|
||||||
Service,
|
|
||||||
TxType,
|
TxType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import { Service } from "../interfaces/resources"
|
||||||
|
|
||||||
export type ForeignCoin =
|
export type ForeignCoin =
|
||||||
| 'BTC'
|
| 'BTC'
|
||||||
| 'LTC'
|
| 'LTC'
|
||||||
@ -31,61 +33,7 @@ export type ForeignCoin =
|
|||||||
qortalAtAddress: string;
|
qortalAtAddress: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Service =
|
|
||||||
| 'AUTO_UPDATE'
|
|
||||||
| 'ARBITRARY_DATA'
|
|
||||||
| 'QCHAT_ATTACHMENT'
|
|
||||||
| 'QCHAT_ATTACHMENT_PRIVATE'
|
|
||||||
| 'ATTACHMENT'
|
|
||||||
| 'ATTACHMENT_PRIVATE'
|
|
||||||
| 'FILE'
|
|
||||||
| 'FILE_PRIVATE'
|
|
||||||
| 'FILES'
|
|
||||||
| 'CHAIN_DATA'
|
|
||||||
| 'WEBSITE'
|
|
||||||
| 'GIT_REPOSITORY'
|
|
||||||
| 'IMAGE'
|
|
||||||
| 'IMAGE_PRIVATE'
|
|
||||||
| 'THUMBNAIL'
|
|
||||||
| 'QCHAT_IMAGE'
|
|
||||||
| 'VIDEO'
|
|
||||||
| 'VIDEO_PRIVATE'
|
|
||||||
| 'AUDIO'
|
|
||||||
| 'AUDIO_PRIVATE'
|
|
||||||
| 'QCHAT_AUDIO'
|
|
||||||
| 'QCHAT_VOICE'
|
|
||||||
| 'VOICE'
|
|
||||||
| 'VOICE_PRIVATE'
|
|
||||||
| 'PODCAST'
|
|
||||||
| 'BLOG'
|
|
||||||
| 'BLOG_POST'
|
|
||||||
| 'BLOG_COMMENT'
|
|
||||||
| 'DOCUMENT'
|
|
||||||
| 'DOCUMENT_PRIVATE'
|
|
||||||
| 'LIST'
|
|
||||||
| 'PLAYLIST'
|
|
||||||
| 'APP'
|
|
||||||
| 'METADATA'
|
|
||||||
| 'JSON'
|
|
||||||
| 'GIF_REPOSITORY'
|
|
||||||
| 'STORE'
|
|
||||||
| 'PRODUCT'
|
|
||||||
| 'OFFER'
|
|
||||||
| 'COUPON'
|
|
||||||
| 'CODE'
|
|
||||||
| 'PLUGIN'
|
|
||||||
| 'EXTENSION'
|
|
||||||
| 'GAME'
|
|
||||||
| 'ITEM'
|
|
||||||
| 'NFT'
|
|
||||||
| 'DATABASE'
|
|
||||||
| 'SNAPSHOT'
|
|
||||||
| 'COMMENT'
|
|
||||||
| 'CHAIN_COMMENT'
|
|
||||||
| 'MAIL'
|
|
||||||
| 'MAIL_PRIVATE'
|
|
||||||
| 'MESSAGE'
|
|
||||||
| 'MESSAGE_PRIVATE'
|
|
||||||
|
|
||||||
|
|
||||||
export type ResourceToPublish =
|
export type ResourceToPublish =
|
||||||
|
@ -119,3 +119,23 @@ export function base64ToObject(base64: string){
|
|||||||
|
|
||||||
return toObject
|
return toObject
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const base64ToBlobUrl = (base64: string, mimeType = 'text/vtt'): string => {
|
||||||
|
console.log('base64ToBlobUrl', base64, mimeType)
|
||||||
|
const cleanedBase64 = base64.length % 4 === 0 ? base64 : base64 + '='.repeat(4 - base64.length % 4);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const binary = atob(cleanedBase64);
|
||||||
|
const len = binary.length;
|
||||||
|
const bytes = new Uint8Array(len);
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
bytes[i] = binary.charCodeAt(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
const blob = new Blob([bytes], { type: mimeType });
|
||||||
|
return URL.createObjectURL(blob);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to decode base64:", err);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user