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( func BuildHistorySidebar(
entries []HistoryEntry, entries []HistoryEntry,
onEntryClick func(HistoryEntry), onEntryClick func(HistoryEntry),
onEntryDelete func(HistoryEntry),
titleColor, bgColor, textColor color.Color, titleColor, bgColor, textColor color.Color,
) fyne.CanvasObject { ) fyne.CanvasObject {
// Filter by status // Filter by status
@ -167,8 +168,8 @@ func BuildHistorySidebar(
} }
// Build lists // Build lists
completedList := buildHistoryList(completedEntries, onEntryClick, bgColor, textColor) completedList := buildHistoryList(completedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
failedList := buildHistoryList(failedEntries, onEntryClick, bgColor, textColor) failedList := buildHistoryList(failedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
// Tabs // Tabs
tabs := container.NewAppTabs( tabs := container.NewAppTabs(
@ -193,6 +194,7 @@ func BuildHistorySidebar(
func buildHistoryList( func buildHistoryList(
entries []HistoryEntry, entries []HistoryEntry,
onEntryClick func(HistoryEntry), onEntryClick func(HistoryEntry),
onEntryDelete func(HistoryEntry),
bgColor, textColor color.Color, bgColor, textColor color.Color,
) *fyne.Container { ) *fyne.Container {
if len(entries) == 0 { if len(entries) == 0 {
@ -201,7 +203,7 @@ func buildHistoryList(
var items []fyne.CanvasObject var items []fyne.CanvasObject
for _, entry := range entries { 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...) return container.NewVBox(items...)
} }
@ -209,11 +211,21 @@ func buildHistoryList(
func buildHistoryItem( func buildHistoryItem(
entry HistoryEntry, entry HistoryEntry,
onEntryClick func(HistoryEntry), onEntryClick func(HistoryEntry),
onEntryDelete func(HistoryEntry),
bgColor, textColor color.Color, bgColor, textColor color.Color,
) fyne.CanvasObject { ) fyne.CanvasObject {
// Badge // Badge
badge := BuildModuleBadge(entry.Type) 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 // Title
titleLabel := widget.NewLabel(utils.ShortenMiddle(entry.Title, 25)) titleLabel := widget.NewLabel(utils.ShortenMiddle(entry.Title, 25))
titleLabel.TextStyle = fyne.TextStyle{Bold: true} titleLabel.TextStyle = fyne.TextStyle{Bold: true}
@ -234,7 +246,7 @@ func buildHistoryItem(
content := container.NewBorder( content := container.NewBorder(
nil, nil, statusRect, nil, nil, nil, statusRect, nil,
container.NewVBox( container.NewVBox(
container.NewHBox(badge, layout.NewSpacer()), container.NewHBox(badge, layout.NewSpacer(), deleteBtn),
titleLabel, titleLabel,
timeLabel, timeLabel,
), ),
@ -245,7 +257,5 @@ func buildHistoryItem(
item := container.NewPadded(container.NewMax(card, content)) item := container.NewPadded(container.NewMax(card, content))
// Capture entry for closure
capturedEntry := entry
return NewTappable(item, func() { onEntryClick(capturedEntry) }) 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) // Layout: details at top (scrollable), FFmpeg at bottom (fixed)
content := container.NewBorder( content := container.NewBorder(
detailsScroll, // Top: job details (scrollable, takes priority) detailsScroll, // Top: job details (scrollable, takes priority)
container.NewVBox( // Bottom: FFmpeg command (fixed) container.NewVBox( // Bottom: FFmpeg command (fixed)
ffmpegSection, ffmpegSection,
container.NewHBox(buttons...), container.NewHBox(buttons...),
), ),
@ -925,6 +925,26 @@ Config:
d.Show() 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() { func (s *appState) stopPreview() {
if s.anim != nil { if s.anim != nil {
s.anim.Stop() s.anim.Stop()
@ -1349,6 +1369,7 @@ func (s *appState) showMainMenu() {
sidebar = ui.BuildHistorySidebar( sidebar = ui.BuildHistorySidebar(
s.historyEntries, s.historyEntries,
s.showHistoryDetails, s.showHistoryDetails,
s.deleteHistoryEntry,
titleColor, titleColor,
utils.MustHex("#1A1F2E"), utils.MustHex("#1A1F2E"),
textColor, 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( crfContainer = container.NewVBox(
widget.NewLabelWithStyle("Manual CRF (overrides Quality preset)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Manual CRF (overrides Quality preset)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
crfEntry, crfEntry,
) )
bitrateContainer = container.NewVBox( // Note: bitrateContainer creation moved below after bitratePresetSelect is initialized
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,
)
type bitratePreset struct { type bitratePreset struct {
Label string Label string
@ -5775,6 +5791,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}) })
simpleBitrateSelect.SetSelected(state.convert.BitratePreset) 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) // Simple resolution selector (separate widget to avoid double-parent issues)
resolutionSelectSimple := widget.NewSelect([]string{ resolutionSelectSimple := widget.NewSelect([]string{
"Source", "360p", "480p", "540p", "720p", "1080p", "1440p", "4K", "8K", "Source", "360p", "480p", "540p", "720p", "1080p", "1440p", "4K", "8K",