fixed sub issues

This commit is contained in:
PhilReact 2025-06-15 23:25:49 +03:00
parent 0e6a5e14a9
commit 793449f486
5 changed files with 361 additions and 173 deletions

View File

@ -1,4 +1,10 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { import {
QortalGetMetadata, QortalGetMetadata,
QortalMetadata, QortalMetadata,
@ -10,6 +16,7 @@ import {
Button, Button,
ButtonBase, ButtonBase,
Card, Card,
CircularProgress,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
@ -18,9 +25,11 @@ import {
Fade, Fade,
IconButton, IconButton,
Popover, Popover,
Skeleton,
Tab, Tab,
Tabs, Tabs,
Typography, Typography,
useTheme,
} from "@mui/material"; } from "@mui/material";
import CheckIcon from "@mui/icons-material/Check"; import CheckIcon from "@mui/icons-material/Check";
import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos"; import ArrowForwardIosIcon from "@mui/icons-material/ArrowForwardIos";
@ -43,6 +52,12 @@ import { ResourceToPublish } from "../../types/qortalRequests/types";
import { useListReturn } from "../../hooks/useListData"; import { useListReturn } from "../../hooks/useListData";
import { usePublish } from "../../hooks/usePublish"; import { usePublish } from "../../hooks/usePublish";
import { Spacer } from "../../common/Spacer"; import { Spacer } from "../../common/Spacer";
import {
dismissToast,
showError,
showLoading,
showSuccess,
} from "../../utils/toast";
interface SubtitleManagerProps { interface SubtitleManagerProps {
qortalMetadata: QortalGetMetadata; qortalMetadata: QortalGetMetadata;
close: () => void; close: () => void;
@ -90,28 +105,36 @@ const SubtitleManagerComponent = ({
const [mode, setMode] = useState(1); const [mode, setMode] = useState(1);
const [isOpenPublish, setIsOpenPublish] = useState(false); const [isOpenPublish, setIsOpenPublish] = useState(false);
const { lists, identifierOperations, auth } = useGlobal(); const { lists, identifierOperations, auth } = useGlobal();
const [isLoading, setIsLoading] = useState(false);
const [showAll, setShowAll] = useState(false);
const { fetchResources } = useResources(); const { fetchResources } = useResources();
// const [subtitles, setSubtitles] = useState([]) // const [subtitles, setSubtitles] = useState([])
const subtitles = useListReturn( const subtitles = useListReturn(
`subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}` `subs-${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`
); );
console.log('subtitles222', subtitles) console.log("subtitles222", subtitles);
const mySubtitles = useMemo(()=> { const mySubtitles = useMemo(() => {
if(!auth?.name)return [] if (!auth?.name) return [];
return subtitles?.filter((sub)=> sub.name === auth?.name) return subtitles?.filter((sub) => sub.name === auth?.name);
}, [subtitles, auth?.name]) }, [subtitles, auth?.name]);
console.log("subtitles222", subtitles); console.log("subtitles222", subtitles);
const getPublishedSubtitles = useCallback(async () => { const getPublishedSubtitles = useCallback(async () => {
try { try {
setIsLoading(true);
const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`; const videoId = `${qortalMetadata?.service}-${qortalMetadata?.name}-${qortalMetadata?.identifier}`;
console.log("videoId", videoId); console.log("videoId", videoId);
const postIdSearch = await identifierOperations.buildSearchPrefix( const postIdSearch = await identifierOperations.buildSearchPrefix(
ENTITY_SUBTITLE, ENTITY_SUBTITLE,
videoId videoId
); );
let name: string | undefined = qortalMetadata?.name;
if (showAll) {
name = undefined;
}
const searchParams = { const searchParams = {
service: SERVICE_SUBTITLE, service: SERVICE_SUBTITLE,
identifier: postIdSearch, identifier: postIdSearch,
name,
limit: 0, limit: 0,
}; };
const res = await lists.fetchResources( const res = await lists.fetchResources(
@ -123,8 +146,10 @@ const SubtitleManagerComponent = ({
console.log("resres2", res); console.log("resres2", res);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} finally {
setIsLoading(false);
} }
}, []); }, [showAll]);
useEffect(() => { useEffect(() => {
if ( if (
@ -215,6 +240,8 @@ const SubtitleManagerComponent = ({
close(); close();
}; };
const theme = useTheme();
return ( return (
<> <>
<Popover <Popover
@ -236,7 +263,11 @@ const SubtitleManagerComponent = ({
borderRadius: 2, borderRadius: 2,
boxShadow: 5, boxShadow: 5,
p: 1, p: 1,
minWidth: 200, minWidth: 225,
height: 300,
overflow: "hidden",
display: "flex",
flexDirection: "column",
}, },
}, },
}} }}
@ -288,16 +319,75 @@ const SubtitleManagerComponent = ({
</ButtonBase> </ButtonBase>
</Box> </Box>
<Divider /> <Divider />
{mode === 1 && ( <Box
<PublisherSubtitles sx={{
subtitles={subtitles} display: "flex",
publisherName={qortalMetadata.name} flexDirection: "column",
setMode={setMode} flexGrow: 1,
onSelect={onSelectHandler} overflow: "auto",
onBack={onBack} "::-webkit-scrollbar-track": {
currentSubTrack={currentSubTrack} backgroundColor: "transparent",
/> },
)}
"::-webkit-scrollbar": {
width: "16px",
height: "10px",
},
"::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
transition: "0.3s background-color",
},
"::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.primary.dark,
},
}}
>
{isLoading && <CircularProgress />}
{!isLoading && subtitles?.length === 0 && (
<Typography
sx={{
fontSize: "1rem",
width: "100%",
textAlign: "center",
marginTop: "20px",
}}
>
No subtitles
</Typography>
)}
{mode === 1 && !isLoading && subtitles?.length > 0 && (
<PublisherSubtitles
subtitles={subtitles}
publisherName={qortalMetadata.name}
setMode={setMode}
onSelect={onSelectHandler}
onBack={onBack}
currentSubTrack={currentSubTrack}
/>
)}
</Box>
<Box
sx={{
display: "flex",
width: "100%",
justifyContent: "center",
}}
>
<Button
variant="contained"
size="small"
disabled={showAll}
onClick={() => setShowAll(true)}
>
Load all
</Button>
</Box>
{/* <Box> {/* <Box>
{[ {[
'Ambient mode', 'Ambient mode',
@ -414,6 +504,23 @@ const PublisherSubtitles = ({
}: PublisherSubtitlesProps) => { }: PublisherSubtitlesProps) => {
return ( return (
<> <>
<ButtonBase
disabled={!currentSubTrack}
onClick={() => onSelect(null)}
sx={{
px: 2,
py: 1,
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.1)",
},
width: "100%",
justifyContent: "space-between",
}}
>
<Typography>Off</Typography>
{!currentSubTrack ? <CheckIcon /> : <ArrowForwardIosIcon />}
</ButtonBase>
{subtitles?.map((sub) => { {subtitles?.map((sub) => {
return ( return (
<Subtitle <Subtitle
@ -429,21 +536,23 @@ const PublisherSubtitles = ({
}; };
interface PublishSubtitlesProps { interface PublishSubtitlesProps {
publishHandler: (subs: Subtitle[]) => void; publishHandler: (subs: Subtitle[]) => Promise<void>;
isOpen: boolean; isOpen: boolean;
setIsOpen: (val: boolean) => void; setIsOpen: (val: boolean) => void;
mySubtitles: QortalGetMetadata[] mySubtitles: QortalGetMetadata[];
} }
const PublishSubtitles = ({ const PublishSubtitles = ({
publishHandler, publishHandler,
isOpen, isOpen,
setIsOpen, setIsOpen,
mySubtitles mySubtitles,
}: PublishSubtitlesProps) => { }: PublishSubtitlesProps) => {
const [language, setLanguage] = useState<null | string>(null); const [language, setLanguage] = useState<null | string>(null);
const [subtitles, setSubtitles] = useState<Subtitle[]>([]); const [subtitles, setSubtitles] = useState<Subtitle[]>([]);
const {lists} = useGlobal() const [isPublishing, setIsPublishing] = useState(false);
const { lists } = useGlobal();
const theme = useTheme();
const onDrop = useCallback(async (acceptedFiles: File[]) => { const onDrop = useCallback(async (acceptedFiles: File[]) => {
const newSubtitles: Subtitle[] = []; const newSubtitles: Subtitle[] = [];
for (const file of acceptedFiles) { for (const file of acceptedFiles) {
@ -495,6 +604,7 @@ const PublishSubtitles = ({
const handleClose = () => { const handleClose = () => {
setIsOpen(false); setIsOpen(false);
setSubtitles([]);
}; };
const [value, setValue] = useState(0); const [value, setValue] = useState(0);
@ -503,15 +613,39 @@ const PublishSubtitles = ({
setValue(newValue); setValue(newValue);
}; };
const onDelete = useCallback(async (sub: QortalGetMetadata)=> { const onDelete = useCallback(async (sub: QortalGetMetadata) => {
let loadId;
try { try {
await lists.deleteResource([ setIsPublishing(true);
sub loadId = showLoading("Deleting subtitle...");
]) await lists.deleteResource([sub]);
showSuccess("Deleted subtitle");
} catch (error) { } 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 ( return (
<Dialog <Dialog
@ -524,6 +658,10 @@ const PublishSubtitles = ({
slotProps={{ slotProps={{
paper: { paper: {
elevation: 0, elevation: 0,
sx: {
height: "600px",
maxHeight: "100vh",
},
}, },
}} }}
> >
@ -539,7 +677,26 @@ const PublishSubtitles = ({
> >
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
<DialogContent> <DialogContent
sx={{
"::-webkit-scrollbar": {
width: "16px",
height: "10px",
},
"::-webkit-scrollbar-thumb": {
backgroundColor: theme.palette.primary.main,
borderRadius: "8px",
backgroundClip: "content-box",
border: "4px solid transparent",
transition: "0.3s background-color",
},
"::-webkit-scrollbar-thumb:hover": {
backgroundColor: theme.palette.primary.dark,
},
}}
>
<Box sx={{ width: "100%" }}> <Box sx={{ width: "100%" }}>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}> <Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs <Tabs
@ -554,113 +711,114 @@ const PublishSubtitles = ({
</Box> </Box>
<Spacer height="25px" /> <Spacer height="25px" />
{value === 0 && ( {value === 0 && (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "20px", gap: "20px",
width: "100%", width: "100%",
alignItems: "center", alignItems: "center",
}} }}
> >
<Box {...getRootProps()}> <Box {...getRootProps()}>
<Button <Button
sx={{
display: "flex",
gap: "10px",
}}
variant="contained"
>
<input {...getInputProps()} />
Import subtitles
</Button>
</Box>
{subtitles?.map((sub, i) => {
return (
<Card
sx={{ sx={{
padding: "10px", display: "flex",
width: "500px", gap: "10px",
maxWidth: "100%",
}} }}
variant="contained"
> >
<Typography <input {...getInputProps()} />
Import subtitles
</Button>
</Box>
{subtitles?.map((sub, i) => {
return (
<Card
sx={{ sx={{
fontSize: "1rem", padding: "10px",
width: "500px",
maxWidth: "100%",
}} }}
> >
{sub.filename} <Typography
</Typography> sx={{
<Spacer height="10px" /> fontSize: "1rem",
<LanguageSelect
value={sub.language}
onChange={(val: string | null) =>
onChangeValue("language", val, i)
}
/>
<Spacer height="10px" />
<Box
sx={{
justifyContent: "flex-end",
width: "100%",
display: "flex",
}}
>
<Button
onClick={() => {
setSubtitles((prev) => {
const newSubtitles = [...prev];
newSubtitles.splice(i, 1); // Remove 1 item at index i
return newSubtitles;
});
}} }}
variant="contained"
size="small"
color="secondary"
> >
remove {sub.filename}
</Button> </Typography>
</Box> <Spacer height="10px" />
</Card> <LanguageSelect
); value={sub.language}
})} onChange={(val: string | null) =>
</Box> onChangeValue("language", val, i)
}
/>
<Spacer height="10px" />
<Box
sx={{
justifyContent: "flex-end",
width: "100%",
display: "flex",
}}
>
<Button
onClick={() => {
setSubtitles((prev) => {
const newSubtitles = [...prev];
newSubtitles.splice(i, 1); // Remove 1 item at index i
return newSubtitles;
});
}}
variant="contained"
size="small"
color="secondary"
>
remove
</Button>
</Box>
</Card>
);
})}
</Box>
)} )}
{value === 1 && ( {value === 1 && (
<Box <Box
sx={{ sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "20px", gap: "20px",
width: "100%", width: "100%",
alignItems: "center", alignItems: "center",
}} }}
> >
{mySubtitles?.map((sub, i) => {
{mySubtitles?.map((sub, i) => { return (
return ( <Card
<Card sx={{
sx={{ padding: "10px",
padding: "10px", width: "500px",
width: "500px", maxWidth: "100%",
maxWidth: "100%", }}
}} >
> <MySubtitle onDelete={onDelete} sub={sub} />
<MySubtitle onDelete={onDelete} sub={sub} /> </Card>
</Card> );
); })}
})} </Box>
</Box>
)} )}
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button {value === 0 && (
onClick={() => publishHandler(subtitles)} <Button
// disabled={disableButton} onClick={() => publishHandlerLocal(subtitles)}
variant="contained" disabled={disableButton}
> variant="contained"
Publish >
</Button> Publish
</Button>
)}
</DialogActions> </DialogActions>
</Dialog> </Dialog>
); );
@ -672,7 +830,7 @@ interface SubProps {
currentSubtrack: null | string; currentSubtrack: null | string;
} }
const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => { const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => {
const { resource, isLoading } = usePublish(2, "JSON", sub); const { resource, isLoading, error } = usePublish(2, "JSON", sub);
console.log("resource", resource); console.log("resource", resource);
const isSelected = currentSubtrack === resource?.data?.language; const isSelected = currentSubtrack === resource?.data?.language;
return ( return (
@ -688,8 +846,13 @@ const Subtitle = ({ sub, onSelect, currentSubtrack }: SubProps) => {
justifyContent: "space-between", justifyContent: "space-between",
}} }}
> >
<Typography>{resource?.data?.language}</Typography> {isLoading && <Skeleton variant="text" sx={{ fontSize: "1.25rem", width: '100%' }} />}
{isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />} {!isLoading && !error && (
<>
<Typography>{resource?.data?.language}</Typography>
{isSelected ? <CheckIcon /> : <ArrowForwardIosIcon />}
</>
)}
</ButtonBase> </ButtonBase>
); );
}; };
@ -699,46 +862,49 @@ interface MySubtitleProps {
onDelete: (subtitle: QortalGetMetadata) => void; onDelete: (subtitle: QortalGetMetadata) => void;
} }
const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => { const MySubtitle = ({ sub, onDelete }: MySubtitleProps) => {
const { resource, isLoading } = usePublish(2, "JSON", sub); const { resource, isLoading, error } = usePublish(2, "JSON", sub);
console.log("resource", resource); console.log("resource", resource);
return ( return (
<Card <Card
sx={{ sx={{
padding: "10px", padding: "10px",
width: "500px", width: "500px",
maxWidth: "100%", maxWidth: "100%",
}} }}
> >
<Typography <Typography
sx={{ sx={{
fontSize: "1rem", fontSize: "1rem",
}} }}
> >
{resource?.data?.filename} {resource?.data?.filename}
</Typography> </Typography>
<Spacer height="10px" /> <Spacer height="10px" />
<Typography sx={{ <Typography
fontSize: '1rem' sx={{
}}>{resource?.data?.language}</Typography> fontSize: "1rem",
<Spacer height="10px" /> }}
<Box >
sx={{ {resource?.data?.language}
justifyContent: "flex-end", </Typography>
width: "100%", <Spacer height="10px" />
display: "flex", <Box
}} sx={{
> justifyContent: "flex-end",
<Button width: "100%",
onClick={() => onDelete(sub)} display: "flex",
variant="contained" }}
size="small" >
color="secondary" <Button
> onClick={() => onDelete(sub)}
delete variant="contained"
</Button> size="small"
</Box> color="secondary"
</Card> >
delete
</Button>
</Box>
</Card>
); );
}; };

View File

@ -13,7 +13,7 @@ import {
VolumeControl, VolumeControl,
} from "./VideoControls"; } from "./VideoControls";
import { Ref } from "react"; import { Ref } from "react";
import SubtitlesIcon from '@mui/icons-material/Subtitles';
interface VideoControlsBarProps { interface VideoControlsBarProps {
canPlay: boolean canPlay: boolean
isScreenSmall: boolean isScreenSmall: boolean
@ -99,7 +99,7 @@ export const VideoControlsBar = ({subtitleBtnRef, showControls, playbackRate, in
<PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} /> <PlaybackRate playbackRate={playbackRate} increaseSpeed={increaseSpeed} decreaseSpeed={decreaseSpeed} />
<ObjectFitButton /> <ObjectFitButton />
<IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}> <IconButton ref={subtitleBtnRef} onClick={openSubtitleManager}>
sub <SubtitlesIcon />
</IconButton> </IconButton>
<PictureInPictureButton /> <PictureInPictureButton />
<FullscreenButton toggleFullscreen={toggleFullscreen} /> <FullscreenButton toggleFullscreen={toggleFullscreen} />

View File

@ -23,14 +23,23 @@ export const VideoElement = styled("video")(({ theme }) => ({
right: 0, right: 0,
left: 0, left: 0,
background: "rgb(33, 33, 33)", background: "rgb(33, 33, 33)",
"&:focus": { outline: "none" },
"&:focus": {
outline: "none !important",
boxShadow: "none !important",
},
"&:focus-visible": {
outline: "none !important",
boxShadow: "none !important",
},
"&::-webkit-media-controls": { "&::-webkit-media-controls": {
display:"none !important" display: "none !important",
}, },
"&:fullscreen": { "&:fullscreen": {
paddingBottom: '50px' paddingBottom: '50px',
} },
})); }));
//1075 x 604 //1075 x 604
export const ControlsContainer = styled(Box)` export const ControlsContainer = styled(Box)`
width: 100%; width: 100%;

View File

@ -591,6 +591,8 @@ export const VideoPlayer = ({
if (!playerRef.current && ref.current) { if (!playerRef.current && ref.current) {
playerRef.current = videojs(ref.current, options, () => { playerRef.current = videojs(ref.current, options, () => {
setIsPlayerInitialized(true); setIsPlayerInitialized(true);
ref.current.tabIndex = -1; // Prevents focus entirely
ref.current.style.outline = 'none'; // Backup
playerRef.current?.poster(""); playerRef.current?.poster("");
playerRef.current?.playbackRate(playbackRate); playerRef.current?.playbackRate(playbackRate);
playerRef.current?.volume(volume); playerRef.current?.volume(volume);
@ -701,7 +703,7 @@ export const VideoPlayer = ({
/> />
<VideoElement <VideoElement
ref={videoRef} ref={videoRef}
tabIndex={0} tabIndex={-1}
className="video-js" className="video-js"
src={isReady && startPlay ? resourceUrl || undefined : undefined} src={isReady && startPlay ? resourceUrl || undefined : undefined}
poster={startPlay ? "" : poster} poster={startPlay ? "" : poster}

View File

@ -6,3 +6,14 @@
box-sizing: border-box; box-sizing: border-box;
} }
video:focus,
video:focus-visible,
.vjs-tech:focus,
.vjs-tech:focus-visible {
outline: none !important;
box-shadow: none !important;
}
.video-js *:focus:not(:focus-visible) {
outline: none !important;
}