- DeviceDiscovery: auto device detection with type and status - LogsPage: comprehensive logs view with filters and stats - RipTargetModal: target directory selection wizard - ConfirmDialog: generic confirmation dialog component - LiveLogSection: real-time job progress and logs - Update App.tsx to include new pages
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
import { useState } from 'react'
|
||||
import { useDarkMode } from '../context/ThemeContext'
|
||||
|
||||
interface TargetConfig {
|
||||
id: string
|
||||
name: string
|
||||
path: string
|
||||
type: 'movies' | 'series' | 'music'
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
interface RipTargetModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onSave: (target: TargetConfig) => void
|
||||
}
|
||||
|
||||
export default function RipTargetModal({ isOpen, onClose, onSave }: RipTargetModalProps) {
|
||||
const [selectedType, setSelectedType] = useState<TargetConfig['type']>('movies')
|
||||
const [customPath, setCustomPath] = useState('')
|
||||
const { theme } = useDarkMode()
|
||||
|
||||
const defaultTargets: TargetConfig[] = [
|
||||
{ id: '1', name: 'Movies', path: '/app/media/movies', type: 'movies', isActive: true },
|
||||
{ id: '2', name: 'Series', path: '/app/media/series', type: 'series', isActive: true },
|
||||
{ id: '3', name: 'Music', path: '/app/media/music', type: 'music', isActive: true },
|
||||
]
|
||||
|
||||
const [targets, setTargets] = useState(defaultTargets)
|
||||
|
||||
const handleSave = () => {
|
||||
const target = targets.find(t => t.type === selectedType)
|
||||
if (target) {
|
||||
onSave(target)
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm transition-all">
|
||||
<div className={`w-full max-w-lg rounded-xl shadow-2xl transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800' : 'bg-white'}`}>
|
||||
{/* Header */}
|
||||
<div className={`px-6 py-4 border-b transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'}`}>
|
||||
<h2 className={`text-xl font-bold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Rip-Ziel auswählen</h2>
|
||||
<p className={`text-sm mt-1 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
|
||||
Wohin sollen die gerippten Dateien gespeichert werden?
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Body */}
|
||||
<div className="p-6 space-y-6">
|
||||
{/* Type Selection */}
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-3 ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>
|
||||
Dateityp
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{['movies', 'series', 'music'].map((type) => (
|
||||
<button
|
||||
key={type}
|
||||
onClick={() => setSelectedType(type as TargetConfig['type'])}
|
||||
className={`flex flex-col items-center justify-center gap-2 p-4 rounded-lg border transition-all ${
|
||||
selectedType === type
|
||||
? theme === 'dark'
|
||||
? 'bg-indigo-900/30 border-indigo-500 text-indigo-400'
|
||||
: 'bg-indigo-50 border-indigo-600 text-indigo-700'
|
||||
: theme === 'dark'
|
||||
? 'bg-slate-900 border-slate-700 text-slate-400 hover:border-slate-600'
|
||||
: 'bg-slate-50 border-slate-200 text-slate-600 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl">
|
||||
{type === 'movies' && '🎥'}
|
||||
{type === 'series' && '📺'}
|
||||
{type === 'music' && '🎵'}
|
||||
</span>
|
||||
<span className="text-sm font-medium capitalize">{type}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Path Display */}
|
||||
<div>
|
||||
<label className={`block text-sm font-medium mb-2 ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>
|
||||
Zielverzeichnis
|
||||
</label>
|
||||
<div className={`flex items-center gap-3 p-4 rounded-lg border ${theme === 'dark' ? 'bg-slate-900 border-slate-700' : 'bg-slate-50 border-slate-200'}`}>
|
||||
<span className="text-xl">📁</span>
|
||||
<input
|
||||
type="text"
|
||||
value={customPath || targets.find(t => t.type === selectedType)?.path || ''}
|
||||
onChange={(e) => setCustomPath(e.target.value)}
|
||||
className={`flex-1 bg-transparent outline-none text-sm font-mono ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}
|
||||
placeholder="/path/to/target"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
const target = targets.find(t => t.type === selectedType)
|
||||
if (target) {
|
||||
setCustomPath(target.path)
|
||||
}
|
||||
}}
|
||||
className={`px-3 py-1.5 rounded text-xs font-medium transition-colors ${theme === 'dark' ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-200 text-slate-700 hover:bg-slate-300'}`}
|
||||
>
|
||||
Standard
|
||||
</button>
|
||||
</div>
|
||||
<p className={`text-xs mt-2 ${theme === 'dark' ? 'text-slate-500' : 'text-slate-500'}`}>
|
||||
{customPath
|
||||
? `Verwende benutzerdefiniertes Verzeichnis: ${customPath}`
|
||||
: `Verwende Standard-Verzeichnis: ${targets.find(t => t.type === selectedType)?.path}`
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className={`px-6 py-4 border-t transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} flex justify-end gap-3`}>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${theme === 'dark' ? 'text-slate-400 hover:bg-slate-700' : 'text-slate-600 hover:bg-slate-100'}`}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
className="px-4 py-2 rounded-lg bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700 transition-colors shadow-lg shadow-indigo-500/30"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user