Compare commits

...

3 Commits

Author SHA1 Message Date
a40f7ad795 Fix thumbnail generation and add preview window
Fixed Issues:
- Exit 234 error: Added font parameter to drawtext filter for individual
  thumbnails (was missing, causing FFmpeg to fail)
- Output directory: Changed from temp to video's directory, creating a
  folder named "{video}_thumbnails" next to the source file

New Features:
- Added video preview window to thumbnail module (640x360)
- Split layout: preview on left (55%), settings on right (45%)
- Preview uses same buildVideoPane as other modules for consistency

The thumbnail module now has a proper preview window for reviewing
the loaded video before generating thumbnails, and outputs are saved
in a logical location next to the source file.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:40:06 -05:00
37fa9d1a5c Use monospace font for contact sheet metadata
Updated FFmpeg drawtext filter to use DejaVu Sans Mono for metadata
text on contact sheets. This matches the monospace font style used
throughout the VideoTools UI.

DejaVu Sans Mono is widely available across Linux, macOS, and Windows,
ensuring consistent appearance across platforms.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:38:11 -05:00
701e2592ee Fix thumbnail UI to show mode-appropriate controls
Refactored thumbnail generation UI to show different controls based on mode:

Individual Thumbnails Mode (contact sheet OFF):
- Shows "Thumbnail Count" slider (3-50)
- Shows "Thumbnail Width" slider (160-640px)

Contact Sheet Mode (contact sheet ON):
- Shows "Columns" slider (2-12)
- Shows "Rows" slider (2-12)
- Displays calculated total: columns × rows
- Uses fixed 320px width for optimal grid layout

Generator logic now:
- Contact sheet: count = columns × rows, width = 320px
- Individual: count and width from user sliders

This provides a clearer, more intuitive interface where users see only
the controls relevant to their selected generation mode.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 20:35:43 -05:00
2 changed files with 78 additions and 41 deletions

View File

