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
This commit is contained in:
parent
85151a7df7
commit
e34bb34bf1
125
main.go
125
main.go
|
|
@ -866,7 +866,8 @@ type appState struct {
|
||||||
// Merge state
|
// Merge state
|
||||||
mergeClips []mergeClip
|
mergeClips []mergeClip
|
||||||
mergeFormat string
|
mergeFormat string
|
||||||
mergeOutput string
|
mergeOutputDir string
|
||||||
|
mergeOutputFilename string
|
||||||
mergeKeepAll bool
|
mergeKeepAll bool
|
||||||
mergeCodecMode string
|
mergeCodecMode string
|
||||||
mergeChapters bool
|
mergeChapters bool
|
||||||
|
|
@ -2769,9 +2770,11 @@ func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) {
|
||||||
}
|
}
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
s.mergeClips = append(s.mergeClips, clips...)
|
s.mergeClips = append(s.mergeClips, clips...)
|
||||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutput) == "" {
|
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputDir) == "" {
|
||||||
first := filepath.Dir(s.mergeClips[0].Path)
|
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||||
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
}
|
||||||
|
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputFilename) == "" {
|
||||||
|
s.mergeOutputFilename = "merged.mkv"
|
||||||
}
|
}
|
||||||
s.showMergeView()
|
s.showMergeView()
|
||||||
}, false)
|
}, false)
|
||||||
|
|
@ -3164,9 +3167,11 @@ func (s *appState) showMergeView() {
|
||||||
Duration: src.Duration,
|
Duration: src.Duration,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(s.mergeClips) >= 2 && s.mergeOutput == "" {
|
if len(s.mergeClips) >= 2 && s.mergeOutputDir == "" {
|
||||||
first := filepath.Dir(s.mergeClips[0].Path)
|
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||||
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
}
|
||||||
|
if len(s.mergeClips) >= 2 && s.mergeOutputFilename == "" {
|
||||||
|
s.mergeOutputFilename = "merged.mkv"
|
||||||
}
|
}
|
||||||
buildList()
|
buildList()
|
||||||
}
|
}
|
||||||
|
|
@ -3232,31 +3237,40 @@ func (s *appState) showMergeView() {
|
||||||
})
|
})
|
||||||
chapterCheck.SetChecked(s.mergeChapters)
|
chapterCheck.SetChecked(s.mergeChapters)
|
||||||
|
|
||||||
// Create output entry widget first so it can be referenced in callbacks
|
// Create output entry widgets first so they can be referenced in callbacks
|
||||||
outputEntry := widget.NewEntry()
|
outputDirEntry := widget.NewEntry()
|
||||||
outputEntry.SetPlaceHolder("merged output path")
|
outputDirEntry.SetPlaceHolder("Output folder path")
|
||||||
outputEntry.SetText(s.mergeOutput)
|
outputDirEntry.SetText(s.mergeOutputDir)
|
||||||
outputEntry.OnChanged = func(val string) {
|
outputDirEntry.OnChanged = func(val string) {
|
||||||
s.mergeOutput = val
|
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() {
|
clearBtn := widget.NewButton("Clear", func() {
|
||||||
s.mergeClips = nil
|
s.mergeClips = nil
|
||||||
s.mergeOutput = ""
|
s.mergeOutputDir = ""
|
||||||
outputEntry.SetText("")
|
s.mergeOutputFilename = ""
|
||||||
|
outputDirEntry.SetText("")
|
||||||
|
outputFilenameEntry.SetText("")
|
||||||
buildList()
|
buildList()
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper to update output path extension (requires outputEntry to exist)
|
// Helper to update output filename extension (requires outputFilenameEntry to exist)
|
||||||
updateOutputExt := func() {
|
updateOutputExt := func() {
|
||||||
if s.mergeOutput == "" {
|
if s.mergeOutputFilename == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
currentExt := filepath.Ext(s.mergeOutput)
|
currentExt := filepath.Ext(s.mergeOutputFilename)
|
||||||
correctExt := getExtForFormat(s.mergeFormat)
|
correctExt := getExtForFormat(s.mergeFormat)
|
||||||
if currentExt != correctExt {
|
if currentExt != correctExt {
|
||||||
s.mergeOutput = strings.TrimSuffix(s.mergeOutput, currentExt) + correctExt
|
s.mergeOutputFilename = strings.TrimSuffix(s.mergeOutputFilename, currentExt) + correctExt
|
||||||
outputEntry.SetText(s.mergeOutput)
|
outputFilenameEntry.SetText(s.mergeOutputFilename)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3294,9 +3308,14 @@ func (s *appState) showMergeView() {
|
||||||
dvdOptionsContainer.Hide()
|
dvdOptionsContainer.Hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default output path if not set
|
// Set default output directory if not set
|
||||||
if s.mergeOutput == "" && len(s.mergeClips) > 0 {
|
if s.mergeOutputDir == "" && len(s.mergeClips) > 0 {
|
||||||
dir := filepath.Dir(s.mergeClips[0].Path)
|
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)
|
ext := getExtForFormat(s.mergeFormat)
|
||||||
basename := "merged"
|
basename := "merged"
|
||||||
if strings.HasPrefix(s.mergeFormat, "dvd") || s.mergeFormat == "dvd" {
|
if strings.HasPrefix(s.mergeFormat, "dvd") || s.mergeFormat == "dvd" {
|
||||||
|
|
@ -3306,10 +3325,10 @@ func (s *appState) showMergeView() {
|
||||||
} else if s.mergeFormat == "mkv-lossless" {
|
} else if s.mergeFormat == "mkv-lossless" {
|
||||||
basename = "merged-lossless"
|
basename = "merged-lossless"
|
||||||
}
|
}
|
||||||
s.mergeOutput = filepath.Join(dir, basename+ext)
|
s.mergeOutputFilename = basename + ext
|
||||||
outputEntry.SetText(s.mergeOutput)
|
outputFilenameEntry.SetText(s.mergeOutputFilename)
|
||||||
} else {
|
} else {
|
||||||
// Update extension of existing path
|
// Update extension of existing filename
|
||||||
updateOutputExt()
|
updateOutputExt()
|
||||||
}
|
}
|
||||||
s.persistMergeConfig()
|
s.persistMergeConfig()
|
||||||
|
|
@ -3347,14 +3366,13 @@ func (s *appState) showMergeView() {
|
||||||
motionInterpCheck,
|
motionInterpCheck,
|
||||||
)
|
)
|
||||||
|
|
||||||
browseOut := widget.NewButton("Browse", func() {
|
browseDirBtn := widget.NewButton("Browse Folder", func() {
|
||||||
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
|
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
|
||||||
if err != nil || writer == nil {
|
if err != nil || uri == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
s.mergeOutput = writer.URI().Path()
|
s.mergeOutputDir = uri.Path()
|
||||||
outputEntry.SetText(s.mergeOutput)
|
outputDirEntry.SetText(s.mergeOutputDir)
|
||||||
writer.Close()
|
|
||||||
}, s.window)
|
}, s.window)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -3480,8 +3498,10 @@ func (s *appState) showMergeView() {
|
||||||
keepAllCheck,
|
keepAllCheck,
|
||||||
chapterCheck,
|
chapterCheck,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
widget.NewLabel("Output Path"),
|
widget.NewLabel("Output Folder"),
|
||||||
container.NewBorder(nil, nil, nil, browseOut, outputEntry),
|
container.NewBorder(nil, nil, nil, browseDirBtn, outputDirEntry),
|
||||||
|
widget.NewLabel("Output Filename"),
|
||||||
|
outputFilenameEntry,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn),
|
container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn),
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
|
|
@ -3499,13 +3519,17 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
||||||
if len(s.mergeClips) < 2 {
|
if len(s.mergeClips) < 2 {
|
||||||
return fmt.Errorf("add at least two clips")
|
return fmt.Errorf("add at least two clips")
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(s.mergeOutput) == "" {
|
|
||||||
firstDir := filepath.Dir(s.mergeClips[0].Path)
|
// Set defaults if not specified
|
||||||
s.mergeOutput = filepath.Join(firstDir, "merged.mkv")
|
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
|
// Ensure output filename has correct extension for selected format
|
||||||
currentExt := filepath.Ext(s.mergeOutput)
|
currentExt := filepath.Ext(s.mergeOutputFilename)
|
||||||
var correctExt string
|
var correctExt string
|
||||||
switch {
|
switch {
|
||||||
case strings.HasPrefix(s.mergeFormat, "dvd"):
|
case strings.HasPrefix(s.mergeFormat, "dvd"):
|
||||||
|
|
@ -3524,10 +3548,13 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
||||||
|
|
||||||
// Auto-fix extension if missing or wrong
|
// Auto-fix extension if missing or wrong
|
||||||
if currentExt == "" {
|
if currentExt == "" {
|
||||||
s.mergeOutput += correctExt
|
s.mergeOutputFilename += correctExt
|
||||||
} else if currentExt != 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))
|
clips := make([]map[string]interface{}, 0, len(s.mergeClips))
|
||||||
for _, c := range s.mergeClips {
|
for _, c := range s.mergeClips {
|
||||||
name := c.Chapter
|
name := c.Chapter
|
||||||
|
|
@ -3547,7 +3574,7 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
||||||
"keepAllStreams": s.mergeKeepAll,
|
"keepAllStreams": s.mergeKeepAll,
|
||||||
"chapters": s.mergeChapters,
|
"chapters": s.mergeChapters,
|
||||||
"codecMode": s.mergeCodecMode,
|
"codecMode": s.mergeCodecMode,
|
||||||
"outputPath": s.mergeOutput,
|
"outputPath": mergeOutput,
|
||||||
"dvdRegion": s.mergeDVDRegion,
|
"dvdRegion": s.mergeDVDRegion,
|
||||||
"dvdAspect": s.mergeDVDAspect,
|
"dvdAspect": s.mergeDVDAspect,
|
||||||
"frameRate": s.mergeFrameRate,
|
"frameRate": s.mergeFrameRate,
|
||||||
|
|
@ -3557,9 +3584,9 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
||||||
job := &queue.Job{
|
job := &queue.Job{
|
||||||
Type: queue.JobTypeMerge,
|
Type: queue.JobTypeMerge,
|
||||||
Title: fmt.Sprintf("Merge %d clips", len(clips)),
|
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),
|
InputFile: clips[0]["path"].(string),
|
||||||
OutputFile: s.mergeOutput,
|
OutputFile: mergeOutput,
|
||||||
Config: config,
|
Config: config,
|
||||||
}
|
}
|
||||||
s.jobQueue.Add(job)
|
s.jobQueue.Add(job)
|
||||||
|
|
@ -10621,10 +10648,12 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
||||||
Duration: src.Duration,
|
Duration: src.Duration,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Set default output path if not set and we have at least 2 clips
|
// Set default output dir and filename if not set and we have at least 2 clips
|
||||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutput) == "" {
|
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputDir) == "" {
|
||||||
first := filepath.Dir(s.mergeClips[0].Path)
|
s.mergeOutputDir = filepath.Dir(s.mergeClips[0].Path)
|
||||||
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
}
|
||||||
|
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutputFilename) == "" {
|
||||||
|
s.mergeOutputFilename = "merged.mkv"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the merge view to show the new clips
|
// Refresh the merge view to show the new clips
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user