Compare commits

...

7 Commits

Author SHA1 Message Date
e923715b95 Update DONE.md with benchmark caching feature
Added documentation for benchmark result persistence and caching system.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 20:24:25 -05:00
c464a7a7dd Add benchmark result caching to avoid redundant benchmarks
Changes:
- Check for existing benchmark results when opening benchmark module
- If recent results exist for same hardware, show cached results instead of auto-running
- Display timestamp of cached results (e.g., "Showing cached results from December 28, 2025 at 2:45 PM")
- Add "Run New Benchmark" button at bottom of cached results view
- Only auto-run benchmark if no previous results exist or hardware has changed

Benefits:
- No more redundant benchmarks every time you open the module
- Results persist across app restarts (saved to ~/.config/VideoTools/benchmark.json)
- Clear indication when viewing cached vs fresh results
- Easy option to re-run if desired

Hardware detection:
- Compares GPU model to detect hardware changes
- If GPU changes, automatically runs new benchmark
- Keeps last 10 benchmark runs in history

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 20:24:11 -05:00
cf219e9770 Update DONE.md with recent workflow improvements
Added documentation for:
- Merge module output path UX improvement (folder + filename split)
- Queue priority system for Convert Now
- Auto-cleanup for failed conversions
- Queue list jankiness reduction

All features completed in dev20+ release cycle (2025-12-28)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 20:19:24 -05:00
ff65928ba0 Implement queue priority for Convert Now and auto-cleanup for failed conversions
Queue Priority Changes:
- Added AddNext() method to queue package that inserts jobs after running jobs
- "Convert Now" now adds to top of queue when conversions are already running
- "Add to Queue" continues to add to end of queue
- User feedback message indicates when job was added to top vs started fresh

Auto-Cleanup for Failed Files:
- Convert jobs now automatically delete incomplete/broken output files on failure
- Prevents accumulation of partial files from failed conversions
- Success tracking ensures complete files are never removed

Benefits:
- Better workflow when adding files during active conversions
- "Convert Now" truly prioritizes the current file
- No more broken partial files cluttering output directories
- Cleaner error handling and disk space management

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 20:14:21 -05:00
b887142401 Improve merge module UX: split output path into folder and filename fields
Changed the merge output path from a single long entry field to two
separate fields for better usability:

UI Changes:
- Output Folder: Entry with "Browse Folder" button for directory selection
- Output Filename: Entry for just the filename (e.g., "merged.mkv")
- Users can now easily change the filename without navigating through
  the entire path

Internal Changes:
- Split `mergeOutput` into `mergeOutputDir` and `mergeOutputFilename`
- Updated all merge logic to combine dir + filename when needed
- Extension correction now works on filename only
- Clear button resets both fields independently
- Auto-population sets dir and filename separately

Benefits:
- Much simpler to change output filename
- No need to scroll to end of long path
- Cleaner, more intuitive interface
- Follows common file dialog patterns

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 20:06:49 -05:00
5026a946f5 Reduce queue list jankiness during auto-refresh
Implemented two key optimizations to smooth queue list updates:

1. Increased auto-refresh interval from 1000ms to 2000ms
   - Reduces frequency of view rebuilds
   - Gives UI more time to stabilize between updates

2. Reduced scroll restoration delay from 50ms to 10ms
   - Minimizes visible jump during position restoration
   - Saves offset to variable before goroutine to avoid race conditions

These changes work together to provide a smoother queue viewing
experience by reducing rebuild frequency while accelerating scroll
position recovery.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 19:48:57 -05:00
3863242ba9 fix(ui): Enable word wrapping for batch settings labels
Issue:
- User reported batch settings text being cut off
- "Settings persist across videos. Change them anytime to affect all sub"
- Text truncated instead of wrapping to next line
- Cache directory hint also had truncation issues

Root Cause:
- settingsInfoLabel didn't have TextWrapWord enabled
- cacheDirHint had TextWrapWord but wasn't in a sized container
- Labels in VBox need padded containers for wrapping to work properly

Solution:
- Enabled TextWrapWord on settingsInfoLabel
- Wrapped both labels in container.NewPadded() containers:
  * settingsInfoContainer: "Settings persist across videos..." text
  * cacheDirHintContainer: "Use an SSD for best performance..." text
- Replaced direct label usage with containers in settingsContent VBox

Affected Labels:
- settingsInfoLabel: Batch settings persistence explanation
- cacheDirHint: Cache/temp directory usage guidance

Implementation:
- Added TextWrapWord to settingsInfoLabel
- Created padded containers for both labels
- Updated settingsContent VBox to use containers instead of labels
- Consistent with fix from commit 1051329

Impact:
- Batch settings text now wraps properly
- "Change them anytime to affect all subsequent videos" fully visible
- Better readability in narrow windows
- No more truncated guidance text

Files Changed:
- main.go: Batch settings label wrapping

Reported-by: User (screenshot showing batch settings truncation)
Related: Commit 1051329 (hint label wrapping fix)
Tested: Build successful (v0.1.0-dev20)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-28 19:43:55 -05:00
3 changed files with 271 additions and 67 deletions

39
DONE.md
View File

@ -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**

View File

@ -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
View File

@ -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