Compare commits

...

5 Commits

Author SHA1 Message Date
03569cd813 Swap hover and selection UI colors
- Blue color now used as default selection/background
- Mid-grey now used as hover color
- Applied through MonoTheme Color() method override
- Affects all dropdowns, lists, and selectable UI elements

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-30 12:02:55 -05:00
f50edeb9c6 Add enhanced logging for DVD authoring pipeline
- Log all generated MPG files with sizes before dvdauthor
- Log complete DVD XML content for debugging
- Add specific error messages at each dvdauthor step
- Verify and log ISO file creation success
- Better error context for diagnosing 80% failure point

This will help diagnose the exit code 1 error when authoring
4-scene discs to ISO format.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 22:14:45 -05:00
de81c9f999 Add clear completed button to all module top bars
- Added clearCompletedJobs() method to appState in main.go
- Clears only completed and failed jobs, keeps pending/running/paused
- Added small ⌫ (backspace) icon button next to View Queue in all modules
- Button has LowImportance styling to keep it subtle
- Implements Jake's suggestion for quick queue cleanup

Modules updated:
- Convert (main.go)
- Author (author_module.go)
- Subtitles (subtitles_module.go)
- Rip (rip_module.go)
- Filters (filters_module.go)
- Thumbnails (thumb_module.go)
- Inspect (inspect_module.go)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 22:02:35 -05:00
16767a5ca6 refactor(ui): Reorganize metadata into compact two-column layout
- Replaced single-column Form widget with two-column grid layout
- Created makeRow helper for compact key-value pairs
- Left column: File, Format, Resolution, Aspect, Duration, FPS, etc.
- Right column: Codecs, Bitrates, Pixel Format, Channels, etc.
- More efficient use of space, matches modern UI design
- Text truncation prevents overflow

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 12:19:15 -05:00
3645291988 refactor(ui): Remove benchmark indicator from Convert module top bar
- Removed benchmark status display and apply button from top bar
- Cleaner UI matching mockup design
- Benchmark functionality still accessible via Settings menu
- Reduces visual clutter in Convert module

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-29 12:15:27 -05:00
8 changed files with 133 additions and 86 deletions

View File

