diff --git a/src/components/games/GamesView.tsx b/src/components/games/GamesView.tsx index 0b2a266..d4a0d04 100644 --- a/src/components/games/GamesView.tsx +++ b/src/components/games/GamesView.tsx @@ -21,6 +21,7 @@ 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 toggleTag = (tagId: string) => setSelectedTagIds((prev) => { @@ -83,20 +84,39 @@ export default function GamesView({ libraryId }: Props) { return true }) + const filtersActive = search !== '' || selectedTagIds.size > 0 + return ( -
-
- + <> +
+
-
+
+ {showFilters && ( +
+ +
+ )} +
{/* Breadcrumb when inside a series */} {selectedSeries && (
@@ -154,8 +174,9 @@ export default function GamesView({ libraryId }: Props) { onCoverUploaded={() => fetchGames(true)} /> )} +
-
+ ) } diff --git a/src/components/mixed/ImageLightbox.tsx b/src/components/mixed/ImageLightbox.tsx index 8cccd70..a474cc7 100644 --- a/src/components/mixed/ImageLightbox.tsx +++ b/src/components/mixed/ImageLightbox.tsx @@ -1,15 +1,21 @@ 'use client' -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' +import TagSelector from '@/components/tags/TagSelector' interface Props { url: string name: string onClose: () => void + mediaKey?: string + onTagsChanged?: () => void } -export default function ImageLightbox({ url, name, onClose }: Props) { +export default function ImageLightbox({ url, name, onClose, mediaKey, onTagsChanged }: Props) { const overlayRef = useRef(null) + const [showTags, setShowTags] = useState( + () => !!mediaKey && typeof window !== 'undefined' && window.innerWidth >= 1280 + ) useEffect(() => { const handleKey = (e: KeyboardEvent) => { @@ -30,35 +36,85 @@ export default function ImageLightbox({ url, name, onClose }: Props) { return (
{/* Toolbar */} -
+
{name} - +
+ {mediaKey && ( + + )} + +
- {/* Image */} - {/* eslint-disable-next-line @next/next/no-img-element */} - {name} e.stopPropagation()} - /> + {showTags ? ( +
+ {/* Image */} +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {name} e.stopPropagation()} + /> +
+ {/* Tag panel */} +
e.stopPropagation()} + > +

+ Tags +

