diff --git a/src/app/api/nfo-refresh/route.ts b/src/app/api/nfo-refresh/route.ts index 576f4a1..dba57f3 100644 --- a/src/app/api/nfo-refresh/route.ts +++ b/src/app/api/nfo-refresh/route.ts @@ -120,7 +120,46 @@ export async function POST(request: NextRequest) { status: nfo.status ?? null, }), }) - return NextResponse.json({ updated: true, title: nfo.title, year: nfo.year }) + + // Optionally also refresh every episode NFO in this series + let episodesUpdated = 0 + const includeEpisodes = searchParams.get('includeEpisodes') === 'true' + if (includeEpisodes) { + type EpRow = { item_key: string; file_path: string | null; metadata: string | null } + const episodeRows = db + .prepare(`SELECT item_key, file_path, metadata FROM media_items WHERE item_type = 'tv_episode' AND item_key LIKE ?`) + .all(`${libraryId}:tv_episode:${encodedDirName}:%`) as EpRow[] + + const updateEp = db.prepare(` + UPDATE media_items SET title = @title, plot = @plot, metadata = @metadata WHERE item_key = @item_key + `) + + db.transaction(() => { + for (const ep of episodeRows) { + if (!ep.file_path) continue + const epDir = path.join(libraryRoot, path.dirname(ep.file_path)) + const baseName = path.basename(ep.file_path, path.extname(ep.file_path)) + const epNfo = parseEpisodeNfo(path.join(epDir, `${baseName}.nfo`)) + if (!epNfo) continue + const epMeta = ep.metadata ? JSON.parse(ep.metadata) : {} + updateEp.run({ + item_key: ep.item_key, + title: epNfo.title ?? null, + plot: epNfo.plot ?? null, + metadata: JSON.stringify({ + ...epMeta, + episodeNumber: epNfo.episode ?? epMeta.episodeNumber ?? null, + seasonNumber: epNfo.season ?? epMeta.seasonNumber ?? null, + aired: epNfo.aired ?? null, + rating: epNfo.rating ?? null, + }), + }) + episodesUpdated++ + } + })() + } + + return NextResponse.json({ updated: true, title: nfo.title, year: nfo.year, episodesUpdated }) } if (itemType === 'tv_episode') { 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..24e7921 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,9 @@ export default function GamesView({ libraryId }: Props) { }) const filtersActive = search !== '' || selectedTagIds.size > 0 + const filteredGames: Game[] = filtered.flatMap((item) => + 'games' in item ? item.games : [item as Game] + ) return ( <> @@ -220,7 +226,7 @@ export default function GamesView({ libraryId }: Props) { setSelected(item)} + onClick={() => { setSelected(item); setSelectedGameIndex(filteredGames.indexOf(item)) }} /> ) )} @@ -231,11 +237,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 */}