diff --git a/internal/thumbnail/generator.go b/internal/thumbnail/generator.go index 521596f..5749bca 100644 --- a/internal/thumbnail/generator.go +++ b/internal/thumbnail/generator.go @@ -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, diff --git a/main.go b/main.go index 3227034..f6b43d7 100644 --- a/main.go +++ b/main.go @@ -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