feat: Add pagination and toggle for sidebar in Home page
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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 >
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ interface Settings {
|
||||
openListApiUrl: string;
|
||||
openListToken: string;
|
||||
cloudDrivePath: string;
|
||||
homeSidebarOpen?: boolean;
|
||||
}
|
||||
|
||||
const SettingsPage: React.FC = () => {
|
||||
|
||||
Reference in New Issue
Block a user