forked from Leak_Technologies/VideoTools
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:
parent
3f4ad59fcd
commit
d327d7f65e
|
|
@ -299,7 +299,13 @@ func (c *ffplayController) startLocked(offset float64) error {
|
|||
env = append(env, fmt.Sprintf("SDL_VIDEO_WINDOW_POS=%s", pos))
|
||||
}
|
||||
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") == "" {
|
||||
run := fmt.Sprintf("/run/user/%d", os.Getuid())
|
||||
|
|
@ -330,8 +336,9 @@ func (c *ffplayController) startLocked(offset float64) error {
|
|||
c.ctx = ctx
|
||||
c.cancel = cancel
|
||||
|
||||
// Best-effort window placement via xdotool in case WM ignores SDL hints.
|
||||
if c.winW > 0 && c.winH > 0 {
|
||||
// Best-effort window placement via xdotool (X11 only) if available and not on Wayland.
|
||||
// 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) {
|
||||
time.Sleep(120 * time.Millisecond)
|
||||
ffID := pickLastID(exec.Command("xdotool", "search", "--name", title))
|
||||
|
|
|
|||
|
|
@ -94,7 +94,6 @@ func (q *Queue) notifyChange() {
|
|||
// Add adds a job to the queue
|
||||
func (q *Queue) Add(job *Job) {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
if job.ID == "" {
|
||||
job.ID = generateID()
|
||||
|
|
@ -107,13 +106,16 @@ func (q *Queue) Add(job *Job) {
|
|||
}
|
||||
|
||||
q.jobs = append(q.jobs, job)
|
||||
q.rebalancePrioritiesLocked()
|
||||
q.mu.Unlock()
|
||||
q.notifyChange()
|
||||
}
|
||||
|
||||
// Remove removes a job from the queue by ID
|
||||
func (q *Queue) Remove(id string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
var removed bool
|
||||
|
||||
for i, job := range q.jobs {
|
||||
if job.ID == id {
|
||||
|
|
@ -122,10 +124,16 @@ func (q *Queue) Remove(id string) error {
|
|||
job.cancel()
|
||||
}
|
||||
q.jobs = append(q.jobs[:i], q.jobs[i+1:]...)
|
||||
q.notifyChange()
|
||||
return nil
|
||||
q.rebalancePrioritiesLocked()
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
q.mu.Unlock()
|
||||
if removed {
|
||||
q.notifyChange()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("job not found: %s", id)
|
||||
}
|
||||
|
||||
|
|
@ -147,8 +155,12 @@ func (q *Queue) List() []*Job {
|
|||
q.mu.RLock()
|
||||
defer q.mu.RUnlock()
|
||||
|
||||
// Return a copy of the jobs to avoid races on the live queue state
|
||||
result := make([]*Job, len(q.jobs))
|
||||
copy(result, q.jobs)
|
||||
for i, job := range q.jobs {
|
||||
clone := *job
|
||||
result[i] = &clone
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
@ -175,57 +187,79 @@ func (q *Queue) Stats() (pending, running, completed, failed int) {
|
|||
// Pause pauses a running job
|
||||
func (q *Queue) Pause(id string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
result := fmt.Errorf("job not found: %s", id)
|
||||
|
||||
for _, job := range q.jobs {
|
||||
if job.ID == id {
|
||||
if job.Status != JobStatusRunning {
|
||||
return fmt.Errorf("job is not running")
|
||||
result = fmt.Errorf("job is not running")
|
||||
break
|
||||
}
|
||||
if job.cancel != nil {
|
||||
job.cancel()
|
||||
}
|
||||
job.Status = JobStatusPaused
|
||||
q.notifyChange()
|
||||
return nil
|
||||
// Keep position; just stop current run
|
||||
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
|
||||
func (q *Queue) Resume(id string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
result := fmt.Errorf("job not found: %s", id)
|
||||
|
||||
for _, job := range q.jobs {
|
||||
if job.ID == id {
|
||||
if job.Status != JobStatusPaused {
|
||||
return fmt.Errorf("job is not paused")
|
||||
result = fmt.Errorf("job is not paused")
|
||||
break
|
||||
}
|
||||
job.Status = JobStatusPending
|
||||
q.notifyChange()
|
||||
return nil
|
||||
// Keep position; move selection via priorities
|
||||
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
|
||||
func (q *Queue) Cancel(id string) error {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
var cancelled bool
|
||||
now := time.Now()
|
||||
for _, job := range q.jobs {
|
||||
if job.ID == id {
|
||||
if job.Status == JobStatusRunning && job.cancel != nil {
|
||||
job.cancel()
|
||||
}
|
||||
job.Status = JobStatusCancelled
|
||||
q.notifyChange()
|
||||
return nil
|
||||
job.CompletedAt = &now
|
||||
q.rebalancePrioritiesLocked()
|
||||
cancelled = true
|
||||
break
|
||||
}
|
||||
}
|
||||
q.mu.Unlock()
|
||||
if cancelled {
|
||||
q.notifyChange()
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("job not found: %s", id)
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +283,37 @@ func (q *Queue) Stop() {
|
|||
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
|
||||
func (q *Queue) processJobs() {
|
||||
for {
|
||||
|
|
@ -295,13 +360,28 @@ func (q *Queue) processJobs() {
|
|||
// Update job status
|
||||
q.mu.Lock()
|
||||
now = time.Now()
|
||||
nextJob.CompletedAt = &now
|
||||
if err != nil {
|
||||
nextJob.Status = JobStatusFailed
|
||||
nextJob.Error = err.Error()
|
||||
if ctx.Err() == context.Canceled {
|
||||
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 {
|
||||
nextJob.Status = JobStatusCompleted
|
||||
nextJob.Progress = 100.0
|
||||
nextJob.CompletedAt = &now
|
||||
}
|
||||
nextJob.cancel = nil
|
||||
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
|
||||
func (q *Queue) Save(path string) error {
|
||||
q.mu.RLock()
|
||||
|
|
@ -348,7 +466,6 @@ func (q *Queue) Load(path string) error {
|
|||
}
|
||||
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
// Reset running jobs to pending
|
||||
for _, job := range jobs {
|
||||
|
|
@ -359,6 +476,8 @@ func (q *Queue) Load(path string) error {
|
|||
}
|
||||
|
||||
q.jobs = jobs
|
||||
q.rebalancePrioritiesLocked()
|
||||
q.mu.Unlock()
|
||||
q.notifyChange()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -366,7 +485,9 @@ func (q *Queue) Load(path string) error {
|
|||
// Clear removes all completed, failed, and cancelled jobs
|
||||
func (q *Queue) Clear() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
// Cancel any running jobs before filtering
|
||||
q.cancelRunningLocked()
|
||||
|
||||
filtered := make([]*Job, 0)
|
||||
for _, job := range q.jobs {
|
||||
|
|
@ -375,15 +496,22 @@ func (q *Queue) Clear() {
|
|||
}
|
||||
}
|
||||
q.jobs = filtered
|
||||
q.rebalancePrioritiesLocked()
|
||||
q.mu.Unlock()
|
||||
q.notifyChange()
|
||||
}
|
||||
|
||||
// ClearAll removes all jobs from the queue
|
||||
func (q *Queue) ClearAll() {
|
||||
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.rebalancePrioritiesLocked()
|
||||
q.mu.Unlock()
|
||||
q.notifyChange()
|
||||
}
|
||||
|
||||
|
|
@ -391,3 +519,24 @@ func (q *Queue) ClearAll() {
|
|||
func generateID() string {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,12 +20,12 @@ type ModuleInfo struct {
|
|||
}
|
||||
|
||||
// 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.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||
title.TextSize = 28
|
||||
|
||||
queueTile := buildQueueTile(queueActive, queueTotal, queueColor, textColor, onQueueClick)
|
||||
queueTile := buildQueueTile(queueCompleted, queueTotal, queueColor, textColor, onQueueClick)
|
||||
|
||||
header := container.New(layout.NewHBoxLayout(),
|
||||
title,
|
||||
|
|
@ -70,12 +70,12 @@ func buildModuleTile(mod ModuleInfo, tapped func(), dropped func([]fyne.URI)) fy
|
|||
}
|
||||
|
||||
// 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.CornerRadius = 8
|
||||
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.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||
text.TextSize = 18
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
|
||||
)
|
||||
|
|
@ -20,6 +21,11 @@ func BuildQueueView(
|
|||
onResume func(string),
|
||||
onCancel func(string),
|
||||
onRemove func(string),
|
||||
onMoveUp func(string),
|
||||
onMoveDown func(string),
|
||||
onPauseAll func(),
|
||||
onResumeAll func(),
|
||||
onStart func(),
|
||||
onClear func(),
|
||||
onClearAll func(),
|
||||
titleColor, bgColor, textColor color.Color,
|
||||
|
|
@ -32,16 +38,27 @@ func BuildQueueView(
|
|||
backBtn := widget.NewButton("← Back", onBack)
|
||||
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.Importance = widget.LowImportance
|
||||
|
||||
clearAllBtn := widget.NewButton("Clear All", onClearAll)
|
||||
clearAllBtn.Importance = widget.DangerImportance
|
||||
|
||||
buttonRow := container.NewHBox(startAllBtn, pauseAllBtn, resumeAllBtn, clearAllBtn, clearBtn)
|
||||
|
||||
header := container.NewBorder(
|
||||
nil, nil,
|
||||
backBtn,
|
||||
container.NewHBox(clearBtn, clearAllBtn),
|
||||
buttonRow,
|
||||
container.NewCenter(title),
|
||||
)
|
||||
|
||||
|
|
@ -54,7 +71,7 @@ func BuildQueueView(
|
|||
jobItems = append(jobItems, container.NewCenter(emptyMsg))
|
||||
} else {
|
||||
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),
|
||||
onCancel func(string),
|
||||
onRemove func(string),
|
||||
onMoveUp func(string),
|
||||
onMoveDown func(string),
|
||||
bgColor, textColor color.Color,
|
||||
) fyne.CanvasObject {
|
||||
// Status color
|
||||
|
|
@ -94,18 +113,15 @@ func buildJobItem(
|
|||
descLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||
|
||||
// Progress bar (for running jobs)
|
||||
var progressWidget fyne.CanvasObject
|
||||
if job.Status == queue.JobStatusRunning {
|
||||
progress := widget.NewProgressBar()
|
||||
progress.SetValue(job.Progress / 100.0)
|
||||
progressWidget = progress
|
||||
} else if job.Status == queue.JobStatusCompleted {
|
||||
progress := widget.NewProgressBar()
|
||||
progress := widget.NewProgressBar()
|
||||
progress.SetValue(job.Progress / 100.0)
|
||||
if job.Status == queue.JobStatusCompleted {
|
||||
progress.SetValue(1.0)
|
||||
progressWidget = progress
|
||||
} else {
|
||||
progressWidget = widget.NewLabel("")
|
||||
}
|
||||
progressWidget := progress
|
||||
|
||||
// Module badge
|
||||
badge := buildModuleBadge(job.Type)
|
||||
|
||||
// Status text
|
||||
statusText := getStatusText(job)
|
||||
|
|
@ -114,6 +130,14 @@ func buildJobItem(
|
|||
|
||||
// Control buttons
|
||||
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 {
|
||||
case queue.JobStatusRunning:
|
||||
buttons = append(buttons,
|
||||
|
|
@ -139,7 +163,7 @@ func buildJobItem(
|
|||
|
||||
// Info section
|
||||
infoBox := container.NewVBox(
|
||||
titleLabel,
|
||||
container.NewHBox(titleLabel, layout.NewSpacer(), badge),
|
||||
descLabel,
|
||||
progressWidget,
|
||||
statusLabel,
|
||||
|
|
@ -161,7 +185,14 @@ func buildJobItem(
|
|||
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
|
||||
|
|
@ -211,3 +242,76 @@ func getStatusText(job *queue.Job) string {
|
|||
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
53
main.go
|
|
@ -1413,23 +1413,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
dvdAspectBox := container.NewVBox(dvdAspectLabel, dvdAspectSelect, dvdInfoLabel)
|
||||
dvdAspectBox.Hide() // Hidden by default
|
||||
|
||||
// Show/hide DVD options based on format selection
|
||||
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()
|
||||
}
|
||||
}
|
||||
// Placeholder for updateDVDOptions - will be defined after resolution/framerate selects are created
|
||||
var updateDVDOptions func()
|
||||
|
||||
// Create formatSelect with callback that updates DVD options
|
||||
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)
|
||||
state.convert.SelectedFormat = opt
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
@ -1573,7 +1560,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
|
||||
// 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
|
||||
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)
|
||||
|
||||
// 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
|
||||
advancedOptions := container.NewVBox(
|
||||
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||
|
|
|
|||
BIN
videotools
BIN
videotools
Binary file not shown.
Loading…
Reference in New Issue
Block a user