From 427aade21a96b1b9dd2b0fbced1dc567a3e98b16 Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Sun, 5 Apr 2026 14:01:32 -0400 Subject: [PATCH 1/3] add tag selector to image and video viewers --- src/components/mixed/ImageLightbox.tsx | 95 ++++++++++++++++----- src/components/mixed/MixedView.tsx | 24 ++++-- src/components/mixed/VideoPlayerModal.tsx | 98 +++++++++++++++++----- src/components/movies/MovieDetailModal.tsx | 10 ++- 4 files changed, 177 insertions(+), 50 deletions(-) diff --git a/src/components/mixed/ImageLightbox.tsx b/src/components/mixed/ImageLightbox.tsx index 8cccd70..e846d32 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) => { @@ -35,30 +41,77 @@ export default function ImageLightbox({ url, name, onClose }: Props) { onClick={handleOverlayClick} > {/* 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..f5eacf9 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 @@ -80,9 +80,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') @@ -201,10 +201,22 @@ export default function MixedView({ libraryId, initialPath }: Props) { )} {modal?.type === 'video' && ( - setModal(null)} /> + { setFilterRefreshKey((k) => k + 1); fetchAssignments() }} + onClose={() => setModal(null)} + /> )} {modal?.type === 'image' && ( - setModal(null)} /> + { setFilterRefreshKey((k) => k + 1); fetchAssignments() }} + onClose={() => setModal(null)} + /> )} {/* Tag panel */} diff --git a/src/components/mixed/VideoPlayerModal.tsx b/src/components/mixed/VideoPlayerModal.tsx index 8098b86..02dfaa9 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) => { @@ -35,31 +41,79 @@ export default function VideoPlayerModal({ url, name, onClose }: Props) { onClick={handleOverlayClick} > {/* 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 -- 2.49.1 From bc77abbd8bd7a244516da145c269c3e0a52351c9 Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Sun, 5 Apr 2026 16:03:51 -0400 Subject: [PATCH 2/3] scaling tweaks --- src/components/mixed/ImageLightbox.tsx | 30 ++++++++++--------- src/components/mixed/VideoPlayerModal.tsx | 36 +++++++++++++---------- 2 files changed, 37 insertions(+), 29 deletions(-) diff --git a/src/components/mixed/ImageLightbox.tsx b/src/components/mixed/ImageLightbox.tsx index e846d32..7db86f5 100644 --- a/src/components/mixed/ImageLightbox.tsx +++ b/src/components/mixed/ImageLightbox.tsx @@ -36,12 +36,12 @@ export default function ImageLightbox({ url, name, onClose, mediaKey, onTagsChan return (
{/* Toolbar */} -
+
{name} @@ -80,20 +80,20 @@ export default function ImageLightbox({ url, name, onClose, mediaKey, onTagsChan
{showTags ? ( -
+
{/* Image */} -
+
{/* eslint-disable-next-line @next/next/no-img-element */} {name} e.stopPropagation()} />
{/* Tag panel */}
e.stopPropagation()} > @@ -104,13 +104,15 @@ export default function ImageLightbox({ url, name, onClose, mediaKey, onTagsChan
) : ( - /* eslint-disable-next-line @next/next/no-img-element */ - {name} e.stopPropagation()} - /> +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + {name} e.stopPropagation()} + /> +
)}
) diff --git a/src/components/mixed/VideoPlayerModal.tsx b/src/components/mixed/VideoPlayerModal.tsx index 02dfaa9..a0476cc 100644 --- a/src/components/mixed/VideoPlayerModal.tsx +++ b/src/components/mixed/VideoPlayerModal.tsx @@ -36,12 +36,12 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC return (
{/* Toolbar */} -
+
{name} @@ -80,21 +80,23 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
{showTags ? ( -
+
{/* Video */} -
+
{/* Tag panel */}
e.stopPropagation()} > @@ -105,14 +107,18 @@ export default function VideoPlayerModal({ url, name, onClose, mediaKey, onTagsC
) : ( -
) -- 2.49.1 From ca4bea084ad4cb1d57fa61369a871e82859e1c72 Mon Sep 17 00:00:00 2001 From: Garret Patti <42485635+garretpatti@users.noreply.github.com> Date: Sun, 5 Apr 2026 16:29:49 -0400 Subject: [PATCH 3/3] make FilterPanel hideable and responsive across all library views Adds a toggle button to show/hide the filter panel in Movies, Games, Mixed, and TV views. On mobile the layout stacks vertically (filter above content); on md+ it returns to the side-by-side layout. The toggle button highlights when filters are active so hidden filters remain discoverable. Also fixes a layout bug where items-start on the flex-col container caused MixedView thumbnails to collapse on narrow screens. Co-Authored-By: Claude Sonnet 4.6 --- src/components/games/GamesView.tsx | 47 +++++++++++++++++++------- src/components/mixed/ImageLightbox.tsx | 7 ++-- src/components/mixed/MixedView.tsx | 47 +++++++++++++++++++------- src/components/movies/MoviesView.tsx | 47 +++++++++++++++++++------- src/components/tv/TvView.tsx | 47 +++++++++++++++++++------- 5 files changed, 140 insertions(+), 55 deletions(-) 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 7db86f5..a474cc7 100644 --- a/src/components/mixed/ImageLightbox.tsx +++ b/src/components/mixed/ImageLightbox.tsx @@ -49,10 +49,11 @@ export default function ImageLightbox({ url, name, onClose, mediaKey, onTagsChan {mediaKey && (
-
+
+ {showFilters && ( +
+ +
+ )} +
{/* Breadcrumb */}
)} +
-
+ ) } 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 && ( -- 2.49.1