Add DVD format options to Convert module UI
Integrated DVD-NTSC and DVD-PAL options into the Convert module's Simple and Advanced modes. New Features: ✓ DVD-NTSC (720×480 @ 29.97fps) option in format selector ✓ DVD-PAL (720×576 @ 25.00fps) option in format selector ✓ DVD aspect ratio selector (4:3 or 16:9) ✓ Dynamic DVD options panel - appears only when DVD format selected ✓ Informative DVD specs displayed based on format selection ✓ Smart show/hide logic for DVD-specific controls ✓ Works in both Simple and Advanced mode tabs DVD Specifications Displayed: - NTSC: 720×480 @ 29.97fps, MPEG-2, AC-3 Stereo 48kHz - PAL: 720×576 @ 25.00fps, MPEG-2, AC-3 Stereo 48kHz - Bitrate ranges and compatibility info Users can now: 1. Select DVD format from dropdown 2. Choose aspect ratio (4:3 or 16:9) 3. See relevant DVD specs and compatibility 4. Queue DVD conversion jobs 5. Process with existing queue system
This commit is contained in:
parent
ebdf7b9ba7
commit
a1fac12dbf
335
main.go
335
main.go
|
|
@ -17,6 +17,7 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
@ -102,6 +103,8 @@ var formatOptions = []formatOption{
|
||||||
{"MP4 (H.264)", ".mp4", "libx264"},
|
{"MP4 (H.264)", ".mp4", "libx264"},
|
||||||
{"MKV (H.265)", ".mkv", "libx265"},
|
{"MKV (H.265)", ".mkv", "libx265"},
|
||||||
{"MOV (ProRes)", ".mov", "prores_ks"},
|
{"MOV (ProRes)", ".mov", "prores_ks"},
|
||||||
|
{"DVD-NTSC (MPEG-2)", ".mpg", "mpeg2video"},
|
||||||
|
{"DVD-PAL (MPEG-2)", ".mpg", "mpeg2video"},
|
||||||
}
|
}
|
||||||
|
|
||||||
type convertConfig struct {
|
type convertConfig struct {
|
||||||
|
|
@ -175,6 +178,7 @@ type appState struct {
|
||||||
playSess *playSession
|
playSess *playSession
|
||||||
jobQueue *queue.Queue
|
jobQueue *queue.Queue
|
||||||
statsBar *ui.ConversionStatsBar
|
statsBar *ui.ConversionStatsBar
|
||||||
|
queueBtn *widget.Button
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) stopPreview() {
|
func (s *appState) stopPreview() {
|
||||||
|
|
@ -208,6 +212,32 @@ func (s *appState) updateStatsBar() {
|
||||||
s.statsBar.UpdateStats(running, pending, completed, failed, progress, jobTitle)
|
s.statsBar.UpdateStats(running, pending, completed, failed, progress, jobTitle)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *appState) queueProgressCounts() (completed, total int) {
|
||||||
|
if s.jobQueue == nil {
|
||||||
|
return 0, 0
|
||||||
|
}
|
||||||
|
pending, running, completedCount, failed := s.jobQueue.Stats()
|
||||||
|
// Total includes all jobs in memory, including cancelled/failed/pending
|
||||||
|
total = len(s.jobQueue.List())
|
||||||
|
completed = completedCount
|
||||||
|
_ = pending
|
||||||
|
_ = running
|
||||||
|
_ = failed
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) updateQueueButtonLabel() {
|
||||||
|
if s.queueBtn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
completed, total := s.queueProgressCounts()
|
||||||
|
label := "View Queue"
|
||||||
|
if total > 0 {
|
||||||
|
label = fmt.Sprintf("View Queue %d/%d", completed, total)
|
||||||
|
}
|
||||||
|
s.queueBtn.SetText(label)
|
||||||
|
}
|
||||||
|
|
||||||
type playerSurface struct {
|
type playerSurface struct {
|
||||||
obj fyne.CanvasObject
|
obj fyne.CanvasObject
|
||||||
width, height int
|
width, height int
|
||||||
|
|
@ -306,13 +336,18 @@ func (s *appState) applyInverseDefaults(src *videoSource) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) setContent(body fyne.CanvasObject) {
|
func (s *appState) setContent(body fyne.CanvasObject) {
|
||||||
bg := canvas.NewRectangle(backgroundColor)
|
update := func() {
|
||||||
// Don't set a minimum size - let content determine layout naturally
|
bg := canvas.NewRectangle(backgroundColor)
|
||||||
if body == nil {
|
// Don't set a minimum size - let content determine layout naturally
|
||||||
s.window.SetContent(bg)
|
if body == nil {
|
||||||
return
|
s.window.SetContent(bg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.window.SetContent(container.NewMax(bg, body))
|
||||||
}
|
}
|
||||||
s.window.SetContent(container.NewMax(bg, body))
|
|
||||||
|
// Always marshal content changes onto the Fyne UI thread
|
||||||
|
fyne.DoAndWait(update)
|
||||||
}
|
}
|
||||||
|
|
||||||
// showErrorWithCopy displays an error dialog with a "Copy Error" button
|
// showErrorWithCopy displays an error dialog with a "Copy Error" button
|
||||||
|
|
@ -361,15 +396,15 @@ func (s *appState) showMainMenu() {
|
||||||
|
|
||||||
titleColor := utils.MustHex("#4CE870")
|
titleColor := utils.MustHex("#4CE870")
|
||||||
|
|
||||||
// Get queue stats - show active jobs (pending+running) out of total
|
// Get queue stats - show completed jobs out of total
|
||||||
var queueActive, queueTotal int
|
var queueCompleted, queueTotal int
|
||||||
if s.jobQueue != nil {
|
if s.jobQueue != nil {
|
||||||
pending, running, _, _ := s.jobQueue.Stats()
|
_, _, completed, _ := s.jobQueue.Stats()
|
||||||
queueActive = pending + running
|
queueCompleted = completed
|
||||||
queueTotal = len(s.jobQueue.List())
|
queueTotal = len(s.jobQueue.List())
|
||||||
}
|
}
|
||||||
|
|
||||||
menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, titleColor, queueColor, textColor, queueActive, queueTotal)
|
menu := ui.BuildMainMenu(mods, s.showModule, s.handleModuleDrop, s.showQueue, titleColor, queueColor, textColor, queueCompleted, queueTotal)
|
||||||
|
|
||||||
// Update stats bar
|
// Update stats bar
|
||||||
s.updateStatsBar()
|
s.updateStatsBar()
|
||||||
|
|
@ -420,12 +455,38 @@ func (s *appState) showQueue() {
|
||||||
}
|
}
|
||||||
s.showQueue() // Refresh
|
s.showQueue() // Refresh
|
||||||
},
|
},
|
||||||
|
func(id string) { // onMoveUp
|
||||||
|
if err := s.jobQueue.MoveUp(id); err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "failed to move job up: %v", err)
|
||||||
|
}
|
||||||
|
s.showQueue() // Refresh
|
||||||
|
},
|
||||||
|
func(id string) { // onMoveDown
|
||||||
|
if err := s.jobQueue.MoveDown(id); err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "failed to move job down: %v", err)
|
||||||
|
}
|
||||||
|
s.showQueue() // Refresh
|
||||||
|
},
|
||||||
|
func() { // onPauseAll
|
||||||
|
s.jobQueue.PauseAll()
|
||||||
|
s.showQueue()
|
||||||
|
},
|
||||||
|
func() { // onResumeAll
|
||||||
|
s.jobQueue.ResumeAll()
|
||||||
|
s.showQueue()
|
||||||
|
},
|
||||||
|
func() { // onStart
|
||||||
|
s.jobQueue.ResumeAll()
|
||||||
|
s.showQueue()
|
||||||
|
},
|
||||||
func() { // onClear
|
func() { // onClear
|
||||||
s.jobQueue.Clear()
|
s.jobQueue.Clear()
|
||||||
|
s.clearVideo()
|
||||||
s.showQueue() // Refresh
|
s.showQueue() // Refresh
|
||||||
},
|
},
|
||||||
func() { // onClearAll
|
func() { // onClearAll
|
||||||
s.jobQueue.ClearAll()
|
s.jobQueue.ClearAll()
|
||||||
|
s.clearVideo()
|
||||||
s.showQueue() // Refresh
|
s.showQueue() // Refresh
|
||||||
},
|
},
|
||||||
utils.MustHex("#4CE870"), // titleColor
|
utils.MustHex("#4CE870"), // titleColor
|
||||||
|
|
@ -482,6 +543,7 @@ func (s *appState) addConvertToQueue() error {
|
||||||
"outputAspect": cfg.OutputAspect,
|
"outputAspect": cfg.OutputAspect,
|
||||||
"sourceWidth": src.Width,
|
"sourceWidth": src.Width,
|
||||||
"sourceHeight": src.Height,
|
"sourceHeight": src.Height,
|
||||||
|
"sourceDuration": src.Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
job := &queue.Job{
|
job := &queue.Job{
|
||||||
|
|
@ -491,7 +553,6 @@ func (s *appState) addConvertToQueue() error {
|
||||||
InputFile: src.Path,
|
InputFile: src.Path,
|
||||||
OutputFile: outPath,
|
OutputFile: outPath,
|
||||||
Config: config,
|
Config: config,
|
||||||
Priority: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.jobQueue.Add(job)
|
s.jobQueue.Add(job)
|
||||||
|
|
@ -652,6 +713,7 @@ func (s *appState) batchAddToQueue(paths []string) {
|
||||||
"outputAspect": s.convert.OutputAspect,
|
"outputAspect": s.convert.OutputAspect,
|
||||||
"sourceWidth": src.Width,
|
"sourceWidth": src.Width,
|
||||||
"sourceHeight": src.Height,
|
"sourceHeight": src.Height,
|
||||||
|
"sourceDuration": src.Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
job := &queue.Job{
|
job := &queue.Job{
|
||||||
|
|
@ -661,7 +723,6 @@ func (s *appState) batchAddToQueue(paths []string) {
|
||||||
InputFile: path,
|
InputFile: path,
|
||||||
OutputFile: outPath,
|
OutputFile: outPath,
|
||||||
Config: config,
|
Config: config,
|
||||||
Priority: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s.jobQueue.Add(job)
|
s.jobQueue.Add(job)
|
||||||
|
|
@ -684,7 +745,21 @@ func (s *appState) batchAddToQueue(paths []string) {
|
||||||
|
|
||||||
// Load all valid videos so user can navigate between them
|
// Load all valid videos so user can navigate between them
|
||||||
if firstValidPath != "" {
|
if firstValidPath != "" {
|
||||||
s.loadVideos(paths)
|
combined := make([]string, 0, len(s.loadedVideos)+len(paths))
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
for _, v := range s.loadedVideos {
|
||||||
|
if v != nil && !seen[v.Path] {
|
||||||
|
combined = append(combined, v.Path)
|
||||||
|
seen[v.Path] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, p := range paths {
|
||||||
|
if !seen[p] {
|
||||||
|
combined = append(combined, p)
|
||||||
|
seen[p] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.loadVideos(combined)
|
||||||
s.showModule("convert")
|
s.showModule("convert")
|
||||||
}
|
}
|
||||||
}, false)
|
}, false)
|
||||||
|
|
@ -935,6 +1010,9 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
||||||
// Parse progress
|
// Parse progress
|
||||||
scanner := bufio.NewScanner(stdout)
|
scanner := bufio.NewScanner(stdout)
|
||||||
var duration float64
|
var duration float64
|
||||||
|
if d, ok := cfg["sourceDuration"].(float64); ok && d > 0 {
|
||||||
|
duration = d
|
||||||
|
}
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Text()
|
line := scanner.Text()
|
||||||
if strings.HasPrefix(line, "out_time_ms=") {
|
if strings.HasPrefix(line, "out_time_ms=") {
|
||||||
|
|
@ -1009,10 +1087,21 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if display := os.Getenv("DISPLAY"); display == "" {
|
// Detect display server (X11 or Wayland)
|
||||||
logging.Debug(logging.CatUI, "DISPLAY environment variable is empty; GUI may not be visible in headless mode")
|
display := os.Getenv("DISPLAY")
|
||||||
|
waylandDisplay := os.Getenv("WAYLAND_DISPLAY")
|
||||||
|
xdgSessionType := os.Getenv("XDG_SESSION_TYPE")
|
||||||
|
|
||||||
|
if waylandDisplay != "" {
|
||||||
|
logging.Debug(logging.CatUI, "Wayland display server detected: WAYLAND_DISPLAY=%s", waylandDisplay)
|
||||||
|
} else if display != "" {
|
||||||
|
logging.Debug(logging.CatUI, "X11 display server detected: DISPLAY=%s", display)
|
||||||
} else {
|
} else {
|
||||||
logging.Debug(logging.CatUI, "DISPLAY=%s", display)
|
logging.Debug(logging.CatUI, "No display server detected (DISPLAY and WAYLAND_DISPLAY are empty); GUI may not be visible in headless mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
if xdgSessionType != "" {
|
||||||
|
logging.Debug(logging.CatUI, "Session type: %s", xdgSessionType)
|
||||||
}
|
}
|
||||||
runGUI()
|
runGUI()
|
||||||
}
|
}
|
||||||
|
|
@ -1022,6 +1111,12 @@ func runGUI() {
|
||||||
ui.SetColors(gridColor, textColor)
|
ui.SetColors(gridColor, textColor)
|
||||||
|
|
||||||
a := app.NewWithID("com.leaktechnologies.videotools")
|
a := app.NewWithID("com.leaktechnologies.videotools")
|
||||||
|
|
||||||
|
// Always start with a clean slate: wipe any persisted app storage (queue or otherwise)
|
||||||
|
if root := a.Storage().RootURI(); root != nil && root.Scheme() == "file" {
|
||||||
|
_ = os.RemoveAll(root.Path())
|
||||||
|
}
|
||||||
|
|
||||||
a.Settings().SetTheme(&ui.MonoTheme{})
|
a.Settings().SetTheme(&ui.MonoTheme{})
|
||||||
logging.Debug(logging.CatUI, "created fyne app: %#v", a)
|
logging.Debug(logging.CatUI, "created fyne app: %#v", a)
|
||||||
w := a.NewWindow("VideoTools")
|
w := a.NewWindow("VideoTools")
|
||||||
|
|
@ -1081,9 +1176,19 @@ func runGUI() {
|
||||||
|
|
||||||
// Initialize job queue
|
// Initialize job queue
|
||||||
state.jobQueue = queue.New(state.jobExecutor)
|
state.jobQueue = queue.New(state.jobExecutor)
|
||||||
|
state.jobQueue.SetChangeCallback(func() {
|
||||||
// Start queue processing (but paused by default)
|
app := fyne.CurrentApp()
|
||||||
state.jobQueue.Start()
|
if app == nil || app.Driver() == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.Driver().DoFromGoroutine(func() {
|
||||||
|
state.updateStatsBar()
|
||||||
|
state.updateQueueButtonLabel()
|
||||||
|
if state.active == "queue" {
|
||||||
|
state.showQueue()
|
||||||
|
}
|
||||||
|
}, false)
|
||||||
|
})
|
||||||
|
|
||||||
defer state.shutdown()
|
defer state.shutdown()
|
||||||
w.SetOnDropped(func(pos fyne.Position, items []fyne.URI) {
|
w.SetOnDropped(func(pos fyne.Position, items []fyne.URI) {
|
||||||
|
|
@ -1260,6 +1365,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
queueBtn := widget.NewButton("View Queue", func() {
|
queueBtn := widget.NewButton("View Queue", func() {
|
||||||
state.showQueue()
|
state.showQueue()
|
||||||
})
|
})
|
||||||
|
state.queueBtn = queueBtn
|
||||||
|
state.updateQueueButtonLabel()
|
||||||
|
|
||||||
backBar := ui.TintedBar(convertColor, container.NewHBox(back, layout.NewSpacer(), navButtons, layout.NewSpacer(), queueBtn))
|
backBar := ui.TintedBar(convertColor, container.NewHBox(back, layout.NewSpacer(), navButtons, layout.NewSpacer(), queueBtn))
|
||||||
|
|
||||||
|
|
@ -1302,6 +1409,48 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
})
|
})
|
||||||
formatSelect.SetSelected(state.convert.SelectedFormat.Label)
|
formatSelect.SetSelected(state.convert.SelectedFormat.Label)
|
||||||
|
|
||||||
|
// DVD-specific aspect ratio selector (only shown for DVD formats)
|
||||||
|
dvdAspectSelect := widget.NewSelect([]string{"4:3", "16:9"}, func(value string) {
|
||||||
|
logging.Debug(logging.CatUI, "DVD aspect set to %s", value)
|
||||||
|
state.convert.OutputAspect = value
|
||||||
|
})
|
||||||
|
dvdAspectSelect.SetSelected("16:9")
|
||||||
|
dvdAspectLabel := widget.NewLabelWithStyle("DVD Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||||
|
|
||||||
|
// DVD info label showing specs based on format selected
|
||||||
|
dvdInfoLabel := widget.NewLabel("")
|
||||||
|
dvdInfoLabel.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
dvdAspectBox := container.NewVBox(dvdAspectLabel, dvdAspectSelect, dvdInfoLabel)
|
||||||
|
dvdAspectBox.Hide() // Hidden by default
|
||||||
|
|
||||||
|
// Show/hide DVD options based on format selection
|
||||||
|
updateDVDOptions := func() {
|
||||||
|
isDVD := state.convert.SelectedFormat.Ext == ".mpg"
|
||||||
|
if isDVD {
|
||||||
|
dvdAspectBox.Show()
|
||||||
|
// Update DVD info based on which DVD format was selected
|
||||||
|
if strings.Contains(state.convert.SelectedFormat.Label, "NTSC") {
|
||||||
|
dvdInfoLabel.SetText("NTSC: 720×480 @ 29.97fps, MPEG-2, AC-3 Stereo 48kHz\nBitrate: 6000k (default), 9000k (max PS2-safe)\nCompatible with DVDStyler, PS2, standalone DVD players")
|
||||||
|
} else if strings.Contains(state.convert.SelectedFormat.Label, "PAL") {
|
||||||
|
dvdInfoLabel.SetText("PAL: 720×576 @ 25.00fps, MPEG-2, AC-3 Stereo 48kHz\nBitrate: 8000k (default), 9500k (max PS2-safe)\nCompatible with European DVD players and authoring tools")
|
||||||
|
} else {
|
||||||
|
dvdInfoLabel.SetText("DVD Format selected")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dvdAspectBox.Hide()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update formatSelect callback to also handle DVD options visibility
|
||||||
|
originalFormatCallback := formatSelect.OnChanged
|
||||||
|
formatSelect.OnChanged = func(value string) {
|
||||||
|
if originalFormatCallback != nil {
|
||||||
|
originalFormatCallback(value)
|
||||||
|
}
|
||||||
|
updateDVDOptions()
|
||||||
|
}
|
||||||
|
|
||||||
qualitySelect := widget.NewSelect([]string{"Draft (CRF 28)", "Standard (CRF 23)", "High (CRF 18)", "Lossless"}, func(value string) {
|
qualitySelect := widget.NewSelect([]string{"Draft (CRF 28)", "Standard (CRF 23)", "High (CRF 18)", "Lossless"}, func(value string) {
|
||||||
logging.Debug(logging.CatUI, "quality preset %s", value)
|
logging.Debug(logging.CatUI, "quality preset %s", value)
|
||||||
state.convert.Quality = value
|
state.convert.Quality = value
|
||||||
|
|
@ -1378,6 +1527,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||||
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
formatSelect,
|
formatSelect,
|
||||||
|
dvdAspectBox, // DVD options appear here when DVD format selected
|
||||||
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
outputEntry,
|
outputEntry,
|
||||||
outputHint,
|
outputHint,
|
||||||
|
|
@ -1488,6 +1638,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("═══ OUTPUT ═══", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||||
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
formatSelect,
|
formatSelect,
|
||||||
|
dvdAspectBox, // DVD options appear here when DVD format selected
|
||||||
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||||
outputEntry,
|
outputEntry,
|
||||||
outputHint,
|
outputHint,
|
||||||
|
|
@ -2819,9 +2970,25 @@ func (s *appState) loadVideo(path string) {
|
||||||
s.playerPos = 0
|
s.playerPos = 0
|
||||||
s.playerPaused = true
|
s.playerPaused = true
|
||||||
|
|
||||||
// Set up single-video navigation
|
// Maintain/extend loaded video list for navigation
|
||||||
s.loadedVideos = []*videoSource{src}
|
found := -1
|
||||||
s.currentIndex = 0
|
for i, v := range s.loadedVideos {
|
||||||
|
if v.Path == src.Path {
|
||||||
|
found = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if found >= 0 {
|
||||||
|
s.loadedVideos[found] = src
|
||||||
|
s.currentIndex = found
|
||||||
|
} else if len(s.loadedVideos) > 0 {
|
||||||
|
s.loadedVideos = append(s.loadedVideos, src)
|
||||||
|
s.currentIndex = len(s.loadedVideos) - 1
|
||||||
|
} else {
|
||||||
|
s.loadedVideos = []*videoSource{src}
|
||||||
|
s.currentIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
logging.Debug(logging.CatModule, "video loaded %+v", src)
|
logging.Debug(logging.CatModule, "video loaded %+v", src)
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
|
|
@ -2849,33 +3016,107 @@ func (s *appState) clearVideo() {
|
||||||
|
|
||||||
// loadVideos loads multiple videos for navigation
|
// loadVideos loads multiple videos for navigation
|
||||||
func (s *appState) loadVideos(paths []string) {
|
func (s *appState) loadVideos(paths []string) {
|
||||||
s.loadedVideos = nil
|
if len(paths) == 0 {
|
||||||
s.currentIndex = 0
|
|
||||||
|
|
||||||
// Load all videos
|
|
||||||
for _, path := range paths {
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
logging.Debug(logging.CatFFMPEG, "ffprobe failed for %s: %v", path, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if frames, err := capturePreviewFrames(src.Path, src.Duration); err == nil {
|
|
||||||
src.PreviewFrames = frames
|
|
||||||
}
|
|
||||||
|
|
||||||
s.loadedVideos = append(s.loadedVideos, src)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(s.loadedVideos) == 0 {
|
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
|
||||||
s.showErrorWithCopy("Failed to Load Videos", fmt.Errorf("no valid videos to load"))
|
|
||||||
}, false)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load the first video
|
go func() {
|
||||||
s.switchToVideo(0)
|
total := len(paths)
|
||||||
|
type result struct {
|
||||||
|
idx int
|
||||||
|
src *videoSource
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress UI
|
||||||
|
status := widget.NewLabel(fmt.Sprintf("Loading 0/%d", total))
|
||||||
|
progress := widget.NewProgressBar()
|
||||||
|
progress.Max = float64(total)
|
||||||
|
var dlg dialog.Dialog
|
||||||
|
fyne.Do(func() {
|
||||||
|
dlg = dialog.NewCustomWithoutButtons("Loading Videos", container.NewVBox(status, progress), s.window)
|
||||||
|
dlg.Show()
|
||||||
|
})
|
||||||
|
defer fyne.Do(func() {
|
||||||
|
if dlg != nil {
|
||||||
|
dlg.Hide()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
results := make([]*videoSource, total)
|
||||||
|
var mu sync.Mutex
|
||||||
|
done := 0
|
||||||
|
|
||||||
|
workerCount := runtime.NumCPU()
|
||||||
|
if workerCount > 4 {
|
||||||
|
workerCount = 4
|
||||||
|
}
|
||||||
|
if workerCount < 1 {
|
||||||
|
workerCount = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
jobs := make(chan int, total)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
for w := 0; w < workerCount; w++ {
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for idx := range jobs {
|
||||||
|
path := paths[idx]
|
||||||
|
src, err := probeVideo(path)
|
||||||
|
if err == nil {
|
||||||
|
if frames, ferr := capturePreviewFrames(src.Path, src.Duration); ferr == nil {
|
||||||
|
src.PreviewFrames = frames
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
results[idx] = src
|
||||||
|
done++
|
||||||
|
curDone := done
|
||||||
|
mu.Unlock()
|
||||||
|
fyne.Do(func() {
|
||||||
|
status.SetText(fmt.Sprintf("Loading %d/%d", curDone, total))
|
||||||
|
progress.SetValue(float64(curDone))
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
logging.Debug(logging.CatFFMPEG, "ffprobe failed for %s: %v", path, err)
|
||||||
|
mu.Lock()
|
||||||
|
done++
|
||||||
|
curDone := done
|
||||||
|
mu.Unlock()
|
||||||
|
fyne.Do(func() {
|
||||||
|
status.SetText(fmt.Sprintf("Loading %d/%d", curDone, total))
|
||||||
|
progress.SetValue(float64(curDone))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for i := range paths {
|
||||||
|
jobs <- i
|
||||||
|
}
|
||||||
|
close(jobs)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Collect valid videos in original order
|
||||||
|
var loaded []*videoSource
|
||||||
|
for _, src := range results {
|
||||||
|
if src != nil {
|
||||||
|
loaded = append(loaded, src)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(loaded) == 0 {
|
||||||
|
fyne.Do(func() {
|
||||||
|
s.showErrorWithCopy("Failed to Load Videos", fmt.Errorf("no valid videos to load"))
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.loadedVideos = loaded
|
||||||
|
s.currentIndex = 0
|
||||||
|
fyne.Do(func() {
|
||||||
|
s.switchToVideo(0)
|
||||||
|
})
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// switchToVideo switches to a specific video by index
|
// switchToVideo switches to a specific video by index
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user