feat: add sound scanning and normalization features to SettingsPage; implement UI components and state management
This commit is contained in:
@@ -1,6 +1,77 @@
|
||||
import { useState } from 'react'
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
Scan,
|
||||
Volume2,
|
||||
Settings as SettingsIcon,
|
||||
Loader2,
|
||||
FolderSync,
|
||||
AudioWaveform
|
||||
} from 'lucide-react'
|
||||
import { adminService, type ScanResponse, type NormalizationResponse } from '@/lib/api/services/admin'
|
||||
|
||||
export function SettingsPage() {
|
||||
// Sound scanning state
|
||||
const [scanningInProgress, setScanningInProgress] = useState(false)
|
||||
const [lastScanResults, setLastScanResults] = useState<ScanResponse | null>(null)
|
||||
|
||||
// Sound normalization state
|
||||
const [normalizationInProgress, setNormalizationInProgress] = useState(false)
|
||||
const [normalizationOptions, setNormalizationOptions] = useState({
|
||||
force: false,
|
||||
onePass: false,
|
||||
soundType: 'all' as 'all' | 'SDB' | 'TTS' | 'EXT'
|
||||
})
|
||||
const [lastNormalizationResults, setLastNormalizationResults] = useState<NormalizationResponse | null>(null)
|
||||
|
||||
const handleScanSounds = async () => {
|
||||
setScanningInProgress(true)
|
||||
try {
|
||||
const response = await adminService.scanSounds()
|
||||
setLastScanResults(response)
|
||||
toast.success(`Sound scan completed! Added: ${response.results.added}, Updated: ${response.results.updated}, Deleted: ${response.results.deleted}`)
|
||||
} catch (error) {
|
||||
toast.error('Failed to scan sounds')
|
||||
console.error('Sound scan error:', error)
|
||||
} finally {
|
||||
setScanningInProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleNormalizeSounds = async () => {
|
||||
setNormalizationInProgress(true)
|
||||
try {
|
||||
let response: NormalizationResponse
|
||||
|
||||
if (normalizationOptions.soundType === 'all') {
|
||||
response = await adminService.normalizeAllSounds(
|
||||
normalizationOptions.force,
|
||||
normalizationOptions.onePass
|
||||
)
|
||||
} else {
|
||||
response = await adminService.normalizeSoundsByType(
|
||||
normalizationOptions.soundType,
|
||||
normalizationOptions.force,
|
||||
normalizationOptions.onePass
|
||||
)
|
||||
}
|
||||
|
||||
setLastNormalizationResults(response)
|
||||
toast.success(`Sound normalization completed! Processed: ${response.results.processed}, Normalized: ${response.results.normalized}`)
|
||||
} catch (error) {
|
||||
toast.error('Failed to normalize sounds')
|
||||
console.error('Sound normalization error:', error)
|
||||
} finally {
|
||||
setNormalizationInProgress(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AppLayout
|
||||
breadcrumb={{
|
||||
@@ -11,11 +82,164 @@ export function SettingsPage() {
|
||||
]
|
||||
}}
|
||||
>
|
||||
<div className="flex-1 rounded-xl bg-muted/50 p-4">
|
||||
<h1 className="text-2xl font-bold mb-4">System Settings</h1>
|
||||
<p className="text-muted-foreground">
|
||||
System administration interface coming soon...
|
||||
</p>
|
||||
<div className="flex-1 space-y-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<h1 className="text-3xl font-bold">System Settings</h1>
|
||||
<SettingsIcon className="h-8 w-8" />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
{/* Sound Scanning */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FolderSync className="h-5 w-5" />
|
||||
Sound Scanning
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Scan the sound directories to synchronize new, updated, and deleted audio files with the database.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
onClick={handleScanSounds}
|
||||
disabled={scanningInProgress}
|
||||
className="w-full"
|
||||
>
|
||||
{scanningInProgress ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Scanning...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Scan className="h-4 w-4 mr-2" />
|
||||
Scan Sound Directory
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{lastScanResults && (
|
||||
<div className="bg-muted rounded-lg p-3 space-y-2">
|
||||
<div className="text-sm font-medium">Last Scan Results:</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div>✅ Added: {lastScanResults.results.added}</div>
|
||||
<div>🔄 Updated: {lastScanResults.results.updated}</div>
|
||||
<div>🗑️ Deleted: {lastScanResults.results.deleted}</div>
|
||||
<div>⏭️ Skipped: {lastScanResults.results.skipped}</div>
|
||||
{lastScanResults.results.errors.length > 0 && (
|
||||
<div>❌ Errors: {lastScanResults.results.errors.length}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Sound Normalization */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<AudioWaveform className="h-5 w-5" />
|
||||
Sound Normalization
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Normalize audio levels across all sounds using FFmpeg's loudnorm filter for consistent volume.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<Label>Sound Type</Label>
|
||||
<Select
|
||||
value={normalizationOptions.soundType}
|
||||
onValueChange={(value: 'all' | 'SDB' | 'TTS' | 'EXT') =>
|
||||
setNormalizationOptions(prev => ({ ...prev, soundType: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">All Sounds</SelectItem>
|
||||
<SelectItem value="SDB">Soundboard (SDB)</SelectItem>
|
||||
<SelectItem value="TTS">Text-to-Speech (TTS)</SelectItem>
|
||||
<SelectItem value="EXT">Extracted (EXT)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="force-normalize"
|
||||
checked={normalizationOptions.force}
|
||||
onCheckedChange={(checked) =>
|
||||
setNormalizationOptions(prev => ({ ...prev, force: !!checked }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="force-normalize" className="text-sm">
|
||||
Force re-normalization of already processed sounds
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="one-pass"
|
||||
checked={normalizationOptions.onePass}
|
||||
onCheckedChange={(checked) =>
|
||||
setNormalizationOptions(prev => ({ ...prev, onePass: !!checked }))
|
||||
}
|
||||
/>
|
||||
<Label htmlFor="one-pass" className="text-sm">
|
||||
Use one-pass normalization (faster, lower quality)
|
||||
</Label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={handleNormalizeSounds}
|
||||
disabled={normalizationInProgress}
|
||||
className="w-full"
|
||||
>
|
||||
{normalizationInProgress ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Normalizing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Volume2 className="h-4 w-4 mr-2" />
|
||||
Normalize Sounds
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{lastNormalizationResults && (
|
||||
<div className="bg-muted rounded-lg p-3 space-y-2">
|
||||
<div className="text-sm font-medium">Last Normalization Results:</div>
|
||||
<div className="text-xs text-muted-foreground space-y-1">
|
||||
<div>🔄 Processed: {lastNormalizationResults.results.processed}</div>
|
||||
<div>✅ Normalized: {lastNormalizationResults.results.normalized}</div>
|
||||
<div>⏭️ Skipped: {lastNormalizationResults.results.skipped}</div>
|
||||
<div>❌ Errors: {lastNormalizationResults.results.errors}</div>
|
||||
{lastNormalizationResults.results.error_details.length > 0 && (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer text-red-600">View Error Details</summary>
|
||||
<div className="mt-1 text-xs text-red-600 space-y-1">
|
||||
{lastNormalizationResults.results.error_details.map((error, index) => (
|
||||
<div key={index}>• {error}</div>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user