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] 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 */}