Files
audio_splitter/web/frontend/src/components/OptionsPanel.tsx
T

313 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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="Container"
select
value={options.format}
onChange={handleFormatChange}
fullWidth
size="small"
error={!!formatError}
helperText={
formatError || "Determines the output file extension and container structure."
}
>
<MenuItem value="mp3">MP3 (.mp3)</MenuItem>
<MenuItem value="m4a">M4A (.m4a)</MenuItem>
<MenuItem value="mkv">MKV (.mkv)</MenuItem>
<MenuItem value="mp4">MP4 (.mp4)</MenuItem>
<MenuItem value="ogg">OGG (.ogg)</MenuItem>
<MenuItem value="opus">OPUS (.opus)</MenuItem>
<MenuItem value="flac">FLAC (.flac)</MenuItem>
<MenuItem value="wav">WAV (.wav)</MenuItem>
<MenuItem value="aac">AAC (.aac)</MenuItem>
</TextField>
{/* Transcode option remains unchanged but clearly labeled */}
<TextField
label="Audio Codec (Transcode)"
select
value={options.transcode_to || ''}
onChange={(e) => handleChange('transcode_to', e.target.value || undefined)}
fullWidth
size="small"
helperText="Select an audio codec to re-encode, or keep 'Copy' to preserve the original."
>
<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 })
}
}