diff --git a/author_module.go b/author_module.go index bde4dd5..4aafb98 100644 --- a/author_module.go +++ b/author_module.go @@ -6,6 +6,7 @@ import ( "context" "encoding/json" "encoding/xml" + "errors" "fmt" "io" "os" @@ -28,11 +29,106 @@ import ( "git.leaktechnologies.dev/stu/VideoTools/internal/utils" ) +type authorConfig struct { + OutputType string `json:"outputType"` + Region string `json:"region"` + AspectRatio string `json:"aspectRatio"` + DiscSize string `json:"discSize"` + Title string `json:"title"` + CreateMenu bool `json:"createMenu"` + TreatAsChapters bool `json:"treatAsChapters"` + SceneThreshold float64 `json:"sceneThreshold"` +} + +func defaultAuthorConfig() authorConfig { + return authorConfig{ + OutputType: "dvd", + Region: "AUTO", + AspectRatio: "AUTO", + DiscSize: "DVD5", + Title: "", + CreateMenu: false, + TreatAsChapters: false, + SceneThreshold: 0.3, + } +} + +func loadPersistedAuthorConfig() (authorConfig, error) { + var cfg authorConfig + path := moduleConfigPath("author") + data, err := os.ReadFile(path) + if err != nil { + return cfg, err + } + if err := json.Unmarshal(data, &cfg); err != nil { + return cfg, err + } + if cfg.OutputType == "" { + cfg.OutputType = "dvd" + } + if cfg.Region == "" { + cfg.Region = "AUTO" + } + if cfg.AspectRatio == "" { + cfg.AspectRatio = "AUTO" + } + if cfg.DiscSize == "" { + cfg.DiscSize = "DVD5" + } + if cfg.SceneThreshold <= 0 { + cfg.SceneThreshold = 0.3 + } + return cfg, nil +} + +func savePersistedAuthorConfig(cfg authorConfig) error { + path := moduleConfigPath("author") + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} + +func (s *appState) applyAuthorConfig(cfg authorConfig) { + s.authorOutputType = cfg.OutputType + s.authorRegion = cfg.Region + s.authorAspectRatio = cfg.AspectRatio + s.authorDiscSize = cfg.DiscSize + s.authorTitle = cfg.Title + s.authorCreateMenu = cfg.CreateMenu + s.authorTreatAsChapters = cfg.TreatAsChapters + s.authorSceneThreshold = cfg.SceneThreshold +} + +func (s *appState) persistAuthorConfig() { + cfg := authorConfig{ + OutputType: s.authorOutputType, + Region: s.authorRegion, + AspectRatio: s.authorAspectRatio, + DiscSize: s.authorDiscSize, + Title: s.authorTitle, + CreateMenu: s.authorCreateMenu, + TreatAsChapters: s.authorTreatAsChapters, + SceneThreshold: s.authorSceneThreshold, + } + if err := savePersistedAuthorConfig(cfg); err != nil { + logging.Debug(logging.CatSystem, "failed to persist author config: %v", err) + } +} + func buildAuthorView(state *appState) fyne.CanvasObject { state.stopPreview() state.lastModule = state.active state.active = "author" + if cfg, err := loadPersistedAuthorConfig(); err == nil { + state.applyAuthorConfig(cfg) + } + if state.authorOutputType == "" { state.authorOutputType = "dvd" } @@ -178,6 +274,7 @@ func buildVideoClipsTab(state *appState) fyne.CanvasObject { state.authorChapters = nil } state.updateAuthorSummary() + state.persistAuthorConfig() if state.authorChaptersRefresh != nil { state.authorChaptersRefresh() } @@ -285,6 +382,7 @@ func buildChaptersTab(state *appState) fyne.CanvasObject { thresholdSlider.OnChanged = func(v float64) { state.authorSceneThreshold = v thresholdLabel.SetText(fmt.Sprintf("Detection Sensitivity: %.2f", v)) + state.persistAuthorConfig() } detectBtn := widget.NewButton("Detect Scenes", func() { @@ -472,6 +570,7 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject { state.authorOutputType = "iso" } state.updateAuthorSummary() + state.persistAuthorConfig() }) if state.authorOutputType == "iso" { outputType.SetSelected("ISO Image") @@ -482,6 +581,7 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject { regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"}, func(value string) { state.authorRegion = value state.updateAuthorSummary() + state.persistAuthorConfig() }) if state.authorRegion == "" { regionSelect.SetSelected("AUTO") @@ -492,6 +592,7 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject { aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"}, func(value string) { state.authorAspectRatio = value state.updateAuthorSummary() + state.persistAuthorConfig() }) if state.authorAspectRatio == "" { aspectSelect.SetSelected("AUTO") @@ -505,17 +606,20 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject { titleEntry.OnChanged = func(value string) { state.authorTitle = value state.updateAuthorSummary() + state.persistAuthorConfig() } createMenuCheck := widget.NewCheck("Create DVD Menu", func(checked bool) { state.authorCreateMenu = checked state.updateAuthorSummary() + state.persistAuthorConfig() }) createMenuCheck.SetChecked(state.authorCreateMenu) discSizeSelect := widget.NewSelect([]string{"DVD5", "DVD9"}, func(value string) { state.authorDiscSize = value state.updateAuthorSummary() + state.persistAuthorConfig() }) if state.authorDiscSize == "" { discSizeSelect.SetSelected("DVD5") @@ -523,6 +627,72 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject { discSizeSelect.SetSelected(state.authorDiscSize) } + applyControls := func() { + if state.authorOutputType == "iso" { + outputType.SetSelected("ISO Image") + } else { + outputType.SetSelected("DVD (VIDEO_TS)") + } + if state.authorRegion == "" { + regionSelect.SetSelected("AUTO") + } else { + regionSelect.SetSelected(state.authorRegion) + } + if state.authorAspectRatio == "" { + aspectSelect.SetSelected("AUTO") + } else { + aspectSelect.SetSelected(state.authorAspectRatio) + } + if state.authorDiscSize == "" { + discSizeSelect.SetSelected("DVD5") + } else { + discSizeSelect.SetSelected(state.authorDiscSize) + } + titleEntry.SetText(state.authorTitle) + createMenuCheck.SetChecked(state.authorCreateMenu) + } + + loadCfgBtn := widget.NewButton("Load Config", func() { + cfg, err := loadPersistedAuthorConfig() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + dialog.ShowInformation("No Config", "No saved config found yet. It will save automatically after your first change.", state.window) + } else { + dialog.ShowError(fmt.Errorf("failed to load config: %w", err), state.window) + } + return + } + state.applyAuthorConfig(cfg) + applyControls() + state.updateAuthorSummary() + }) + + saveCfgBtn := widget.NewButton("Save Config", func() { + cfg := authorConfig{ + OutputType: state.authorOutputType, + Region: state.authorRegion, + AspectRatio: state.authorAspectRatio, + DiscSize: state.authorDiscSize, + Title: state.authorTitle, + CreateMenu: state.authorCreateMenu, + TreatAsChapters: state.authorTreatAsChapters, + SceneThreshold: state.authorSceneThreshold, + } + if err := savePersistedAuthorConfig(cfg); err != nil { + dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window) + return + } + dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", moduleConfigPath("author")), state.window) + }) + + resetBtn := widget.NewButton("Reset", func() { + cfg := defaultAuthorConfig() + state.applyAuthorConfig(cfg) + applyControls() + state.updateAuthorSummary() + state.persistAuthorConfig() + }) + info := widget.NewLabel("Requires: ffmpeg, dvdauthor, and mkisofs/genisoimage (for ISO).") info.Wrapping = fyne.TextWrapWord @@ -542,6 +712,8 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject { createMenuCheck, widget.NewSeparator(), info, + widget.NewSeparator(), + container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn), ) return container.NewPadded(controls) diff --git a/config_helpers.go b/config_helpers.go new file mode 100644 index 0000000..2fa6491 --- /dev/null +++ b/config_helpers.go @@ -0,0 +1,20 @@ +package main + +import ( + "os" + "path/filepath" +) + +func moduleConfigPath(name string) string { + configDir, err := os.UserConfigDir() + if err != nil || configDir == "" { + home := os.Getenv("HOME") + if home != "" { + configDir = filepath.Join(home, ".config") + } + } + if configDir == "" { + return name + ".json" + } + return filepath.Join(configDir, "VideoTools", name+".json") +} diff --git a/main.go b/main.go index 6223bc2..7213102 100644 --- a/main.go +++ b/main.go @@ -2957,6 +2957,12 @@ func (s *appState) showMergeView() { mergeColor := moduleColor("merge") + if cfg, err := loadPersistedMergeConfig(); err == nil { + s.applyMergeConfig(cfg) + } else if !errors.Is(err, os.ErrNotExist) { + logging.Debug(logging.CatSystem, "failed to load persisted merge config: %v", err) + } + if s.mergeFormat == "" { s.mergeFormat = "mkv-copy" } @@ -3130,11 +3136,13 @@ func (s *appState) showMergeView() { keepAllCheck := widget.NewCheck("Keep all audio/subtitle tracks", func(v bool) { s.mergeKeepAll = v + s.persistMergeConfig() }) keepAllCheck.SetChecked(s.mergeKeepAll) chapterCheck := widget.NewCheck("Create chapters from each clip", func(v bool) { s.mergeChapters = v + s.persistMergeConfig() }) chapterCheck.SetChecked(s.mergeChapters) @@ -3169,11 +3177,13 @@ func (s *appState) showMergeView() { // DVD-specific options dvdRegionSelect := widget.NewSelect([]string{"NTSC", "PAL"}, func(val string) { s.mergeDVDRegion = val + s.persistMergeConfig() }) dvdRegionSelect.SetSelected(s.mergeDVDRegion) dvdAspectSelect := widget.NewSelect([]string{"16:9", "4:3"}, func(val string) { s.mergeDVDAspect = val + s.persistMergeConfig() }) dvdAspectSelect.SetSelected(s.mergeDVDAspect) @@ -3216,6 +3226,7 @@ func (s *appState) showMergeView() { // Update extension of existing path updateOutputExt() } + s.persistMergeConfig() }) for label, val := range formatMap { if val == s.mergeFormat { @@ -3234,11 +3245,13 @@ func (s *appState) showMergeView() { // Frame Rate controls frameRateSelect := widget.NewSelect([]string{"Source", "23.976", "24", "25", "29.97", "30", "50", "59.94", "60"}, func(val string) { s.mergeFrameRate = val + s.persistMergeConfig() }) frameRateSelect.SetSelected(s.mergeFrameRate) motionInterpCheck := widget.NewCheck("Use Motion Interpolation (slower, smoother)", func(checked bool) { s.mergeMotionInterpolation = checked + s.persistMergeConfig() }) motionInterpCheck.SetChecked(s.mergeMotionInterpolation) @@ -3284,6 +3297,66 @@ func (s *appState) showMergeView() { runNowBtn.Disable() } + applyMergeControls := func() { + for label, val := range formatMap { + if val == s.mergeFormat { + formatSelect.SetSelected(label) + break + } + } + keepAllCheck.SetChecked(s.mergeKeepAll) + chapterCheck.SetChecked(s.mergeChapters) + dvdRegionSelect.SetSelected(s.mergeDVDRegion) + dvdAspectSelect.SetSelected(s.mergeDVDAspect) + frameRateSelect.SetSelected(s.mergeFrameRate) + motionInterpCheck.SetChecked(s.mergeMotionInterpolation) + if s.mergeFormat == "dvd" { + dvdOptionsContainer.Show() + } else { + dvdOptionsContainer.Hide() + } + updateOutputExt() + } + + loadCfgBtn := widget.NewButton("Load Config", func() { + cfg, err := loadPersistedMergeConfig() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + dialog.ShowInformation("No Config", "No saved config found yet. It will save automatically after your first change.", s.window) + } else { + dialog.ShowError(fmt.Errorf("failed to load config: %w", err), s.window) + } + return + } + s.applyMergeConfig(cfg) + applyMergeControls() + }) + + saveCfgBtn := widget.NewButton("Save Config", func() { + cfg := mergeConfig{ + Format: s.mergeFormat, + KeepAllStreams: s.mergeKeepAll, + Chapters: s.mergeChapters, + CodecMode: s.mergeCodecMode, + DVDRegion: s.mergeDVDRegion, + DVDAspect: s.mergeDVDAspect, + FrameRate: s.mergeFrameRate, + MotionInterpolation: s.mergeMotionInterpolation, + } + if err := savePersistedMergeConfig(cfg); err != nil { + dialog.ShowError(fmt.Errorf("failed to save config: %w", err), s.window) + return + } + dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", moduleConfigPath("merge")), s.window) + }) + + resetBtn := widget.NewButton("Reset", func() { + cfg := defaultMergeConfig() + s.applyMergeConfig(cfg) + applyMergeControls() + s.persistMergeConfig() + }) + listScroll := container.NewVScroll(listBox) // Use border layout so the list expands to fill available vertical space @@ -3324,6 +3397,8 @@ func (s *appState) showMergeView() { widget.NewLabel("Output Path"), container.NewBorder(nil, nil, nil, browseOut, outputEntry), widget.NewSeparator(), + container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn), + widget.NewSeparator(), container.NewHBox(addQueueBtn, runNowBtn), ) diff --git a/merge_config.go b/merge_config.go new file mode 100644 index 0000000..b1b9811 --- /dev/null +++ b/merge_config.go @@ -0,0 +1,97 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + + "git.leaktechnologies.dev/stu/VideoTools/internal/logging" +) + +type mergeConfig struct { + Format string `json:"format"` + KeepAllStreams bool `json:"keepAllStreams"` + Chapters bool `json:"chapters"` + CodecMode string `json:"codecMode"` + DVDRegion string `json:"dvdRegion"` + DVDAspect string `json:"dvdAspect"` + FrameRate string `json:"frameRate"` + MotionInterpolation bool `json:"motionInterpolation"` +} + +func defaultMergeConfig() mergeConfig { + return mergeConfig{ + Format: "mkv-copy", + KeepAllStreams: false, + Chapters: true, + CodecMode: "", + DVDRegion: "NTSC", + DVDAspect: "16:9", + FrameRate: "Source", + MotionInterpolation: false, + } +} + +func loadPersistedMergeConfig() (mergeConfig, error) { + var cfg mergeConfig + path := moduleConfigPath("merge") + data, err := os.ReadFile(path) + if err != nil { + return cfg, err + } + if err := json.Unmarshal(data, &cfg); err != nil { + return cfg, err + } + if cfg.Format == "" { + cfg.Format = "mkv-copy" + } + if cfg.DVDRegion == "" { + cfg.DVDRegion = "NTSC" + } + if cfg.DVDAspect == "" { + cfg.DVDAspect = "16:9" + } + if cfg.FrameRate == "" { + cfg.FrameRate = "Source" + } + return cfg, nil +} + +func savePersistedMergeConfig(cfg mergeConfig) error { + path := moduleConfigPath("merge") + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} + +func (s *appState) applyMergeConfig(cfg mergeConfig) { + s.mergeFormat = cfg.Format + s.mergeKeepAll = cfg.KeepAllStreams + s.mergeChapters = cfg.Chapters + s.mergeCodecMode = cfg.CodecMode + s.mergeDVDRegion = cfg.DVDRegion + s.mergeDVDAspect = cfg.DVDAspect + s.mergeFrameRate = cfg.FrameRate + s.mergeMotionInterpolation = cfg.MotionInterpolation +} + +func (s *appState) persistMergeConfig() { + cfg := mergeConfig{ + Format: s.mergeFormat, + KeepAllStreams: s.mergeKeepAll, + Chapters: s.mergeChapters, + CodecMode: s.mergeCodecMode, + DVDRegion: s.mergeDVDRegion, + DVDAspect: s.mergeDVDAspect, + FrameRate: s.mergeFrameRate, + MotionInterpolation: s.mergeMotionInterpolation, + } + if err := savePersistedMergeConfig(cfg); err != nil { + logging.Debug(logging.CatSystem, "failed to persist merge config: %v", err) + } +} diff --git a/rip_module.go b/rip_module.go index 419fbbe..0034a25 100644 --- a/rip_module.go +++ b/rip_module.go @@ -3,6 +3,8 @@ package main import ( "bufio" "context" + "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -28,11 +30,68 @@ const ( ripFormatH264MP4 = "H.264 MP4 (CRF 18)" ) +type ripConfig struct { + Format string `json:"format"` +} + +func defaultRipConfig() ripConfig { + return ripConfig{ + Format: ripFormatLosslessMKV, + } +} + +func loadPersistedRipConfig() (ripConfig, error) { + var cfg ripConfig + path := moduleConfigPath("rip") + data, err := os.ReadFile(path) + if err != nil { + return cfg, err + } + if err := json.Unmarshal(data, &cfg); err != nil { + return cfg, err + } + if cfg.Format == "" { + cfg.Format = ripFormatLosslessMKV + } + return cfg, nil +} + +func savePersistedRipConfig(cfg ripConfig) error { + path := moduleConfigPath("rip") + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} + +func (s *appState) applyRipConfig(cfg ripConfig) { + s.ripFormat = cfg.Format +} + +func (s *appState) persistRipConfig() { + cfg := ripConfig{ + Format: s.ripFormat, + } + if err := savePersistedRipConfig(cfg); err != nil { + logging.Debug(logging.CatSystem, "failed to persist rip config: %v", err) + } +} + func (s *appState) showRipView() { s.stopPreview() s.lastModule = s.active s.active = "rip" + if cfg, err := loadPersistedRipConfig(); err == nil { + s.applyRipConfig(cfg) + } else if !errors.Is(err, os.ErrNotExist) { + logging.Debug(logging.CatSystem, "failed to load persisted rip config: %v", err) + } + if s.ripFormat == "" { s.ripFormat = ripFormatLosslessMKV } @@ -78,6 +137,7 @@ func buildRipView(state *appState) fyne.CanvasObject { state.ripFormat = val state.ripOutputPath = defaultRipOutputPath(state.ripSourcePath, state.ripFormat) outputEntry.SetText(state.ripOutputPath) + state.persistRipConfig() }) formatSelect.SetSelected(state.ripFormat) @@ -122,6 +182,45 @@ func buildRipView(state *appState) fyne.CanvasObject { }) runNowBtn.Importance = widget.HighImportance + applyControls := func() { + formatSelect.SetSelected(state.ripFormat) + outputEntry.SetText(state.ripOutputPath) + } + + loadCfgBtn := widget.NewButton("Load Config", func() { + cfg, err := loadPersistedRipConfig() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + dialog.ShowInformation("No Config", "No saved config found yet. It will save automatically after your first change.", state.window) + } else { + dialog.ShowError(fmt.Errorf("failed to load config: %w", err), state.window) + } + return + } + state.applyRipConfig(cfg) + state.ripOutputPath = defaultRipOutputPath(state.ripSourcePath, state.ripFormat) + applyControls() + }) + + saveCfgBtn := widget.NewButton("Save Config", func() { + cfg := ripConfig{ + Format: state.ripFormat, + } + if err := savePersistedRipConfig(cfg); err != nil { + dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window) + return + } + dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", moduleConfigPath("rip")), state.window) + }) + + resetBtn := widget.NewButton("Reset", func() { + cfg := defaultRipConfig() + state.applyRipConfig(cfg) + state.ripOutputPath = defaultRipOutputPath(state.ripSourcePath, state.ripFormat) + applyControls() + state.persistRipConfig() + }) + controls := container.NewVBox( widget.NewLabelWithStyle("Source", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), ui.NewDroppable(sourceEntry, func(items []fyne.URI) { @@ -139,6 +238,8 @@ func buildRipView(state *appState) fyne.CanvasObject { outputEntry, container.NewHBox(addQueueBtn, runNowBtn), widget.NewSeparator(), + container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn), + widget.NewSeparator(), widget.NewLabelWithStyle("Status", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), statusLabel, progressBar, diff --git a/subtitles_module.go b/subtitles_module.go index 5312508..e41081c 100644 --- a/subtitles_module.go +++ b/subtitles_module.go @@ -3,6 +3,8 @@ package main import ( "bufio" "bytes" + "encoding/json" + "errors" "fmt" "os" "os/exec" @@ -14,8 +16,10 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/canvas" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/widget" + "git.leaktechnologies.dev/stu/VideoTools/internal/logging" "git.leaktechnologies.dev/stu/VideoTools/internal/ui" "git.leaktechnologies.dev/stu/VideoTools/internal/utils" ) @@ -32,11 +36,80 @@ type subtitleCue struct { Text string } +type subtitlesConfig struct { + OutputMode string `json:"outputMode"` + ModelPath string `json:"modelPath"` + BackendPath string `json:"backendPath"` + BurnOutput string `json:"burnOutput"` +} + +func defaultSubtitlesConfig() subtitlesConfig { + return subtitlesConfig{ + OutputMode: subtitleModeExternal, + ModelPath: "", + BackendPath: "", + BurnOutput: "", + } +} + +func loadPersistedSubtitlesConfig() (subtitlesConfig, error) { + var cfg subtitlesConfig + path := moduleConfigPath("subtitles") + data, err := os.ReadFile(path) + if err != nil { + return cfg, err + } + if err := json.Unmarshal(data, &cfg); err != nil { + return cfg, err + } + if cfg.OutputMode == "" { + cfg.OutputMode = subtitleModeExternal + } + return cfg, nil +} + +func savePersistedSubtitlesConfig(cfg subtitlesConfig) error { + path := moduleConfigPath("subtitles") + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + return err + } + return os.WriteFile(path, data, 0o644) +} + +func (s *appState) applySubtitlesConfig(cfg subtitlesConfig) { + s.subtitleOutputMode = cfg.OutputMode + s.subtitleModelPath = cfg.ModelPath + s.subtitleBackendPath = cfg.BackendPath + s.subtitleBurnOutput = cfg.BurnOutput +} + +func (s *appState) persistSubtitlesConfig() { + cfg := subtitlesConfig{ + OutputMode: s.subtitleOutputMode, + ModelPath: s.subtitleModelPath, + BackendPath: s.subtitleBackendPath, + BurnOutput: s.subtitleBurnOutput, + } + if err := savePersistedSubtitlesConfig(cfg); err != nil { + logging.Debug(logging.CatSystem, "failed to persist subtitles config: %v", err) + } +} + func (s *appState) showSubtitlesView() { s.stopPreview() s.lastModule = s.active s.active = "subtitles" + if cfg, err := loadPersistedSubtitlesConfig(); err == nil { + s.applySubtitlesConfig(cfg) + } else if !errors.Is(err, os.ErrNotExist) { + logging.Debug(logging.CatSystem, "failed to load persisted subtitles config: %v", err) + } + if s.subtitleOutputMode == "" { s.subtitleOutputMode = subtitleModeExternal } @@ -80,6 +153,7 @@ func buildSubtitlesView(state *appState) fyne.CanvasObject { modelEntry.SetText(state.subtitleModelPath) modelEntry.OnChanged = func(val string) { state.subtitleModelPath = strings.TrimSpace(val) + state.persistSubtitlesConfig() } backendEntry := widget.NewEntry() @@ -87,6 +161,7 @@ func buildSubtitlesView(state *appState) fyne.CanvasObject { backendEntry.SetText(state.subtitleBackendPath) backendEntry.OnChanged = func(val string) { state.subtitleBackendPath = strings.TrimSpace(val) + state.persistSubtitlesConfig() } outputEntry := widget.NewEntry() @@ -94,6 +169,7 @@ func buildSubtitlesView(state *appState) fyne.CanvasObject { outputEntry.SetText(state.subtitleBurnOutput) outputEntry.OnChanged = func(val string) { state.subtitleBurnOutput = strings.TrimSpace(val) + state.persistSubtitlesConfig() } statusLabel := widget.NewLabel("") @@ -255,6 +331,7 @@ func buildSubtitlesView(state *appState) fyne.CanvasObject { []string{subtitleModeExternal, subtitleModeEmbed, subtitleModeBurn}, func(val string) { state.subtitleOutputMode = val + state.persistSubtitlesConfig() }, ) outputModeSelect.SetSelected(state.subtitleOutputMode) @@ -264,6 +341,48 @@ func buildSubtitlesView(state *appState) fyne.CanvasObject { }) applyBtn.Importance = widget.HighImportance + applyControls := func() { + outputModeSelect.SetSelected(state.subtitleOutputMode) + backendEntry.SetText(state.subtitleBackendPath) + modelEntry.SetText(state.subtitleModelPath) + outputEntry.SetText(state.subtitleBurnOutput) + } + + loadCfgBtn := widget.NewButton("Load Config", func() { + cfg, err := loadPersistedSubtitlesConfig() + if err != nil { + if errors.Is(err, os.ErrNotExist) { + dialog.ShowInformation("No Config", "No saved config found yet. It will save automatically after your first change.", state.window) + } else { + dialog.ShowError(fmt.Errorf("failed to load config: %w", err), state.window) + } + return + } + state.applySubtitlesConfig(cfg) + applyControls() + }) + + saveCfgBtn := widget.NewButton("Save Config", func() { + cfg := subtitlesConfig{ + OutputMode: state.subtitleOutputMode, + ModelPath: state.subtitleModelPath, + BackendPath: state.subtitleBackendPath, + BurnOutput: state.subtitleBurnOutput, + } + if err := savePersistedSubtitlesConfig(cfg); err != nil { + dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window) + return + } + dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", moduleConfigPath("subtitles")), state.window) + }) + + resetBtn := widget.NewButton("Reset", func() { + cfg := defaultSubtitlesConfig() + state.applySubtitlesConfig(cfg) + applyControls() + state.persistSubtitlesConfig() + }) + left := container.NewVBox( widget.NewLabelWithStyle("Sources", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), ui.NewDroppable(videoEntry, handleDrop), @@ -278,6 +397,8 @@ func buildSubtitlesView(state *appState) fyne.CanvasObject { applyBtn, widget.NewLabelWithStyle("Status", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), statusLabel, + widget.NewSeparator(), + container.NewHBox(resetBtn, loadCfgBtn, saveCfgBtn), ) right := container.NewBorder(