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)
|
||||
|
||||
```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**.
|
||||
|
|
|
|||
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`)
|
||||
|
||||
#### 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`)
|
||||
|
|
|
|||
|
|
@ -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! 🎬
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 <repository-url>
|
||||
cd VideoTools
|
||||
go build -o VideoTools
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
4. **Run**:
|
||||
```bash
|
||||
./VideoTools
|
||||
./scripts/run.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
944
main.go
944
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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
set -e
|
||||
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo " VideoTools Dependency Installer (Linux)"
|
||||
echo " VideoTools Linux Installation"
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user