diff --git a/README.md b/README.md
index d73d386..15fb535 100644
--- a/README.md
+++ b/README.md
@@ -30,7 +30,7 @@ VideoTools is a professional-grade video processing application with a modern GU
### Installation (One Command)
```bash
-bash install.sh
+bash scripts/install.sh
```
The installer will build, install, and set up everything automatically with a guided wizard!
@@ -43,12 +43,12 @@ VideoTools
### Alternative: Developer Setup
-If you already have the repo cloned:
+If you already have the repo cloned (dev workflow):
```bash
cd /path/to/VideoTools
-source scripts/alias.sh
-VideoTools
+bash scripts/build.sh
+bash scripts/run.sh
```
For detailed installation options, troubleshooting, and platform-specific notes, see **INSTALLATION.md**.
diff --git a/author_dvd_functions.go b/author_dvd_functions.go
new file mode 100644
index 0000000..ba35a82
--- /dev/null
+++ b/author_dvd_functions.go
@@ -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)
+ }
+}
diff --git a/author_module.go b/author_module.go
new file mode 100644
index 0000000..1fbc8b8
--- /dev/null
+++ b/author_module.go
@@ -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("\n")
+ b.WriteString(" \n")
+ b.WriteString(" \n")
+ b.WriteString(" \n")
+ b.WriteString(fmt.Sprintf(" \n", format, aspect))
+ for _, mpg := range mpgPaths {
+ b.WriteString(" \n")
+ b.WriteString(fmt.Sprintf(" \n", escapeXMLAttr(mpg)))
+ b.WriteString(" \n")
+ }
+ b.WriteString(" \n")
+ b.WriteString(" \n")
+ b.WriteString("\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()
+}
diff --git a/author_module_temp.go b/author_module_temp.go
deleted file mode 100644
index 2687870..0000000
--- a/author_module_temp.go
+++ /dev/null
@@ -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)
-}
\ No newline at end of file
diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md
index 67a1c1f..708866e 100644
--- a/docs/CHANGELOG.md
+++ b/docs/CHANGELOG.md
@@ -13,7 +13,7 @@
- **Cross-compilation script** (`scripts/build-windows.sh`)
#### 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
- **System-wide vs user-local installation** options
- **Convenience aliases** (`VideoTools`, `VideoToolsRebuild`, `VideoToolsClean`)
diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md
index d28034e..fb9e86b 100644
--- a/docs/INSTALLATION.md
+++ b/docs/INSTALLATION.md
@@ -7,7 +7,7 @@ This guide will help you install VideoTools with minimal setup.
### One-Command Installation
```bash
-bash install.sh
+bash scripts/install.sh
```
That's it! The installer will:
@@ -43,7 +43,7 @@ VideoTools
### Option 1: System-Wide Installation (Recommended for Shared Computers)
```bash
-bash install.sh
+bash scripts/install.sh
# Select option 1 when prompted
# Enter your password if requested
```
@@ -61,7 +61,7 @@ bash install.sh
### Option 2: User-Local Installation (Recommended for Personal Use)
```bash
-bash install.sh
+bash scripts/install.sh
# Select option 2 when prompted (default)
```
@@ -78,7 +78,7 @@ bash install.sh
## What the Installer Does
-The `install.sh` script performs these steps:
+The `scripts/install.sh` script performs these steps:
### Step 1: Go Verification
- 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
### Essential
@@ -135,7 +148,7 @@ VideoToolsClean # Clean build artifacts and cache
```
### System
-- Linux, macOS, or WSL (Windows Subsystem for Linux)
+- Linux, macOS, or Windows (native)
- At least 2 GB free disk space
- Stable internet connection (for dependencies)
@@ -157,7 +170,7 @@ go version
**Solution:** Check build log for specific errors:
```bash
-bash install.sh
+bash scripts/install.sh
# 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! š¬
-
diff --git a/docs/LATEST_UPDATES.md b/docs/LATEST_UPDATES.md
index e37b5f7..f1800e1 100644
--- a/docs/LATEST_UPDATES.md
+++ b/docs/LATEST_UPDATES.md
@@ -88,7 +88,7 @@ The queue view now displays:
### New Files
-1. **Enhanced `install.sh`** - One-command installation
+1. **Enhanced `scripts/install.sh`** - One-command installation
2. **New `INSTALLATION.md`** - Comprehensive installation guide
### install.sh Features
@@ -96,7 +96,7 @@ The queue view now displays:
The installer now performs all setup automatically:
```bash
-bash install.sh
+bash scripts/install.sh
```
This handles:
@@ -113,13 +113,13 @@ This handles:
**Option 1: System-Wide (for shared computers)**
```bash
-bash install.sh
+bash scripts/install.sh
# Select option 1 when prompted
```
**Option 2: User-Local (default, no sudo required)**
```bash
-bash install.sh
+bash scripts/install.sh
# 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
### 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
3. Verify aliases are available
diff --git a/docs/QUICKSTART.md b/docs/QUICKSTART.md
index 69c3ff8..fd38360 100644
--- a/docs/QUICKSTART.md
+++ b/docs/QUICKSTART.md
@@ -14,18 +14,20 @@ Get VideoTools running in minutes!
cd VideoTools
```
-2. **Run the setup script**:
- - Double-click `setup-windows.bat`
- - OR run in PowerShell:
- ```powershell
- .\scripts\setup-windows.ps1 -Portable
- ```
+2. **Install dependencies and build** (Git Bash or similar):
+ ```bash
+ ./scripts/install.sh
+ ```
-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**:
- - Navigate to `dist/windows/`
- - Double-click `VideoTools.exe`
+3. **Run VideoTools**:
+ ```bash
+ ./scripts/run.sh
+ ```
### If You Need to Build
@@ -70,14 +72,14 @@ If `VideoTools.exe` doesn't exist yet:
sudo pacman -S ffmpeg
```
-3. **Build VideoTools**:
+3. **Install dependencies and build**:
```bash
- ./scripts/build.sh
+ ./scripts/install.sh
```
4. **Run**:
```bash
- ./VideoTools
+ ./scripts/run.sh
```
### Cross-Compile for Windows from Linux
@@ -112,16 +114,16 @@ sudo apt install gcc-mingw-w64 # Ubuntu/Debian
brew install ffmpeg
```
-3. **Clone and build**:
+3. **Clone and install dependencies/build**:
```bash
git clone
cd VideoTools
- go build -o VideoTools
+ ./scripts/install.sh
```
4. **Run**:
```bash
- ./VideoTools
+ ./scripts/run.sh
```
---
diff --git a/main.go b/main.go
index 3301265..dadd839 100644
--- a/main.go
+++ b/main.go
@@ -2739,23 +2739,12 @@ func (s *appState) showMergeView() {
for _, uri := range items {
if uri.Scheme() == "file" {
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 {
- state.addAuthorFiles(paths)
- }
- })
-
- list.Add(container.NewMax(emptyDrop))
+ }
+ }
+ if len(paths) > 0 {
+ addFiles(paths)
+ }
+ })
listBox.Add(container.NewMax(emptyDrop))
} else {
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
-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 {
// Ensure even dimensions for encoders
makeEven := func(v int) int {
diff --git a/scripts/README.md b/scripts/README.md
index 4ed480e..4d4d2f6 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -2,6 +2,18 @@
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
### Install Dependencies
@@ -73,6 +85,7 @@ Run in PowerShell as Administrator:
- MinGW-w64 (GCC compiler)
- ffmpeg
- Git (optional, for development)
+- DVD authoring tools (via DVDStyler portable: dvdauthor + mkisofs)
**Package managers supported:**
- Chocolatey (default, requires admin)
diff --git a/scripts/build.sh b/scripts/build.sh
index 6494d9b..c170643 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -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/')"
[ -z "$APP_VERSION" ] && APP_VERSION="(version unknown)"
-echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
-echo " VideoTools Universal Build Script"
-echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
-echo ""
-
# Detect platform
PLATFORM="$(uname -s)"
case "$PLATFORM" in
@@ -22,6 +17,11 @@ case "$PLATFORM" in
CYGWIN*|MINGW*|MSYS*) OS="Windows" ;;
*) echo "ā Unknown platform: $PLATFORM"; exit 1 ;;
esac
+
+echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+echo " VideoTools ${OS} Build"
+echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
+echo ""
echo "š Detected platform: $OS"
echo ""
diff --git a/scripts/install-deps-linux.sh b/scripts/install-deps-linux.sh
index 0566355..d5bf6ec 100755
--- a/scripts/install-deps-linux.sh
+++ b/scripts/install-deps-linux.sh
@@ -5,7 +5,7 @@
set -e
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
-echo " VideoTools Dependency Installer (Linux)"
+echo " VideoTools Linux Installation"
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
echo ""
diff --git a/scripts/install-deps-windows.bat b/scripts/install-deps-windows.bat
index df37e92..2985c25 100644
--- a/scripts/install-deps-windows.bat
+++ b/scripts/install-deps-windows.bat
@@ -4,60 +4,22 @@ chcp 65001 >nul
title VideoTools Windows Dependency Installer
echo ========================================================
-echo VideoTools Windows Dependency Installer (.bat)
-echo Installs Go, MinGW (GCC), Git, and FFmpeg
+echo VideoTools Windows Installation
+echo Delegating to PowerShell for full dependency setup
echo ========================================================
echo.
-REM Prefer Chocolatey if available; otherwise fall back to winget.
-where choco >nul 2>&1
-if %errorlevel%==0 (
- echo Using Chocolatey...
- call :install_choco
- goto :verify
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0install-deps-windows.ps1"
+set EXIT_CODE=%errorlevel%
+
+if not %EXIT_CODE%==0 (
+ echo.
+ 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 Installing dependencies via Chocolatey...
-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).
+echo Done. Restart your terminal to refresh PATH.
pause
exit /b 0
diff --git a/scripts/install-deps-windows.ps1 b/scripts/install-deps-windows.ps1
index dfa6d68..a8713f7 100644
--- a/scripts/install-deps-windows.ps1
+++ b/scripts/install-deps-windows.ps1
@@ -7,7 +7,7 @@ param(
)
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 ""
@@ -32,6 +32,57 @@ function Test-Command {
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 Install-ViaChocolatey {
Write-Host "š¦ Using Chocolatey package manager..." -ForegroundColor Green
@@ -191,6 +242,8 @@ if ($UseScoop) {
}
}
+Ensure-DVDStylerTools
+
Write-Host ""
Write-Host "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā" -ForegroundColor Cyan
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) {
$gitVersion = git --version
Write-Host "ā Git: $gitVersion" -ForegroundColor Green
diff --git a/install.sh b/scripts/install.sh
similarity index 52%
rename from install.sh
rename to scripts/install.sh
index 667be12..6fbaee5 100755
--- a/install.sh
+++ b/scripts/install.sh
@@ -27,17 +27,43 @@ spinner() {
# Configuration
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"
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 " VideoTools Professional Installation"
+echo " $INSTALL_TITLE"
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
echo ""
# 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
echo -e "${RED}ā Error: Go is not installed or not in PATH${NC}"
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//')
echo -e "${GREEN}ā${NC} Found Go version: $GO_VERSION"
-# Step 2: Build the binary
+# Step 2: Check authoring dependencies
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"
CGO_ENABLED=1 go build -o "$BINARY_NAME" . > /tmp/videotools-build.log 2>&1 &
BUILD_PID=$!
@@ -67,9 +161,9 @@ else
fi
rm -f /tmp/videotools-build.log
-# Step 3: Determine installation path
+# Step 4: Determine installation path
echo ""
-echo -e "${CYAN}[3/5]${NC} Installation path selection"
+echo -e "${CYAN}[4/6]${NC} Installation path selection"
echo ""
echo "Where would you like to install $BINARY_NAME?"
echo " 1) System-wide (/usr/local/bin) - requires sudo, available to all users"
@@ -95,15 +189,12 @@ case $choice in
;;
esac
-# Step 4: Install the binary
+# Step 5: Install the binary
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
- sudo install -m 755 "$BINARY_NAME" "$INSTALL_PATH/$BINARY_NAME" > /dev/null 2>&1 &
- INSTALL_PID=$!
- spinner $INSTALL_PID "Installing $BINARY_NAME"
-
- if wait $INSTALL_PID; then
+ echo "Installing $BINARY_NAME (sudo required)..."
+ if sudo install -m 755 "$BINARY_NAME" "$INSTALL_PATH/$BINARY_NAME" > /dev/null 2>&1; then
echo -e "${GREEN}ā${NC} Installation successful"
else
echo -e "${RED}ā Installation failed${NC}"
@@ -126,9 +217,9 @@ fi
rm -f "$BINARY_NAME"
-# Step 5: Setup shell aliases and environment
+# Step 6: Setup shell aliases and environment
echo ""
-echo -e "${CYAN}[5/5]${NC} Setting up shell environment..."
+echo -e "${CYAN}[6/6]${NC} Setting up shell environment..."
# Detect shell
if [ -n "$ZSH_VERSION" ]; then
@@ -167,21 +258,21 @@ fi
echo ""
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
-echo -e "${GREEN}Installation Complete!${NC}"
+echo "Installation Complete!"
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
echo ""
echo "Next steps:"
echo ""
-echo "1. ${CYAN}Reload your shell configuration:${NC}"
+echo "1. Reload your shell configuration:"
echo " source $SHELL_RC"
echo ""
-echo "2. ${CYAN}Run VideoTools:${NC}"
+echo "2. Run VideoTools:"
echo " VideoTools"
echo ""
-echo "3. ${CYAN}Available commands:${NC}"
-echo " ⢠VideoTools - Run the application"
-echo " ⢠VideoToolsRebuild - Force rebuild from source"
-echo " ⢠VideoToolsClean - Clean build artifacts and cache"
+echo "3. Available commands:"
+echo " - VideoTools - Run the application"
+echo " - VideoToolsRebuild - Force rebuild from source"
+echo " - VideoToolsClean - Clean build artifacts and cache"
echo ""
echo "For more information, see BUILD_AND_RUN.md and DVD_USER_GUIDE.md"
echo ""
diff --git a/scripts/run.sh b/scripts/run.sh
index 023e1e1..b96bcc5 100755
--- a/scripts/run.sh
+++ b/scripts/run.sh
@@ -5,8 +5,17 @@
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
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 " VideoTools - Run Script"
+echo " VideoTools ${OS} Run"
echo "āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā"
echo ""