Fix thumbnail generation and add viewing capability

Fixed Thumbnail Count Issue:
- Changed frame selection from hardcoded 30fps to timestamp-based
- Now uses gte(t,timestamp) filter for accurate frame selection
- This fixes the issue where 5x8 grid only generated 34 instead of 40 thumbnails

Improved Contact Sheet Display:
- Reduced thumbnail width from 320px to 200px for better window fit
- Changed background color from black to app theme (#0B0F1A)
- Contact sheets now match the VideoTools dark blue theme

Added Viewing Capability:
- New "View Results" button in thumbnail module
- Contact sheet mode: Shows image in full-screen dialog (900x700)
- Individual mode: Opens thumbnail folder in file manager
- Button checks if output exists before showing
- Provides user-friendly messages when no results found

Benefits:
- Correct number of thumbnails generated for any grid size
- Contact sheets fit better in display window
- Visual consistency with app theme
- Easy access to view generated results within the app

🤖 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-13 20:56:05 -05:00
parent d6fd5fc762
commit f1d445dd0a
2 changed files with 63 additions and 13 deletions

View File

@ -284,15 +284,17 @@ func (g *Generator) generateContactSheet(ctx context.Context, config Config, dur
tempConfig.Interval = 0
timestamps := g.calculateTimestamps(tempConfig, duration)
// Build select filter for timestamps
// Build select filter using timestamps (more reliable than frame numbers)
// Use gte(t,timestamp) approach to select frames closest to target times
selectFilter := "select='"
for i, ts := range timestamps {
if i > 0 {
selectFilter += "+"
}
selectFilter += fmt.Sprintf("eq(n\\,%d)", int(ts*30)) // Assuming 30fps, should calculate actual fps
// Select frame at or after this timestamp, limiting to one frame per timestamp
selectFilter += fmt.Sprintf("gte(t\\,%.2f)*lt(t\\,%.2f)", ts, ts+0.1)
}
selectFilter += "'"
selectFilter += "',setpts=N/TB"
outputPath := filepath.Join(config.OutputDir, fmt.Sprintf("contact_sheet.%s", config.Format))
@ -362,11 +364,12 @@ func (g *Generator) buildMetadataFilter(config Config, duration float64, thumbWi
// Create filter that:
// 1. Generates contact sheet from selected frames
// 2. Creates a blank header area
// 2. Creates a blank header area with app background color
// 3. Draws metadata text on header (using monospace font)
// 4. Stacks header on top of contact sheet
// App background color: #0B0F1A (dark blue)
filter := fmt.Sprintf(
"%s,%s,pad=%d:%d:0:%d:black,"+
"%s,%s,pad=%d:%d:0:%d:0x0B0F1A,"+
"drawtext=text='%s':fontcolor=white:fontsize=14:font='DejaVu Sans Mono':x=10:y=10,"+
"drawtext=text='%s':fontcolor=white:fontsize=12:font='DejaVu Sans Mono':x=10:y=35",
selectFilter,

63
main.go
View File

@ -617,12 +617,13 @@ type appState struct {
mergeChapters bool
// Thumbnail module state
thumbFile *videoSource
thumbCount int
thumbWidth int
thumbContactSheet bool
thumbColumns int
thumbRows int
thumbFile *videoSource
thumbCount int
thumbWidth int
thumbContactSheet bool
thumbColumns int
thumbRows int
thumbLastOutputPath string // Path to last generated output
// Interlacing detection state
interlaceResult *interlace.DetectionResult
@ -9682,9 +9683,9 @@ func buildThumbView(state *appState) fyne.CanvasObject {
var count, width int
var description string
if state.thumbContactSheet {
// Contact sheet: count is determined by grid, use default width
// Contact sheet: count is determined by grid, use smaller width to fit window
count = state.thumbColumns * state.thumbRows
width = 320 // Fixed width for contact sheets
width = 200 // Smaller width for contact sheets to fit larger grids
description = fmt.Sprintf("Contact sheet: %dx%d grid (%d thumbnails)", state.thumbColumns, state.thumbRows, count)
} else {
// Individual thumbnails: use user settings
@ -9729,6 +9730,51 @@ func buildThumbView(state *appState) fyne.CanvasObject {
})
viewQueueBtn.Importance = widget.MediumImportance
// View Results button - shows output folder if it exists
viewResultsBtn := widget.NewButton("View Results", func() {
if state.thumbFile == nil {
dialog.ShowInformation("No Video", "Load a video first to locate results.", state.window)
return
}
videoDir := filepath.Dir(state.thumbFile.Path)
videoBaseName := strings.TrimSuffix(filepath.Base(state.thumbFile.Path), filepath.Ext(state.thumbFile.Path))
outputDir := filepath.Join(videoDir, fmt.Sprintf("%s_thumbnails", videoBaseName))
// Check if output exists
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
dialog.ShowInformation("No Results", "No generated thumbnails found. Generate thumbnails first.", state.window)
return
}
// If contact sheet mode, try to show the contact sheet image
if state.thumbContactSheet {
contactSheetPath := filepath.Join(outputDir, "contact_sheet.jpg")
if _, err := os.Stat(contactSheetPath); err == nil {
// Show contact sheet in a dialog
go func() {
img := canvas.NewImageFromFile(contactSheetPath)
img.FillMode = canvas.ImageFillContain
img.SetMinSize(fyne.NewSize(800, 600))
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
d := dialog.NewCustom("Contact Sheet", "Close", img, state.window)
d.Resize(fyne.NewSize(900, 700))
d.Show()
}, false)
}()
return
}
}
// Otherwise, open folder
openFolder(outputDir)
})
viewResultsBtn.Importance = widget.MediumImportance
if state.thumbFile == nil {
viewResultsBtn.Disable()
}
// Settings panel
settingsPanel := container.NewVBox(
widget.NewLabel("Settings:"),
@ -9738,6 +9784,7 @@ func buildThumbView(state *appState) fyne.CanvasObject {
widget.NewSeparator(),
generateBtn,
viewQueueBtn,
viewResultsBtn,
)
// Main content - split layout with preview on left, settings on right