@ -234,7 +234,7 @@ func (g *Generator) generateIndividual(ctx context.Context, config Config, durat
seconds := int(ts) % 60
timeStr := fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
drawTextFilter := fmt.Sprintf("scale=%d:%d,drawtext=text='%s':fontcolor=white:fontsize=20:box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=h-th-10",
drawTextFilter := fmt.Sprintf("scale=%d:%d,drawtext=text='%s':fontcolor=white:fontsize=20:font='DejaVu Sans Mono':box=1:boxcolor=black@0.5:boxborderw=5:x=(w-text_w)/2:y=h-th-10",
thumbWidth, thumbHeight, timeStr)
// Replace scale filter with combined filter
@ -363,12 +363,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
// 3. Draws metadata text on header
// 3. Draws metadata text on header (using monospace font)
// 4. Stacks header on top of contact sheet
filter := fmt.Sprintf(
"%s,%s,pad=%d:%d:0:%d:black,"+
"drawtext=text='%s':fontcolor=white:fontsize=14:x=10:y=10,"+
"drawtext=text='%s':fontcolor=white:fontsize=12:x=10:y=35",
"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,
tileFilter,
sheetWidth,

111
main.go
View File

@ -9504,12 +9504,16 @@ func buildThumbView(state *appState) fyne.CanvasObject {
state.thumbRows = 6 // 4x6 = 24 thumbnails
}
// File label
// File label and video preview
fileLabel := widget.NewLabel("No file loaded")
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
var videoContainer fyne.CanvasObject
if state.thumbFile != nil {
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(state.thumbFile.Path)))
videoContainer = buildVideoPane(state, fyne.NewSize(640, 360), state.thumbFile, nil)
} else {
videoContainer = container.NewCenter(widget.NewLabel("No video loaded"))
}
// Load button
@ -9540,26 +9544,6 @@ func buildThumbView(state *appState) fyne.CanvasObject {
})
clearBtn.Importance = widget.LowImportance
// Thumbnail count slider
countLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Count: %d", state.thumbCount))
countSlider := widget.NewSlider(3, 50)
countSlider.Value = float64(state.thumbCount)
countSlider.Step = 1
countSlider.OnChanged = func(val float64) {
state.thumbCount = int(val)
countLabel.SetText(fmt.Sprintf("Thumbnail Count: %d", int(val)))
}
// Thumbnail width slider
widthLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Width: %d px", state.thumbWidth))
widthSlider := widget.NewSlider(160, 640)
widthSlider.Value = float64(state.thumbWidth)
widthSlider.Step = 32
widthSlider.OnChanged = func(val float64) {
state.thumbWidth = int(val)
widthLabel.SetText(fmt.Sprintf("Thumbnail Width: %d px", int(val)))
}
// Contact sheet checkbox
contactSheetCheck := widget.NewCheck("Generate Contact Sheet (single image)", func(checked bool) {
state.thumbContactSheet = checked
@ -9567,9 +9551,10 @@ func buildThumbView(state *appState) fyne.CanvasObject {
})
contactSheetCheck.Checked = state.thumbContactSheet
// Contact sheet grid options (only show if contact sheet is enabled)
var gridOptions fyne.CanvasObject
// Conditional settings based on contact sheet mode
var settingsOptions fyne.CanvasObject
if state.thumbContactSheet {
// Contact sheet mode: show columns and rows
colLabel := widget.NewLabel(fmt.Sprintf("Columns: %d", state.thumbColumns))
colSlider := widget.NewSlider(2, 12)
colSlider.Value = float64(state.thumbColumns)
@ -9588,16 +9573,47 @@ func buildThumbView(state *appState) fyne.CanvasObject {
rowLabel.SetText(fmt.Sprintf("Rows: %d", int(val)))
}
gridOptions = container.NewVBox(
totalThumbs := state.thumbColumns * state.thumbRows
totalLabel := widget.NewLabel(fmt.Sprintf("Total thumbnails: %d", totalThumbs))
totalLabel.TextStyle = fyne.TextStyle{Italic: true}
settingsOptions = container.NewVBox(
widget.NewSeparator(),
widget.NewLabel("Contact Sheet Grid:"),
colLabel,
colSlider,
rowLabel,
rowSlider,
totalLabel,
)
} else {
gridOptions = container.NewVBox()
// Individual thumbnails mode: show count and width
countLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Count: %d", state.thumbCount))
countSlider := widget.NewSlider(3, 50)
countSlider.Value = float64(state.thumbCount)
countSlider.Step = 1
countSlider.OnChanged = func(val float64) {
state.thumbCount = int(val)
countLabel.SetText(fmt.Sprintf("Thumbnail Count: %d", int(val)))
}
widthLabel := widget.NewLabel(fmt.Sprintf("Thumbnail Width: %d px", state.thumbWidth))
widthSlider := widget.NewSlider(160, 640)
widthSlider.Value = float64(state.thumbWidth)
widthSlider.Step = 32
widthSlider.OnChanged = func(val float64) {
state.thumbWidth = int(val)
widthLabel.SetText(fmt.Sprintf("Thumbnail Width: %d px", int(val)))
}
settingsOptions = container.NewVBox(
widget.NewSeparator(),
widget.NewLabel("Individual Thumbnails:"),
countLabel,
countSlider,
widthLabel,
widthSlider,
)
}
// Generate button
@ -9611,15 +9627,30 @@ func buildThumbView(state *appState) fyne.CanvasObject {
state.showThumbView()
go func() {
// Create temp directory for thumbnails
outputDir := filepath.Join(os.TempDir(), fmt.Sprintf("videotools_thumbs_%d", time.Now().Unix()))
// 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))
generator := thumbnail.NewGenerator(platformConfig.FFmpegPath)
// 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: state.thumbCount,
Width: state.thumbWidth,
Count: count,
Width: width,
Format: "jpg",
Quality: 85,
ContactSheet: state.thumbContactSheet,
@ -9681,24 +9712,30 @@ func buildThumbView(state *appState) fyne.CanvasObject {
settingsPanel := container.NewVBox(
widget.NewLabel("Settings:"),
widget.NewSeparator(),
countLabel,
countSlider,
widthLabel,
widthSlider,
widget.NewSeparator(),
contactSheetCheck,
gridOptions,
settingsOptions,
widget.NewSeparator(),
generateBtn,
)
// Main content
// Main content - split layout with preview on left, settings on right
leftColumn := container.NewVBox(
videoContainer,
)
rightColumn := container.NewVBox(
settingsPanel,
)
mainContent := container.NewHSplit(leftColumn, rightColumn)
mainContent.Offset = 0.55 // Give more space to preview
content := container.NewBorder(
container.NewVBox(instructions, widget.NewSeparator(), fileLabel, container.NewHBox(loadBtn, clearBtn)),
nil,
nil,
nil,
settingsPanel,
mainContent,
)
return container.NewBorder(topBar, nil, nil, nil, content)