From b02cd844c4bd2302af8e8739eb19d8e1cd5ecb49 Mon Sep 17 00:00:00 2001 From: Stu Leak Date: Tue, 23 Dec 2025 22:05:54 -0500 Subject: [PATCH] Finish thumb module extraction fixes --- main.go | 382 ------------------------------------------------ thumb_module.go | 2 +- 2 files changed, 1 insertion(+), 383 deletions(-) diff --git a/main.go b/main.go index 56e95ba..12a2108 100644 --- a/main.go +++ b/main.go @@ -44,7 +44,6 @@ import ( "git.leaktechnologies.dev/stu/VideoTools/internal/player" "git.leaktechnologies.dev/stu/VideoTools/internal/queue" "git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo" - "git.leaktechnologies.dev/stu/VideoTools/internal/thumbnail" "git.leaktechnologies.dev/stu/VideoTools/internal/ui" "git.leaktechnologies.dev/stu/VideoTools/internal/utils" "github.com/hajimehoshi/oto" @@ -4277,49 +4276,6 @@ 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) @@ -9275,7 +9231,6 @@ func (p *playSession) runVideo(offset float64) { if p.fps > 0 { frameDur = time.Duration(float64(time.Second) / math.Max(p.fps, 0.1)) } - nextFrameAt := time.Now() p.videoCmd = cmd frameSize := p.targetW * p.targetH * 3 buf := make([]byte, frameSize) @@ -9290,7 +9245,6 @@ func (p *playSession) runVideo(offset float64) { } if p.paused { time.Sleep(30 * time.Millisecond) - nextFrameAt = time.Now().Add(frameDur) continue } _, err := io.ReadFull(stdout, buf) @@ -12729,342 +12683,6 @@ func buildCompareView(state *appState) fyne.CanvasObject { return container.NewBorder(topBar, bottomBar, nil, nil, content) } -// buildThumbView creates the thumbnail generation UI -func buildThumbView(state *appState) fyne.CanvasObject { - thumbColor := moduleColor("thumb") - - // Back button - backBtn := widget.NewButton("< THUMBNAILS", func() { - state.showMainMenu() - }) - backBtn.Importance = widget.LowImportance - - // Top bar with module color - queueBtn := widget.NewButton("View Queue", func() { - state.showQueue() - }) - state.queueBtn = queueBtn - state.updateQueueButtonLabel() - topBar := ui.TintedBar(thumbColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn)) - - // Instructions - instructions := widget.NewLabel("Generate thumbnails from a video file. Load a video and configure settings.") - instructions.Wrapping = fyne.TextWrapWord - instructions.Alignment = fyne.TextAlignCenter - - // Initialize state defaults - if state.thumbCount == 0 { - state.thumbCount = 24 // Default to 24 thumbnails (good for contact sheets) - } - if state.thumbWidth == 0 { - state.thumbWidth = 320 - } - if state.thumbColumns == 0 { - state.thumbColumns = 4 // 4 columns works well for widescreen videos - } - if state.thumbRows == 0 { - state.thumbRows = 6 // 4x6 = 24 thumbnails - } - - // 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(480, 270), state.thumbFile, nil) - } else { - videoContainer = container.NewCenter(widget.NewLabel("No video loaded")) - } - - // Load button - loadBtn := widget.NewButton("Load Video", func() { - dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) { - if err != nil || reader == nil { - return - } - path := reader.URI().Path() - reader.Close() - - src, err := probeVideo(path) - if err != nil { - dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window) - return - } - - state.thumbFile = src - state.showThumbView() - logging.Debug(logging.CatModule, "loaded thumbnail file: %s", path) - }, state.window) - }) - - // Clear button - clearBtn := widget.NewButton("Clear", func() { - state.thumbFile = nil - state.showThumbView() - }) - clearBtn.Importance = widget.LowImportance - - // Contact sheet checkbox - contactSheetCheck := widget.NewCheck("Generate Contact Sheet (single image)", func(checked bool) { - state.thumbContactSheet = checked - state.showThumbView() - }) - contactSheetCheck.Checked = state.thumbContactSheet - - // 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)) - rowLabel := widget.NewLabel(fmt.Sprintf("Rows: %d", state.thumbRows)) - - totalThumbs := state.thumbColumns * state.thumbRows - totalLabel := widget.NewLabel(fmt.Sprintf("Total thumbnails: %d", totalThumbs)) - totalLabel.TextStyle = fyne.TextStyle{Italic: true} - - colSlider := widget.NewSlider(2, 12) - colSlider.Value = float64(state.thumbColumns) - colSlider.Step = 1 - colSlider.OnChanged = func(val float64) { - state.thumbColumns = int(val) - colLabel.SetText(fmt.Sprintf("Columns: %d", int(val))) - totalLabel.SetText(fmt.Sprintf("Total thumbnails: %d", state.thumbColumns*state.thumbRows)) - } - - rowSlider := widget.NewSlider(2, 12) - rowSlider.Value = float64(state.thumbRows) - rowSlider.Step = 1 - rowSlider.OnChanged = func(val float64) { - state.thumbRows = int(val) - rowLabel.SetText(fmt.Sprintf("Rows: %d", int(val))) - totalLabel.SetText(fmt.Sprintf("Total thumbnails: %d", state.thumbColumns*state.thumbRows)) - } - - settingsOptions = container.NewVBox( - widget.NewSeparator(), - widget.NewLabel("Contact Sheet Grid:"), - colLabel, - colSlider, - rowLabel, - rowSlider, - totalLabel, - ) - } else { - // 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, - ) - } - - // 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 larger width for analyzable screenshots - count = state.thumbColumns * state.thumbRows - width = 280 // Larger width for contact sheets to make screenshots analyzable (4x8 grid = ~1144x1416) - 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 - } - - if state.jobQueue == nil { - dialog.ShowInformation("Queue", "Queue not initialized.", state.window) - return - } - - job := createThumbJob() - state.jobQueue.Add(job) - - // Start the queue if not already running - if !state.jobQueue.IsRunning() { - state.jobQueue.Start() - logging.Debug(logging.CatSystem, "started queue from Generate Now") - } - - dialog.ShowInformation("Thumbnails", "Thumbnail generation started! View progress in Job Queue.", state.window) - }) - generateNowBtn.Importance = widget.HighImportance - - if state.thumbFile == nil { - generateNowBtn.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 - // Adaptive size for small screens - use scrollable dialog - img.SetMinSize(fyne.NewSize(640, 480)) - - fyne.CurrentApp().Driver().DoFromGoroutine(func() { - // Wrap in scroll container for large contact sheets - scroll := container.NewScroll(img) - d := dialog.NewCustom("Contact Sheet", "Close", scroll, state.window) - // Adaptive dialog size that fits on 1280x768 screens - d.Resize(fyne.NewSize(700, 600)) - 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:"), - widget.NewSeparator(), - contactSheetCheck, - settingsOptions, - widget.NewSeparator(), - generateNowBtn, - addQueueBtn, - viewQueueBtn, - viewResultsBtn, - ) - - // Main content - split layout with preview on left, settings on right - leftColumn := container.NewVBox( - videoContainer, - ) - - rightColumn := container.NewVBox( - settingsPanel, - ) - - mainContent := container.New(&fixedHSplitLayout{ratio: 0.6}, leftColumn, rightColumn) - - content := container.NewBorder( - container.NewVBox(instructions, widget.NewSeparator(), fileLabel, container.NewHBox(loadBtn, clearBtn)), - nil, - nil, - nil, - mainContent, - ) - - bottomBar := moduleFooter(thumbColor, layout.NewSpacer(), state.statsBar) - - return container.NewBorder(topBar, bottomBar, nil, nil, content) -} - // buildPlayerView creates the VT_Player UI func buildPlayerView(state *appState) fyne.CanvasObject { playerColor := moduleColor("player") diff --git a/thumb_module.go b/thumb_module.go index c4cedba..afce2b4 100644 --- a/thumb_module.go +++ b/thumb_module.go @@ -5,7 +5,7 @@ import ( "fmt" "os" "path/filepath" - "strconv" + "strings" "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas"