feat: Implement DVD menu templating system
- Refactor author_menu.go to support multiple menu templates - Add Simple, Dark, and Poster menu templates - Add UI for selecting menu template and background image
This commit is contained in:
parent
1a9e0e0d05
commit
7dc5be6ecc
16
DONE.md
16
DONE.md
|
|
@ -1,7 +1,23 @@
|
||||||
# VideoTools - Completed Features
|
# VideoTools - Completed Features
|
||||||
|
|
||||||
|
## Version 0.1.0-dev24 (2026-01-06) - DVD Menu Templating System
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- ✅ **DVD Menu Templating System**
|
||||||
|
- Refactored `author_menu.go` to support multiple, selectable menu templates.
|
||||||
|
- Implemented a `MenuTemplate` interface for easy extensibility.
|
||||||
|
- Created three initial menu templates:
|
||||||
|
- **Simple**: The default, clean menu style.
|
||||||
|
- **Dark**: A dark-themed menu for a more cinematic feel.
|
||||||
|
- **Poster**: A template that uses a user-provided image as the background.
|
||||||
|
- ✅ **Menu Customization UI**
|
||||||
|
- Added a "Menu Template" dropdown to the authoring settings tab.
|
||||||
|
- Added a "Select Background Image" button that appears when the "Poster" template is selected.
|
||||||
|
- User's menu template and background image choices are persisted in the configuration.
|
||||||
|
|
||||||
## Version 0.1.0-dev23 (2026-01-04) - UI Cleanup & About Dialog
|
## Version 0.1.0-dev23 (2026-01-04) - UI Cleanup & About Dialog
|
||||||
|
|
||||||
|
|
||||||
### UI/UX
|
### UI/UX
|
||||||
- ✅ **Colored select polish** - one-click dropdown, left accent bar, softer blue-grey background, rounded corners, larger text
|
- ✅ **Colored select polish** - one-click dropdown, left accent bar, softer blue-grey background, rounded corners, larger text
|
||||||
- ✅ **Panel input styling** - input and panel backgrounds aligned to dropdown tone
|
- ✅ **Panel input styling** - input and panel backgrounds aligned to dropdown tone
|
||||||
|
|
|
||||||
222
author_menu.go
222
author_menu.go
|
|
@ -21,7 +21,22 @@ type dvdMenuButton struct {
|
||||||
Y1 int
|
Y1 int
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDVDMenuAssets(ctx context.Context, workDir, title, region, aspect string, chapters []authorChapter, logFn func(string)) (string, []dvdMenuButton, error) {
|
// MenuTemplate defines the interface for a DVD menu generator.
|
||||||
|
type MenuTemplate interface {
|
||||||
|
Generate(ctx context.Context, workDir, title, region, aspect string, chapters []authorChapter, backgroundImage string, logFn func(string)) (string, []dvdMenuButton, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var menuTemplates = map[string]MenuTemplate{
|
||||||
|
"Simple": &SimpleMenu{},
|
||||||
|
"Dark": &DarkMenu{},
|
||||||
|
"Poster": &PosterMenu{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SimpleMenu is a basic menu template.
|
||||||
|
type SimpleMenu struct{}
|
||||||
|
|
||||||
|
// Generate creates a simple DVD menu.
|
||||||
|
func (t *SimpleMenu) Generate(ctx context.Context, workDir, title, region, aspect string, chapters []authorChapter, backgroundImage string, logFn func(string)) (string, []dvdMenuButton, error) {
|
||||||
width, height := dvdMenuDimensions(region)
|
width, height := dvdMenuDimensions(region)
|
||||||
buttons := buildDVDMenuButtons(chapters, width, height)
|
buttons := buildDVDMenuButtons(chapters, width, height)
|
||||||
if len(buttons) == 0 {
|
if len(buttons) == 0 {
|
||||||
|
|
@ -29,6 +44,9 @@ func buildDVDMenuAssets(ctx context.Context, workDir, title, region, aspect stri
|
||||||
}
|
}
|
||||||
|
|
||||||
bgPath := filepath.Join(workDir, "menu_bg.png")
|
bgPath := filepath.Join(workDir, "menu_bg.png")
|
||||||
|
if backgroundImage != "" {
|
||||||
|
bgPath = backgroundImage
|
||||||
|
}
|
||||||
overlayPath := filepath.Join(workDir, "menu_overlay.png")
|
overlayPath := filepath.Join(workDir, "menu_overlay.png")
|
||||||
highlightPath := filepath.Join(workDir, "menu_highlight.png")
|
highlightPath := filepath.Join(workDir, "menu_highlight.png")
|
||||||
selectPath := filepath.Join(workDir, "menu_select.png")
|
selectPath := filepath.Join(workDir, "menu_select.png")
|
||||||
|
|
@ -37,12 +55,15 @@ func buildDVDMenuAssets(ctx context.Context, workDir, title, region, aspect stri
|
||||||
spumuxXML := filepath.Join(workDir, "menu_spu.xml")
|
spumuxXML := filepath.Join(workDir, "menu_spu.xml")
|
||||||
|
|
||||||
if logFn != nil {
|
if logFn != nil {
|
||||||
logFn("Building DVD menu assets...")
|
logFn("Building DVD menu assets with SimpleMenu template...")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := buildMenuBackground(ctx, bgPath, title, buttons, width, height); err != nil {
|
if backgroundImage == "" {
|
||||||
return "", nil, err
|
if err := buildMenuBackground(ctx, bgPath, title, buttons, width, height); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := buildMenuOverlays(ctx, overlayPath, highlightPath, selectPath, buttons, width, height); err != nil {
|
if err := buildMenuOverlays(ctx, overlayPath, highlightPath, selectPath, buttons, width, height); err != nil {
|
||||||
return "", nil, err
|
return "", nil, err
|
||||||
}
|
}
|
||||||
|
|
@ -61,6 +82,111 @@ func buildDVDMenuAssets(ctx context.Context, workDir, title, region, aspect stri
|
||||||
return menuSpu, buttons, nil
|
return menuSpu, buttons, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DarkMenu is a dark-themed menu template.
|
||||||
|
type DarkMenu struct{}
|
||||||
|
|
||||||
|
// Generate creates a dark-themed DVD menu.
|
||||||
|
func (t *DarkMenu) Generate(ctx context.Context, workDir, title, region, aspect string, chapters []authorChapter, backgroundImage string, logFn func(string)) (string, []dvdMenuButton, error) {
|
||||||
|
width, height := dvdMenuDimensions(region)
|
||||||
|
buttons := buildDVDMenuButtons(chapters, width, height)
|
||||||
|
if len(buttons) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bgPath := filepath.Join(workDir, "menu_bg.png")
|
||||||
|
if backgroundImage != "" {
|
||||||
|
bgPath = backgroundImage
|
||||||
|
}
|
||||||
|
overlayPath := filepath.Join(workDir, "menu_overlay.png")
|
||||||
|
highlightPath := filepath.Join(workDir, "menu_highlight.png")
|
||||||
|
selectPath := filepath.Join(workDir, "menu_select.png")
|
||||||
|
menuMpg := filepath.Join(workDir, "menu.mpg")
|
||||||
|
menuSpu := filepath.Join(workDir, "menu_spu.mpg")
|
||||||
|
spumuxXML := filepath.Join(workDir, "menu_spu.xml")
|
||||||
|
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Building DVD menu assets with DarkMenu template...")
|
||||||
|
}
|
||||||
|
|
||||||
|
if backgroundImage == "" {
|
||||||
|
if err := buildDarkMenuBackground(ctx, bgPath, title, buttons, width, height); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMenuOverlays(ctx, overlayPath, highlightPath, selectPath, buttons, width, height); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := buildMenuMPEG(ctx, bgPath, menuMpg, region, aspect); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := writeSpumuxXML(spumuxXML, overlayPath, highlightPath, selectPath, buttons); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := runSpumux(ctx, spumuxXML, menuMpg, menuSpu, logFn); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if logFn != nil {
|
||||||
|
logFn(fmt.Sprintf("DVD menu created: %s", filepath.Base(menuSpu)))
|
||||||
|
}
|
||||||
|
return menuSpu, buttons, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PosterMenu is a template that uses a poster image as a background.
|
||||||
|
type PosterMenu struct{}
|
||||||
|
|
||||||
|
// Generate creates a poster-themed DVD menu.
|
||||||
|
func (t *PosterMenu) Generate(ctx context.Context, workDir, title, region, aspect string, chapters []authorChapter, backgroundImage string, logFn func(string)) (string, []dvdMenuButton, error) {
|
||||||
|
width, height := dvdMenuDimensions(region)
|
||||||
|
buttons := buildDVDMenuButtons(chapters, width, height)
|
||||||
|
if len(buttons) == 0 {
|
||||||
|
return "", nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
bgPath := filepath.Join(workDir, "menu_bg.png")
|
||||||
|
if backgroundImage == "" {
|
||||||
|
return "", nil, fmt.Errorf("poster menu requires a background image")
|
||||||
|
}
|
||||||
|
overlayPath := filepath.Join(workDir, "menu_overlay.png")
|
||||||
|
highlightPath := filepath.Join(workDir, "menu_highlight.png")
|
||||||
|
selectPath := filepath.Join(workDir, "menu_select.png")
|
||||||
|
menuMpg := filepath.Join(workDir, "menu.mpg")
|
||||||
|
menuSpu := filepath.Join(workDir, "menu_spu.mpg")
|
||||||
|
spumuxXML := filepath.Join(workDir, "menu_spu.xml")
|
||||||
|
|
||||||
|
if logFn != nil {
|
||||||
|
logFn("Building DVD menu assets with PosterMenu template...")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildPosterMenuBackground(ctx, bgPath, title, buttons, width, height, backgroundImage); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := buildMenuOverlays(ctx, overlayPath, highlightPath, selectPath, buttons, width, height); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := buildMenuMPEG(ctx, bgPath, menuMpg, region, aspect); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := writeSpumuxXML(spumuxXML, overlayPath, highlightPath, selectPath, buttons); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := runSpumux(ctx, spumuxXML, menuMpg, menuSpu, logFn); err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if logFn != nil {
|
||||||
|
logFn(fmt.Sprintf("DVD menu created: %s", filepath.Base(menuSpu)))
|
||||||
|
}
|
||||||
|
return menuSpu, buttons, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDVDMenuAssets(ctx context.Context, workDir, title, region, aspect string, chapters []authorChapter, logFn func(string), template MenuTemplate, backgroundImage string) (string, []dvdMenuButton, error) {
|
||||||
|
if template == nil {
|
||||||
|
template = &SimpleMenu{}
|
||||||
|
}
|
||||||
|
return template.Generate(ctx, workDir, title, region, aspect, chapters, backgroundImage, logFn)
|
||||||
|
}
|
||||||
|
|
||||||
func dvdMenuDimensions(region string) (int, int) {
|
func dvdMenuDimensions(region string) (int, int) {
|
||||||
if strings.ToLower(region) == "pal" {
|
if strings.ToLower(region) == "pal" {
|
||||||
return 720, 576
|
return 720, 576
|
||||||
|
|
@ -150,6 +276,81 @@ func buildMenuBackground(ctx context.Context, outputPath, title string, buttons
|
||||||
return runCommandWithLogger(ctx, utils.GetFFmpegPath(), args, nil)
|
return runCommandWithLogger(ctx, utils.GetFFmpegPath(), args, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildDarkMenuBackground(ctx context.Context, outputPath, title string, buttons []dvdMenuButton, width, height int) error {
|
||||||
|
logoPath := findVTLogoPath()
|
||||||
|
if logoPath == "" {
|
||||||
|
return fmt.Errorf("VT logo not found for menu rendering")
|
||||||
|
}
|
||||||
|
|
||||||
|
safeTitle := utils.ShortenMiddle(strings.TrimSpace(title), 40)
|
||||||
|
if safeTitle == "" {
|
||||||
|
safeTitle = "DVD Menu"
|
||||||
|
}
|
||||||
|
|
||||||
|
bgColor := "0x000000"
|
||||||
|
headerColor := "0x111111"
|
||||||
|
textColor := "white"
|
||||||
|
accentColor := "0xeeeeee"
|
||||||
|
|
||||||
|
filterParts := []string{
|
||||||
|
fmt.Sprintf("drawbox=x=0:y=0:w=%d:h=72:color=%s:t=fill", width, headerColor),
|
||||||
|
fmt.Sprintf("drawtext=font='DejaVu Sans Mono':fontcolor=%s:fontsize=28:x=36:y=20:text='%s'", textColor, escapeDrawtextText("VideoTools DVD")),
|
||||||
|
fmt.Sprintf("drawtext=font='DejaVu Sans Mono':fontcolor=%s:fontsize=18:x=36:y=80:text='%s'", textColor, escapeDrawtextText(safeTitle)),
|
||||||
|
fmt.Sprintf("drawbox=x=36:y=108:w=%d:h=2:color=%s:t=fill", width-72, accentColor),
|
||||||
|
fmt.Sprintf("drawtext=font='DejaVu Sans Mono':fontcolor=%s:fontsize=16:x=36:y=122:text='%s'", textColor, escapeDrawtextText("Select a title or chapter to play")),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, btn := range buttons {
|
||||||
|
label := escapeDrawtextText(btn.Label)
|
||||||
|
y := 184 + i*34
|
||||||
|
filterParts = append(filterParts, fmt.Sprintf("drawtext=font='DejaVu Sans Mono':fontcolor=%s:fontsize=20:x=110:y=%d:text='%s'", textColor, y, label))
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain := strings.Join(filterParts, ",")
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-y",
|
||||||
|
"-f", "lavfi",
|
||||||
|
"-i", fmt.Sprintf("color=c=%s:s=%dx%d", bgColor, width, height),
|
||||||
|
"-i", logoPath,
|
||||||
|
"-filter_complex", fmt.Sprintf("[0:v]%s[bg];[1:v]scale=72:-1[logo];[bg][logo]overlay=W-w-36:18", filterChain),
|
||||||
|
"-frames:v", "1",
|
||||||
|
outputPath,
|
||||||
|
}
|
||||||
|
return runCommandWithLogger(ctx, utils.GetFFmpegPath(), args, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPosterMenuBackground(ctx context.Context, outputPath, title string, buttons []dvdMenuButton, width, height int, backgroundImage string) error {
|
||||||
|
safeTitle := utils.ShortenMiddle(strings.TrimSpace(title), 40)
|
||||||
|
if safeTitle == "" {
|
||||||
|
safeTitle = "DVD Menu"
|
||||||
|
}
|
||||||
|
|
||||||
|
textColor := "white"
|
||||||
|
|
||||||
|
filterParts := []string{
|
||||||
|
fmt.Sprintf("drawtext=font='DejaVu Sans Mono':fontcolor=%s:fontsize=28:x=36:y=20:text='%s'", textColor, escapeDrawtextText("VideoTools DVD")),
|
||||||
|
fmt.Sprintf("drawtext=font='DejaVu Sans Mono':fontcolor=%s:fontsize=18:x=36:y=80:text='%s'", textColor, escapeDrawtextText(safeTitle)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, btn := range buttons {
|
||||||
|
label := escapeDrawtextText(btn.Label)
|
||||||
|
y := 184 + i*34
|
||||||
|
filterParts = append(filterParts, fmt.Sprintf("drawtext=font='DejaVu Sans Mono':fontcolor=%s:fontsize=20:x=110:y=%d:text='%s'", textColor, y, label))
|
||||||
|
}
|
||||||
|
|
||||||
|
filterChain := strings.Join(filterParts, ",")
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-y",
|
||||||
|
"-i", backgroundImage,
|
||||||
|
"-vf", fmt.Sprintf("scale=%d:%d,%s", width, height, filterChain),
|
||||||
|
"-frames:v", "1",
|
||||||
|
outputPath,
|
||||||
|
}
|
||||||
|
return runCommandWithLogger(ctx, utils.GetFFmpegPath(), args, nil)
|
||||||
|
}
|
||||||
|
|
||||||
func buildMenuOverlays(ctx context.Context, overlayPath, highlightPath, selectPath string, buttons []dvdMenuButton, width, height int) error {
|
func buildMenuOverlays(ctx context.Context, overlayPath, highlightPath, selectPath string, buttons []dvdMenuButton, width, height int) error {
|
||||||
if err := buildMenuOverlay(ctx, overlayPath, buttons, width, height, "0x000000@0.0"); err != nil {
|
if err := buildMenuOverlay(ctx, overlayPath, buttons, width, height, "0x000000@0.0"); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -214,7 +415,8 @@ func writeSpumuxXML(path, overlayPath, highlightPath, selectPath string, buttons
|
||||||
var b strings.Builder
|
var b strings.Builder
|
||||||
b.WriteString("<subpictures>\n")
|
b.WriteString("<subpictures>\n")
|
||||||
b.WriteString(" <stream>\n")
|
b.WriteString(" <stream>\n")
|
||||||
b.WriteString(fmt.Sprintf(" <spu start=\"00:00:00.00\" end=\"00:00:30.00\" image=\"%s\" highlight=\"%s\" select=\"%s\" force=\"yes\">\n",
|
b.WriteString(fmt.Sprintf(" <spu start=\"00:00:00.00\" end=\"00:00:30.00\" image=\"%s\" highlight=\"%s\" select=\"%s\" force=\"yes\">
|
||||||
|
",
|
||||||
escapeXMLAttr(overlayPath),
|
escapeXMLAttr(overlayPath),
|
||||||
escapeXMLAttr(highlightPath),
|
escapeXMLAttr(highlightPath),
|
||||||
escapeXMLAttr(selectPath),
|
escapeXMLAttr(selectPath),
|
||||||
|
|
@ -230,7 +432,7 @@ func writeSpumuxXML(path, overlayPath, highlightPath, selectPath string, buttons
|
||||||
}
|
}
|
||||||
|
|
||||||
func runSpumux(ctx context.Context, spumuxXML, inputMpg, outputMpg string, logFn func(string)) error {
|
func runSpumux(ctx context.Context, spumuxXML, inputMpg, outputMpg string, logFn func(string)) error {
|
||||||
args := []string{"-m", "dvd", spumuxXML}
|
args := []string{" -m", "dvd", spumuxXML}
|
||||||
if logFn != nil {
|
if logFn != nil {
|
||||||
logFn(fmt.Sprintf(">> spumux -m dvd %s < %s > %s", spumuxXML, filepath.Base(inputMpg), filepath.Base(outputMpg)))
|
logFn(fmt.Sprintf(">> spumux -m dvd %s < %s > %s", spumuxXML, filepath.Base(inputMpg), filepath.Base(outputMpg)))
|
||||||
}
|
}
|
||||||
|
|
@ -262,7 +464,7 @@ func findVTLogoPath() string {
|
||||||
}
|
}
|
||||||
if exe, err := os.Executable(); err == nil {
|
if exe, err := os.Executable(); err == nil {
|
||||||
dir := filepath.Dir(exe)
|
dir := filepath.Dir(exe)
|
||||||
search = append(search, filepath.Join(dir, "assets", "logo", "VT_Icon.png"))
|
search = append(search, filepath.Join(dir, "assets", "logo", "VT_Icon.png")),
|
||||||
}
|
}
|
||||||
for _, p := range search {
|
for _, p := range search {
|
||||||
if _, err := os.Stat(p); err == nil {
|
if _, err := os.Stat(p); err == nil {
|
||||||
|
|
@ -273,9 +475,9 @@ func findVTLogoPath() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func escapeDrawtextText(text string) string {
|
func escapeDrawtextText(text string) string {
|
||||||
escaped := strings.ReplaceAll(text, "\\", "\\\\")
|
escaped := strings.ReplaceAll(text, "\", "\\\\")
|
||||||
escaped = strings.ReplaceAll(escaped, ":", "\\:")
|
escaped = strings.ReplaceAll(escaped, ":", "\\:")
|
||||||
escaped = strings.ReplaceAll(escaped, "'", "\\'")
|
escaped = strings.ReplaceAll(escaped, "'", "\' ")
|
||||||
escaped = strings.ReplaceAll(escaped, "%", "\\%")
|
escaped = strings.ReplaceAll(escaped, "%", "\\%")
|
||||||
return escaped
|
return escaped
|
||||||
}
|
}
|
||||||
|
|
@ -103,6 +103,7 @@ func (s *appState) applyAuthorConfig(cfg authorConfig) {
|
||||||
s.authorCreateMenu = cfg.CreateMenu
|
s.authorCreateMenu = cfg.CreateMenu
|
||||||
s.authorTreatAsChapters = cfg.TreatAsChapters
|
s.authorTreatAsChapters = cfg.TreatAsChapters
|
||||||
s.authorSceneThreshold = cfg.SceneThreshold
|
s.authorSceneThreshold = cfg.SceneThreshold
|
||||||
|
s.authorMenuTemplate = cfg.MenuTemplate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) persistAuthorConfig() {
|
func (s *appState) persistAuthorConfig() {
|
||||||
|
|
@ -115,6 +116,7 @@ func (s *appState) persistAuthorConfig() {
|
||||||
CreateMenu: s.authorCreateMenu,
|
CreateMenu: s.authorCreateMenu,
|
||||||
TreatAsChapters: s.authorTreatAsChapters,
|
TreatAsChapters: s.authorTreatAsChapters,
|
||||||
SceneThreshold: s.authorSceneThreshold,
|
SceneThreshold: s.authorSceneThreshold,
|
||||||
|
MenuTemplate: s.authorMenuTemplate,
|
||||||
}
|
}
|
||||||
if err := savePersistedAuthorConfig(cfg); err != nil {
|
if err := savePersistedAuthorConfig(cfg); err != nil {
|
||||||
logging.Debug(logging.CatSystem, "failed to persist author config: %v", err)
|
logging.Debug(logging.CatSystem, "failed to persist author config: %v", err)
|
||||||
|
|
@ -726,6 +728,36 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
||||||
})
|
})
|
||||||
createMenuCheck.SetChecked(state.authorCreateMenu)
|
createMenuCheck.SetChecked(state.authorCreateMenu)
|
||||||
|
|
||||||
|
menuTemplateSelect := widget.NewSelect([]string{"Simple", "Dark", "Poster"}, func(value string) {
|
||||||
|
state.authorMenuTemplate = value
|
||||||
|
state.updateAuthorSummary()
|
||||||
|
state.persistAuthorConfig()
|
||||||
|
})
|
||||||
|
menuTemplateSelect.SetSelected(state.authorMenuTemplate)
|
||||||
|
|
||||||
|
bgImageLabel := widget.NewLabel(state.authorMenuBackgroundImage)
|
||||||
|
bgImageButton := widget.NewButton("Select Background Image", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
state.authorMenuBackgroundImage = reader.URI().Path()
|
||||||
|
bgImageLabel.SetText(state.authorMenuBackgroundImage)
|
||||||
|
state.updateAuthorSummary()
|
||||||
|
state.persistAuthorConfig()
|
||||||
|
}, state.window)
|
||||||
|
})
|
||||||
|
bgImageButton.Importance = widget.HighImportance
|
||||||
|
bgImageButton.Hidden = state.authorMenuTemplate != "Poster"
|
||||||
|
|
||||||
|
menuTemplateSelect.OnChanged = func(value string) {
|
||||||
|
state.authorMenuTemplate = value
|
||||||
|
bgImageButton.Hidden = value != "Poster"
|
||||||
|
state.updateAuthorSummary()
|
||||||
|
state.persistAuthorConfig()
|
||||||
|
}
|
||||||
|
|
||||||
discSizeSelect := widget.NewSelect([]string{"DVD5", "DVD9"}, func(value string) {
|
discSizeSelect := widget.NewSelect([]string{"DVD5", "DVD9"}, func(value string) {
|
||||||
state.authorDiscSize = value
|
state.authorDiscSize = value
|
||||||
state.updateAuthorSummary()
|
state.updateAuthorSummary()
|
||||||
|
|
@ -760,6 +792,9 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
||||||
}
|
}
|
||||||
titleEntry.SetText(state.authorTitle)
|
titleEntry.SetText(state.authorTitle)
|
||||||
createMenuCheck.SetChecked(state.authorCreateMenu)
|
createMenuCheck.SetChecked(state.authorCreateMenu)
|
||||||
|
menuTemplateSelect.SetSelected(state.authorMenuTemplate)
|
||||||
|
bgImageLabel.SetText(state.authorMenuBackgroundImage)
|
||||||
|
bgImageButton.Hidden = state.authorMenuTemplate != "Poster"
|
||||||
}
|
}
|
||||||
|
|
||||||
loadCfgBtn := widget.NewButton("Load Config", func() {
|
loadCfgBtn := widget.NewButton("Load Config", func() {
|
||||||
|
|
@ -787,6 +822,7 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
||||||
CreateMenu: state.authorCreateMenu,
|
CreateMenu: state.authorCreateMenu,
|
||||||
TreatAsChapters: state.authorTreatAsChapters,
|
TreatAsChapters: state.authorTreatAsChapters,
|
||||||
SceneThreshold: state.authorSceneThreshold,
|
SceneThreshold: state.authorSceneThreshold,
|
||||||
|
MenuTemplate: state.authorMenuTemplate,
|
||||||
}
|
}
|
||||||
if err := savePersistedAuthorConfig(cfg); err != nil {
|
if err := savePersistedAuthorConfig(cfg); err != nil {
|
||||||
dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window)
|
dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window)
|
||||||
|
|
@ -820,6 +856,10 @@ func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
||||||
widget.NewLabel("DVD Title:"),
|
widget.NewLabel("DVD Title:"),
|
||||||
titleEntry,
|
titleEntry,
|
||||||
createMenuCheck,
|
createMenuCheck,
|
||||||
|
widget.NewLabel("Menu Template:"),
|
||||||
|
menuTemplateSelect,
|
||||||
|
bgImageLabel,
|
||||||
|
bgImageButton,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
info,
|
info,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
|
|
@ -1741,6 +1781,8 @@ func (s *appState) addAuthorToQueue(paths []string, region, aspect, title, outpu
|
||||||
"chapterSource": s.authorChapterSource,
|
"chapterSource": s.authorChapterSource,
|
||||||
"subtitleTracks": append([]string{}, s.authorSubtitles...),
|
"subtitleTracks": append([]string{}, s.authorSubtitles...),
|
||||||
"additionalAudios": append([]string{}, s.authorAudioTracks...),
|
"additionalAudios": append([]string{}, s.authorAudioTracks...),
|
||||||
|
"menuTemplate": s.authorMenuTemplate,
|
||||||
|
"menuBackgroundImage": s.authorMenuBackgroundImage,
|
||||||
}
|
}
|
||||||
|
|
||||||
titleLabel := title
|
titleLabel := title
|
||||||
|
|
@ -1802,7 +1844,7 @@ func (s *appState) addAuthorVideoTSToQueue(videoTSPath, title, outputPath string
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, region, aspect, title, outputPath string, makeISO bool, clips []authorClip, chapters []authorChapter, treatAsChapters bool, createMenu bool, logFn func(string), progressFn func(float64)) error {
|
func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, region, aspect, title, outputPath string, makeISO bool, clips []authorClip, chapters []authorChapter, treatAsChapters bool, createMenu bool, menuTemplate string, menuBackgroundImage string, logFn func(string), progressFn func(float64)) error {
|
||||||
tempRoot := authorTempRoot(outputPath)
|
tempRoot := authorTempRoot(outputPath)
|
||||||
if err := os.MkdirAll(tempRoot, 0755); err != nil {
|
if err := os.MkdirAll(tempRoot, 0755); err != nil {
|
||||||
return fmt.Errorf("failed to create temp root: %w", err)
|
return fmt.Errorf("failed to create temp root: %w", err)
|
||||||
|
|
@ -1984,7 +2026,11 @@ func (s *appState) runAuthoringPipeline(ctx context.Context, paths []string, reg
|
||||||
var menuMpg string
|
var menuMpg string
|
||||||
var menuButtons []dvdMenuButton
|
var menuButtons []dvdMenuButton
|
||||||
if createMenu {
|
if createMenu {
|
||||||
menuMpg, menuButtons, err = buildDVDMenuAssets(ctx, workDir, title, region, aspect, chapters, logFn)
|
template, ok := menuTemplates[menuTemplate]
|
||||||
|
if !ok {
|
||||||
|
template = &SimpleMenu{}
|
||||||
|
}
|
||||||
|
menuMpg, menuButtons, err = buildDVDMenuAssets(ctx, workDir, title, region, aspect, chapters, logFn, template, menuBackgroundImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -2294,7 +2340,7 @@ func (s *appState) executeAuthorJob(ctx context.Context, job *queue.Job, progres
|
||||||
}, false)
|
}, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
err := s.runAuthoringPipeline(ctx, paths, region, aspect, title, outputPath, makeISO, clips, chapters, treatAsChapters, createMenu, appendLog, updateProgress)
|
err := s.runAuthoringPipeline(ctx, paths, region, aspect, title, outputPath, makeISO, clips, chapters, treatAsChapters, createMenu, toString(cfg["menuTemplate"]), toString(cfg["menuBackgroundImage"]), appendLog, updateProgress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
friendly := authorFriendlyError(err)
|
friendly := authorFriendlyError(err)
|
||||||
appendLog("ERROR: " + friendly)
|
appendLog("ERROR: " + friendly)
|
||||||
|
|
|
||||||
26
diagnostic_tools/diagnostic_tool.go
Normal file
26
diagnostic_tools/diagnostic_tool.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
fmt.Println("Usage: ./diagnostic_tool <video_path>")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
videoPath := os.Args[1]
|
||||||
|
|
||||||
|
fmt.Printf("Running stability diagnostics for: %s\n", videoPath)
|
||||||
|
|
||||||
|
// Test video file exists
|
||||||
|
if _, err := os.Stat(videoPath); os.IsNotExist(err) {
|
||||||
|
fmt.Printf("Error: video file not found: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Diagnostics completed successfully")
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user