Compare commits

...

4 Commits

Author SHA1 Message Date
50237f741a Add Generate Now and Add to Queue buttons
Changed thumbnail module to match convert module behavior with two
action buttons:

GENERATE NOW (High Importance):
- Adds job to queue and starts it immediately
- Runs right away if queue is idle
- Queues for later if jobs are running
- Shows "Thumbnail generation started!" message

Add to Queue (Medium Importance):
- Adds job to queue without starting
- Allows queuing multiple jobs
- Shows "Thumbnail job added to queue!" message

Implementation:
- Refactored job creation into createThumbJob() helper function
- Both buttons use same job creation logic
- Generate Now auto-starts queue if not running
- Follows same pattern as convert module

Benefits:
- Immediate generation when queue is idle
- Queue multiple jobs without starting
- Consistent UX with convert module
- Clear user feedback on action taken

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 21:00:43 -05:00
56141be0d4 Disable timestamp overlay to fix exit 234 error
Fixed the exit 234 error when generating individual thumbnails by
disabling the timestamp overlay feature which was causing FFmpeg
font-related failures on some systems.

Changes:
- ShowTimestamp: false (was true)
- ShowMetadata: only true for contact sheets (was always true)

The timestamp overlay was causing issues because:
1. DejaVu Sans Mono font might not be available on all systems
2. FFmpeg exits with code 234 when drawtext filter fails
3. Individual thumbnails don't need timestamp overlays anyway

Contact sheets still get metadata headers, which is the main use case
for showing video information on thumbnails.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:58:36 -05:00
f1d445dd0a 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>
2025-12-13 20:56:05 -05:00
d6fd5fc762 Integrate thumbnails with job queue system
Added full job queue integration for thumbnail generation:

Job Queue Integration:
- Implemented executeThumbJob() to handle thumbnail generation in queue
- Changed "Generate Thumbnails" to "Add to Queue" button
- Added "View Queue" button to thumbnail module
- Removed direct generation code in favor of queue system

Progress Tracking:
- Jobs now show in queue with progress bar
- Contact sheet mode: shows grid dimensions in description
- Individual mode: shows count and width in description
- Job title: "Thumbnails: {filename}"

Benefits:
- Real-time progress tracking via queue progress bar
- Can queue multiple thumbnail jobs
- Access queue from thumbnail screen
- Consistent with other modules (convert, merge, snippet)
- Background processing without blocking UI

The thumbnail module now uses the same job queue system as other
modules, providing progress tracking and background processing.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:49:59 -05:00
2 changed files with 191 additions and 91 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,

269
main.go
View File

