forked from Leak_Technologies/VideoTools
Fix Fyne threading error and queue persistence issues
This commit resolves three critical issues:
1. **Fyne Threading Error on Startup**: Fixed by improving setContent() to
check the initComplete flag. During initialization, setContent() calls
SetContent() directly since we're on the main thread. After initialization,
it safely marshals calls via app.Driver().DoFromGoroutine().
2. **Queue Persisting Between Sessions**: Fixed by removing queue persistence.
The shutdown() function no longer saves the queue to disk, ensuring a
clean slate for each new app session.
3. **Queue Auto-Processing**: Fixed by making the queue start in 'paused'
state. Users must explicitly click 'Process Queue' to start batch
conversion. Queue methods PauseProcessing() and ResumeProcessing()
control the paused state.
Changes:
- main.go: Added initComplete flag to appState, improved setContent()
logic, disabled queue persistence in shutdown()
- queue/queue.go: Added paused field to Queue struct, initialize paused=true,
added PauseProcessing()/ResumeProcessing() methods
- ui/queueview.go: Added UI controls for queue processing and clearing
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4a6fda83ab
commit
cfb608e191
|
|
@ -51,6 +51,7 @@ type Job struct {
|
|||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||
Priority int `json:"priority"` // Higher priority = runs first
|
||||
HasModifiedSettings bool `json:"has_modified_settings"`
|
||||
cancel context.CancelFunc `json:"-"`
|
||||
}
|
||||
|
||||
|
|
@ -59,11 +60,12 @@ type JobExecutor func(ctx context.Context, job *Job, progressCallback func(float
|
|||
|
||||
// Queue manages a queue of jobs
|
||||
type Queue struct {
|
||||
jobs []*Job
|
||||
executor JobExecutor
|
||||
running bool
|
||||
mu sync.RWMutex
|
||||
onChange func() // Callback when queue state changes
|
||||
jobs []*Job
|
||||
executor JobExecutor
|
||||
running bool
|
||||
paused bool // When paused, no jobs are processed even if running
|
||||
mu sync.RWMutex
|
||||
onChange func() // Callback when queue state changes
|
||||
}
|
||||
|
||||
// New creates a new queue with the given executor
|
||||
|
|
@ -72,6 +74,7 @@ func New(executor JobExecutor) *Queue {
|
|||
jobs: make([]*Job, 0),
|
||||
executor: executor,
|
||||
running: false,
|
||||
paused: true, // Start paused by default
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -249,6 +252,22 @@ func (q *Queue) Stop() {
|
|||
q.running = false
|
||||
}
|
||||
|
||||
// PauseProcessing pauses job processing (jobs stay in queue)
|
||||
func (q *Queue) PauseProcessing() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.paused = true
|
||||
q.notifyChange()
|
||||
}
|
||||
|
||||
// ResumeProcessing resumes job processing
|
||||
func (q *Queue) ResumeProcessing() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
q.paused = false
|
||||
q.notifyChange()
|
||||
}
|
||||
|
||||
// processJobs continuously processes pending jobs
|
||||
func (q *Queue) processJobs() {
|
||||
for {
|
||||
|
|
@ -258,6 +277,13 @@ func (q *Queue) processJobs() {
|
|||
return
|
||||
}
|
||||
|
||||
// If paused, wait and don't process jobs
|
||||
if q.paused {
|
||||
q.mu.Unlock()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
// Find highest priority pending job
|
||||
var nextJob *Job
|
||||
highestPriority := -1
|
||||
|
|
@ -378,6 +404,15 @@ func (q *Queue) Clear() {
|
|||
q.notifyChange()
|
||||
}
|
||||
|
||||
// ClearAll removes all jobs from the queue
|
||||
func (q *Queue) ClearAll() {
|
||||
q.mu.Lock()
|
||||
defer q.mu.Unlock()
|
||||
|
||||
q.jobs = make([]*Job, 0)
|
||||
q.notifyChange()
|
||||
}
|
||||
|
||||
// generateID generates a unique ID for a job
|
||||
func generateID() string {
|
||||
return fmt.Sprintf("job-%d", time.Now().UnixNano())
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ func BuildQueueView(
|
|||
onCancel func(string),
|
||||
onRemove func(string),
|
||||
onClear func(),
|
||||
onClearAll func(),
|
||||
onProcess func(),
|
||||
titleColor, bgColor, textColor color.Color,
|
||||
) fyne.CanvasObject {
|
||||
// Header
|
||||
|
|
@ -29,20 +31,73 @@ func BuildQueueView(
|
|||
title.TextSize = 24
|
||||
|
||||
backBtn := widget.NewButton("← Back", onBack)
|
||||
backBtn.Importance = widget.LowImportance
|
||||
|
||||
clearBtn := widget.NewButton("Clear Completed", onClear)
|
||||
clearBtn.Importance = widget.LowImportance
|
||||
|
||||
clearAllBtn := widget.NewButton("Clear All", onClearAll)
|
||||
clearAllBtn.Importance = widget.DangerImportance
|
||||
|
||||
processBtn := widget.NewButton("▶ Process Queue", onProcess)
|
||||
processBtn.Importance = widget.HighImportance
|
||||
|
||||
// Only show process button if there are pending jobs
|
||||
if len(jobs) == 0 {
|
||||
processBtn.Disable()
|
||||
}
|
||||
var hasPending bool
|
||||
for _, job := range jobs {
|
||||
if job.Status == queue.JobStatusPending {
|
||||
hasPending = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasPending {
|
||||
processBtn.Disable()
|
||||
}
|
||||
|
||||
header := container.NewBorder(
|
||||
nil, nil,
|
||||
backBtn,
|
||||
clearBtn,
|
||||
container.NewHBox(clearBtn, clearAllBtn, processBtn),
|
||||
container.NewCenter(title),
|
||||
)
|
||||
|
||||
// Count stats
|
||||
pending := 0
|
||||
running := 0
|
||||
failed := 0
|
||||
completed := 0
|
||||
for _, job := range jobs {
|
||||
switch job.Status {
|
||||
case queue.JobStatusPending:
|
||||
pending++
|
||||
case queue.JobStatusRunning:
|
||||
running++
|
||||
case queue.JobStatusCompleted:
|
||||
completed++
|
||||
case queue.JobStatusFailed, queue.JobStatusCancelled:
|
||||
failed++
|
||||
}
|
||||
}
|
||||
|
||||
// Stats display with better formatting
|
||||
var statsText string
|
||||
if len(jobs) == 0 {
|
||||
statsText = "Queue is empty"
|
||||
} else {
|
||||
statsText = fmt.Sprintf(" Total: %d | Running: %d | Pending: %d | Completed: %d | Failed: %d ",
|
||||
len(jobs), running, pending, completed, failed)
|
||||
}
|
||||
statsLabel := widget.NewLabel(statsText)
|
||||
statsLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Job list
|
||||
var jobItems []fyne.CanvasObject
|
||||
|
||||
if len(jobs) == 0 {
|
||||
emptyMsg := widget.NewLabel("No jobs in queue")
|
||||
emptyMsg := widget.NewLabel("Drop videos on modules to add conversion jobs")
|
||||
emptyMsg.Alignment = fyne.TextAlignCenter
|
||||
jobItems = append(jobItems, container.NewCenter(emptyMsg))
|
||||
} else {
|
||||
|
|
@ -54,9 +109,10 @@ func BuildQueueView(
|
|||
jobList := container.NewVBox(jobItems...)
|
||||
scrollable := container.NewVScroll(jobList)
|
||||
|
||||
// Create body with header, stats, and scrollable list
|
||||
body := container.NewBorder(
|
||||
header,
|
||||
nil, nil, nil,
|
||||
statsLabel, nil, nil,
|
||||
scrollable,
|
||||
)
|
||||
|
||||
|
|
@ -75,26 +131,31 @@ func buildJobItem(
|
|||
// Status color
|
||||
statusColor := getStatusColor(job.Status)
|
||||
|
||||
// Status indicator
|
||||
// Status indicator bar
|
||||
statusRect := canvas.NewRectangle(statusColor)
|
||||
statusRect.SetMinSize(fyne.NewSize(6, 0))
|
||||
statusRect.SetMinSize(fyne.NewSize(4, 0))
|
||||
|
||||
// Title and description
|
||||
titleLabel := widget.NewLabel(job.Title)
|
||||
// Title with modified indicator
|
||||
titleText := job.Title
|
||||
if job.HasModifiedSettings && job.Status == queue.JobStatusPending {
|
||||
titleText = titleText + " ⚙ (custom settings)"
|
||||
}
|
||||
titleLabel := widget.NewLabel(titleText)
|
||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
// Description/output path
|
||||
descLabel := widget.NewLabel(job.Description)
|
||||
descLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||
|
||||
// Progress bar (for running jobs)
|
||||
// Progress bar (for running/completed jobs)
|
||||
var progressWidget fyne.CanvasObject
|
||||
if job.Status == queue.JobStatusRunning {
|
||||
if job.Status == queue.JobStatusRunning || job.Status == queue.JobStatusCompleted {
|
||||
progress := widget.NewProgressBar()
|
||||
progress.SetValue(job.Progress / 100.0)
|
||||
progressWidget = progress
|
||||
} else if job.Status == queue.JobStatusCompleted {
|
||||
progress := widget.NewProgressBar()
|
||||
progress.SetValue(1.0)
|
||||
if job.Status == queue.JobStatusRunning {
|
||||
progress.SetValue(job.Progress / 100.0)
|
||||
} else {
|
||||
progress.SetValue(1.0)
|
||||
}
|
||||
progressWidget = progress
|
||||
} else {
|
||||
progressWidget = widget.NewLabel("")
|
||||
|
|
@ -105,29 +166,40 @@ func buildJobItem(
|
|||
statusLabel := widget.NewLabel(statusText)
|
||||
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
|
||||
// Control buttons
|
||||
// Control buttons with status-appropriate styling
|
||||
var buttons []fyne.CanvasObject
|
||||
switch job.Status {
|
||||
case queue.JobStatusRunning:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Pause", func() { onPause(job.ID) }),
|
||||
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||
)
|
||||
pauseBtn := widget.NewButton("⏸ Pause", func() { onPause(job.ID) })
|
||||
pauseBtn.Importance = widget.MediumImportance
|
||||
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
|
||||
cancelBtn.Importance = widget.DangerImportance
|
||||
buttons = append(buttons, pauseBtn, cancelBtn)
|
||||
case queue.JobStatusPaused:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Resume", func() { onResume(job.ID) }),
|
||||
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||
)
|
||||
resumeBtn := widget.NewButton("▶ Resume", func() { onResume(job.ID) })
|
||||
resumeBtn.Importance = widget.MediumImportance
|
||||
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
|
||||
cancelBtn.Importance = widget.DangerImportance
|
||||
buttons = append(buttons, resumeBtn, cancelBtn)
|
||||
case queue.JobStatusPending:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||
)
|
||||
case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Remove", func() { onRemove(job.ID) }),
|
||||
)
|
||||
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
|
||||
cancelBtn.Importance = widget.DangerImportance
|
||||
buttons = append(buttons, cancelBtn)
|
||||
case queue.JobStatusCompleted:
|
||||
removeBtn := widget.NewButton("✓ Remove", func() { onRemove(job.ID) })
|
||||
removeBtn.Importance = widget.LowImportance
|
||||
buttons = append(buttons, removeBtn)
|
||||
case queue.JobStatusFailed:
|
||||
removeBtn := widget.NewButton("✗ Remove", func() { onRemove(job.ID) })
|
||||
removeBtn.Importance = widget.LowImportance
|
||||
buttons = append(buttons, removeBtn)
|
||||
case queue.JobStatusCancelled:
|
||||
removeBtn := widget.NewButton("⊗ Remove", func() { onRemove(job.ID) })
|
||||
removeBtn.Importance = widget.LowImportance
|
||||
buttons = append(buttons, removeBtn)
|
||||
}
|
||||
|
||||
// Layout buttons in a responsive way
|
||||
buttonBox := container.NewHBox(buttons...)
|
||||
|
||||
// Info section
|
||||
|
|
@ -138,7 +210,7 @@ func buildJobItem(
|
|||
statusLabel,
|
||||
)
|
||||
|
||||
// Main content
|
||||
// Main content with borders
|
||||
content := container.NewBorder(
|
||||
nil, nil,
|
||||
statusRect,
|
||||
|
|
@ -146,7 +218,7 @@ func buildJobItem(
|
|||
infoBox,
|
||||
)
|
||||
|
||||
// Card background
|
||||
// Card background with padding
|
||||
card := canvas.NewRectangle(bgColor)
|
||||
card.CornerRadius = 4
|
||||
|
||||
|
|
|
|||
301
main.go
301
main.go
|
|
@ -151,30 +151,31 @@ func (c convertConfig) CoverLabel() string {
|
|||
}
|
||||
|
||||
type appState struct {
|
||||
window fyne.Window
|
||||
active string
|
||||
source *videoSource
|
||||
loadedVideos []*videoSource // Multiple loaded videos for navigation
|
||||
currentIndex int // Current video index in loadedVideos
|
||||
anim *previewAnimator
|
||||
convert convertConfig
|
||||
currentFrame string
|
||||
player player.Controller
|
||||
playerReady bool
|
||||
playerVolume float64
|
||||
playerMuted bool
|
||||
lastVolume float64
|
||||
playerPaused bool
|
||||
playerPos float64
|
||||
playerLast time.Time
|
||||
progressQuit chan struct{}
|
||||
convertCancel context.CancelFunc
|
||||
playerSurf *playerSurface
|
||||
convertBusy bool
|
||||
convertStatus string
|
||||
playSess *playSession
|
||||
jobQueue *queue.Queue
|
||||
statsBar *ui.ConversionStatsBar
|
||||
window fyne.Window
|
||||
active string
|
||||
initComplete bool // Track if initialization is complete
|
||||
source *videoSource
|
||||
loadedVideos []*videoSource // Multiple loaded videos for navigation
|
||||
currentIndex int // Current video index in loadedVideos
|
||||
anim *previewAnimator
|
||||
convert convertConfig
|
||||
currentFrame string
|
||||
player player.Controller
|
||||
playerReady bool
|
||||
playerVolume float64
|
||||
playerMuted bool
|
||||
lastVolume float64
|
||||
playerPaused bool
|
||||
playerPos float64
|
||||
playerLast time.Time
|
||||
progressQuit chan struct{}
|
||||
convertCancel context.CancelFunc
|
||||
playerSurf *playerSurface
|
||||
convertBusy bool
|
||||
convertStatus string
|
||||
playSess *playSession
|
||||
jobQueue *queue.Queue
|
||||
statsBar *ui.ConversionStatsBar
|
||||
}
|
||||
|
||||
func (s *appState) stopPreview() {
|
||||
|
|
@ -308,11 +309,36 @@ func (s *appState) applyInverseDefaults(src *videoSource) {
|
|||
func (s *appState) setContent(body fyne.CanvasObject) {
|
||||
bg := canvas.NewRectangle(backgroundColor)
|
||||
// Don't set a minimum size - let content determine layout naturally
|
||||
if body == nil {
|
||||
s.window.SetContent(bg)
|
||||
return
|
||||
|
||||
// Only use DoFromGoroutine if initialization is complete and we might be on a goroutine
|
||||
// During early initialization, always call directly since we're on the main thread
|
||||
if !s.initComplete {
|
||||
// During initialization, call directly (we're on main thread)
|
||||
if body == nil {
|
||||
s.window.SetContent(bg)
|
||||
} else {
|
||||
s.window.SetContent(container.NewMax(bg, body))
|
||||
}
|
||||
} else {
|
||||
// After initialization, use DoFromGoroutine to be safe
|
||||
app := fyne.CurrentApp()
|
||||
if app != nil && app.Driver() != nil {
|
||||
app.Driver().DoFromGoroutine(func() {
|
||||
if body == nil {
|
||||
s.window.SetContent(bg)
|
||||
} else {
|
||||
s.window.SetContent(container.NewMax(bg, body))
|
||||
}
|
||||
}, false)
|
||||
} else {
|
||||
// Fallback if driver not available
|
||||
if body == nil {
|
||||
s.window.SetContent(bg)
|
||||
} else {
|
||||
s.window.SetContent(container.NewMax(bg, body))
|
||||
}
|
||||
}
|
||||
}
|
||||
s.window.SetContent(container.NewMax(bg, body))
|
||||
}
|
||||
|
||||
// showErrorWithCopy displays an error dialog with a "Copy Error" button
|
||||
|
|
@ -424,6 +450,14 @@ func (s *appState) showQueue() {
|
|||
s.jobQueue.Clear()
|
||||
s.showQueue() // Refresh
|
||||
},
|
||||
func() { // onClearAll
|
||||
s.jobQueue.ClearAll()
|
||||
s.showQueue() // Refresh
|
||||
},
|
||||
func() { // onProcess
|
||||
s.jobQueue.ResumeProcessing()
|
||||
logging.Debug(logging.CatSystem, "queue processing started")
|
||||
},
|
||||
utils.MustHex("#4CE870"), // titleColor
|
||||
gridColor, // bgColor
|
||||
textColor, // textColor
|
||||
|
|
@ -432,6 +466,102 @@ func (s *appState) showQueue() {
|
|||
s.setContent(container.NewPadded(view))
|
||||
}
|
||||
|
||||
// hasModifiedConvertSettings checks if the current conversion settings differ from defaults
|
||||
func (s *appState) hasModifiedConvertSettings() bool {
|
||||
cfg := s.convert
|
||||
|
||||
// Check if any non-default values are set
|
||||
if cfg.OutputBase != "" && cfg.OutputBase != "converted" {
|
||||
return true
|
||||
}
|
||||
if cfg.Quality != "" && cfg.Quality != "Standard (CRF 23)" {
|
||||
return true
|
||||
}
|
||||
if cfg.VideoCodec != "" && cfg.VideoCodec != "H.264" {
|
||||
return true
|
||||
}
|
||||
if cfg.EncoderPreset != "" && cfg.EncoderPreset != "medium" {
|
||||
return true
|
||||
}
|
||||
if cfg.CRF != "" {
|
||||
return true
|
||||
}
|
||||
if cfg.BitrateMode != "" && cfg.BitrateMode != "CRF" {
|
||||
return true
|
||||
}
|
||||
if cfg.VideoBitrate != "" {
|
||||
return true
|
||||
}
|
||||
if cfg.TargetResolution != "" && cfg.TargetResolution != "Source" {
|
||||
return true
|
||||
}
|
||||
if cfg.FrameRate != "" && cfg.FrameRate != "Source" {
|
||||
return true
|
||||
}
|
||||
if cfg.PixelFormat != "" && cfg.PixelFormat != "yuv420p" {
|
||||
return true
|
||||
}
|
||||
if cfg.HardwareAccel != "" && cfg.HardwareAccel != "none" {
|
||||
return true
|
||||
}
|
||||
if cfg.TwoPass {
|
||||
return true
|
||||
}
|
||||
if cfg.AudioCodec != "" && cfg.AudioCodec != "AAC" {
|
||||
return true
|
||||
}
|
||||
if cfg.AudioBitrate != "" && cfg.AudioBitrate != "192k" {
|
||||
return true
|
||||
}
|
||||
if cfg.AudioChannels != "" && cfg.AudioChannels != "Source" {
|
||||
return true
|
||||
}
|
||||
if cfg.InverseTelecine {
|
||||
return true
|
||||
}
|
||||
if cfg.CoverArtPath != "" {
|
||||
return true
|
||||
}
|
||||
if cfg.AspectHandling != "" && cfg.AspectHandling != "Auto" {
|
||||
return true
|
||||
}
|
||||
if cfg.OutputAspect != "" && cfg.OutputAspect != "Source" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// removeQueuedConvertJob removes pending conversion jobs for the current video
|
||||
func (s *appState) removeQueuedConvertJob() error {
|
||||
if s.source == nil {
|
||||
return fmt.Errorf("no video loaded")
|
||||
}
|
||||
|
||||
jobs := s.jobQueue.List()
|
||||
removed := 0
|
||||
|
||||
// Remove all pending convert jobs for this video
|
||||
for _, job := range jobs {
|
||||
if job.Type == queue.JobTypeConvert && job.Status == queue.JobStatusPending {
|
||||
if inputPath, ok := job.Config["inputPath"].(string); ok && inputPath == s.source.Path {
|
||||
if err := s.jobQueue.Remove(job.ID); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to remove job %s: %v", job.ID, err)
|
||||
} else {
|
||||
removed++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if removed == 0 {
|
||||
return fmt.Errorf("no pending conversion jobs found for this video")
|
||||
}
|
||||
|
||||
logging.Debug(logging.CatSystem, "removed %d conversion jobs from queue for: %s", removed, s.source.Path)
|
||||
return nil
|
||||
}
|
||||
|
||||
// addConvertToQueue adds a conversion job to the queue
|
||||
func (s *appState) addConvertToQueue() error {
|
||||
if s.source == nil {
|
||||
|
|
@ -481,13 +611,14 @@ func (s *appState) addConvertToQueue() error {
|
|||
}
|
||||
|
||||
job := &queue.Job{
|
||||
Type: queue.JobTypeConvert,
|
||||
Title: fmt.Sprintf("Convert %s", filepath.Base(src.Path)),
|
||||
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(src.Path), filepath.Base(outPath)),
|
||||
InputFile: src.Path,
|
||||
OutputFile: outPath,
|
||||
Config: config,
|
||||
Priority: 0,
|
||||
Type: queue.JobTypeConvert,
|
||||
Title: fmt.Sprintf("Convert %s", filepath.Base(src.Path)),
|
||||
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(src.Path), filepath.Base(outPath)),
|
||||
InputFile: src.Path,
|
||||
OutputFile: outPath,
|
||||
Config: config,
|
||||
Priority: 0,
|
||||
HasModifiedSettings: s.hasModifiedConvertSettings(),
|
||||
}
|
||||
|
||||
s.jobQueue.Add(job)
|
||||
|
|
@ -651,13 +782,14 @@ func (s *appState) batchAddToQueue(paths []string) {
|
|||
}
|
||||
|
||||
job := &queue.Job{
|
||||
Type: queue.JobTypeConvert,
|
||||
Title: fmt.Sprintf("Convert %s", filepath.Base(path)),
|
||||
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(path), filepath.Base(outPath)),
|
||||
InputFile: path,
|
||||
OutputFile: outPath,
|
||||
Config: config,
|
||||
Priority: 0,
|
||||
Type: queue.JobTypeConvert,
|
||||
Title: fmt.Sprintf("Convert %s", filepath.Base(path)),
|
||||
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(path), filepath.Base(outPath)),
|
||||
InputFile: path,
|
||||
OutputFile: outPath,
|
||||
Config: config,
|
||||
Priority: 0,
|
||||
HasModifiedSettings: s.hasModifiedConvertSettings(),
|
||||
}
|
||||
|
||||
s.jobQueue.Add(job)
|
||||
|
|
@ -962,13 +1094,10 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
}
|
||||
|
||||
func (s *appState) shutdown() {
|
||||
// Save queue before shutting down
|
||||
// Queue is not persisted between sessions - start fresh each time
|
||||
if s.jobQueue != nil {
|
||||
s.jobQueue.Stop()
|
||||
queuePath := filepath.Join(os.TempDir(), "videotools-queue.json")
|
||||
if err := s.jobQueue.Save(queuePath); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to save queue: %v", err)
|
||||
}
|
||||
// Don't save the queue - we want a clean slate each session
|
||||
}
|
||||
|
||||
s.stopPlayer()
|
||||
|
|
@ -1036,7 +1165,10 @@ func runGUI() {
|
|||
logging.Debug(logging.CatUI, "window initialized at 1120x640")
|
||||
|
||||
state := &appState{
|
||||
window: w,
|
||||
window: w,
|
||||
source: nil, // Start with no video source
|
||||
loadedVideos: nil, // Start with no videos loaded
|
||||
currentIndex: 0,
|
||||
convert: convertConfig{
|
||||
OutputBase: "converted",
|
||||
SelectedFormat: formatOptions[0],
|
||||
|
|
@ -1081,23 +1213,11 @@ func runGUI() {
|
|||
|
||||
// Initialize job queue
|
||||
state.jobQueue = queue.New(state.jobExecutor)
|
||||
state.jobQueue.SetChangeCallback(func() {
|
||||
// Update stats bar
|
||||
state.updateStatsBar()
|
||||
|
||||
// Refresh UI when queue changes
|
||||
if state.active == "" {
|
||||
state.showMainMenu()
|
||||
}
|
||||
})
|
||||
|
||||
// Load saved queue
|
||||
queuePath := filepath.Join(os.TempDir(), "videotools-queue.json")
|
||||
if err := state.jobQueue.Load(queuePath); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to load queue: %v", err)
|
||||
}
|
||||
|
||||
// Start queue processing
|
||||
// Start with a clean queue for each session
|
||||
// Queue will be populated as user adds videos via drag-and-drop
|
||||
// Queue is saved to disk on shutdown but not loaded on startup
|
||||
// Start the queue but keep it paused by default - only process when user explicitly requests
|
||||
state.jobQueue.Start()
|
||||
|
||||
defer state.shutdown()
|
||||
|
|
@ -1106,6 +1226,36 @@ func runGUI() {
|
|||
})
|
||||
state.showMainMenu()
|
||||
logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList))
|
||||
|
||||
// Set queue change callback AFTER window is shown to avoid threading issues during startup
|
||||
// Use a small delay to ensure everything is fully initialized
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
state.jobQueue.SetChangeCallback(func() {
|
||||
// Only handle queue changes after initialization is complete
|
||||
if !state.initComplete {
|
||||
return
|
||||
}
|
||||
|
||||
// Queue callbacks come from goroutines, so wrap UI calls
|
||||
app := fyne.CurrentApp()
|
||||
if app == nil || app.Driver() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
app.Driver().DoFromGoroutine(func() {
|
||||
// Update stats bar
|
||||
state.updateStatsBar()
|
||||
|
||||
// Refresh UI when queue changes
|
||||
if state.active == "" {
|
||||
state.showMainMenu()
|
||||
}
|
||||
}, false)
|
||||
})
|
||||
state.initComplete = true
|
||||
}()
|
||||
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
||||
|
|
@ -1635,18 +1785,31 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
cancelBtn.Importance = widget.DangerImportance
|
||||
cancelBtn.Disable()
|
||||
|
||||
// Add to Queue button
|
||||
addQueueBtn := widget.NewButton("Add to Queue", func() {
|
||||
// Queue buttons - Add and Remove
|
||||
addQueueBtn := widget.NewButton("+ Add", func() {
|
||||
if err := state.addConvertToQueue(); err != nil {
|
||||
dialog.ShowError(err, state.window)
|
||||
} else {
|
||||
dialog.ShowInformation("Queue", "Job added to queue!", state.window)
|
||||
}
|
||||
})
|
||||
addQueueBtn.Importance = widget.MediumImportance
|
||||
if src == nil {
|
||||
addQueueBtn.Disable()
|
||||
}
|
||||
|
||||
removeQueueBtn := widget.NewButton("- Remove", func() {
|
||||
if err := state.removeQueuedConvertJob(); err != nil {
|
||||
dialog.ShowError(err, state.window)
|
||||
} else {
|
||||
dialog.ShowInformation("Queue", "Job removed from queue!", state.window)
|
||||
}
|
||||
})
|
||||
removeQueueBtn.Importance = widget.MediumImportance
|
||||
if src == nil {
|
||||
removeQueueBtn.Disable()
|
||||
}
|
||||
|
||||
convertBtn = widget.NewButton("CONVERT NOW", func() {
|
||||
state.startConvert(statusLabel, convertBtn, cancelBtn, activity)
|
||||
})
|
||||
|
|
@ -1658,9 +1821,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
convertBtn.Disable()
|
||||
cancelBtn.Enable()
|
||||
addQueueBtn.Disable()
|
||||
removeQueueBtn.Disable()
|
||||
}
|
||||
|
||||
actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, addQueueBtn, convertBtn)
|
||||
// Queue management container
|
||||
queueBox := container.NewHBox(addQueueBtn, removeQueueBtn)
|
||||
|
||||
actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, queueBox, convertBtn)
|
||||
actionBar := ui.TintedBar(convertColor, actionInner)
|
||||
|
||||
// Wrap mainArea in a scroll container to prevent content from forcing window resize
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user