diff --git a/backend/src/controllers/settingsController.ts b/backend/src/controllers/settingsController.ts index 65429e5..da4376e 100644 --- a/backend/src/controllers/settingsController.ts +++ b/backend/src/controllers/settingsController.ts @@ -18,6 +18,7 @@ interface Settings { openListApiUrl?: string; openListToken?: string; cloudDrivePath?: string; + homeSidebarOpen?: boolean; } const defaultSettings: Settings = { @@ -30,7 +31,8 @@ const defaultSettings: Settings = { cloudDriveEnabled: false, openListApiUrl: '', openListToken: '', - cloudDrivePath: '' + cloudDrivePath: '', + homeSidebarOpen: true }; export const getSettings = async (_req: Request, res: Response) => { diff --git a/frontend/src/pages/DownloadPage.tsx b/frontend/src/pages/DownloadPage.tsx index 8bd1fd5..cda0e65 100644 --- a/frontend/src/pages/DownloadPage.tsx +++ b/frontend/src/pages/DownloadPage.tsx @@ -17,6 +17,7 @@ import { List, ListItem, ListItemText, + Pagination, Paper, Tab, Tabs, @@ -33,6 +34,7 @@ import { useLanguage } from '../contexts/LanguageContext'; import { useSnackbar } from '../contexts/SnackbarContext'; const API_URL = import.meta.env.VITE_API_URL; +const ITEMS_PER_PAGE = 20; interface DownloadHistoryItem { id: string; @@ -82,6 +84,8 @@ const DownloadPage: React.FC = () => { const [showBatchModal, setShowBatchModal] = useState(false); const [uploadModalOpen, setUploadModalOpen] = useState(false); const [showScanConfirmModal, setShowScanConfirmModal] = useState(false); + const [queuePage, setQueuePage] = useState(1); + const [historyPage, setHistoryPage] = useState(1); // Scan files mutation const scanMutation = useMutation({ @@ -366,25 +370,39 @@ const DownloadPage: React.FC = () => { {queuedDownloads.length === 0 ? ( {t('noQueuedDownloads') || 'No queued downloads'} ) : ( - - {queuedDownloads.map((download) => ( - - handleRemoveFromQueue(download.id)}> - - - } - > - - - - ))} - + <> + + {queuedDownloads + .slice((queuePage - 1) * ITEMS_PER_PAGE, queuePage * ITEMS_PER_PAGE) + .map((download) => ( + + handleRemoveFromQueue(download.id)}> + + + } + > + + + + ))} + + {queuedDownloads.length > ITEMS_PER_PAGE && ( + + , page: number) => setQueuePage(page)} + color="primary" + /> + + )} + )} @@ -403,49 +421,63 @@ const DownloadPage: React.FC = () => { {history.length === 0 ? ( {t('noDownloadHistory') || 'No download history'} ) : ( - - {history.map((item: DownloadHistoryItem) => ( - - handleRemoveFromHistory(item.id)}> - - - } - > - - {item.sourceUrl && ( - - {item.sourceUrl} - - )} - - {formatDate(item.finishedAt)} - - {item.error && ( - - {item.error} - + <> + + {history + .slice((historyPage - 1) * ITEMS_PER_PAGE, historyPage * ITEMS_PER_PAGE) + .map((item: DownloadHistoryItem) => ( + + handleRemoveFromHistory(item.id)}> + + + } + > + + {item.sourceUrl && ( + + {item.sourceUrl} + + )} + + {formatDate(item.finishedAt)} + + {item.error && ( + + {item.error} + + )} + + } + /> + + {item.status === 'success' ? ( + } label={t('success') || 'Success'} color="success" size="small" /> + ) : ( + } label={t('failed') || 'Failed'} color="error" size="small" /> )} - } - /> - - {item.status === 'success' ? ( - } label={t('success') || 'Success'} color="success" size="small" /> - ) : ( - } label={t('failed') || 'Failed'} color="error" size="small" /> - )} - - - - ))} - + + + ))} + + {history.length > ITEMS_PER_PAGE && ( + + , page: number) => setHistoryPage(page)} + color="primary" + /> + + )} + )} diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 26bf3f1..1166473 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import { ArrowBack, Collections as CollectionsIcon, Download, GridView, OndemandVideo, YouTube } from '@mui/icons-material'; +import { ArrowBack, Collections as CollectionsIcon, Download, GridView, OndemandVideo, ViewSidebar, YouTube } from '@mui/icons-material'; import { Alert, Box, @@ -8,7 +8,9 @@ import { CardContent, CardMedia, Chip, + CircularProgress, + Collapse, Container, Grid, Pagination, @@ -16,6 +18,7 @@ import { ToggleButtonGroup, Typography } from '@mui/material'; +import axios from 'axios'; import { useEffect, useState } from 'react'; import AuthorsList from '../components/AuthorsList'; import CollectionCard from '../components/CollectionCard'; @@ -27,6 +30,8 @@ import { useDownload } from '../contexts/DownloadContext'; import { useLanguage } from '../contexts/LanguageContext'; import { useVideo } from '../contexts/VideoContext'; +const API_URL = import.meta.env.VITE_API_URL; + const Home: React.FC = () => { const { t } = useLanguage(); const { @@ -54,6 +59,50 @@ const Home: React.FC = () => { const saved = localStorage.getItem('homeViewMode'); return (saved as 'collections' | 'all-videos') || 'collections'; }); + const [isSidebarOpen, setIsSidebarOpen] = useState(true); + + // Fetch settings on mount + useEffect(() => { + const fetchSettings = async () => { + try { + const response = await axios.get(`${API_URL}/settings`); + if (response.data && typeof response.data.homeSidebarOpen !== 'undefined') { + setIsSidebarOpen(response.data.homeSidebarOpen); + } + } catch (error) { + console.error('Failed to fetch settings:', error); + } + }; + fetchSettings(); + }, []); + + const handleSidebarToggle = async () => { + const newState = !isSidebarOpen; + setIsSidebarOpen(newState); + try { + // We need to fetch current settings first to not overwrite other settings + // Or better, the backend should support partial updates, but the current controller + // implementation replaces the whole object or merges with defaults. + // Let's fetch first to be safe, similar to how SettingsPage does it, + // but for a simple toggle, we might want a lighter endpoint. + // However, given the current backend structure, we'll fetch then save. + // Actually, the backend `updateSettings` merges with `defaultSettings` but expects the full object + // in `req.body` to be the new state. + // Wait, looking at `settingsController.ts`: `const newSettings: Settings = req.body;` + // and `storageService.saveSettings(newSettings);` + // It seems it REPLACES the settings with what's sent. + // So we MUST fetch existing settings first. + + const response = await axios.get(`${API_URL}/settings`); + const currentSettings = response.data; + await axios.post(`${API_URL}/settings`, { + ...currentSettings, + homeSidebarOpen: newState + }); + } catch (error) { + console.error('Failed to save sidebar state:', error); + } + }; // Reset page when filters change useEffect(() => { @@ -289,29 +338,65 @@ const Home: React.FC = () => { ) : ( - + {/* Sidebar container for Collections, Authors, and Tags */} - - - - - + + + + + + + + + + + + + + - - - - - + + {/* Videos grid */} - + {/* View mode toggle */} - + + {t('videos')} { {displayedVideos.map(video => { + const gridProps = isSidebarOpen + ? { xs: 12, sm: 6, lg: 4, xl: 3 } + : { xs: 12, sm: 6, md: 4, lg: 3, xl: 2 }; + // In all-videos mode, ALWAYS render as VideoCard if (viewMode === 'all-videos') { return ( - + { // If it is, render CollectionCard if (collection) { return ( - + { // Otherwise render VideoCard for non-collection videos return ( - + { })} + + {totalPages > 1 && ( { /> )} - - + + )} - + ); }; diff --git a/frontend/src/pages/SettingsPage.tsx b/frontend/src/pages/SettingsPage.tsx index 9b92022..2962612 100644 --- a/frontend/src/pages/SettingsPage.tsx +++ b/frontend/src/pages/SettingsPage.tsx @@ -48,6 +48,7 @@ interface Settings { openListApiUrl: string; openListToken: string; cloudDrivePath: string; + homeSidebarOpen?: boolean; } const SettingsPage: React.FC = () => {