Add persistent configs for author/subtitles/merge/rip

This commit is contained in:
Stu Leak 2025-12-24 15:39:22 -05:00
parent 9237bae4ff
commit 7226da0970
6 changed files with 586 additions and 0 deletions

View File

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

20
config_helpers.go Normal file
View File

@ -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")
}

75
main.go
View File

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

97
merge_config.go Normal file
View File

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

View File

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

View File

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