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:
parent
4616dee10a
commit
5b544b8484
|
|
@ -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
42
main.go
|
|
@ -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",
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user