Add persistent configs for author/subtitles/merge/rip
This commit is contained in:
parent
9237bae4ff
commit
7226da0970
172
author_module.go
172
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)
|
||||
|
|
|
|||
20
config_helpers.go
Normal file
20
config_helpers.go
Normal 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
75
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),
|
||||
)
|
||||
|
||||
|
|
|
|||
97
merge_config.go
Normal file
97
merge_config.go
Normal 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)
|
||||
}
|
||||
}
|
||||
101
rip_module.go
101
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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user