Jikan/MAL-Quelle + Job-Titel + Abbrechen-Knopf + Dark-Hover + Ordner-Verwaltung
Ampel / ampel (push) Failing after 38s

Commander-Befunde (Screenshots 23.07. abends):
- Jikan (MyAnimeList, kostenlos OHNE Key) als Anime-Quelle vor OMDb —
  waehlt per Titel-Aehnlichkeit (SequenceMatcher, Schwelle 0.55) statt
  blind Treffer 1; damit trifft "Evangelion 2.22" den exakten Film
- Job-Titel: POST /jobs uebernimmt den erkannten Disc-Titel aus dem
  DISC_CACHE — Schluss mit "Unbekannt" in der Queue
- Kooperativer Job-Abbruch: POST /jobs/{id}/cancel setzt canceling,
  der Worker prueft das Flag bei jedem Fortschritts-Update und killt
  den Encoder-Prozess sauber (RipAbbruch); UI-Knopf "Abbrechen" fuer
  laufende, Status-Badge "Wird abgebrochen"
- Dark-Mode: Job-Zeilen-Hover war hart hell (hover:bg-slate-50)
- Speicherziele: lokaler Ordner-Browser mit "Ordner anlegen"
  (GET /browse + POST /browse/mkdir, nur unter /app/media)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-23 21:22:57 +02:00
parent 85f352b14f
commit 07be54e62a
8 changed files with 307 additions and 21 deletions
+74 -2
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react'
import { HardDrive, Plus, Trash2, CheckCircle, AlertCircle, RefreshCw } from 'lucide-react'
import { HardDrive, Plus, Trash2, CheckCircle, AlertCircle, RefreshCw, Folder, ArrowUp, FolderPlus } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
@@ -31,8 +31,35 @@ export default function StorageMounts() {
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
const [feedback, setFeedback] = useState<string | null>(null)
const [browsePath, setBrowsePath] = useState('/app/media')
const [browseParent, setBrowseParent] = useState<string | null>(null)
const [browseDirs, setBrowseDirs] = useState<{ name: string, path: string }[]>([])
const [neuerOrdner, setNeuerOrdner] = useState('')
const { theme } = useDarkMode()
const browsen = async (pfad: string) => {
try {
const r = await api.get('/browse', { params: { path: pfad } })
setBrowsePath(r.data.path)
setBrowseParent(r.data.parent)
setBrowseDirs(r.data.dirs)
} catch {
setBrowseDirs([])
}
}
const ordnerAnlegen = async () => {
if (!neuerOrdner) return
try {
await api.post('/browse/mkdir', { path: browsePath, name: neuerOrdner })
setNeuerOrdner('')
browsen(browsePath)
laden()
} catch (e: any) {
setFeedback(`${e?.response?.data?.detail || 'Ordner anlegen fehlgeschlagen'}`)
}
}
const laden = async () => {
try {
const [m, t] = await Promise.all([
@@ -46,7 +73,7 @@ export default function StorageMounts() {
}
}
useEffect(() => { laden() }, [])
useEffect(() => { laden(); browsen('/app/media') }, [])
const hinzufuegen = async () => {
setBusy(true)
@@ -116,6 +143,51 @@ export default function StorageMounts() {
)}
</div>
{/* Lokale Ordner durchsuchen + anlegen */}
<div className={`rounded-lg border overflow-hidden ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'}`}>
<div className={`flex items-center gap-2 px-3 py-2 border-b ${theme === 'dark' ? 'border-slate-700 bg-slate-900' : 'border-slate-200 bg-slate-50'}`}>
<button
onClick={() => browseParent && browsen(browseParent)}
disabled={!browseParent}
className={`p-1 rounded disabled:opacity-30 ${theme === 'dark' ? 'hover:bg-slate-700 text-slate-400' : 'hover:bg-slate-200 text-slate-500'}`}
title="Ordner hoch"
>
<ArrowUp size={16} />
</button>
<span className={`text-xs font-mono truncate flex-1 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>{browsePath}</span>
<input
value={neuerOrdner}
onChange={e => setNeuerOrdner(e.target.value)}
placeholder="neuer-ordner"
className={`w-32 px-2 py-1 text-xs border rounded ${theme === 'dark' ? 'bg-slate-800 border-slate-600 text-slate-200' : 'bg-white border-slate-300 text-slate-900'}`}
/>
<button
onClick={ordnerAnlegen}
disabled={!neuerOrdner}
className={`p-1.5 rounded disabled:opacity-30 text-indigo-500 ${theme === 'dark' ? 'hover:bg-slate-700' : 'hover:bg-slate-200'}`}
title="Ordner anlegen"
>
<FolderPlus size={16} />
</button>
</div>
<div className="max-h-36 overflow-y-auto">
{browseDirs.length === 0 ? (
<p className={`text-xs px-3 py-3 ${theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}`}>Keine Unterordner</p>
) : (
browseDirs.map(d => (
<button
key={d.path}
onClick={() => browsen(d.path)}
className={`w-full flex items-center gap-2 px-3 py-2 text-sm text-left transition-colors ${theme === 'dark' ? 'text-slate-300 hover:bg-slate-700' : 'text-slate-700 hover:bg-slate-100'}`}
>
<Folder size={15} className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'} />
{d.name}
</button>
))
)}
</div>
</div>
{/* Neues Netzwerk-Ziel */}
<div className={`rounded-lg p-4 border ${theme === 'dark' ? 'border-slate-700 bg-slate-800/50' : 'border-slate-200 bg-slate-50'}`}>
<h3 className={`font-medium mb-3 flex items-center gap-2 ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>