diff --git a/.gitignore b/.gitignore index 9b1f768..4d3aa8c 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,8 @@ notes.md labels.db # Test output directories -tests/*/output/ \ No newline at end of file +tests/*/output/ + +# Windows links +*.lnk +*.url \ No newline at end of file diff --git a/server/routes/labels.ts b/server/routes/labels.ts index b64d0e9..277e385 100644 --- a/server/routes/labels.ts +++ b/server/routes/labels.ts @@ -648,9 +648,10 @@ router.get('/page/:page', async (req, res) => { const filteredEntries = applyFilters(mergedEntries, { region, language, videoMode, search, ownedIds: filterOwnedIds }); const totalEntries = filteredEntries.length; - const totalPages = Math.ceil(totalEntries / pageSize); - const start = page * pageSize; - const end = Math.min(start + pageSize, totalEntries); + const effectivePageSize = pageSize === 0 ? totalEntries : pageSize; + const totalPages = Math.ceil(totalEntries / effectivePageSize); + const start = page * effectivePageSize; + const end = Math.min(start + effectivePageSize, totalEntries); const entries = filteredEntries.slice(start, end); res.json({ @@ -658,7 +659,7 @@ router.get('/page/:page', async (req, res) => { hasLabels: !!labelsEntries, hasOwnedCarts: allOwnedIds.length > 0, page, - pageSize, + pageSize: effectivePageSize, totalPages, totalEntries, totalUnfiltered: mergedEntries.length, diff --git a/src/components/LabelsBrowser.tsx b/src/components/LabelsBrowser.tsx index 33c25bd..f5a36f3 100644 --- a/src/components/LabelsBrowser.tsx +++ b/src/components/LabelsBrowser.tsx @@ -85,6 +85,10 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB const [languageFilter, setLanguageFilter] = useState(searchParams.get('language') || ''); const [videoModeFilter, setVideoModeFilter] = useState(searchParams.get('videoMode') || ''); const [ownedFilter, setOwnedFilter] = useState(searchParams.get('owned') === 'true'); + const [pageSize, setPageSize] = useState(() => { + const saved = localStorage.getItem('labelsPageSize'); + return saved ? parseInt(saved, 10) : 48; + }); // Modal states const [showImportModal, setShowImportModal] = useState(false); @@ -100,7 +104,6 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB // Unowned cartridges indicator const [unownedOnSDCount, setUnownedOnSDCount] = useState(0); - const pageSize = 48; const hasActiveFilters = regionFilter || languageFilter || videoModeFilter || searchQuery || ownedFilter; const hasClearableFilters = regionFilter || languageFilter || videoModeFilter || searchQuery; @@ -157,6 +160,7 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB videoMode?: string; search?: string; owned?: boolean; + pageSize?: number; } ) => { try { @@ -164,7 +168,7 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB setError(null); const params = new URLSearchParams(); - params.set('pageSize', pageSize.toString()); + params.set('pageSize', (options?.pageSize ?? pageSize).toString()); // Add filter parameters const region = options?.region ?? regionFilter; @@ -202,7 +206,7 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB } finally { setLoading(false); } - }, [regionFilter, languageFilter, videoModeFilter, searchQuery, ownedFilter]); + }, [regionFilter, languageFilter, videoModeFilter, searchQuery, ownedFilter, pageSize]); // Check for unowned cartridges on SD card const checkUnownedOnSD = useCallback(async () => { @@ -253,6 +257,14 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB fetchPage(newPage); }; + const handlePageSizeChange = (newPageSize: number) => { + localStorage.setItem('labelsPageSize', newPageSize.toString()); + setPageSize(newPageSize); + // Reset to first page when page size changes + updateURL(0, { search: searchQuery, region: regionFilter, language: languageFilter, videoMode: videoModeFilter, owned: ownedFilter }); + fetchPage(0, { pageSize: newPageSize }); + }; + const handleFilterChange = ( type: 'search' | 'region' | 'language' | 'videoMode' | 'owned', value: string | boolean @@ -418,6 +430,23 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB +
+ + +
+
@@ -607,7 +636,9 @@ export function LabelsBrowser({ onSelectLabel, refreshKey, sdCardPath }: LabelsB diff --git a/src/components/Pagination.css b/src/components/Pagination.css index 8a2cb76..e7bed14 100644 --- a/src/components/Pagination.css +++ b/src/components/Pagination.css @@ -7,6 +7,38 @@ padding: 1rem; } +.pagination-controls { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem; + background: var(--color-bg-secondary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); +} + +.pagination-controls label { + font-size: 0.875rem; + color: var(--color-text-muted); + white-space: nowrap; +} + +.page-size-select { + padding: 0.25rem 0.5rem; + background: var(--color-bg); + color: var(--color-text); + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + font-family: var(--font-body); + cursor: pointer; + min-width: 30px; +} + +.page-size-select:hover { + border-color: var(--color-accent); + color: var(--color-accent); +} + .pagination button { padding: 0.5rem 1rem; background: transparent; diff --git a/src/components/Pagination.tsx b/src/components/Pagination.tsx index e48824a..52f60ef 100644 --- a/src/components/Pagination.tsx +++ b/src/components/Pagination.tsx @@ -3,11 +3,13 @@ import './Pagination.css'; interface PaginationProps { page: number; totalPages: number; + pageSize: number; onPageChange: (page: number) => void; + onPageSizeChange: (pageSize: number) => void; disabled?: boolean; } -export function Pagination({ page, totalPages, onPageChange, disabled }: PaginationProps) { +export function Pagination({ page, totalPages, pageSize, onPageChange, onPageSizeChange, disabled }: PaginationProps) { if (totalPages <= 1) return null; return (