mirror of
https://github.com/Qortal/q-tube.git
synced 2026-04-22 08:03:59 +00:00
started update on bookmarks
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
import React from 'react';
|
||||
import { CardContentContainerComment } from '../common/Comments/Comments-styles';
|
||||
import { useIsSmall } from '../../hooks/useIsSmall';
|
||||
import { AddToBookmarks } from '../common/ContentButtons/AddToBookmarks';
|
||||
|
||||
interface PlaylistsProps {
|
||||
playlistData;
|
||||
@@ -26,7 +27,7 @@ export const Playlists = ({
|
||||
const theme = useTheme();
|
||||
const isScreenSmall = !useMediaQuery(`(min-width:700px)`);
|
||||
const PlaylistsHeight = '36vw'; // This is videoplayer width * 9/16 (inverse of aspect ratio)
|
||||
|
||||
console.log('playlistData', playlistData);
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
@@ -38,6 +39,14 @@ export const Playlists = ({
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<AddToBookmarks
|
||||
metadataReference={{
|
||||
identifier: playlistData?.identifier,
|
||||
service: 'PLAYLIST',
|
||||
name: playlistData?.name,
|
||||
}}
|
||||
type="playlist"
|
||||
/>
|
||||
<CardContentContainerComment
|
||||
sx={{
|
||||
marginTop: '0px',
|
||||
|
||||
@@ -165,6 +165,7 @@ export const PublishVideo = ({
|
||||
},
|
||||
maxSize,
|
||||
onDrop: async (acceptedFiles, rejectedFiles) => {
|
||||
const unsupportedFiles = [];
|
||||
const formattedFiles = [];
|
||||
|
||||
for (const file of acceptedFiles) {
|
||||
@@ -176,9 +177,11 @@ export const PublishVideo = ({
|
||||
}
|
||||
|
||||
const notSupportedCodec = await isHEVC(file);
|
||||
console.log('isGood', isGood);
|
||||
if (notSupportedCodec) {
|
||||
unsupportedFiles.push(file);
|
||||
continue;
|
||||
}
|
||||
formattedFiles.push({
|
||||
isHEVC: notSupportedCodec,
|
||||
file,
|
||||
title: filteredTitle || '',
|
||||
description: '',
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import BookmarksIcon from '@mui/icons-material/Bookmarks';
|
||||
import {
|
||||
alpha,
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
@@ -15,129 +14,128 @@ import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { usePersistedState } from '../../../state/persist/persist';
|
||||
import ShortUniqueId from 'short-unique-id';
|
||||
import { Service, Spacer, useGlobal } from 'qapp-core';
|
||||
import { Service, useGlobal } from 'qapp-core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CustomTooltip } from './CustomTooltip';
|
||||
import { BookmarkList } from '../../../types/bookmark';
|
||||
|
||||
const uid = new ShortUniqueId({ length: 15, dictionary: 'alphanum' });
|
||||
const MAX_NESTING_LEVEL = 1;
|
||||
|
||||
export const AddToBookmarks = ({ metadataReference }) => {
|
||||
export const AddToBookmarks = ({ metadataReference, type = 'video' }) => {
|
||||
const { t } = useTranslation(['core']);
|
||||
|
||||
const { lists: globalLists } = useGlobal();
|
||||
const [bookmarks, setBookmarks, isHydratedSubscriptions] = usePersistedState<
|
||||
const [bookmarks, setBookmarks] = usePersistedState<
|
||||
Record<string, BookmarkList>
|
||||
>('bookmarks-v1', {});
|
||||
const [bookmarkList, setBookmarkList] = useState([]);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [mode, setMode] = useState(1);
|
||||
const [mode, setMode] = useState(1); // 1: main, 2: new list, 3: new folder
|
||||
const [title, setTitle] = useState('');
|
||||
const [parentId, setParentId] = useState<string | null>(null);
|
||||
const theme = useTheme();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const ref = useRef<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
ref?.current?.focus();
|
||||
}
|
||||
if (isOpen) ref?.current?.focus();
|
||||
}, [isOpen]);
|
||||
|
||||
const handleBlur = (e: React.FocusEvent) => {
|
||||
if (!e.currentTarget.contains(e.relatedTarget) && isOpen) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (mode !== 2) return;
|
||||
inputRef.current?.focus();
|
||||
if (mode === 2 || mode === 3) inputRef.current?.focus();
|
||||
}, [mode]);
|
||||
|
||||
const handleCreateList = () => {
|
||||
const handleCreateList = (parentId?: string) => {
|
||||
const newId = uid.rnd();
|
||||
setBookmarks((prev) => {
|
||||
return {
|
||||
const updated: Record<string, BookmarkList> = {
|
||||
...prev,
|
||||
[newId]: {
|
||||
id: newId,
|
||||
title: title,
|
||||
title,
|
||||
description: '',
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
lastAdded: 0,
|
||||
videos: [],
|
||||
lastAccessed: 0,
|
||||
type: 'list', // list, playlist, folder
|
||||
playlistReference: null,
|
||||
folderName: null,
|
||||
type: 'list',
|
||||
},
|
||||
};
|
||||
if (parentId && prev[parentId]?.type === 'folder') {
|
||||
updated[parentId] = {
|
||||
...prev[parentId],
|
||||
children: [...(prev[parentId].children || []), newId],
|
||||
updated: Date.now(),
|
||||
};
|
||||
}
|
||||
return updated;
|
||||
});
|
||||
setMode(1);
|
||||
setTitle('');
|
||||
setParentId(null);
|
||||
};
|
||||
|
||||
const handleCreateFolder = () => {
|
||||
const newId = uid.rnd();
|
||||
setBookmarks((prev) => ({
|
||||
...prev,
|
||||
[newId]: {
|
||||
id: newId,
|
||||
title,
|
||||
description: '',
|
||||
created: Date.now(),
|
||||
updated: Date.now(),
|
||||
lastAdded: 0,
|
||||
videos: [],
|
||||
lastAccessed: 0,
|
||||
type: 'folder',
|
||||
children: [],
|
||||
},
|
||||
}));
|
||||
setMode(1);
|
||||
setTitle('');
|
||||
};
|
||||
|
||||
const handleInputKeyDown = (event: any) => {
|
||||
if (event.key === 'Enter' && title?.trim()) {
|
||||
handleCreateList();
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddVideoToList = (
|
||||
listId: string,
|
||||
video: {
|
||||
name: string;
|
||||
identifier: string;
|
||||
service: Service;
|
||||
}
|
||||
) => {
|
||||
const handleAddVideoToList = (listId: string, video: any) => {
|
||||
globalLists.deleteList('bookmarks-all');
|
||||
globalLists.deleteList(`bookmarks-all-${listId}`);
|
||||
setBookmarks((prev) => {
|
||||
const list = prev[listId];
|
||||
if (!list || list.type !== 'list') return prev;
|
||||
|
||||
const videoExists = list.videos.some(
|
||||
(v) =>
|
||||
v.name === video.name &&
|
||||
v.identifier === video.identifier &&
|
||||
v.service === video.service
|
||||
);
|
||||
|
||||
if (videoExists) return prev;
|
||||
|
||||
const newVideo = {
|
||||
...video,
|
||||
addedAt: Date.now(),
|
||||
created: Date.now(),
|
||||
size: 200,
|
||||
};
|
||||
|
||||
if (
|
||||
list.videos.some(
|
||||
(v) =>
|
||||
v.name === video.name &&
|
||||
v.identifier === video.identifier &&
|
||||
v.service === video.service
|
||||
)
|
||||
) {
|
||||
return prev;
|
||||
}
|
||||
return {
|
||||
...prev,
|
||||
[listId]: {
|
||||
...list,
|
||||
videos: [...list.videos, newVideo],
|
||||
videos: [
|
||||
...list.videos,
|
||||
{
|
||||
...video,
|
||||
addedAt: Date.now(),
|
||||
created: Date.now(),
|
||||
size: 200,
|
||||
type,
|
||||
},
|
||||
],
|
||||
updated: Date.now(),
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleRemoveVideoFromList = (
|
||||
listId: string,
|
||||
video: {
|
||||
name: string;
|
||||
identifier: string;
|
||||
service: string;
|
||||
}
|
||||
) => {
|
||||
const handleRemoveVideoFromList = (listId: string, video: any) => {
|
||||
setBookmarks((prev) => {
|
||||
const list = prev[listId];
|
||||
if (!list || list.type !== 'list') return prev;
|
||||
|
||||
const updatedVideos = list.videos.filter(
|
||||
(v) =>
|
||||
!(
|
||||
@@ -146,9 +144,7 @@ export const AddToBookmarks = ({ metadataReference }) => {
|
||||
v.service === video.service
|
||||
)
|
||||
);
|
||||
// If no change, avoid unnecessary update
|
||||
if (updatedVideos.length === list.videos.length) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[listId]: {
|
||||
@@ -160,26 +156,20 @@ export const AddToBookmarks = ({ metadataReference }) => {
|
||||
});
|
||||
};
|
||||
|
||||
const isVideoInList = (
|
||||
listId: string,
|
||||
video: { name: string; identifier: string; service: string }
|
||||
): boolean => {
|
||||
const isVideoInList = (listId: string, video: any): boolean => {
|
||||
const list = bookmarks[listId];
|
||||
if (!list || list.type !== 'list') return false;
|
||||
|
||||
return list.videos.some(
|
||||
(v) =>
|
||||
v.name === video.name &&
|
||||
v.identifier === video.identifier &&
|
||||
v.service === video.service
|
||||
return (
|
||||
list?.type === 'list' &&
|
||||
list.videos.some(
|
||||
(v) =>
|
||||
v.name === video.name &&
|
||||
v.identifier === video.identifier &&
|
||||
v.service === video.service
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
const isVideoInAnyList = (video: {
|
||||
name: string;
|
||||
identifier: string;
|
||||
service: string;
|
||||
}): boolean => {
|
||||
const isVideoInAnyList = (video: any): boolean => {
|
||||
return Object.values(bookmarks).some(
|
||||
(b) =>
|
||||
b.type === 'list' &&
|
||||
@@ -192,32 +182,96 @@ export const AddToBookmarks = ({ metadataReference }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const isInABookmark = useMemo(() => {
|
||||
return isVideoInAnyList(metadataReference);
|
||||
}, [bookmarks]);
|
||||
const buildBookmarkTree = (
|
||||
bookmarks: Record<string, BookmarkList>
|
||||
): BookmarkList[] => {
|
||||
const roots: BookmarkList[] = [];
|
||||
const childSet = new Set(
|
||||
Object.values(bookmarks).flatMap((b) => b.children ?? [])
|
||||
);
|
||||
Object.values(bookmarks).forEach((item) => {
|
||||
if (
|
||||
!childSet.has(item.id) &&
|
||||
(item.type === 'list' || item.type === 'folder')
|
||||
) {
|
||||
roots.push(item);
|
||||
}
|
||||
});
|
||||
return roots.sort((a, b) => a.title.localeCompare(b.title));
|
||||
};
|
||||
|
||||
const lists = Object.values(bookmarks)
|
||||
.filter((bookmark) => bookmark?.type === 'list' && !!bookmark?.title)
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
const renderBookmarkNode = (
|
||||
node: BookmarkList,
|
||||
depth = 0
|
||||
): React.ReactNode => {
|
||||
if (node.type === 'folder') {
|
||||
return (
|
||||
<Box key={node.id} sx={{ ml: depth + 1, mt: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontWeight: 'bold' }}>{node.title}</Typography>
|
||||
{depth < MAX_NESTING_LEVEL && (
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => {
|
||||
setMode(2);
|
||||
setParentId(node.id);
|
||||
}}
|
||||
>
|
||||
{t('core:bookmarks.new_list')}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
{depth < MAX_NESTING_LEVEL &&
|
||||
(node.children || []).map((childId) => {
|
||||
const child = bookmarks[childId];
|
||||
return child ? renderBookmarkNode(child, depth + 1) : null;
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const isInList = isVideoInList(node.id, metadataReference);
|
||||
return (
|
||||
<ButtonBase
|
||||
key={node.id}
|
||||
onClick={() =>
|
||||
isInList
|
||||
? handleRemoveVideoFromList(node.id, metadataReference)
|
||||
: handleAddVideoToList(node.id, metadataReference)
|
||||
}
|
||||
sx={{ pl: depth * 2, justifyContent: 'flex-start', width: '100%' }}
|
||||
>
|
||||
<Checkbox checked={isInList} />
|
||||
<Typography>{node.title}</Typography>
|
||||
</ButtonBase>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomTooltip
|
||||
title={t('core:action.bookmark_video', {
|
||||
title={t('core:action.bookmark_playlist', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
arrow
|
||||
placement={'top'}
|
||||
placement="top"
|
||||
>
|
||||
<ButtonBase onClick={() => setIsOpen(true)}>
|
||||
<BookmarksIcon color={isInABookmark ? 'success' : 'info'} />
|
||||
<BookmarksIcon
|
||||
color={isVideoInAnyList(metadataReference) ? 'success' : 'info'}
|
||||
/>
|
||||
</ButtonBase>
|
||||
</CustomTooltip>
|
||||
{isOpen && (
|
||||
<Box
|
||||
ref={ref}
|
||||
tabIndex={-1}
|
||||
// onBlur={handleBlur}
|
||||
bgcolor={theme.palette.background.paper}
|
||||
sx={{
|
||||
position: 'fixed',
|
||||
@@ -238,230 +292,112 @@ export const AddToBookmarks = ({ metadataReference }) => {
|
||||
{mode === 1 && (
|
||||
<>
|
||||
<ButtonBase
|
||||
sx={{
|
||||
padding: '5px 0px 10px 0px',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
}}
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setMode(1);
|
||||
setTitle('');
|
||||
}}
|
||||
sx={{
|
||||
p: '5px 0 10px',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '0.85rem' }}>
|
||||
{t('core:bookmarks.bookmark_lists', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</Typography>
|
||||
|
||||
<CloseIcon
|
||||
sx={{
|
||||
fontSize: '1.15em',
|
||||
}}
|
||||
/>
|
||||
<CloseIcon sx={{ fontSize: '1.15em' }} />
|
||||
</ButtonBase>
|
||||
|
||||
<Divider />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
'::-webkit-scrollbar-track': {
|
||||
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,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{lists?.length === 0 && (
|
||||
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
{buildBookmarkTree(bookmarks).length === 0 ? (
|
||||
<Typography
|
||||
sx={{
|
||||
fontSize: '1rem',
|
||||
width: '100%',
|
||||
textAlign: 'center',
|
||||
marginTop: '20px',
|
||||
}}
|
||||
sx={{ fontSize: '1rem', textAlign: 'center', mt: 2 }}
|
||||
>
|
||||
{t('core:bookmarks.no_bookmarks_lists', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</Typography>
|
||||
) : (
|
||||
buildBookmarkTree(bookmarks).map((node) =>
|
||||
renderBookmarkNode(node, 0)
|
||||
)
|
||||
)}
|
||||
<Spacer height="10px" />
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
{lists?.map((list) => {
|
||||
const isInList = isVideoInList(list.id, metadataReference);
|
||||
return (
|
||||
<ButtonBase
|
||||
sx={{
|
||||
width: '100%',
|
||||
justifyContent: 'flex-start',
|
||||
}}
|
||||
key={list.id}
|
||||
onClick={() =>
|
||||
isInList
|
||||
? handleRemoveVideoFromList(
|
||||
list.id,
|
||||
metadataReference
|
||||
)
|
||||
: handleAddVideoToList(list.id, metadataReference)
|
||||
}
|
||||
>
|
||||
<Checkbox checked={isInList} />
|
||||
<Typography>{list?.title}</Typography>
|
||||
</ButtonBase>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
onClick={() => setMode(2)}
|
||||
variant="contained"
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
{t('core:bookmarks.new_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
{t('core:bookmarks.new_list')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setMode(3)}
|
||||
size="small"
|
||||
variant="contained"
|
||||
>
|
||||
{t('core:bookmarks.new_folder')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{mode === 2 && (
|
||||
|
||||
{(mode === 2 || mode === 3) && (
|
||||
<>
|
||||
<Box
|
||||
sx={{
|
||||
padding: '5px 0px 10px 0px',
|
||||
display: 'flex',
|
||||
gap: '10px',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', gap: '10px', width: '100%', py: 1 }}>
|
||||
<ButtonBase
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setMode(1);
|
||||
setTitle('');
|
||||
setParentId(null);
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosIcon
|
||||
sx={{
|
||||
fontSize: '1.15em',
|
||||
}}
|
||||
/>
|
||||
</ButtonBase>
|
||||
<ButtonBase>
|
||||
<Typography
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
setMode(1);
|
||||
setTitle('');
|
||||
}}
|
||||
sx={{
|
||||
fontSize: '0.85rem',
|
||||
}}
|
||||
>
|
||||
{t('core:bookmarks.bookmark_lists', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</Typography>
|
||||
<ArrowBackIosIcon sx={{ fontSize: '1.15em' }} />
|
||||
</ButtonBase>
|
||||
<Typography sx={{ fontSize: '0.85rem' }}>
|
||||
{t('core:bookmarks.bookmark_lists', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Divider />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
flexGrow: 1,
|
||||
overflow: 'auto',
|
||||
'::-webkit-scrollbar-track': {
|
||||
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,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ flexGrow: 1, overflow: 'auto' }}>
|
||||
<TextField
|
||||
inputRef={inputRef}
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && title.trim()) {
|
||||
mode === 2
|
||||
? handleCreateList(parentId ?? undefined)
|
||||
: handleCreateFolder();
|
||||
}
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
width: '100%',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
|
||||
<Button
|
||||
onClick={() => setMode(1)}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
{t('core:action.cancel', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
{t('core:action.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateList}
|
||||
onClick={() =>
|
||||
mode === 2
|
||||
? handleCreateList(parentId ?? undefined)
|
||||
: handleCreateFolder()
|
||||
}
|
||||
disabled={!title}
|
||||
variant="contained"
|
||||
size="small"
|
||||
>
|
||||
{t('core:action.create', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
{t('core:action.create')}
|
||||
</Button>
|
||||
</Box>
|
||||
</>
|
||||
|
||||
@@ -121,9 +121,7 @@ export default function FileElement({
|
||||
|
||||
return (
|
||||
<Box
|
||||
// onClick={handlePlay}
|
||||
sx={{
|
||||
width: '100%',
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -21,14 +21,15 @@ export const FrameExtractor = ({
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
video.addEventListener('loadedmetadata', () => {
|
||||
if (!video) return;
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
const duration = video.duration;
|
||||
if (isFinite(duration)) {
|
||||
// Proceed with your logic
|
||||
|
||||
const newVideoDurations = [...videoDurations];
|
||||
newVideoDurations[index] = duration;
|
||||
setVideoDurations([...newVideoDurations]);
|
||||
setVideoDurations(newVideoDurations);
|
||||
|
||||
const section = duration / 4;
|
||||
const timestamps = [];
|
||||
|
||||
@@ -41,9 +42,13 @@ export const FrameExtractor = ({
|
||||
} else {
|
||||
onFramesExtracted([]);
|
||||
}
|
||||
});
|
||||
}, [videoFile]);
|
||||
};
|
||||
|
||||
video.addEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
return () => {
|
||||
video.removeEventListener('loadedmetadata', handleLoadedMetadata);
|
||||
};
|
||||
}, [videoFile]);
|
||||
useEffect(() => {
|
||||
if (durations.length === 4) {
|
||||
extractFrames();
|
||||
@@ -54,12 +59,24 @@ export const FrameExtractor = ({
|
||||
return URL.createObjectURL(videoFile);
|
||||
}, [videoFile]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
URL.revokeObjectURL(fileUrl);
|
||||
};
|
||||
}, [fileUrl]);
|
||||
|
||||
const extractFrames = async () => {
|
||||
const video = videoRef.current;
|
||||
const canvas = canvasRef.current;
|
||||
canvas.width = video.videoWidth;
|
||||
canvas.height = video.videoHeight;
|
||||
if (!video || !canvas) return;
|
||||
|
||||
const MAX_WIDTH = 800;
|
||||
const scale = Math.min(1, MAX_WIDTH / video.videoWidth);
|
||||
|
||||
canvas.width = video.videoWidth * scale;
|
||||
canvas.height = video.videoHeight * scale;
|
||||
const context = canvas.getContext('2d');
|
||||
if (!context) return;
|
||||
|
||||
const frameData = [];
|
||||
|
||||
@@ -69,7 +86,9 @@ export const FrameExtractor = ({
|
||||
const onSeeked = () => {
|
||||
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
||||
canvas.toBlob((blob) => {
|
||||
frameData.push(blob);
|
||||
if (blob) {
|
||||
frameData.push(blob);
|
||||
}
|
||||
resolve();
|
||||
}, 'image/png');
|
||||
video.removeEventListener('seeked', onSeeked);
|
||||
@@ -78,9 +97,8 @@ export const FrameExtractor = ({
|
||||
});
|
||||
}
|
||||
|
||||
onFramesExtracted(frameData);
|
||||
await onFramesExtracted(frameData);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<video ref={videoRef} style={{ display: 'none' }} src={fileUrl}></video>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"supported_containers": "Supported File Containers",
|
||||
"audio_codecs": "audio codecs",
|
||||
"video_codecs": "video codecs",
|
||||
"unsupported_codecs_description": "Using unsupported Codecs may result in video or audio notworking properly",
|
||||
"unsupported_codecs_description": "Using unsupported Codecs may result in video or audio not working properly",
|
||||
"select_category": "Select a Category",
|
||||
"select_subcategory": "Select a Sub-Category",
|
||||
"add_cover_image": "add cover image",
|
||||
|
||||
@@ -2,6 +2,8 @@ import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonBase,
|
||||
Card,
|
||||
CardContent,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@@ -12,7 +14,10 @@ import {
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import FolderIcon from '@mui/icons-material/Folder';
|
||||
import ListIcon from '@mui/icons-material/List';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Spacer, useGlobal } from 'qapp-core';
|
||||
@@ -28,13 +33,15 @@ import { BookmarkList } from '../../types/bookmark.ts';
|
||||
|
||||
export const Bookmarks = () => {
|
||||
const { t } = useTranslation(['core']);
|
||||
|
||||
const isSmall = useIsSmall();
|
||||
const [selectedList, setSelectedList, isHydratedSelectedList] =
|
||||
usePersistedState<number | any>('selectedBookmarkList', 0);
|
||||
const [bookmarks, setBookmarks, isHydratedBookmarks] = usePersistedState<
|
||||
const [selectedList, setSelectedList] = usePersistedState<any>(
|
||||
'selectedBookmarkList',
|
||||
0
|
||||
);
|
||||
const [bookmarks, setBookmarks] = usePersistedState<
|
||||
Record<string, BookmarkList>
|
||||
>('bookmarks-v1', {});
|
||||
const [folderView, setFolderView] = useState<string | null>(null);
|
||||
const inputRef = useRef(null);
|
||||
const [isOpenEdit, setIsOpenEdit] = useState(false);
|
||||
const [newTitle, setNewTitle] = useState('');
|
||||
@@ -61,6 +68,22 @@ export const Bookmarks = () => {
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
}, [bookmarks]);
|
||||
|
||||
const rootLists = useMemo(() => {
|
||||
const childIds = new Set(
|
||||
Object.values(bookmarks).flatMap((b) => b.children ?? [])
|
||||
);
|
||||
return Object.values(bookmarks).filter(
|
||||
(b) => (b.type === 'list' || b.type === 'folder') && !childIds.has(b.id)
|
||||
);
|
||||
}, [bookmarks]);
|
||||
|
||||
const currentLists = useMemo(() => {
|
||||
if (!folderView) return rootLists;
|
||||
const folder = bookmarks[folderView];
|
||||
if (!folder || folder.type !== 'folder') return [];
|
||||
return (folder.children || []).map((id) => bookmarks[id]).filter(Boolean);
|
||||
}, [folderView, bookmarks, rootLists]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const listId = event.target.value;
|
||||
if (!listId) {
|
||||
@@ -79,9 +102,8 @@ export const Bookmarks = () => {
|
||||
|
||||
const handleDeleteList = (listId: string) => {
|
||||
setBookmarks((prev) => {
|
||||
if (!prev[listId]) return prev; // List doesn't exist
|
||||
|
||||
const { [listId]: _, ...rest } = prev; // Remove the key using object destructuring
|
||||
if (!prev[listId]) return prev;
|
||||
const { [listId]: _, ...rest } = prev;
|
||||
return rest;
|
||||
});
|
||||
};
|
||||
@@ -90,7 +112,6 @@ export const Bookmarks = () => {
|
||||
setBookmarks((prev) => {
|
||||
const list = prev[listId];
|
||||
if (!list || list.type !== 'list') return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[listId]: {
|
||||
@@ -110,18 +131,10 @@ export const Bookmarks = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveVideoFromList = (
|
||||
listId: string,
|
||||
video: {
|
||||
name: string;
|
||||
identifier: string;
|
||||
service: string;
|
||||
}
|
||||
) => {
|
||||
const handleRemoveVideoFromList = (listId: string, video: any) => {
|
||||
setBookmarks((prev) => {
|
||||
const list = prev[listId];
|
||||
if (!list || list.type !== 'list') return prev;
|
||||
|
||||
const updatedVideos = list.videos.filter(
|
||||
(v) =>
|
||||
!(
|
||||
@@ -130,9 +143,7 @@ export const Bookmarks = () => {
|
||||
v.service === video.service
|
||||
)
|
||||
);
|
||||
// If no change, avoid unnecessary update
|
||||
if (updatedVideos.length === list.videos.length) return prev;
|
||||
|
||||
return {
|
||||
...prev,
|
||||
[listId]: {
|
||||
@@ -152,7 +163,27 @@ export const Bookmarks = () => {
|
||||
setIsOpenEdit(false);
|
||||
};
|
||||
|
||||
if (!selectedList && isHydratedBookmarks) {
|
||||
const handleDeleteFolder = (folderId: string) => {
|
||||
if (!bookmarks[folderId] || bookmarks[folderId].type !== 'folder') return;
|
||||
|
||||
const folder = bookmarks[folderId];
|
||||
const children = folder.children || [];
|
||||
|
||||
setBookmarks((prev) => {
|
||||
const updated = { ...prev };
|
||||
delete updated[folderId];
|
||||
children.forEach((childId) => {
|
||||
delete updated[childId];
|
||||
});
|
||||
return updated;
|
||||
});
|
||||
|
||||
setFolderView(null);
|
||||
};
|
||||
|
||||
console.log('bookmark', bookmarks);
|
||||
|
||||
if (!selectedList) {
|
||||
return (
|
||||
<PageTransition>
|
||||
<Box
|
||||
@@ -161,79 +192,79 @@ export const Bookmarks = () => {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
width: '100%',
|
||||
alignItems: isSmall ? 'center' : 'flex-start',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<FormControl
|
||||
sx={{
|
||||
maxWidth: '100%',
|
||||
width: '320px',
|
||||
}}
|
||||
>
|
||||
<InputLabel id="bookmark-list-label">
|
||||
{t('core:bookmarks.select_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</InputLabel>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
{(folderView !== null || selectedList !== 0) && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setFolderView(null);
|
||||
setSelectedList(0);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
←{' '}
|
||||
{t('core:action.back', { postProcess: 'capitalizeFirstChar' })}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Select
|
||||
labelId="bookmark-list-label"
|
||||
value={selectedList?.id || 0}
|
||||
label={t('core:bookmarks.select_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
onChange={handleChange}
|
||||
displayEmpty
|
||||
{folderView && (
|
||||
<Button
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => {
|
||||
const confirm = window.confirm(
|
||||
t('core:bookmarks.confirm_delete_folder') ||
|
||||
'Are you sure you want to delete this folder and all its lists?'
|
||||
);
|
||||
if (confirm) handleDeleteFolder(folderView);
|
||||
}}
|
||||
>
|
||||
{t('core:action.delete', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
</Box>
|
||||
{/* {folderView && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<MenuItem value={0}>
|
||||
<em>
|
||||
{t('core:publish.all_videos', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</em>
|
||||
</MenuItem>
|
||||
{sortedLists.map((list) => (
|
||||
<MenuItem key={list.id} value={list.id}>
|
||||
{list.title}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Spacer height="20px" />
|
||||
<FolderIcon />
|
||||
<Typography>{bookmarks[folderView]?.title}</Typography>
|
||||
</Box>
|
||||
)} */}
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fill, minmax(160px, 1fr))',
|
||||
gap: 2,
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
display: 'flex',
|
||||
alignItems: isSmall ? 'center' : 'flex-start',
|
||||
}}
|
||||
>
|
||||
<PageSubTitle>
|
||||
{t('core:bookmarks.all_bookmarks', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</PageSubTitle>
|
||||
<Spacer height="14px" />
|
||||
<Divider flexItem />
|
||||
<Spacer height="20px" />
|
||||
</Box>
|
||||
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<VideoListPreloaded
|
||||
listName="bookmarks-all"
|
||||
videoList={allVideos}
|
||||
disableActions
|
||||
/>
|
||||
</Box>
|
||||
{currentLists.map((item) => (
|
||||
<Card
|
||||
key={item.id}
|
||||
sx={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
if (item.type === 'folder') setFolderView(item.id);
|
||||
else setSelectedList(item);
|
||||
}}
|
||||
>
|
||||
<CardContent
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 1 }}
|
||||
>
|
||||
{item.type === 'folder' ? <FolderIcon /> : <ListIcon />}
|
||||
<Typography>{item?.title}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</PageTransition>
|
||||
@@ -242,85 +273,55 @@ export const Bookmarks = () => {
|
||||
|
||||
return (
|
||||
<PageTransition>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="bookmark-list-label">
|
||||
{t('core:bookmarks.select_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</InputLabel>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSelectedList(0);
|
||||
setFolderView(null);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
← {t('core:action.back', { postProcess: 'capitalizeFirstChar' })}
|
||||
</Button>
|
||||
<Spacer height="10px" />
|
||||
|
||||
<Select
|
||||
labelId="bookmark-list-label"
|
||||
value={selectedList?.id || 0}
|
||||
label={t('core:bookmarks.select_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
onChange={handleChange}
|
||||
displayEmpty
|
||||
>
|
||||
<MenuItem value={0}>
|
||||
<em>
|
||||
{t('core:publish.all_videos', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
</em>
|
||||
</MenuItem>
|
||||
{sortedLists.map((list) => (
|
||||
<MenuItem key={list.id} value={list.id}>
|
||||
{list.title}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<PageSubTitle>
|
||||
{t('core:bookmarks.bookmark_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
: {selectedList?.title}
|
||||
</PageSubTitle>
|
||||
|
||||
<Spacer height="10px" />
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setIsOpenEdit(true)}
|
||||
variant="outlined"
|
||||
startIcon={<EditIcon />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<Spacer height="10px" />
|
||||
<Divider flexItem />
|
||||
<Spacer height="20px" />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<PageSubTitle
|
||||
sx={{
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
>
|
||||
{t('core:bookmarks.bookmark_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
: {selectedList?.title}
|
||||
</PageSubTitle>
|
||||
<Spacer height="10px" />
|
||||
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => setIsOpenEdit(true)}
|
||||
variant="outlined"
|
||||
startIcon={<EditIcon />}
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
|
||||
<Spacer height="10px" />
|
||||
<Divider flexItem />
|
||||
<Spacer height="20px" />
|
||||
</Box>
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<VideoListPreloaded
|
||||
listName={`bookmarks-all-${selectedList?.id}`}
|
||||
videoList={allVideos}
|
||||
handleRemoveVideoFromList={handleRemoveVideoFromList}
|
||||
listId={selectedList?.id}
|
||||
/>
|
||||
</Box>
|
||||
<VideoListPreloaded
|
||||
listName={`bookmarks-all-${selectedList?.id}`}
|
||||
videoList={allVideos}
|
||||
handleRemoveVideoFromList={handleRemoveVideoFromList}
|
||||
listId={selectedList?.id}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Dialog
|
||||
open={isOpenEdit}
|
||||
onClose={() => setIsOpenEdit(false)}
|
||||
@@ -351,13 +352,9 @@ export const Bookmarks = () => {
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={deleteList}
|
||||
sx={{
|
||||
marginRight: 'auto',
|
||||
}}
|
||||
sx={{ marginRight: 'auto' }}
|
||||
>
|
||||
{t('core:action.delete', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
{t('core:action.delete', { postProcess: 'capitalizeFirstChar' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -366,9 +363,7 @@ export const Bookmarks = () => {
|
||||
setNewTitle(selectedList?.title);
|
||||
}}
|
||||
>
|
||||
{t('core:action.close', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
{t('core:action.close', { postProcess: 'capitalizeFirstChar' })}
|
||||
</Button>
|
||||
<Button
|
||||
disabled={!newTitle.trim() || selectedList?.title === newTitle}
|
||||
@@ -384,9 +379,7 @@ export const Bookmarks = () => {
|
||||
setIsOpenEdit(false);
|
||||
}}
|
||||
>
|
||||
{t('core:action.save', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
{t('core:action.save', { postProcess: 'capitalizeFirstChar' })}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
@@ -122,6 +122,7 @@ export const VideoActionsBar = ({
|
||||
service: 'DOCUMENT',
|
||||
name: videoData?.user,
|
||||
}}
|
||||
type="video"
|
||||
/>
|
||||
<IndexButton channelName={channelName} />
|
||||
<CopyLinkButton
|
||||
|
||||
@@ -72,47 +72,70 @@ export const VideoListItem = ({
|
||||
zIndex: 2,
|
||||
}}
|
||||
>
|
||||
{qortalMetadata?.name === username && (
|
||||
<Tooltip
|
||||
title={t('core:publish.edit_playlist', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
placement="top"
|
||||
>
|
||||
<BlockIconContainer>
|
||||
<EditIcon
|
||||
onClick={() => {
|
||||
const resourceData = {
|
||||
title: qortalMetadata?.metadata?.title,
|
||||
category: qortalMetadata?.metadata?.category,
|
||||
categoryName: qortalMetadata?.metadata?.categoryName,
|
||||
tags: qortalMetadata?.metadata?.tags || [],
|
||||
description: qortalMetadata?.metadata?.description,
|
||||
created: qortalMetadata?.created,
|
||||
updated: qortalMetadata?.updated,
|
||||
name: qortalMetadata.name,
|
||||
videoImage: '',
|
||||
identifier: qortalMetadata.identifier,
|
||||
service: qortalMetadata.service,
|
||||
};
|
||||
setEditPlaylist({ ...resourceData, ...video });
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
)}
|
||||
{qortalMetadata?.name === username &&
|
||||
!isBookmarks &&
|
||||
!disableActions && (
|
||||
<Tooltip
|
||||
title={t('core:publish.edit_playlist', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
placement="top"
|
||||
>
|
||||
<BlockIconContainer>
|
||||
<EditIcon
|
||||
onClick={() => {
|
||||
const resourceData = {
|
||||
title: qortalMetadata?.metadata?.title,
|
||||
category: qortalMetadata?.metadata?.category,
|
||||
categoryName: qortalMetadata?.metadata?.categoryName,
|
||||
tags: qortalMetadata?.metadata?.tags || [],
|
||||
description: qortalMetadata?.metadata?.description,
|
||||
created: qortalMetadata?.created,
|
||||
updated: qortalMetadata?.updated,
|
||||
name: qortalMetadata.name,
|
||||
videoImage: '',
|
||||
identifier: qortalMetadata.identifier,
|
||||
service: qortalMetadata.service,
|
||||
};
|
||||
setEditPlaylist({ ...resourceData, ...video });
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{qortalMetadata?.name !== username && (
|
||||
{qortalMetadata?.name !== username &&
|
||||
!isBookmarks &&
|
||||
!disableActions && (
|
||||
<Tooltip
|
||||
title={t('core:publish.block_user_content', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
placement="top"
|
||||
>
|
||||
<BlockIconContainer>
|
||||
<BlockIcon
|
||||
onClick={() => {
|
||||
blockUserFunc(qortalMetadata?.name);
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isBookmarks && handleRemoveVideoFromList && !disableActions && (
|
||||
<Tooltip
|
||||
title={t('core:publish.block_user_content', {
|
||||
title={t('core:publish.remove_playlist_list', {
|
||||
postProcess: 'capitalizeFirstChar',
|
||||
})}
|
||||
placement="top"
|
||||
>
|
||||
<BlockIconContainer>
|
||||
<BlockIcon
|
||||
<DeleteIcon
|
||||
onClick={() => {
|
||||
blockUserFunc(qortalMetadata?.name);
|
||||
handleRemoveVideoFromList([
|
||||
qortalMetadata,
|
||||
video.videoReference,
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</BlockIconContainer>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { QortalMetadata } from 'qapp-core';
|
||||
|
||||
interface VideoBookmark extends QortalMetadata {
|
||||
addedAt: number;
|
||||
type: 'video' | 'playlist';
|
||||
}
|
||||
|
||||
export interface BookmarkList {
|
||||
@@ -13,7 +14,7 @@ export interface BookmarkList {
|
||||
lastAdded: number;
|
||||
videos: VideoBookmark[];
|
||||
lastAccessed: number;
|
||||
type: 'list' | 'playlist' | 'folder';
|
||||
playlistReference: string | null;
|
||||
folderName: string | null;
|
||||
type: 'list' | 'folder';
|
||||
// New: for folders only
|
||||
children?: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user