308 lines
9.8 KiB
TypeScript
308 lines
9.8 KiB
TypeScript
import React, { useEffect } from 'react'
|
|
import {
|
|
Box,
|
|
Paper,
|
|
Typography,
|
|
TextField,
|
|
MenuItem,
|
|
FormControlLabel,
|
|
Switch,
|
|
Collapse,
|
|
IconButton,
|
|
Divider,
|
|
Alert,
|
|
} from '@mui/material'
|
|
import { ExpandMore, ExpandLess } from '@mui/icons-material'
|
|
import { useOptionsStore } from '../stores/optionsStore'
|
|
import { useUploadStore } from '../stores/uploadStore'
|
|
import { useValidationStore } from '../stores/validationStore'
|
|
|
|
interface SectionProps {
|
|
title: string
|
|
children: React.ReactNode
|
|
defaultExpanded?: boolean
|
|
}
|
|
|
|
const Section: React.FC<SectionProps> = ({ title, children, defaultExpanded = false }) => {
|
|
const [expanded, setExpanded] = React.useState(defaultExpanded)
|
|
|
|
return (
|
|
<Box sx={{ mb: 2 }}>
|
|
<Box
|
|
sx={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
cursor: 'pointer',
|
|
py: 1,
|
|
}}
|
|
onClick={() => setExpanded(!expanded)}
|
|
>
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 600 }}>
|
|
{title}
|
|
</Typography>
|
|
<IconButton size="small">{expanded ? <ExpandLess /> : <ExpandMore />}</IconButton>
|
|
</Box>
|
|
<Divider />
|
|
<Collapse in={expanded}>
|
|
<Box sx={{ pt: 2, pb: 1 }}>{children}</Box>
|
|
</Collapse>
|
|
</Box>
|
|
)
|
|
}
|
|
|
|
export const OptionsPanel: React.FC = () => {
|
|
const { options, setOptions } = useOptionsStore()
|
|
const { hasVideo } = useUploadStore()
|
|
const { formatError, setFormatError } = useValidationStore()
|
|
|
|
// Audio-only formats from backend constants (hardcoded for now)
|
|
const audioOnlyFormats = ['mp3', 'm4a', 'ogg', 'opus', 'flac', 'wav', 'aac']
|
|
|
|
const handleFormatChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const newFormat = e.target.value
|
|
setOptions({ format: newFormat })
|
|
|
|
// Validate format
|
|
if (audioOnlyFormats.includes(newFormat) && hasVideo && !options.drop_video) {
|
|
setFormatError(
|
|
`Format '${newFormat}' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4).`
|
|
)
|
|
} else {
|
|
setFormatError(null)
|
|
}
|
|
}
|
|
|
|
const handleDropVideoChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const checked = e.target.checked
|
|
setOptions({ drop_video: checked })
|
|
// Re-validate format
|
|
if (audioOnlyFormats.includes(options.format) && hasVideo && !checked) {
|
|
setFormatError(
|
|
`Format '${options.format}' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4).`
|
|
)
|
|
} else {
|
|
setFormatError(null)
|
|
}
|
|
}
|
|
|
|
// Re-validate when hasVideo changes (e.g., after upload)
|
|
useEffect(() => {
|
|
const shouldShowError = audioOnlyFormats.includes(options.format) && hasVideo && !options.drop_video
|
|
const newError = shouldShowError
|
|
? `Format '${options.format}' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4).`
|
|
: null
|
|
|
|
// Only update if the error state actually changes
|
|
if (newError !== formatError) {
|
|
setFormatError(newError)
|
|
}
|
|
}, [hasVideo, options.format, options.drop_video, formatError, setFormatError])
|
|
|
|
return (
|
|
<Paper sx={{ p: 3 }}>
|
|
<Typography variant="h6" sx={{ mb: 2 }}>
|
|
⚙️ Options
|
|
</Typography>
|
|
|
|
{formatError && (
|
|
<Alert severity="warning" sx={{ mb: 2 }}>
|
|
{formatError}
|
|
</Alert>
|
|
)}
|
|
|
|
{/* Output Settings */}
|
|
<Section title="Output Settings" defaultExpanded>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<TextField
|
|
label="Format"
|
|
select
|
|
value={options.format}
|
|
onChange={handleFormatChange}
|
|
fullWidth
|
|
size="small"
|
|
error={!!formatError}
|
|
>
|
|
<MenuItem value="mp3">MP3</MenuItem>
|
|
<MenuItem value="m4a">M4A</MenuItem>
|
|
<MenuItem value="mkv">MKV</MenuItem>
|
|
<MenuItem value="mp4">MP4</MenuItem>
|
|
<MenuItem value="ogg">OGG</MenuItem>
|
|
<MenuItem value="opus">OPUS</MenuItem>
|
|
<MenuItem value="flac">FLAC</MenuItem>
|
|
<MenuItem value="wav">WAV</MenuItem>
|
|
<MenuItem value="aac">AAC</MenuItem>
|
|
</TextField>
|
|
|
|
<TextField
|
|
label="Transcode to"
|
|
select
|
|
value={options.transcode_to || ''}
|
|
onChange={(e) => handleChange('transcode_to', e.target.value || undefined)}
|
|
fullWidth
|
|
size="small"
|
|
>
|
|
<MenuItem value="">Copy (no transcoding)</MenuItem>
|
|
<MenuItem value="libmp3lame">MP3 (LAME)</MenuItem>
|
|
<MenuItem value="aac">AAC</MenuItem>
|
|
<MenuItem value="libopus">OPUS</MenuItem>
|
|
</TextField>
|
|
|
|
{hasVideo && (
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={options.drop_video}
|
|
onChange={handleDropVideoChange}
|
|
/>
|
|
}
|
|
label="Drop video streams"
|
|
/>
|
|
)}
|
|
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={options.drop_subs}
|
|
onChange={(e) => handleChange('drop_subs', e.target.checked)}
|
|
/>
|
|
}
|
|
label="Drop subtitle streams"
|
|
/>
|
|
</Box>
|
|
</Section>
|
|
|
|
{/* Filename Settings */}
|
|
<Section title="Filename Settings">
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<TextField
|
|
label="Output template"
|
|
value={options.output_template}
|
|
onChange={(e) => handleChange('output_template', e.target.value)}
|
|
fullWidth
|
|
size="small"
|
|
helperText="Placeholders: %tn (track name), %an (author), %al (album), %date, %ext, %num"
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={options.number_tracks}
|
|
onChange={(e) => handleChange('number_tracks', e.target.checked)}
|
|
/>
|
|
}
|
|
label="Number tracks (01 - )"
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={options.replace_bad_chars}
|
|
onChange={(e) => handleChange('replace_bad_chars', e.target.checked)}
|
|
/>
|
|
}
|
|
label="Replace bad characters"
|
|
/>
|
|
<TextField
|
|
label="Replacement character"
|
|
value={options.replacement_char}
|
|
onChange={(e) => handleChange('replacement_char', e.target.value)}
|
|
size="small"
|
|
disabled={!options.replace_bad_chars}
|
|
/>
|
|
<TextField
|
|
label="Bad characters list"
|
|
value={options.bad_chars}
|
|
onChange={(e) => handleChange('bad_chars', e.target.value)}
|
|
fullWidth
|
|
size="small"
|
|
disabled={!options.replace_bad_chars}
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={options.skip_existing}
|
|
onChange={(e) => handleChange('skip_existing', e.target.checked)}
|
|
/>
|
|
}
|
|
label="Skip existing files"
|
|
/>
|
|
</Box>
|
|
</Section>
|
|
|
|
{/* Metadata Settings */}
|
|
<Section title="Metadata Settings">
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<TextField
|
|
label="Album"
|
|
value={options.album}
|
|
onChange={(e) => handleChange('album', e.target.value)}
|
|
fullWidth
|
|
size="small"
|
|
/>
|
|
<TextField
|
|
label="Comment"
|
|
value={options.comment}
|
|
onChange={(e) => handleChange('comment', e.target.value)}
|
|
fullWidth
|
|
size="small"
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={options.no_comment}
|
|
onChange={(e) => handleChange('no_comment', e.target.checked)}
|
|
/>
|
|
}
|
|
label="No comment"
|
|
/>
|
|
<TextField
|
|
label="Comment stream index"
|
|
type="number"
|
|
value={options.comment_stream ?? ''}
|
|
onChange={(e) =>
|
|
handleChange('comment_stream', e.target.value === '' ? null : parseInt(e.target.value))
|
|
}
|
|
size="small"
|
|
disabled={options.no_comment}
|
|
/>
|
|
<FormControlLabel
|
|
control={
|
|
<Switch
|
|
checked={options.merge_comments}
|
|
onChange={(e) => handleChange('merge_comments', e.target.checked)}
|
|
/>
|
|
}
|
|
label="Merge all comments"
|
|
disabled={options.no_comment}
|
|
/>
|
|
<TextField
|
|
label="Comment separator"
|
|
value={options.comment_separator}
|
|
onChange={(e) => handleChange('comment_separator', e.target.value)}
|
|
size="small"
|
|
disabled={!options.merge_comments || options.no_comment}
|
|
/>
|
|
</Box>
|
|
</Section>
|
|
|
|
{/* Tracklist Settings */}
|
|
<Section title="Tracklist Settings" defaultExpanded>
|
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
|
<TextField
|
|
label="Tracklist Format"
|
|
value={options.tracklist_format}
|
|
onChange={(e) => handleChange('tracklist_format', e.target.value)}
|
|
fullWidth
|
|
size="small"
|
|
helperText="Placeholders: %ts (timestamp), %tn (track name), %an (author), %al (album), %date, %ext"
|
|
/>
|
|
</Box>
|
|
</Section>
|
|
</Paper>
|
|
)
|
|
|
|
// Helper function for option updates
|
|
function handleChange(field: string, value: any) {
|
|
setOptions({ [field]: value })
|
|
}
|
|
}
|