@ -617,13 +617,13 @@ type appState struct {
mergeChapters bool
// Thumbnail module state
thumbFile *videoSource
thumbCount int
thumbWidth int
thumbContactSheet bool
thumbColumns int
thumbRows int
thumbGenerating bool
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
@ -2339,7 +2339,7 @@ func (s *appState) jobExecutor(ctx context.Context, job *queue.Job, progressCall
case queue.JobTypeAudio:
return fmt.Errorf("audio jobs not yet implemented")
case queue.JobTypeThumb:
return fmt.Errorf("thumb jobs not yet implemented")
return s.executeThumbJob(ctx, job, progressCallback)
case queue.JobTypeSnippet:
return s.executeSnippetJob(ctx, job, progressCallback)
default:
@ -3226,6 +3226,49 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
return nil
}
func (s *appState) executeThumbJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
cfg := job.Config
inputPath := cfg["inputPath"].(string)
outputDir := cfg["outputDir"].(string)
count := int(cfg["count"].(float64))
width := int(cfg["width"].(float64))
contactSheet := cfg["contactSheet"].(bool)
columns := int(cfg["columns"].(float64))
rows := int(cfg["rows"].(float64))
if progressCallback != nil {
progressCallback(0)
}
generator := thumbnail.NewGenerator(platformConfig.FFmpegPath)
config := thumbnail.Config{
VideoPath: inputPath,
OutputDir: outputDir,
Count: count,
Width: width,
Format: "jpg",
Quality: 85,
ContactSheet: contactSheet,
Columns: columns,
Rows: rows,
ShowTimestamp: false, // Disabled to avoid font issues
ShowMetadata: contactSheet,
}
result, err := generator.Generate(ctx, config)
if err != nil {
return fmt.Errorf("thumbnail generation failed: %w", err)
}
logging.Debug(logging.CatSystem, "generated %d thumbnails", len(result.Thumbnails))
if progressCallback != nil {
progressCallback(1)
}
return nil
}
func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
cfg := job.Config
inputPath := cfg["inputPath"].(string)
@ -9619,96 +9662,147 @@ func buildThumbView(state *appState) fyne.CanvasObject {
)
}
// Generate button
generateBtn := widget.NewButton("Generate Thumbnails", func() {
// Helper function to create thumbnail job
createThumbJob := func() *queue.Job {
// Create output directory in same folder as video
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))
// Configure based on mode
var count, width int
var description string
if state.thumbContactSheet {
// Contact sheet: count is determined by grid, use smaller width to fit window
count = state.thumbColumns * state.thumbRows
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
count = state.thumbCount
width = state.thumbWidth
description = fmt.Sprintf("%d individual thumbnails (%dpx width)", count, width)
}
return &queue.Job{
Type: queue.JobTypeThumb,
Title: "Thumbnails: " + filepath.Base(state.thumbFile.Path),
Description: description,
InputFile: state.thumbFile.Path,
OutputFile: outputDir,
Config: map[string]interface{}{
"inputPath": state.thumbFile.Path,
"outputDir": outputDir,
"count": float64(count),
"width": float64(width),
"contactSheet": state.thumbContactSheet,
"columns": float64(state.thumbColumns),
"rows": float64(state.thumbRows),
},
}
}
// Generate Now button - adds to queue and starts it
generateNowBtn := widget.NewButton("GENERATE NOW", func() {
if state.thumbFile == nil {
dialog.ShowInformation("No Video", "Please load a video file first.", state.window)
return
}
state.thumbGenerating = true
state.showThumbView()
if state.jobQueue == nil {
dialog.ShowInformation("Queue", "Queue not initialized.", state.window)
return
}
go func() {
// Create output directory in same folder as video
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))
job := createThumbJob()
state.jobQueue.Add(job)
generator := thumbnail.NewGenerator(platformConfig.FFmpegPath)
// Start the queue if not already running
if !state.jobQueue.IsRunning() {
state.jobQueue.Start()
logging.Debug(logging.CatSystem, "started queue from Generate Now")
}
// Configure based on mode
var count, width int
if state.thumbContactSheet {
// Contact sheet: count is determined by grid, use default width
count = state.thumbColumns * state.thumbRows
width = 320 // Fixed width for contact sheets
} else {
// Individual thumbnails: use user settings
count = state.thumbCount
width = state.thumbWidth
}
config := thumbnail.Config{
VideoPath: state.thumbFile.Path,
OutputDir: outputDir,
Count: count,
Width: width,
Format: "jpg",
Quality: 85,
ContactSheet: state.thumbContactSheet,
Columns: state.thumbColumns,
Rows: state.thumbRows,
ShowTimestamp: true,
ShowMetadata: true,
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
result, err := generator.Generate(ctx, config)
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
state.thumbGenerating = false
if err != nil {
logging.Debug(logging.CatSystem, "thumbnail generation failed: %v", err)
dialog.ShowError(fmt.Errorf("Thumbnail generation failed: %w", err), state.window)
state.showThumbView()
return
}
logging.Debug(logging.CatSystem, "generated %d thumbnails", len(result.Thumbnails))
// Show success dialog with option to open folder
confirmDialog := dialog.NewConfirm(
"Thumbnails Generated",
fmt.Sprintf("Successfully generated %d thumbnail(s) at:\n%s\n\nOpen folder?",
len(result.Thumbnails), outputDir),
func(open bool) {
if open {
openFolder(outputDir)
}
},
state.window,
)
confirmDialog.SetConfirmText("Open Folder")
confirmDialog.SetDismissText("Close")
confirmDialog.Show()
state.showThumbView()
}, false)
}()
dialog.ShowInformation("Thumbnails", "Thumbnail generation started! View progress in Job Queue.", state.window)
})
generateBtn.Importance = widget.HighImportance
generateNowBtn.Importance = widget.HighImportance
if state.thumbFile == nil {
generateBtn.Disable()
generateNowBtn.Disable()
}
if state.thumbGenerating {
generateBtn.SetText("Generating...")
generateBtn.Disable()
// Add to Queue button
addQueueBtn := widget.NewButton("Add to Queue", func() {
if state.thumbFile == nil {
dialog.ShowInformation("No Video", "Please load a video file first.", state.window)
return
}
if state.jobQueue == nil {
dialog.ShowInformation("Queue", "Queue not initialized.", state.window)
return
}
job := createThumbJob()
state.jobQueue.Add(job)
dialog.ShowInformation("Queue", "Thumbnail job added to queue!", state.window)
})
addQueueBtn.Importance = widget.MediumImportance
if state.thumbFile == nil {
addQueueBtn.Disable()
}
// View Queue button
viewQueueBtn := widget.NewButton("View Queue", func() {
state.showQueue()
})
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
@ -9718,7 +9812,10 @@ func buildThumbView(state *appState) fyne.CanvasObject {
contactSheetCheck,
settingsOptions,
widget.NewSeparator(),
generateBtn,
generateNowBtn,
addQueueBtn,
viewQueueBtn,
viewResultsBtn,
)
// Main content - split layout with preview on left, settings on right