From 421b418bd6c90d9daa8079765a02ca2161b656b4 Mon Sep 17 00:00:00 2001 From: Peifan Li Date: Fri, 5 Dec 2025 09:17:06 -0500 Subject: [PATCH] feat: Add SearchPage component and route --- frontend/src/App.tsx | 2 + frontend/src/components/Header.tsx | 22 +--- frontend/src/pages/Home.tsx | 170 ++----------------------- frontend/src/pages/ManagePage.tsx | 9 -- frontend/src/pages/SearchPage.tsx | 184 ++++++++++++++++++++++++++++ frontend/src/pages/SettingsPage.tsx | 10 -- frontend/src/pages/VideoPlayer.tsx | 20 ++- 7 files changed, 218 insertions(+), 199 deletions(-) create mode 100644 frontend/src/pages/SearchPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a98e01..6a0ffb2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,6 +18,7 @@ import Home from './pages/Home'; import InstructionPage from './pages/InstructionPage'; import LoginPage from './pages/LoginPage'; import ManagePage from './pages/ManagePage'; +import SearchPage from './pages/SearchPage'; import SettingsPage from './pages/SettingsPage'; import SubscriptionsPage from './pages/SubscriptionsPage'; import VideoPlayer from './pages/VideoPlayer'; @@ -107,6 +108,7 @@ function AppContent() { } /> + } /> } /> } /> } /> diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx index 6063ad9..1cfb4bc 100644 --- a/frontend/src/components/Header.tsx +++ b/frontend/src/components/Header.tsx @@ -137,26 +137,16 @@ const Header: React.FC = ({ setVideoUrl(''); setMobileMenuOpen(false); } else if (result.isSearchTerm) { - const searchResult = await onSearch(videoUrl); - if (searchResult.success) { - setVideoUrl(''); - setMobileMenuOpen(false); - navigate('/'); - } else { - setError(searchResult.error); - } + setVideoUrl(''); + setMobileMenuOpen(false); + navigate(`/search?q=${encodeURIComponent(videoUrl)}`); } else { setError(result.error); } } else { - const result = await onSearch(videoUrl); - if (result.success) { - setVideoUrl(''); - setMobileMenuOpen(false); - navigate('/'); - } else { - setError(result.error); - } + setVideoUrl(''); + setMobileMenuOpen(false); + navigate(`/search?q=${encodeURIComponent(videoUrl)}`); } } catch (err) { setError(t('unexpectedErrorOccurred')); diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 10edec8..eaa1f80 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,14 +1,9 @@ -import { ArrowBack, Collections as CollectionsIcon, Download, GridView, OndemandVideo, ViewSidebar, YouTube } from '@mui/icons-material'; + +import { Collections as CollectionsIcon, GridView, ViewSidebar } from '@mui/icons-material'; import { Alert, Box, Button, - Card, - CardActions, - CardContent, - CardMedia, - Chip, - CircularProgress, Collapse, Container, @@ -26,7 +21,6 @@ import Collections from '../components/Collections'; import TagsList from '../components/TagsList'; import VideoCard from '../components/VideoCard'; import { useCollection } from '../contexts/CollectionContext'; -import { useDownload } from '../contexts/DownloadContext'; import { useLanguage } from '../contexts/LanguageContext'; import { useVideo } from '../contexts/VideoContext'; @@ -38,20 +32,12 @@ const Home: React.FC = () => { videos, loading, error, - deleteVideo, availableTags, selectedTags, - handleTagToggle, - isSearchMode, - searchTerm, - localSearchResults, - searchResults, - youtubeLoading, - setIsSearchMode, - resetSearch + handleTagToggle } = useVideo(); const { collections } = useCollection(); - const { handleVideoSubmit } = useDownload(); + const [page, setPage] = useState(1); const ITEMS_PER_PAGE = 12; @@ -112,19 +98,12 @@ const Home: React.FC = () => { setPage(1); }, [videos, collections, selectedTags]); - const handleDownload = async (url: string) => { - try { - setIsSearchMode(false); - await handleVideoSubmit(url); - } catch (error) { - console.error('Error downloading from search:', error); - } - }; + // Add default empty array to ensure videos is always an array const videoArray = Array.isArray(videos) ? videos : []; - if (!settingsLoaded || (loading && videoArray.length === 0 && !isSearchMode)) { + if (!settingsLoaded || (loading && videoArray.length === 0)) { return ( @@ -133,7 +112,7 @@ const Home: React.FC = () => { ); } - if (error && videoArray.length === 0 && !isSearchMode) { + if (error && videoArray.length === 0) { return ( {error} @@ -196,141 +175,6 @@ const Home: React.FC = () => { window.scrollTo({ top: 0, behavior: 'smooth' }); }; - // Helper function to format duration in seconds to MM:SS - const formatDuration = (seconds?: number) => { - if (!seconds) return ''; - const minutes = Math.floor(seconds / 60); - const remainingSeconds = Math.floor(seconds % 60); - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; - }; - - // Helper function to format view count - const formatViewCount = (count?: number) => { - if (!count) return '0'; - if (count < 1000) return count.toString(); - if (count < 1000000) return `${(count / 1000).toFixed(1)}K`; - return `${(count / 1000000).toFixed(1)}M`; - }; - - // If in search mode, show search results - if (isSearchMode) { - const hasLocalResults = localSearchResults && localSearchResults.length > 0; - const hasYouTubeResults = searchResults && searchResults.length > 0; - - return ( - - - - {t('searchResultsFor')} "{searchTerm}" - - {resetSearch && ( - - )} - - - {/* Local Video Results */} - - - {t('fromYourLibrary')} - - {hasLocalResults ? ( - - {localSearchResults.map((video) => ( - - - - ))} - - ) : ( - {t('noMatchingVideos')} - )} - - - {/* YouTube Search Results */} - - - {t('fromYouTube')} - - - {youtubeLoading ? ( - - - {t('loadingYouTubeResults')} - - ) : hasYouTubeResults ? ( - - {searchResults.map((result) => ( - - - - { - const target = e.target as HTMLImageElement; - target.onerror = null; - target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail'; - }} - /> - {result.duration && ( - - )} - - {result.source === 'bilibili' ? : } - - - - - {result.title} - - - {result.author} - - {result.viewCount && ( - - {formatViewCount(result.viewCount)} {t('views')} - - )} - - - - - - - ))} - - ) : ( - {t('noYouTubeResults')} - )} - - - ); - } - // Regular home view (not in search mode) return ( diff --git a/frontend/src/pages/ManagePage.tsx b/frontend/src/pages/ManagePage.tsx index d52998c..8e197ca 100644 --- a/frontend/src/pages/ManagePage.tsx +++ b/frontend/src/pages/ManagePage.tsx @@ -1,5 +1,4 @@ import { - ArrowBack, Check, Close, @@ -270,14 +269,6 @@ const ManagePage: React.FC = () => { > {scanMutation.isPending ? (t('scanning') || 'Scanning...') : (t('scanFiles') || 'Scan Files')} - diff --git a/frontend/src/pages/SearchPage.tsx b/frontend/src/pages/SearchPage.tsx new file mode 100644 index 0000000..3c608aa --- /dev/null +++ b/frontend/src/pages/SearchPage.tsx @@ -0,0 +1,184 @@ +import { Download, OndemandVideo, YouTube } from '@mui/icons-material'; +import { + Box, + Button, + Card, + CardActions, + CardContent, + CardMedia, + Chip, + CircularProgress, + Container, + Grid, + Typography +} from '@mui/material'; +import { useEffect } from 'react'; +import { useNavigate, useSearchParams } from 'react-router-dom'; +import VideoCard from '../components/VideoCard'; +import { useCollection } from '../contexts/CollectionContext'; +import { useDownload } from '../contexts/DownloadContext'; +import { useLanguage } from '../contexts/LanguageContext'; +import { useVideo } from '../contexts/VideoContext'; + +const SearchPage: React.FC = () => { + const { t } = useLanguage(); + const { + deleteVideo, + localSearchResults, + searchResults, + youtubeLoading, + handleSearch, + resetSearch, + searchTerm: contextSearchTerm + } = useVideo(); + const { collections } = useCollection(); + const { handleVideoSubmit } = useDownload(); + const [searchParams] = useSearchParams(); + const navigate = useNavigate(); + + const query = searchParams.get('q'); + + useEffect(() => { + if (query && query !== contextSearchTerm) { + handleSearch(query); + } + }, [query, contextSearchTerm, handleSearch]); + + const handleDownload = async (url: string) => { + try { + await handleVideoSubmit(url); + } catch (error) { + console.error('Error downloading from search:', error); + } + }; + + const handleBack = () => { + resetSearch(); + navigate('/'); + }; + + // Helper function to format duration in seconds to MM:SS + const formatDuration = (seconds?: number) => { + if (!seconds) return ''; + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + }; + + // Helper function to format view count + const formatViewCount = (count?: number) => { + if (!count) return '0'; + if (count < 1000) return count.toString(); + if (count < 1000000) return `${(count / 1000).toFixed(1)}K`; + return `${(count / 1000000).toFixed(1)}M`; + }; + + const hasLocalResults = localSearchResults && localSearchResults.length > 0; + const hasYouTubeResults = searchResults && searchResults.length > 0; + + return ( + + + + {t('searchResultsFor')} "{query}" + + + + {/* Local Video Results */} + + + {t('fromYourLibrary')} + + {hasLocalResults ? ( + + {localSearchResults.map((video) => ( + + + + ))} + + ) : ( + {t('noMatchingVideos')} + )} + + + {/* YouTube Search Results */} + + + {t('fromYouTube')} + + + {youtubeLoading ? ( + + + {t('loadingYouTubeResults')} + + ) : hasYouTubeResults ? ( + + {searchResults.map((result) => ( + + + + { + const target = e.target as HTMLImageElement; + target.onerror = null; + target.src = 'https://via.placeholder.com/480x360?text=No+Thumbnail'; + }} + /> + {result.duration && ( + + )} + + {result.source === 'bilibili' ? : } + + + + + {result.title} + + + {result.author} + + {result.viewCount && ( + + {formatViewCount(result.viewCount)} {t('views')} + + )} + + + + + + + ))} + + ) : ( + {t('noYouTubeResults')} + )} + + + ); +}; + +export default SearchPage; diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index f6260c9..2d4a060 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -1,5 +1,4 @@ import { - ArrowBack, CloudUpload, Save } from '@mui/icons-material'; @@ -27,7 +26,6 @@ import { import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import axios from 'axios'; import React, { useEffect, useState } from 'react'; -import { Link } from 'react-router-dom'; import ConfirmationModal from '../components/ConfirmationModal'; import { useDownload } from '../contexts/DownloadContext'; import { useLanguage } from '../contexts/LanguageContext'; @@ -277,14 +275,6 @@ const SettingsPage: React.FC = () => { {t('settings')} - diff --git a/frontend/src/pages/VideoPlayer.tsx b/frontend/src/pages/VideoPlayer.tsx index a14b7b4..2a360e9 100644 --- a/frontend/src/pages/VideoPlayer.tsx +++ b/frontend/src/pages/VideoPlayer.tsx @@ -147,6 +147,24 @@ const VideoPlayer: React.FC = () => { return `${year}-${month}-${day}`; }; + // Format duration (seconds or MM:SS) + const formatDuration = (duration: string | number | undefined) => { + if (!duration) return '00:00'; + + // If it's already a string with colon, assume it's formatted + if (typeof duration === 'string' && duration.includes(':')) { + return duration; + } + + // Otherwise treat as seconds + const seconds = typeof duration === 'string' ? parseInt(duration, 10) : duration; + if (isNaN(seconds)) return '00:00'; + + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`; + }; + // Handle navigation to author videos page const handleAuthorClick = () => { if (video) { @@ -499,7 +517,7 @@ const VideoPlayer: React.FC = () => { /> {relatedVideo.duration && (