Revert "Fix Fyne threading error and queue persistence issues"
This reverts commit cfb608e191.
This commit is contained in:
parent
c44074f043
commit
2552e0fcad
|
|
@ -51,7 +51,6 @@ type Job struct {
|
||||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||||
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
CompletedAt *time.Time `json:"completed_at,omitempty"`
|
||||||
Priority int `json:"priority"` // Higher priority = runs first
|
Priority int `json:"priority"` // Higher priority = runs first
|
||||||
HasModifiedSettings bool `json:"has_modified_settings"`
|
|
||||||
cancel context.CancelFunc `json:"-"`
|
cancel context.CancelFunc `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,12 +59,11 @@ type JobExecutor func(ctx context.Context, job *Job, progressCallback func(float
|
||||||
|
|
||||||
// Queue manages a queue of jobs
|
// Queue manages a queue of jobs
|
||||||
type Queue struct {
|
type Queue struct {
|
||||||
jobs []*Job
|
jobs []*Job
|
||||||
executor JobExecutor
|
executor JobExecutor
|
||||||
running bool
|
running bool
|
||||||
paused bool // When paused, no jobs are processed even if running
|
mu sync.RWMutex
|
||||||
mu sync.RWMutex
|
onChange func() // Callback when queue state changes
|
||||||
onChange func() // Callback when queue state changes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new queue with the given executor
|
// New creates a new queue with the given executor
|
||||||
|
|
@ -74,7 +72,6 @@ func New(executor JobExecutor) *Queue {
|
||||||
jobs: make([]*Job, 0),
|
jobs: make([]*Job, 0),
|
||||||
executor: executor,
|
executor: executor,
|
||||||
running: false,
|
running: false,
|
||||||
paused: true, // Start paused by default
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,22 +249,6 @@ func (q *Queue) Stop() {
|
||||||
q.running = false
|
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
|
// processJobs continuously processes pending jobs
|
||||||
func (q *Queue) processJobs() {
|
func (q *Queue) processJobs() {
|
||||||
for {
|
for {
|
||||||
|
|
@ -277,13 +258,6 @@ func (q *Queue) processJobs() {
|
||||||
return
|
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
|
// Find highest priority pending job
|
||||||
var nextJob *Job
|
var nextJob *Job
|
||||||
highestPriority := -1
|
highestPriority := -1
|
||||||
|
|
@ -404,15 +378,6 @@ func (q *Queue) Clear() {
|
||||||
q.notifyChange()
|
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
|
// generateID generates a unique ID for a job
|
||||||
func generateID() string {
|
func generateID() string {
|
||||||
return fmt.Sprintf("job-%d", time.Now().UnixNano())
|
return fmt.Sprintf("job-%d", time.Now().UnixNano())
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,6 @@ func BuildQueueView(
|
||||||
onCancel func(string),
|
onCancel func(string),
|
||||||
onRemove func(string),
|
onRemove func(string),
|
||||||
onClear func(),
|
onClear func(),
|
||||||
onClearAll func(),
|
|
||||||
onProcess func(),
|
|
||||||
titleColor, bgColor, textColor color.Color,
|
titleColor, bgColor, textColor color.Color,
|
||||||
) fyne.CanvasObject {
|
) fyne.CanvasObject {
|
||||||
// Header
|
// Header
|
||||||
|
|
@ -31,73 +29,20 @@ func BuildQueueView(
|
||||||
title.TextSize = 24
|
title.TextSize = 24
|
||||||
|
|
||||||
backBtn := widget.NewButton("← Back", onBack)
|
backBtn := widget.NewButton("← Back", onBack)
|
||||||
backBtn.Importance = widget.LowImportance
|
|
||||||
|
|
||||||
clearBtn := widget.NewButton("Clear Completed", onClear)
|
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(
|
header := container.NewBorder(
|
||||||
nil, nil,
|
nil, nil,
|
||||||
backBtn,
|
backBtn,
|
||||||
container.NewHBox(clearBtn, clearAllBtn, processBtn),
|
clearBtn,
|
||||||
container.NewCenter(title),
|
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
|
// Job list
|
||||||
var jobItems []fyne.CanvasObject
|
var jobItems []fyne.CanvasObject
|
||||||
|
|
||||||
if len(jobs) == 0 {
|
if len(jobs) == 0 {
|
||||||
emptyMsg := widget.NewLabel("Drop videos on modules to add conversion jobs")
|
emptyMsg := widget.NewLabel("No jobs in queue")
|
||||||
emptyMsg.Alignment = fyne.TextAlignCenter
|
emptyMsg.Alignment = fyne.TextAlignCenter
|
||||||
jobItems = append(jobItems, container.NewCenter(emptyMsg))
|
jobItems = append(jobItems, container.NewCenter(emptyMsg))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -109,10 +54,9 @@ func BuildQueueView(
|
||||||
jobList := container.NewVBox(jobItems...)
|
jobList := container.NewVBox(jobItems...)
|
||||||
scrollable := container.NewVScroll(jobList)
|
scrollable := container.NewVScroll(jobList)
|
||||||
|
|
||||||
// Create body with header, stats, and scrollable list
|
|
||||||
body := container.NewBorder(
|
body := container.NewBorder(
|
||||||
header,
|
header,
|
||||||
statsLabel, nil, nil,
|
nil, nil, nil,
|
||||||
scrollable,
|
scrollable,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -131,31 +75,26 @@ func buildJobItem(
|
||||||
// Status color
|
// Status color
|
||||||
statusColor := getStatusColor(job.Status)
|
statusColor := getStatusColor(job.Status)
|
||||||
|
|
||||||
// Status indicator bar
|
// Status indicator
|
||||||
statusRect := canvas.NewRectangle(statusColor)
|
statusRect := canvas.NewRectangle(statusColor)
|
||||||
statusRect.SetMinSize(fyne.NewSize(4, 0))
|
statusRect.SetMinSize(fyne.NewSize(6, 0))
|
||||||
|
|
||||||
// Title with modified indicator
|
// Title and description
|
||||||
titleText := job.Title
|
titleLabel := widget.NewLabel(job.Title)
|
||||||
if job.HasModifiedSettings && job.Status == queue.JobStatusPending {
|
|
||||||
titleText = titleText + " ⚙ (custom settings)"
|
|
||||||
}
|
|
||||||
titleLabel := widget.NewLabel(titleText)
|
|
||||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
|
||||||
// Description/output path
|
|
||||||
descLabel := widget.NewLabel(job.Description)
|
descLabel := widget.NewLabel(job.Description)
|
||||||
descLabel.TextStyle = fyne.TextStyle{Italic: true}
|
descLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||||
|
|
||||||
// Progress bar (for running/completed jobs)
|
// Progress bar (for running jobs)
|
||||||
var progressWidget fyne.CanvasObject
|
var progressWidget fyne.CanvasObject
|
||||||
if job.Status == queue.JobStatusRunning || job.Status == queue.JobStatusCompleted {
|
if job.Status == queue.JobStatusRunning {
|
||||||
progress := widget.NewProgressBar()
|
progress := widget.NewProgressBar()
|
||||||
if job.Status == queue.JobStatusRunning {
|
progress.SetValue(job.Progress / 100.0)
|
||||||
progress.SetValue(job.Progress / 100.0)
|
progressWidget = progress
|
||||||
} else {
|
} else if job.Status == queue.JobStatusCompleted {
|
||||||
progress.SetValue(1.0)
|
progress := widget.NewProgressBar()
|
||||||
}
|
progress.SetValue(1.0)
|
||||||
progressWidget = progress
|
progressWidget = progress
|
||||||
} else {
|
} else {
|
||||||
progressWidget = widget.NewLabel("")
|
progressWidget = widget.NewLabel("")
|
||||||
|
|
@ -166,40 +105,29 @@ func buildJobItem(
|
||||||
statusLabel := widget.NewLabel(statusText)
|
statusLabel := widget.NewLabel(statusText)
|
||||||
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
||||||
|
|
||||||
// Control buttons with status-appropriate styling
|
// Control buttons
|
||||||
var buttons []fyne.CanvasObject
|
var buttons []fyne.CanvasObject
|
||||||
switch job.Status {
|
switch job.Status {
|
||||||
case queue.JobStatusRunning:
|
case queue.JobStatusRunning:
|
||||||
pauseBtn := widget.NewButton("⏸ Pause", func() { onPause(job.ID) })
|
buttons = append(buttons,
|
||||||
pauseBtn.Importance = widget.MediumImportance
|
widget.NewButton("Pause", func() { onPause(job.ID) }),
|
||||||
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
|
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||||
cancelBtn.Importance = widget.DangerImportance
|
)
|
||||||
buttons = append(buttons, pauseBtn, cancelBtn)
|
|
||||||
case queue.JobStatusPaused:
|
case queue.JobStatusPaused:
|
||||||
resumeBtn := widget.NewButton("▶ Resume", func() { onResume(job.ID) })
|
buttons = append(buttons,
|
||||||
resumeBtn.Importance = widget.MediumImportance
|
widget.NewButton("Resume", func() { onResume(job.ID) }),
|
||||||
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
|
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||||
cancelBtn.Importance = widget.DangerImportance
|
)
|
||||||
buttons = append(buttons, resumeBtn, cancelBtn)
|
|
||||||
case queue.JobStatusPending:
|
case queue.JobStatusPending:
|
||||||
cancelBtn := widget.NewButton("⊗ Cancel", func() { onCancel(job.ID) })
|
buttons = append(buttons,
|
||||||
cancelBtn.Importance = widget.DangerImportance
|
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||||
buttons = append(buttons, cancelBtn)
|
)
|
||||||
case queue.JobStatusCompleted:
|
case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled:
|
||||||
removeBtn := widget.NewButton("✓ Remove", func() { onRemove(job.ID) })
|
buttons = append(buttons,
|
||||||
removeBtn.Importance = widget.LowImportance
|
widget.NewButton("Remove", func() { onRemove(job.ID) }),
|
||||||
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...)
|
buttonBox := container.NewHBox(buttons...)
|
||||||
|
|
||||||
// Info section
|
// Info section
|
||||||
|
|
@ -210,7 +138,7 @@ func buildJobItem(
|
||||||
statusLabel,
|
statusLabel,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Main content with borders
|
// Main content
|
||||||
content := container.NewBorder(
|
content := container.NewBorder(
|
||||||
nil, nil,
|
nil, nil,
|
||||||
statusRect,
|
statusRect,
|
||||||
|
|
@ -218,7 +146,7 @@ func buildJobItem(
|
||||||
infoBox,
|
infoBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Card background with padding
|
// Card background
|
||||||
card := canvas.NewRectangle(bgColor)
|
card := canvas.NewRectangle(bgColor)
|
||||||
card.CornerRadius = 4
|
card.CornerRadius = 4
|
||||||
|
|
||||||
|
|
|
||||||
301
main.go
301
main.go
|
|
@ -151,31 +151,30 @@ func (c convertConfig) CoverLabel() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
type appState struct {
|
type appState struct {
|
||||||
window fyne.Window
|
window fyne.Window
|
||||||
active string
|
active string
|
||||||
initComplete bool // Track if initialization is complete
|
source *videoSource
|
||||||
source *videoSource
|
loadedVideos []*videoSource // Multiple loaded videos for navigation
|
||||||
loadedVideos []*videoSource // Multiple loaded videos for navigation
|
currentIndex int // Current video index in loadedVideos
|
||||||
currentIndex int // Current video index in loadedVideos
|
anim *previewAnimator
|
||||||
anim *previewAnimator
|
convert convertConfig
|
||||||
convert convertConfig
|
currentFrame string
|
||||||
currentFrame string
|
player player.Controller
|
||||||
player player.Controller
|
playerReady bool
|
||||||
playerReady bool
|
playerVolume float64
|
||||||
playerVolume float64
|
playerMuted bool
|
||||||
playerMuted bool
|
lastVolume float64
|
||||||
lastVolume float64
|
playerPaused bool
|
||||||
playerPaused bool
|
playerPos float64
|
||||||
playerPos float64
|
playerLast time.Time
|
||||||
playerLast time.Time
|
progressQuit chan struct{}
|
||||||
progressQuit chan struct{}
|
convertCancel context.CancelFunc
|
||||||
convertCancel context.CancelFunc
|
playerSurf *playerSurface
|
||||||
playerSurf *playerSurface
|
convertBusy bool
|
||||||
convertBusy bool
|
convertStatus string
|
||||||
convertStatus string
|
playSess *playSession
|
||||||
playSess *playSession
|
jobQueue *queue.Queue
|
||||||
jobQueue *queue.Queue
|
statsBar *ui.ConversionStatsBar
|
||||||
statsBar *ui.ConversionStatsBar
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) stopPreview() {
|
func (s *appState) stopPreview() {
|
||||||
|
|
@ -309,36 +308,11 @@ func (s *appState) applyInverseDefaults(src *videoSource) {
|
||||||
func (s *appState) setContent(body fyne.CanvasObject) {
|
func (s *appState) setContent(body fyne.CanvasObject) {
|
||||||
bg := canvas.NewRectangle(backgroundColor)
|
bg := canvas.NewRectangle(backgroundColor)
|
||||||
// Don't set a minimum size - let content determine layout naturally
|
// Don't set a minimum size - let content determine layout naturally
|
||||||
|
if body == nil {
|
||||||
// Only use DoFromGoroutine if initialization is complete and we might be on a goroutine
|
s.window.SetContent(bg)
|
||||||
// During early initialization, always call directly since we're on the main thread
|
return
|
||||||
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
|
// showErrorWithCopy displays an error dialog with a "Copy Error" button
|
||||||
|
|
@ -450,14 +424,6 @@ func (s *appState) showQueue() {
|
||||||
s.jobQueue.Clear()
|
s.jobQueue.Clear()
|
||||||
s.showQueue() // Refresh
|
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
|
utils.MustHex("#4CE870"), // titleColor
|
||||||
gridColor, // bgColor
|
gridColor, // bgColor
|
||||||
textColor, // textColor
|
textColor, // textColor
|
||||||
|
|
@ -466,102 +432,6 @@ func (s *appState) showQueue() {
|
||||||
s.setContent(container.NewPadded(view))
|
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
|
// addConvertToQueue adds a conversion job to the queue
|
||||||
func (s *appState) addConvertToQueue() error {
|
func (s *appState) addConvertToQueue() error {
|
||||||
if s.source == nil {
|
if s.source == nil {
|
||||||
|
|
@ -611,14 +481,13 @@ func (s *appState) addConvertToQueue() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
job := &queue.Job{
|
job := &queue.Job{
|
||||||
Type: queue.JobTypeConvert,
|
Type: queue.JobTypeConvert,
|
||||||
Title: fmt.Sprintf("Convert %s", filepath.Base(src.Path)),
|
Title: fmt.Sprintf("Convert %s", filepath.Base(src.Path)),
|
||||||
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(src.Path), filepath.Base(outPath)),
|
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(src.Path), filepath.Base(outPath)),
|
||||||
InputFile: src.Path,
|
InputFile: src.Path,
|
||||||
OutputFile: outPath,
|
OutputFile: outPath,
|
||||||
Config: config,
|
Config: config,
|
||||||
Priority: 0,
|
Priority: 0,
|
||||||
HasModifiedSettings: s.hasModifiedConvertSettings(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.jobQueue.Add(job)
|
s.jobQueue.Add(job)
|
||||||
|
|
@ -782,14 +651,13 @@ func (s *appState) batchAddToQueue(paths []string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
job := &queue.Job{
|
job := &queue.Job{
|
||||||
Type: queue.JobTypeConvert,
|
Type: queue.JobTypeConvert,
|
||||||
Title: fmt.Sprintf("Convert %s", filepath.Base(path)),
|
Title: fmt.Sprintf("Convert %s", filepath.Base(path)),
|
||||||
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(path), filepath.Base(outPath)),
|
Description: fmt.Sprintf("Output: %s → %s", filepath.Base(path), filepath.Base(outPath)),
|
||||||
InputFile: path,
|
InputFile: path,
|
||||||
OutputFile: outPath,
|
OutputFile: outPath,
|
||||||
Config: config,
|
Config: config,
|
||||||
Priority: 0,
|
Priority: 0,
|
||||||
HasModifiedSettings: s.hasModifiedConvertSettings(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.jobQueue.Add(job)
|
s.jobQueue.Add(job)
|
||||||
|
|
@ -1094,10 +962,13 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) shutdown() {
|
func (s *appState) shutdown() {
|
||||||
// Queue is not persisted between sessions - start fresh each time
|
// Save queue before shutting down
|
||||||
if s.jobQueue != nil {
|
if s.jobQueue != nil {
|
||||||
s.jobQueue.Stop()
|
s.jobQueue.Stop()
|
||||||
// Don't save the queue - we want a clean slate each session
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
s.stopPlayer()
|
s.stopPlayer()
|
||||||
|
|
@ -1165,10 +1036,7 @@ func runGUI() {
|
||||||
logging.Debug(logging.CatUI, "window initialized at 1120x640")
|
logging.Debug(logging.CatUI, "window initialized at 1120x640")
|
||||||
|
|
||||||
state := &appState{
|
state := &appState{
|
||||||
window: w,
|
window: w,
|
||||||
source: nil, // Start with no video source
|
|
||||||
loadedVideos: nil, // Start with no videos loaded
|
|
||||||
currentIndex: 0,
|
|
||||||
convert: convertConfig{
|
convert: convertConfig{
|
||||||
OutputBase: "converted",
|
OutputBase: "converted",
|
||||||
SelectedFormat: formatOptions[0],
|
SelectedFormat: formatOptions[0],
|
||||||
|
|
@ -1213,11 +1081,23 @@ func runGUI() {
|
||||||
|
|
||||||
// Initialize job queue
|
// Initialize job queue
|
||||||
state.jobQueue = queue.New(state.jobExecutor)
|
state.jobQueue = queue.New(state.jobExecutor)
|
||||||
|
state.jobQueue.SetChangeCallback(func() {
|
||||||
|
// Update stats bar
|
||||||
|
state.updateStatsBar()
|
||||||
|
|
||||||
// Start with a clean queue for each session
|
// Refresh UI when queue changes
|
||||||
// Queue will be populated as user adds videos via drag-and-drop
|
if state.active == "" {
|
||||||
// Queue is saved to disk on shutdown but not loaded on startup
|
state.showMainMenu()
|
||||||
// Start the queue but keep it paused by default - only process when user explicitly requests
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 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
|
||||||
state.jobQueue.Start()
|
state.jobQueue.Start()
|
||||||
|
|
||||||
defer state.shutdown()
|
defer state.shutdown()
|
||||||
|
|
@ -1226,36 +1106,6 @@ func runGUI() {
|
||||||
})
|
})
|
||||||
state.showMainMenu()
|
state.showMainMenu()
|
||||||
logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList))
|
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()
|
w.ShowAndRun()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1785,31 +1635,18 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
cancelBtn.Importance = widget.DangerImportance
|
cancelBtn.Importance = widget.DangerImportance
|
||||||
cancelBtn.Disable()
|
cancelBtn.Disable()
|
||||||
|
|
||||||
// Queue buttons - Add and Remove
|
// Add to Queue button
|
||||||
addQueueBtn := widget.NewButton("+ Add", func() {
|
addQueueBtn := widget.NewButton("Add to Queue", func() {
|
||||||
if err := state.addConvertToQueue(); err != nil {
|
if err := state.addConvertToQueue(); err != nil {
|
||||||
dialog.ShowError(err, state.window)
|
dialog.ShowError(err, state.window)
|
||||||
} else {
|
} else {
|
||||||
dialog.ShowInformation("Queue", "Job added to queue!", state.window)
|
dialog.ShowInformation("Queue", "Job added to queue!", state.window)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
addQueueBtn.Importance = widget.MediumImportance
|
|
||||||
if src == nil {
|
if src == nil {
|
||||||
addQueueBtn.Disable()
|
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() {
|
convertBtn = widget.NewButton("CONVERT NOW", func() {
|
||||||
state.startConvert(statusLabel, convertBtn, cancelBtn, activity)
|
state.startConvert(statusLabel, convertBtn, cancelBtn, activity)
|
||||||
})
|
})
|
||||||
|
|
@ -1821,13 +1658,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
convertBtn.Disable()
|
convertBtn.Disable()
|
||||||
cancelBtn.Enable()
|
cancelBtn.Enable()
|
||||||
addQueueBtn.Disable()
|
addQueueBtn.Disable()
|
||||||
removeQueueBtn.Disable()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Queue management container
|
actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, addQueueBtn, convertBtn)
|
||||||
queueBox := container.NewHBox(addQueueBtn, removeQueueBtn)
|
|
||||||
|
|
||||||
actionInner := container.NewHBox(resetBtn, activity, statusLabel, layout.NewSpacer(), cancelBtn, queueBox, convertBtn)
|
|
||||||
actionBar := ui.TintedBar(convertColor, actionInner)
|
actionBar := ui.TintedBar(convertColor, actionInner)
|
||||||
|
|
||||||
// Wrap mainArea in a scroll container to prevent content from forcing window resize
|
// Wrap mainArea in a scroll container to prevent content from forcing window resize
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user