Jikan/MAL-Quelle + Job-Titel + Abbrechen-Knopf + Dark-Hover + Ordner-Verwaltung
Ampel / ampel (push) Failing after 38s
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:
@@ -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'}`}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import LiveLogSection from '../components/LiveLogSection'
|
||||
interface Job {
|
||||
id: string
|
||||
type: 'cd' | 'dvd' | 'bluray'
|
||||
status: 'pending' | 'processing' | 'transcoding' | 'completed' | 'failed'
|
||||
status: 'pending' | 'processing' | 'transcoding' | 'canceling' | 'completed' | 'failed'
|
||||
device: string
|
||||
startTime: string
|
||||
endTime?: string
|
||||
@@ -32,6 +32,7 @@ function StatusBadge({ status }: { status: string }) {
|
||||
pending: 'bg-amber-900/30 text-amber-400 ring-amber-700/20',
|
||||
processing: 'bg-blue-900/30 text-blue-400 ring-blue-700/20',
|
||||
transcoding: 'bg-purple-900/30 text-purple-400 ring-purple-700/20',
|
||||
canceling: 'bg-orange-900/30 text-orange-400 ring-orange-700/20',
|
||||
completed: 'bg-emerald-900/30 text-emerald-400 ring-emerald-700/20',
|
||||
failed: 'bg-rose-900/30 text-rose-400 ring-rose-700/20',
|
||||
}
|
||||
@@ -39,6 +40,7 @@ function StatusBadge({ status }: { status: string }) {
|
||||
pending: 'bg-amber-100 text-amber-700 ring-amber-600/20',
|
||||
processing: 'bg-blue-100 text-blue-700 ring-blue-600/20',
|
||||
transcoding: 'bg-purple-100 text-purple-700 ring-purple-600/20',
|
||||
canceling: 'bg-orange-100 text-orange-700 ring-orange-600/20',
|
||||
completed: 'bg-emerald-100 text-emerald-700 ring-emerald-600/20',
|
||||
failed: 'bg-rose-100 text-rose-700 ring-rose-600/20',
|
||||
}
|
||||
@@ -47,6 +49,7 @@ function StatusBadge({ status }: { status: string }) {
|
||||
pending: 'Wartend',
|
||||
processing: 'Rippen',
|
||||
transcoding: 'Komprimieren',
|
||||
canceling: 'Wird abgebrochen…',
|
||||
completed: 'Fertig',
|
||||
failed: 'Fehler',
|
||||
}
|
||||
@@ -273,7 +276,7 @@ export default function Dashboard() {
|
||||
</tr>
|
||||
) : (
|
||||
jobs.slice(0, 10).map(job => (
|
||||
<tr key={job.id} className="hover:bg-slate-50 transition-colors duration-300">
|
||||
<tr key={job.id} className={`transition-colors duration-300 ${theme === 'dark' ? 'hover:bg-slate-700/40' : 'hover:bg-slate-50'}`}>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<TypeBadge type={job.type} />
|
||||
</td>
|
||||
@@ -304,6 +307,15 @@ export default function Dashboard() {
|
||||
Neu komprimieren
|
||||
</button>
|
||||
)}
|
||||
{(job.status === 'pending' || job.status === 'processing' || job.status === 'transcoding') && (
|
||||
<button
|
||||
onClick={() => api.post(`/jobs/${job.id}/cancel`).catch(() => {})}
|
||||
title="Job abbrechen (Rohdaten bleiben erhalten)"
|
||||
className={`text-xs font-medium px-3 py-1.5 rounded-lg transition-colors ${theme === 'dark' ? 'bg-slate-700 text-rose-300 hover:bg-slate-600' : 'bg-slate-100 text-rose-700 hover:bg-slate-200'}`}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
|
||||
Reference in New Issue
Block a user