From 16d3152483bb6919357a860f4e6a8d5e95b64bbf Mon Sep 17 00:00:00 2001 From: Peifan Li Date: Tue, 9 Dec 2025 15:36:59 -0500 Subject: [PATCH] feat: Add sorting functionality for videos on Home page --- frontend/src/pages/Home.tsx | 259 +++++++++++++++++++++++-------- frontend/src/utils/locales/en.ts | 10 ++ 2 files changed, 202 insertions(+), 67 deletions(-) diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 2689229..d3890c6 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,5 +1,5 @@ -import { Collections as CollectionsIcon, GridView, History, ViewSidebar } from '@mui/icons-material'; +import { AccessTime, Collections as CollectionsIcon, GridView, History, Shuffle, Sort, SortByAlpha, ViewSidebar, Visibility } from '@mui/icons-material'; import { Alert, Box, @@ -8,13 +8,17 @@ import { Collapse, Container, Grid, + ListItemIcon, + ListItemText, + Menu, + MenuItem, Pagination, ToggleButton, ToggleButtonGroup, Typography } from '@mui/material'; import axios from 'axios'; -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useSearchParams } from 'react-router-dom'; import AuthorsList from '../components/AuthorsList'; import CollectionCard from '../components/CollectionCard'; @@ -48,6 +52,9 @@ const Home: React.FC = () => { const saved = localStorage.getItem('homeViewMode'); return (saved as 'collections' | 'all-videos' | 'history') || 'collections'; }); + const [sortOption, setSortOption] = useState('dateDesc'); + const [sortAnchorEl, setSortAnchorEl] = useState(null); + const [shuffleSeed, setShuffleSeed] = useState(0); const [isSidebarOpen, setIsSidebarOpen] = useState(true); const [settingsLoaded, setSettingsLoaded] = useState(false); @@ -135,6 +142,75 @@ const Home: React.FC = () => { // 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') { + return videoArray.filter(video => { + // In all-videos mode, only apply tag filtering + if (selectedTags.length > 0) { + const videoTags = video.tags || []; + return selectedTags.every(tag => videoTags.includes(tag)); + } + return true; + }); + } + + if (viewMode === 'history') { + return videoArray + .filter(video => video.lastPlayedAt) + .sort((a, b) => (b.lastPlayedAt || 0) - (a.lastPlayedAt || 0)); + } + + // Collections mode + return videoArray.filter(video => { + // In collections mode, show only first video from each collection + // Tag filtering + if (selectedTags.length > 0) { + const videoTags = video.tags || []; + const hasMatchingTag = selectedTags.every(tag => videoTags.includes(tag)); + if (!hasMatchingTag) return false; + } + + // If the video is not in any collection, show it + const videoCollections = collections.filter(collection => + collection.videos.includes(video.id) + ); + + if (videoCollections.length === 0) { + return false; + } + + // For each collection this video is in, check if it's the first video + return videoCollections.some(collection => { + // Get the first video ID in this collection + const firstVideoId = collection.videos[0]; + // Show this video if it's the first in at least one collection + return video.id === firstVideoId; + }); + }); + }, [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': + return result.sort(() => 0.5 - Math.random()); + default: + return result; + } + }, [filteredVideos, sortOption, shuffleSeed]); + if (!settingsLoaded || (loading && videoArray.length === 0)) { return ( @@ -153,45 +229,7 @@ const Home: React.FC = () => { } // Filter videos based on view mode - const filteredVideos = viewMode === 'all-videos' - ? videoArray.filter(video => { - // In all-videos mode, only apply tag filtering - if (selectedTags.length > 0) { - const videoTags = video.tags || []; - return selectedTags.every(tag => videoTags.includes(tag)); - } - return true; - }) - : viewMode === 'history' - ? videoArray - .filter(video => video.lastPlayedAt) - .sort((a, b) => (b.lastPlayedAt || 0) - (a.lastPlayedAt || 0)) - : videoArray.filter(video => { - // In collections mode, show only first video from each collection - // Tag filtering - if (selectedTags.length > 0) { - const videoTags = video.tags || []; - const hasMatchingTag = selectedTags.every(tag => videoTags.includes(tag)); - if (!hasMatchingTag) return false; - } - // If the video is not in any collection, show it - const videoCollections = collections.filter(collection => - collection.videos.includes(video.id) - ); - - if (videoCollections.length === 0) { - return false; - } - - // For each collection this video is in, check if it's the first video - return videoCollections.some(collection => { - // Get the first video ID in this collection - const firstVideoId = collection.videos[0]; - // Show this video if it's the first in at least one collection - return video.id === firstVideoId; - }); - }); const handleViewModeChange = (mode: 'collections' | 'all-videos' | 'history') => { setViewMode(mode); @@ -203,9 +241,38 @@ const Home: React.FC = () => { }); }; + const handleSortClick = (event: React.MouseEvent) => { + setSortAnchorEl(event.currentTarget); + }; + + const handleSortClose = (option?: string) => { + if (option) { + if (option === 'random') { + setShuffleSeed(prev => prev + 1); + } + + if (option !== sortOption) { + setSortOption(option); + setSearchParams((prev: URLSearchParams) => { + const newParams = new URLSearchParams(prev); + newParams.set('page', '1'); + return newParams; + }); + } else if (option === 'random') { + // Even if it matches, if it is random, we want to reset page to 1 because the order changed + setSearchParams((prev: URLSearchParams) => { + const newParams = new URLSearchParams(prev); + newParams.set('page', '1'); + return newParams; + }); + } + } + setSortAnchorEl(null); + }; + // Pagination logic - const totalPages = Math.ceil(filteredVideos.length / itemsPerPage); - const displayedVideos = filteredVideos.slice( + const totalPages = Math.ceil(sortedVideos.length / itemsPerPage); + const displayedVideos = sortedVideos.slice( (page - 1) * itemsPerPage, page * itemsPerPage ); @@ -300,31 +367,89 @@ const Home: React.FC = () => { }[viewMode]} - newMode && handleViewModeChange(newMode)} - size="small" - > - - - - {t('allVideos')} - - - - - - {t('collections')} - - - - - - {t('history')} - - - + + newMode && handleViewModeChange(newMode)} + size="small" + > + + + + {t('allVideos')} + + + + + + {t('collections')} + + + + + + {t('history')} + + + + + + handleSortClose()} + > + handleSortClose('dateDesc')} selected={sortOption === 'dateDesc'}> + + + + {t('dateDesc')} + + handleSortClose('dateAsc')} selected={sortOption === 'dateAsc'}> + + + + {t('dateAsc')} + + handleSortClose('viewsDesc')} selected={sortOption === 'viewsDesc'}> + + + + {t('viewsDesc')} + + handleSortClose('viewsAsc')} selected={sortOption === 'viewsAsc'}> + + + + {t('viewsAsc')} + + handleSortClose('nameAsc')} selected={sortOption === 'nameAsc'}> + + + + {t('nameAsc')} + + handleSortClose('random')} selected={sortOption === 'random'}> + + + + {t('random')} + + + {viewMode === 'collections' && displayedVideos.length === 0 ? ( diff --git a/frontend/src/utils/locales/en.ts b/frontend/src/utils/locales/en.ts index f027292..98a3603 100644 --- a/frontend/src/utils/locales/en.ts +++ b/frontend/src/utils/locales/en.ts @@ -397,4 +397,14 @@ export const en = { videoSkippedDeleted: "Video was previously deleted, skipped download", downloading: "Downloading...", poweredBy: "Powered by MyTube", + + // Sorting + sort: "Sort", + sortBy: "Sort by", + dateDesc: "Date Added (Newest)", + dateAsc: "Date Added (Oldest)", + viewsDesc: "Views (High to Low)", + viewsAsc: "Views (Low to High)", + nameAsc: "Name (A-Z)", + random: "Random Shuffle", };