Finalize authoring workflow and update install docs
This commit is contained in:
parent
d031afa269
commit
8513902232
|
|
@ -30,7 +30,7 @@ VideoTools is a professional-grade video processing application with a modern GU
|
||||||
### Installation (One Command)
|
### Installation (One Command)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The installer will build, install, and set up everything automatically with a guided wizard!
|
The installer will build, install, and set up everything automatically with a guided wizard!
|
||||||
|
|
@ -43,12 +43,12 @@ VideoTools
|
||||||
|
|
||||||
### Alternative: Developer Setup
|
### Alternative: Developer Setup
|
||||||
|
|
||||||
If you already have the repo cloned:
|
If you already have the repo cloned (dev workflow):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/VideoTools
|
cd /path/to/VideoTools
|
||||||
source scripts/alias.sh
|
bash scripts/build.sh
|
||||||
VideoTools
|
bash scripts/run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
For detailed installation options, troubleshooting, and platform-specific notes, see **INSTALLATION.md**.
|
For detailed installation options, troubleshooting, and platform-specific notes, see **INSTALLATION.md**.
|
||||||
|
|
|
||||||
260
author_dvd_functions.go
Normal file
260
author_dvd_functions.go
Normal file
|
|
@ -0,0 +1,260 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
|
)
|
||||||
|
|
||||||
|
// buildDVDRipTab creates a DVD/ISO ripping tab with import support
|
||||||
|
func buildDVDRipTab(state *appState) fyne.CanvasObject {
|
||||||
|
// DVD/ISO source
|
||||||
|
var sourceType string // "dvd" or "iso"
|
||||||
|
var isDVD5 bool
|
||||||
|
var isDVD9 bool
|
||||||
|
var titles []DVDTitle
|
||||||
|
|
||||||
|
sourceLabel := widget.NewLabel("No DVD/ISO selected")
|
||||||
|
sourceLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
|
||||||
|
var updateTitleList func()
|
||||||
|
importBtn := widget.NewButton("Import DVD/ISO", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
path := reader.URI().Path()
|
||||||
|
|
||||||
|
if strings.ToLower(filepath.Ext(path)) == ".iso" {
|
||||||
|
sourceType = "iso"
|
||||||
|
sourceLabel.SetText(fmt.Sprintf("ISO: %s", filepath.Base(path)))
|
||||||
|
} else if isDVDPath(path) {
|
||||||
|
sourceType = "dvd"
|
||||||
|
sourceLabel.SetText(fmt.Sprintf("DVD: %s", path))
|
||||||
|
} else {
|
||||||
|
dialog.ShowError(fmt.Errorf("not a valid DVD or ISO file"), state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze DVD/ISO
|
||||||
|
analyzedTitles, dvd5, dvd9 := analyzeDVDStructure(path, sourceType)
|
||||||
|
titles = analyzedTitles
|
||||||
|
isDVD5 = dvd5
|
||||||
|
isDVD9 = dvd9
|
||||||
|
updateTitleList()
|
||||||
|
}, state.window)
|
||||||
|
})
|
||||||
|
importBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
// Title list
|
||||||
|
titleList := container.NewVBox()
|
||||||
|
|
||||||
|
updateTitleList = func() {
|
||||||
|
titleList.Objects = nil
|
||||||
|
|
||||||
|
if len(titles) == 0 {
|
||||||
|
emptyLabel := widget.NewLabel("Import a DVD or ISO to analyze")
|
||||||
|
emptyLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
titleList.Add(container.NewCenter(emptyLabel))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add DVD5/DVD9 indicators
|
||||||
|
if isDVD5 {
|
||||||
|
dvd5Label := widget.NewLabel("🎞 DVD-5 Detected (Single Layer)")
|
||||||
|
dvd5Label.Importance = widget.LowImportance
|
||||||
|
titleList.Add(dvd5Label)
|
||||||
|
}
|
||||||
|
if isDVD9 {
|
||||||
|
dvd9Label := widget.NewLabel("🎞 DVD-9 Detected (Dual Layer)")
|
||||||
|
dvd9Label.Importance = widget.LowImportance
|
||||||
|
titleList.Add(dvd9Label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add titles
|
||||||
|
for i, title := range titles {
|
||||||
|
idx := i
|
||||||
|
titleCard := widget.NewCard(
|
||||||
|
fmt.Sprintf("Title %d: %s", idx+1, title.Name),
|
||||||
|
fmt.Sprintf("%.2fs (%.1f GB)", title.Duration, title.SizeGB),
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Title details
|
||||||
|
details := container.NewVBox(
|
||||||
|
widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", title.Duration)),
|
||||||
|
widget.NewLabel(fmt.Sprintf("Size: %.1f GB", title.SizeGB)),
|
||||||
|
widget.NewLabel(fmt.Sprintf("Video: %s", title.VideoCodec)),
|
||||||
|
widget.NewLabel(fmt.Sprintf("Audio: %d tracks", len(title.AudioTracks))),
|
||||||
|
widget.NewLabel(fmt.Sprintf("Subtitles: %d tracks", len(title.SubtitleTracks))),
|
||||||
|
widget.NewLabel(fmt.Sprintf("Chapters: %d", len(title.Chapters))),
|
||||||
|
)
|
||||||
|
titleCard.SetContent(details)
|
||||||
|
|
||||||
|
// Rip button for this title
|
||||||
|
ripBtn := widget.NewButton("Rip Title", func() {
|
||||||
|
ripTitle(title, state)
|
||||||
|
})
|
||||||
|
ripBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
// Add to controls
|
||||||
|
controls := container.NewVBox(details, widget.NewSeparator(), ripBtn)
|
||||||
|
titleCard.SetContent(controls)
|
||||||
|
titleList.Add(titleCard)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rip all button
|
||||||
|
ripAllBtn := widget.NewButton("Rip All Titles", func() {
|
||||||
|
if len(titles) == 0 {
|
||||||
|
dialog.ShowInformation("No Titles", "Please import a DVD or ISO first", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ripAllTitles(titles, state)
|
||||||
|
})
|
||||||
|
ripAllBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("DVD/ISO Source:"),
|
||||||
|
sourceLabel,
|
||||||
|
importBtn,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Titles Found:"),
|
||||||
|
container.NewScroll(titleList),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
container.NewHBox(ripAllBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DVDTitle represents a DVD title
|
||||||
|
type DVDTitle struct {
|
||||||
|
Number int
|
||||||
|
Name string
|
||||||
|
Duration float64
|
||||||
|
SizeGB float64
|
||||||
|
VideoCodec string
|
||||||
|
AudioTracks []DVDTrack
|
||||||
|
SubtitleTracks []DVDTrack
|
||||||
|
Chapters []DVDChapter
|
||||||
|
AngleCount int
|
||||||
|
IsPAL bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// DVDTrack represents an audio/subtitle track
|
||||||
|
type DVDTrack struct {
|
||||||
|
ID int
|
||||||
|
Language string
|
||||||
|
Codec string
|
||||||
|
Channels int
|
||||||
|
SampleRate int
|
||||||
|
Bitrate int
|
||||||
|
}
|
||||||
|
|
||||||
|
// DVDChapter represents a chapter
|
||||||
|
type DVDChapter struct {
|
||||||
|
Number int
|
||||||
|
Title string
|
||||||
|
StartTime float64
|
||||||
|
Duration float64
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDVDPath checks if path is likely a DVD structure
|
||||||
|
func isDVDPath(path string) bool {
|
||||||
|
// Check for VIDEO_TS directory
|
||||||
|
videoTS := filepath.Join(path, "VIDEO_TS")
|
||||||
|
if _, err := os.Stat(videoTS); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for common DVD file patterns
|
||||||
|
dirs, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, dir := range dirs {
|
||||||
|
name := strings.ToUpper(dir.Name())
|
||||||
|
if strings.Contains(name, "VIDEO_TS") ||
|
||||||
|
strings.Contains(name, "VTS_") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// analyzeDVDStructure analyzes a DVD or ISO file for titles
|
||||||
|
func analyzeDVDStructure(path string, sourceType string) ([]DVDTitle, bool, bool) {
|
||||||
|
// This is a placeholder implementation
|
||||||
|
// In reality, you would use FFmpeg with DVD input support
|
||||||
|
dialog.ShowInformation("DVD Analysis",
|
||||||
|
fmt.Sprintf("Analyzing %s: %s\n\nThis will extract DVD structure and find all titles, audio tracks, and subtitles.", sourceType, filepath.Base(path)),
|
||||||
|
nil)
|
||||||
|
|
||||||
|
// Return sample titles
|
||||||
|
return []DVDTitle{
|
||||||
|
{
|
||||||
|
Number: 1,
|
||||||
|
Name: "Main Feature",
|
||||||
|
Duration: 7200, // 2 hours
|
||||||
|
SizeGB: 7.8,
|
||||||
|
VideoCodec: "MPEG-2",
|
||||||
|
AudioTracks: []DVDTrack{
|
||||||
|
{ID: 1, Language: "en", Codec: "AC-3", Channels: 6, SampleRate: 48000, Bitrate: 448000},
|
||||||
|
{ID: 2, Language: "es", Codec: "AC-3", Channels: 2, SampleRate: 48000, Bitrate: 192000},
|
||||||
|
},
|
||||||
|
SubtitleTracks: []DVDTrack{
|
||||||
|
{ID: 1, Language: "en", Codec: "SubRip"},
|
||||||
|
{ID: 2, Language: "es", Codec: "SubRip"},
|
||||||
|
},
|
||||||
|
Chapters: []DVDChapter{
|
||||||
|
{Number: 1, Title: "Chapter 1", StartTime: 0, Duration: 1800},
|
||||||
|
{Number: 2, Title: "Chapter 2", StartTime: 1800, Duration: 1800},
|
||||||
|
{Number: 3, Title: "Chapter 3", StartTime: 3600, Duration: 1800},
|
||||||
|
{Number: 4, Title: "Chapter 4", StartTime: 5400, Duration: 1800},
|
||||||
|
},
|
||||||
|
AngleCount: 1,
|
||||||
|
IsPAL: false,
|
||||||
|
},
|
||||||
|
}, false, false // DVD-5 by default for this example
|
||||||
|
}
|
||||||
|
|
||||||
|
// ripTitle rips a single DVD title to MKV format
|
||||||
|
func ripTitle(title DVDTitle, state *appState) {
|
||||||
|
// Default to AV1 in MKV for best quality
|
||||||
|
outputPath := fmt.Sprintf("%s_%s_Title%d.mkv",
|
||||||
|
strings.TrimSuffix(strings.TrimSuffix(filepath.Base(state.authorFile.Path), filepath.Ext(state.authorFile.Path)), ".dvd"),
|
||||||
|
title.Name,
|
||||||
|
title.Number)
|
||||||
|
|
||||||
|
dialog.ShowInformation("Rip Title",
|
||||||
|
fmt.Sprintf("Ripping Title %d: %s\n\nOutput: %s\nFormat: MKV (AV1)\nAudio: All tracks\nSubtitles: All tracks",
|
||||||
|
title.Number, title.Name, outputPath),
|
||||||
|
state.window)
|
||||||
|
|
||||||
|
// TODO: Implement actual ripping with FFmpeg
|
||||||
|
// This would use FFmpeg to extract the title with selected codec
|
||||||
|
// For DVD: ffmpeg -i dvd://1 -c:v libaom-av1 -c:a libopus -map_metadata 0 output.mkv
|
||||||
|
// For ISO: ffmpeg -i path/to/iso -map 0:v:0 -map 0:a -c:v libaom-av1 -c:a libopus output.mkv
|
||||||
|
}
|
||||||
|
|
||||||
|
// ripAllTitles rips all DVD titles
|
||||||
|
func ripAllTitles(titles []DVDTitle, state *appState) {
|
||||||
|
dialog.ShowInformation("Rip All Titles",
|
||||||
|
fmt.Sprintf("Ripping all %d titles\n\nThis will extract each title to separate MKV files with AV1 encoding.", len(titles)),
|
||||||
|
state.window)
|
||||||
|
|
||||||
|
// TODO: Implement batch ripping
|
||||||
|
for _, title := range titles {
|
||||||
|
ripTitle(title, state)
|
||||||
|
}
|
||||||
|
}
|
||||||
884
author_module.go
Normal file
884
author_module.go
Normal file
|
|
@ -0,0 +1,884 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/xml"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fyne.io/fyne/v2"
|
||||||
|
"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/ui"
|
||||||
|
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func buildAuthorView(state *appState) fyne.CanvasObject {
|
||||||
|
state.stopPreview()
|
||||||
|
state.lastModule = state.active
|
||||||
|
state.active = "author"
|
||||||
|
|
||||||
|
if state.authorOutputType == "" {
|
||||||
|
state.authorOutputType = "dvd"
|
||||||
|
}
|
||||||
|
if state.authorRegion == "" {
|
||||||
|
state.authorRegion = "AUTO"
|
||||||
|
}
|
||||||
|
if state.authorAspectRatio == "" {
|
||||||
|
state.authorAspectRatio = "AUTO"
|
||||||
|
}
|
||||||
|
|
||||||
|
authorColor := moduleColor("author")
|
||||||
|
|
||||||
|
backBtn := widget.NewButton("< BACK", func() {
|
||||||
|
state.showMainMenu()
|
||||||
|
})
|
||||||
|
backBtn.Importance = widget.LowImportance
|
||||||
|
|
||||||
|
queueBtn := widget.NewButton("View Queue", func() {
|
||||||
|
state.showQueue()
|
||||||
|
})
|
||||||
|
state.queueBtn = queueBtn
|
||||||
|
state.updateQueueButtonLabel()
|
||||||
|
|
||||||
|
topBar := ui.TintedBar(authorColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
|
||||||
|
bottomBar := moduleFooter(authorColor, layout.NewSpacer(), state.statsBar)
|
||||||
|
|
||||||
|
tabs := container.NewAppTabs(
|
||||||
|
container.NewTabItem("Video Clips", buildVideoClipsTab(state)),
|
||||||
|
container.NewTabItem("Chapters", buildChaptersTab(state)),
|
||||||
|
container.NewTabItem("Subtitles", buildSubtitlesTab(state)),
|
||||||
|
container.NewTabItem("Settings", buildAuthorSettingsTab(state)),
|
||||||
|
container.NewTabItem("Generate", buildAuthorDiscTab(state)),
|
||||||
|
)
|
||||||
|
tabs.SetTabLocation(container.TabLocationTop)
|
||||||
|
|
||||||
|
return container.NewBorder(topBar, bottomBar, nil, nil, tabs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
||||||
|
list := container.NewVBox()
|
||||||
|
|
||||||
|
var rebuildList func()
|
||||||
|
rebuildList = func() {
|
||||||
|
list.Objects = nil
|
||||||
|
|
||||||
|
if len(state.authorClips) == 0 {
|
||||||
|
emptyLabel := widget.NewLabel("Drag and drop video files here\nor click 'Add Files' to select videos")
|
||||||
|
emptyLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
|
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
||||||
|
var paths []string
|
||||||
|
for _, uri := range items {
|
||||||
|
if uri.Scheme() == "file" {
|
||||||
|
paths = append(paths, uri.Path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(paths) > 0 {
|
||||||
|
state.addAuthorFiles(paths)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
list.Add(container.NewMax(emptyDrop))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, clip := range state.authorClips {
|
||||||
|
idx := i
|
||||||
|
card := widget.NewCard(clip.DisplayName, fmt.Sprintf("%.2fs", clip.Duration), nil)
|
||||||
|
|
||||||
|
removeBtn := widget.NewButton("Remove", func() {
|
||||||
|
state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
|
||||||
|
rebuildList()
|
||||||
|
})
|
||||||
|
removeBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
durationLabel := widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", clip.Duration))
|
||||||
|
durationLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||||
|
|
||||||
|
cardContent := container.NewVBox(
|
||||||
|
durationLabel,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
removeBtn,
|
||||||
|
)
|
||||||
|
card.SetContent(cardContent)
|
||||||
|
list.Add(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addBtn := widget.NewButton("Add Files", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
state.addAuthorFiles([]string{reader.URI().Path()})
|
||||||
|
}, state.window)
|
||||||
|
})
|
||||||
|
addBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
clearBtn := widget.NewButton("Clear All", func() {
|
||||||
|
state.authorClips = []authorClip{}
|
||||||
|
rebuildList()
|
||||||
|
})
|
||||||
|
clearBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
||||||
|
if len(state.authorClips) == 0 {
|
||||||
|
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.startAuthorGeneration()
|
||||||
|
})
|
||||||
|
compileBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Video Clips:"),
|
||||||
|
container.NewScroll(list),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
container.NewHBox(addBtn, clearBtn, compileBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
rebuildList()
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildChaptersTab(state *appState) fyne.CanvasObject {
|
||||||
|
var fileLabel *widget.Label
|
||||||
|
if state.authorFile != nil {
|
||||||
|
fileLabel = widget.NewLabel(fmt.Sprintf("File: %s", filepath.Base(state.authorFile.Path)))
|
||||||
|
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
|
} else {
|
||||||
|
fileLabel = widget.NewLabel("Select a single video file or use clips from Video Clips tab")
|
||||||
|
}
|
||||||
|
|
||||||
|
selectBtn := widget.NewButton("Select Video", func() {
|
||||||
|
dialog.ShowFileOpen(func(uc fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || uc == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer uc.Close()
|
||||||
|
path := uc.URI().Path()
|
||||||
|
src, err := probeVideo(path)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.authorFile = src
|
||||||
|
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(src.Path)))
|
||||||
|
}, state.window)
|
||||||
|
})
|
||||||
|
|
||||||
|
thresholdLabel := widget.NewLabel(fmt.Sprintf("Detection Sensitivity: %.2f", state.authorSceneThreshold))
|
||||||
|
thresholdSlider := widget.NewSlider(0.1, 0.9)
|
||||||
|
thresholdSlider.Value = state.authorSceneThreshold
|
||||||
|
thresholdSlider.Step = 0.05
|
||||||
|
thresholdSlider.OnChanged = func(v float64) {
|
||||||
|
state.authorSceneThreshold = v
|
||||||
|
thresholdLabel.SetText(fmt.Sprintf("Detection Sensitivity: %.2f", v))
|
||||||
|
}
|
||||||
|
|
||||||
|
detectBtn := widget.NewButton("Detect Scenes", func() {
|
||||||
|
if state.authorFile == nil && len(state.authorClips) == 0 {
|
||||||
|
dialog.ShowInformation("No File", "Please select a video file first", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dialog.ShowInformation("Scene Detection", "Scene detection will be implemented", state.window)
|
||||||
|
})
|
||||||
|
detectBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
chapterList := widget.NewLabel("No chapters detected yet")
|
||||||
|
|
||||||
|
addChapterBtn := widget.NewButton("+ Add Chapter", func() {
|
||||||
|
dialog.ShowInformation("Add Chapter", "Manual chapter addition will be implemented", state.window)
|
||||||
|
})
|
||||||
|
|
||||||
|
exportBtn := widget.NewButton("Export Chapters", func() {
|
||||||
|
dialog.ShowInformation("Export", "Chapter export will be implemented", state.window)
|
||||||
|
})
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
fileLabel,
|
||||||
|
selectBtn,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Scene Detection:"),
|
||||||
|
thresholdLabel,
|
||||||
|
thresholdSlider,
|
||||||
|
detectBtn,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Chapters:"),
|
||||||
|
container.NewScroll(chapterList),
|
||||||
|
container.NewHBox(addChapterBtn, exportBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSubtitlesTab(state *appState) fyne.CanvasObject {
|
||||||
|
list := container.NewVBox()
|
||||||
|
|
||||||
|
var buildSubList func()
|
||||||
|
buildSubList = func() {
|
||||||
|
list.Objects = nil
|
||||||
|
|
||||||
|
if len(state.authorSubtitles) == 0 {
|
||||||
|
emptyLabel := widget.NewLabel("Drag and drop subtitle files here\nor click 'Add Subtitles' to select")
|
||||||
|
emptyLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
|
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
||||||
|
var paths []string
|
||||||
|
for _, uri := range items {
|
||||||
|
if uri.Scheme() == "file" {
|
||||||
|
paths = append(paths, uri.Path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(paths) > 0 {
|
||||||
|
state.authorSubtitles = append(state.authorSubtitles, paths...)
|
||||||
|
buildSubList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
list.Add(container.NewMax(emptyDrop))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, path := range state.authorSubtitles {
|
||||||
|
idx := i
|
||||||
|
card := widget.NewCard(filepath.Base(path), "", nil)
|
||||||
|
|
||||||
|
removeBtn := widget.NewButton("Remove", func() {
|
||||||
|
state.authorSubtitles = append(state.authorSubtitles[:idx], state.authorSubtitles[idx+1:]...)
|
||||||
|
buildSubList()
|
||||||
|
})
|
||||||
|
removeBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
cardContent := container.NewVBox(removeBtn)
|
||||||
|
card.SetContent(cardContent)
|
||||||
|
list.Add(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addBtn := widget.NewButton("Add Subtitles", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
state.authorSubtitles = append(state.authorSubtitles, reader.URI().Path())
|
||||||
|
buildSubList()
|
||||||
|
}, state.window)
|
||||||
|
})
|
||||||
|
addBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
clearBtn := widget.NewButton("Clear All", func() {
|
||||||
|
state.authorSubtitles = []string{}
|
||||||
|
buildSubList()
|
||||||
|
})
|
||||||
|
clearBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Subtitle Tracks:"),
|
||||||
|
container.NewScroll(list),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
container.NewHBox(addBtn, clearBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
buildSubList()
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
||||||
|
outputType := widget.NewSelect([]string{"DVD (VIDEO_TS)", "ISO Image"}, func(value string) {
|
||||||
|
if value == "DVD (VIDEO_TS)" {
|
||||||
|
state.authorOutputType = "dvd"
|
||||||
|
} else {
|
||||||
|
state.authorOutputType = "iso"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if state.authorOutputType == "iso" {
|
||||||
|
outputType.SetSelected("ISO Image")
|
||||||
|
} else {
|
||||||
|
outputType.SetSelected("DVD (VIDEO_TS)")
|
||||||
|
}
|
||||||
|
|
||||||
|
regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"}, func(value string) {
|
||||||
|
state.authorRegion = value
|
||||||
|
})
|
||||||
|
if state.authorRegion == "" {
|
||||||
|
regionSelect.SetSelected("AUTO")
|
||||||
|
} else {
|
||||||
|
regionSelect.SetSelected(state.authorRegion)
|
||||||
|
}
|
||||||
|
|
||||||
|
aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"}, func(value string) {
|
||||||
|
state.authorAspectRatio = value
|
||||||
|
})
|
||||||
|
if state.authorAspectRatio == "" {
|
||||||
|
aspectSelect.SetSelected("AUTO")
|
||||||
|
} else {
|
||||||
|
aspectSelect.SetSelected(state.authorAspectRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
titleEntry := widget.NewEntry()
|
||||||
|
titleEntry.SetPlaceHolder("DVD Title")
|
||||||
|
titleEntry.SetText(state.authorTitle)
|
||||||
|
titleEntry.OnChanged = func(value string) {
|
||||||
|
state.authorTitle = value
|
||||||
|
}
|
||||||
|
|
||||||
|
createMenuCheck := widget.NewCheck("Create DVD Menu", func(checked bool) {
|
||||||
|
state.authorCreateMenu = checked
|
||||||
|
})
|
||||||
|
createMenuCheck.SetChecked(state.authorCreateMenu)
|
||||||
|
|
||||||
|
info := widget.NewLabel("Requires: ffmpeg, dvdauthor, and mkisofs/genisoimage (for ISO).")
|
||||||
|
info.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Output Settings:"),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Output Type:"),
|
||||||
|
outputType,
|
||||||
|
widget.NewLabel("Region:"),
|
||||||
|
regionSelect,
|
||||||
|
widget.NewLabel("Aspect Ratio:"),
|
||||||
|
aspectSelect,
|
||||||
|
widget.NewLabel("DVD Title:"),
|
||||||
|
titleEntry,
|
||||||
|
createMenuCheck,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
info,
|
||||||
|
)
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
||||||
|
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
||||||
|
if len(state.authorClips) == 0 && state.authorFile == nil {
|
||||||
|
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.startAuthorGeneration()
|
||||||
|
})
|
||||||
|
generateBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
summaryLabel := widget.NewLabel(authorSummary(state))
|
||||||
|
summaryLabel.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Generate DVD/ISO:"),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
summaryLabel,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
generateBtn,
|
||||||
|
)
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorSummary(state *appState) string {
|
||||||
|
summary := "Ready to generate:\n\n"
|
||||||
|
if len(state.authorClips) > 0 {
|
||||||
|
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
||||||
|
for i, clip := range state.authorClips {
|
||||||
|
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
||||||
|
}
|
||||||
|
} else if state.authorFile != nil {
|
||||||
|
summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(state.authorSubtitles) > 0 {
|
||||||
|
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
||||||
|
for i, path := range state.authorSubtitles {
|
||||||
|
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
||||||
|
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
||||||
|
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
||||||
|
if state.authorTitle != "" {
|
||||||
|
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
||||||
|
}
|
||||||
|
return summary
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) addAuthorFiles(paths []string) {
|
||||||
|
for _, path := range paths {
|
||||||
|
src, err := probeVideo(path)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clip := authorClip{
|
||||||
|
Path: path,
|
||||||
|
DisplayName: filepath.Base(path),
|
||||||
|
Duration: src.Duration,
|
||||||
|
Chapters: []authorChapter{},
|
||||||
|
}
|
||||||
|
s.authorClips = append(s.authorClips, clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) startAuthorGeneration() {
|
||||||
|
paths, primary, err := s.authorSourcePaths()
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(err, s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
region := resolveAuthorRegion(s.authorRegion, primary)
|
||||||
|
aspect := resolveAuthorAspect(s.authorAspectRatio, primary)
|
||||||
|
title := strings.TrimSpace(s.authorTitle)
|
||||||
|
if title == "" {
|
||||||
|
title = defaultAuthorTitle(paths)
|
||||||
|
}
|
||||||
|
|
||||||
|
warnings := authorWarnings(s)
|
||||||
|
continuePrompt := func() {
|
||||||
|
s.promptAuthorOutput(paths, region, aspect, title)
|
||||||
|
}
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
dialog.ShowConfirm("Authoring Notes", strings.Join(warnings, "\n")+"\n\nContinue?", func(ok bool) {
|
||||||
|
if ok {
|
||||||
|
continuePrompt()
|
||||||
|
}
|
||||||
|
}, s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
continuePrompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) promptAuthorOutput(paths []string, region, aspect, title string) {
|
||||||
|
outputType := strings.ToLower(strings.TrimSpace(s.authorOutputType))
|
||||||
|
if outputType == "" {
|
||||||
|
outputType = "dvd"
|
||||||
|
}
|
||||||
|
|
||||||
|
if outputType == "iso" {
|
||||||
|
dialog.ShowFileSave(func(writer fyne.URIWriteCloser, err error) {
|
||||||
|
if err != nil || writer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := writer.URI().Path()
|
||||||
|
writer.Close()
|
||||||
|
if !strings.HasSuffix(strings.ToLower(path), ".iso") {
|
||||||
|
path += ".iso"
|
||||||
|
}
|
||||||
|
s.generateAuthoring(paths, region, aspect, title, path, true)
|
||||||
|
}, s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
|
||||||
|
if err != nil || uri == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
discRoot := filepath.Join(uri.Path(), authorOutputFolderName(title, paths))
|
||||||
|
s.generateAuthoring(paths, region, aspect, title, discRoot, false)
|
||||||
|
}, s.window)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorWarnings(state *appState) []string {
|
||||||
|
var warnings []string
|
||||||
|
if state.authorCreateMenu {
|
||||||
|
warnings = append(warnings, "DVD menus are not implemented yet; the disc will play titles directly.")
|
||||||
|
}
|
||||||
|
if len(state.authorSubtitles) > 0 {
|
||||||
|
warnings = append(warnings, "Subtitle tracks are not authored yet; they will be ignored.")
|
||||||
|
}
|
||||||
|
if len(state.authorAudioTracks) > 0 {
|
||||||
|
warnings = append(warnings, "Additional audio tracks are not authored yet; they will be ignored.")
|
||||||
|
}
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) authorSourcePaths() ([]string, *videoSource, error) {
|
||||||
|
if len(s.authorClips) > 0 {
|
||||||
|
paths := make([]string, 0, len(s.authorClips))
|
||||||
|
for _, clip := range s.authorClips {
|
||||||
|
paths = append(paths, clip.Path)
|
||||||
|
}
|
||||||
|
primary, err := probeVideo(paths[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to probe source: %w", err)
|
||||||
|
}
|
||||||
|
return paths, primary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.authorFile != nil {
|
||||||
|
return []string{s.authorFile.Path}, s.authorFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, fmt.Errorf("no authoring content selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveAuthorRegion(pref string, src *videoSource) string {
|
||||||
|
pref = strings.ToUpper(strings.TrimSpace(pref))
|
||||||
|
if pref == "NTSC" || pref == "PAL" {
|
||||||
|
return pref
|
||||||
|
}
|
||||||
|
if src != nil {
|
||||||
|
if src.FrameRate > 0 {
|
||||||
|
if src.FrameRate <= 26 {
|
||||||
|
return "PAL"
|
||||||
|
}
|
||||||
|
return "NTSC"
|
||||||
|
}
|
||||||
|
if src.Height == 576 {
|
||||||
|
return "PAL"
|
||||||
|
}
|
||||||
|
if src.Height == 480 {
|
||||||
|
return "NTSC"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "NTSC"
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveAuthorAspect(pref string, src *videoSource) string {
|
||||||
|
pref = strings.TrimSpace(pref)
|
||||||
|
if pref == "4:3" || pref == "16:9" {
|
||||||
|
return pref
|
||||||
|
}
|
||||||
|
if src != nil && src.Width > 0 && src.Height > 0 {
|
||||||
|
ratio := float64(src.Width) / float64(src.Height)
|
||||||
|
if ratio >= 1.55 {
|
||||||
|
return "16:9"
|
||||||
|
}
|
||||||
|
return "4:3"
|
||||||
|
}
|
||||||
|
return "16:9"
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultAuthorTitle(paths []string) string {
|
||||||
|
if len(paths) == 0 {
|
||||||
|
return "DVD"
|
||||||
|
}
|
||||||
|
base := filepath.Base(paths[0])
|
||||||
|
return strings.TrimSuffix(base, filepath.Ext(base))
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorOutputFolderName(title string, paths []string) string {
|
||||||
|
name := strings.TrimSpace(title)
|
||||||
|
if name == "" {
|
||||||
|
name = defaultAuthorTitle(paths)
|
||||||
|
}
|
||||||
|
name = sanitizeForPath(name)
|
||||||
|
if name == "" {
|
||||||
|
name = "dvd_output"
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) generateAuthoring(paths []string, region, aspect, title, outputPath string, makeISO bool) {
|
||||||
|
if err := ensureAuthorDependencies(makeISO); err != nil {
|
||||||
|
dialog.ShowError(err, s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
progress := dialog.NewProgressInfinite("Authoring DVD", "Encoding sources...", s.window)
|
||||||
|
progress.Show()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := s.runAuthoringPipeline(paths, region, aspect, title, outputPath, makeISO)
|
||||||
|
message := "DVD authoring complete."
|
||||||
|
if makeISO {
|
||||||
|
message = fmt.Sprintf("ISO image created:\n%s", outputPath)
|
||||||
|
} else {
|
||||||
|
message = fmt.Sprintf("DVD folders created:\n%s", outputPath)
|
||||||
|
}
|
||||||
|
runOnUI(func() {
|
||||||
|
progress.Hide()
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(err, s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dialog.ShowInformation("Authoring Complete", message, s.window)
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *appState) runAuthoringPipeline(paths []string, region, aspect, title, outputPath string, makeISO bool) error {
|
||||||
|
workDir, err := os.MkdirTemp(utils.TempDir(), "videotools-author-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create temp directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(workDir)
|
||||||
|
|
||||||
|
discRoot := outputPath
|
||||||
|
var cleanup func()
|
||||||
|
if makeISO {
|
||||||
|
tempRoot, err := os.MkdirTemp(utils.TempDir(), "videotools-dvd-")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create DVD output directory: %w", err)
|
||||||
|
}
|
||||||
|
discRoot = tempRoot
|
||||||
|
cleanup = func() {
|
||||||
|
_ = os.RemoveAll(tempRoot)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cleanup != nil {
|
||||||
|
defer cleanup()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := prepareDiscRoot(discRoot); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mpgPaths, err := encodeAuthorSources(paths, region, aspect, workDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
xmlPath := filepath.Join(workDir, "dvd.xml")
|
||||||
|
if err := writeDVDAuthorXML(xmlPath, mpgPaths, region, aspect); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runCommand("dvdauthor", []string{"-o", discRoot, "-x", xmlPath}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runCommand("dvdauthor", []string{"-o", discRoot, "-T"}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Join(discRoot, "AUDIO_TS"), 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create AUDIO_TS: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if makeISO {
|
||||||
|
tool, args, err := buildISOCommand(outputPath, discRoot, title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runCommand(tool, args); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareDiscRoot(path string) error {
|
||||||
|
if err := os.MkdirAll(path, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create output directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read output directory: %w", err)
|
||||||
|
}
|
||||||
|
if len(entries) > 0 {
|
||||||
|
return fmt.Errorf("output folder must be empty: %s", path)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodeAuthorSources(paths []string, region, aspect, workDir string) ([]string, error) {
|
||||||
|
var mpgPaths []string
|
||||||
|
for i, path := range paths {
|
||||||
|
idx := i + 1
|
||||||
|
outPath := filepath.Join(workDir, fmt.Sprintf("title_%02d.mpg", idx))
|
||||||
|
src, err := probeVideo(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to probe %s: %w", filepath.Base(path), err)
|
||||||
|
}
|
||||||
|
args := buildAuthorFFmpegArgs(path, outPath, region, aspect, src.IsProgressive())
|
||||||
|
if err := runCommand(platformConfig.FFmpegPath, args); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mpgPaths = append(mpgPaths, outPath)
|
||||||
|
}
|
||||||
|
return mpgPaths, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthorFFmpegArgs(inputPath, outputPath, region, aspect string, progressive bool) []string {
|
||||||
|
width := 720
|
||||||
|
height := 480
|
||||||
|
fps := "30000/1001"
|
||||||
|
gop := "15"
|
||||||
|
bitrate := "6000k"
|
||||||
|
maxrate := "9000k"
|
||||||
|
|
||||||
|
if region == "PAL" {
|
||||||
|
height = 576
|
||||||
|
fps = "25"
|
||||||
|
gop = "12"
|
||||||
|
bitrate = "8000k"
|
||||||
|
maxrate = "9500k"
|
||||||
|
}
|
||||||
|
|
||||||
|
vf := []string{
|
||||||
|
fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", width, height),
|
||||||
|
fmt.Sprintf("pad=%d:%d:(ow-iw)/2:(oh-ih)/2", width, height),
|
||||||
|
fmt.Sprintf("setdar=%s", aspect),
|
||||||
|
"setsar=1",
|
||||||
|
fmt.Sprintf("fps=%s", fps),
|
||||||
|
}
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
"-y",
|
||||||
|
"-hide_banner",
|
||||||
|
"-loglevel", "error",
|
||||||
|
"-i", inputPath,
|
||||||
|
"-vf", strings.Join(vf, ","),
|
||||||
|
"-c:v", "mpeg2video",
|
||||||
|
"-r", fps,
|
||||||
|
"-b:v", bitrate,
|
||||||
|
"-maxrate", maxrate,
|
||||||
|
"-bufsize", "1835k",
|
||||||
|
"-g", gop,
|
||||||
|
"-pix_fmt", "yuv420p",
|
||||||
|
}
|
||||||
|
|
||||||
|
if !progressive {
|
||||||
|
args = append(args, "-flags", "+ilme+ildct")
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args,
|
||||||
|
"-c:a", "ac3",
|
||||||
|
"-b:a", "192k",
|
||||||
|
"-ar", "48000",
|
||||||
|
"-ac", "2",
|
||||||
|
outputPath,
|
||||||
|
)
|
||||||
|
|
||||||
|
return args
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeDVDAuthorXML(path string, mpgPaths []string, region, aspect string) error {
|
||||||
|
format := strings.ToLower(region)
|
||||||
|
if format != "pal" {
|
||||||
|
format = "ntsc"
|
||||||
|
}
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString("<dvdauthor>\n")
|
||||||
|
b.WriteString(" <vmgm />\n")
|
||||||
|
b.WriteString(" <titleset>\n")
|
||||||
|
b.WriteString(" <titles>\n")
|
||||||
|
b.WriteString(fmt.Sprintf(" <video format=\"%s\" aspect=\"%s\" />\n", format, aspect))
|
||||||
|
for _, mpg := range mpgPaths {
|
||||||
|
b.WriteString(" <pgc>\n")
|
||||||
|
b.WriteString(fmt.Sprintf(" <vob file=\"%s\" />\n", escapeXMLAttr(mpg)))
|
||||||
|
b.WriteString(" </pgc>\n")
|
||||||
|
}
|
||||||
|
b.WriteString(" </titles>\n")
|
||||||
|
b.WriteString(" </titleset>\n")
|
||||||
|
b.WriteString("</dvdauthor>\n")
|
||||||
|
|
||||||
|
if err := os.WriteFile(path, []byte(b.String()), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to write dvdauthor XML: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeXMLAttr(value string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
if err := xml.EscapeText(&b, []byte(value)); err != nil {
|
||||||
|
return strings.ReplaceAll(value, "\"", """)
|
||||||
|
}
|
||||||
|
escaped := b.String()
|
||||||
|
return strings.ReplaceAll(escaped, "\"", """)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureAuthorDependencies(makeISO bool) error {
|
||||||
|
if err := ensureExecutable(platformConfig.FFmpegPath, "ffmpeg"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath("dvdauthor"); err != nil {
|
||||||
|
return fmt.Errorf("dvdauthor not found in PATH")
|
||||||
|
}
|
||||||
|
if makeISO {
|
||||||
|
if _, _, err := buildISOCommand("output.iso", "output", "VIDEO_TOOLS"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureExecutable(path, label string) error {
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
if _, err := os.Stat(path); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := exec.LookPath(path); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("%s not found (%s)", label, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildISOCommand(outputISO, discRoot, title string) (string, []string, error) {
|
||||||
|
tool, prefixArgs, err := findISOTool()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
label := isoVolumeLabel(title)
|
||||||
|
args := append([]string{}, prefixArgs...)
|
||||||
|
args = append(args, "-dvd-video", "-V", label, "-o", outputISO, discRoot)
|
||||||
|
return tool, args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func findISOTool() (string, []string, error) {
|
||||||
|
if path, err := exec.LookPath("mkisofs"); err == nil {
|
||||||
|
return path, nil, nil
|
||||||
|
}
|
||||||
|
if path, err := exec.LookPath("genisoimage"); err == nil {
|
||||||
|
return path, nil, nil
|
||||||
|
}
|
||||||
|
if path, err := exec.LookPath("xorriso"); err == nil {
|
||||||
|
return path, []string{"-as", "mkisofs"}, nil
|
||||||
|
}
|
||||||
|
return "", nil, fmt.Errorf("mkisofs, genisoimage, or xorriso not found in PATH")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isoVolumeLabel(title string) string {
|
||||||
|
label := strings.ToUpper(strings.TrimSpace(title))
|
||||||
|
if label == "" {
|
||||||
|
label = "VIDEO_TOOLS"
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
for _, r := range label {
|
||||||
|
switch {
|
||||||
|
case r >= 'A' && r <= 'Z':
|
||||||
|
b.WriteRune(r)
|
||||||
|
case r >= '0' && r <= '9':
|
||||||
|
b.WriteRune(r)
|
||||||
|
case r == '_' || r == '-':
|
||||||
|
b.WriteRune('_')
|
||||||
|
default:
|
||||||
|
b.WriteRune('_')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clean := strings.Trim(b.String(), "_")
|
||||||
|
if clean == "" {
|
||||||
|
clean = "VIDEO_TOOLS"
|
||||||
|
}
|
||||||
|
if len(clean) > 32 {
|
||||||
|
clean = clean[:32]
|
||||||
|
}
|
||||||
|
return clean
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommand(name string, args []string) error {
|
||||||
|
cmd := exec.Command(name, args...)
|
||||||
|
utils.ApplyNoWindow(cmd)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s failed: %s", name, strings.TrimSpace(string(output)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runOnUI(fn func()) {
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
@ -1,334 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"fyne.io/fyne/v2"
|
|
||||||
"fyne.io/fyne/v2/container"
|
|
||||||
"fyne.io/fyne/v2/dialog"
|
|
||||||
"fyne.io/fyne/v2/widget"
|
|
||||||
"git.leaktechnologies.dev/stu/VideoTools/internal/ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
// buildVideoClipsTab creates the video clips tab with drag-and-drop support
|
|
||||||
func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Video clips list with drag-and-drop support
|
|
||||||
list := container.NewVBox()
|
|
||||||
|
|
||||||
rebuildList := func() {
|
|
||||||
list.Objects = nil
|
|
||||||
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
emptyLabel := widget.NewLabel("Drag and drop video files here\nor click 'Add Files' to select videos")
|
|
||||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
// Make empty state a drop target
|
|
||||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
|
||||||
var paths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() == "file" {
|
|
||||||
paths = append(paths, uri.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
|
||||||
state.addAuthorFiles(paths)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
} else {
|
|
||||||
for i, clip := range state.authorClips {
|
|
||||||
idx := i
|
|
||||||
card := widget.NewCard(clip.DisplayName, fmt.Sprintf("%.2fs", clip.Duration), nil)
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
removeBtn := widget.NewButton("Remove", func() {
|
|
||||||
state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
|
|
||||||
rebuildList()
|
|
||||||
})
|
|
||||||
removeBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Duration label
|
|
||||||
durationLabel := widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", clip.Duration))
|
|
||||||
durationLabel.TextStyle = fyne.TextStyle{Italic: true}
|
|
||||||
|
|
||||||
cardContent := container.NewVBox(
|
|
||||||
durationLabel,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
removeBtn,
|
|
||||||
)
|
|
||||||
card.SetContent(cardContent)
|
|
||||||
list.Add(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add files button
|
|
||||||
addBtn := widget.NewButton("Add Files", func() {
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
state.addAuthorFiles([]string{reader.URI().Path()})
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
addBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Clear all button
|
|
||||||
clearBtn := widget.NewButton("Clear All", func() {
|
|
||||||
state.authorClips = []authorClip{}
|
|
||||||
rebuildList()
|
|
||||||
})
|
|
||||||
clearBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Compile button
|
|
||||||
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Implement compilation to DVD
|
|
||||||
dialog.ShowInformation("Compile", "DVD compilation will be implemented", state.window)
|
|
||||||
})
|
|
||||||
compileBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Video Clips:"),
|
|
||||||
container.NewScroll(list),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(addBtn, clearBtn, compileBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize the list
|
|
||||||
rebuildList()
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addAuthorFiles helper function
|
|
||||||
func (s *appState) addAuthorFiles(paths []string) {
|
|
||||||
for _, path := range paths {
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
clip := authorClip{
|
|
||||||
Path: path,
|
|
||||||
DisplayName: filepath.Base(path),
|
|
||||||
Duration: src.Duration,
|
|
||||||
Chapters: []authorChapter{},
|
|
||||||
}
|
|
||||||
s.authorClips = append(s.authorClips, clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildSubtitlesTab creates the subtitles tab with drag-and-drop support
|
|
||||||
func buildSubtitlesTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Subtitle files list with drag-and-drop support
|
|
||||||
list := container.NewVBox()
|
|
||||||
|
|
||||||
rebuildSubList := func() {
|
|
||||||
list.Objects = nil
|
|
||||||
|
|
||||||
if len(state.authorSubtitles) == 0 {
|
|
||||||
emptyLabel := widget.NewLabel("Drag and drop subtitle files here\nor click 'Add Subtitles' to select")
|
|
||||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
// Make empty state a drop target
|
|
||||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
|
||||||
var paths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() == "file" {
|
|
||||||
paths = append(paths, uri.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles, paths...)
|
|
||||||
rebuildSubList()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
} else {
|
|
||||||
for i, path := range state.authorSubtitles {
|
|
||||||
idx := i
|
|
||||||
card := widget.NewCard(filepath.Base(path), "", nil)
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
removeBtn := widget.NewButton("Remove", func() {
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles[:idx], state.authorSubtitles[idx+1:]...)
|
|
||||||
rebuildSubList()
|
|
||||||
})
|
|
||||||
removeBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
cardContent := container.NewVBox(removeBtn)
|
|
||||||
card.SetContent(cardContent)
|
|
||||||
list.Add(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add subtitles button
|
|
||||||
addBtn := widget.NewButton("Add Subtitles", func() {
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles, reader.URI().Path())
|
|
||||||
rebuildSubList()
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
addBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Clear all button
|
|
||||||
clearBtn := widget.NewButton("Clear All", func() {
|
|
||||||
state.authorSubtitles = []string{}
|
|
||||||
rebuildSubList()
|
|
||||||
})
|
|
||||||
clearBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Subtitle Tracks:"),
|
|
||||||
container.NewScroll(list),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(addBtn, clearBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
rebuildSubList()
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildAuthorSettingsTab creates the author settings tab
|
|
||||||
func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Output type selection
|
|
||||||
outputType := widget.NewSelect([]string{"DVD (VIDEO_TS)", "ISO Image"})
|
|
||||||
outputType.OnChanged = func(value string) {
|
|
||||||
if value == "DVD (VIDEO_TS)" {
|
|
||||||
state.authorOutputType = "dvd"
|
|
||||||
} else {
|
|
||||||
state.authorOutputType = "iso"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if state.authorOutputType == "iso" {
|
|
||||||
outputType.SetSelected("ISO Image")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Region selection
|
|
||||||
regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"})
|
|
||||||
regionSelect.OnChanged = func(value string) {
|
|
||||||
state.authorRegion = value
|
|
||||||
})
|
|
||||||
if state.authorRegion == "" {
|
|
||||||
state.authorRegion = "AUTO"
|
|
||||||
regionSelect.SetSelected("AUTO")
|
|
||||||
} else {
|
|
||||||
regionSelect.SetSelected(state.authorRegion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aspect ratio selection
|
|
||||||
aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"})
|
|
||||||
aspectSelect.OnChanged = func(value string) {
|
|
||||||
state.authorAspectRatio = value
|
|
||||||
})
|
|
||||||
if state.authorAspectRatio == "" {
|
|
||||||
state.authorAspectRatio = "AUTO"
|
|
||||||
aspectSelect.SetSelected("AUTO")
|
|
||||||
} else {
|
|
||||||
aspectSelect.SetSelected(state.authorAspectRatio)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DVD title entry
|
|
||||||
titleEntry := widget.NewEntry()
|
|
||||||
titleEntry.SetPlaceHolder("DVD Title")
|
|
||||||
titleEntry.SetText(state.authorTitle)
|
|
||||||
titleEntry.OnChanged = func(value string) {
|
|
||||||
state.authorTitle = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create menu checkbox
|
|
||||||
createMenuCheck := widget.NewCheck("Create DVD Menu", func(checked bool) {
|
|
||||||
state.authorCreateMenu = checked
|
|
||||||
})
|
|
||||||
createMenuCheck.SetChecked(state.authorCreateMenu)
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Output Settings:"),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
widget.NewLabel("Output Type:"),
|
|
||||||
outputType,
|
|
||||||
widget.NewLabel("Region:"),
|
|
||||||
regionSelect,
|
|
||||||
widget.NewLabel("Aspect Ratio:"),
|
|
||||||
aspectSelect,
|
|
||||||
widget.NewLabel("DVD Title:"),
|
|
||||||
titleEntry,
|
|
||||||
createMenuCheck,
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildAuthorDiscTab creates the DVD generation tab
|
|
||||||
func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Generate DVD/ISO
|
|
||||||
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
dialog.ShowInformation("No Content", "Please add video clips first", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show compilation options
|
|
||||||
dialog.ShowInformation("DVD Generation",
|
|
||||||
"DVD/ISO generation will be implemented in next step.\n\n"+
|
|
||||||
"Features planned:\n"+
|
|
||||||
"• Create VIDEO_TS folder structure\n"+
|
|
||||||
"• Generate burn-ready ISO\n"+
|
|
||||||
"• Include subtitle tracks\n"+
|
|
||||||
"• Include alternate audio tracks\n"+
|
|
||||||
"• Support for alternate camera angles", state.window)
|
|
||||||
})
|
|
||||||
generateBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Show summary
|
|
||||||
summary := "Ready to generate:\n\n"
|
|
||||||
if len(state.authorClips) > 0 {
|
|
||||||
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
|
||||||
for i, clip := range state.authorClips {
|
|
||||||
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.authorSubtitles) > 0 {
|
|
||||||
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
|
||||||
for i, path := range state.authorSubtitles {
|
|
||||||
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
|
||||||
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
|
||||||
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
|
||||||
if state.authorTitle != "" {
|
|
||||||
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryLabel := widget.NewLabel(summary)
|
|
||||||
summaryLabel.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Generate DVD/ISO:"),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
summaryLabel,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
generateBtn,
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
- **Cross-compilation script** (`scripts/build-windows.sh`)
|
- **Cross-compilation script** (`scripts/build-windows.sh`)
|
||||||
|
|
||||||
#### Professional Installation System
|
#### Professional Installation System
|
||||||
- **One-command installer** (`install.sh`) with guided wizard
|
- **One-command installer** (`scripts/install.sh`) with guided wizard
|
||||||
- **Automatic shell detection** (bash/zsh) and configuration
|
- **Automatic shell detection** (bash/zsh) and configuration
|
||||||
- **System-wide vs user-local installation** options
|
- **System-wide vs user-local installation** options
|
||||||
- **Convenience aliases** (`VideoTools`, `VideoToolsRebuild`, `VideoToolsClean`)
|
- **Convenience aliases** (`VideoTools`, `VideoToolsRebuild`, `VideoToolsClean`)
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ This guide will help you install VideoTools with minimal setup.
|
||||||
### One-Command Installation
|
### One-Command Installation
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
That's it! The installer will:
|
That's it! The installer will:
|
||||||
|
|
@ -43,7 +43,7 @@ VideoTools
|
||||||
### Option 1: System-Wide Installation (Recommended for Shared Computers)
|
### Option 1: System-Wide Installation (Recommended for Shared Computers)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
# Select option 1 when prompted
|
# Select option 1 when prompted
|
||||||
# Enter your password if requested
|
# Enter your password if requested
|
||||||
```
|
```
|
||||||
|
|
@ -61,7 +61,7 @@ bash install.sh
|
||||||
### Option 2: User-Local Installation (Recommended for Personal Use)
|
### Option 2: User-Local Installation (Recommended for Personal Use)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
# Select option 2 when prompted (default)
|
# Select option 2 when prompted (default)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -78,7 +78,7 @@ bash install.sh
|
||||||
|
|
||||||
## What the Installer Does
|
## What the Installer Does
|
||||||
|
|
||||||
The `install.sh` script performs these steps:
|
The `scripts/install.sh` script performs these steps:
|
||||||
|
|
||||||
### Step 1: Go Verification
|
### Step 1: Go Verification
|
||||||
- Checks if Go 1.21+ is installed
|
- Checks if Go 1.21+ is installed
|
||||||
|
|
@ -122,6 +122,19 @@ VideoToolsClean # Clean build artifacts and cache
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
For day-to-day development:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/build.sh
|
||||||
|
./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `./scripts/install.sh` when you add new system dependencies or want to reinstall.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
### Essential
|
### Essential
|
||||||
|
|
@ -135,7 +148,7 @@ VideoToolsClean # Clean build artifacts and cache
|
||||||
```
|
```
|
||||||
|
|
||||||
### System
|
### System
|
||||||
- Linux, macOS, or WSL (Windows Subsystem for Linux)
|
- Linux, macOS, or Windows (native)
|
||||||
- At least 2 GB free disk space
|
- At least 2 GB free disk space
|
||||||
- Stable internet connection (for dependencies)
|
- Stable internet connection (for dependencies)
|
||||||
|
|
||||||
|
|
@ -157,7 +170,7 @@ go version
|
||||||
**Solution:** Check build log for specific errors:
|
**Solution:** Check build log for specific errors:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
# Look for error messages in the build log output
|
# Look for error messages in the build log output
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -356,4 +369,3 @@ Installation works in WSL environment. Ensure you have WSL with Linux distro ins
|
||||||
---
|
---
|
||||||
|
|
||||||
Enjoy using VideoTools! 🎬
|
Enjoy using VideoTools! 🎬
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ The queue view now displays:
|
||||||
|
|
||||||
### New Files
|
### New Files
|
||||||
|
|
||||||
1. **Enhanced `install.sh`** - One-command installation
|
1. **Enhanced `scripts/install.sh`** - One-command installation
|
||||||
2. **New `INSTALLATION.md`** - Comprehensive installation guide
|
2. **New `INSTALLATION.md`** - Comprehensive installation guide
|
||||||
|
|
||||||
### install.sh Features
|
### install.sh Features
|
||||||
|
|
@ -96,7 +96,7 @@ The queue view now displays:
|
||||||
The installer now performs all setup automatically:
|
The installer now performs all setup automatically:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
This handles:
|
This handles:
|
||||||
|
|
@ -113,13 +113,13 @@ This handles:
|
||||||
|
|
||||||
**Option 1: System-Wide (for shared computers)**
|
**Option 1: System-Wide (for shared computers)**
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
# Select option 1 when prompted
|
# Select option 1 when prompted
|
||||||
```
|
```
|
||||||
|
|
||||||
**Option 2: User-Local (default, no sudo required)**
|
**Option 2: User-Local (default, no sudo required)**
|
||||||
```bash
|
```bash
|
||||||
bash install.sh
|
bash scripts/install.sh
|
||||||
# Select option 2 when prompted (or just press Enter)
|
# Select option 2 when prompted (or just press Enter)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -235,7 +235,7 @@ All features are built and ready:
|
||||||
3. Test reordering with up/down arrows
|
3. Test reordering with up/down arrows
|
||||||
|
|
||||||
### For Testing Installation
|
### For Testing Installation
|
||||||
1. Run `bash install.sh` on a clean system
|
1. Run `bash scripts/install.sh` on a clean system
|
||||||
2. Verify binary is in PATH
|
2. Verify binary is in PATH
|
||||||
3. Verify aliases are available
|
3. Verify aliases are available
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,20 @@ Get VideoTools running in minutes!
|
||||||
cd VideoTools
|
cd VideoTools
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Run the setup script**:
|
2. **Install dependencies and build** (Git Bash or similar):
|
||||||
- Double-click `setup-windows.bat`
|
```bash
|
||||||
- OR run in PowerShell:
|
./scripts/install.sh
|
||||||
```powershell
|
|
||||||
.\scripts\setup-windows.ps1 -Portable
|
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Done!** FFmpeg will be downloaded automatically and VideoTools will be ready to run.
|
Or install Windows dependencies directly:
|
||||||
|
```powershell
|
||||||
|
.\scripts\install-deps-windows.ps1
|
||||||
|
```
|
||||||
|
|
||||||
4. **Launch VideoTools**:
|
3. **Run VideoTools**:
|
||||||
- Navigate to `dist/windows/`
|
```bash
|
||||||
- Double-click `VideoTools.exe`
|
./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
### If You Need to Build
|
### If You Need to Build
|
||||||
|
|
||||||
|
|
@ -70,14 +72,14 @@ If `VideoTools.exe` doesn't exist yet:
|
||||||
sudo pacman -S ffmpeg
|
sudo pacman -S ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Build VideoTools**:
|
3. **Install dependencies and build**:
|
||||||
```bash
|
```bash
|
||||||
./scripts/build.sh
|
./scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Run**:
|
4. **Run**:
|
||||||
```bash
|
```bash
|
||||||
./VideoTools
|
./scripts/run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Cross-Compile for Windows from Linux
|
### Cross-Compile for Windows from Linux
|
||||||
|
|
@ -112,16 +114,16 @@ sudo apt install gcc-mingw-w64 # Ubuntu/Debian
|
||||||
brew install ffmpeg
|
brew install ffmpeg
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Clone and build**:
|
3. **Clone and install dependencies/build**:
|
||||||
```bash
|
```bash
|
||||||
git clone <repository-url>
|
git clone <repository-url>
|
||||||
cd VideoTools
|
cd VideoTools
|
||||||
go build -o VideoTools
|
./scripts/install.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
4. **Run**:
|
4. **Run**:
|
||||||
```bash
|
```bash
|
||||||
./VideoTools
|
./scripts/run.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
934
main.go
934
main.go
|
|
@ -2741,21 +2741,10 @@ func (s *appState) showMergeView() {
|
||||||
paths = append(paths, uri.Path())
|
paths = append(paths, uri.Path())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make empty state a drop target
|
|
||||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
|
||||||
var paths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() == "file" {
|
|
||||||
paths = append(paths, uri.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
if len(paths) > 0 {
|
||||||
state.addAuthorFiles(paths)
|
addFiles(paths)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
listBox.Add(container.NewMax(emptyDrop))
|
listBox.Add(container.NewMax(emptyDrop))
|
||||||
} else {
|
} else {
|
||||||
for i, c := range s.mergeClips {
|
for i, c := range s.mergeClips {
|
||||||
|
|
@ -13994,927 +13983,6 @@ func parseResolutionPreset(preset string, srcW, srcH int) (width, height int, pr
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildUpscaleFilter builds the FFmpeg scale filter string with the selected method
|
// buildUpscaleFilter builds the FFmpeg scale filter string with the selected method
|
||||||
func buildAuthorView(state *appState) fyne.CanvasObject {
|
|
||||||
state.stopPreview()
|
|
||||||
state.lastModule = state.active
|
|
||||||
state.active = "author"
|
|
||||||
|
|
||||||
// Initialize default values
|
|
||||||
if state.authorOutputType == "" {
|
|
||||||
state.authorOutputType = "dvd"
|
|
||||||
}
|
|
||||||
if state.authorRegion == "" {
|
|
||||||
state.authorRegion = "AUTO"
|
|
||||||
}
|
|
||||||
if state.authorAspectRatio == "" {
|
|
||||||
state.authorAspectRatio = "AUTO"
|
|
||||||
}
|
|
||||||
|
|
||||||
authorColor := moduleColor("author")
|
|
||||||
|
|
||||||
// Back button
|
|
||||||
backBtn := widget.NewButton("< BACK", func() {
|
|
||||||
state.showMainMenu()
|
|
||||||
})
|
|
||||||
backBtn.Importance = widget.LowImportance
|
|
||||||
|
|
||||||
// Queue button
|
|
||||||
queueBtn := widget.NewButton("View Queue", func() {
|
|
||||||
state.showQueue()
|
|
||||||
})
|
|
||||||
state.queueBtn = queueBtn
|
|
||||||
state.updateQueueButtonLabel()
|
|
||||||
|
|
||||||
topBar := ui.TintedBar(authorColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
|
|
||||||
bottomBar := moduleFooter(authorColor, layout.NewSpacer(), state.statsBar)
|
|
||||||
|
|
||||||
// Create tabs for different authoring tasks
|
|
||||||
tabs := container.NewAppTabs(
|
|
||||||
container.NewTabItem("Video Clips", buildVideoClipsTab(state)),
|
|
||||||
container.NewTabItem("Chapters", buildChaptersTab(state)),
|
|
||||||
container.NewTabItem("Subtitles", buildSubtitlesTab(state)),
|
|
||||||
container.NewTabItem("Settings", buildAuthorSettingsTab(state)),
|
|
||||||
container.NewTabItem("Generate", buildAuthorDiscTab(state)),
|
|
||||||
)
|
|
||||||
tabs.SetTabLocation(container.TabLocationTop)
|
|
||||||
|
|
||||||
return container.NewBorder(topBar, bottomBar, nil, nil, tabs)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Video clips list with drag-and-drop support
|
|
||||||
list := container.NewVBox()
|
|
||||||
|
|
||||||
rebuildList := func() {
|
|
||||||
list.Objects = nil
|
|
||||||
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
emptyLabel := widget.NewLabel("Drag and drop video files here\nor click 'Add Files' to select videos")
|
|
||||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
// Make empty state a drop target
|
|
||||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
|
||||||
var paths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() == "file" {
|
|
||||||
paths = append(paths, uri.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
|
||||||
state.addAuthorFiles(paths)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
} else {
|
|
||||||
for i, clip := range state.authorClips {
|
|
||||||
idx := i
|
|
||||||
card := widget.NewCard(clip.DisplayName, fmt.Sprintf("%.2fs", clip.Duration), nil)
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
removeBtn := widget.NewButton("Remove", func() {
|
|
||||||
state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
|
|
||||||
rebuildList()
|
|
||||||
})
|
|
||||||
removeBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Duration label
|
|
||||||
durationLabel := widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", clip.Duration))
|
|
||||||
durationLabel.TextStyle = fyne.TextStyle{Italic: true}
|
|
||||||
|
|
||||||
cardContent := container.NewVBox(
|
|
||||||
durationLabel,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
removeBtn,
|
|
||||||
)
|
|
||||||
card.SetContent(cardContent)
|
|
||||||
list.Add(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add files button
|
|
||||||
addBtn := widget.NewButton("Add Files", func() {
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
state.addAuthorFiles([]string{reader.URI().Path()})
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
addBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Clear all button
|
|
||||||
clearBtn := widget.NewButton("Clear All", func() {
|
|
||||||
state.authorClips = []authorClip{}
|
|
||||||
rebuildList()
|
|
||||||
})
|
|
||||||
clearBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Compile button
|
|
||||||
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Implement compilation to DVD
|
|
||||||
dialog.ShowInformation("Compile", "DVD compilation will be implemented", state.window)
|
|
||||||
})
|
|
||||||
compileBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Video Clips:"),
|
|
||||||
container.NewScroll(list),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(addBtn, clearBtn, compileBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize the list
|
|
||||||
rebuildList()
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addAuthorFiles helper function
|
|
||||||
func (s *appState) addAuthorFiles(paths []string) {
|
|
||||||
for _, path := range paths {
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
clip := authorClip{
|
|
||||||
Path: path,
|
|
||||||
DisplayName: filepath.Base(path),
|
|
||||||
Duration: src.Duration,
|
|
||||||
Chapters: []authorChapter{},
|
|
||||||
}
|
|
||||||
s.authorClips = append(s.authorClips, clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
|
||||||
addFiles(paths)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
} else {
|
|
||||||
for i, clip := range state.authorClips {
|
|
||||||
idx := i
|
|
||||||
clip := widget.NewCard(clip.DisplayName, fmt.Sprintf("%.2fs", clip.Duration), nil)
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
removeBtn := widget.NewButton("Remove", func() {
|
|
||||||
state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
|
|
||||||
buildList()
|
|
||||||
})
|
|
||||||
removeBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Duration label
|
|
||||||
durationLabel := widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", clip.Duration))
|
|
||||||
durationLabel.TextStyle = fyne.TextStyle{Italic: true}
|
|
||||||
|
|
||||||
cardContent := container.NewVBox(
|
|
||||||
durationLabel,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
removeBtn,
|
|
||||||
)
|
|
||||||
clip.SetContent(cardContent)
|
|
||||||
list.Add(clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addFiles := func(paths []string) {
|
|
||||||
for _, path := range paths {
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), state.window)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
clip := authorClip{
|
|
||||||
Path: path,
|
|
||||||
DisplayName: filepath.Base(path),
|
|
||||||
Duration: src.Duration,
|
|
||||||
Chapters: []authorChapter{},
|
|
||||||
}
|
|
||||||
state.authorClips = append(state.authorClips, clip)
|
|
||||||
}
|
|
||||||
buildList()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
clip := authorClip{
|
|
||||||
Path: path,
|
|
||||||
DisplayName: filepath.Base(path),
|
|
||||||
Duration: src.Duration,
|
|
||||||
Chapters: []authorChapter{},
|
|
||||||
}
|
|
||||||
state.authorClips = append(state.authorClips, clip)
|
|
||||||
}
|
|
||||||
buildList()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add files button
|
|
||||||
addBtn := widget.NewButton("Add Files", func() {
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
addFiles([]string{reader.URI().Path()})
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
addBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Clear all button
|
|
||||||
clearBtn := widget.NewButton("Clear All", func() {
|
|
||||||
state.authorClips = []authorClip{}
|
|
||||||
buildList()
|
|
||||||
})
|
|
||||||
clearBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Compile button
|
|
||||||
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Implement compilation to DVD
|
|
||||||
dialog.ShowInformation("Compile", "DVD compilation will be implemented", state.window)
|
|
||||||
})
|
|
||||||
compileBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Video Clips:"),
|
|
||||||
container.NewScroll(list),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(addBtn, clearBtn, compileBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize the list
|
|
||||||
buildList()
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildChaptersTab(state *appState) fyne.CanvasObject {
|
|
||||||
var fileLabel *widget.Label
|
|
||||||
if state.authorFile != nil {
|
|
||||||
fileLabel = widget.NewLabel(fmt.Sprintf("File: %s", filepath.Base(state.authorFile.Path)))
|
|
||||||
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
|
|
||||||
} else {
|
|
||||||
fileLabel = widget.NewLabel("Select a single video file or use clips from Video Clips tab")
|
|
||||||
}
|
|
||||||
|
|
||||||
selectBtn := widget.NewButton("Select Video", func() {
|
|
||||||
dialog.ShowFileOpen(func(uc fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || uc == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer uc.Close()
|
|
||||||
path := uc.URI().Path()
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
state.authorFile = src
|
|
||||||
fileLabel.SetText(fmt.Sprintf("File: %s", filepath.Base(src.Path)))
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Scene detection threshold
|
|
||||||
thresholdLabel := widget.NewLabel(fmt.Sprintf("Detection Sensitivity: %.2f", state.authorSceneThreshold))
|
|
||||||
thresholdSlider := widget.NewSlider(0.1, 0.9)
|
|
||||||
thresholdSlider.Value = state.authorSceneThreshold
|
|
||||||
thresholdSlider.Step = 0.05
|
|
||||||
thresholdSlider.OnChanged = func(v float64) {
|
|
||||||
state.authorSceneThreshold = v
|
|
||||||
thresholdLabel.SetText(fmt.Sprintf("Detection Sensitivity: %.2f", v))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect scenes button
|
|
||||||
detectBtn := widget.NewButton("Detect Scenes", func() {
|
|
||||||
if state.authorFile == nil && len(state.authorClips) == 0 {
|
|
||||||
dialog.ShowInformation("No File", "Please select a video file first", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Implement scene detection
|
|
||||||
dialog.ShowInformation("Scene Detection", "Scene detection will be implemented", state.window)
|
|
||||||
})
|
|
||||||
detectBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Chapter list (placeholder)
|
|
||||||
chapterList := widget.NewLabel("No chapters detected yet")
|
|
||||||
|
|
||||||
// Add manual chapter button
|
|
||||||
addChapterBtn := widget.NewButton("+ Add Chapter", func() {
|
|
||||||
// TODO: Implement manual chapter addition
|
|
||||||
dialog.ShowInformation("Add Chapter", "Manual chapter addition will be implemented", state.window)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Export chapters button
|
|
||||||
exportBtn := widget.NewButton("Export Chapters", func() {
|
|
||||||
// TODO: Implement chapter export
|
|
||||||
dialog.ShowInformation("Export", "Chapter export will be implemented", state.window)
|
|
||||||
})
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
fileLabel,
|
|
||||||
selectBtn,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
widget.NewLabel("Scene Detection:"),
|
|
||||||
thresholdLabel,
|
|
||||||
thresholdSlider,
|
|
||||||
detectBtn,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
widget.NewLabel("Chapters:"),
|
|
||||||
container.NewScroll(chapterList),
|
|
||||||
container.NewHBox(addChapterBtn, exportBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildRipTab(state *appState) fyne.CanvasObject {
|
|
||||||
placeholder := widget.NewLabel("DVD/ISO ripping will be implemented here.\n\nFeatures:\n• Mount and scan DVD/ISO\n• Select titles and tracks\n• Rip at highest quality (like FLAC from CD)\n• Preserve all audio and subtitle tracks")
|
|
||||||
placeholder.Wrapping = fyne.TextWrapWord
|
|
||||||
return container.NewCenter(placeholder)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addAuthorFiles helper function
|
|
||||||
func (s *appState) addAuthorFiles(paths []string) {
|
|
||||||
for _, path := range paths {
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
clip := authorClip{
|
|
||||||
Path: path,
|
|
||||||
DisplayName: filepath.Base(path),
|
|
||||||
Duration: src.Duration,
|
|
||||||
Chapters: []authorChapter{},
|
|
||||||
}
|
|
||||||
s.authorClips = append(s.authorClips, clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addAuthorFiles helper function
|
|
||||||
func (s *appState) addAuthorFiles(paths []string) {
|
|
||||||
for _, path := range paths {
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
clip := authorClip{
|
|
||||||
Path: path,
|
|
||||||
DisplayName: filepath.Base(path),
|
|
||||||
Duration: src.Duration,
|
|
||||||
Chapters: []authorChapter{},
|
|
||||||
}
|
|
||||||
s.authorClips = append(s.authorClips, clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSubtitlesTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Subtitle files list with drag-and-drop support
|
|
||||||
list := container.NewVBox()
|
|
||||||
|
|
||||||
var buildSubList func()
|
|
||||||
buildSubList = func() {
|
|
||||||
list.Objects = nil
|
|
||||||
|
|
||||||
if len(state.authorSubtitles) == 0 {
|
|
||||||
emptyLabel := widget.NewLabel("Drag and drop subtitle files here\nor click 'Add Subtitles' to select")
|
|
||||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
// Make empty state a drop target
|
|
||||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
|
||||||
var paths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() == "file" {
|
|
||||||
paths = append(paths, uri.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles, paths...)
|
|
||||||
buildSubList()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
} else {
|
|
||||||
for i, path := range state.authorSubtitles {
|
|
||||||
idx := i
|
|
||||||
card := widget.NewCard(filepath.Base(path), "", nil)
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
removeBtn := widget.NewButton("Remove", func() {
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles[:idx], state.authorSubtitles[idx+1:]...)
|
|
||||||
buildSubList()
|
|
||||||
})
|
|
||||||
removeBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
cardContent := container.NewVBox(removeBtn)
|
|
||||||
card.SetContent(cardContent)
|
|
||||||
list.Add(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add subtitles button
|
|
||||||
addBtn := widget.NewButton("Add Subtitles", func() {
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles, reader.URI().Path())
|
|
||||||
buildSubList()
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
addBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Clear all button
|
|
||||||
clearBtn := widget.NewButton("Clear All", func() {
|
|
||||||
state.authorSubtitles = []string{}
|
|
||||||
buildSubList()
|
|
||||||
})
|
|
||||||
clearBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Subtitle Tracks:"),
|
|
||||||
container.NewScroll(list),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(addBtn, clearBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
buildSubList()
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Output type selection
|
|
||||||
outputType := widget.NewSelect([]string{"DVD (VIDEO_TS)", "ISO Image"})
|
|
||||||
if state.authorOutputType == "iso" {
|
|
||||||
outputType.SetSelected("ISO Image")
|
|
||||||
}
|
|
||||||
outputType.OnChanged = func(value string) {
|
|
||||||
if value == "DVD (VIDEO_TS)" {
|
|
||||||
state.authorOutputType = "dvd"
|
|
||||||
} else {
|
|
||||||
state.authorOutputType = "iso"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Region selection
|
|
||||||
regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"}, func(value string) {
|
|
||||||
state.authorRegion = value
|
|
||||||
})
|
|
||||||
if state.authorRegion == "" {
|
|
||||||
regionSelect.SetSelected("AUTO")
|
|
||||||
} else {
|
|
||||||
regionSelect.SetSelected(state.authorRegion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aspect ratio selection
|
|
||||||
aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"}, func(value string) {
|
|
||||||
state.authorAspectRatio = value
|
|
||||||
})
|
|
||||||
if state.authorAspectRatio == "" {
|
|
||||||
aspectSelect.SetSelected("AUTO")
|
|
||||||
} else {
|
|
||||||
aspectSelect.SetSelected(state.authorAspectRatio)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DVD title entry
|
|
||||||
titleEntry := widget.NewEntry()
|
|
||||||
titleEntry.SetPlaceHolder("DVD Title")
|
|
||||||
titleEntry.SetText(state.authorTitle)
|
|
||||||
titleEntry.OnChanged = func(value string) {
|
|
||||||
state.authorTitle = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create menu checkbox
|
|
||||||
createMenuCheck := widget.NewCheck("Create DVD Menu", state.authorCreateMenu)
|
|
||||||
createMenuCheck.OnChanged = func(checked bool) {
|
|
||||||
state.authorCreateMenu = checked
|
|
||||||
}
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Output Settings:"),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
widget.NewLabel("Output Type:"),
|
|
||||||
outputType,
|
|
||||||
widget.NewLabel("Region:"),
|
|
||||||
regionSelect,
|
|
||||||
widget.NewLabel("Aspect Ratio:"),
|
|
||||||
aspectSelect,
|
|
||||||
widget.NewLabel("DVD Title:"),
|
|
||||||
titleEntry,
|
|
||||||
createMenuCheck,
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Generate DVD/ISO
|
|
||||||
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
|
||||||
if len(state.authorClips) == 0 && state.authorFile == nil {
|
|
||||||
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show compilation options
|
|
||||||
dialog.ShowFormConfirm("Generate DVD",
|
|
||||||
"Choose generation options:",
|
|
||||||
func(callback bool, options map[string]interface{}) {
|
|
||||||
if !callback {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Implement actual DVD/ISO generation
|
|
||||||
dialog.ShowInformation("DVD Generation", "DVD/ISO generation will be implemented in next step", state.window)
|
|
||||||
},
|
|
||||||
map[string]string{
|
|
||||||
"include_subtitles": "Include Subtitles",
|
|
||||||
"include_chapters": "Include Chapters",
|
|
||||||
"preserve_quality": "Preserve Original Quality",
|
|
||||||
},
|
|
||||||
map[string]interface{}{
|
|
||||||
"include_subtitles": len(state.authorSubtitles) > 0,
|
|
||||||
"include_chapters": len(state.authorChapters) > 0,
|
|
||||||
"preserve_quality": true,
|
|
||||||
},
|
|
||||||
state.window)
|
|
||||||
})
|
|
||||||
generateBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Show summary
|
|
||||||
summary := "Ready to generate:\n\n"
|
|
||||||
if len(state.authorClips) > 0 {
|
|
||||||
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
|
||||||
for i, clip := range state.authorClips {
|
|
||||||
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
|
||||||
}
|
|
||||||
} else if state.authorFile != nil {
|
|
||||||
summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.authorSubtitles) > 0 {
|
|
||||||
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
|
||||||
for i, path := range state.authorSubtitles {
|
|
||||||
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
|
||||||
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
|
||||||
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
|
||||||
if state.authorTitle != "" {
|
|
||||||
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryLabel := widget.NewLabel(summary)
|
|
||||||
summaryLabel.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Generate DVD/ISO:"),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
summaryLabel,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
generateBtn,
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Video clips list with drag-and-drop support
|
|
||||||
list := container.NewVBox()
|
|
||||||
|
|
||||||
rebuildList := func() {
|
|
||||||
list.Objects = nil
|
|
||||||
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
emptyLabel := widget.NewLabel("Drag and drop video files here\nor click 'Add Files' to select videos")
|
|
||||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
// Make empty state a drop target
|
|
||||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
|
||||||
var paths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() == "file" {
|
|
||||||
paths = append(paths, uri.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
|
||||||
state.addAuthorFiles(paths)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
} else {
|
|
||||||
for i, clip := range state.authorClips {
|
|
||||||
idx := i
|
|
||||||
card := widget.NewCard(clip.DisplayName, fmt.Sprintf("%.2fs", clip.Duration), nil)
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
removeBtn := widget.NewButton("Remove", func() {
|
|
||||||
state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
|
|
||||||
rebuildList()
|
|
||||||
})
|
|
||||||
removeBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Duration label
|
|
||||||
durationLabel := widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", clip.Duration))
|
|
||||||
durationLabel.TextStyle = fyne.TextStyle{Italic: true}
|
|
||||||
|
|
||||||
cardContent := container.NewVBox(
|
|
||||||
durationLabel,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
removeBtn,
|
|
||||||
)
|
|
||||||
card.SetContent(cardContent)
|
|
||||||
list.Add(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add files button
|
|
||||||
addBtn := widget.NewButton("Add Files", func() {
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
state.addAuthorFiles([]string{reader.URI().Path()})
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
addBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Clear all button
|
|
||||||
clearBtn := widget.NewButton("Clear All", func() {
|
|
||||||
state.authorClips = []authorClip{}
|
|
||||||
rebuildList()
|
|
||||||
})
|
|
||||||
clearBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
// Compile button
|
|
||||||
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
|
||||||
if len(state.authorClips) == 0 {
|
|
||||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// TODO: Implement compilation to DVD
|
|
||||||
dialog.ShowInformation("Compile", "DVD compilation will be implemented", state.window)
|
|
||||||
})
|
|
||||||
compileBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Video Clips:"),
|
|
||||||
container.NewScroll(list),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(addBtn, clearBtn, compileBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize the list
|
|
||||||
rebuildList()
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildSubtitlesTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Subtitle files list with drag-and-drop support
|
|
||||||
list := container.NewVBox()
|
|
||||||
|
|
||||||
rebuildSubList := func() {
|
|
||||||
list.Objects = nil
|
|
||||||
|
|
||||||
if len(state.authorSubtitles) == 0 {
|
|
||||||
emptyLabel := widget.NewLabel("Drag and drop subtitle files here\nor click 'Add Subtitles' to select")
|
|
||||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
|
||||||
|
|
||||||
// Make empty state a drop target
|
|
||||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
|
||||||
var paths []string
|
|
||||||
for _, uri := range items {
|
|
||||||
if uri.Scheme() == "file" {
|
|
||||||
paths = append(paths, uri.Path())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(paths) > 0 {
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles, paths...)
|
|
||||||
rebuildSubList()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
list.Add(container.NewMax(emptyDrop))
|
|
||||||
} else {
|
|
||||||
for i, path := range state.authorSubtitles {
|
|
||||||
idx := i
|
|
||||||
card := widget.NewCard(filepath.Base(path), "", nil)
|
|
||||||
|
|
||||||
// Remove button
|
|
||||||
removeBtn := widget.NewButton("Remove", func() {
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles[:idx], state.authorSubtitles[idx+1:]...)
|
|
||||||
rebuildSubList()
|
|
||||||
})
|
|
||||||
removeBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
cardContent := container.NewVBox(removeBtn)
|
|
||||||
card.SetContent(cardContent)
|
|
||||||
list.Add(card)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add subtitles button
|
|
||||||
addBtn := widget.NewButton("Add Subtitles", func() {
|
|
||||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
|
||||||
if err != nil || reader == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer reader.Close()
|
|
||||||
state.authorSubtitles = append(state.authorSubtitles, reader.URI().Path())
|
|
||||||
rebuildSubList()
|
|
||||||
}, state.window)
|
|
||||||
})
|
|
||||||
addBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Clear all button
|
|
||||||
clearBtn := widget.NewButton("Clear All", func() {
|
|
||||||
state.authorSubtitles = []string{}
|
|
||||||
rebuildSubList()
|
|
||||||
})
|
|
||||||
clearBtn.Importance = widget.MediumImportance
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Subtitle Tracks:"),
|
|
||||||
container.NewScroll(list),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
container.NewHBox(addBtn, clearBtn),
|
|
||||||
)
|
|
||||||
|
|
||||||
// Initialize
|
|
||||||
rebuildSubList()
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Output type selection
|
|
||||||
outputType := widget.NewSelect([]string{"DVD (VIDEO_TS)", "ISO Image"}, func(value string) {
|
|
||||||
if value == "DVD (VIDEO_TS)" {
|
|
||||||
state.authorOutputType = "dvd"
|
|
||||||
} else {
|
|
||||||
state.authorOutputType = "iso"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if state.authorOutputType == "iso" {
|
|
||||||
outputType.SetSelected("ISO Image")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Region selection
|
|
||||||
regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"}, func(value string) {
|
|
||||||
state.authorRegion = value
|
|
||||||
})
|
|
||||||
if state.authorRegion == "" {
|
|
||||||
regionSelect.SetSelected("AUTO")
|
|
||||||
} else {
|
|
||||||
regionSelect.SetSelected(state.authorRegion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Aspect ratio selection
|
|
||||||
aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"}, func(value string) {
|
|
||||||
state.authorAspectRatio = value
|
|
||||||
})
|
|
||||||
if state.authorAspectRatio == "" {
|
|
||||||
aspectSelect.SetSelected("AUTO")
|
|
||||||
} else {
|
|
||||||
aspectSelect.SetSelected(state.authorAspectRatio)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DVD title entry
|
|
||||||
titleEntry := widget.NewEntry()
|
|
||||||
titleEntry.SetPlaceHolder("DVD Title")
|
|
||||||
titleEntry.SetText(state.authorTitle)
|
|
||||||
titleEntry.OnChanged = func(value string) {
|
|
||||||
state.authorTitle = value
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create menu checkbox
|
|
||||||
createMenuCheck := widget.NewCheck("Create DVD Menu", func(checked bool) {
|
|
||||||
state.authorCreateMenu = checked
|
|
||||||
})
|
|
||||||
createMenuCheck.SetChecked(state.authorCreateMenu)
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Output Settings:"),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
widget.NewLabel("Output Type:"),
|
|
||||||
outputType,
|
|
||||||
widget.NewLabel("Region:"),
|
|
||||||
regionSelect,
|
|
||||||
widget.NewLabel("Aspect Ratio:"),
|
|
||||||
aspectSelect,
|
|
||||||
widget.NewLabel("DVD Title:"),
|
|
||||||
titleEntry,
|
|
||||||
createMenuCheck,
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
|
||||||
// Generate DVD/ISO
|
|
||||||
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
|
||||||
if len(state.authorClips) == 0 && state.authorFile == nil {
|
|
||||||
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show compilation options
|
|
||||||
dialog.ShowInformation("DVD Generation",
|
|
||||||
"DVD/ISO generation will be implemented in next step.\n\n"+
|
|
||||||
"Features planned:\n"+
|
|
||||||
"• Create VIDEO_TS folder structure\n"+
|
|
||||||
"• Generate burn-ready ISO\n"+
|
|
||||||
"• Include subtitle tracks\n"+
|
|
||||||
"• Include alternate audio tracks\n"+
|
|
||||||
"• Support for alternate camera angles", state.window)
|
|
||||||
})
|
|
||||||
generateBtn.Importance = widget.HighImportance
|
|
||||||
|
|
||||||
// Show summary
|
|
||||||
summary := "Ready to generate:\n\n"
|
|
||||||
if len(state.authorClips) > 0 {
|
|
||||||
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
|
||||||
for i, clip := range state.authorClips {
|
|
||||||
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
|
||||||
}
|
|
||||||
} else if state.authorFile != nil {
|
|
||||||
summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(state.authorSubtitles) > 0 {
|
|
||||||
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
|
||||||
for i, path := range state.authorSubtitles {
|
|
||||||
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
|
||||||
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
|
||||||
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
|
||||||
if state.authorTitle != "" {
|
|
||||||
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
|
||||||
}
|
|
||||||
|
|
||||||
summaryLabel := widget.NewLabel(summary)
|
|
||||||
summaryLabel.Wrapping = fyne.TextWrapWord
|
|
||||||
|
|
||||||
controls := container.NewVBox(
|
|
||||||
widget.NewLabel("Generate DVD/ISO:"),
|
|
||||||
widget.NewSeparator(),
|
|
||||||
summaryLabel,
|
|
||||||
widget.NewSeparator(),
|
|
||||||
generateBtn,
|
|
||||||
)
|
|
||||||
|
|
||||||
return container.NewPadded(controls)
|
|
||||||
}
|
|
||||||
|
|
||||||
// addAuthorFiles helper function
|
|
||||||
func (s *appState) addAuthorFiles(paths []string) {
|
|
||||||
for _, path := range paths {
|
|
||||||
src, err := probeVideo(path)
|
|
||||||
if err != nil {
|
|
||||||
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
clip := authorClip{
|
|
||||||
Path: path,
|
|
||||||
DisplayName: filepath.Base(path),
|
|
||||||
Duration: src.Duration,
|
|
||||||
Chapters: []authorChapter{},
|
|
||||||
}
|
|
||||||
s.authorClips = append(s.authorClips, clip)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildUpscaleFilter(targetWidth, targetHeight int, method string, preserveAspect bool) string {
|
func buildUpscaleFilter(targetWidth, targetHeight int, method string, preserveAspect bool) string {
|
||||||
// Ensure even dimensions for encoders
|
// Ensure even dimensions for encoders
|
||||||
makeEven := func(v int) int {
|
makeEven := func(v int) int {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,18 @@
|
||||||
|
|
||||||
This directory contains scripts for building and managing VideoTools on different platforms.
|
This directory contains scripts for building and managing VideoTools on different platforms.
|
||||||
|
|
||||||
|
## Recommended Workflow
|
||||||
|
|
||||||
|
For development on any platform:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/install.sh
|
||||||
|
./scripts/build.sh
|
||||||
|
./scripts/run.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `./scripts/install.sh` whenever you add new dependencies or need to reinstall.
|
||||||
|
|
||||||
## Linux
|
## Linux
|
||||||
|
|
||||||
### Install Dependencies
|
### Install Dependencies
|
||||||
|
|
@ -73,6 +85,7 @@ Run in PowerShell as Administrator:
|
||||||
- MinGW-w64 (GCC compiler)
|
- MinGW-w64 (GCC compiler)
|
||||||
- ffmpeg
|
- ffmpeg
|
||||||
- Git (optional, for development)
|
- Git (optional, for development)
|
||||||
|
- DVD authoring tools (via DVDStyler portable: dvdauthor + mkisofs)
|
||||||
|
|
||||||
**Package managers supported:**
|
**Package managers supported:**
|
||||||
- Chocolatey (default, requires admin)
|
- Chocolatey (default, requires admin)
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
APP_VERSION="$(grep -m1 'appVersion' "$PROJECT_ROOT/main.go" | sed -E 's/.*\"([^\"]+)\".*/\1/')"
|
APP_VERSION="$(grep -m1 'appVersion' "$PROJECT_ROOT/main.go" | sed -E 's/.*\"([^\"]+)\".*/\1/')"
|
||||||
[ -z "$APP_VERSION" ] && APP_VERSION="(version unknown)"
|
[ -z "$APP_VERSION" ] && APP_VERSION="(version unknown)"
|
||||||
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo " VideoTools Universal Build Script"
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Detect platform
|
# Detect platform
|
||||||
PLATFORM="$(uname -s)"
|
PLATFORM="$(uname -s)"
|
||||||
case "$PLATFORM" in
|
case "$PLATFORM" in
|
||||||
|
|
@ -22,6 +17,11 @@ case "$PLATFORM" in
|
||||||
CYGWIN*|MINGW*|MSYS*) OS="Windows" ;;
|
CYGWIN*|MINGW*|MSYS*) OS="Windows" ;;
|
||||||
*) echo "❌ Unknown platform: $PLATFORM"; exit 1 ;;
|
*) echo "❌ Unknown platform: $PLATFORM"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
|
echo " VideoTools ${OS} Build"
|
||||||
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
|
echo ""
|
||||||
echo "🔍 Detected platform: $OS"
|
echo "🔍 Detected platform: $OS"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo " VideoTools Dependency Installer (Linux)"
|
echo " VideoTools Linux Installation"
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,60 +4,22 @@ chcp 65001 >nul
|
||||||
title VideoTools Windows Dependency Installer
|
title VideoTools Windows Dependency Installer
|
||||||
|
|
||||||
echo ========================================================
|
echo ========================================================
|
||||||
echo VideoTools Windows Dependency Installer (.bat)
|
echo VideoTools Windows Installation
|
||||||
echo Installs Go, MinGW (GCC), Git, and FFmpeg
|
echo Delegating to PowerShell for full dependency setup
|
||||||
echo ========================================================
|
echo ========================================================
|
||||||
echo.
|
echo.
|
||||||
|
|
||||||
REM Prefer Chocolatey if available; otherwise fall back to winget.
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-deps-windows.ps1"
|
||||||
where choco >nul 2>&1
|
set EXIT_CODE=%errorlevel%
|
||||||
if %errorlevel%==0 (
|
|
||||||
echo Using Chocolatey...
|
if not %EXIT_CODE%==0 (
|
||||||
call :install_choco
|
echo.
|
||||||
goto :verify
|
echo Dependency installer failed with exit code %EXIT_CODE%.
|
||||||
|
pause
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
)
|
)
|
||||||
|
|
||||||
where winget >nul 2>&1
|
|
||||||
if %errorlevel%==0 (
|
|
||||||
echo Chocolatey not found; using winget...
|
|
||||||
call :install_winget
|
|
||||||
goto :verify
|
|
||||||
)
|
|
||||||
|
|
||||||
echo Neither Chocolatey nor winget found.
|
|
||||||
echo Please install Chocolatey (recommended): https://chocolatey.org/install
|
|
||||||
echo Then re-run this script.
|
|
||||||
pause
|
|
||||||
exit /b 1
|
|
||||||
|
|
||||||
:install_choco
|
|
||||||
echo.
|
echo.
|
||||||
echo Installing dependencies via Chocolatey...
|
echo Done. Restart your terminal to refresh PATH.
|
||||||
choco install -y golang mingw git ffmpeg
|
|
||||||
goto :eof
|
|
||||||
|
|
||||||
:install_winget
|
|
||||||
echo.
|
|
||||||
echo Installing dependencies via winget...
|
|
||||||
REM Winget package IDs can vary; these are common defaults.
|
|
||||||
winget install -e --id GoLang.Go
|
|
||||||
winget install -e --id Git.Git
|
|
||||||
winget install -e --id GnuWin32.Mingw
|
|
||||||
winget install -e --id Gyan.FFmpeg
|
|
||||||
goto :eof
|
|
||||||
|
|
||||||
:verify
|
|
||||||
echo.
|
|
||||||
echo ========================================================
|
|
||||||
echo Verifying installs
|
|
||||||
echo ========================================================
|
|
||||||
where go >nul 2>&1 && go version
|
|
||||||
where gcc >nul 2>&1 && gcc --version | findstr /R /C:"gcc"
|
|
||||||
where git >nul 2>&1 && git --version
|
|
||||||
where ffmpeg >nul 2>&1 && ffmpeg -version | head -n 1
|
|
||||||
|
|
||||||
echo.
|
|
||||||
echo Done. If any tool is missing, ensure its bin folder is in PATH
|
|
||||||
echo (restart terminal after installation).
|
|
||||||
pause
|
pause
|
||||||
exit /b 0
|
exit /b 0
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ param(
|
||||||
)
|
)
|
||||||
|
|
||||||
Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
Write-Host " VideoTools Dependency Installer (Windows)" -ForegroundColor Cyan
|
Write-Host " VideoTools Windows Installation" -ForegroundColor Cyan
|
||||||
Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
|
||||||
|
|
@ -32,6 +32,57 @@ function Test-Command {
|
||||||
return $?
|
return $?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Ensure DVD authoring tools exist on Windows by downloading DVDStyler portable
|
||||||
|
function Ensure-DVDStylerTools {
|
||||||
|
$toolsRoot = Join-Path $PSScriptRoot "tools"
|
||||||
|
$dvdstylerDir = Join-Path $toolsRoot "dvdstyler"
|
||||||
|
$dvdstylerBin = Join-Path $dvdstylerDir "bin"
|
||||||
|
$dvdstylerUrl = "https://sourceforge.net/projects/dvdstyler/files/DVDStyler/3.2.1/DVDStyler-3.2.1-win64.zip/download"
|
||||||
|
$dvdstylerZip = Join-Path $env:TEMP "dvdstyler-win64.zip"
|
||||||
|
$needsDVDTools = (-not (Test-Command dvdauthor)) -or (-not (Test-Command mkisofs))
|
||||||
|
|
||||||
|
if (-not $needsDVDTools) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Installing DVD authoring tools (DVDStyler portable)..." -ForegroundColor Yellow
|
||||||
|
if (-not (Test-Path $toolsRoot)) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $toolsRoot | Out-Null
|
||||||
|
}
|
||||||
|
if (Test-Path $dvdstylerDir) {
|
||||||
|
Remove-Item -Recurse -Force $dvdstylerDir
|
||||||
|
}
|
||||||
|
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||||
|
Invoke-WebRequest -Uri $dvdstylerUrl -OutFile $dvdstylerZip
|
||||||
|
|
||||||
|
$extractRoot = Join-Path $env:TEMP ("dvdstyler-extract-" + [System.Guid]::NewGuid().ToString())
|
||||||
|
New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null
|
||||||
|
Expand-Archive -Path $dvdstylerZip -DestinationPath $extractRoot -Force
|
||||||
|
|
||||||
|
$entries = Get-ChildItem -Path $extractRoot
|
||||||
|
if ($entries.Count -eq 1 -and $entries[0].PSIsContainer) {
|
||||||
|
Copy-Item -Path (Join-Path $entries[0].FullName "*") -Destination $dvdstylerDir -Recurse -Force
|
||||||
|
} else {
|
||||||
|
Copy-Item -Path (Join-Path $extractRoot "*") -Destination $dvdstylerDir -Recurse -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Remove-Item -Force $dvdstylerZip
|
||||||
|
Remove-Item -Recurse -Force $extractRoot
|
||||||
|
|
||||||
|
if (Test-Path $dvdstylerBin) {
|
||||||
|
$env:Path = "$dvdstylerBin;$env:Path"
|
||||||
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
|
if ($userPath -notmatch [Regex]::Escape($dvdstylerBin)) {
|
||||||
|
[Environment]::SetEnvironmentVariable("Path", "$dvdstylerBin;$userPath", "User")
|
||||||
|
}
|
||||||
|
Write-Host "✓ DVD authoring tools installed to $dvdstylerDir" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "❌ DVDStyler tools missing after install" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# Function to install via Chocolatey
|
# Function to install via Chocolatey
|
||||||
function Install-ViaChocolatey {
|
function Install-ViaChocolatey {
|
||||||
Write-Host "📦 Using Chocolatey package manager..." -ForegroundColor Green
|
Write-Host "📦 Using Chocolatey package manager..." -ForegroundColor Green
|
||||||
|
|
@ -191,6 +242,8 @@ if ($UseScoop) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ensure-DVDStylerTools
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||||
Write-Host "✅ DEPENDENCIES INSTALLED" -ForegroundColor Green
|
Write-Host "✅ DEPENDENCIES INSTALLED" -ForegroundColor Green
|
||||||
|
|
@ -229,6 +282,18 @@ if (Test-Command ffmpeg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Test-Command dvdauthor) {
|
||||||
|
Write-Host "✓ dvdauthor: found" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ dvdauthor not found in PATH (restart terminal)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Command mkisofs) {
|
||||||
|
Write-Host "✓ mkisofs: found" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host "⚠️ mkisofs not found in PATH (restart terminal)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
if (Test-Command git) {
|
if (Test-Command git) {
|
||||||
$gitVersion = git --version
|
$gitVersion = git --version
|
||||||
Write-Host "✓ Git: $gitVersion" -ForegroundColor Green
|
Write-Host "✓ Git: $gitVersion" -ForegroundColor Green
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,43 @@ spinner() {
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
BINARY_NAME="VideoTools"
|
BINARY_NAME="VideoTools"
|
||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
DEFAULT_INSTALL_PATH="/usr/local/bin"
|
DEFAULT_INSTALL_PATH="/usr/local/bin"
|
||||||
USER_INSTALL_PATH="$HOME/.local/bin"
|
USER_INSTALL_PATH="$HOME/.local/bin"
|
||||||
|
|
||||||
|
# Platform detection
|
||||||
|
UNAME_S="$(uname -s)"
|
||||||
|
IS_WINDOWS=false
|
||||||
|
IS_DARWIN=false
|
||||||
|
IS_LINUX=false
|
||||||
|
case "$UNAME_S" in
|
||||||
|
MINGW*|MSYS*|CYGWIN*)
|
||||||
|
IS_WINDOWS=true
|
||||||
|
;;
|
||||||
|
Darwin*)
|
||||||
|
IS_DARWIN=true
|
||||||
|
;;
|
||||||
|
Linux*)
|
||||||
|
IS_LINUX=true
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
INSTALL_TITLE="VideoTools Installation"
|
||||||
|
if [ "$IS_WINDOWS" = true ]; then
|
||||||
|
INSTALL_TITLE="VideoTools Windows Installation"
|
||||||
|
elif [ "$IS_DARWIN" = true ]; then
|
||||||
|
INSTALL_TITLE="VideoTools macOS Installation"
|
||||||
|
elif [ "$IS_LINUX" = true ]; then
|
||||||
|
INSTALL_TITLE="VideoTools Linux Installation"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo " VideoTools Professional Installation"
|
echo " $INSTALL_TITLE"
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Step 1: Check if Go is installed
|
# Step 1: Check if Go is installed
|
||||||
echo -e "${CYAN}[1/5]${NC} Checking Go installation..."
|
echo -e "${CYAN}[1/6]${NC} Checking Go installation..."
|
||||||
if ! command -v go &> /dev/null; then
|
if ! command -v go &> /dev/null; then
|
||||||
echo -e "${RED}✗ Error: Go is not installed or not in PATH${NC}"
|
echo -e "${RED}✗ Error: Go is not installed or not in PATH${NC}"
|
||||||
echo "Please install Go 1.21+ from https://go.dev/dl/"
|
echo "Please install Go 1.21+ from https://go.dev/dl/"
|
||||||
|
|
@ -47,9 +73,77 @@ fi
|
||||||
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
|
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//')
|
||||||
echo -e "${GREEN}✓${NC} Found Go version: $GO_VERSION"
|
echo -e "${GREEN}✓${NC} Found Go version: $GO_VERSION"
|
||||||
|
|
||||||
# Step 2: Build the binary
|
# Step 2: Check authoring dependencies
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}[2/5]${NC} Building VideoTools..."
|
echo -e "${CYAN}[2/6]${NC} Checking authoring dependencies..."
|
||||||
|
|
||||||
|
if [ "$IS_WINDOWS" = true ]; then
|
||||||
|
echo "Detected Windows environment."
|
||||||
|
if command -v powershell.exe &> /dev/null; then
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$PROJECT_ROOT/scripts/install-deps-windows.ps1"
|
||||||
|
echo -e "${GREEN}✓${NC} Windows dependency installer completed"
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ powershell.exe not found.${NC}"
|
||||||
|
echo "Please run: $PROJECT_ROOT\\scripts\\install-deps-windows.ps1"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
missing_deps=()
|
||||||
|
if ! command -v ffmpeg &> /dev/null; then
|
||||||
|
missing_deps+=("ffmpeg")
|
||||||
|
fi
|
||||||
|
if ! command -v dvdauthor &> /dev/null; then
|
||||||
|
missing_deps+=("dvdauthor")
|
||||||
|
fi
|
||||||
|
if ! command -v mkisofs &> /dev/null && ! command -v genisoimage &> /dev/null && ! command -v xorriso &> /dev/null; then
|
||||||
|
missing_deps+=("iso-tool")
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_deps=false
|
||||||
|
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||||
|
echo -e "${YELLOW}WARNING${NC} Missing dependencies: ${missing_deps[*]}"
|
||||||
|
read -p "Install missing dependencies now? [y/N]: " install_choice
|
||||||
|
if [[ "$install_choice" =~ ^[Yy]$ ]]; then
|
||||||
|
install_deps=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${GREEN}✓${NC} All authoring dependencies found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$install_deps" = true ]; then
|
||||||
|
if command -v apt-get &> /dev/null; then
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y ffmpeg dvdauthor genisoimage
|
||||||
|
elif command -v dnf &> /dev/null; then
|
||||||
|
sudo dnf install -y ffmpeg dvdauthor genisoimage
|
||||||
|
elif command -v pacman &> /dev/null; then
|
||||||
|
sudo pacman -Sy --noconfirm ffmpeg dvdauthor cdrtools
|
||||||
|
elif command -v zypper &> /dev/null; then
|
||||||
|
sudo zypper install -y ffmpeg dvdauthor genisoimage
|
||||||
|
elif command -v brew &> /dev/null; then
|
||||||
|
brew install ffmpeg dvdauthor xorriso
|
||||||
|
else
|
||||||
|
echo -e "${RED}✗ No supported package manager found.${NC}"
|
||||||
|
echo "Please install: ffmpeg, dvdauthor, and mkisofs/genisoimage/xorriso"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v ffmpeg &> /dev/null || ! command -v dvdauthor &> /dev/null; then
|
||||||
|
echo -e "${RED}✗ Missing required dependencies after install attempt.${NC}"
|
||||||
|
echo "Please install: ffmpeg and dvdauthor"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if ! command -v mkisofs &> /dev/null && ! command -v genisoimage &> /dev/null && ! command -v xorriso &> /dev/null; then
|
||||||
|
echo -e "${RED}✗ Missing ISO creation tool after install attempt.${NC}"
|
||||||
|
echo "Please install: mkisofs (cdrtools), genisoimage, or xorriso"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Step 3: Build the binary
|
||||||
|
echo ""
|
||||||
|
echo -e "${CYAN}[3/6]${NC} Building VideoTools..."
|
||||||
cd "$PROJECT_ROOT"
|
cd "$PROJECT_ROOT"
|
||||||
CGO_ENABLED=1 go build -o "$BINARY_NAME" . > /tmp/videotools-build.log 2>&1 &
|
CGO_ENABLED=1 go build -o "$BINARY_NAME" . > /tmp/videotools-build.log 2>&1 &
|
||||||
BUILD_PID=$!
|
BUILD_PID=$!
|
||||||
|
|
@ -67,9 +161,9 @@ else
|
||||||
fi
|
fi
|
||||||
rm -f /tmp/videotools-build.log
|
rm -f /tmp/videotools-build.log
|
||||||
|
|
||||||
# Step 3: Determine installation path
|
# Step 4: Determine installation path
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}[3/5]${NC} Installation path selection"
|
echo -e "${CYAN}[4/6]${NC} Installation path selection"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Where would you like to install $BINARY_NAME?"
|
echo "Where would you like to install $BINARY_NAME?"
|
||||||
echo " 1) System-wide (/usr/local/bin) - requires sudo, available to all users"
|
echo " 1) System-wide (/usr/local/bin) - requires sudo, available to all users"
|
||||||
|
|
@ -95,15 +189,12 @@ case $choice in
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# Step 4: Install the binary
|
# Step 5: Install the binary
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}[4/5]${NC} Installing binary to $INSTALL_PATH..."
|
echo -e "${CYAN}[5/6]${NC} Installing binary to $INSTALL_PATH..."
|
||||||
if [ "$NEEDS_SUDO" = true ]; then
|
if [ "$NEEDS_SUDO" = true ]; then
|
||||||
sudo install -m 755 "$BINARY_NAME" "$INSTALL_PATH/$BINARY_NAME" > /dev/null 2>&1 &
|
echo "Installing $BINARY_NAME (sudo required)..."
|
||||||
INSTALL_PID=$!
|
if sudo install -m 755 "$BINARY_NAME" "$INSTALL_PATH/$BINARY_NAME" > /dev/null 2>&1; then
|
||||||
spinner $INSTALL_PID "Installing $BINARY_NAME"
|
|
||||||
|
|
||||||
if wait $INSTALL_PID; then
|
|
||||||
echo -e "${GREEN}✓${NC} Installation successful"
|
echo -e "${GREEN}✓${NC} Installation successful"
|
||||||
else
|
else
|
||||||
echo -e "${RED}✗ Installation failed${NC}"
|
echo -e "${RED}✗ Installation failed${NC}"
|
||||||
|
|
@ -126,9 +217,9 @@ fi
|
||||||
|
|
||||||
rm -f "$BINARY_NAME"
|
rm -f "$BINARY_NAME"
|
||||||
|
|
||||||
# Step 5: Setup shell aliases and environment
|
# Step 6: Setup shell aliases and environment
|
||||||
echo ""
|
echo ""
|
||||||
echo -e "${CYAN}[5/5]${NC} Setting up shell environment..."
|
echo -e "${CYAN}[6/6]${NC} Setting up shell environment..."
|
||||||
|
|
||||||
# Detect shell
|
# Detect shell
|
||||||
if [ -n "$ZSH_VERSION" ]; then
|
if [ -n "$ZSH_VERSION" ]; then
|
||||||
|
|
@ -167,21 +258,21 @@ fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo -e "${GREEN}Installation Complete!${NC}"
|
echo "Installation Complete!"
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
echo "Next steps:"
|
echo "Next steps:"
|
||||||
echo ""
|
echo ""
|
||||||
echo "1. ${CYAN}Reload your shell configuration:${NC}"
|
echo "1. Reload your shell configuration:"
|
||||||
echo " source $SHELL_RC"
|
echo " source $SHELL_RC"
|
||||||
echo ""
|
echo ""
|
||||||
echo "2. ${CYAN}Run VideoTools:${NC}"
|
echo "2. Run VideoTools:"
|
||||||
echo " VideoTools"
|
echo " VideoTools"
|
||||||
echo ""
|
echo ""
|
||||||
echo "3. ${CYAN}Available commands:${NC}"
|
echo "3. Available commands:"
|
||||||
echo " • VideoTools - Run the application"
|
echo " - VideoTools - Run the application"
|
||||||
echo " • VideoToolsRebuild - Force rebuild from source"
|
echo " - VideoToolsRebuild - Force rebuild from source"
|
||||||
echo " • VideoToolsClean - Clean build artifacts and cache"
|
echo " - VideoToolsClean - Clean build artifacts and cache"
|
||||||
echo ""
|
echo ""
|
||||||
echo "For more information, see BUILD_AND_RUN.md and DVD_USER_GUIDE.md"
|
echo "For more information, see BUILD_AND_RUN.md and DVD_USER_GUIDE.md"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
@ -5,8 +5,17 @@
|
||||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
BUILD_OUTPUT="$PROJECT_ROOT/VideoTools"
|
BUILD_OUTPUT="$PROJECT_ROOT/VideoTools"
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
PLATFORM="$(uname -s)"
|
||||||
|
case "$PLATFORM" in
|
||||||
|
Linux*) OS="Linux" ;;
|
||||||
|
Darwin*) OS="macOS" ;;
|
||||||
|
CYGWIN*|MINGW*|MSYS*) OS="Windows" ;;
|
||||||
|
*) echo "❌ Unknown platform: $PLATFORM"; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo " VideoTools - Run Script"
|
echo " VideoTools ${OS} Run"
|
||||||
echo "════════════════════════════════════════════════════════════════"
|
echo "════════════════════════════════════════════════════════════════"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user