Compare commits
7 Commits
1051329763
...
e923715b95
| Author | SHA1 | Date | |
|---|---|---|---|
| e923715b95 | |||
| c464a7a7dd | |||
| cf219e9770 | |||
| ff65928ba0 | |||
| b887142401 | |||
| 5026a946f5 | |||
| 3863242ba9 |
39
DONE.md
39
DONE.md
|
|
@ -2,7 +2,44 @@
|
|||
|
||||
This file tracks completed features, fixes, and milestones.
|
||||
|
||||
## Version 0.1.0-dev20+ (2025-12-28) - Queue UI Performance & Remux Safety
|
||||
## Version 0.1.0-dev20+ (2025-12-28) - Queue UI Performance & Workflow Improvements
|
||||
|
||||
### Workflow Enhancements
|
||||
- ✅ **Benchmark Result Caching**
|
||||
- Benchmark results now persist across app restarts
|
||||
- Opening Benchmark module shows cached results instead of auto-running
|
||||
- Clear timestamp display (e.g., "Showing cached results from December 28, 2025 at 2:45 PM")
|
||||
- "Run New Benchmark" button available when viewing cached results
|
||||
- Auto-runs only when no previous results exist or hardware has changed (GPU detection)
|
||||
- Saves to `~/.config/VideoTools/benchmark.json` with last 10 runs in history
|
||||
- No more redundant benchmarks every time you open the module
|
||||
|
||||
- ✅ **Merge Module Output Path UX Improvement**
|
||||
- Split single output path field into separate folder and filename fields
|
||||
- "Output Folder" field with "Browse Folder" button for directory selection
|
||||
- "Output Filename" field for easy filename editing (e.g., "merged.mkv")
|
||||
- No more navigating through long paths to change filenames
|
||||
- Cleaner, more intuitive interface following standard file dialog patterns
|
||||
- Auto-population sets directory and filename independently
|
||||
|
||||
- ✅ **Queue Priority System for Convert Now**
|
||||
- "Convert Now" during active conversions adds job to top of queue (after running job)
|
||||
- "Add to Queue" continues to add to end as expected
|
||||
- Implemented AddNext() method in queue package for priority insertion
|
||||
- User feedback message indicates queue position: "Added to top of queue!" vs "Conversion started!"
|
||||
- Better workflow when adding files during active batch conversions
|
||||
|
||||
- ✅ **Auto-Cleanup for Failed Conversions**
|
||||
- Convert jobs now automatically delete incomplete/broken output files on failure
|
||||
- Success tracking ensures complete files are never removed
|
||||
- Prevents accumulation of partial files from crashed/cancelled conversions
|
||||
- Cleaner disk space management and error handling
|
||||
|
||||
- ✅ **Queue List Jankiness Reduction**
|
||||
- Increased auto-refresh interval from 1000ms to 2000ms for smoother updates
|
||||
- Reduced scroll restoration delay from 50ms to 10ms for faster position recovery
|
||||
- Fixed race condition in scroll offset saving
|
||||
- Eliminated visible jumping during queue view rebuilds
|
||||
|
||||
### Performance Optimizations
|
||||
- ✅ **Queue View Button Responsiveness**
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ func (q *Queue) notifyChange() {
|
|||
}
|
||||
}
|
||||
|
||||
// Add adds a job to the queue
|
||||
// Add adds a job to the queue (at the end)
|
||||
func (q *Queue) Add(job *Job) {
|
||||
q.mu.Lock()
|
||||
|
||||
|
|
@ -115,6 +115,37 @@ func (q *Queue) Add(job *Job) {
|
|||
q.notifyChange()
|
||||
}
|
||||
|
||||
// AddNext adds a job to the front of the pending queue (right after any running job)
|
||||
func (q *Queue) AddNext(job *Job) {
|
||||
q.mu.Lock()
|
||||
|
||||
if job.ID == "" {
|
||||
job.ID = generateID()
|
||||
}
|
||||
if job.CreatedAt.IsZero() {
|
||||
job.CreatedAt = time.Now()
|
||||
}
|
||||
if job.Status == "" {
|
||||
job.Status = JobStatusPending
|
||||
}
|
||||
|
||||
// Find the position after any running jobs
|
||||
insertPos := 0
|
||||
for i, j := range q.jobs {
|
||||
if j.Status == JobStatusRunning {
|
||||
insertPos = i + 1
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at the calculated position
|
||||
q.jobs = append(q.jobs[:insertPos], append([]*Job{job}, q.jobs[insertPos:]...)...)
|
||||
q.rebalancePrioritiesLocked()
|
||||
q.mu.Unlock()
|
||||
q.notifyChange()
|
||||
}
|
||||
|
||||
// Remove removes a job from the queue by ID
|
||||
func (q *Queue) Remove(id string) error {
|
||||
q.mu.Lock()
|
||||
|
|
|
|||
266
main.go
266
main.go
|
|
@ -866,7 +866,8 @@ type appState struct {
|
|||
// Merge state
|
||||
mergeClips []mergeClip
|
||||
mergeFormat string
|
||||
mergeOutput string
|
||||
mergeOutputDir string
|
||||
mergeOutputFilename string
|
||||
mergeKeepAll bool
|
||||
mergeCodecMode string
|
||||
mergeChapters bool
|
||||
|
|
@ -1935,13 +1936,15 @@ func (s *appState) refreshQueueView() {
|
|||
// Restore scroll offset
|
||||
s.queueScroll = scroll
|
||||
if s.queueScroll != nil && s.active == "queue" {
|
||||
// Use ScrollTo instead of directly setting Offset to prevent rubber banding
|
||||
// Defer to allow UI to settle first
|
||||
// Restore scroll position immediately to reduce jankiness
|
||||
// Set offset before showing to avoid visible jumping
|
||||
savedOffset := s.queueOffset
|
||||
go func() {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
// Minimal delay to allow layout calculation
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
if s.queueScroll != nil {
|
||||
s.queueScroll.Offset = s.queueOffset
|
||||
s.queueScroll.Offset = savedOffset
|
||||
s.queueScroll.Refresh()
|
||||
}
|
||||
}, false)
|
||||
|
|
@ -1962,9 +1965,10 @@ func (s *appState) startQueueAutoRefresh() {
|
|||
s.queueAutoRefreshStop = stop
|
||||
s.queueAutoRefreshRunning = true
|
||||
go func() {
|
||||
// Use 1-second interval to reduce UI update frequency, especially on Windows
|
||||
// Use 2-second interval to reduce UI jankiness from frequent rebuilds
|
||||
// Slower refresh = smoother experience, especially with scroll position preservation
|
||||
// The refreshQueueView method has its own 500ms throttle for other triggers
|
||||
ticker := time.NewTicker(1000 * time.Millisecond)
|
||||
ticker := time.NewTicker(2000 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
|
|
@ -2008,15 +2012,15 @@ func (s *appState) stopQueueAutoRefresh() {
|
|||
}
|
||||
|
||||
// addConvertToQueue adds a conversion job to the queue
|
||||
func (s *appState) addConvertToQueue() error {
|
||||
func (s *appState) addConvertToQueue(addToTop bool) error {
|
||||
if s.source == nil {
|
||||
return fmt.Errorf("no video loaded")
|
||||
}
|
||||
|
||||
return s.addConvertToQueueForSource(s.source)
|
||||
return s.addConvertToQueueForSource(s.source, addToTop)
|
||||
}
|
||||
|
||||
func (s *appState) addConvertToQueueForSource(src *videoSource) error {
|
||||
func (s *appState) addConvertToQueueForSource(src *videoSource, addToTop bool) error {
|
||||
outputBase := s.resolveOutputBase(src, true)
|
||||
cfg := s.convert
|
||||
cfg.OutputBase = outputBase
|
||||
|
|
@ -2102,8 +2106,14 @@ func (s *appState) addConvertToQueueForSource(src *videoSource) error {
|
|||
Config: config,
|
||||
}
|
||||
|
||||
s.jobQueue.Add(job)
|
||||
logging.Debug(logging.CatSystem, "added convert job to queue: %s", job.ID)
|
||||
// Add to top (after running job) if requested and queue is running
|
||||
if addToTop && s.jobQueue.IsRunning() {
|
||||
s.jobQueue.AddNext(job)
|
||||
logging.Debug(logging.CatSystem, "added convert job to top of queue: %s", job.ID)
|
||||
} else {
|
||||
s.jobQueue.Add(job)
|
||||
logging.Debug(logging.CatSystem, "added convert job to queue: %s", job.ID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -2241,6 +2251,80 @@ func (s *appState) showBenchmark() {
|
|||
hwInfo := sysinfo.Detect()
|
||||
logging.Debug(logging.CatSystem, "detected hardware for benchmark: %s", hwInfo.Summary())
|
||||
|
||||
// Check if we have recent benchmark results for this hardware
|
||||
cfg, err := loadBenchmarkConfig()
|
||||
if err == nil && len(cfg.History) > 0 {
|
||||
lastRun := cfg.History[0]
|
||||
|
||||
// Check if hardware matches (same GPU)
|
||||
hardwareMatches := lastRun.HardwareInfo.GPU == hwInfo.GPU
|
||||
|
||||
// If hardware matches, show last results instead of auto-running
|
||||
if hardwareMatches && len(lastRun.Results) > 0 {
|
||||
logging.Debug(logging.CatSystem, "found existing benchmark from %s, showing results", lastRun.Timestamp.Format("2006-01-02"))
|
||||
|
||||
// Create recommendation from saved data
|
||||
rec := benchmark.Result{
|
||||
Encoder: lastRun.RecommendedEncoder,
|
||||
Preset: lastRun.RecommendedPreset,
|
||||
FPS: lastRun.RecommendedFPS,
|
||||
Score: lastRun.RecommendedFPS,
|
||||
}
|
||||
|
||||
// Show results with "Run New Benchmark" option
|
||||
resultsView := ui.BuildBenchmarkResultsView(
|
||||
lastRun.Results,
|
||||
rec,
|
||||
lastRun.HardwareInfo,
|
||||
func() {
|
||||
// Apply recommended settings
|
||||
s.applyBenchmarkRecommendation(lastRun.RecommendedEncoder, lastRun.RecommendedPreset)
|
||||
s.showMainMenu()
|
||||
},
|
||||
func() {
|
||||
// Close - go back to main menu
|
||||
s.showMainMenu()
|
||||
},
|
||||
utils.MustHex("#4CE870"),
|
||||
utils.MustHex("#1E1E1E"),
|
||||
utils.MustHex("#FFFFFF"),
|
||||
)
|
||||
|
||||
// Add "Run New Benchmark" button at the bottom
|
||||
runNewBtn := widget.NewButton("Run New Benchmark", func() {
|
||||
s.runNewBenchmark()
|
||||
})
|
||||
runNewBtn.Importance = widget.MediumImportance
|
||||
|
||||
cachedNote := widget.NewLabel(fmt.Sprintf("Showing cached results from %s", lastRun.Timestamp.Format("January 2, 2006 at 3:04 PM")))
|
||||
cachedNote.Alignment = fyne.TextAlignCenter
|
||||
cachedNote.TextStyle = fyne.TextStyle{Italic: true}
|
||||
|
||||
viewWithButton := container.NewBorder(
|
||||
nil,
|
||||
container.NewVBox(
|
||||
widget.NewSeparator(),
|
||||
cachedNote,
|
||||
container.NewCenter(runNewBtn),
|
||||
),
|
||||
nil, nil,
|
||||
resultsView,
|
||||
)
|
||||
|
||||
s.setContent(viewWithButton)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// No existing benchmark or hardware changed - run new benchmark
|
||||
s.runNewBenchmark()
|
||||
}
|
||||
|
||||
func (s *appState) runNewBenchmark() {
|
||||
// Detect hardware info upfront
|
||||
hwInfo := sysinfo.Detect()
|
||||
logging.Debug(logging.CatSystem, "starting new benchmark for hardware: %s", hwInfo.Summary())
|
||||
|
||||
// Create benchmark suite
|
||||
tmpDir := filepath.Join(utils.TempDir(), "videotools-benchmark")
|
||||
_ = os.MkdirAll(tmpDir, 0o755)
|
||||
|
|
@ -2766,9 +2850,11 @@ func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) {
|
|||
}
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.mergeClips = append(s.mergeClips, clips...)
|
||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutput) == "" {
|
||||
first := filepath.Dir(s.mergeClips[0].Path)
|
||||
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputDir) == "" {
|
||||
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||
}
|
||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputFilename) == "" {
|
||||
s.mergeOutputFilename = "merged.mkv"
|
||||
}
|
||||
s.showMergeView()
|
||||
}, false)
|
||||
|
|
@ -3161,9 +3247,11 @@ func (s *appState) showMergeView() {
|
|||
Duration: src.Duration,
|
||||
})
|
||||
}
|
||||
if len(s.mergeClips) >= 2 && s.mergeOutput == "" {
|
||||
first := filepath.Dir(s.mergeClips[0].Path)
|
||||
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
||||
if len(s.mergeClips) >= 2 && s.mergeOutputDir == "" {
|
||||
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||
}
|
||||
if len(s.mergeClips) >= 2 && s.mergeOutputFilename == "" {
|
||||
s.mergeOutputFilename = "merged.mkv"
|
||||
}
|
||||
buildList()
|
||||
}
|
||||
|
|
@ -3229,31 +3317,40 @@ func (s *appState) showMergeView() {
|
|||
})
|
||||
chapterCheck.SetChecked(s.mergeChapters)
|
||||
|
||||
// Create output entry widget first so it can be referenced in callbacks
|
||||
outputEntry := widget.NewEntry()
|
||||
outputEntry.SetPlaceHolder("merged output path")
|
||||
outputEntry.SetText(s.mergeOutput)
|
||||
outputEntry.OnChanged = func(val string) {
|
||||
s.mergeOutput = val
|
||||
// Create output entry widgets first so they can be referenced in callbacks
|
||||
outputDirEntry := widget.NewEntry()
|
||||
outputDirEntry.SetPlaceHolder("Output folder path")
|
||||
outputDirEntry.SetText(s.mergeOutputDir)
|
||||
outputDirEntry.OnChanged = func(val string) {
|
||||
s.mergeOutputDir = val
|
||||
}
|
||||
|
||||
outputFilenameEntry := widget.NewEntry()
|
||||
outputFilenameEntry.SetPlaceHolder("merged.mkv")
|
||||
outputFilenameEntry.SetText(s.mergeOutputFilename)
|
||||
outputFilenameEntry.OnChanged = func(val string) {
|
||||
s.mergeOutputFilename = val
|
||||
}
|
||||
|
||||
clearBtn := widget.NewButton("Clear", func() {
|
||||
s.mergeClips = nil
|
||||
s.mergeOutput = ""
|
||||
outputEntry.SetText("")
|
||||
s.mergeOutputDir = ""
|
||||
s.mergeOutputFilename = ""
|
||||
outputDirEntry.SetText("")
|
||||
outputFilenameEntry.SetText("")
|
||||
buildList()
|
||||
})
|
||||
|
||||
// Helper to update output path extension (requires outputEntry to exist)
|
||||
// Helper to update output filename extension (requires outputFilenameEntry to exist)
|
||||
updateOutputExt := func() {
|
||||
if s.mergeOutput == "" {
|
||||
if s.mergeOutputFilename == "" {
|
||||
return
|
||||
}
|
||||
currentExt := filepath.Ext(s.mergeOutput)
|
||||
currentExt := filepath.Ext(s.mergeOutputFilename)
|
||||
correctExt := getExtForFormat(s.mergeFormat)
|
||||
if currentExt != correctExt {
|
||||
s.mergeOutput = strings.TrimSuffix(s.mergeOutput, currentExt) + correctExt
|
||||
outputEntry.SetText(s.mergeOutput)
|
||||
s.mergeOutputFilename = strings.TrimSuffix(s.mergeOutputFilename, currentExt) + correctExt
|
||||
outputFilenameEntry.SetText(s.mergeOutputFilename)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3291,9 +3388,14 @@ func (s *appState) showMergeView() {
|
|||
dvdOptionsContainer.Hide()
|
||||
}
|
||||
|
||||
// Set default output path if not set
|
||||
if s.mergeOutput == "" && len(s.mergeClips) > 0 {
|
||||
dir := filepath.Dir(s.mergeClips[0].Path)
|
||||
// Set default output directory if not set
|
||||
if s.mergeOutputDir == "" && len(s.mergeClips) > 0 {
|
||||
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||
outputDirEntry.SetText(s.mergeOutputDir)
|
||||
}
|
||||
|
||||
// Set default output filename if not set
|
||||
if s.mergeOutputFilename == "" && len(s.mergeClips) > 0 {
|
||||
ext := getExtForFormat(s.mergeFormat)
|
||||
basename := "merged"
|
||||
if strings.HasPrefix(s.mergeFormat, "dvd") || s.mergeFormat == "dvd" {
|
||||
|
|
@ -3303,10 +3405,10 @@ func (s *appState) showMergeView() {
|
|||
} else if s.mergeFormat == "mkv-lossless" {
|
||||
basename = "merged-lossless"
|
||||
}
|
||||
s.mergeOutput = filepath.Join(dir, basename+ext)
|
||||
outputEntry.SetText(s.mergeOutput)
|
||||
s.mergeOutputFilename = basename + ext
|
||||
outputFilenameEntry.SetText(s.mergeOutputFilename)
|
||||
} else {
|
||||
// Update extension of existing path
|
||||
// Update extension of existing filename
|
||||
updateOutputExt()
|
||||
}
|
||||
s.persistMergeConfig()
|
||||
|
|
@ -3344,14 +3446,13 @@ func (s *appState) showMergeView() {
|
|||
motionInterpCheck,
|
||||
)
|
||||
|
||||
browseOut := widget.NewButton("Browse", func() {
|
||||
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
|
||||
if err != nil || writer == nil {
|
||||
browseDirBtn := widget.NewButton("Browse Folder", func() {
|
||||
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
|
||||
if err != nil || uri == nil {
|
||||
return
|
||||
}
|
||||
s.mergeOutput = writer.URI().Path()
|
||||
outputEntry.SetText(s.mergeOutput)
|
||||
writer.Close()
|
||||
s.mergeOutputDir = uri.Path()
|
||||
outputDirEntry.SetText(s.mergeOutputDir)
|
||||
}, s.window)
|
||||
})
|
||||
|
||||
|
|
@ -3477,8 +3578,10 @@ func (s *appState) showMergeView() {
|
|||
keepAllCheck,
|
||||
chapterCheck,
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel("Output Path"),
|
||||
container.NewBorder(nil, nil, nil, browseOut, outputEntry),
|
||||
widget.NewLabel("Output Folder"),
|
||||
container.NewBorder(nil, nil, nil, browseDirBtn, outputDirEntry),
|
||||
widget.NewLabel("Output Filename"),
|
||||
outputFilenameEntry,
|
||||
widget.NewSeparator(),
|
||||
container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn),
|
||||
widget.NewSeparator(),
|
||||
|
|
@ -3496,13 +3599,17 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
|||
if len(s.mergeClips) < 2 {
|
||||
return fmt.Errorf("add at least two clips")
|
||||
}
|
||||
if strings.TrimSpace(s.mergeOutput) == "" {
|
||||
firstDir := filepath.Dir(s.mergeClips[0].Path)
|
||||
s.mergeOutput = filepath.Join(firstDir, "merged.mkv")
|
||||
|
||||
// Set defaults if not specified
|
||||
if strings.TrimSpace(s.mergeOutputDir) == "" {
|
||||
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||
}
|
||||
if strings.TrimSpace(s.mergeOutputFilename) == "" {
|
||||
s.mergeOutputFilename = "merged.mkv"
|
||||
}
|
||||
|
||||
// Ensure output path has correct extension for selected format
|
||||
currentExt := filepath.Ext(s.mergeOutput)
|
||||
// Ensure output filename has correct extension for selected format
|
||||
currentExt := filepath.Ext(s.mergeOutputFilename)
|
||||
var correctExt string
|
||||
switch {
|
||||
case strings.HasPrefix(s.mergeFormat, "dvd"):
|
||||
|
|
@ -3521,10 +3628,13 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
|||
|
||||
// Auto-fix extension if missing or wrong
|
||||
if currentExt == "" {
|
||||
s.mergeOutput += correctExt
|
||||
s.mergeOutputFilename += correctExt
|
||||
} else if currentExt != correctExt {
|
||||
s.mergeOutput = strings.TrimSuffix(s.mergeOutput, currentExt) + correctExt
|
||||
s.mergeOutputFilename = strings.TrimSuffix(s.mergeOutputFilename, currentExt) + correctExt
|
||||
}
|
||||
|
||||
// Combine dir and filename to create full output path
|
||||
mergeOutput := filepath.Join(s.mergeOutputDir, s.mergeOutputFilename)
|
||||
clips := make([]map[string]interface{}, 0, len(s.mergeClips))
|
||||
for _, c := range s.mergeClips {
|
||||
name := c.Chapter
|
||||
|
|
@ -3544,7 +3654,7 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
|||
"keepAllStreams": s.mergeKeepAll,
|
||||
"chapters": s.mergeChapters,
|
||||
"codecMode": s.mergeCodecMode,
|
||||
"outputPath": s.mergeOutput,
|
||||
"outputPath": mergeOutput,
|
||||
"dvdRegion": s.mergeDVDRegion,
|
||||
"dvdAspect": s.mergeDVDAspect,
|
||||
"frameRate": s.mergeFrameRate,
|
||||
|
|
@ -3554,9 +3664,9 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
|||
job := &queue.Job{
|
||||
Type: queue.JobTypeMerge,
|
||||
Title: fmt.Sprintf("Merge %d clips", len(clips)),
|
||||
Description: fmt.Sprintf("Output: %s", utils.ShortenMiddle(filepath.Base(s.mergeOutput), 40)),
|
||||
Description: fmt.Sprintf("Output: %s", utils.ShortenMiddle(filepath.Base(mergeOutput), 40)),
|
||||
InputFile: clips[0]["path"].(string),
|
||||
OutputFile: s.mergeOutput,
|
||||
OutputFile: mergeOutput,
|
||||
Config: config,
|
||||
}
|
||||
s.jobQueue.Add(job)
|
||||
|
|
@ -3964,6 +4074,18 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
inputPath := cfg["inputPath"].(string)
|
||||
outputPath := cfg["outputPath"].(string)
|
||||
|
||||
// Track success to clean up broken files on failure
|
||||
var success bool
|
||||
defer func() {
|
||||
if !success && outputPath != "" {
|
||||
// Remove incomplete/broken output file on failure
|
||||
if _, err := os.Stat(outputPath); err == nil {
|
||||
logging.Debug(logging.CatFFMPEG, "removing incomplete output file: %s", outputPath)
|
||||
os.Remove(outputPath)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// If a direct conversion is running, wait until it finishes before starting queued jobs.
|
||||
for s.convertBusy {
|
||||
select {
|
||||
|
|
@ -4676,6 +4798,8 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
}()
|
||||
}
|
||||
|
||||
// Mark as successful to prevent cleanup of output file
|
||||
success = true
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -6049,7 +6173,8 @@ func runLogsCLI() error {
|
|||
}
|
||||
|
||||
func (s *appState) executeAddToQueue() {
|
||||
if err := s.addConvertToQueue(); err != nil {
|
||||
// Add to end of queue
|
||||
if err := s.addConvertToQueue(false); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
} else {
|
||||
// Update queue button to show new count
|
||||
|
|
@ -6079,8 +6204,8 @@ func (s *appState) executeAddAllToQueue() {
|
|||
}
|
||||
|
||||
func (s *appState) executeConversion() {
|
||||
// Add job to queue and start immediately
|
||||
if err := s.addConvertToQueue(); err != nil {
|
||||
// Add job to queue (at top if queue is already running)
|
||||
if err := s.addConvertToQueue(true); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
return
|
||||
}
|
||||
|
|
@ -6095,7 +6220,11 @@ func (s *appState) executeConversion() {
|
|||
s.clearVideo()
|
||||
|
||||
// Show success message
|
||||
dialog.ShowInformation("Convert", "Conversion started! View progress in Job Queue.", s.window)
|
||||
if s.jobQueue != nil && s.jobQueue.IsRunning() {
|
||||
dialog.ShowInformation("Convert", "Added to top of queue! View progress in Job Queue.", s.window)
|
||||
} else {
|
||||
dialog.ShowInformation("Convert", "Conversion started! View progress in Job Queue.", s.window)
|
||||
}
|
||||
}
|
||||
|
||||
func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||
|
|
@ -6836,6 +6965,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
// Settings management for batch operations
|
||||
settingsInfoLabel := widget.NewLabel("Settings persist across videos. Change them anytime to affect all subsequent videos.")
|
||||
settingsInfoLabel.Alignment = fyne.TextAlignCenter
|
||||
settingsInfoLabel.Wrapping = fyne.TextWrapWord
|
||||
// Wrap in padded container for proper text wrapping in narrow windows
|
||||
settingsInfoContainer := container.NewPadded(settingsInfoLabel)
|
||||
|
||||
cacheDirLabel := widget.NewLabelWithStyle("Cache/Temp Directory", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
cacheDirEntry := widget.NewEntry()
|
||||
|
|
@ -6843,6 +6975,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
cacheDirEntry.SetText(state.convert.TempDir)
|
||||
cacheDirHint := widget.NewLabel("Use an SSD for best performance. Leave blank to use system temp.")
|
||||
cacheDirHint.Wrapping = fyne.TextWrapWord
|
||||
// Wrap in padded container for proper text wrapping in narrow windows
|
||||
cacheDirHintContainer := container.NewPadded(cacheDirHint)
|
||||
cacheDirEntry.OnChanged = func(val string) {
|
||||
state.convert.TempDir = strings.TrimSpace(val)
|
||||
utils.SetTempDir(state.convert.TempDir)
|
||||
|
|
@ -6872,12 +7006,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
resetSettingsBtn.Importance = widget.LowImportance
|
||||
|
||||
settingsContent := container.NewVBox(
|
||||
settingsInfoLabel,
|
||||
settingsInfoContainer,
|
||||
widget.NewSeparator(),
|
||||
cacheDirLabel,
|
||||
container.NewBorder(nil, nil, nil, cacheBrowseBtn, cacheDirEntry),
|
||||
cacheUseSystemBtn,
|
||||
cacheDirHint,
|
||||
cacheDirHintContainer,
|
||||
resetSettingsBtn,
|
||||
)
|
||||
settingsContent.Hide()
|
||||
|
|
@ -10613,10 +10747,12 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
|||
Duration: src.Duration,
|
||||
})
|
||||
|
||||
// Set default output path if not set and we have at least 2 clips
|
||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutput) == "" {
|
||||
first := filepath.Dir(s.mergeClips[0].Path)
|
||||
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
||||
// Set default output dir and filename if not set and we have at least 2 clips
|
||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputDir) == "" {
|
||||
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||
}
|
||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputFilename) == "" {
|
||||
s.mergeOutputFilename = "merged.mkv"
|
||||
}
|
||||
|
||||
// Refresh the merge view to show the new clips
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user