@ -156,7 +156,12 @@ func buildAuthorView(state *appState) fyne.CanvasObject {
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
topBar := ui.TintedBar(authorColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
clearCompletedBtn := widget.NewButton("⌫", func() {
state.clearCompletedJobs()
})
clearCompletedBtn.Importance = widget.LowImportance
topBar := ui.TintedBar(authorColor, container.NewHBox(backBtn, layout.NewSpacer(), clearCompletedBtn, queueBtn))
bottomBar := moduleFooter(authorColor, layout.NewSpacer(), state.statsBar)
tabs := container.NewAppTabs(
@ -1761,15 +1766,34 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
chapters = nil
}
// Log details about encoded MPG files
if logFn != nil {
logFn(fmt.Sprintf("Created %d MPEG file(s):", len(mpgPaths)))
for i, mpg := range mpgPaths {
if info, err := os.Stat(mpg); err == nil {
logFn(fmt.Sprintf(" %d. %s (%d bytes)", i+1, filepath.Base(mpg), info.Size()))
} else {
logFn(fmt.Sprintf(" %d. %s (stat failed: %v)", i+1, filepath.Base(mpg), err))
}
}
}
xmlPath := filepath.Join(workDir, "dvd.xml")
if err := writeDVDAuthorXML(xmlPath, mpgPaths, region, aspect, chapters); err != nil {
return err
}
// Log the XML content for debugging
if xmlContent, err := os.ReadFile(xmlPath); err == nil {
logFn("Generated DVD XML:")
logFn(string(xmlContent))
}
logFn("Authoring DVD structure...")
logFn(fmt.Sprintf(">> dvdauthor -o %s -x %s", discRoot, xmlPath))
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-x", xmlPath}, logFn); err != nil {
return err
logFn(fmt.Sprintf("ERROR: dvdauthor failed: %v", err))
return fmt.Errorf("dvdauthor structure creation failed: %w", err)
}
accumulatedProgress += progressForOtherStep
progressFn(accumulatedProgress)
@ -1777,7 +1801,8 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
logFn("Building DVD tables...")
logFn(fmt.Sprintf(">> dvdauthor -o %s -T", discRoot))
if err := runCommandWithLogger(ctx, "dvdauthor", []string{"-o", discRoot, "-T"}, logFn); err != nil {
return err
logFn(fmt.Sprintf("ERROR: dvdauthor -T failed: %v", err))
return fmt.Errorf("dvdauthor table build failed: %w", err)
}
accumulatedProgress += progressForOtherStep
progressFn(accumulatedProgress)
@ -1789,15 +1814,24 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
if makeISO {
tool, args, err := buildISOCommand(outputPath, discRoot, title)
if err != nil {
return err
logFn(fmt.Sprintf("ERROR: ISO tool not found: %v", err))
return fmt.Errorf("ISO creation setup failed: %w", err)
}
logFn("Creating ISO image...")
logFn(fmt.Sprintf(">> %s %s", tool, strings.Join(args, " ")))
if err := runCommandWithLogger(ctx, tool, args, logFn); err != nil {
return err
logFn(fmt.Sprintf("ERROR: ISO creation failed: %v", err))
return fmt.Errorf("ISO creation failed: %w", err)
}
accumulatedProgress += progressForOtherStep
progressFn(accumulatedProgress)
// Verify ISO was created
if info, err := os.Stat(outputPath); err == nil {
logFn(fmt.Sprintf("ISO created successfully: %s (%d bytes)", filepath.Base(outputPath), info.Size()))
} else {
logFn(fmt.Sprintf("WARNING: ISO file verification failed: %v", err))
}
}
progressFn(100.0)

View File

@ -36,8 +36,13 @@ func buildFiltersView(state *appState) fyne.CanvasObject {
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
clearCompletedBtn := widget.NewButton("⌫", func() {
state.clearCompletedJobs()
})
clearCompletedBtn.Importance = widget.LowImportance
// Top bar with module color
topBar := ui.TintedBar(filtersColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
topBar := ui.TintedBar(filtersColor, container.NewHBox(backBtn, layout.NewSpacer(), clearCompletedBtn, queueBtn))
bottomBar := moduleFooter(filtersColor, layout.NewSpacer(), state.statsBar)
// Instructions

View File

@ -42,7 +42,13 @@ func buildInspectView(state *appState) fyne.CanvasObject {
})
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
topBar := ui.TintedBar(inspectColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
clearCompletedBtn := widget.NewButton("⌫", func() {
state.clearCompletedJobs()
})
clearCompletedBtn.Importance = widget.LowImportance
topBar := ui.TintedBar(inspectColor, container.NewHBox(backBtn, layout.NewSpacer(), clearCompletedBtn, queueBtn))
bottomBar := moduleFooter(inspectColor, layout.NewSpacer(), state.statsBar)
// Instructions

View File

@ -32,10 +32,19 @@ func SetColors(grid, text color.Color) {
TextColor = text
}
// MonoTheme ensures all text uses a monospace font
// MonoTheme ensures all text uses a monospace font and swaps hover/selection colors
type MonoTheme struct{}
func (m *MonoTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color {
// Swap hover and selection colors
switch name {
case theme.ColorNameSelection:
// Use the default hover color for selection
return theme.DefaultTheme().Color(theme.ColorNameHover, variant)
case theme.ColorNameHover:
// Use the default selection color for hover
return theme.DefaultTheme().Color(theme.ColorNameSelection, variant)
}
return theme.DefaultTheme().Color(name, variant)
}

127
main.go
View File

@ -1745,6 +1745,13 @@ func (s *appState) showQueue() {
s.startQueueAutoRefresh()
}
// clearCompletedJobs removes all completed and failed jobs from the queue
func (s *appState) clearCompletedJobs() {
if s.jobQueue != nil {
s.jobQueue.Clear()
}
}
// refreshQueueView rebuilds the queue UI while preserving scroll position and inline active conversion.
func (s *appState) refreshQueueView() {
if s.active == "queue" {
@ -6344,6 +6351,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
clearCompletedBtn := widget.NewButton("⌫", func() {
state.clearCompletedJobs()
})
clearCompletedBtn.Importance = widget.LowImportance
// Command Preview toggle button
cmdPreviewBtn := widget.NewButton("Command Preview", func() {
state.convertCommandPreviewShow = !state.convertCommandPreviewShow
@ -6360,60 +6372,16 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
cmdPreviewBtn.SetText("Show Preview")
}
var (
benchmarkStatus *canvas.Text
benchmarkApplyBtn *widget.Button
benchmarkIndicator fyne.CanvasObject
)
if cfg, err := loadBenchmarkConfig(); err == nil && len(cfg.History) > 0 {
run := cfg.History[0]
encoder := run.RecommendedEncoder
preset := run.RecommendedPreset
benchHW := "none"
switch {
case strings.Contains(encoder, "nvenc"):
benchHW = "nvenc"
case strings.Contains(encoder, "qsv"):
benchHW = "qsv"
case strings.Contains(encoder, "amf"):
benchHW = "amf"
case strings.Contains(encoder, "videotoolbox"):
benchHW = "videotoolbox"
}
applied := friendlyCodecFromPreset(encoder) == state.convert.VideoCodec &&
state.convert.EncoderPreset == preset &&
state.convert.HardwareAccel == benchHW
// Only show benchmark indicator if settings are NOT already applied
if !applied {
statusColor := utils.MustHex("#FFC857")
statusText := "Benchmark: Not Applied"
benchmarkStatus = canvas.NewText(statusText, statusColor)
benchmarkStatus.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
benchmarkStatus.TextSize = 12
benchmarkApplyBtn = widget.NewButton("Apply Benchmark", func() {
state.applyBenchmarkRecommendation(encoder, preset)
// Hide the entire indicator once applied
benchmarkIndicator.Hide()
})
benchmarkApplyBtn.Importance = widget.MediumImportance
benchmarkIndicator = container.NewHBox(benchmarkStatus, benchmarkApplyBtn)
}
}
// Build back bar with optional benchmark indicator
// Build back bar
backBarItems := []fyne.CanvasObject{
back,
layout.NewSpacer(),
navButtons,
layout.NewSpacer(),
cmdPreviewBtn,
clearCompletedBtn,
queueBtn,
}
if benchmarkIndicator != nil {
backBarItems = append(backBarItems, benchmarkIndicator)
}
backBarItems = append(backBarItems, cmdPreviewBtn, queueBtn)
backBar := ui.TintedBar(convertColor, container.NewHBox(backBarItems...))
@ -9155,34 +9123,43 @@ Metadata: %s`,
metadata,
)
info := widget.NewForm(
widget.NewFormItem("File", widget.NewLabel(src.DisplayName)),
widget.NewFormItem("Format Family", widget.NewLabel(utils.FirstNonEmpty(src.Format, "Unknown"))),
widget.NewFormItem("Resolution", widget.NewLabel(fmt.Sprintf("%dx%d", src.Width, src.Height))),
widget.NewFormItem("Aspect Ratio", widget.NewLabel(src.AspectRatioString())),
widget.NewFormItem("Pixel Aspect Ratio", widget.NewLabel(par)),
widget.NewFormItem("Duration", widget.NewLabel(src.DurationString())),
widget.NewFormItem("Video Codec", widget.NewLabel(utils.FirstNonEmpty(src.VideoCodec, "Unknown"))),
widget.NewFormItem("Video Bitrate", widget.NewLabel(bitrate)),
widget.NewFormItem("Frame Rate", widget.NewLabel(fmt.Sprintf("%.2f fps", src.FrameRate))),
widget.NewFormItem("Pixel Format", widget.NewLabel(utils.FirstNonEmpty(src.PixelFormat, "Unknown"))),
widget.NewFormItem("Interlacing", widget.NewLabel(interlacing)),
widget.NewFormItem("Color Space", widget.NewLabel(colorSpace)),
widget.NewFormItem("Color Range", widget.NewLabel(colorRange)),
widget.NewFormItem("GOP Size", widget.NewLabel(gopSize)),
widget.NewFormItem("Audio Codec", widget.NewLabel(utils.FirstNonEmpty(src.AudioCodec, "Unknown"))),
widget.NewFormItem("Audio Bitrate", widget.NewLabel(audioBitrate)),
widget.NewFormItem("Audio Rate", widget.NewLabel(fmt.Sprintf("%d Hz", src.AudioRate))),
widget.NewFormItem("Channels", widget.NewLabel(utils.ChannelLabel(src.Channels))),
widget.NewFormItem("Chapters", widget.NewLabel(chapters)),
widget.NewFormItem("Metadata", widget.NewLabel(metadata)),
// Helper function to create compact key-value rows
makeRow := func(key, value string) fyne.CanvasObject {
keyLabel := widget.NewLabel(key + ":")
keyLabel.TextStyle = fyne.TextStyle{Bold: true}
valueLabel := widget.NewLabel(value)
valueLabel.Wrapping = fyne.TextTruncate
return container.NewBorder(nil, nil, keyLabel, nil, container.NewHBox(layout.NewSpacer(), valueLabel))
}
// Organize metadata into a compact two-column grid
col1 := container.NewVBox(
makeRow("File", src.DisplayName),
makeRow("Format", utils.FirstNonEmpty(src.Format, "Unknown")),
makeRow("Resolution", fmt.Sprintf("%dx%d", src.Width, src.Height)),
makeRow("Aspect Ratio", src.AspectRatioString()),
makeRow("Duration", src.DurationString()),
makeRow("Frame Rate", fmt.Sprintf("%.2f fps", src.FrameRate)),
makeRow("Interlacing", interlacing),
makeRow("Color Space", colorSpace),
makeRow("Color Range", colorRange),
makeRow("GOP Size", gopSize),
)
for _, item := range info.Items {
if lbl, ok := item.Widget.(*widget.Label); ok {
lbl.Wrapping = fyne.TextWrapWord
lbl.TextStyle = fyne.TextStyle{} // prevent selection
}
}
col2 := container.NewVBox(
makeRow("Video Codec", utils.FirstNonEmpty(src.VideoCodec, "Unknown")),
makeRow("Video Bitrate", bitrate),
makeRow("Pixel Format", utils.FirstNonEmpty(src.PixelFormat, "Unknown")),
makeRow("Pixel AR", par),
makeRow("Audio Codec", utils.FirstNonEmpty(src.AudioCodec, "Unknown")),
makeRow("Audio Bitrate", audioBitrate),
makeRow("Audio Rate", fmt.Sprintf("%d Hz", src.AudioRate)),
makeRow("Channels", utils.ChannelLabel(src.Channels)),
makeRow("Chapters", chapters),
makeRow("Metadata", metadata),
)
info := container.NewHBox(col1, col2)
// Copy metadata button - beside header text
copyBtn := widget.NewButton("📋", func() {

View File

@ -115,7 +115,12 @@ func buildRipView(state *appState) fyne.CanvasObject {
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
topBar := ui.TintedBar(ripColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
clearCompletedBtn := widget.NewButton("⌫", func() {
state.clearCompletedJobs()
})
clearCompletedBtn.Importance = widget.LowImportance
topBar := ui.TintedBar(ripColor, container.NewHBox(backBtn, layout.NewSpacer(), clearCompletedBtn, queueBtn))
bottomBar := moduleFooter(ripColor, layout.NewSpacer(), state.statsBar)
sourceEntry := widget.NewEntry()

View File

@ -134,7 +134,12 @@ func buildSubtitlesView(state *appState) fyne.CanvasObject {
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
topBar := ui.TintedBar(subtitlesColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
clearCompletedBtn := widget.NewButton("⌫", func() {
state.clearCompletedJobs()
})
clearCompletedBtn.Importance = widget.LowImportance
topBar := ui.TintedBar(subtitlesColor, container.NewHBox(backBtn, layout.NewSpacer(), clearCompletedBtn, queueBtn))
bottomBar := moduleFooter(subtitlesColor, layout.NewSpacer(), state.statsBar)
videoEntry := widget.NewEntry()

View File

@ -42,7 +42,13 @@ func buildThumbView(state *appState) fyne.CanvasObject {
})
state.queueBtn = queueBtn
state.updateQueueButtonLabel()
topBar := ui.TintedBar(thumbColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
clearCompletedBtn := widget.NewButton("⌫", func() {
state.clearCompletedJobs()
})
clearCompletedBtn.Importance = widget.LowImportance
topBar := ui.TintedBar(thumbColor, container.NewHBox(backBtn, layout.NewSpacer(), clearCompletedBtn, queueBtn))
// Instructions
instructions := widget.NewLabel("Generate thumbnails from a video file. Load a video and configure settings.")