Refactor Compare module with auto-loading and thumbnails
Major improvements to Compare module user experience: - Auto-populate metadata when files are loaded (no Compare button needed) - Show video thumbnails for both files (320x180) - Support drag-and-drop onto Compare tile from main menu - Load up to 2 videos when dropped on Compare tile - Show dialog if more than 2 videos dropped - Files loaded via drag show immediately with metadata Changes to handleModuleDrop: - Added special handling for Compare module - Loads videos into compareFile1 and compareFile2 state - Shows module with files already populated Changes to buildCompareView: - Added thumbnail display with dark background placeholders - Created helper functions: formatMetadata(), loadThumbnail(), updateFile1(), updateFile2() - Initialize view with any preloaded files - Removed manual Compare button - metadata shows automatically - Button handlers now call update functions to refresh display - Cleaner, more intuitive workflow This addresses the user feedback that dragging videos onto Compare didn't load the module, and adds the requested thumbnail previews.
This commit is contained in:
parent
1e49fd2f05
commit
6990f18829
372
main.go
372
main.go
|
|
@ -703,6 +703,49 @@ func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If compare module, load up to 2 videos into compare slots
|
||||||
|
if moduleID == "compare" {
|
||||||
|
go func() {
|
||||||
|
// Load first video
|
||||||
|
src1, err := probeVideo(videoPaths[0])
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatModule, "failed to load first video for compare: %v", err)
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), s.window)
|
||||||
|
}, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load second video if available
|
||||||
|
var src2 *videoSource
|
||||||
|
if len(videoPaths) >= 2 {
|
||||||
|
src2, err = probeVideo(videoPaths[1])
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatModule, "failed to load second video for compare: %v", err)
|
||||||
|
// Continue with just first video
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show dialog if more than 2 videos
|
||||||
|
if len(videoPaths) > 2 {
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
dialog.ShowInformation("Compare Videos",
|
||||||
|
fmt.Sprintf("You dropped %d videos. Only the first two will be loaded for comparison.", len(videoPaths)),
|
||||||
|
s.window)
|
||||||
|
}, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update state and show module
|
||||||
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
s.compareFile1 = src1
|
||||||
|
s.compareFile2 = src2
|
||||||
|
s.showModule(moduleID)
|
||||||
|
logging.Debug(logging.CatModule, "loaded %d video(s) for compare module", len(videoPaths))
|
||||||
|
}, false)
|
||||||
|
}()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Single file or non-convert module: load first video and show module
|
// Single file or non-convert module: load first video and show module
|
||||||
path := videoPaths[0]
|
path := videoPaths[0]
|
||||||
logging.Debug(logging.CatModule, "drop on module %s path=%s - starting load", moduleID, path)
|
logging.Debug(logging.CatModule, "drop on module %s path=%s - starting load", moduleID, path)
|
||||||
|
|
@ -5297,206 +5340,193 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
||||||
topBar := ui.TintedBar(compareColor, container.NewHBox(backBtn, layout.NewSpacer()))
|
topBar := ui.TintedBar(compareColor, container.NewHBox(backBtn, layout.NewSpacer()))
|
||||||
|
|
||||||
// Instructions
|
// Instructions
|
||||||
instructions := widget.NewLabel("Load two videos to compare their metadata and visual differences side by side. Drag videos here or use buttons below.")
|
instructions := widget.NewLabel("Load two videos to compare their metadata side by side. Drag videos here or use buttons below.")
|
||||||
instructions.Wrapping = fyne.TextWrapWord
|
instructions.Wrapping = fyne.TextWrapWord
|
||||||
instructions.Alignment = fyne.TextAlignCenter
|
instructions.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
// File labels (declare early for use in helper)
|
// File labels
|
||||||
file1Label := widget.NewLabel("File 1: Not loaded")
|
file1Label := widget.NewLabel("File 1: Not loaded")
|
||||||
file1Label.TextStyle = fyne.TextStyle{Bold: true}
|
file1Label.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
|
||||||
file2Label := widget.NewLabel("File 2: Not loaded")
|
file2Label := widget.NewLabel("File 2: Not loaded")
|
||||||
file2Label.TextStyle = fyne.TextStyle{Bold: true}
|
file2Label.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
|
||||||
file1SelectBtn := widget.NewButton("Load File 1", func() {
|
// Thumbnail images
|
||||||
// File picker for first file
|
file1Thumbnail := canvas.NewImageFromImage(nil)
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
file1Thumbnail.FillMode = canvas.ImageFillContain
|
||||||
if err != nil || reader == nil {
|
file1Thumbnail.SetMinSize(fyne.NewSize(320, 180))
|
||||||
return
|
|
||||||
}
|
|
||||||
path := reader.URI().Path()
|
|
||||||
reader.Close()
|
|
||||||
|
|
||||||
// Probe the video
|
file2Thumbnail := canvas.NewImageFromImage(nil)
|
||||||
src, err := probeVideo(path)
|
file2Thumbnail.FillMode = canvas.ImageFillContain
|
||||||
if err != nil {
|
file2Thumbnail.SetMinSize(fyne.NewSize(320, 180))
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file1Label.SetText(fmt.Sprintf("File 1: %s", filepath.Base(path)))
|
// Placeholder backgrounds
|
||||||
state.compareFile1 = src
|
file1ThumbBg := canvas.NewRectangle(utils.MustHex("#0F1529"))
|
||||||
logging.Debug(logging.CatModule, "loaded compare file 1: %s", path)
|
file1ThumbBg.SetMinSize(fyne.NewSize(320, 180))
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
file2ThumbBg := canvas.NewRectangle(utils.MustHex("#0F1529"))
|
||||||
|
file2ThumbBg.SetMinSize(fyne.NewSize(320, 180))
|
||||||
|
|
||||||
|
// Info labels
|
||||||
file1Info := widget.NewLabel("No file loaded")
|
file1Info := widget.NewLabel("No file loaded")
|
||||||
file1Info.Wrapping = fyne.TextWrapWord
|
file1Info.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
// File 2 button (label already declared earlier)
|
|
||||||
file2SelectBtn := widget.NewButton("Load File 2", func() {
|
|
||||||
// File picker for second file
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
path := reader.URI().Path()
|
|
||||||
reader.Close()
|
|
||||||
|
|
||||||
// Probe the video
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
file2Label.SetText(fmt.Sprintf("File 2: %s", filepath.Base(path)))
|
|
||||||
state.compareFile2 = src
|
|
||||||
logging.Debug(logging.CatModule, "loaded compare file 2: %s", path)
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
|
|
||||||
file2Info := widget.NewLabel("No file loaded")
|
file2Info := widget.NewLabel("No file loaded")
|
||||||
file2Info.Wrapping = fyne.TextWrapWord
|
file2Info.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
// Compare button
|
// Helper function to format metadata
|
||||||
compareBtn := widget.NewButton("COMPARE", func() {
|
formatMetadata := func(src *videoSource) string {
|
||||||
if state.compareFile1 == nil || state.compareFile2 == nil {
|
fileSize := "Unknown"
|
||||||
dialog.ShowInformation("Compare Videos", "Please load both files first.", state.window)
|
if fi, err := os.Stat(src.Path); err == nil {
|
||||||
|
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
||||||
|
if sizeMB >= 1024 {
|
||||||
|
fileSize = fmt.Sprintf("%.2f GB", sizeMB/1024)
|
||||||
|
} else {
|
||||||
|
fileSize = fmt.Sprintf("%.2f MB", sizeMB)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"━━━ FILE INFO ━━━\n"+
|
||||||
|
"Path: %s\n"+
|
||||||
|
"File Size: %s\n"+
|
||||||
|
"Format: %s\n"+
|
||||||
|
"\n━━━ VIDEO ━━━\n"+
|
||||||
|
"Codec: %s\n"+
|
||||||
|
"Resolution: %dx%d\n"+
|
||||||
|
"Aspect Ratio: %s\n"+
|
||||||
|
"Frame Rate: %.2f fps\n"+
|
||||||
|
"Bitrate: %s\n"+
|
||||||
|
"Pixel Format: %s\n"+
|
||||||
|
"Color Space: %s\n"+
|
||||||
|
"Color Range: %s\n"+
|
||||||
|
"Field Order: %s\n"+
|
||||||
|
"GOP Size: %d\n"+
|
||||||
|
"\n━━━ AUDIO ━━━\n"+
|
||||||
|
"Codec: %s\n"+
|
||||||
|
"Bitrate: %s\n"+
|
||||||
|
"Sample Rate: %d Hz\n"+
|
||||||
|
"Channels: %d\n"+
|
||||||
|
"\n━━━ OTHER ━━━\n"+
|
||||||
|
"Duration: %s\n"+
|
||||||
|
"SAR (Pixel Aspect): %s\n"+
|
||||||
|
"Chapters: %v\n"+
|
||||||
|
"Metadata: %v",
|
||||||
|
filepath.Base(src.Path),
|
||||||
|
fileSize,
|
||||||
|
src.Format,
|
||||||
|
src.VideoCodec,
|
||||||
|
src.Width, src.Height,
|
||||||
|
src.AspectRatioString(),
|
||||||
|
src.FrameRate,
|
||||||
|
formatBitrate(src.Bitrate),
|
||||||
|
src.PixelFormat,
|
||||||
|
src.ColorSpace,
|
||||||
|
src.ColorRange,
|
||||||
|
src.FieldOrder,
|
||||||
|
src.GOPSize,
|
||||||
|
src.AudioCodec,
|
||||||
|
formatBitrate(src.AudioBitrate),
|
||||||
|
src.AudioRate,
|
||||||
|
src.Channels,
|
||||||
|
src.DurationString(),
|
||||||
|
src.SampleAspectRatio,
|
||||||
|
src.HasChapters,
|
||||||
|
src.HasMetadata,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to load thumbnail for a video
|
||||||
|
loadThumbnail := func(src *videoSource, img *canvas.Image) {
|
||||||
|
if src == nil || len(src.PreviewFrames) == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Load the first preview frame as thumbnail
|
||||||
// Build comparison data
|
thumbImg := canvas.NewImageFromFile(src.PreviewFrames[0])
|
||||||
f1 := state.compareFile1
|
if thumbImg.Image != nil {
|
||||||
f2 := state.compareFile2
|
img.Image = thumbImg.Image
|
||||||
|
img.Refresh()
|
||||||
// Calculate file size
|
|
||||||
file1Size := "Unknown"
|
|
||||||
if fi, err := os.Stat(f1.Path); err == nil {
|
|
||||||
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
|
||||||
if sizeMB >= 1024 {
|
|
||||||
file1Size = fmt.Sprintf("%.2f GB", sizeMB/1024)
|
|
||||||
} else {
|
|
||||||
file1Size = fmt.Sprintf("%.2f MB", sizeMB)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
file1Info.SetText(fmt.Sprintf(
|
// Helper to update file display
|
||||||
"━━━ FILE INFO ━━━\n"+
|
updateFile1 := func() {
|
||||||
"Path: %s\n"+
|
if state.compareFile1 != nil {
|
||||||
"File Size: %s\n"+
|
file1Label.SetText(fmt.Sprintf("File 1: %s", filepath.Base(state.compareFile1.Path)))
|
||||||
"Format: %s\n"+
|
file1Info.SetText(formatMetadata(state.compareFile1))
|
||||||
"\n━━━ VIDEO ━━━\n"+
|
loadThumbnail(state.compareFile1, file1Thumbnail)
|
||||||
"Codec: %s\n"+
|
} else {
|
||||||
"Resolution: %dx%d\n"+
|
file1Label.SetText("File 1: Not loaded")
|
||||||
"Aspect Ratio: %s\n"+
|
file1Info.SetText("No file loaded")
|
||||||
"Frame Rate: %.2f fps\n"+
|
file1Thumbnail.Image = nil
|
||||||
"Bitrate: %s\n"+
|
file1Thumbnail.Refresh()
|
||||||
"Pixel Format: %s\n"+
|
|
||||||
"Color Space: %s\n"+
|
|
||||||
"Color Range: %s\n"+
|
|
||||||
"Field Order: %s\n"+
|
|
||||||
"GOP Size: %d\n"+
|
|
||||||
"\n━━━ AUDIO ━━━\n"+
|
|
||||||
"Codec: %s\n"+
|
|
||||||
"Bitrate: %s\n"+
|
|
||||||
"Sample Rate: %d Hz\n"+
|
|
||||||
"Channels: %d\n"+
|
|
||||||
"\n━━━ OTHER ━━━\n"+
|
|
||||||
"Duration: %s\n"+
|
|
||||||
"SAR (Pixel Aspect): %s\n"+
|
|
||||||
"Chapters: %v\n"+
|
|
||||||
"Metadata: %v",
|
|
||||||
filepath.Base(f1.Path),
|
|
||||||
file1Size,
|
|
||||||
f1.Format,
|
|
||||||
f1.VideoCodec,
|
|
||||||
f1.Width, f1.Height,
|
|
||||||
f1.AspectRatioString(),
|
|
||||||
f1.FrameRate,
|
|
||||||
formatBitrate(f1.Bitrate),
|
|
||||||
f1.PixelFormat,
|
|
||||||
f1.ColorSpace,
|
|
||||||
f1.ColorRange,
|
|
||||||
f1.FieldOrder,
|
|
||||||
f1.GOPSize,
|
|
||||||
f1.AudioCodec,
|
|
||||||
formatBitrate(f1.AudioBitrate),
|
|
||||||
f1.AudioRate,
|
|
||||||
f1.Channels,
|
|
||||||
f1.DurationString(),
|
|
||||||
f1.SampleAspectRatio,
|
|
||||||
f1.HasChapters,
|
|
||||||
f1.HasMetadata,
|
|
||||||
))
|
|
||||||
|
|
||||||
// Calculate file size
|
|
||||||
file2Size := "Unknown"
|
|
||||||
if fi, err := os.Stat(f2.Path); err == nil {
|
|
||||||
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
|
||||||
if sizeMB >= 1024 {
|
|
||||||
file2Size = fmt.Sprintf("%.2f GB", sizeMB/1024)
|
|
||||||
} else {
|
|
||||||
file2Size = fmt.Sprintf("%.2f MB", sizeMB)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
file2Info.SetText(fmt.Sprintf(
|
updateFile2 := func() {
|
||||||
"━━━ FILE INFO ━━━\n"+
|
if state.compareFile2 != nil {
|
||||||
"Path: %s\n"+
|
file2Label.SetText(fmt.Sprintf("File 2: %s", filepath.Base(state.compareFile2.Path)))
|
||||||
"File Size: %s\n"+
|
file2Info.SetText(formatMetadata(state.compareFile2))
|
||||||
"Format: %s\n"+
|
loadThumbnail(state.compareFile2, file2Thumbnail)
|
||||||
"\n━━━ VIDEO ━━━\n"+
|
} else {
|
||||||
"Codec: %s\n"+
|
file2Label.SetText("File 2: Not loaded")
|
||||||
"Resolution: %dx%d\n"+
|
file2Info.SetText("No file loaded")
|
||||||
"Aspect Ratio: %s\n"+
|
file2Thumbnail.Image = nil
|
||||||
"Frame Rate: %.2f fps\n"+
|
file2Thumbnail.Refresh()
|
||||||
"Bitrate: %s\n"+
|
}
|
||||||
"Pixel Format: %s\n"+
|
}
|
||||||
"Color Space: %s\n"+
|
|
||||||
"Color Range: %s\n"+
|
// Initialize with any already-loaded files
|
||||||
"Field Order: %s\n"+
|
updateFile1()
|
||||||
"GOP Size: %d\n"+
|
updateFile2()
|
||||||
"\n━━━ AUDIO ━━━\n"+
|
|
||||||
"Codec: %s\n"+
|
file1SelectBtn := widget.NewButton("Load File 1", func() {
|
||||||
"Bitrate: %s\n"+
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
"Sample Rate: %d Hz\n"+
|
if err != nil || reader == nil {
|
||||||
"Channels: %d\n"+
|
return
|
||||||
"\n━━━ OTHER ━━━\n"+
|
}
|
||||||
"Duration: %s\n"+
|
path := reader.URI().Path()
|
||||||
"SAR (Pixel Aspect): %s\n"+
|
reader.Close()
|
||||||
"Chapters: %v\n"+
|
|
||||||
"Metadata: %v",
|
src, err := probeVideo(path)
|
||||||
filepath.Base(f2.Path),
|
if err != nil {
|
||||||
file2Size,
|
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
|
||||||
f2.Format,
|
return
|
||||||
f2.VideoCodec,
|
}
|
||||||
f2.Width, f2.Height,
|
|
||||||
f2.AspectRatioString(),
|
state.compareFile1 = src
|
||||||
f2.FrameRate,
|
updateFile1()
|
||||||
formatBitrate(f2.Bitrate),
|
logging.Debug(logging.CatModule, "loaded compare file 1: %s", path)
|
||||||
f2.PixelFormat,
|
}, state.window)
|
||||||
f2.ColorSpace,
|
})
|
||||||
f2.ColorRange,
|
|
||||||
f2.FieldOrder,
|
file2SelectBtn := widget.NewButton("Load File 2", func() {
|
||||||
f2.GOPSize,
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
f2.AudioCodec,
|
if err != nil || reader == nil {
|
||||||
formatBitrate(f2.AudioBitrate),
|
return
|
||||||
f2.AudioRate,
|
}
|
||||||
f2.Channels,
|
path := reader.URI().Path()
|
||||||
f2.DurationString(),
|
reader.Close()
|
||||||
f2.SampleAspectRatio,
|
|
||||||
f2.HasChapters,
|
src, err := probeVideo(path)
|
||||||
f2.HasMetadata,
|
if err != nil {
|
||||||
))
|
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
state.compareFile2 = src
|
||||||
|
updateFile2()
|
||||||
|
logging.Debug(logging.CatModule, "loaded compare file 2: %s", path)
|
||||||
|
}, state.window)
|
||||||
})
|
})
|
||||||
compareBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
file1Box := container.NewVBox(
|
file1Box := container.NewVBox(
|
||||||
file1Label,
|
file1Label,
|
||||||
file1SelectBtn,
|
file1SelectBtn,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
|
container.NewMax(file1ThumbBg, file1Thumbnail),
|
||||||
|
widget.NewSeparator(),
|
||||||
container.NewScroll(file1Info),
|
container.NewScroll(file1Info),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -5504,6 +5534,8 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
||||||
file2Label,
|
file2Label,
|
||||||
file2SelectBtn,
|
file2SelectBtn,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
|
container.NewMax(file2ThumbBg, file2Thumbnail),
|
||||||
|
widget.NewSeparator(),
|
||||||
container.NewScroll(file2Info),
|
container.NewScroll(file2Info),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -5514,8 +5546,6 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
||||||
content := container.NewVBox(
|
content := container.NewVBox(
|
||||||
instructions,
|
instructions,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
compareBtn,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewGridWithColumns(2,
|
container.NewGridWithColumns(2,
|
||||||
file1Box,
|
file1Box,
|
||||||
file2Box,
|
file2Box,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user