Add history entry delete button and fix Convert module crash

Features:
- Add "×" delete button to each history entry in sidebar
- Click to remove individual entries from history
- Automatically saves and refreshes sidebar after deletion

Bug Fixes:
- Fix nil pointer crash when opening Convert module
- Fixed widget initialization order: bitrateContainer now created
  AFTER bitratePresetSelect is initialized
- Prevented "invalid memory address" panic in tabs layout

Technical Details:
- Added deleteHistoryEntry() method to remove entries by ID
- Updated BuildHistorySidebar signature to accept onEntryDelete callback
- Moved bitrateContainer creation from line 5742 to 5794
- All Select widgets now properly initialized before container creation

The crash was caused by bitrateContainer containing a nil
bitratePresetSelect widget, which crashed when Fyne's layout system
called .Visible() during tab initialization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Stu Leak 2025-12-18 11:51:26 -05:00
parent 4616dee10a
commit 5b544b8484
2 changed files with 49 additions and 15 deletions

View File

@ -154,6 +154,7 @@ func sortedKeys(m map[string][]fyne.CanvasObject) []string {
func BuildHistorySidebar(
entries []HistoryEntry,
onEntryClick func(HistoryEntry),
onEntryDelete func(HistoryEntry),
titleColor, bgColor, textColor color.Color,
) fyne.CanvasObject {
// Filter by status
@ -167,8 +168,8 @@ func BuildHistorySidebar(
}
// Build lists
completedList := buildHistoryList(completedEntries, onEntryClick, bgColor, textColor)
failedList := buildHistoryList(failedEntries, onEntryClick, bgColor, textColor)
completedList := buildHistoryList(completedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
failedList := buildHistoryList(failedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
// Tabs
tabs := container.NewAppTabs(
@ -193,6 +194,7 @@ func BuildHistorySidebar(
func buildHistoryList(
entries []HistoryEntry,
onEntryClick func(HistoryEntry),
onEntryDelete func(HistoryEntry),
bgColor, textColor color.Color,
) *fyne.Container {
if len(entries) == 0 {
@ -201,7 +203,7 @@ func buildHistoryList(
var items []fyne.CanvasObject
for _, entry := range entries {
items = append(items, buildHistoryItem(entry, onEntryClick, bgColor, textColor))
items = append(items, buildHistoryItem(entry, onEntryClick, onEntryDelete, bgColor, textColor))
}
return container.NewVBox(items...)
}
@ -209,11 +211,21 @@ func buildHistoryList(
func buildHistoryItem(
entry HistoryEntry,
onEntryClick func(HistoryEntry),
onEntryDelete func(HistoryEntry),
bgColor, textColor color.Color,
) fyne.CanvasObject {
// Badge
badge := BuildModuleBadge(entry.Type)
// Capture entry for closures
capturedEntry := entry
// Delete button - small "×" button
deleteBtn := widget.NewButton("×", func() {
onEntryDelete(capturedEntry)
})
deleteBtn.Importance = widget.LowImportance
// Title
titleLabel := widget.NewLabel(utils.ShortenMiddle(entry.Title, 25))
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
@ -234,7 +246,7 @@ func buildHistoryItem(
content := container.NewBorder(
nil, nil, statusRect, nil,
container.NewVBox(
container.NewHBox(badge, layout.NewSpacer()),
container.NewHBox(badge, layout.NewSpacer(), deleteBtn),
titleLabel,
timeLabel,
),
@ -245,7 +257,5 @@ func buildHistoryItem(
item := container.NewPadded(container.NewMax(card, content))
// Capture entry for closure
capturedEntry := entry
return NewTappable(item, func() { onEntryClick(capturedEntry) })
}

42
main.go
View File

@ -910,8 +910,8 @@ Config:
// Layout: details at top (scrollable), FFmpeg at bottom (fixed)
content := container.NewBorder(
detailsScroll, // Top: job details (scrollable, takes priority)
container.NewVBox( // Bottom: FFmpeg command (fixed)
detailsScroll, // Top: job details (scrollable, takes priority)
container.NewVBox( // Bottom: FFmpeg command (fixed)
ffmpegSection,
container.NewHBox(buttons...),
),
@ -925,6 +925,26 @@ Config:
d.Show()
}
func (s *appState) deleteHistoryEntry(entry ui.HistoryEntry) {
// Remove entry from history
var updated []ui.HistoryEntry
for _, e := range s.historyEntries {
if e.ID != entry.ID {
updated = append(updated, e)
}
}
s.historyEntries = updated
// Save updated history
cfg := historyConfig{Entries: s.historyEntries}
if err := saveHistoryConfig(cfg); err != nil {
logging.Debug(logging.CatUI, "failed to save history after delete: %v", err)
}
// Refresh main menu to update sidebar
s.showMainMenu()
}
func (s *appState) stopPreview() {
if s.anim != nil {
s.anim.Stop()
@ -1349,6 +1369,7 @@ func (s *appState) showMainMenu() {
sidebar = ui.BuildHistorySidebar(
s.historyEntries,
s.showHistoryDetails,
s.deleteHistoryEntry,
titleColor,
utils.MustHex("#1A1F2E"),
textColor,
@ -5712,18 +5733,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
}
// Create containers for hideable sections
// Create CRF container (crfEntry already initialized)
crfContainer = container.NewVBox(
widget.NewLabelWithStyle("Manual CRF (overrides Quality preset)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
crfEntry,
)
bitrateContainer = container.NewVBox(
widget.NewLabelWithStyle("Video Bitrate (for CBR/VBR)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
videoBitrateEntry,
widget.NewLabelWithStyle("Recommended Bitrate Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
bitratePresetSelect,
)
// Note: bitrateContainer creation moved below after bitratePresetSelect is initialized
type bitratePreset struct {
Label string
@ -5775,6 +5791,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
})
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
// Create bitrate container now that bitratePresetSelect is initialized
bitrateContainer = container.NewVBox(
widget.NewLabelWithStyle("Video Bitrate (for CBR/VBR)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
videoBitrateEntry,
widget.NewLabelWithStyle("Recommended Bitrate Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
bitratePresetSelect,
)
// Simple resolution selector (separate widget to avoid double-parent issues)
resolutionSelectSimple := widget.NewSelect([]string{
"Source", "360p", "480p", "540p", "720p", "1080p", "1440p", "4K", "8K",