Optimize queue updates and colored selects
This commit is contained in:
parent
2332f2e9ca
commit
69a00e922f
|
|
@ -1186,10 +1186,18 @@ func (cs *ColoredSelect) showPopup() {
|
|||
// Create scrollable list with proper spacing
|
||||
list := container.NewVBox(items...)
|
||||
scroll := container.NewVScroll(list)
|
||||
scroll.SetMinSize(fyne.NewSize(300, 200)) // Add back minimum size for usability
|
||||
dropWidth := cs.Size().Width
|
||||
if dropWidth <= 0 {
|
||||
dropWidth = cs.MinSize().Width
|
||||
}
|
||||
if dropWidth < 200 {
|
||||
dropWidth = 200
|
||||
}
|
||||
scroll.SetMinSize(fyne.NewSize(dropWidth, 200))
|
||||
|
||||
// Create popup
|
||||
cs.popup = widget.NewPopUp(scroll, cs.window.Canvas())
|
||||
cs.popup.Resize(fyne.NewSize(dropWidth, 200))
|
||||
|
||||
// Position popup below the select widget
|
||||
popupPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(cs)
|
||||
|
|
|
|||
|
|
@ -193,6 +193,54 @@ func applyAlpha(c color.Color, alpha uint8) color.Color {
|
|||
return color.NRGBA{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8), A: alpha}
|
||||
}
|
||||
|
||||
type queueCallbacks struct {
|
||||
onBack func()
|
||||
onPause func(string)
|
||||
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()
|
||||
onCopyError func(string)
|
||||
onViewLog func(string)
|
||||
onCopyCommand func(string)
|
||||
}
|
||||
|
||||
type queueItemWidgets struct {
|
||||
jobID string
|
||||
status queue.JobStatus
|
||||
container fyne.CanvasObject
|
||||
titleLabel *widget.Label
|
||||
descLabel *widget.Label
|
||||
statusLabel *widget.Label
|
||||
progress *StripedProgress
|
||||
buttonBox *fyne.Container
|
||||
}
|
||||
|
||||
type QueueView struct {
|
||||
Root fyne.CanvasObject
|
||||
Scroll *container.Scroll
|
||||
jobList *fyne.Container
|
||||
emptyLabel fyne.CanvasObject
|
||||
items map[string]*queueItemWidgets
|
||||
callbacks queueCallbacks
|
||||
bgColor color.Color
|
||||
textColor color.Color
|
||||
}
|
||||
|
||||
func (v *QueueView) StopAnimations() {
|
||||
for _, item := range v.items {
|
||||
if item != nil && item.progress != nil {
|
||||
item.progress.StopAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// BuildQueueView creates the queue viewer UI
|
||||
func BuildQueueView(
|
||||
jobs []*queue.Job,
|
||||
|
|
@ -212,9 +260,7 @@ func BuildQueueView(
|
|||
onViewLog func(string),
|
||||
onCopyCommand func(string),
|
||||
titleColor, bgColor, textColor color.Color,
|
||||
) (fyne.CanvasObject, *container.Scroll, []*StripedProgress) {
|
||||
// Track active progress animations to prevent goroutine leaks
|
||||
var activeProgress []*StripedProgress
|
||||
) *QueueView {
|
||||
// Header
|
||||
title := canvas.NewText("JOB QUEUE", titleColor)
|
||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||
|
|
@ -247,30 +293,11 @@ func BuildQueueView(
|
|||
container.NewCenter(title),
|
||||
)
|
||||
|
||||
// Job list
|
||||
var jobItems []fyne.CanvasObject
|
||||
jobList := container.NewVBox()
|
||||
emptyMsg := widget.NewLabel("No jobs in queue")
|
||||
emptyMsg.Alignment = fyne.TextAlignCenter
|
||||
emptyLabel := container.NewCenter(emptyMsg)
|
||||
|
||||
if len(jobs) == 0 {
|
||||
emptyMsg := widget.NewLabel("No jobs in queue")
|
||||
emptyMsg.Alignment = fyne.TextAlignCenter
|
||||
jobItems = append(jobItems, container.NewCenter(emptyMsg))
|
||||
} else {
|
||||
// Calculate queue positions for pending/paused jobs
|
||||
queuePositions := make(map[string]int)
|
||||
position := 1
|
||||
for _, job := range jobs {
|
||||
if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused {
|
||||
queuePositions[job.ID] = position
|
||||
position++
|
||||
}
|
||||
}
|
||||
|
||||
for _, job := range jobs {
|
||||
jobItems = append(jobItems, buildJobItem(job, queuePositions, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, onViewLog, onCopyCommand, bgColor, textColor, &activeProgress))
|
||||
}
|
||||
}
|
||||
|
||||
jobList := container.NewVBox(jobItems...)
|
||||
// Use a scroll container anchored to the top to avoid jumpy scroll-to-content behavior.
|
||||
scrollable := container.NewScroll(jobList)
|
||||
// scrollable.SetMinSize(fyne.NewSize(0, 0)) // Removed for flexible sizing
|
||||
|
|
@ -282,25 +309,43 @@ func BuildQueueView(
|
|||
scrollable,
|
||||
)
|
||||
|
||||
return container.NewPadded(body), scrollable, activeProgress
|
||||
view := &QueueView{
|
||||
Root: container.NewPadded(body),
|
||||
Scroll: scrollable,
|
||||
jobList: jobList,
|
||||
emptyLabel: emptyLabel,
|
||||
items: make(map[string]*queueItemWidgets),
|
||||
callbacks: queueCallbacks{
|
||||
onBack: onBack,
|
||||
onPause: onPause,
|
||||
onResume: onResume,
|
||||
onCancel: onCancel,
|
||||
onRemove: onRemove,
|
||||
onMoveUp: onMoveUp,
|
||||
onMoveDown: onMoveDown,
|
||||
onPauseAll: onPauseAll,
|
||||
onResumeAll: onResumeAll,
|
||||
onStart: onStart,
|
||||
onClear: onClear,
|
||||
onClearAll: onClearAll,
|
||||
onCopyError: onCopyError,
|
||||
onViewLog: onViewLog,
|
||||
onCopyCommand: onCopyCommand,
|
||||
},
|
||||
bgColor: bgColor,
|
||||
textColor: textColor,
|
||||
}
|
||||
view.UpdateJobs(jobs)
|
||||
return view
|
||||
}
|
||||
|
||||
// buildJobItem creates a single job item in the queue list
|
||||
func buildJobItem(
|
||||
job *queue.Job,
|
||||
queuePositions map[string]int,
|
||||
onPause func(string),
|
||||
onResume func(string),
|
||||
onCancel func(string),
|
||||
onRemove func(string),
|
||||
onMoveUp func(string),
|
||||
onMoveDown func(string),
|
||||
onCopyError func(string),
|
||||
onViewLog func(string),
|
||||
onCopyCommand func(string),
|
||||
callbacks queueCallbacks,
|
||||
bgColor, textColor color.Color,
|
||||
activeProgress *[]*StripedProgress,
|
||||
) fyne.CanvasObject {
|
||||
) *queueItemWidgets {
|
||||
// Status color
|
||||
statusColor := GetStatusColor(job.Status)
|
||||
|
||||
|
|
@ -328,8 +373,6 @@ func buildJobItem(
|
|||
if job.Status == queue.JobStatusRunning {
|
||||
progress.SetActivity(job.Progress <= 0.01)
|
||||
progress.StartAnimation()
|
||||
// Track active progress to stop animation on next refresh (prevents goroutine leaks)
|
||||
*activeProgress = append(*activeProgress, progress)
|
||||
} else {
|
||||
progress.SetActivity(false)
|
||||
progress.StopAnimation()
|
||||
|
|
@ -345,52 +388,7 @@ func buildJobItem(
|
|||
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
statusLabel.Wrapping = fyne.TextTruncate
|
||||
|
||||
// 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,
|
||||
widget.NewButton("Copy Command", func() { onCopyCommand(job.ID) }),
|
||||
widget.NewButton("Pause", func() { onPause(job.ID) }),
|
||||
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||
)
|
||||
case queue.JobStatusPaused:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Resume", func() { onResume(job.ID) }),
|
||||
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
|
||||
)
|
||||
case queue.JobStatusPending:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Copy Command", func() { onCopyCommand(job.ID) }),
|
||||
)
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Remove", func() { onRemove(job.ID) }),
|
||||
)
|
||||
case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled:
|
||||
if job.Status == queue.JobStatusFailed && strings.TrimSpace(job.Error) != "" && onCopyError != nil {
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Copy Error", func() { onCopyError(job.ID) }),
|
||||
)
|
||||
}
|
||||
if job.LogPath != "" && onViewLog != nil {
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("View Log", func() { onViewLog(job.ID) }),
|
||||
)
|
||||
}
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Remove", func() { onRemove(job.ID) }),
|
||||
)
|
||||
}
|
||||
|
||||
buttonBox := container.NewHBox(buttons...)
|
||||
buttonBox := buildJobButtons(job, callbacks)
|
||||
|
||||
// Info section
|
||||
infoBox := container.NewVBox(
|
||||
|
|
@ -418,13 +416,136 @@ func buildJobItem(
|
|||
)
|
||||
|
||||
// Wrap with draggable to allow drag-to-reorder (up/down by drag direction)
|
||||
return newDraggableJobItem(job.ID, item, func(id string, dir int) {
|
||||
wrapped := newDraggableJobItem(job.ID, item, func(id string, dir int) {
|
||||
if dir < 0 {
|
||||
onMoveUp(id)
|
||||
callbacks.onMoveUp(id)
|
||||
} else if dir > 0 {
|
||||
onMoveDown(id)
|
||||
callbacks.onMoveDown(id)
|
||||
}
|
||||
})
|
||||
|
||||
return &queueItemWidgets{
|
||||
jobID: job.ID,
|
||||
status: job.Status,
|
||||
container: wrapped,
|
||||
titleLabel: titleLabel,
|
||||
descLabel: descLabel,
|
||||
statusLabel: statusLabel,
|
||||
progress: progress,
|
||||
buttonBox: buttonBox,
|
||||
}
|
||||
}
|
||||
|
||||
func buildJobButtons(job *queue.Job, callbacks queueCallbacks) *fyne.Container {
|
||||
var buttons []fyne.CanvasObject
|
||||
|
||||
if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused {
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("↑", func() { callbacks.onMoveUp(job.ID) }),
|
||||
widget.NewButton("↓", func() { callbacks.onMoveDown(job.ID) }),
|
||||
)
|
||||
}
|
||||
|
||||
switch job.Status {
|
||||
case queue.JobStatusRunning:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Copy Command", func() { callbacks.onCopyCommand(job.ID) }),
|
||||
widget.NewButton("Pause", func() { callbacks.onPause(job.ID) }),
|
||||
widget.NewButton("Cancel", func() { callbacks.onCancel(job.ID) }),
|
||||
)
|
||||
case queue.JobStatusPaused:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Resume", func() { callbacks.onResume(job.ID) }),
|
||||
widget.NewButton("Cancel", func() { callbacks.onCancel(job.ID) }),
|
||||
)
|
||||
case queue.JobStatusPending:
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Copy Command", func() { callbacks.onCopyCommand(job.ID) }),
|
||||
widget.NewButton("Remove", func() { callbacks.onRemove(job.ID) }),
|
||||
)
|
||||
case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled:
|
||||
if job.Status == queue.JobStatusFailed && strings.TrimSpace(job.Error) != "" && callbacks.onCopyError != nil {
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Copy Error", func() { callbacks.onCopyError(job.ID) }),
|
||||
)
|
||||
}
|
||||
if job.LogPath != "" && callbacks.onViewLog != nil {
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("View Log", func() { callbacks.onViewLog(job.ID) }),
|
||||
)
|
||||
}
|
||||
buttons = append(buttons,
|
||||
widget.NewButton("Remove", func() { callbacks.onRemove(job.ID) }),
|
||||
)
|
||||
}
|
||||
|
||||
return container.NewHBox(buttons...)
|
||||
}
|
||||
|
||||
func updateJobItem(item *queueItemWidgets, job *queue.Job, queuePositions map[string]int, callbacks queueCallbacks) {
|
||||
item.titleLabel.SetText(utils.ShortenMiddle(job.Title, 60))
|
||||
item.descLabel.SetText(utils.ShortenMiddle(job.Description, 90))
|
||||
item.statusLabel.SetText(getStatusText(job, queuePositions))
|
||||
|
||||
if job.Status == queue.JobStatusCompleted {
|
||||
item.progress.SetProgress(1.0)
|
||||
} else {
|
||||
item.progress.SetProgress(job.Progress / 100.0)
|
||||
}
|
||||
|
||||
if job.Status == queue.JobStatusRunning {
|
||||
item.progress.SetActivity(job.Progress <= 0.01)
|
||||
item.progress.StartAnimation()
|
||||
} else {
|
||||
item.progress.SetActivity(false)
|
||||
item.progress.StopAnimation()
|
||||
}
|
||||
|
||||
if item.status != job.Status {
|
||||
item.status = job.Status
|
||||
item.buttonBox.Objects = buildJobButtons(job, callbacks).Objects
|
||||
item.buttonBox.Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
func (v *QueueView) UpdateJobs(jobs []*queue.Job) {
|
||||
if len(jobs) == 0 {
|
||||
v.jobList.Objects = []fyne.CanvasObject{v.emptyLabel}
|
||||
v.jobList.Refresh()
|
||||
return
|
||||
}
|
||||
|
||||
queuePositions := make(map[string]int)
|
||||
position := 1
|
||||
for _, job := range jobs {
|
||||
if job.Status == queue.JobStatusPending || job.Status == queue.JobStatusPaused {
|
||||
queuePositions[job.ID] = position
|
||||
position++
|
||||
}
|
||||
}
|
||||
|
||||
ordered := make([]fyne.CanvasObject, 0, len(jobs))
|
||||
seen := make(map[string]struct{}, len(jobs))
|
||||
for _, job := range jobs {
|
||||
seen[job.ID] = struct{}{}
|
||||
item := v.items[job.ID]
|
||||
if item == nil {
|
||||
item = buildJobItem(job, queuePositions, v.callbacks, v.bgColor, v.textColor)
|
||||
v.items[job.ID] = item
|
||||
} else {
|
||||
updateJobItem(item, job, queuePositions, v.callbacks)
|
||||
}
|
||||
ordered = append(ordered, item.container)
|
||||
}
|
||||
|
||||
for id := range v.items {
|
||||
if _, ok := seen[id]; !ok {
|
||||
delete(v.items, id)
|
||||
}
|
||||
}
|
||||
|
||||
v.jobList.Objects = ordered
|
||||
v.jobList.Refresh()
|
||||
}
|
||||
|
||||
// getStatusText returns a human-readable status string
|
||||
|
|
|
|||
345
main.go
345
main.go
|
|
@ -1002,7 +1002,7 @@ type appState struct {
|
|||
|
||||
queueAutoRefreshStop chan struct{}
|
||||
queueAutoRefreshRunning bool
|
||||
queueActiveProgress []*ui.StripedProgress // Track active progress animations to prevent goroutine leaks
|
||||
queueView *ui.QueueView
|
||||
|
||||
// Main menu refresh throttling
|
||||
mainMenuLastRefresh time.Time
|
||||
|
|
@ -1643,6 +1643,9 @@ func (s *appState) showMainMenu() {
|
|||
s.stopPreview()
|
||||
s.stopPlayer()
|
||||
s.stopQueueAutoRefresh()
|
||||
if s.queueView != nil {
|
||||
s.queueView.StopAnimations()
|
||||
}
|
||||
s.active = ""
|
||||
s.queueBackTarget = ""
|
||||
|
||||
|
|
@ -1791,6 +1794,9 @@ func (s *appState) showQueue() {
|
|||
}
|
||||
s.active = "queue"
|
||||
s.refreshQueueView()
|
||||
if s.queueView != nil {
|
||||
s.setContent(s.queueView.Root)
|
||||
}
|
||||
s.startQueueAutoRefresh()
|
||||
}
|
||||
|
||||
|
|
@ -1839,178 +1845,159 @@ func (s *appState) refreshQueueView() {
|
|||
}}, jobs...)
|
||||
}
|
||||
|
||||
// CRITICAL: Stop all active progress animations before rebuilding to prevent goroutine leaks
|
||||
// Each refresh creates new StripedProgress widgets, and old animation goroutines must be stopped
|
||||
for _, progress := range s.queueActiveProgress {
|
||||
if progress != nil {
|
||||
progress.StopAnimation()
|
||||
}
|
||||
}
|
||||
s.queueActiveProgress = nil
|
||||
|
||||
view, scroll, activeProgress := ui.BuildQueueView(
|
||||
jobs,
|
||||
func() { // onBack
|
||||
// Stop auto-refresh before navigating away for snappy response
|
||||
s.stopQueueAutoRefresh()
|
||||
target := s.queueBackTarget
|
||||
if target == "" {
|
||||
target = s.lastModule
|
||||
}
|
||||
if target != "" && target != "queue" && target != "menu" {
|
||||
s.showModule(target)
|
||||
} else {
|
||||
s.showMainMenu()
|
||||
}
|
||||
},
|
||||
func(id string) { // onPause
|
||||
if err := s.jobQueue.Pause(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to pause job: %v", err)
|
||||
}
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func(id string) { // onResume
|
||||
if err := s.jobQueue.Resume(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to resume job: %v", err)
|
||||
}
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func(id string) { // onCancel
|
||||
if err := s.jobQueue.Cancel(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to cancel job: %v", err)
|
||||
}
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func(id string) { // onRemove
|
||||
if err := s.jobQueue.Remove(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to remove job: %v", err)
|
||||
}
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func(id string) { // onMoveUp
|
||||
if err := s.jobQueue.MoveUp(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to move job up: %v", err)
|
||||
}
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func(id string) { // onMoveDown
|
||||
if err := s.jobQueue.MoveDown(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to move job down: %v", err)
|
||||
}
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func() { // onPauseAll
|
||||
s.jobQueue.PauseAll()
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func() { // onResumeAll
|
||||
s.jobQueue.ResumeAll()
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func() { // onStart
|
||||
s.jobQueue.ResumeAll()
|
||||
// Queue onChange callback handles refresh automatically
|
||||
},
|
||||
func() { // onClear
|
||||
// Stop auto-refresh to prevent double UI updates
|
||||
s.stopQueueAutoRefresh()
|
||||
s.jobQueue.Clear()
|
||||
|
||||
// Always return to main menu after clearing
|
||||
if len(s.jobQueue.List()) == 0 {
|
||||
s.showMainMenu()
|
||||
} else {
|
||||
// Restart auto-refresh and do single refresh
|
||||
s.startQueueAutoRefresh()
|
||||
s.refreshQueueView()
|
||||
}
|
||||
},
|
||||
func() { // onClearAll
|
||||
// Stop auto-refresh to prevent double UI updates during navigation
|
||||
s.stopQueueAutoRefresh()
|
||||
s.jobQueue.ClearAll()
|
||||
// Return to the module we were working on if possible
|
||||
if s.lastModule != "" && s.lastModule != "queue" && s.lastModule != "menu" {
|
||||
s.showModule(s.lastModule)
|
||||
} else {
|
||||
s.showMainMenu()
|
||||
}
|
||||
},
|
||||
func(id string) { // onCopyError
|
||||
job, err := s.jobQueue.Get(id)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatSystem, "copy error text failed: %v", err)
|
||||
return
|
||||
}
|
||||
text := strings.TrimSpace(job.Error)
|
||||
if text == "" {
|
||||
text = fmt.Sprintf("%s: no error message available", job.Title)
|
||||
}
|
||||
s.window.Clipboard().SetContent(text)
|
||||
},
|
||||
func(id string) { // onViewLog
|
||||
job, err := s.jobQueue.Get(id)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatSystem, "view log failed: %v", err)
|
||||
return
|
||||
}
|
||||
path := strings.TrimSpace(job.LogPath)
|
||||
if path == "" {
|
||||
dialog.ShowInformation("No Log", "No log path recorded for this job.", s.window)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
dialog.ShowError(fmt.Errorf("failed to read log: %w", err), s.window)
|
||||
return
|
||||
}
|
||||
text := widget.NewMultiLineEntry()
|
||||
text.SetText(string(data))
|
||||
text.Wrapping = fyne.TextWrapWord
|
||||
text.Disable()
|
||||
dialog.ShowCustom("Conversion Log", "Close", container.NewVScroll(text), s.window)
|
||||
},
|
||||
func(id string) { // onCopyCommand
|
||||
job, err := s.jobQueue.Get(id)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatSystem, "copy command failed: %v", err)
|
||||
return
|
||||
}
|
||||
cmdStr := buildFFmpegCommandFromJob(job)
|
||||
if cmdStr == "" {
|
||||
dialog.ShowInformation("No Command", "Unable to generate FFmpeg command for this job.", s.window)
|
||||
return
|
||||
}
|
||||
s.window.Clipboard().SetContent(cmdStr)
|
||||
dialog.ShowInformation("Copied", "FFmpeg command copied to clipboard", s.window)
|
||||
},
|
||||
utils.MustHex("#4CE870"), // titleColor
|
||||
gridColor, // bgColor
|
||||
textColor, // textColor
|
||||
)
|
||||
|
||||
// Restore scroll offset
|
||||
s.queueScroll = scroll
|
||||
if s.queueScroll != nil && s.active == "queue" {
|
||||
// Restore scroll position immediately to reduce jankiness
|
||||
// Set offset before showing to avoid visible jumping
|
||||
savedOffset := s.queueOffset
|
||||
go func() {
|
||||
// Minimal delay to allow layout calculation
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
if s.queueScroll != nil {
|
||||
s.queueScroll.Offset = savedOffset
|
||||
s.queueScroll.Refresh()
|
||||
if s.queueView == nil {
|
||||
view := ui.BuildQueueView(
|
||||
jobs,
|
||||
func() { // onBack
|
||||
// Stop auto-refresh before navigating away for snappy response
|
||||
s.stopQueueAutoRefresh()
|
||||
target := s.queueBackTarget
|
||||
if target == "" {
|
||||
target = s.lastModule
|
||||
}
|
||||
}, false)
|
||||
}()
|
||||
if target != "" && target != "queue" && target != "menu" {
|
||||
s.showModule(target)
|
||||
} else {
|
||||
s.showMainMenu()
|
||||
}
|
||||
},
|
||||
func(id string) { // onPause
|
||||
if err := s.jobQueue.Pause(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to pause job: %v", err)
|
||||
}
|
||||
},
|
||||
func(id string) { // onResume
|
||||
if err := s.jobQueue.Resume(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to resume job: %v", err)
|
||||
}
|
||||
},
|
||||
func(id string) { // onCancel
|
||||
if err := s.jobQueue.Cancel(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to cancel job: %v", err)
|
||||
}
|
||||
},
|
||||
func(id string) { // onRemove
|
||||
if err := s.jobQueue.Remove(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to remove job: %v", err)
|
||||
}
|
||||
},
|
||||
func(id string) { // onMoveUp
|
||||
if err := s.jobQueue.MoveUp(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to move job up: %v", err)
|
||||
}
|
||||
},
|
||||
func(id string) { // onMoveDown
|
||||
if err := s.jobQueue.MoveDown(id); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to move job down: %v", err)
|
||||
}
|
||||
},
|
||||
func() { // onPauseAll
|
||||
s.jobQueue.PauseAll()
|
||||
},
|
||||
func() { // onResumeAll
|
||||
s.jobQueue.ResumeAll()
|
||||
},
|
||||
func() { // onStart
|
||||
s.jobQueue.ResumeAll()
|
||||
},
|
||||
func() { // onClear
|
||||
// Stop auto-refresh to prevent double UI updates
|
||||
s.stopQueueAutoRefresh()
|
||||
s.jobQueue.Clear()
|
||||
|
||||
// Always return to main menu after clearing
|
||||
if len(s.jobQueue.List()) == 0 {
|
||||
s.showMainMenu()
|
||||
} else {
|
||||
// Restart auto-refresh and do single refresh
|
||||
s.startQueueAutoRefresh()
|
||||
s.refreshQueueView()
|
||||
}
|
||||
},
|
||||
func() { // onClearAll
|
||||
// Stop auto-refresh to prevent double UI updates during navigation
|
||||
s.stopQueueAutoRefresh()
|
||||
s.jobQueue.ClearAll()
|
||||
// Return to the module we were working on if possible
|
||||
if s.lastModule != "" && s.lastModule != "queue" && s.lastModule != "menu" {
|
||||
s.showModule(s.lastModule)
|
||||
} else {
|
||||
s.showMainMenu()
|
||||
}
|
||||
},
|
||||
func(id string) { // onCopyError
|
||||
job, err := s.jobQueue.Get(id)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatSystem, "copy error text failed: %v", err)
|
||||
return
|
||||
}
|
||||
text := strings.TrimSpace(job.Error)
|
||||
if text == "" {
|
||||
text = fmt.Sprintf("%s: no error message available", job.Title)
|
||||
}
|
||||
s.window.Clipboard().SetContent(text)
|
||||
},
|
||||
func(id string) { // onViewLog
|
||||
job, err := s.jobQueue.Get(id)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatSystem, "view log failed: %v", err)
|
||||
return
|
||||
}
|
||||
path := strings.TrimSpace(job.LogPath)
|
||||
if path == "" {
|
||||
dialog.ShowInformation("No Log", "No log path recorded for this job.", s.window)
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
dialog.ShowError(fmt.Errorf("failed to read log: %w", err), s.window)
|
||||
return
|
||||
}
|
||||
text := widget.NewMultiLineEntry()
|
||||
text.SetText(string(data))
|
||||
text.Wrapping = fyne.TextWrapWord
|
||||
text.Disable()
|
||||
dialog.ShowCustom("Conversion Log", "Close", container.NewVScroll(text), s.window)
|
||||
},
|
||||
func(id string) { // onCopyCommand
|
||||
job, err := s.jobQueue.Get(id)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatSystem, "copy command failed: %v", err)
|
||||
return
|
||||
}
|
||||
cmdStr := buildFFmpegCommandFromJob(job)
|
||||
if cmdStr == "" {
|
||||
dialog.ShowInformation("No Command", "Unable to generate FFmpeg command for this job.", s.window)
|
||||
return
|
||||
}
|
||||
s.window.Clipboard().SetContent(cmdStr)
|
||||
dialog.ShowInformation("Copied", "FFmpeg command copied to clipboard", s.window)
|
||||
},
|
||||
utils.MustHex("#4CE870"), // titleColor
|
||||
gridColor, // bgColor
|
||||
textColor, // textColor
|
||||
)
|
||||
|
||||
s.queueView = view
|
||||
s.queueScroll = view.Scroll
|
||||
s.setContent(view.Root)
|
||||
|
||||
// Restore scroll offset
|
||||
if s.queueScroll != nil && s.active == "queue" {
|
||||
savedOffset := s.queueOffset
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
if s.queueScroll != nil {
|
||||
s.queueScroll.Offset = savedOffset
|
||||
s.queueScroll.Refresh()
|
||||
}
|
||||
}, false)
|
||||
}()
|
||||
}
|
||||
} else {
|
||||
s.queueView.UpdateJobs(jobs)
|
||||
}
|
||||
|
||||
// Store active progress bars to stop them on next refresh
|
||||
s.queueActiveProgress = activeProgress
|
||||
|
||||
s.setContent(container.NewPadded(view))
|
||||
}
|
||||
|
||||
func (s *appState) startQueueAutoRefresh() {
|
||||
|
|
@ -2058,13 +2045,9 @@ func (s *appState) stopQueueAutoRefresh() {
|
|||
s.queueAutoRefreshStop = nil
|
||||
s.queueAutoRefreshRunning = false
|
||||
|
||||
// Stop all active progress animations to prevent goroutine leaks when leaving queue view
|
||||
for _, progress := range s.queueActiveProgress {
|
||||
if progress != nil {
|
||||
progress.StopAnimation()
|
||||
}
|
||||
if s.queueView != nil {
|
||||
s.queueView.StopAnimations()
|
||||
}
|
||||
s.queueActiveProgress = nil
|
||||
}
|
||||
|
||||
// addConvertToQueue adds a conversion job to the queue
|
||||
|
|
@ -2738,6 +2721,9 @@ func (s *appState) showMissingDependenciesDialog(moduleID string) {
|
|||
func (s *appState) showModule(id string) {
|
||||
if id != "queue" {
|
||||
s.stopQueueAutoRefresh()
|
||||
if s.queueView != nil {
|
||||
s.queueView.StopAnimations()
|
||||
}
|
||||
}
|
||||
|
||||
// Check if module has missing dependencies
|
||||
|
|
@ -6629,7 +6615,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
|
||||
// Format selector
|
||||
formatContainer := widget.NewSelect(formatLabels, func(selected string) {
|
||||
formatColors := ui.BuildFormatColorMap(formatLabels)
|
||||
formatContainer := ui.NewColoredSelect(formatLabels, formatColors, func(selected string) {
|
||||
for _, opt := range formatOptions {
|
||||
if opt.Label == selected {
|
||||
state.convert.SelectedFormat = opt
|
||||
|
|
@ -6643,7 +6630,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
break
|
||||
}
|
||||
}
|
||||
})
|
||||
}, state.window)
|
||||
formatContainer.SetSelected(state.convert.SelectedFormat.Label)
|
||||
|
||||
outputHint := widget.NewLabel(fmt.Sprintf("Output file: %s", state.convert.OutputFile()))
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user