add tagging to tv
This commit is contained in:
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
import type { TvSeries, TvSeason, TvEpisode } from '@/types'
|
||||
import FilterPanel from '@/components/FilterPanel'
|
||||
import VideoPlayerModal from '@/components/mixed/VideoPlayerModal'
|
||||
import TagSelector from '@/components/tags/TagSelector'
|
||||
import EpisodeCard from './EpisodeCard'
|
||||
|
||||
interface Props {
|
||||
@@ -27,6 +28,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
const [assignments, setAssignments] = useState<Record<string, string[]>>({})
|
||||
const [filterRefreshKey, setFilterRefreshKey] = useState(0)
|
||||
const [showFilters, setShowFilters] = useState(true)
|
||||
const [tagPanel, setTagPanel] = useState<{ mediaKey: string; title: string } | null>(null)
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const [confirming, setConfirming] = useState(false)
|
||||
const [deleting, setDeleting] = useState(false)
|
||||
@@ -141,6 +143,8 @@ export default function TvView({ libraryId }: Props) {
|
||||
<VideoPlayerModal
|
||||
url={videoUrl}
|
||||
name={playingEpisode.title}
|
||||
mediaKey={`${libraryId}:${playingEpisode.id}`}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments() }}
|
||||
onClose={() => setPlayingEpisode(null)}
|
||||
context="tv"
|
||||
/>
|
||||
@@ -225,10 +229,13 @@ export default function TvView({ libraryId }: Props) {
|
||||
) : (
|
||||
<div className="grid gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6">
|
||||
{filteredSeries.map((s) => (
|
||||
<button
|
||||
<div
|
||||
key={s.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => openSeries(s)}
|
||||
className="group text-left rounded-xl overflow-hidden border transition-all focus:outline-none focus-visible:ring-2"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openSeries(s) } }}
|
||||
className="group text-left rounded-xl overflow-hidden border transition-all focus:outline-none focus-visible:ring-2 cursor-pointer"
|
||||
style={{ borderColor: 'var(--border)', backgroundColor: 'var(--surface)' }}
|
||||
onMouseEnter={(e) => {
|
||||
;(e.currentTarget as HTMLElement).style.borderColor = 'var(--accent)'
|
||||
@@ -246,6 +253,15 @@ export default function TvView({ libraryId }: Props) {
|
||||
) : (
|
||||
<div className="absolute inset-0 flex items-center justify-center text-4xl">📺</div>
|
||||
)}
|
||||
<button
|
||||
onClick={(e) => { e.stopPropagation(); setTagPanel({ mediaKey: `${libraryId}:${s.id}`, title: s.title }) }}
|
||||
className="absolute top-2 left-2 w-6 h-6 rounded-full items-center justify-center text-xs opacity-0 group-hover:opacity-100 transition-opacity hidden group-hover:flex"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.55)', color: '#fff' }}
|
||||
aria-label={`Tag ${s.title}`}
|
||||
title="Tags"
|
||||
>
|
||||
🏷
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-2">
|
||||
<p className="text-xs font-medium truncate leading-tight" style={{ color: 'var(--text-primary)' }} title={s.title}>
|
||||
@@ -255,7 +271,7 @@ export default function TvView({ libraryId }: Props) {
|
||||
{s.year ? `${s.year} · ` : ''}{s.seasonCount} season{s.seasonCount !== 1 ? 's' : ''}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -416,12 +432,52 @@ export default function TvView({ libraryId }: Props) {
|
||||
key={ep.id}
|
||||
episode={ep}
|
||||
onClick={() => setPlayingEpisode(ep)}
|
||||
onTag={() => setTagPanel({ mediaKey: `${libraryId}:${ep.id}`, title: ep.title })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{tagPanel && (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4"
|
||||
style={{ backgroundColor: 'rgba(0,0,0,0.6)' }}
|
||||
onClick={(e) => { if (e.target === e.currentTarget) setTagPanel(null) }}
|
||||
>
|
||||
<div
|
||||
className="w-full max-w-md rounded-2xl shadow-2xl overflow-hidden"
|
||||
style={{ backgroundColor: 'var(--surface)', border: '1px solid var(--border)' }}
|
||||
>
|
||||
<div className="flex items-center justify-between px-5 py-4" style={{ borderBottom: '1px solid var(--border)' }}>
|
||||
<div className="min-w-0">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider mb-0.5" style={{ color: 'var(--text-secondary)' }}>
|
||||
Tags
|
||||
</p>
|
||||
<p className="text-sm font-medium truncate" style={{ color: 'var(--text-primary)' }}>
|
||||
{tagPanel.title}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setTagPanel(null)}
|
||||
className="ml-4 w-8 h-8 flex-shrink-0 rounded-full flex items-center justify-center text-sm transition-colors"
|
||||
style={{ backgroundColor: 'var(--border)', color: 'var(--text-secondary)' }}
|
||||
onMouseEnter={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-primary)')}
|
||||
onMouseLeave={(e) => ((e.currentTarget as HTMLElement).style.color = 'var(--text-secondary)')}
|
||||
aria-label="Close"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<TagSelector
|
||||
mediaKey={tagPanel.mediaKey}
|
||||
onTagsChanged={() => { setFilterRefreshKey((k) => k + 1); fetchAssignments(); setTagPanel(null) }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user