From b2e9df8ab8337e4c1a761f96dfaeb8a24c171281 Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Fri, 17 Apr 2026 23:55:33 -0400 Subject: [PATCH 1/4] add gameview navigation --- src/app/library/[id]/page.tsx | 37 +++++++++++++--------- src/components/games/GameDetailModal.tsx | 28 ++++++++++++++-- src/components/games/GamesView.tsx | 17 ++++++++-- src/components/mixed/MixedView.tsx | 17 ++++++++-- src/components/movies/MovieDetailModal.tsx | 2 +- src/components/movies/MoviesView.tsx | 4 ++- src/components/tv/TvView.tsx | 31 ++++++++++++++++-- 7 files changed, 108 insertions(+), 28 deletions(-) diff --git a/src/app/library/[id]/page.tsx b/src/app/library/[id]/page.tsx index 3ae6ffa..3ba1d1c 100644 --- a/src/app/library/[id]/page.tsx +++ b/src/app/library/[id]/page.tsx @@ -30,23 +30,30 @@ export default async function LibraryPage({ params, searchParams }: Props) { return (
-
- - Libraries - - / - - {library.name} - - {session.role === 'admin' && ( -
- -
- )} -
+ {library.type !== 'mixed' && ( +
+ + Libraries + + / + + {library.name} + + {session.role === 'admin' && ( +
+ +
+ )} +
+ )} + {library.type === 'mixed' && session.role === 'admin' && ( +
+ +
+ )} {library.type === 'games' && } - {library.type === 'mixed' && } + {library.type === 'mixed' && } {library.type === 'movies' && } {library.type === 'tv' && }
diff --git a/src/components/games/GameDetailModal.tsx b/src/components/games/GameDetailModal.tsx index 1c582ae..0dbafd7 100644 --- a/src/components/games/GameDetailModal.tsx +++ b/src/components/games/GameDetailModal.tsx @@ -30,12 +30,14 @@ interface Props { game: Game libraryId: string onClose: () => void + onPrev?: () => void + onNext?: () => void onTagsChanged?: () => void onCoverUploaded?: () => void onDeleted?: (gameId: string) => void } -export default function GameDetailModal({ game, libraryId, onClose, onTagsChanged, onCoverUploaded, onDeleted }: Props) { +export default function GameDetailModal({ game, libraryId, onClose, onPrev, onNext, onTagsChanged, onCoverUploaded, onDeleted }: Props) { const overlayRef = useRef(null) const menuRef = useRef(null) const screenshotInputRef = useRef(null) @@ -178,7 +180,7 @@ export default function GameDetailModal({ game, libraryId, onClose, onTagsChange {/* ── Left pane — relative container for floating controls ── */}
onClose()}> {/* Scrollable card area */} -
+
+ + {/* Prev / Next */} + {onPrev && ( + + )} + {onNext && ( + + )}
{/* ── Tag panel — bottom half on mobile, right sidebar on desktop ── */} diff --git a/src/components/games/GamesView.tsx b/src/components/games/GamesView.tsx index a278d1b..d5ec092 100644 --- a/src/components/games/GamesView.tsx +++ b/src/components/games/GamesView.tsx @@ -72,7 +72,10 @@ export default function GamesView({ libraryId }: Props) { const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) - const [showFilters, setShowFilters] = useState(true) + const [showFilters, setShowFilters] = useState( + () => typeof window !== 'undefined' && window.innerWidth >= 768 + ) + const [selectedGameIndex, setSelectedGameIndex] = useState(null) const toggleTag = (tagId: string) => setSelectedTagIds((prev) => { @@ -147,6 +150,7 @@ export default function GamesView({ libraryId }: Props) { }) const filtersActive = search !== '' || selectedTagIds.size > 0 + const filteredGames = filtered.filter((i): i is Game => !('games' in i)) return ( <> @@ -220,7 +224,7 @@ export default function GamesView({ libraryId }: Props) { setSelected(item)} + onClick={() => { setSelected(item); setSelectedGameIndex(filteredGames.indexOf(item)) }} /> ) )} @@ -231,11 +235,18 @@ export default function GamesView({ libraryId }: Props) { setSelected(null)} + onClose={() => { setSelected(null); setSelectedGameIndex(null) }} + onPrev={selectedGameIndex !== null && selectedGameIndex > 0 + ? () => { const g = filteredGames[selectedGameIndex - 1]; setSelected(g); setSelectedGameIndex(selectedGameIndex - 1) } + : undefined} + onNext={selectedGameIndex !== null && selectedGameIndex < filteredGames.length - 1 + ? () => { const g = filteredGames[selectedGameIndex + 1]; setSelected(g); setSelectedGameIndex(selectedGameIndex + 1) } + : undefined} onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments() }} onCoverUploaded={() => fetchGames(true)} onDeleted={() => { setSelected(null) + setSelectedGameIndex(null) fetchGames() fetchAssignments() }} diff --git a/src/components/mixed/MixedView.tsx b/src/components/mixed/MixedView.tsx index a543a14..4fd7c4b 100644 --- a/src/components/mixed/MixedView.tsx +++ b/src/components/mixed/MixedView.tsx @@ -11,6 +11,7 @@ import { isBrowserPlayable } from '@/lib/browser-media' interface Props { libraryId: string + libraryName: string initialPath: string } @@ -21,7 +22,7 @@ type ModalState = type TagPanelState = { entry: FileEntry; itemKey: string } | null -export default function MixedView({ libraryId, initialPath }: Props) { +export default function MixedView({ libraryId, libraryName, initialPath }: Props) { const [currentPath, setCurrentPath] = useState(initialPath) const [listing, setListing] = useState(null) const [loading, setLoading] = useState(true) @@ -33,7 +34,9 @@ export default function MixedView({ libraryId, initialPath }: Props) { const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) - const [showFilters, setShowFilters] = useState(true) + const [showFilters, setShowFilters] = useState( + () => typeof window !== 'undefined' && window.innerWidth >= 768 + ) const [recursiveEntries, setRecursiveEntries] = useState([]) const [recursiveLoading, setRecursiveLoading] = useState(false) const [recursiveLoaded, setRecursiveLoaded] = useState(false) @@ -339,12 +342,20 @@ export default function MixedView({ libraryId, initialPath }: Props) {
{/* Breadcrumb */}