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 = () => {