+ +
+
+ ) : ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {name} e.stopPropagation()} + /> +
+ )}
) } diff --git a/src/components/mixed/MixedView.tsx b/src/components/mixed/MixedView.tsx index ee344a1..6ac4118 100644 --- a/src/components/mixed/MixedView.tsx +++ b/src/components/mixed/MixedView.tsx @@ -13,8 +13,8 @@ interface Props { } type ModalState = - | { type: 'video'; url: string; name: string } - | { type: 'image'; url: string; name: string } + | { type: 'video'; url: string; name: string; mediaKey: string } + | { type: 'image'; url: string; name: string; mediaKey: string } | null type TagPanelState = { entry: FileEntry; mediaKey: string } | null @@ -30,6 +30,7 @@ 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 toggleTag = (tagId: string) => setSelectedTagIds((prev) => { @@ -80,9 +81,9 @@ export default function MixedView({ libraryId, initialPath }: Props) { } if (!entry.url) return if (entry.mediaType === 'video') { - setModal({ type: 'video', url: entry.url, name: entry.name }) + setModal({ type: 'video', url: entry.url, name: entry.name, mediaKey: mediaKeyFor(entry) }) } else if (entry.mediaType === 'image') { - setModal({ type: 'image', url: entry.url, name: entry.name }) + setModal({ type: 'image', url: entry.url, name: entry.name, mediaKey: mediaKeyFor(entry) }) } else { // Download other file types window.open(entry.url, '_blank') @@ -120,20 +121,39 @@ export default function MixedView({ libraryId, initialPath }: Props) { return true }) + const filtersActive = search !== '' || selectedTagIds.size > 0 + return ( -
-
- + <> +
+
-
+
+ {showFilters && ( +
+ +
+ )} +
{/* Breadcrumb */}
)} +
-
+ ) } diff --git a/src/components/mixed/VideoPlayerModal.tsx b/src/components/mixed/VideoPlayerModal.tsx index 8098b86..a0476cc 100644 --- a/src/components/mixed/VideoPlayerModal.tsx +++ b/src/components/mixed/VideoPlayerModal.tsx @@ -1,15 +1,21 @@ 'use client' -import { useEffect, useRef } from 'react' +import { useEffect, useRef, useState } from 'react' +import TagSelector from '@/components/tags/TagSelector' interface Props { url: string name: string onClose: () => void + mediaKey?: string + onTagsChanged?: () => void } -export default function VideoPlayerModal({ url, name, onClose }: Props) { +export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsChanged }: Props) { const overlayRef = useRef(null) + const [showTags, setShowTags] = useState( + () => !!mediaKey && typeof window !== 'undefined' && window.innerWidth >= 1280 + ) useEffect(() => { const handleKey = (e: KeyboardEvent) => { @@ -30,36 +36,90 @@ export default function VideoPlayerModal({ url, name, onClose }: Props) { return (
{/* Toolbar */} -
+
{name} - +
+ {mediaKey && ( + + )} + +
- {/* Video */} -
) } diff --git a/src/components/movies/MovieDetailModal.tsx b/src/components/movies/MovieDetailModal.tsx index e111ff9..4411943 100644 --- a/src/components/movies/MovieDetailModal.tsx +++ b/src/components/movies/MovieDetailModal.tsx @@ -65,7 +65,15 @@ export default function MovieDetailModal({ movie, libraryId, onClose, onTagsChan } if (playing) { - return setPlaying(false)} /> + return ( + setPlaying(false)} + /> + ) } const heroUrl = movie.backdropUrl ?? movie.posterUrl diff --git a/src/components/movies/MoviesView.tsx b/src/components/movies/MoviesView.tsx index a19778b..27739eb 100644 --- a/src/components/movies/MoviesView.tsx +++ b/src/components/movies/MoviesView.tsx @@ -18,6 +18,7 @@ export default function MoviesView({ libraryId }: Props) { const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) + const [showFilters, setShowFilters] = useState(true) const toggleTag = (tagId: string) => setSelectedTagIds((prev) => { @@ -64,20 +65,39 @@ export default function MoviesView({ libraryId }: Props) { setMovies((prev) => prev.filter((m) => m.id !== movieId)) } + const filtersActive = search !== '' || selectedTagIds.size > 0 + return ( -
-
- + <> +
+
-
+
+ {showFilters && ( +
+ +
+ )} +
{loading ? ( ) : error ? ( @@ -148,8 +168,9 @@ export default function MoviesView({ libraryId }: Props) { onDeleted={handleDeleted} /> )} +
-
+ ) } diff --git a/src/components/tv/TvView.tsx b/src/components/tv/TvView.tsx index 58b5c04..3639cfc 100644 --- a/src/components/tv/TvView.tsx +++ b/src/components/tv/TvView.tsx @@ -26,6 +26,7 @@ export default function TvView({ libraryId }: Props) { const [selectedTagIds, setSelectedTagIds] = useState>(new Set()) const [assignments, setAssignments] = useState>({}) const [filterRefreshKey, setFilterRefreshKey] = useState(0) + const [showFilters, setShowFilters] = useState(true) const [menuOpen, setMenuOpen] = useState(false) const [confirming, setConfirming] = useState(false) const [deleting, setDeleting] = useState(false) @@ -123,6 +124,8 @@ export default function TvView({ libraryId }: Props) { .catch(() => setDeleting(false)) } + const filtersActive = search !== '' || selectedTagIds.size > 0 + const filteredSeries = series.filter((s) => { if (search && !s.title.toLowerCase().includes(search.toLowerCase())) return false if (selectedTagIds.size > 0) { @@ -179,19 +182,36 @@ export default function TvView({ libraryId }: Props) {
{view === 'series' && ( -
-
- + <> +
+
-
+
+ {showFilters && ( +
+ +
+ )} +
{loading ? ( ) : error ? ( @@ -238,8 +258,9 @@ export default function TvView({ libraryId }: Props) { ))}
)} +
-
+ )} {view === 'seasons' && selectedSeries && (