feat: Add pagination and toggle for sidebar in Home page

This commit is contained in:
Peifan Li
2025-12-01 16:46:56 -05:00
parent 62aedcd5b2
commit 15bd9f4339
4 changed files with 211 additions and 85 deletions

View File

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

View File

@@ -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,8 +370,11 @@ const DownloadPage: React.FC = () => {
{queuedDownloads.length === 0 ? (
<Typography color="textSecondary">{t('noQueuedDownloads') || 'No queued downloads'}</Typography>
) : (
<>
<List>
{queuedDownloads.map((download) => (
{queuedDownloads
.slice((queuePage - 1) * ITEMS_PER_PAGE, queuePage * ITEMS_PER_PAGE)
.map((download) => (
<Paper key={download.id} sx={{ mb: 2, p: 2 }}>
<ListItem
disableGutters
@@ -385,6 +392,17 @@ const DownloadPage: React.FC = () => {
</Paper>
))}
</List>
{queuedDownloads.length > ITEMS_PER_PAGE && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
<Pagination
count={Math.ceil(queuedDownloads.length / ITEMS_PER_PAGE)}
page={queuePage}
onChange={(_: React.ChangeEvent<unknown>, page: number) => setQueuePage(page)}
color="primary"
/>
</Box>
)}
</>
)}
</CustomTabPanel>
@@ -403,8 +421,11 @@ const DownloadPage: React.FC = () => {
{history.length === 0 ? (
<Typography color="textSecondary">{t('noDownloadHistory') || 'No download history'}</Typography>
) : (
<>
<List>
{history.map((item: DownloadHistoryItem) => (
{history
.slice((historyPage - 1) * ITEMS_PER_PAGE, historyPage * ITEMS_PER_PAGE)
.map((item: DownloadHistoryItem) => (
<Paper key={item.id} sx={{ mb: 2, p: 2 }}>
<ListItem
disableGutters
@@ -446,6 +467,17 @@ const DownloadPage: React.FC = () => {
</Paper>
))}
</List>
{history.length > ITEMS_PER_PAGE && (
<Box sx={{ display: 'flex', justifyContent: 'center', mt: 2 }}>
<Pagination
count={Math.ceil(history.length / ITEMS_PER_PAGE)}
page={historyPage}
onChange={(_: React.ChangeEvent<unknown>, page: number) => setHistoryPage(page)}
color="primary"
/>
</Box>
)}
</>
)}
</CustomTabPanel>

View File

@@ -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,10 +338,30 @@ const Home: React.FC = () => {
</Typography>
</Box>
) : (
<Grid container spacing={4}>
<Box sx={{ display: 'flex', alignItems: 'stretch' }}>
{/* Sidebar container for Collections, Authors, and Tags */}
<Grid size={{ xs: 12, md: 3 }} sx={{ display: { xs: 'none', md: 'block' } }}>
<Box sx={{ position: 'sticky', top: 80 }}>
<Box sx={{ display: { xs: 'none', md: 'block' } }}>
<Collapse in={isSidebarOpen} orientation="horizontal" timeout={300} sx={{ height: '100%', '& .MuiCollapse-wrapper': { height: '100%' }, '& .MuiCollapse-wrapperInner': { height: '100%' } }}>
<Box sx={{ width: 280, mr: 4, flexShrink: 0, height: '100%', position: 'relative' }}>
<Box sx={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
<Box sx={{
position: 'sticky',
maxHeight: 'calc(100% - 80px)',
overflowY: 'auto',
'&::-webkit-scrollbar': {
width: '6px',
},
'&::-webkit-scrollbar-track': {
background: 'transparent',
},
'&::-webkit-scrollbar-thumb': {
background: 'rgba(0,0,0,0.1)',
borderRadius: '3px',
},
'&:hover::-webkit-scrollbar-thumb': {
background: 'rgba(0,0,0,0.2)',
},
}}>
<Collections collections={collections} />
<Box sx={{ mt: 2 }}>
<TagsList
@@ -305,13 +374,29 @@ const Home: React.FC = () => {
<AuthorsList videos={videoArray} />
</Box>
</Box>
</Grid>
</Box>
</Box>
</Collapse>
</Box>
{/* Videos grid */}
<Grid size={{ xs: 12, md: 9 }}>
<Box sx={{ flex: 1, minWidth: 0 }}>
{/* View mode toggle */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mb: 3 }}>
<Typography variant="h5" fontWeight="bold">
<Typography variant="h5" fontWeight="bold" sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Button
onClick={handleSidebarToggle}
variant="outlined"
sx={{
minWidth: 'auto',
p: 1,
display: { xs: 'none', md: 'inline-flex' },
color: 'text.primary',
borderColor: 'text.primary',
}}
>
<ViewSidebar sx={{ transform: 'rotate(180deg)' }} />
</Button>
{t('videos')}
</Typography>
<ToggleButtonGroup
@@ -332,10 +417,14 @@ const Home: React.FC = () => {
</Box>
<Grid container spacing={3}>
{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 (
<Grid size={{ xs: 12, sm: 6, lg: 4, xl: 3 }} key={video.id}>
<Grid size={gridProps} key={video.id}>
<VideoCard
video={video}
collections={collections}
@@ -351,7 +440,7 @@ const Home: React.FC = () => {
// If it is, render CollectionCard
if (collection) {
return (
<Grid size={{ xs: 12, sm: 6, lg: 4, xl: 3 }} key={`collection-${collection.id}`}>
<Grid size={gridProps} key={`collection-${collection.id}`}>
<CollectionCard
collection={collection}
videos={videoArray}
@@ -362,7 +451,7 @@ const Home: React.FC = () => {
// Otherwise render VideoCard for non-collection videos
return (
<Grid size={{ xs: 12, sm: 6, lg: 4, xl: 3 }} key={video.id}>
<Grid size={gridProps} key={video.id}>
<VideoCard
video={video}
collections={collections}
@@ -372,6 +461,8 @@ const Home: React.FC = () => {
})}
</Grid>
{totalPages > 1 && (
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center' }}>
<Pagination
@@ -385,10 +476,10 @@ const Home: React.FC = () => {
/>
</Box>
)}
</Grid>
</Grid>
</Box>
</Box >
)}
</Container>
</Container >
);
};

View File

@@ -48,6 +48,7 @@ interface Settings {
openListApiUrl: string;
openListToken: string;
cloudDrivePath: string;
homeSidebarOpen?: boolean;
}
const SettingsPage: React.FC = () => {