feat: Implement SortControl component for sorting videos
This commit is contained in:
86
frontend/src/components/SortControl.tsx
Normal file
86
frontend/src/components/SortControl.tsx
Normal file
@@ -0,0 +1,86 @@
|
||||
import { AccessTime, Shuffle, Sort, SortByAlpha, Visibility } from '@mui/icons-material';
|
||||
import { Box, Button, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
|
||||
interface SortControlProps {
|
||||
sortOption: string;
|
||||
sortAnchorEl: null | HTMLElement;
|
||||
onSortClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onSortClose: (option?: string) => void;
|
||||
}
|
||||
|
||||
const SortControl: React.FC<SortControlProps> = ({
|
||||
sortOption,
|
||||
sortAnchorEl,
|
||||
onSortClick,
|
||||
onSortClose
|
||||
}) => {
|
||||
const { t } = useLanguage();
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex' }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onSortClick}
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 'auto',
|
||||
px: { xs: 1, md: 2 },
|
||||
height: '100%',
|
||||
color: 'text.secondary',
|
||||
borderColor: 'text.secondary'
|
||||
}}
|
||||
>
|
||||
<Sort fontSize="small" sx={{ mr: { xs: 0, md: 1 } }} />
|
||||
<Box component="span" sx={{ display: { xs: 'none', md: 'block' } }}>
|
||||
{t('sort')}
|
||||
</Box>
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={sortAnchorEl}
|
||||
open={Boolean(sortAnchorEl)}
|
||||
onClose={() => onSortClose()}
|
||||
>
|
||||
<MenuItem onClick={() => onSortClose('dateDesc')} selected={sortOption === 'dateDesc'}>
|
||||
<ListItemIcon>
|
||||
<AccessTime fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dateDesc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onSortClose('dateAsc')} selected={sortOption === 'dateAsc'}>
|
||||
<ListItemIcon>
|
||||
<AccessTime fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dateAsc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onSortClose('viewsDesc')} selected={sortOption === 'viewsDesc'}>
|
||||
<ListItemIcon>
|
||||
<Visibility fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('viewsDesc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onSortClose('viewsAsc')} selected={sortOption === 'viewsAsc'}>
|
||||
<ListItemIcon>
|
||||
<Visibility fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('viewsAsc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onSortClose('nameAsc')} selected={sortOption === 'nameAsc'}>
|
||||
<ListItemIcon>
|
||||
<SortByAlpha fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('nameAsc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => onSortClose('random')} selected={sortOption === 'random'}>
|
||||
<ListItemIcon>
|
||||
<Shuffle fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('random')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortControl;
|
||||
106
frontend/src/hooks/useVideoSort.ts
Normal file
106
frontend/src/hooks/useVideoSort.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
|
||||
export interface VideoWithDetails {
|
||||
id: string;
|
||||
title: string;
|
||||
addedAt: string;
|
||||
viewCount?: number;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface UseVideoSortProps<T> {
|
||||
videos: T[];
|
||||
onSortChange?: (option: string) => void;
|
||||
}
|
||||
|
||||
export const useVideoSort = <T extends VideoWithDetails>({ videos, onSortChange }: UseVideoSortProps<T>) => {
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
|
||||
// Initialize sort option from URL or default
|
||||
const sortOptionP = searchParams.get('sort') || 'dateDesc';
|
||||
const seedP = parseInt(searchParams.get('seed') || '0', 10);
|
||||
|
||||
const [sortOption, setSortOption] = useState<string>(sortOptionP);
|
||||
const [shuffleSeed, setShuffleSeed] = useState<number>(seedP);
|
||||
const [sortAnchorEl, setSortAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
// Sync state with URL params
|
||||
useEffect(() => {
|
||||
const currentSort = searchParams.get('sort') || 'dateDesc';
|
||||
const currentSeed = parseInt(searchParams.get('seed') || '0', 10);
|
||||
setSortOption(currentSort);
|
||||
setShuffleSeed(currentSeed);
|
||||
}, [searchParams]);
|
||||
|
||||
const handleSortClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setSortAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleSortClose = (option?: string) => {
|
||||
if (option) {
|
||||
if (onSortChange) {
|
||||
onSortChange(option);
|
||||
}
|
||||
|
||||
setSearchParams((prev: URLSearchParams) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
|
||||
if (option === 'random') {
|
||||
newParams.set('sort', 'random');
|
||||
// Always generate a new seed when clicking 'random'
|
||||
const newSeed = Math.floor(Math.random() * 1000000);
|
||||
newParams.set('seed', newSeed.toString());
|
||||
} else {
|
||||
newParams.set('sort', option);
|
||||
newParams.delete('seed');
|
||||
}
|
||||
return newParams;
|
||||
});
|
||||
}
|
||||
setSortAnchorEl(null);
|
||||
};
|
||||
|
||||
const sortedVideos = useMemo(() => {
|
||||
if (!videos) return [];
|
||||
const result = [...videos];
|
||||
switch (sortOption) {
|
||||
case 'dateDesc':
|
||||
return result.sort((a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime());
|
||||
case 'dateAsc':
|
||||
return result.sort((a, b) => new Date(a.addedAt).getTime() - new Date(b.addedAt).getTime());
|
||||
case 'viewsDesc':
|
||||
return result.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
|
||||
case 'viewsAsc':
|
||||
return result.sort((a, b) => (a.viewCount || 0) - (b.viewCount || 0));
|
||||
case 'nameAsc':
|
||||
return result.sort((a, b) => a.title.localeCompare(b.title));
|
||||
case 'random':
|
||||
// Use a seeded predictable random sort
|
||||
return result.map(v => {
|
||||
// Simple hash function for stability with seed
|
||||
let h = 0x811c9dc5;
|
||||
const s = v.id + shuffleSeed;
|
||||
// Hash string id + seed
|
||||
const str = s.toString();
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
h ^= str.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return { v, score: h >>> 0 };
|
||||
})
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.map(item => item.v);
|
||||
default:
|
||||
return result;
|
||||
}
|
||||
}, [videos, sortOption, shuffleSeed]);
|
||||
|
||||
return {
|
||||
sortedVideos,
|
||||
sortOption,
|
||||
sortAnchorEl,
|
||||
handleSortClick,
|
||||
handleSortClose
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
import { AccessTime, Collections as CollectionsIcon, GridView, History, Shuffle, Sort, SortByAlpha, ViewSidebar, Visibility } from '@mui/icons-material';
|
||||
import { Collections as CollectionsIcon, GridView, History, ViewSidebar } from '@mui/icons-material';
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
@@ -8,10 +8,6 @@ import {
|
||||
Collapse,
|
||||
Container,
|
||||
Grid,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Pagination,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
@@ -24,11 +20,13 @@ import { VirtuosoGrid } from 'react-virtuoso';
|
||||
import AuthorsList from '../components/AuthorsList';
|
||||
import CollectionCard from '../components/CollectionCard';
|
||||
import Collections from '../components/Collections';
|
||||
import SortControl from '../components/SortControl';
|
||||
import TagsList from '../components/TagsList';
|
||||
import VideoCard from '../components/VideoCard';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useVideo } from '../contexts/VideoContext';
|
||||
import { useVideoSort } from '../hooks/useVideoSort';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
@@ -55,13 +53,7 @@ const Home: React.FC = () => {
|
||||
return (saved as 'collections' | 'all-videos' | 'history') || 'all-videos';
|
||||
});
|
||||
|
||||
// Initialize sort option from URL or default
|
||||
const sortOptionP = searchParams.get('sort') || 'dateDesc';
|
||||
const seedP = parseInt(searchParams.get('seed') || '0', 10);
|
||||
|
||||
const [sortOption, setSortOption] = useState<string>(sortOptionP);
|
||||
const [shuffleSeed, setShuffleSeed] = useState<number>(seedP);
|
||||
const [sortAnchorEl, setSortAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [isSidebarOpen, setIsSidebarOpen] = useState(true);
|
||||
const [settingsLoaded, setSettingsLoaded] = useState(false);
|
||||
const [infiniteScroll, setInfiniteScroll] = useState(false);
|
||||
@@ -131,13 +123,7 @@ const Home: React.FC = () => {
|
||||
[gridProps]
|
||||
);
|
||||
|
||||
// Sync state with URL params
|
||||
useEffect(() => {
|
||||
const currentSort = searchParams.get('sort') || 'dateDesc';
|
||||
const currentSeed = parseInt(searchParams.get('seed') || '0', 10);
|
||||
setSortOption(currentSort);
|
||||
setShuffleSeed(currentSeed);
|
||||
}, [searchParams]);
|
||||
|
||||
|
||||
// Fetch settings on mount
|
||||
useEffect(() => {
|
||||
@@ -195,11 +181,9 @@ const Home: React.FC = () => {
|
||||
}
|
||||
}, [selectedTags, setSearchParams]);
|
||||
|
||||
|
||||
// Add default empty array to ensure videos is always an array
|
||||
const videoArray = Array.isArray(videos) ? videos : [];
|
||||
|
||||
|
||||
// Filter videos based on view mode
|
||||
const filteredVideos = useMemo(() => {
|
||||
if (viewMode === 'all-videos') {
|
||||
@@ -248,37 +232,23 @@ const Home: React.FC = () => {
|
||||
});
|
||||
}, [viewMode, videoArray, selectedTags, collections]);
|
||||
|
||||
const sortedVideos = useMemo(() => {
|
||||
const result = [...filteredVideos];
|
||||
switch (sortOption) {
|
||||
case 'dateDesc':
|
||||
return result.sort((a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime());
|
||||
case 'dateAsc':
|
||||
return result.sort((a, b) => new Date(a.addedAt).getTime() - new Date(b.addedAt).getTime());
|
||||
case 'viewsDesc':
|
||||
return result.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
|
||||
case 'viewsAsc':
|
||||
return result.sort((a, b) => (a.viewCount || 0) - (b.viewCount || 0));
|
||||
case 'nameAsc':
|
||||
return result.sort((a, b) => a.title.localeCompare(b.title));
|
||||
case 'random':
|
||||
// Use a seeded predictable random sort
|
||||
return result.map(v => {
|
||||
// Simple hash function for stability with seed
|
||||
let h = 0x811c9dc5;
|
||||
const s = v.id + shuffleSeed;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return { v, score: h >>> 0 };
|
||||
})
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.map(item => item.v);
|
||||
default:
|
||||
return result;
|
||||
// Use the custom hook for sorting
|
||||
const {
|
||||
sortedVideos,
|
||||
sortOption,
|
||||
sortAnchorEl,
|
||||
handleSortClick,
|
||||
handleSortClose
|
||||
} = useVideoSort({
|
||||
videos: filteredVideos,
|
||||
onSortChange: () => {
|
||||
setSearchParams((prev: URLSearchParams) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
newParams.set('page', '1');
|
||||
return newParams;
|
||||
});
|
||||
}
|
||||
}, [filteredVideos, sortOption, shuffleSeed]);
|
||||
});
|
||||
|
||||
// Pagination logic
|
||||
const totalPages = Math.ceil(sortedVideos.length / itemsPerPage);
|
||||
@@ -377,30 +347,7 @@ const Home: React.FC = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSortClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setSortAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleSortClose = (option?: string) => {
|
||||
if (option) {
|
||||
setSearchParams((prev: URLSearchParams) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
newParams.set('page', '1');
|
||||
|
||||
if (option === 'random') {
|
||||
newParams.set('sort', 'random');
|
||||
// Always generate a new seed when clicking 'random'
|
||||
const newSeed = Math.floor(Math.random() * 1000000);
|
||||
newParams.set('seed', newSeed.toString());
|
||||
} else {
|
||||
newParams.set('sort', option);
|
||||
newParams.delete('seed');
|
||||
}
|
||||
return newParams;
|
||||
});
|
||||
}
|
||||
setSortAnchorEl(null);
|
||||
};
|
||||
|
||||
|
||||
// Regular home view (not in search mode)
|
||||
@@ -511,64 +458,12 @@ const Home: React.FC = () => {
|
||||
</ToggleButton>
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={handleSortClick}
|
||||
size="small"
|
||||
sx={{
|
||||
minWidth: 'auto',
|
||||
px: { xs: 1, md: 2 },
|
||||
color: 'text.secondary',
|
||||
borderColor: 'text.secondary'
|
||||
}}
|
||||
>
|
||||
<Sort fontSize="small" sx={{ mr: { xs: 0, md: 1 } }} />
|
||||
<Box component="span" sx={{ display: { xs: 'none', md: 'block' } }}>
|
||||
{t('sort')}
|
||||
</Box>
|
||||
</Button>
|
||||
<Menu
|
||||
anchorEl={sortAnchorEl}
|
||||
open={Boolean(sortAnchorEl)}
|
||||
onClose={() => handleSortClose()}
|
||||
>
|
||||
<MenuItem onClick={() => handleSortClose('dateDesc')} selected={sortOption === 'dateDesc'}>
|
||||
<ListItemIcon>
|
||||
<AccessTime fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dateDesc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleSortClose('dateAsc')} selected={sortOption === 'dateAsc'}>
|
||||
<ListItemIcon>
|
||||
<AccessTime fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('dateAsc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleSortClose('viewsDesc')} selected={sortOption === 'viewsDesc'}>
|
||||
<ListItemIcon>
|
||||
<Visibility fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('viewsDesc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleSortClose('viewsAsc')} selected={sortOption === 'viewsAsc'}>
|
||||
<ListItemIcon>
|
||||
<Visibility fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('viewsAsc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleSortClose('nameAsc')} selected={sortOption === 'nameAsc'}>
|
||||
<ListItemIcon>
|
||||
<SortByAlpha fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('nameAsc')}</ListItemText>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => handleSortClose('random')} selected={sortOption === 'random'}>
|
||||
<ListItemIcon>
|
||||
<Shuffle fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{t('random')}</ListItemText>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
<SortControl
|
||||
sortOption={sortOption}
|
||||
sortAnchorEl={sortAnchorEl}
|
||||
onSortClick={handleSortClick}
|
||||
onSortClose={handleSortClose}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
{viewMode === 'collections' && displayedVideos.length === 0 ? (
|
||||
|
||||
@@ -12,8 +12,9 @@ import {
|
||||
Grid,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import SortControl from '../components/SortControl';
|
||||
import VideoCard from '../components/VideoCard';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useDownload } from '../contexts/DownloadContext';
|
||||
@@ -36,9 +37,17 @@ const SearchPage: React.FC = () => {
|
||||
} = useVideo();
|
||||
const { collections } = useCollection();
|
||||
const { handleVideoSubmit } = useDownload();
|
||||
const [searchParams] = useSearchParams();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const [downloadingId, setDownloadingId] = useState<string | null>(null);
|
||||
|
||||
// Initialize sort option from URL or default
|
||||
const sortOptionP = searchParams.get('sort') || 'dateDesc';
|
||||
const seedP = parseInt(searchParams.get('seed') || '0', 10);
|
||||
|
||||
const [sortOption, setSortOption] = useState<string>(sortOptionP);
|
||||
const [shuffleSeed, setShuffleSeed] = useState<number>(seedP);
|
||||
const [sortAnchorEl, setSortAnchorEl] = useState<null | HTMLElement>(null);
|
||||
|
||||
const query = searchParams.get('q');
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,6 +56,37 @@ const SearchPage: React.FC = () => {
|
||||
}
|
||||
}, [query, contextSearchTerm, handleSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentSort = searchParams.get('sort') || 'dateDesc';
|
||||
const currentSeed = parseInt(searchParams.get('seed') || '0', 10);
|
||||
setSortOption(currentSort);
|
||||
setShuffleSeed(currentSeed);
|
||||
}, [searchParams]);
|
||||
|
||||
const handleSortClick = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setSortAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleSortClose = (option?: string) => {
|
||||
if (option) {
|
||||
setSearchParams((prev: URLSearchParams) => {
|
||||
const newParams = new URLSearchParams(prev);
|
||||
|
||||
if (option === 'random') {
|
||||
newParams.set('sort', 'random');
|
||||
// Always generate a new seed when clicking 'random'
|
||||
const newSeed = Math.floor(Math.random() * 1000000);
|
||||
newParams.set('seed', newSeed.toString());
|
||||
} else {
|
||||
newParams.set('sort', option);
|
||||
newParams.delete('seed');
|
||||
}
|
||||
return newParams;
|
||||
});
|
||||
}
|
||||
setSortAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleDownload = async (videoId: string, url: string) => {
|
||||
try {
|
||||
setDownloadingId(videoId);
|
||||
@@ -70,6 +110,39 @@ const SearchPage: React.FC = () => {
|
||||
const hasLocalResults = localSearchResults && localSearchResults.length > 0;
|
||||
const hasYouTubeResults = searchResults && searchResults.length > 0;
|
||||
|
||||
const sortedLocalSearchResults = useMemo(() => {
|
||||
if (!localSearchResults) return [];
|
||||
const result = [...localSearchResults];
|
||||
switch (sortOption) {
|
||||
case 'dateDesc':
|
||||
return result.sort((a, b) => new Date(b.addedAt).getTime() - new Date(a.addedAt).getTime());
|
||||
case 'dateAsc':
|
||||
return result.sort((a, b) => new Date(a.addedAt).getTime() - new Date(b.addedAt).getTime());
|
||||
case 'viewsDesc':
|
||||
return result.sort((a, b) => (b.viewCount || 0) - (a.viewCount || 0));
|
||||
case 'viewsAsc':
|
||||
return result.sort((a, b) => (a.viewCount || 0) - (b.viewCount || 0));
|
||||
case 'nameAsc':
|
||||
return result.sort((a, b) => a.title.localeCompare(b.title));
|
||||
case 'random':
|
||||
// Use a seeded predictable random sort
|
||||
return result.map(v => {
|
||||
// Simple hash function for stability with seed
|
||||
let h = 0x811c9dc5;
|
||||
const s = v.id + shuffleSeed;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return { v, score: h >>> 0 };
|
||||
})
|
||||
.sort((a, b) => a.score - b.score)
|
||||
.map(item => item.v);
|
||||
default:
|
||||
return result;
|
||||
}
|
||||
}, [localSearchResults, sortOption, shuffleSeed]);
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl" sx={{ py: 4 }}>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 4 }}>
|
||||
@@ -80,12 +153,24 @@ const SearchPage: React.FC = () => {
|
||||
|
||||
{/* Local Video Results */}
|
||||
<Box sx={{ mb: 6 }}>
|
||||
<Typography variant="h5" sx={{ mb: 3, fontWeight: 600, color: 'primary.main' }}>
|
||||
{t('fromYourLibrary')}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
|
||||
<Typography variant="h5" sx={{ fontWeight: 600, color: 'primary.main' }}>
|
||||
{t('fromYourLibrary')}
|
||||
</Typography>
|
||||
|
||||
{hasLocalResults && (
|
||||
<SortControl
|
||||
sortOption={sortOption}
|
||||
sortAnchorEl={sortAnchorEl}
|
||||
onSortClick={handleSortClick}
|
||||
onSortClose={handleSortClose}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{hasLocalResults ? (
|
||||
<Grid container spacing={3}>
|
||||
{localSearchResults.map((video) => (
|
||||
{sortedLocalSearchResults.map((video) => (
|
||||
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={video.id}>
|
||||
<VideoCard
|
||||
video={video}
|
||||
|
||||
Reference in New Issue
Block a user