Finish thumb module extraction fixes
This commit is contained in:
parent
f5a162b440
commit
b02cd844c4
382
main.go
382
main.go
|
|
@ -44,7 +44,6 @@ import (
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/player"
|
"git.leaktechnologies.dev/stu/VideoTools/internal/player"
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
|
"git.leaktechnologies.dev/stu/VideoTools/internal/queue"
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/sysinfo"
|
"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/ui"
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
||||||
"github.com/hajimehoshi/oto"
|
"github.com/hajimehoshi/oto"
|
||||||
|
|
@ -4277,49 +4276,6 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
||||||
return nil
|
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 {
|
func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progressCallback func(float64)) error {
|
||||||
cfg := job.Config
|
cfg := job.Config
|
||||||
inputPath := cfg["inputPath"].(string)
|
inputPath := cfg["inputPath"].(string)
|
||||||
|
|
@ -9275,7 +9231,6 @@ func (p *playSession) runVideo(offset float64) {
|
||||||
if p.fps > 0 {
|
if p.fps > 0 {
|
||||||
frameDur = time.Duration(float64(time.Second) / math.Max(p.fps, 0.1))
|
frameDur = time.Duration(float64(time.Second) / math.Max(p.fps, 0.1))
|
||||||
}
|
}
|
||||||
nextFrameAt := time.Now()
|
|
||||||
p.videoCmd = cmd
|
p.videoCmd = cmd
|
||||||
frameSize := p.targetW * p.targetH * 3
|
frameSize := p.targetW * p.targetH * 3
|
||||||
buf := make([]byte, frameSize)
|
buf := make([]byte, frameSize)
|
||||||
|
|
@ -9290,7 +9245,6 @@ func (p *playSession) runVideo(offset float64) {
|
||||||
}
|
}
|
||||||
if p.paused {
|
if p.paused {
|
||||||
time.Sleep(30 * time.Millisecond)
|
time.Sleep(30 * time.Millisecond)
|
||||||
nextFrameAt = time.Now().Add(frameDur)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
_, err := io.ReadFull(stdout, buf)
|
_, err := io.ReadFull(stdout, buf)
|
||||||
|
|
@ -12729,342 +12683,6 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
||||||
return container.NewBorder(topBar, bottomBar, nil, nil, content)
|
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
|
// buildPlayerView creates the VT_Player UI
|
||||||
func buildPlayerView(state *appState) fyne.CanvasObject {
|
func buildPlayerView(state *appState) fyne.CanvasObject {
|
||||||
playerColor := moduleColor("player")
|
playerColor := moduleColor("player")
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strings"
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/canvas"
|
"fyne.io/fyne/v2/canvas"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user