+ {/* Description section */}
+
+
+
+ Description
+
+
- {/* Scrollable panel content */}
-
-
- {/* Description section */}
-
- {/* Heading row */}
-
+ {/* Text extraction section — only for images */}
+ {isImage && (
+
+
- Description
+ Text Extraction
- {/* Editable textarea */}
-
- {/* Text extraction section — only for images */}
- {isImage && (
-
- {/* Heading row */}
-
-
- Text Extraction
-
- {/* AI button — forces LLM, no OCR */}
-
-
+ {extractError &&
{extractError}
}
- {/* OCR button row */}
-
-
- setOcrLanguageInput(e.target.value)}
- placeholder={defaultOcrLanguages}
- className="text-xs px-2 py-0.5 rounded-full outline-none"
- style={{
- backgroundColor: 'var(--background)',
- border: '1px solid var(--border)',
- color: 'var(--text-primary)',
- width: 120,
- }}
- title="Tesseract language(s) for this extraction (e.g. jpn+jpn_vert). Leave blank to use the configured default."
- />
-
-
- {extractError && (
-
{extractError}
- )}
-
- {extractedText && (
-
-
-
- {translatedText && (
-
-
- Translation
-
-
- {translatedText}
-
-
- )}
-
-
-
setSourceLanguage(e.target.value)}
- placeholder="Source lang…"
- className="text-xs px-2 py-0.5 rounded-full outline-none"
- style={{
- backgroundColor: 'var(--background)',
- border: '1px solid var(--border)',
- color: 'var(--text-primary)',
- width: 100,
- }}
- />
+ {extractedText && (
+
- )}
-
- )}
- {/* Tags section */}
-
-
-
- Tags
-
- {onAiTag && (
-
- )}
-
- {aiTagError &&
{aiTagError}
}
-
+ {translatedText && (
+
+
+ Translation
+
+
+ {translatedText}
+
+
+ )}
+
+
+ setSourceLanguage(e.target.value)}
+ placeholder="Source lang…"
+ className="text-xs px-2 py-0.5 rounded-full outline-none"
+ style={{
+ backgroundColor: 'var(--background)',
+ border: '1px solid var(--border)',
+ color: 'var(--text-primary)',
+ width: 100,
+ }}
+ />
+
+
+
+ )}
-
-
+ )}
+
)}
diff --git a/src/components/mixed/VideoPlayerModal.tsx b/src/components/mixed/VideoPlayerModal.tsx
index 2769901..9309943 100644
--- a/src/components/mixed/VideoPlayerModal.tsx
+++ b/src/components/mixed/VideoPlayerModal.tsx
@@ -1,7 +1,7 @@
'use client'
import { useEffect, useRef, useState } from 'react'
-import TagSelector from '@/components/tags/TagSelector'
+import MediaTagPanel from '@/components/tags/MediaTagPanel'
import { useUserSettings } from '@/hooks/useUserSettings'
interface Props {
@@ -28,9 +28,6 @@ export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, i
const [showTagsLocal, setShowTagsLocal] = useState(false)
const showTags = showTagsProp ?? showTagsLocal
const setShowTags = onShowTagsChange ?? setShowTagsLocal
- const [aiTagging, setAiTagging] = useState(false)
- const [aiTagError, setAiTagError] = useState
(null)
- const [tagRefreshKey, setTagRefreshKey] = useState(0)
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
@@ -50,22 +47,6 @@ export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, i
if (e.target === overlayRef.current) onClose()
}
- const handleAiTag = async () => {
- if (!onAiTag) return
- setAiTagging(true)
- setAiTagError(null)
- try {
- await onAiTag()
- setTagRefreshKey((k) => k + 1)
- onTagsChanged?.()
- } catch (err) {
- setAiTagError(err instanceof Error ? err.message : 'AI tagging failed')
- setTimeout(() => setAiTagError(null), 4000)
- } finally {
- setAiTagging(false)
- }
- }
-
const smallBtn = 'w-7 h-7 rounded-full flex items-center justify-center transition-colors flex-shrink-0'
return (
@@ -157,72 +138,13 @@ export default function VideoPlayerModal({ url, name, onClose, onPrev, onNext, i
{/* ── Tag panel ── bottom half on mobile, right sidebar on desktop */}
{showTags && (
- e.stopPropagation()}
- >
- {/* Panel header — ‹ hide | ✕ close */}
-
-
-
-
-
-
-
- {/* Tags */}
-
-
-
- Tags
-
- {onAiTag && (
-
- )}
-
- {aiTagError &&
{aiTagError}
}
-
-
-
+ setShowTags(false)}
+ onClose={onClose}
+ onTagsChanged={onTagsChanged}
+ onAiTag={onAiTag}
+ />
)}