Improve queue system reliability and add auto-resolution for DVD formats

This commit includes several improvements:

Queue System Enhancements:
- Improved thread-safety in Add, Remove, Pause, Resume, Cancel operations
- Added PauseAll and ResumeAll methods for batch control
- Added MoveUp and MoveDown methods to reorder queue items
- Better handling of running job cancellation with proper state management
- Improved Copy strategy in List() to prevent race conditions

Convert Module Enhancement:
- Auto-set resolution to 720×480 when NTSC DVD format selected
- Auto-set resolution to 720×576 when PAL DVD format selected
- Auto-set framerate to 29.97fps (30) for NTSC, 25fps for PAL
- Added DVD resolution options to resolution selector dropdown

Display Server Improvements:
- Auto-detect Wayland vs X11 display servers in player controller
- Conditionally apply xdotool window placement (X11 only)

UI Improvements:
- Added Pause All, Resume All, and queue reordering buttons
- Fixed queue counter labeling (completed count display)
This commit is contained in:
Stu Leak 2025-11-29 20:07:35 -05:00
parent 18b45db7de
commit 0bfdd3258d
6 changed files with 339 additions and 64 deletions

View File

@ -299,7 +299,13 @@ func (c *ffplayController) startLocked(offset float64) error {
env = append(env, fmt.Sprintf("SDL_VIDEO_WINDOW_POS=%s", pos)) env = append(env, fmt.Sprintf("SDL_VIDEO_WINDOW_POS=%s", pos))
} }
if os.Getenv("SDL_VIDEODRIVER") == "" { if os.Getenv("SDL_VIDEODRIVER") == "" {
env = append(env, "SDL_VIDEODRIVER=x11") // Auto-detect display server and set appropriate SDL video driver
if os.Getenv("WAYLAND_DISPLAY") != "" {
env = append(env, "SDL_VIDEODRIVER=wayland")
} else {
// Default to X11 for compatibility, but Wayland takes precedence if available
env = append(env, "SDL_VIDEODRIVER=x11")
}
} }
if os.Getenv("XDG_RUNTIME_DIR") == "" { if os.Getenv("XDG_RUNTIME_DIR") == "" {
run := fmt.Sprintf("/run/user/%d", os.Getuid()) run := fmt.Sprintf("/run/user/%d", os.Getuid())
@ -330,8 +336,9 @@ func (c *ffplayController) startLocked(offset float64) error {
c.ctx = ctx c.ctx = ctx
c.cancel = cancel c.cancel = cancel
// Best-effort window placement via xdotool in case WM ignores SDL hints. // Best-effort window placement via xdotool (X11 only) if available and not on Wayland.
if c.winW > 0 && c.winH > 0 { // Wayland compositors don't support window manipulation via xdotool.
if c.winW > 0 && c.winH > 0 && os.Getenv("WAYLAND_DISPLAY") == "" {
go func(title string, x, y, w, h int) { go func(title string, x, y, w, h int) {
time.Sleep(120 * time.Millisecond) time.Sleep(120 * time.Millisecond)
ffID := pickLastID(exec.Command("xdotool", "search", "--name", title)) ffID := pickLastID(exec.Command("xdotool", "search", "--name", title))

View File

@ -94,7 +94,6 @@ func (q *Queue) notifyChange() {
// Add adds a job to the queue // Add adds a job to the queue
func (q *Queue) Add(job *Job) { func (q *Queue) Add(job *Job) {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
if job.ID == "" { if job.ID == "" {
job.ID = generateID() job.ID = generateID()
@ -107,13 +106,16 @@ func (q *Queue) Add(job *Job) {
} }
q.jobs = append(q.jobs, job) q.jobs = append(q.jobs, job)
q.rebalancePrioritiesLocked()
q.mu.Unlock()
q.notifyChange() q.notifyChange()
} }
// Remove removes a job from the queue by ID // Remove removes a job from the queue by ID
func (q *Queue) Remove(id string) error { func (q *Queue) Remove(id string) error {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
var removed bool
for i, job := range q.jobs { for i, job := range q.jobs {
if job.ID == id { if job.ID == id {
@ -122,10 +124,16 @@ func (q *Queue) Remove(id string) error {
job.cancel() job.cancel()
} }
q.jobs = append(q.jobs[:i], q.jobs[i+1:]...) q.jobs = append(q.jobs[:i], q.jobs[i+1:]...)
q.notifyChange() q.rebalancePrioritiesLocked()
return nil removed = true
break
} }
} }
q.mu.Unlock()
if removed {
q.notifyChange()
return nil
}
return fmt.Errorf("job not found: %s", id) return fmt.Errorf("job not found: %s", id)
} }
@ -147,8 +155,12 @@ func (q *Queue) List() []*Job {
q.mu.RLock() q.mu.RLock()
defer q.mu.RUnlock() defer q.mu.RUnlock()
// Return a copy of the jobs to avoid races on the live queue state
result := make([]*Job, len(q.jobs)) result := make([]*Job, len(q.jobs))
copy(result, q.jobs) for i, job := range q.jobs {
clone := *job
result[i] = &clone
}
return result return result
} }
@ -175,57 +187,79 @@ func (q *Queue) Stats() (pending, running, completed, failed int) {
// Pause pauses a running job // Pause pauses a running job
func (q *Queue) Pause(id string) error { func (q *Queue) Pause(id string) error {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
result := fmt.Errorf("job not found: %s", id)
for _, job := range q.jobs { for _, job := range q.jobs {
if job.ID == id { if job.ID == id {
if job.Status != JobStatusRunning { if job.Status != JobStatusRunning {
return fmt.Errorf("job is not running") result = fmt.Errorf("job is not running")
break
} }
if job.cancel != nil { if job.cancel != nil {
job.cancel() job.cancel()
} }
job.Status = JobStatusPaused job.Status = JobStatusPaused
q.notifyChange() // Keep position; just stop current run
return nil result = nil
break
} }
} }
return fmt.Errorf("job not found: %s", id) q.mu.Unlock()
if result == nil {
q.notifyChange()
}
return result
} }
// Resume resumes a paused job // Resume resumes a paused job
func (q *Queue) Resume(id string) error { func (q *Queue) Resume(id string) error {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
result := fmt.Errorf("job not found: %s", id)
for _, job := range q.jobs { for _, job := range q.jobs {
if job.ID == id { if job.ID == id {
if job.Status != JobStatusPaused { if job.Status != JobStatusPaused {
return fmt.Errorf("job is not paused") result = fmt.Errorf("job is not paused")
break
} }
job.Status = JobStatusPending job.Status = JobStatusPending
q.notifyChange() // Keep position; move selection via priorities
return nil result = nil
break
} }
} }
return fmt.Errorf("job not found: %s", id) q.mu.Unlock()
if result == nil {
q.notifyChange()
}
return result
} }
// Cancel cancels a job // Cancel cancels a job
func (q *Queue) Cancel(id string) error { func (q *Queue) Cancel(id string) error {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
var cancelled bool
now := time.Now()
for _, job := range q.jobs { for _, job := range q.jobs {
if job.ID == id { if job.ID == id {
if job.Status == JobStatusRunning && job.cancel != nil { if job.Status == JobStatusRunning && job.cancel != nil {
job.cancel() job.cancel()
} }
job.Status = JobStatusCancelled job.Status = JobStatusCancelled
q.notifyChange() job.CompletedAt = &now
return nil q.rebalancePrioritiesLocked()
cancelled = true
break
} }
} }
q.mu.Unlock()
if cancelled {
q.notifyChange()
return nil
}
return fmt.Errorf("job not found: %s", id) return fmt.Errorf("job not found: %s", id)
} }
@ -249,6 +283,37 @@ func (q *Queue) Stop() {
q.running = false q.running = false
} }
// PauseAll pauses any running job and stops processing
func (q *Queue) PauseAll() {
q.mu.Lock()
for _, job := range q.jobs {
if job.Status == JobStatusRunning && job.cancel != nil {
job.cancel()
job.Status = JobStatusPaused
job.cancel = nil
job.StartedAt = nil
job.CompletedAt = nil
job.Error = ""
}
}
q.running = false
q.mu.Unlock()
q.notifyChange()
}
// ResumeAll restarts processing the queue
func (q *Queue) ResumeAll() {
q.mu.Lock()
if q.running {
q.mu.Unlock()
return
}
q.running = true
q.mu.Unlock()
q.notifyChange()
go q.processJobs()
}
// processJobs continuously processes pending jobs // processJobs continuously processes pending jobs
func (q *Queue) processJobs() { func (q *Queue) processJobs() {
for { for {
@ -295,13 +360,28 @@ func (q *Queue) processJobs() {
// Update job status // Update job status
q.mu.Lock() q.mu.Lock()
now = time.Now() now = time.Now()
nextJob.CompletedAt = &now
if err != nil { if err != nil {
nextJob.Status = JobStatusFailed if ctx.Err() == context.Canceled {
nextJob.Error = err.Error() if nextJob.Status == JobStatusPaused {
// Leave as paused without timestamps/error
nextJob.StartedAt = nil
nextJob.CompletedAt = nil
nextJob.Error = ""
} else {
// Cancelled
nextJob.Status = JobStatusCancelled
nextJob.CompletedAt = &now
nextJob.Error = ""
}
} else {
nextJob.Status = JobStatusFailed
nextJob.CompletedAt = &now
nextJob.Error = err.Error()
}
} else { } else {
nextJob.Status = JobStatusCompleted nextJob.Status = JobStatusCompleted
nextJob.Progress = 100.0 nextJob.Progress = 100.0
nextJob.CompletedAt = &now
} }
nextJob.cancel = nil nextJob.cancel = nil
q.mu.Unlock() q.mu.Unlock()
@ -309,6 +389,44 @@ func (q *Queue) processJobs() {
} }
} }
// MoveUp moves a pending or paused job one position up in the queue
func (q *Queue) MoveUp(id string) error {
return q.move(id, -1)
}
// MoveDown moves a pending or paused job one position down in the queue
func (q *Queue) MoveDown(id string) error {
return q.move(id, 1)
}
func (q *Queue) move(id string, delta int) error {
q.mu.Lock()
defer q.mu.Unlock()
var idx int = -1
for i, job := range q.jobs {
if job.ID == id {
idx = i
if job.Status != JobStatusPending && job.Status != JobStatusPaused {
return fmt.Errorf("job must be pending or paused to reorder")
}
break
}
}
if idx == -1 {
return fmt.Errorf("job not found: %s", id)
}
newIdx := idx + delta
if newIdx < 0 || newIdx >= len(q.jobs) {
return nil // already at boundary; no-op
}
q.jobs[idx], q.jobs[newIdx] = q.jobs[newIdx], q.jobs[idx]
q.rebalancePrioritiesLocked()
return nil
}
// Save saves the queue to a JSON file // Save saves the queue to a JSON file
func (q *Queue) Save(path string) error { func (q *Queue) Save(path string) error {
q.mu.RLock() q.mu.RLock()
@ -348,7 +466,6 @@ func (q *Queue) Load(path string) error {
} }
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
// Reset running jobs to pending // Reset running jobs to pending
for _, job := range jobs { for _, job := range jobs {
@ -359,6 +476,8 @@ func (q *Queue) Load(path string) error {
} }
q.jobs = jobs q.jobs = jobs
q.rebalancePrioritiesLocked()
q.mu.Unlock()
q.notifyChange() q.notifyChange()
return nil return nil
} }
@ -366,7 +485,9 @@ func (q *Queue) Load(path string) error {
// Clear removes all completed, failed, and cancelled jobs // Clear removes all completed, failed, and cancelled jobs
func (q *Queue) Clear() { func (q *Queue) Clear() {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
// Cancel any running jobs before filtering
q.cancelRunningLocked()
filtered := make([]*Job, 0) filtered := make([]*Job, 0)
for _, job := range q.jobs { for _, job := range q.jobs {
@ -375,15 +496,22 @@ func (q *Queue) Clear() {
} }
} }
q.jobs = filtered q.jobs = filtered
q.rebalancePrioritiesLocked()
q.mu.Unlock()
q.notifyChange() q.notifyChange()
} }
// ClearAll removes all jobs from the queue // ClearAll removes all jobs from the queue
func (q *Queue) ClearAll() { func (q *Queue) ClearAll() {
q.mu.Lock() q.mu.Lock()
defer q.mu.Unlock()
// Cancel any running work and stop the processor
q.cancelRunningLocked()
q.running = false
q.jobs = make([]*Job, 0) q.jobs = make([]*Job, 0)
q.rebalancePrioritiesLocked()
q.mu.Unlock()
q.notifyChange() q.notifyChange()
} }
@ -391,3 +519,24 @@ func (q *Queue) ClearAll() {
func generateID() string { func generateID() string {
return fmt.Sprintf("job-%d", time.Now().UnixNano()) return fmt.Sprintf("job-%d", time.Now().UnixNano())
} }
// rebalancePrioritiesLocked assigns descending priorities so earlier items are selected first
func (q *Queue) rebalancePrioritiesLocked() {
for i := range q.jobs {
q.jobs[i].Priority = len(q.jobs) - i
}
}
// cancelRunningLocked cancels any currently running job and marks it cancelled.
func (q *Queue) cancelRunningLocked() {
now := time.Now()
for _, job := range q.jobs {
if job.Status == JobStatusRunning {
if job.cancel != nil {
job.cancel()
}
job.Status = JobStatusCancelled
job.CompletedAt = &now
}
}
}

View File

@ -20,12 +20,12 @@ type ModuleInfo struct {
} }
// BuildMainMenu creates the main menu view with module tiles // BuildMainMenu creates the main menu view with module tiles
func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDrop func(string, []fyne.URI), onQueueClick func(), titleColor, queueColor, textColor color.Color, queueActive, queueTotal int) fyne.CanvasObject { func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDrop func(string, []fyne.URI), onQueueClick func(), titleColor, queueColor, textColor color.Color, queueCompleted, queueTotal int) fyne.CanvasObject {
title := canvas.NewText("VIDEOTOOLS", titleColor) title := canvas.NewText("VIDEOTOOLS", titleColor)
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
title.TextSize = 28 title.TextSize = 28
queueTile := buildQueueTile(queueActive, queueTotal, queueColor, textColor, onQueueClick) queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
header := container.New(layout.NewHBoxLayout(), header := container.New(layout.NewHBoxLayout(),
title, title,
@ -70,12 +70,12 @@ func buildModuleTile(mod ModuleInfo, tapped func(), dropped func([]fyne.URI)) fy
} }
// buildQueueTile creates the queue status tile // buildQueueTile creates the queue status tile
func buildQueueTile(active, total int, queueColor, textColor color.Color, onClick func()) fyne.CanvasObject { func buildQueueTile(completed, total int, queueColor, textColor color.Color, onClick func()) fyne.CanvasObject {
rect := canvas.NewRectangle(queueColor) rect := canvas.NewRectangle(queueColor)
rect.CornerRadius = 8 rect.CornerRadius = 8
rect.SetMinSize(fyne.NewSize(160, 60)) rect.SetMinSize(fyne.NewSize(160, 60))
text := canvas.NewText(fmt.Sprintf("QUEUE: %d/%d", active, total), textColor) text := canvas.NewText(fmt.Sprintf("QUEUE: %d/%d", completed, total), textColor)
text.Alignment = fyne.TextAlignCenter text.Alignment = fyne.TextAlignCenter
text.TextStyle = fyne.TextStyle{Monospace: true, Bold: true} text.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
text.TextSize = 18 text.TextSize = 18

View File

@ -8,6 +8,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container" "fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/widget" "fyne.io/fyne/v2/widget"
"git.leaktechnologies.dev/stu/VideoTools/internal/queue" "git.leaktechnologies.dev/stu/VideoTools/internal/queue"
) )
@ -20,6 +21,11 @@ func BuildQueueView(
onResume func(string), onResume func(string),
onCancel func(string), onCancel func(string),
onRemove func(string), onRemove func(string),
onMoveUp func(string),
onMoveDown func(string),
onPauseAll func(),
onResumeAll func(),
onStart func(),
onClear func(), onClear func(),
onClearAll func(), onClearAll func(),
titleColor, bgColor, textColor color.Color, titleColor, bgColor, textColor color.Color,
@ -32,16 +38,27 @@ func BuildQueueView(
backBtn := widget.NewButton("← Back", onBack) backBtn := widget.NewButton("← Back", onBack)
backBtn.Importance = widget.LowImportance backBtn.Importance = widget.LowImportance
startAllBtn := widget.NewButton("Start Queue", onStart)
startAllBtn.Importance = widget.MediumImportance
pauseAllBtn := widget.NewButton("Pause All", onPauseAll)
pauseAllBtn.Importance = widget.LowImportance
resumeAllBtn := widget.NewButton("Resume All", onResumeAll)
resumeAllBtn.Importance = widget.LowImportance
clearBtn := widget.NewButton("Clear Completed", onClear) clearBtn := widget.NewButton("Clear Completed", onClear)
clearBtn.Importance = widget.LowImportance clearBtn.Importance = widget.LowImportance
clearAllBtn := widget.NewButton("Clear All", onClearAll) clearAllBtn := widget.NewButton("Clear All", onClearAll)
clearAllBtn.Importance = widget.DangerImportance clearAllBtn.Importance = widget.DangerImportance
buttonRow := container.NewHBox(startAllBtn, pauseAllBtn, resumeAllBtn, clearAllBtn, clearBtn)
header := container.NewBorder( header := container.NewBorder(
nil, nil, nil, nil,
backBtn, backBtn,
container.NewHBox(clearBtn, clearAllBtn), buttonRow,
container.NewCenter(title), container.NewCenter(title),
) )
@ -54,7 +71,7 @@ func BuildQueueView(
jobItems = append(jobItems, container.NewCenter(emptyMsg)) jobItems = append(jobItems, container.NewCenter(emptyMsg))
} else { } else {
for _, job := range jobs { for _, job := range jobs {
jobItems = append(jobItems, buildJobItem(job, onPause, onResume, onCancel, onRemove, bgColor, textColor)) jobItems = append(jobItems, buildJobItem(job, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, bgColor, textColor))
} }
} }
@ -77,6 +94,8 @@ func buildJobItem(
onResume func(string), onResume func(string),
onCancel func(string), onCancel func(string),
onRemove func(string), onRemove func(string),
onMoveUp func(string),
onMoveDown func(string),
bgColor, textColor color.Color, bgColor, textColor color.Color,
) fyne.CanvasObject { ) fyne.CanvasObject {
// Status color // Status color
@ -94,18 +113,15 @@ func buildJobItem(
descLabel.TextStyle = fyne.TextStyle{Italic: true} descLabel.TextStyle = fyne.TextStyle{Italic: true}
// Progress bar (for running jobs) // Progress bar (for running jobs)
var progressWidget fyne.CanvasObject progress := widget.NewProgressBar()
if job.Status == queue.JobStatusRunning { progress.SetValue(job.Progress / 100.0)
progress := widget.NewProgressBar() if job.Status == queue.JobStatusCompleted {
progress.SetValue(job.Progress / 100.0)
progressWidget = progress
} else if job.Status == queue.JobStatusCompleted {
progress := widget.NewProgressBar()
progress.SetValue(1.0) progress.SetValue(1.0)
progressWidget = progress
} else {
progressWidget = widget.NewLabel("")
} }
progressWidget := progress
// Module badge
badge := buildModuleBadge(job.Type)
// Status text // Status text
statusText := getStatusText(job) statusText := getStatusText(job)
@ -114,6 +130,14 @@ func buildJobItem(
// Control buttons // Control buttons
var buttons []fyne.CanvasObject var buttons []fyne.CanvasObject
// Reorder arrows for pending/paused jobs
if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused {
buttons = append(buttons,
widget.NewButton("↑", func() { onMoveUp(job.ID) }),
widget.NewButton("↓", func() { onMoveDown(job.ID) }),
)
}
switch job.Status { switch job.Status {
case queue.JobStatusRunning: case queue.JobStatusRunning:
buttons = append(buttons, buttons = append(buttons,
@ -139,7 +163,7 @@ func buildJobItem(
// Info section // Info section
infoBox := container.NewVBox( infoBox := container.NewVBox(
titleLabel, container.NewHBox(titleLabel, layout.NewSpacer(), badge),
descLabel, descLabel,
progressWidget, progressWidget,
statusLabel, statusLabel,
@ -161,7 +185,14 @@ func buildJobItem(
container.NewMax(card, content), container.NewMax(card, content),
) )
return item // Wrap with draggable to allow drag-to-reorder (up/down by drag direction)
return newDraggableJobItem(job.ID, item, func(id string, dir int) {
if dir < 0 {
onMoveUp(id)
} else if dir > 0 {
onMoveDown(id)
}
})
} }
// getStatusColor returns the color for a job status // getStatusColor returns the color for a job status
@ -211,3 +242,76 @@ func getStatusText(job *queue.Job) string {
return fmt.Sprintf("Status: %s", job.Status) return fmt.Sprintf("Status: %s", job.Status)
} }
} }
// buildModuleBadge renders a small colored pill to show which module created the job.
func buildModuleBadge(t queue.JobType) fyne.CanvasObject {
label := widget.NewLabel(string(t))
label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter
bg := canvas.NewRectangle(moduleColor(t))
bg.CornerRadius = 6
bg.SetMinSize(fyne.NewSize(label.MinSize().Width+12, label.MinSize().Height+6))
return container.NewMax(bg, container.NewCenter(label))
}
// moduleColor maps job types to distinct colors for quick visual scanning.
func moduleColor(t queue.JobType) color.Color {
switch t {
case queue.JobTypeConvert:
return color.RGBA{R: 76, G: 232, B: 112, A: 255} // green
case queue.JobTypeMerge:
return color.RGBA{R: 68, G: 136, B: 255, A: 255} // blue
case queue.JobTypeTrim:
return color.RGBA{R: 255, G: 193, B: 7, A: 255} // amber
case queue.JobTypeFilter:
return color.RGBA{R: 160, G: 86, B: 255, A: 255} // purple
case queue.JobTypeUpscale:
return color.RGBA{R: 255, G: 138, B: 101, A: 255} // coral
case queue.JobTypeAudio:
return color.RGBA{R: 255, G: 215, B: 64, A: 255} // gold
case queue.JobTypeThumb:
return color.RGBA{R: 102, G: 217, B: 239, A: 255} // teal
default:
return color.Gray{Y: 180}
}
}
// draggableJobItem allows simple drag up/down to reorder one slot at a time.
type draggableJobItem struct {
widget.BaseWidget
jobID string
content fyne.CanvasObject
onReorder func(string, int) // id, direction (-1 up, +1 down)
accumY float32
}
func newDraggableJobItem(id string, content fyne.CanvasObject, onReorder func(string, int)) *draggableJobItem {
d := &draggableJobItem{
jobID: id,
content: content,
onReorder: onReorder,
}
d.ExtendBaseWidget(d)
return d
}
func (d *draggableJobItem) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(d.content)
}
func (d *draggableJobItem) Dragged(ev *fyne.DragEvent) {
// fyne.Delta is a struct with dx, dy fields
d.accumY += ev.Dragged.DY
}
func (d *draggableJobItem) DragEnd() {
const threshold float32 = 25
if d.accumY <= -threshold {
d.onReorder(d.jobID, -1)
} else if d.accumY >= threshold {
d.onReorder(d.jobID, 1)
}
d.accumY = 0
}

53
main.go
View File

@ -1413,23 +1413,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
dvdAspectBox := container.NewVBox(dvdAspectLabel, dvdAspectSelect, dvdInfoLabel) dvdAspectBox := container.NewVBox(dvdAspectLabel, dvdAspectSelect, dvdInfoLabel)
dvdAspectBox.Hide() // Hidden by default dvdAspectBox.Hide() // Hidden by default
// Show/hide DVD options based on format selection // Placeholder for updateDVDOptions - will be defined after resolution/framerate selects are created
updateDVDOptions := func() { var updateDVDOptions func()
isDVD := state.convert.SelectedFormat.Ext == ".mpg"
if isDVD {
dvdAspectBox.Show()
// Update DVD info based on which DVD format was selected
if strings.Contains(state.convert.SelectedFormat.Label, "NTSC") {
dvdInfoLabel.SetText("NTSC: 720×480 @ 29.97fps, MPEG-2, AC-3 Stereo 48kHz\nBitrate: 6000k (default), 9000k (max PS2-safe)\nCompatible with DVDStyler, PS2, standalone DVD players")
} else if strings.Contains(state.convert.SelectedFormat.Label, "PAL") {
dvdInfoLabel.SetText("PAL: 720×576 @ 25.00fps, MPEG-2, AC-3 Stereo 48kHz\nBitrate: 8000k (default), 9500k (max PS2-safe)\nCompatible with European DVD players and authoring tools")
} else {
dvdInfoLabel.SetText("DVD Format selected")
}
} else {
dvdAspectBox.Hide()
}
}
// Create formatSelect with callback that updates DVD options // Create formatSelect with callback that updates DVD options
formatSelect := widget.NewSelect(formatLabels, func(value string) { formatSelect := widget.NewSelect(formatLabels, func(value string) {
@ -1438,7 +1423,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
logging.Debug(logging.CatUI, "format set to %s", value) logging.Debug(logging.CatUI, "format set to %s", value)
state.convert.SelectedFormat = opt state.convert.SelectedFormat = opt
outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile())) outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile()))
updateDVDOptions() // Show/hide DVD options if updateDVDOptions != nil {
updateDVDOptions() // Show/hide DVD options and auto-set resolution
}
break break
} }
} }
@ -1573,7 +1560,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
// Target Resolution // Target Resolution
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K"}, func(value string) { resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
state.convert.TargetResolution = value state.convert.TargetResolution = value
logging.Debug(logging.CatUI, "target resolution set to %s", value) logging.Debug(logging.CatUI, "target resolution set to %s", value)
}) })
@ -1627,6 +1614,34 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}) })
audioChannelsSelect.SetSelected(state.convert.AudioChannels) audioChannelsSelect.SetSelected(state.convert.AudioChannels)
// Now define updateDVDOptions with access to resolution and framerate selects
updateDVDOptions = func() {
isDVD := state.convert.SelectedFormat.Ext == ".mpg"
if isDVD {
dvdAspectBox.Show()
// Auto-set resolution and framerate based on DVD format
if strings.Contains(state.convert.SelectedFormat.Label, "NTSC") {
dvdInfoLabel.SetText("NTSC: 720×480 @ 29.97fps, MPEG-2, AC-3 Stereo 48kHz\nBitrate: 6000k (default), 9000k (max PS2-safe)\nCompatible with DVDStyler, PS2, standalone DVD players")
// Auto-set to NTSC resolution
resolutionSelect.SetSelected("NTSC (720×480)")
frameRateSelect.SetSelected("30") // Will be converted to 29.97fps
state.convert.TargetResolution = "NTSC (720×480)"
state.convert.FrameRate = "30"
} else if strings.Contains(state.convert.SelectedFormat.Label, "PAL") {
dvdInfoLabel.SetText("PAL: 720×576 @ 25.00fps, MPEG-2, AC-3 Stereo 48kHz\nBitrate: 8000k (default), 9500k (max PS2-safe)\nCompatible with European DVD players and authoring tools")
// Auto-set to PAL resolution
resolutionSelect.SetSelected("PAL (720×576)")
frameRateSelect.SetSelected("25")
state.convert.TargetResolution = "PAL (720×576)"
state.convert.FrameRate = "25"
} else {
dvdInfoLabel.SetText("DVD Format selected")
}
} else {
dvdAspectBox.Hide()
}
}
// Advanced mode options - full controls with organized sections // Advanced mode options - full controls with organized sections
advancedOptions := container.NewVBox( advancedOptions := container.NewVBox(
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),

Binary file not shown.