viewer-improvements #6
@@ -21,6 +21,7 @@ export default function GamesView({ libraryId }: Props) {
|
|||||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
||||||
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
||||||
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
||||||
|
const [showFilters, setShowFilters] = useState(true)
|
||||||
|
|
||||||
const toggleTag = (tagId: string) =>
|
const toggleTag = (tagId: string) =>
|
||||||
setSelectedTagIds((prev) => {
|
setSelectedTagIds((prev) => {
|
||||||
@@ -83,20 +84,39 @@ export default function GamesView({ libraryId }: Props) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const filtersActive = search !== '' || selectedTagIds.size > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 items-start">
|
<>
|
||||||
<div className="w-52 flex-shrink-0">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<FilterPanel
|
<button
|
||||||
libraryId={libraryId}
|
onClick={() => setShowFilters((v) => !v)}
|
||||||
assignments={assignments}
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||||
search={search}
|
style={{
|
||||||
onSearchChange={setSearch}
|
backgroundColor: (showFilters || filtersActive) ? 'var(--accent)' : 'var(--surface)',
|
||||||
selectedTagIds={selectedTagIds}
|
color: (showFilters || filtersActive) ? '#fff' : 'var(--text-secondary)',
|
||||||
onTagToggle={toggleTag}
|
border: '1px solid var(--border)',
|
||||||
refreshKey={filterRefreshKey}
|
}}
|
||||||
/>
|
aria-label={showFilters ? 'Hide filters' : 'Show filters'}
|
||||||
|
>
|
||||||
|
Filters{filtersActive ? ' ●' : ''}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex flex-col md:flex-row gap-6 md:items-start">
|
||||||
|
{showFilters && (
|
||||||
|
<div className="w-full md:w-52 md:flex-shrink-0">
|
||||||
|
<FilterPanel
|
||||||
|
libraryId={libraryId}
|
||||||
|
assignments={assignments}
|
||||||
|
search={search}
|
||||||
|
onSearchChange={setSearch}
|
||||||
|
selectedTagIds={selectedTagIds}
|
||||||
|
onTagToggle={toggleTag}
|
||||||
|
refreshKey={filterRefreshKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
{/* Breadcrumb when inside a series */}
|
{/* Breadcrumb when inside a series */}
|
||||||
{selectedSeries && (
|
{selectedSeries && (
|
||||||
<div className="flex items-center gap-2 mb-4 text-sm">
|
<div className="flex items-center gap-2 mb-4 text-sm">
|
||||||
@@ -154,8 +174,9 @@ export default function GamesView({ libraryId }: Props) {
|
|||||||
onCoverUploaded={() => fetchGames(true)}
|
onCoverUploaded={() => fetchGames(true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,10 +49,11 @@ export default function ImageLightbox({ url, name, onClose, mediaKey, onTagsChan
|
|||||||
{mediaKey && (
|
{mediaKey && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); setShowTags((v) => !v) }}
|
onClick={(e) => { e.stopPropagation(); setShowTags((v) => !v) }}
|
||||||
className="w-8 h-8 rounded-full flex items-center justify-center text-sm transition-colors"
|
className="w-12 h-12 rounded-full flex items-center justify-center text-sm transition-colors"
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: showTags ? 'var(--accent)' : 'var(--surface)',
|
backgroundColor: showTags ? 'var(--accent)' : 'var(--surface)',
|
||||||
color: showTags ? '#fff' : 'var(--text-primary)',
|
color: showTags ? '#fff' : 'var(--text-primary)',
|
||||||
|
fontSize: '1.5rem',
|
||||||
}}
|
}}
|
||||||
onMouseEnter={(e) => {
|
onMouseEnter={(e) => {
|
||||||
if (!showTags) (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)'
|
if (!showTags) (e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)'
|
||||||
@@ -68,8 +69,8 @@ export default function ImageLightbox({ url, name, onClose, mediaKey, onTagsChan
|
|||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="w-8 h-8 rounded-full flex items-center justify-center text-sm transition-colors"
|
className="w-12 h-12 rounded-full flex items-center justify-center text-sm transition-colors"
|
||||||
style={{ backgroundColor: 'var(--surface)', color: 'var(--text-primary)' }}
|
style={{ backgroundColor: 'var(--surface)', color: 'var(--text-primary)', fontSize: '1.5rem' }}
|
||||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)')}
|
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface-hover)')}
|
||||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface)')}
|
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.backgroundColor = 'var(--surface)')}
|
||||||
aria-label="Close"
|
aria-label="Close"
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export default function MixedView({ libraryId, initialPath }: Props) {
|
|||||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
||||||
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
||||||
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
||||||
|
const [showFilters, setShowFilters] = useState(true)
|
||||||
|
|
||||||
const toggleTag = (tagId: string) =>
|
const toggleTag = (tagId: string) =>
|
||||||
setSelectedTagIds((prev) => {
|
setSelectedTagIds((prev) => {
|
||||||
@@ -120,20 +121,39 @@ export default function MixedView({ libraryId, initialPath }: Props) {
|
|||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const filtersActive = search !== '' || selectedTagIds.size > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 items-start">
|
<>
|
||||||
<div className="w-52 flex-shrink-0">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<FilterPanel
|
<button
|
||||||
libraryId={libraryId}
|
onClick={() => setShowFilters((v) => !v)}
|
||||||
assignments={assignments}
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||||
search={search}
|
style={{
|
||||||
onSearchChange={setSearch}
|
backgroundColor: (showFilters || filtersActive) ? 'var(--accent)' : 'var(--surface)',
|
||||||
selectedTagIds={selectedTagIds}
|
color: (showFilters || filtersActive) ? '#fff' : 'var(--text-secondary)',
|
||||||
onTagToggle={toggleTag}
|
border: '1px solid var(--border)',
|
||||||
refreshKey={filterRefreshKey}
|
}}
|
||||||
/>
|
aria-label={showFilters ? 'Hide filters' : 'Show filters'}
|
||||||
|
>
|
||||||
|
Filters{filtersActive ? ' ●' : ''}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex flex-col md:flex-row gap-6 md:items-start">
|
||||||
|
{showFilters && (
|
||||||
|
<div className="w-full md:w-52 md:flex-shrink-0">
|
||||||
|
<FilterPanel
|
||||||
|
libraryId={libraryId}
|
||||||
|
assignments={assignments}
|
||||||
|
search={search}
|
||||||
|
onSearchChange={setSearch}
|
||||||
|
selectedTagIds={selectedTagIds}
|
||||||
|
onTagToggle={toggleTag}
|
||||||
|
refreshKey={filterRefreshKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
{/* Breadcrumb */}
|
{/* Breadcrumb */}
|
||||||
<nav className="flex items-center gap-1 mb-6 flex-wrap text-sm">
|
<nav className="flex items-center gap-1 mb-6 flex-wrap text-sm">
|
||||||
<button
|
<button
|
||||||
@@ -259,8 +279,9 @@ export default function MixedView({ libraryId, initialPath }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export default function MoviesView({ libraryId }: Props) {
|
|||||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
||||||
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
||||||
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
||||||
|
const [showFilters, setShowFilters] = useState(true)
|
||||||
|
|
||||||
const toggleTag = (tagId: string) =>
|
const toggleTag = (tagId: string) =>
|
||||||
setSelectedTagIds((prev) => {
|
setSelectedTagIds((prev) => {
|
||||||
@@ -64,20 +65,39 @@ export default function MoviesView({ libraryId }: Props) {
|
|||||||
setMovies((prev) => prev.filter((m) => m.id !== movieId))
|
setMovies((prev) => prev.filter((m) => m.id !== movieId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filtersActive = search !== '' || selectedTagIds.size > 0
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6 items-start">
|
<>
|
||||||
<div className="w-52 flex-shrink-0">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<FilterPanel
|
<button
|
||||||
libraryId={libraryId}
|
onClick={() => setShowFilters((v) => !v)}
|
||||||
assignments={assignments}
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||||
search={search}
|
style={{
|
||||||
onSearchChange={setSearch}
|
backgroundColor: (showFilters || filtersActive) ? 'var(--accent)' : 'var(--surface)',
|
||||||
selectedTagIds={selectedTagIds}
|
color: (showFilters || filtersActive) ? '#fff' : 'var(--text-secondary)',
|
||||||
onTagToggle={toggleTag}
|
border: '1px solid var(--border)',
|
||||||
refreshKey={filterRefreshKey}
|
}}
|
||||||
/>
|
aria-label={showFilters ? 'Hide filters' : 'Show filters'}
|
||||||
|
>
|
||||||
|
Filters{filtersActive ? ' ●' : ''}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex flex-col md:flex-row gap-6 md:items-start">
|
||||||
|
{showFilters && (
|
||||||
|
<div className="w-full md:w-52 md:flex-shrink-0">
|
||||||
|
<FilterPanel
|
||||||
|
libraryId={libraryId}
|
||||||
|
assignments={assignments}
|
||||||
|
search={search}
|
||||||
|
onSearchChange={setSearch}
|
||||||
|
selectedTagIds={selectedTagIds}
|
||||||
|
onTagToggle={toggleTag}
|
||||||
|
refreshKey={filterRefreshKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<LoadingGrid />
|
<LoadingGrid />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -148,8 +168,9 @@ export default function MoviesView({ libraryId }: Props) {
|
|||||||
onDeleted={handleDeleted}
|
onDeleted={handleDeleted}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export default function TvView({ libraryId }: Props) {
|
|||||||
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
const [selectedTagIds, setSelectedTagIds] = useState<Set<string>>(new Set())
|
||||||
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
||||||
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
||||||
|
const [showFilters, setShowFilters] = useState(true)
|
||||||
const [menuOpen, setMenuOpen] = useState(false)
|
const [menuOpen, setMenuOpen] = useState(false)
|
||||||
const [confirming, setConfirming] = useState(false)
|
const [confirming, setConfirming] = useState(false)
|
||||||
const [deleting, setDeleting] = useState(false)
|
const [deleting, setDeleting] = useState(false)
|
||||||
@@ -123,6 +124,8 @@ export default function TvView({ libraryId }: Props) {
|
|||||||
.catch(() => setDeleting(false))
|
.catch(() => setDeleting(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const filtersActive = search !== '' || selectedTagIds.size > 0
|
||||||
|
|
||||||
const filteredSeries = series.filter((s) => {
|
const filteredSeries = series.filter((s) => {
|
||||||
if (search && !s.title.toLowerCase().includes(search.toLowerCase())) return false
|
if (search && !s.title.toLowerCase().includes(search.toLowerCase())) return false
|
||||||
if (selectedTagIds.size > 0) {
|
if (selectedTagIds.size > 0) {
|
||||||
@@ -179,19 +182,36 @@ export default function TvView({ libraryId }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{view === 'series' && (
|
{view === 'series' && (
|
||||||
<div className="flex gap-6 items-start">
|
<>
|
||||||
<div className="w-52 flex-shrink-0">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<FilterPanel
|
<button
|
||||||
libraryId={libraryId}
|
onClick={() => setShowFilters((v) => !v)}
|
||||||
assignments={assignments}
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors"
|
||||||
search={search}
|
style={{
|
||||||
onSearchChange={setSearch}
|
backgroundColor: (showFilters || filtersActive) ? 'var(--accent)' : 'var(--surface)',
|
||||||
selectedTagIds={selectedTagIds}
|
color: (showFilters || filtersActive) ? '#fff' : 'var(--text-secondary)',
|
||||||
onTagToggle={toggleTag}
|
border: '1px solid var(--border)',
|
||||||
refreshKey={filterRefreshKey}
|
}}
|
||||||
/>
|
aria-label={showFilters ? 'Hide filters' : 'Show filters'}
|
||||||
|
>
|
||||||
|
Filters{filtersActive ? ' ●' : ''}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex flex-col md:flex-row gap-6 md:items-start">
|
||||||
|
{showFilters && (
|
||||||
|
<div className="w-full md:w-52 md:flex-shrink-0">
|
||||||
|
<FilterPanel
|
||||||
|
libraryId={libraryId}
|
||||||
|
assignments={assignments}
|
||||||
|
search={search}
|
||||||
|
onSearchChange={setSearch}
|
||||||
|
selectedTagIds={selectedTagIds}
|
||||||
|
onTagToggle={toggleTag}
|
||||||
|
refreshKey={filterRefreshKey}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<SeriesLoadingGrid />
|
<SeriesLoadingGrid />
|
||||||
) : error ? (
|
) : error ? (
|
||||||
@@ -238,8 +258,9 @@ export default function TvView({ libraryId }: Props) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{view === 'seasons' && selectedSeries && (
|
{view === 'seasons' && selectedSeries && (
|
||||||
|
|||||||
Reference in New Issue
Block a user