feat: Introduce AuthProvider for authentication
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import { Box, CssBaseline, ThemeProvider } from '@mui/material';
|
||||
import axios from 'axios';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import './App.css';
|
||||
@@ -7,6 +6,7 @@ import AnimatedRoutes from './components/AnimatedRoutes';
|
||||
import BilibiliPartsModal from './components/BilibiliPartsModal';
|
||||
import Footer from './components/Footer';
|
||||
import Header from './components/Header';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { CollectionProvider, useCollection } from './contexts/CollectionContext';
|
||||
import { DownloadProvider, useDownload } from './contexts/DownloadContext';
|
||||
import { LanguageProvider } from './contexts/LanguageContext';
|
||||
@@ -15,31 +15,17 @@ import { VideoProvider, useVideo } from './contexts/VideoContext';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import getTheme from './theme';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
function AppContent() {
|
||||
const {
|
||||
videos,
|
||||
loading,
|
||||
error,
|
||||
deleteVideo,
|
||||
isSearchMode,
|
||||
searchTerm,
|
||||
searchResults,
|
||||
localSearchResults,
|
||||
youtubeLoading,
|
||||
handleSearch,
|
||||
resetSearch,
|
||||
setIsSearchMode
|
||||
resetSearch
|
||||
} = useVideo();
|
||||
|
||||
const {
|
||||
collections,
|
||||
createCollection,
|
||||
addToCollection,
|
||||
removeFromCollection,
|
||||
deleteCollection
|
||||
} = useCollection();
|
||||
const { collections } = useCollection();
|
||||
|
||||
const {
|
||||
activeDownloads,
|
||||
@@ -53,16 +39,13 @@ function AppContent() {
|
||||
handleDownloadCurrentBilibiliPart
|
||||
} = useDownload();
|
||||
|
||||
const { isAuthenticated, loginRequired, checkingAuth } = useAuth();
|
||||
|
||||
// Theme state
|
||||
const [themeMode, setThemeMode] = useState<'light' | 'dark'>(() => {
|
||||
return (localStorage.getItem('theme') as 'light' | 'dark') || 'dark';
|
||||
});
|
||||
|
||||
// Login state
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [loginRequired, setLoginRequired] = useState<boolean>(true); // Assume required until checked
|
||||
const [checkingAuth, setCheckingAuth] = useState<boolean>(true);
|
||||
|
||||
const theme = useMemo(() => getTheme(themeMode), [themeMode]);
|
||||
|
||||
// Apply theme to body
|
||||
@@ -75,58 +58,6 @@ function AppContent() {
|
||||
setThemeMode(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
// Check login settings and authentication status
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
// Check if login is enabled in settings
|
||||
const response = await axios.get(`${API_URL}/settings`);
|
||||
const { loginEnabled, isPasswordSet } = response.data;
|
||||
|
||||
// Login is required only if enabled AND a password is set
|
||||
if (!loginEnabled || !isPasswordSet) {
|
||||
setLoginRequired(false);
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setLoginRequired(true);
|
||||
// Check if already authenticated in this session
|
||||
const sessionAuth = sessionStorage.getItem('mytube_authenticated');
|
||||
if (sessionAuth === 'true') {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error checking auth settings:', error);
|
||||
// If error, default to requiring login for security, but maybe allow if backend is down?
|
||||
// Better to fail safe.
|
||||
} finally {
|
||||
setCheckingAuth(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
const handleLoginSuccess = () => {
|
||||
setIsAuthenticated(true);
|
||||
sessionStorage.setItem('mytube_authenticated', 'true');
|
||||
};
|
||||
|
||||
const handleDownloadFromSearch = async (videoUrl: string) => {
|
||||
try {
|
||||
// We need to stop the search mode
|
||||
setIsSearchMode(false);
|
||||
|
||||
const result = await handleVideoSubmit(videoUrl);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Error in handleDownloadFromSearch:', error);
|
||||
return { success: false, error: 'Failed to download video' };
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
@@ -134,7 +65,7 @@ function AppContent() {
|
||||
checkingAuth ? (
|
||||
<div className="loading">Loading...</div>
|
||||
) : (
|
||||
<LoginPage onLoginSuccess={handleLoginSuccess} />
|
||||
<LoginPage />
|
||||
)
|
||||
) : (
|
||||
<Router>
|
||||
@@ -165,25 +96,8 @@ function AppContent() {
|
||||
type={bilibiliPartsInfo.type}
|
||||
/>
|
||||
|
||||
<Box component="main" sx={{ flex: 1, display: 'flex', flexDirection: 'column', width: '100%', px: { xs: 1, md: 2, lg: 4 } }}>
|
||||
<AnimatedRoutes
|
||||
videos={videos}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onDeleteVideo={deleteVideo}
|
||||
collections={collections}
|
||||
isSearchMode={isSearchMode}
|
||||
searchTerm={searchTerm}
|
||||
localSearchResults={localSearchResults}
|
||||
youtubeLoading={youtubeLoading}
|
||||
searchResults={searchResults}
|
||||
onDownload={handleDownloadFromSearch}
|
||||
onResetSearch={resetSearch}
|
||||
onAddToCollection={addToCollection}
|
||||
onCreateCollection={createCollection}
|
||||
onRemoveFromCollection={removeFromCollection}
|
||||
onDeleteCollection={deleteCollection}
|
||||
/>
|
||||
<Box component="main" sx={{ flexGrow: 1, p: 0, width: '100%', overflowX: 'hidden' }}>
|
||||
<AnimatedRoutes />
|
||||
</Box>
|
||||
|
||||
<Footer />
|
||||
@@ -203,13 +117,15 @@ function App() {
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<LanguageProvider>
|
||||
<SnackbarProvider>
|
||||
<VideoProvider>
|
||||
<CollectionProvider>
|
||||
<DownloadProvider>
|
||||
<AppContent />
|
||||
</DownloadProvider>
|
||||
</CollectionProvider>
|
||||
</VideoProvider>
|
||||
<AuthProvider>
|
||||
<VideoProvider>
|
||||
<CollectionProvider>
|
||||
<DownloadProvider>
|
||||
<AppContent />
|
||||
</DownloadProvider>
|
||||
</CollectionProvider>
|
||||
</VideoProvider>
|
||||
</AuthProvider>
|
||||
</SnackbarProvider>
|
||||
</LanguageProvider>
|
||||
</QueryClientProvider>
|
||||
|
||||
@@ -1,53 +1,17 @@
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
import { Navigate, Route, Routes, useLocation } from 'react-router-dom';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import React from 'react';
|
||||
import { Route, Routes, useLocation } from 'react-router-dom';
|
||||
import AuthorVideos from '../pages/AuthorVideos';
|
||||
import CollectionPage from '../pages/CollectionPage';
|
||||
import DownloadPage from '../pages/DownloadPage';
|
||||
import Home from '../pages/Home';
|
||||
import LoginPage from '../pages/LoginPage';
|
||||
import ManagePage from '../pages/ManagePage';
|
||||
import SearchResults from '../pages/SearchResults';
|
||||
import SettingsPage from '../pages/SettingsPage';
|
||||
import VideoPlayer from '../pages/VideoPlayer';
|
||||
import { Collection, Video } from '../types';
|
||||
import PageTransition from './PageTransition';
|
||||
|
||||
interface AnimatedRoutesProps {
|
||||
videos: Video[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onDeleteVideo: (id: string) => Promise<{ success: boolean; error?: string }>;
|
||||
collections: Collection[];
|
||||
isSearchMode: boolean;
|
||||
searchTerm: string;
|
||||
localSearchResults: Video[];
|
||||
youtubeLoading: boolean;
|
||||
searchResults: any[];
|
||||
onDownload: (videoUrl: string) => Promise<any>;
|
||||
onResetSearch: () => void;
|
||||
onAddToCollection: (collectionId: string, videoId: string) => Promise<any>;
|
||||
onCreateCollection: (name: string, videoId: string) => Promise<any>;
|
||||
onRemoveFromCollection: (videoId: string) => Promise<boolean>;
|
||||
onDeleteCollection: (collectionId: string, deleteVideos?: boolean) => Promise<{ success: boolean; error?: string }>;
|
||||
}
|
||||
|
||||
const AnimatedRoutes = ({
|
||||
videos,
|
||||
loading,
|
||||
error,
|
||||
onDeleteVideo,
|
||||
collections,
|
||||
isSearchMode,
|
||||
searchTerm,
|
||||
localSearchResults,
|
||||
youtubeLoading,
|
||||
searchResults,
|
||||
onDownload,
|
||||
onResetSearch,
|
||||
onAddToCollection,
|
||||
onCreateCollection,
|
||||
onRemoveFromCollection,
|
||||
onDeleteCollection
|
||||
}: AnimatedRoutesProps) => {
|
||||
const AnimatedRoutes: React.FC = () => {
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
@@ -56,115 +20,120 @@ const AnimatedRoutes = ({
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<PageTransition>
|
||||
<Home
|
||||
videos={videos}
|
||||
loading={loading}
|
||||
error={error}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
collections={collections}
|
||||
isSearchMode={isSearchMode}
|
||||
searchTerm={searchTerm}
|
||||
localSearchResults={localSearchResults}
|
||||
youtubeLoading={youtubeLoading}
|
||||
searchResults={searchResults}
|
||||
onDownload={onDownload}
|
||||
onResetSearch={onResetSearch}
|
||||
/>
|
||||
</PageTransition>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/video/:id"
|
||||
element={
|
||||
<PageTransition>
|
||||
<VideoPlayer
|
||||
videos={videos}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
collections={collections}
|
||||
onAddToCollection={onAddToCollection}
|
||||
onCreateCollection={onCreateCollection}
|
||||
onRemoveFromCollection={onRemoveFromCollection}
|
||||
/>
|
||||
</PageTransition>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/author/:author"
|
||||
element={
|
||||
<PageTransition>
|
||||
<AuthorVideos
|
||||
videos={videos}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
collections={collections}
|
||||
/>
|
||||
</PageTransition>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: 20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<Home />
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/collection/:id"
|
||||
element={
|
||||
<PageTransition>
|
||||
<CollectionPage
|
||||
collections={collections}
|
||||
videos={videos}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
onDeleteCollection={onDeleteCollection}
|
||||
/>
|
||||
</PageTransition>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<CollectionPage />
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/search"
|
||||
path="/video/:id"
|
||||
element={
|
||||
<PageTransition>
|
||||
<SearchResults
|
||||
results={searchResults}
|
||||
localResults={localSearchResults}
|
||||
youtubeLoading={youtubeLoading}
|
||||
loading={loading}
|
||||
onDownload={onDownload}
|
||||
onResetSearch={onResetSearch}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
collections={collections}
|
||||
searchTerm={searchTerm}
|
||||
/>
|
||||
</PageTransition>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<VideoPlayer />
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/manage"
|
||||
path="/author/:authorName"
|
||||
element={
|
||||
<PageTransition>
|
||||
<ManagePage
|
||||
videos={videos}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
collections={collections}
|
||||
onDeleteCollection={onDeleteCollection}
|
||||
/>
|
||||
</PageTransition>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: 20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<AuthorVideos />
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/downloads"
|
||||
element={
|
||||
<PageTransition>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<DownloadPage />
|
||||
</PageTransition>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/settings"
|
||||
element={
|
||||
<PageTransition>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<SettingsPage />
|
||||
</PageTransition>
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/manage"
|
||||
element={
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<ManagePage />
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/search"
|
||||
element={
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<SearchResults />
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/login"
|
||||
element={
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{ duration: 0.3 }}
|
||||
>
|
||||
<LoginPage />
|
||||
</motion.div>
|
||||
}
|
||||
/>
|
||||
{/* Redirect /login to home if already authenticated (or login disabled) */}
|
||||
<Route path="/login" element={<Navigate to="/" replace />} />
|
||||
{/* Catch all - redirect to home */}
|
||||
<Route path="*" element={<Navigate to="/" replace />} />
|
||||
</Routes>
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
@@ -28,7 +28,7 @@ const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
interface VideoCardProps {
|
||||
video: Video;
|
||||
collections?: Collection[];
|
||||
onDeleteVideo?: (id: string) => Promise<void>;
|
||||
onDeleteVideo?: (id: string) => Promise<any>;
|
||||
showDeleteButton?: boolean;
|
||||
disableCollectionGrouping?: boolean;
|
||||
}
|
||||
|
||||
77
frontend/src/contexts/AuthContext.tsx
Normal file
77
frontend/src/contexts/AuthContext.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import React, { createContext, useContext, useState } from 'react';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
interface AuthContextType {
|
||||
isAuthenticated: boolean;
|
||||
loginRequired: boolean;
|
||||
checkingAuth: boolean;
|
||||
login: () => void;
|
||||
logout: () => void;
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||
|
||||
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
|
||||
const [loginRequired, setLoginRequired] = useState<boolean>(true); // Assume required until checked
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Check login settings and authentication status
|
||||
const { isLoading: checkingAuth } = useQuery({
|
||||
queryKey: ['authSettings'],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
// Check if login is enabled in settings
|
||||
const response = await axios.get(`${API_URL}/settings`);
|
||||
const { loginEnabled, isPasswordSet } = response.data;
|
||||
|
||||
// Login is required only if enabled AND a password is set
|
||||
if (!loginEnabled || !isPasswordSet) {
|
||||
setLoginRequired(false);
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setLoginRequired(true);
|
||||
// Check if already authenticated in this session
|
||||
const sessionAuth = sessionStorage.getItem('mytube_authenticated');
|
||||
if (sessionAuth === 'true') {
|
||||
setIsAuthenticated(true);
|
||||
} else {
|
||||
setIsAuthenticated(false);
|
||||
}
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error('Error checking auth settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const login = () => {
|
||||
setIsAuthenticated(true);
|
||||
sessionStorage.setItem('mytube_authenticated', 'true');
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setIsAuthenticated(false);
|
||||
sessionStorage.removeItem('mytube_authenticated');
|
||||
queryClient.invalidateQueries({ queryKey: ['authSettings'] });
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isAuthenticated, loginRequired, checkingAuth, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (context === undefined) {
|
||||
throw new Error('useAuth must be used within an AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -12,28 +12,20 @@ import {
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import VideoCard from '../components/VideoCard';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useVideo } from '../contexts/VideoContext';
|
||||
import { Collection, Video } from '../types';
|
||||
import { Video } from '../types';
|
||||
|
||||
interface AuthorVideosProps {
|
||||
videos?: Video[]; // Make optional since we can get from context
|
||||
onDeleteVideo: (id: string) => Promise<any>;
|
||||
collections: Collection[];
|
||||
}
|
||||
|
||||
const AuthorVideos: React.FC<AuthorVideosProps> = ({ videos: propVideos, onDeleteVideo, collections = [] }) => {
|
||||
const AuthorVideos: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { author } = useParams<{ author: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { videos: contextVideos, loading: contextLoading } = useVideo();
|
||||
const { videos, loading, deleteVideo } = useVideo();
|
||||
const { collections } = useCollection();
|
||||
|
||||
const [authorVideos, setAuthorVideos] = useState<Video[]>([]);
|
||||
|
||||
// Use prop videos if available, otherwise context videos
|
||||
const videos = propVideos && propVideos.length > 0 ? propVideos : contextVideos;
|
||||
const loading = (propVideos && propVideos.length > 0) ? false : contextLoading;
|
||||
|
||||
useEffect(() => {
|
||||
if (!author) return;
|
||||
|
||||
@@ -112,7 +104,7 @@ const AuthorVideos: React.FC<AuthorVideosProps> = ({ videos: propVideos, onDelet
|
||||
<VideoCard
|
||||
video={video}
|
||||
collections={collections}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
onDeleteVideo={deleteVideo}
|
||||
showDeleteButton={true}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -4,59 +4,34 @@ import {
|
||||
Avatar,
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
Container,
|
||||
Grid,
|
||||
Pagination,
|
||||
Typography
|
||||
} from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import DeleteCollectionModal from '../components/DeleteCollectionModal';
|
||||
import VideoCard from '../components/VideoCard';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { Collection, Video } from '../types';
|
||||
import { useVideo } from '../contexts/VideoContext';
|
||||
|
||||
interface CollectionPageProps {
|
||||
collections: Collection[];
|
||||
videos: Video[];
|
||||
onDeleteVideo: (id: string) => Promise<any>;
|
||||
onDeleteCollection: (id: string, deleteVideos: boolean) => Promise<any>;
|
||||
}
|
||||
|
||||
const CollectionPage: React.FC<CollectionPageProps> = ({ collections, videos, onDeleteVideo, onDeleteCollection }) => {
|
||||
const CollectionPage: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const [collection, setCollection] = useState<Collection | null>(null);
|
||||
const [collectionVideos, setCollectionVideos] = useState<Video[]>([]);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const { collections, deleteCollection } = useCollection();
|
||||
const { videos, deleteVideo } = useVideo();
|
||||
|
||||
const [showDeleteModal, setShowDeleteModal] = useState<boolean>(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const ITEMS_PER_PAGE = 12;
|
||||
|
||||
useEffect(() => {
|
||||
if (collections && collections.length > 0 && id) {
|
||||
const foundCollection = collections.find(c => c.id === id);
|
||||
|
||||
if (foundCollection) {
|
||||
setCollection(foundCollection);
|
||||
|
||||
// Find all videos that are in this collection
|
||||
const videosInCollection = videos.filter(video =>
|
||||
foundCollection.videos.includes(video.id)
|
||||
);
|
||||
|
||||
setCollectionVideos(videosInCollection);
|
||||
} else {
|
||||
// Collection not found, redirect to home
|
||||
navigate('/');
|
||||
}
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
setLoading(false);
|
||||
}, [id, collections, videos, navigate]);
|
||||
const collection = collections.find(c => c.id === id);
|
||||
const collectionVideos = collection
|
||||
? videos.filter(video => collection.videos.includes(video.id))
|
||||
: [];
|
||||
|
||||
// Pagination logic
|
||||
const totalPages = Math.ceil(collectionVideos.length / ITEMS_PER_PAGE);
|
||||
@@ -80,8 +55,8 @@ const CollectionPage: React.FC<CollectionPageProps> = ({ collections, videos, on
|
||||
|
||||
const handleDeleteCollectionOnly = async () => {
|
||||
if (!id) return;
|
||||
const success = await onDeleteCollection(id, false);
|
||||
if (success) {
|
||||
const result = await deleteCollection(id, false);
|
||||
if (result.success) {
|
||||
navigate('/');
|
||||
}
|
||||
setShowDeleteModal(false);
|
||||
@@ -89,26 +64,25 @@ const CollectionPage: React.FC<CollectionPageProps> = ({ collections, videos, on
|
||||
|
||||
const handleDeleteCollectionAndVideos = async () => {
|
||||
if (!id) return;
|
||||
const success = await onDeleteCollection(id, true);
|
||||
if (success) {
|
||||
const result = await deleteCollection(id, true);
|
||||
if (result.success) {
|
||||
navigate('/');
|
||||
}
|
||||
setShowDeleteModal(false);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>{t('loadingCollection')}</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (!collection) {
|
||||
return (
|
||||
<Container sx={{ mt: 4 }}>
|
||||
<Alert severity="error">{t('collectionNotFound')}</Alert>
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ArrowBack />}
|
||||
onClick={handleBack}
|
||||
sx={{ mt: 2 }}
|
||||
>
|
||||
{t('back')}
|
||||
</Button>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
@@ -148,7 +122,7 @@ const CollectionPage: React.FC<CollectionPageProps> = ({ collections, videos, on
|
||||
<VideoCard
|
||||
video={video}
|
||||
collections={collections}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
onDeleteVideo={deleteVideo}
|
||||
showDeleteButton={true}
|
||||
disableCollectionGrouping={true}
|
||||
/>
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
LinearProgress,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Paper,
|
||||
Tab,
|
||||
@@ -206,7 +205,14 @@ const DownloadPage: React.FC = () => {
|
||||
<List>
|
||||
{activeDownloads.map((download) => (
|
||||
<Paper key={download.id} sx={{ mb: 2, p: 2 }}>
|
||||
<ListItem disableGutters>
|
||||
<ListItem
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
<IconButton edge="end" aria-label="cancel" onClick={() => handleCancelDownload(download.id)}>
|
||||
<CancelIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
primary={download.title}
|
||||
secondaryTypographyProps={{ component: 'div' }}
|
||||
@@ -219,11 +225,6 @@ const DownloadPage: React.FC = () => {
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="cancel" onClick={() => handleCancelDownload(download.id)}>
|
||||
<CancelIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</Paper>
|
||||
))}
|
||||
@@ -249,16 +250,18 @@ const DownloadPage: React.FC = () => {
|
||||
<List>
|
||||
{queuedDownloads.map((download) => (
|
||||
<Paper key={download.id} sx={{ mb: 2, p: 2 }}>
|
||||
<ListItem disableGutters>
|
||||
<ListItem
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
<IconButton edge="end" aria-label="remove" onClick={() => handleRemoveFromQueue(download.id)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
primary={download.title}
|
||||
secondary={t('queued') || 'Queued'}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="remove" onClick={() => handleRemoveFromQueue(download.id)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</Paper>
|
||||
))}
|
||||
@@ -284,7 +287,14 @@ const DownloadPage: React.FC = () => {
|
||||
<List>
|
||||
{history.map((item: DownloadHistoryItem) => (
|
||||
<Paper key={item.id} sx={{ mb: 2, p: 2 }}>
|
||||
<ListItem disableGutters>
|
||||
<ListItem
|
||||
disableGutters
|
||||
secondaryAction={
|
||||
<IconButton edge="end" aria-label="remove" onClick={() => handleRemoveFromHistory(item.id)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
secondaryTypographyProps={{ component: 'div' }}
|
||||
@@ -301,18 +311,13 @@ const DownloadPage: React.FC = () => {
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<Box sx={{ mr: 2 }}>
|
||||
<Box sx={{ mr: 8 }}>
|
||||
{item.status === 'success' ? (
|
||||
<Chip icon={<CheckCircleIcon />} label={t('success') || 'Success'} color="success" size="small" />
|
||||
) : (
|
||||
<Chip icon={<ErrorIcon />} label={t('failed') || 'Failed'} color="error" size="small" />
|
||||
)}
|
||||
</Box>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="remove" onClick={() => handleRemoveFromHistory(item.id)}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</Paper>
|
||||
))}
|
||||
|
||||
@@ -22,55 +22,34 @@ import CollectionCard from '../components/CollectionCard';
|
||||
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';
|
||||
import { Collection, Video } from '../types';
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
thumbnailUrl: string;
|
||||
duration?: number;
|
||||
viewCount?: number;
|
||||
source: 'youtube' | 'bilibili';
|
||||
sourceUrl: string;
|
||||
}
|
||||
|
||||
interface HomeProps {
|
||||
videos: Video[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
onDeleteVideo: (id: string) => Promise<any>;
|
||||
collections: Collection[];
|
||||
isSearchMode: boolean;
|
||||
searchTerm: string;
|
||||
localSearchResults: Video[];
|
||||
youtubeLoading: boolean;
|
||||
searchResults: SearchResult[];
|
||||
onDownload: (url: string, title?: string) => void;
|
||||
onResetSearch: () => void;
|
||||
}
|
||||
|
||||
const Home: React.FC<HomeProps> = ({
|
||||
videos = [],
|
||||
loading,
|
||||
error,
|
||||
onDeleteVideo,
|
||||
collections = [],
|
||||
isSearchMode = false,
|
||||
searchTerm = '',
|
||||
localSearchResults = [],
|
||||
youtubeLoading = false,
|
||||
searchResults = [],
|
||||
onDownload,
|
||||
onResetSearch
|
||||
}) => {
|
||||
const Home: React.FC = () => {
|
||||
const { t } = useLanguage();
|
||||
const {
|
||||
videos,
|
||||
loading,
|
||||
error,
|
||||
deleteVideo,
|
||||
availableTags,
|
||||
selectedTags,
|
||||
handleTagToggle,
|
||||
isSearchMode,
|
||||
searchTerm,
|
||||
localSearchResults,
|
||||
searchResults,
|
||||
youtubeLoading,
|
||||
setIsSearchMode,
|
||||
resetSearch
|
||||
} = useVideo();
|
||||
const { collections } = useCollection();
|
||||
const { handleVideoSubmit } = useDownload();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const ITEMS_PER_PAGE = 12;
|
||||
const { t } = useLanguage();
|
||||
const { availableTags, selectedTags, handleTagToggle } = useVideo();
|
||||
const [viewMode, setViewMode] = useState<'collections' | 'all-videos'>(() => {
|
||||
const saved = localStorage.getItem('homeViewMode');
|
||||
return (saved as 'collections' | 'all-videos') || 'collections';
|
||||
@@ -81,6 +60,14 @@ const Home: React.FC<HomeProps> = ({
|
||||
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 : [];
|
||||
@@ -139,8 +126,6 @@ const Home: React.FC<HomeProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
const handleViewModeChange = (mode: 'collections' | 'all-videos') => {
|
||||
setViewMode(mode);
|
||||
localStorage.setItem('homeViewMode', mode);
|
||||
@@ -159,8 +144,6 @@ const Home: React.FC<HomeProps> = ({
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Helper function to format duration in seconds to MM:SS
|
||||
const formatDuration = (seconds?: number) => {
|
||||
if (!seconds) return '';
|
||||
@@ -188,11 +171,11 @@ const Home: React.FC<HomeProps> = ({
|
||||
<Typography variant="h4" component="h1" fontWeight="bold">
|
||||
{t('searchResultsFor')} "{searchTerm}"
|
||||
</Typography>
|
||||
{onResetSearch && (
|
||||
{resetSearch && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
startIcon={<ArrowBack />}
|
||||
onClick={onResetSearch}
|
||||
onClick={resetSearch}
|
||||
>
|
||||
{t('backToHome')}
|
||||
</Button>
|
||||
@@ -211,7 +194,7 @@ const Home: React.FC<HomeProps> = ({
|
||||
<VideoCard
|
||||
video={video}
|
||||
collections={collections}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
onDeleteVideo={deleteVideo}
|
||||
showDeleteButton={true}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -279,7 +262,7 @@ const Home: React.FC<HomeProps> = ({
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<Download />}
|
||||
onClick={() => onDownload(result.sourceUrl, result.title)}
|
||||
onClick={() => handleDownload(result.sourceUrl)}
|
||||
>
|
||||
{t('download')}
|
||||
</Button>
|
||||
|
||||
@@ -13,19 +13,17 @@ import {
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import axios from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import getTheme from '../theme';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
|
||||
interface LoginPageProps {
|
||||
onLoginSuccess: () => void;
|
||||
}
|
||||
|
||||
const LoginPage: React.FC<LoginPageProps> = ({ onLoginSuccess }) => {
|
||||
const LoginPage: React.FC = () => {
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { t } = useLanguage();
|
||||
const { login } = useAuth();
|
||||
|
||||
// Use dark theme for login page to match app style
|
||||
const theme = getTheme('dark');
|
||||
@@ -37,7 +35,7 @@ const LoginPage: React.FC<LoginPageProps> = ({ onLoginSuccess }) => {
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
if (data.success) {
|
||||
onLoginSuccess();
|
||||
login();
|
||||
} else {
|
||||
setError(t('incorrectPassword'));
|
||||
}
|
||||
|
||||
@@ -33,23 +33,18 @@ import { useState } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import ConfirmationModal from '../components/ConfirmationModal';
|
||||
import DeleteCollectionModal from '../components/DeleteCollectionModal';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useVideo } from '../contexts/VideoContext';
|
||||
import { Collection, Video } from '../types';
|
||||
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
interface ManagePageProps {
|
||||
videos: Video[];
|
||||
onDeleteVideo: (id: string) => Promise<any>;
|
||||
collections: Collection[];
|
||||
onDeleteCollection: (id: string, deleteVideos: boolean) => Promise<any>;
|
||||
}
|
||||
|
||||
const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collections = [], onDeleteCollection }) => {
|
||||
const ManagePage: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState<string>('');
|
||||
const { t } = useLanguage();
|
||||
const { refreshThumbnail, updateVideo } = useVideo();
|
||||
const { videos, deleteVideo, refreshThumbnail, updateVideo } = useVideo();
|
||||
const { collections, deleteCollection } = useCollection();
|
||||
const [deletingId, setDeletingId] = useState<string | null>(null);
|
||||
const [refreshingId, setRefreshingId] = useState<string | null>(null);
|
||||
const [collectionToDelete, setCollectionToDelete] = useState<Collection | null>(null);
|
||||
@@ -98,7 +93,7 @@ const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collecti
|
||||
if (!videoToDelete) return;
|
||||
|
||||
setDeletingId(videoToDelete);
|
||||
await onDeleteVideo(videoToDelete);
|
||||
await deleteVideo(videoToDelete);
|
||||
setDeletingId(null);
|
||||
setVideoToDelete(null);
|
||||
setShowVideoDeleteModal(false); // Close the modal after deletion
|
||||
@@ -116,7 +111,7 @@ const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collecti
|
||||
const handleCollectionDeleteOnly = async () => {
|
||||
if (!collectionToDelete) return;
|
||||
setIsDeletingCollection(true);
|
||||
await onDeleteCollection(collectionToDelete.id, false);
|
||||
await deleteCollection(collectionToDelete.id, false);
|
||||
setIsDeletingCollection(false);
|
||||
setCollectionToDelete(null);
|
||||
};
|
||||
@@ -124,7 +119,7 @@ const ManagePage: React.FC<ManagePageProps> = ({ videos, onDeleteVideo, collecti
|
||||
const handleCollectionDeleteAll = async () => {
|
||||
if (!collectionToDelete) return;
|
||||
setIsDeletingCollection(true);
|
||||
await onDeleteCollection(collectionToDelete.id, true);
|
||||
await deleteCollection(collectionToDelete.id, true);
|
||||
setIsDeletingCollection(false);
|
||||
setCollectionToDelete(null);
|
||||
};
|
||||
|
||||
@@ -16,56 +16,45 @@ import {
|
||||
import React, { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import VideoCard from '../components/VideoCard';
|
||||
import { Collection, Video } from '../types';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useDownload } from '../contexts/DownloadContext';
|
||||
import { useVideo } from '../contexts/VideoContext';
|
||||
|
||||
interface SearchResult {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
thumbnailUrl: string;
|
||||
duration?: number;
|
||||
viewCount?: number;
|
||||
source: 'youtube' | 'bilibili';
|
||||
sourceUrl: string;
|
||||
}
|
||||
|
||||
interface SearchResultsProps {
|
||||
results: SearchResult[];
|
||||
localResults: Video[];
|
||||
searchTerm: string;
|
||||
loading: boolean;
|
||||
youtubeLoading: boolean;
|
||||
onDownload: (url: string, title?: string) => void;
|
||||
onDeleteVideo: (id: string) => Promise<any>;
|
||||
onResetSearch: () => void;
|
||||
collections: Collection[];
|
||||
}
|
||||
|
||||
const SearchResults: React.FC<SearchResultsProps> = ({
|
||||
results,
|
||||
localResults,
|
||||
searchTerm,
|
||||
loading,
|
||||
youtubeLoading,
|
||||
onDownload,
|
||||
onDeleteVideo,
|
||||
onResetSearch,
|
||||
collections = []
|
||||
}) => {
|
||||
const SearchResults: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
searchResults,
|
||||
localSearchResults,
|
||||
searchTerm,
|
||||
loading,
|
||||
youtubeLoading,
|
||||
deleteVideo,
|
||||
resetSearch,
|
||||
setIsSearchMode
|
||||
} = useVideo();
|
||||
const { collections } = useCollection();
|
||||
const { handleVideoSubmit } = useDownload();
|
||||
|
||||
// If search term is empty, reset search and go back to home
|
||||
useEffect(() => {
|
||||
if (!searchTerm || searchTerm.trim() === '') {
|
||||
if (onResetSearch) {
|
||||
onResetSearch();
|
||||
if (resetSearch) {
|
||||
resetSearch();
|
||||
}
|
||||
}
|
||||
}, [searchTerm, onResetSearch]);
|
||||
}, [searchTerm, resetSearch]);
|
||||
|
||||
const handleDownload = async (videoUrl: string, title: string) => {
|
||||
const handleDownload = async (videoUrl: string) => {
|
||||
try {
|
||||
await onDownload(videoUrl, title);
|
||||
// We need to stop the search mode before downloading?
|
||||
// Actually App.tsx implementation was:
|
||||
// setIsSearchMode(false);
|
||||
// await handleVideoSubmit(videoUrl);
|
||||
// Let's replicate that behavior if we want to exit search on download
|
||||
// Or maybe just download and stay on search results?
|
||||
// The original implementation in App.tsx exited search mode.
|
||||
setIsSearchMode(false);
|
||||
await handleVideoSubmit(videoUrl);
|
||||
} catch (error) {
|
||||
console.error('Error downloading from search results:', error);
|
||||
}
|
||||
@@ -73,8 +62,8 @@ const SearchResults: React.FC<SearchResultsProps> = ({
|
||||
|
||||
const handleBackClick = () => {
|
||||
// Call the onResetSearch function to reset search mode
|
||||
if (onResetSearch) {
|
||||
onResetSearch();
|
||||
if (resetSearch) {
|
||||
resetSearch();
|
||||
} else {
|
||||
// Fallback to navigate if onResetSearch is not provided
|
||||
navigate('/');
|
||||
@@ -96,8 +85,8 @@ const SearchResults: React.FC<SearchResultsProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const hasLocalResults = localResults && localResults.length > 0;
|
||||
const hasYouTubeResults = results && results.length > 0;
|
||||
const hasLocalResults = localSearchResults && localSearchResults.length > 0;
|
||||
const hasYouTubeResults = searchResults && searchResults.length > 0;
|
||||
const noResults = !hasLocalResults && !hasYouTubeResults && !youtubeLoading;
|
||||
|
||||
// Helper function to format duration in seconds to MM:SS
|
||||
@@ -158,11 +147,11 @@ const SearchResults: React.FC<SearchResultsProps> = ({
|
||||
</Typography>
|
||||
{hasLocalResults ? (
|
||||
<Grid container spacing={3}>
|
||||
{localResults.map((video) => <Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={video.id}>
|
||||
{localSearchResults.map((video) => <Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={video.id}>
|
||||
<VideoCard
|
||||
video={video}
|
||||
collections={collections}
|
||||
onDeleteVideo={onDeleteVideo}
|
||||
onDeleteVideo={deleteVideo}
|
||||
showDeleteButton={true}
|
||||
/>
|
||||
</Grid>
|
||||
@@ -186,7 +175,7 @@ const SearchResults: React.FC<SearchResultsProps> = ({
|
||||
</Box>
|
||||
) : hasYouTubeResults ? (
|
||||
<Grid container spacing={3}>
|
||||
{results.map((result) => <Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={result.id}>
|
||||
{searchResults.map((result) => <Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }} key={result.id}>
|
||||
<Card sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<Box sx={{ position: 'relative', paddingTop: '56.25%' }}>
|
||||
<CardMedia
|
||||
@@ -229,7 +218,7 @@ const SearchResults: React.FC<SearchResultsProps> = ({
|
||||
fullWidth
|
||||
variant="contained"
|
||||
startIcon={<Download />}
|
||||
onClick={() => handleDownload(result.sourceUrl, result.title)}
|
||||
onClick={() => handleDownload(result.sourceUrl)}
|
||||
>
|
||||
Download
|
||||
</Button>
|
||||
|
||||
@@ -20,36 +20,30 @@ import CollectionModal from '../components/VideoPlayer/CollectionModal';
|
||||
import CommentsSection from '../components/VideoPlayer/CommentsSection';
|
||||
import VideoControls from '../components/VideoPlayer/VideoControls';
|
||||
import VideoInfo from '../components/VideoPlayer/VideoInfo';
|
||||
import { useCollection } from '../contexts/CollectionContext';
|
||||
import { useLanguage } from '../contexts/LanguageContext';
|
||||
import { useSnackbar } from '../contexts/SnackbarContext';
|
||||
import { useVideo } from '../contexts/VideoContext';
|
||||
import { Collection, Video } from '../types';
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL;
|
||||
const BACKEND_URL = import.meta.env.VITE_BACKEND_URL;
|
||||
|
||||
interface VideoPlayerProps {
|
||||
videos: Video[];
|
||||
onDeleteVideo: (id: string) => Promise<{ success: boolean; error?: string }>;
|
||||
collections: Collection[];
|
||||
onAddToCollection: (collectionId: string, videoId: string) => Promise<void>;
|
||||
onCreateCollection: (name: string, videoId: string) => Promise<void>;
|
||||
onRemoveFromCollection: (videoId: string) => Promise<any>;
|
||||
}
|
||||
|
||||
const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
videos,
|
||||
onDeleteVideo,
|
||||
collections,
|
||||
onAddToCollection,
|
||||
onCreateCollection,
|
||||
onRemoveFromCollection
|
||||
}) => {
|
||||
const VideoPlayer: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useLanguage();
|
||||
const { showSnackbar } = useSnackbar();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { videos, deleteVideo } = useVideo();
|
||||
const {
|
||||
collections,
|
||||
addToCollection,
|
||||
createCollection,
|
||||
removeFromCollection
|
||||
} = useCollection();
|
||||
|
||||
const [showCollectionModal, setShowCollectionModal] = useState<boolean>(false);
|
||||
const [videoCollections, setVideoCollections] = useState<Collection[]>([]);
|
||||
const [showComments, setShowComments] = useState<boolean>(false);
|
||||
@@ -155,7 +149,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
// Delete mutation
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: async (videoId: string) => {
|
||||
return await onDeleteVideo(videoId);
|
||||
return await deleteVideo(videoId);
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
if (result.success) {
|
||||
@@ -192,7 +186,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const handleCreateCollection = async (name: string) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
await onCreateCollection(name, id);
|
||||
await createCollection(name, id);
|
||||
} catch (error) {
|
||||
console.error('Error creating collection:', error);
|
||||
}
|
||||
@@ -201,7 +195,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
const handleAddToExistingCollection = async (collectionId: string) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
await onAddToCollection(collectionId, id);
|
||||
await addToCollection(collectionId, id);
|
||||
} catch (error) {
|
||||
console.error('Error adding to collection:', error);
|
||||
}
|
||||
@@ -211,7 +205,7 @@ const VideoPlayer: React.FC<VideoPlayerProps> = ({
|
||||
if (!id) return;
|
||||
|
||||
try {
|
||||
await onRemoveFromCollection(id);
|
||||
await removeFromCollection(id);
|
||||
} catch (error) {
|
||||
console.error('Error removing from collection:', error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user