@@ -16,7 +16,11 @@ import { ExpandMore, ExpandLess } from '@mui/icons-material'
import { useOptionsStore } from '../stores/optionsStore'
import { useUploadStore } from '../stores/uploadStore'
import { useValidationStore } from '../stores/validationStore'
import { getFormats } from '../api/client'
// ------------------------------------------------------------------------------
// Section component (collapsible)
// ------------------------------------------------------------------------------
interface SectionProps {
title : string
children : React.ReactNode
@@ -51,54 +55,70 @@ const Section: React.FC<SectionProps> = ({ title, children, defaultExpanded = fa
)
}
// ------------------------------------------------------------------------------
// Main OptionsPanel component
// ------------------------------------------------------------------------------
export const OptionsPanel : React.FC = ( ) = > {
const { options , setOptions } = useOptionsStore ( )
const {
options ,
setOptions ,
containers ,
codecs ,
setContainers ,
setCodecs ,
} = useOptionsStore ( )
const { hasVideo } = useUploadStore ( )
const { formatError , setFormatError } = useValidationStore ( )
// Audio-only format s from backend c onstants (hardcoded for now)
const audioOnlyFormats = [ 'mp3' , 'm4a' , 'ogg' , 'opus' , 'flac' , 'wav' , 'aac' ]
// Fetch containers and codec s from backend on mount
useEffect ( ( ) = > {
const fetchFormats = async ( ) = > {
try {
const data = await getFormats ( )
setContainers ( data . containers )
setCodecs ( data . codecs )
} catch ( err ) {
console . error ( 'Failed to fetch formats:' , err )
}
}
fetchFormats ( )
} , [ setContainers , setCodecs ] )
// --------------------------------------------------------------
// Handlers
// --------------------------------------------------------------
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 ) {
}
const handleChange = ( field : string , value : any ) = > {
setOptions ( { [ field ] : value } )
}
// --------------------------------------------------------------
// Format validation – re-run when relevant state changes
// --------------------------------------------------------------
useEffect ( ( ) = > {
const selectedContainer = containers . find ( c = > c . name === options . format )
if ( selectedContainer ? . audio_only && hasVideo && ! options . drop_video ) {
setFormatError (
` Format '${ options . format } ' does not support video streams. Please enable "Drop video" or choose a container that supports video (e.g., MKV, MP4). `
` Container '${ 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 ] )
} , [ containers , options . format , options . drop_video , hasVideo , setFormatError ] )
// --------------------------------------------------------------
// Render
// --------------------------------------------------------------
return (
< Paper sx = { { p : 3 } } >
< Typography variant = "h6" sx = { { mb : 2 } } >
@@ -111,9 +131,12 @@ export const OptionsPanel: React.FC = () => {
< / Alert >
) }
{ /* Output Settings */ }
{ /* ------------------------------------------------------------------------
Output Settings
------------------------------------------------------------------------ */ }
< Section title = "Output Settings" defaultExpanded >
< Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 2 } } >
{ /* Container dropdown */ }
< TextField
label = "Container"
select
@@ -126,18 +149,14 @@ export const OptionsPanel: React.FC = () => {
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 >
{ containers . map ( ( container ) = > (
< MenuItem key = { container . name } value = { container . name } >
{ container . name . toUpperCase ( ) } ( { container . extension } )
< / MenuItem >
) ) }
< / TextField >
{ /* Transcode option – remains unchanged but clearly labeled */ }
{ /* Audio Codec (Transcode) dropdown */ }
< TextField
label = "Audio Codec (Transcode)"
select
@@ -148,11 +167,16 @@ export const OptionsPanel: React.FC = () => {
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 >
{ codecs
. filter ( c = > c . supports_transcoding )
. map ( ( codec ) = > (
< MenuItem key = { codec . name } value = { codec . ffmpeg } >
{ codec . name . toUpperCase ( ) } ( → { codec . recommended_container } )
< / MenuItem >
) ) }
< / TextField >
{ /* Drop video stream (only shown if video present) */ }
{ hasVideo && (
< FormControlLabel
control = {
@@ -165,6 +189,7 @@ export const OptionsPanel: React.FC = () => {
/ >
) }
{ /* Drop subtitle streams */ }
< FormControlLabel
control = {
< Switch
@@ -177,7 +202,9 @@ export const OptionsPanel: React.FC = () => {
< / Box >
< / Section >
{ /* Filename Settings */ }
{ /* ------------------------------------------------------------------------
Filename Settings
------------------------------------------------------------------------ */ }
< Section title = "Filename Settings" >
< Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 2 } } >
< TextField
@@ -233,7 +260,9 @@ export const OptionsPanel: React.FC = () => {
< / Box >
< / Section >
{ /* Metadata Settings */ }
{ /* ------------------------------------------------------------------------
Metadata Settings
------------------------------------------------------------------------ */ }
< Section title = "Metadata Settings" >
< Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 2 } } >
< TextField
@@ -289,7 +318,9 @@ export const OptionsPanel: React.FC = () => {
< / Box >
< / Section >
{ /* Tracklist Settings */ }
{ /* ------------------------------------------------------------------------
Tracklist Settings
------------------------------------------------------------------------ */ }
< Section title = "Tracklist Settings" defaultExpanded >
< Box sx = { { display : 'flex' , flexDirection : 'column' , gap : 2 } } >
< TextField
@@ -304,9 +335,4 @@ export const OptionsPanel: React.FC = () => {
< / Section >
< / Paper >
)
// Helper function for option updates
function handleChange ( field : string , value : any ) {
setOptions ( { [ field ] : value } )
}
}