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
|
// Create scrollable list with proper spacing
|
||||||
list := container.NewVBox(items...)
|
list := container.NewVBox(items...)
|
||||||
scroll := container.NewVScroll(list)
|
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
|
// Create popup
|
||||||
cs.popup = widget.NewPopUp(scroll, cs.window.Canvas())
|
cs.popup = widget.NewPopUp(scroll, cs.window.Canvas())
|
||||||
|
cs.popup.Resize(fyne.NewSize(dropWidth, 200))
|
||||||
|
|
||||||
// Position popup below the select widget
|
// Position popup below the select widget
|
||||||
popupPos := fyne.CurrentApp().Driver().AbsolutePositionForObject(cs)
|
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}
|
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
|
// BuildQueueView creates the queue viewer UI
|
||||||
func BuildQueueView(
|
func BuildQueueView(
|
||||||
jobs []*queue.Job,
|
jobs []*queue.Job,
|
||||||
|
|
@ -212,9 +260,7 @@ func BuildQueueView(
|
||||||
onViewLog func(string),
|
onViewLog func(string),
|
||||||
onCopyCommand func(string),
|
onCopyCommand func(string),
|
||||||
titleColor, bgColor, textColor color.Color,
|
titleColor, bgColor, textColor color.Color,
|
||||||
) (fyne.CanvasObject, *container.Scroll, []*StripedProgress) {
|
) *QueueView {
|
||||||
// Track active progress animations to prevent goroutine leaks
|
|
||||||
var activeProgress []*StripedProgress
|
|
||||||
// Header
|
// Header
|
||||||
title := canvas.NewText("JOB QUEUE", titleColor)
|
title := canvas.NewText("JOB QUEUE", titleColor)
|
||||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||||
|
|
@ -247,30 +293,11 @@ func BuildQueueView(
|
||||||
container.NewCenter(title),
|
container.NewCenter(title),
|
||||||
)
|
)
|
||||||
|
|
||||||
// Job list
|
jobList := container.NewVBox()
|
||||||
var jobItems []fyne.CanvasObject
|
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.
|
// Use a scroll container anchored to the top to avoid jumpy scroll-to-content behavior.
|
||||||
scrollable := container.NewScroll(jobList)
|
scrollable := container.NewScroll(jobList)
|
||||||
// scrollable.SetMinSize(fyne.NewSize(0, 0)) // Removed for flexible sizing
|
// scrollable.SetMinSize(fyne.NewSize(0, 0)) // Removed for flexible sizing
|
||||||
|
|
@ -282,25 +309,43 @@ func BuildQueueView(
|
||||||
scrollable,
|
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
|
// buildJobItem creates a single job item in the queue list
|
||||||
func buildJobItem(
|
func buildJobItem(
|
||||||
job *queue.Job,
|
job *queue.Job,
|
||||||
queuePositions map[string]int,
|
queuePositions map[string]int,
|
||||||
onPause func(string),
|
callbacks queueCallbacks,
|
||||||
onResume func(string),
|
|
||||||
onCancel func(string),
|
|
||||||
onRemove func(string),
|
|
||||||
onMoveUp func(string),
|
|
||||||
onMoveDown func(string),
|
|
||||||
onCopyError func(string),
|
|
||||||
onViewLog func(string),
|
|
||||||
onCopyCommand func(string),
|
|
||||||
bgColor, textColor color.Color,
|
bgColor, textColor color.Color,
|
||||||
activeProgress *[]*StripedProgress,
|
) *queueItemWidgets {
|
||||||
) fyne.CanvasObject {
|
|
||||||
// Status color
|
// Status color
|
||||||
statusColor := GetStatusColor(job.Status)
|
statusColor := GetStatusColor(job.Status)
|
||||||
|
|
||||||
|
|
@ -328,8 +373,6 @@ func buildJobItem(
|
||||||
if job.Status == queue.JobStatusRunning {
|
if job.Status == queue.JobStatusRunning {
|
||||||
progress.SetActivity(job.Progress <= 0.01)
|
progress.SetActivity(job.Progress <= 0.01)
|
||||||
progress.StartAnimation()
|
progress.StartAnimation()
|
||||||
// Track active progress to stop animation on next refresh (prevents goroutine leaks)
|
|
||||||
*activeProgress = append(*activeProgress, progress)
|
|
||||||
} else {
|
} else {
|
||||||
progress.SetActivity(false)
|
progress.SetActivity(false)
|
||||||
progress.StopAnimation()
|
progress.StopAnimation()
|
||||||
|
|
@ -345,52 +388,7 @@ func buildJobItem(
|
||||||
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
||||||
statusLabel.Wrapping = fyne.TextTruncate
|
statusLabel.Wrapping = fyne.TextTruncate
|
||||||
|
|
||||||
// Control buttons
|
buttonBox := buildJobButtons(job, callbacks)
|
||||||
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...)
|
|
||||||
|
|
||||||
// Info section
|
// Info section
|
||||||
infoBox := container.NewVBox(
|
infoBox := container.NewVBox(
|
||||||
|
|
@ -418,13 +416,136 @@ func buildJobItem(
|
||||||
)
|
)
|
||||||
|
|
||||||
// Wrap with draggable to allow drag-to-reorder (up/down by drag direction)
|
// 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 {
|
if dir < 0 {
|
||||||
onMoveUp(id)
|
callbacks.onMoveUp(id)
|
||||||
} else if dir > 0 {
|
} 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
|
// getStatusText returns a human-readable status string
|
||||||
|
|
|
||||||
345
main.go
345
main.go
|
|
@ -1002,7 +1002,7 @@ type appState struct {
|
||||||
|
|
||||||
queueAutoRefreshStop chan struct{}
|
queueAutoRefreshStop chan struct{}
|
||||||
queueAutoRefreshRunning bool
|
queueAutoRefreshRunning bool
|
||||||
queueActiveProgress []*ui.StripedProgress // Track active progress animations to prevent goroutine leaks
|
queueView *ui.QueueView
|
||||||
|
|
||||||
// Main menu refresh throttling
|
// Main menu refresh throttling
|
||||||
mainMenuLastRefresh time.Time
|
mainMenuLastRefresh time.Time
|
||||||
|
|
@ -1643,6 +1643,9 @@ func (s *appState) showMainMenu() {
|
||||||
s.stopPreview()
|
s.stopPreview()
|
||||||
s.stopPlayer()
|
s.stopPlayer()
|
||||||
s.stopQueueAutoRefresh()
|
s.stopQueueAutoRefresh()
|
||||||
|
if s.queueView != nil {
|
||||||
|
s.queueView.StopAnimations()
|
||||||
|
}
|
||||||
s.active = ""
|
s.active = ""
|
||||||
s.queueBackTarget = ""
|
s.queueBackTarget = ""
|
||||||
|
|
||||||
|
|
@ -1791,6 +1794,9 @@ func (s *appState) showQueue() {
|
||||||
}
|
}
|
||||||
s.active = "queue"
|
s.active = "queue"
|
||||||
s.refreshQueueView()
|
s.refreshQueueView()
|
||||||
|
if s.queueView != nil {
|
||||||
|
s.setContent(s.queueView.Root)
|
||||||
|
}
|
||||||
s.startQueueAutoRefresh()
|
s.startQueueAutoRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1839,178 +1845,159 @@ func (s *appState) refreshQueueView() {
|
||||||
}}, jobs...)
|
}}, jobs...)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CRITICAL: Stop all active progress animations before rebuilding to prevent goroutine leaks
|
if s.queueView == nil {
|
||||||
// Each refresh creates new StripedProgress widgets, and old animation goroutines must be stopped
|
view := ui.BuildQueueView(
|
||||||
for _, progress := range s.queueActiveProgress {
|
jobs,
|
||||||
if progress != nil {
|
func() { // onBack
|
||||||
progress.StopAnimation()
|
// Stop auto-refresh before navigating away for snappy response
|
||||||
}
|
s.stopQueueAutoRefresh()
|
||||||
}
|
target := s.queueBackTarget
|
||||||
s.queueActiveProgress = nil
|
if target == "" {
|
||||||
|
target = s.lastModule
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}, 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() {
|
func (s *appState) startQueueAutoRefresh() {
|
||||||
|
|
@ -2058,13 +2045,9 @@ func (s *appState) stopQueueAutoRefresh() {
|
||||||
s.queueAutoRefreshStop = nil
|
s.queueAutoRefreshStop = nil
|
||||||
s.queueAutoRefreshRunning = false
|
s.queueAutoRefreshRunning = false
|
||||||
|
|
||||||
// Stop all active progress animations to prevent goroutine leaks when leaving queue view
|
if s.queueView != nil {
|
||||||
for _, progress := range s.queueActiveProgress {
|
s.queueView.StopAnimations()
|
||||||
if progress != nil {
|
|
||||||
progress.StopAnimation()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
s.queueActiveProgress = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// addConvertToQueue adds a conversion job to the queue
|
// addConvertToQueue adds a conversion job to the queue
|
||||||
|
|
@ -2738,6 +2721,9 @@ func (s *appState) showMissingDependenciesDialog(moduleID string) {
|
||||||
func (s *appState) showModule(id string) {
|
func (s *appState) showModule(id string) {
|
||||||
if id != "queue" {
|
if id != "queue" {
|
||||||
s.stopQueueAutoRefresh()
|
s.stopQueueAutoRefresh()
|
||||||
|
if s.queueView != nil {
|
||||||
|
s.queueView.StopAnimations()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if module has missing dependencies
|
// Check if module has missing dependencies
|
||||||
|
|
@ -6629,7 +6615,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format selector
|
// 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 {
|
for _, opt := range formatOptions {
|
||||||
if opt.Label == selected {
|
if opt.Label == selected {
|
||||||
state.convert.SelectedFormat = opt
|
state.convert.SelectedFormat = opt
|
||||||
|
|
@ -6643,7 +6630,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}, state.window)
|
||||||
formatContainer.SetSelected(state.convert.SelectedFormat.Label)
|
formatContainer.SetSelected(state.convert.SelectedFormat.Label)
|
||||||
|
|
||||||
outputHint := widget.NewLabel(fmt.Sprintf("Output file: %s", state.convert.OutputFile()))
|
outputHint := widget.NewLabel(fmt.Sprintf("Output file: %s", state.convert.OutputFile()))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user