Compare commits
5 Commits
e9608c6085
...
8644fc5d9a
| Author | SHA1 | Date | |
|---|---|---|---|
| 8644fc5d9a | |||
| 9f47d503ff | |||
| 931fda6dd2 | |||
| 8513902232 | |||
| d031afa269 |
74
DONE.md
74
DONE.md
|
|
@ -2,7 +2,77 @@
|
|||
|
||||
This file tracks completed features, fixes, and milestones.
|
||||
|
||||
## Version 0.1.0-dev19 (2025-12-18 to 2025-12-20) - Convert Module Cleanup & UX Polish
|
||||
## Version 0.1.0-dev20 (2025-12-21) - VT_Player Framework Implementation
|
||||
|
||||
### Features (2025-12-21 Session)
|
||||
- ✅ **VT_Player Module - Complete Framework Implementation**
|
||||
- **Frame-Accurate Video Player Interface** (`internal/player/vtplayer.go`)
|
||||
- Microsecond precision seeking with `SeekToTime()` and `SeekToFrame()`
|
||||
- Frame extraction capabilities for preview systems (`ExtractFrame()`, `ExtractCurrentFrame()`)
|
||||
- Real-time callbacks for position and state updates
|
||||
- Preview mode support for trim/upscale/filter integration
|
||||
- **Multiple Backend Support**
|
||||
- **MPV Controller** (`internal/player/mpv_controller.go`)
|
||||
- Primary backend with best frame accuracy
|
||||
- High-precision seeking with `--hr-seek=yes` and `--hr-seek-framedrop=no`
|
||||
- Command-line MPV integration with IPC control foundation
|
||||
- Hardware acceleration and configuration options
|
||||
- **VLC Controller** (`internal/player/vlc_controller.go`)
|
||||
- Cross-platform fallback option
|
||||
- Command-line VLC integration for compatibility
|
||||
- Basic playback control foundation for RC interface expansion
|
||||
- **FFplay Wrapper** (`internal/player/ffplay_wrapper.go`)
|
||||
- Bridges existing ffplay controller to new VTPlayer interface
|
||||
- Maintains backward compatibility with current codebase
|
||||
- Provides smooth migration path to enhanced player system
|
||||
- **Factory Pattern Implementation** (`internal/player/factory.go`)
|
||||
- Automatic backend detection and selection
|
||||
- Priority order: MPV > VLC > FFplay for optimal performance
|
||||
- Runtime backend availability checking
|
||||
- Configuration-driven backend choice
|
||||
- **Fyne UI Integration** (`internal/player/fyne_ui.go`)
|
||||
- Clean, responsive interface with real-time controls
|
||||
- Frame-accurate seeking with visual feedback
|
||||
- Volume and speed controls
|
||||
- File loading and playback management
|
||||
- Cross-platform compatibility without icon dependencies
|
||||
- **Frame-Accurate Functionality**
|
||||
- Microsecond-precision seeking for professional editing workflows
|
||||
- Frame calculation based on actual video FPS
|
||||
- Real-time position callbacks with 50Hz update rate
|
||||
- Accurate duration tracking and state management
|
||||
- **Preview System Foundation**
|
||||
- `EnablePreviewMode()` for trim/upscale workflow integration
|
||||
- Frame extraction at specific timestamps for preview generation
|
||||
- Live preview support for filter parameter changes
|
||||
- Optimized for preview performance in professional workflows
|
||||
- **Demo and Testing** (`cmd/player_demo/main.go`)
|
||||
- Working demonstration of VT_Player capabilities
|
||||
- Backend detection and selection validation
|
||||
- Frame-accurate method testing
|
||||
- Integration example for other modules
|
||||
|
||||
### Technical Implementation Details
|
||||
- **Cross-Platform Backend Support**: Command-line integration for MPV/VLC with future IPC expansion
|
||||
- **Frame Accuracy**: Microsecond precision timing with time.Duration throughout
|
||||
- **Error Handling**: Graceful fallbacks and comprehensive error reporting
|
||||
- **Resource Management**: Proper process cleanup and context cancellation
|
||||
- **Interface Design**: Clean separation between UI and playback engine
|
||||
- **Future Extensibility**: Foundation for enhanced IPC control and additional backends
|
||||
|
||||
### Integration Points
|
||||
- **Trim Module**: Frame-accurate preview of cut points and timeline navigation
|
||||
- **Upscale Module**: Real-time preview with live parameter updates
|
||||
- **Filters Module**: Frame-by-frame comparison and live effect preview
|
||||
- **Convert Module**: Video loading and preview integration
|
||||
|
||||
### Documentation
|
||||
- ✅ Created comprehensive implementation documentation (`docs/VT_PLAYER_IMPLEMENTATION.md`)
|
||||
- ✅ Documented architecture decisions and backend selection logic
|
||||
- ✅ Provided integration examples for module developers
|
||||
- ✅ Outlined future enhancement roadmap
|
||||
|
||||
## Version 0.1.0-dev20 (2025-12-18 to 2025-12-20) - Convert Module Cleanup & UX Polish
|
||||
|
||||
### Features (2025-12-20 Session)
|
||||
- ✅ **History Sidebar - In Progress Tab**
|
||||
|
|
@ -917,4 +987,4 @@ This file tracks completed features, fixes, and milestones.
|
|||
|
||||
---
|
||||
|
||||
*Last Updated: 2025-12-20*
|
||||
*Last Updated: 2025-12-21*
|
||||
|
|
|
|||
|
|
@ -2,5 +2,5 @@
|
|||
Icon = "assets/logo/VT_Icon.png"
|
||||
Name = "VideoTools"
|
||||
ID = "com.leaktechnologies.videotools"
|
||||
Version = "0.1.0-dev19"
|
||||
Version = "0.1.0-dev20"
|
||||
Build = 19
|
||||
|
|
|
|||
|
|
@ -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,15 +43,16 @@ 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**.
|
||||
For upcoming work and priorities, see **docs/ROADMAP.md**.
|
||||
|
||||
## How to Create a Professional DVD
|
||||
|
||||
|
|
|
|||
12
TODO.md
12
TODO.md
|
|
@ -1,4 +1,4 @@
|
|||
# VideoTools TODO (v0.1.0-dev19+ plan)
|
||||
# VideoTools TODO (v0.1.0-dev20+ plan)
|
||||
|
||||
This file tracks upcoming features, improvements, and known issues.
|
||||
|
||||
|
|
@ -19,7 +19,7 @@ This file tracks upcoming features, improvements, and known issues.
|
|||
- Ensure all conversions preserve color metadata (color_space, color_primaries, color_trc, color_range)
|
||||
- Test with HDR content
|
||||
|
||||
### Completed in dev19 (2025-12-20)
|
||||
### Completed in dev20 (2025-12-20)
|
||||
- [x] **History Sidebar - In Progress Tab** ✅ COMPLETED
|
||||
- Shows running/pending jobs without opening full queue
|
||||
- Animated progress bars per module color
|
||||
|
|
@ -70,7 +70,7 @@ This file tracks upcoming features, improvements, and known issues.
|
|||
- Frame interpolation presets in Filters with Upscale linkage
|
||||
- Real-ESRGAN AI upscale controls with ncnn pipeline (models, presets, tiles, TTA)
|
||||
|
||||
*Last Updated: 2025-12-20*
|
||||
*Last Updated: 2025-12-21*
|
||||
|
||||
## Priority Features for dev20+
|
||||
|
||||
|
|
@ -467,13 +467,15 @@ This file tracks upcoming features, improvements, and known issues.
|
|||
- [ ] Transition effects (optional)
|
||||
- [ ] Chapter markers at join points
|
||||
|
||||
### Trim Module (Lossless-Cut Inspired) 🔄 PLANNED
|
||||
### Trim Module (Lossless-Cut Inspired) ✅ FRAMEWORK READY
|
||||
Trim provides frame-accurate cutting with lossless-first philosophy (inspired by Lossless-Cut):
|
||||
|
||||
#### Core Features
|
||||
- [x] **VT_Player Framework** - Frame-accurate video playback system implemented
|
||||
- [x] **Frame-Accurate Navigation** - Microsecond precision seeking available
|
||||
- [x] **Preview System** - Frame extraction for trim preview functionality
|
||||
- [ ] **Lossless-First Approach** - Stream copy when possible, smart re-encode fallback
|
||||
- [ ] **Keyframe-Snapping Timeline** - Visual keyframe markers with smart snapping
|
||||
- [ ] **Frame-Accurate Navigation** - Reuse VT_Player's keyframe detection system
|
||||
- [ ] **Smart Export System** - Automatic method selection (lossless/re-encode/hybrid)
|
||||
- [ ] **Multi-Segment Trimming** - Multiple cuts from single source with auto-chapters
|
||||
|
||||
|
|
|
|||
BIN
assets/logo/VT_Icon.ico.backup
Normal file
BIN
assets/logo/VT_Icon.ico.backup
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
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()
|
||||
}
|
||||
|
|
@ -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,23 @@ 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.
|
||||
|
||||
## Roadmap
|
||||
|
||||
See `docs/ROADMAP.md` for the current dev focus and priorities.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Essential
|
||||
|
|
@ -135,7 +152,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 +174,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 +373,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
|
||||
|
||||
|
|
@ -58,26 +60,14 @@ If `VideoTools.exe` doesn't exist yet:
|
|||
cd VideoTools
|
||||
```
|
||||
|
||||
2. **Install FFmpeg** (if not already installed):
|
||||
2. **Install dependencies and build**:
|
||||
```bash
|
||||
# Fedora/RHEL
|
||||
sudo dnf install ffmpeg
|
||||
|
||||
# Ubuntu/Debian
|
||||
sudo apt install ffmpeg
|
||||
|
||||
# Arch Linux
|
||||
sudo pacman -S ffmpeg
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
3. **Build VideoTools**:
|
||||
3. **Run**:
|
||||
```bash
|
||||
./scripts/build.sh
|
||||
```
|
||||
|
||||
4. **Run**:
|
||||
```bash
|
||||
./VideoTools
|
||||
./scripts/run.sh
|
||||
```
|
||||
|
||||
### Cross-Compile for Windows from Linux
|
||||
|
|
@ -107,21 +97,16 @@ sudo apt install gcc-mingw-w64 # Ubuntu/Debian
|
|||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
||||
```
|
||||
|
||||
2. **Install FFmpeg**:
|
||||
```bash
|
||||
brew install ffmpeg
|
||||
```
|
||||
|
||||
3. **Clone and build**:
|
||||
2. **Clone and install dependencies/build**:
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd VideoTools
|
||||
go build -o VideoTools
|
||||
./scripts/install.sh
|
||||
```
|
||||
|
||||
4. **Run**:
|
||||
3. **Run**:
|
||||
```bash
|
||||
./VideoTools
|
||||
./scripts/run.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
39
docs/ROADMAP.md
Normal file
39
docs/ROADMAP.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
# VideoTools Roadmap
|
||||
|
||||
This roadmap is intentionally lightweight. It captures the next few
|
||||
high-priority goals without locking the project into a rigid plan.
|
||||
|
||||
## How We Use This
|
||||
|
||||
- The roadmap is a short list, not a full backlog.
|
||||
- Items can move between buckets as priorities change.
|
||||
- We update this at the start of each dev cycle.
|
||||
|
||||
## Current State
|
||||
|
||||
- dev20 focused on cleanup and the Authoring module.
|
||||
- Authoring is now functional (DVD folders + ISO pipeline).
|
||||
|
||||
## Now (dev21 focus)
|
||||
|
||||
- Finalize Convert module cleanup and preset behavior.
|
||||
- Validate preset defaults and edge cases (aspect, bitrate, CRF).
|
||||
- Tighten UI copy and error messaging for Convert/Queue.
|
||||
- Add smoke tests for authoring and DVD encode workflows.
|
||||
|
||||
## Next
|
||||
|
||||
- Color space preservation across Convert/Upscale.
|
||||
- Merge module completion (reorder, mixed format handling).
|
||||
- Filters module polish (controls + real-time preview stability).
|
||||
|
||||
## Later
|
||||
|
||||
- Trim module UX and timeline tooling.
|
||||
- AI frame interpolation support (model management + UI).
|
||||
- Packaging polish for v0.1.1 (AppImage + Windows EXE).
|
||||
|
||||
## Versioning Note
|
||||
|
||||
We keep continuous dev numbering. After v0.1.1 release, the next dev
|
||||
tag becomes v0.1.1-dev26 (or whatever the next number is).
|
||||
|
|
@ -47,7 +47,8 @@ func HandleAudio(files []string) {
|
|||
// HandleAuthor handles the disc authoring module (DVD/Blu-ray) (placeholder)
|
||||
func HandleAuthor(files []string) {
|
||||
logging.Debug(logging.CatModule, "author handler invoked with %v", files)
|
||||
fmt.Println("author", files)
|
||||
// This will be handled by the UI drag-and-drop system
|
||||
// File loading is managed in buildAuthorView()
|
||||
}
|
||||
|
||||
// HandleSubtitles handles the subtitles module (placeholder)
|
||||
|
|
|
|||
160
main.go
160
main.go
|
|
@ -73,7 +73,7 @@ var (
|
|||
logsDirOnce sync.Once
|
||||
logsDirPath string
|
||||
feedbackBundler = utils.NewFeedbackBundler()
|
||||
appVersion = "v0.1.0-dev19"
|
||||
appVersion = "v0.1.0-dev20"
|
||||
|
||||
hwAccelProbeOnce sync.Once
|
||||
hwAccelSupported atomic.Value // map[string]bool
|
||||
|
|
@ -82,18 +82,18 @@ var (
|
|||
nvencRuntimeOK bool
|
||||
|
||||
modulesList = []Module{
|
||||
{"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet
|
||||
{"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue
|
||||
{"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan
|
||||
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
|
||||
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
||||
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
||||
{"author", "Author", utils.MustHex("#FFAA44"), "Convert", modules.HandleAuthor}, // Orange
|
||||
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
||||
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
||||
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
||||
{"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red
|
||||
{"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal
|
||||
{"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet
|
||||
{"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue
|
||||
{"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan
|
||||
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
|
||||
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
||||
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
||||
{"author", "Author", utils.MustHex("#FFAA44"), "Convert", modules.HandleAuthor}, // Orange
|
||||
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
||||
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
||||
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
||||
{"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red
|
||||
{"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal
|
||||
}
|
||||
|
||||
// Platform-specific configuration
|
||||
|
|
@ -907,6 +907,14 @@ type appState struct {
|
|||
authorChapters []authorChapter
|
||||
authorSceneThreshold float64
|
||||
authorDetecting bool
|
||||
authorClips []authorClip // Multiple video clips for compilation
|
||||
authorOutputType string // "dvd" or "iso"
|
||||
authorRegion string // "NTSC", "PAL", "AUTO"
|
||||
authorAspectRatio string // "4:3", "16:9", "AUTO"
|
||||
authorCreateMenu bool // Whether to create DVD menu
|
||||
authorTitle string // DVD title
|
||||
authorSubtitles []string // Subtitle file paths
|
||||
authorAudioTracks []string // Additional audio tracks
|
||||
}
|
||||
|
||||
type mergeClip struct {
|
||||
|
|
@ -921,6 +929,13 @@ type authorChapter struct {
|
|||
Auto bool // True if auto-detected, false if manual
|
||||
}
|
||||
|
||||
type authorClip struct {
|
||||
Path string // Video file path
|
||||
DisplayName string // Display name in UI
|
||||
Duration float64 // Video duration
|
||||
Chapters []authorChapter // Chapters for this clip
|
||||
}
|
||||
|
||||
func (s *appState) persistConvertConfig() {
|
||||
if err := savePersistedConvertConfig(s.convert); err != nil {
|
||||
logging.Debug(logging.CatSystem, "failed to persist convert config: %v", err)
|
||||
|
|
@ -13968,125 +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 {
|
||||
authorColor := moduleColor("author")
|
||||
|
||||
// Back button
|
||||
backBtn := widget.NewButton("< BACK", func() {
|
||||
state.showMainMenu()
|
||||
})
|
||||
backBtn.Importance = widget.LowImportance
|
||||
|
||||
// Title
|
||||
title := canvas.NewText("AUTHOR", authorColor)
|
||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||
title.TextSize = 20
|
||||
|
||||
header := container.NewBorder(nil, nil, backBtn, nil, container.NewCenter(title))
|
||||
|
||||
// Create tabs for different authoring tasks
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Chapters", buildChaptersTab(state)),
|
||||
container.NewTabItem("Rip DVD/ISO", buildRipTab(state)),
|
||||
container.NewTabItem("Author Disc", buildAuthorDiscTab(state)),
|
||||
)
|
||||
tabs.SetTabLocation(container.TabLocationTop)
|
||||
|
||||
return container.NewBorder(header, nil, nil, nil, tabs)
|
||||
}
|
||||
|
||||
func buildChaptersTab(state *appState) fyne.CanvasObject {
|
||||
// File selection
|
||||
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("No file loaded")
|
||||
}
|
||||
|
||||
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 {
|
||||
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 in the next step", 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 soon", state.window)
|
||||
})
|
||||
|
||||
// Export chapters button
|
||||
exportBtn := widget.NewButton("Export Chapters", func() {
|
||||
// TODO: Implement chapter export
|
||||
dialog.ShowInformation("Export", "Chapter export will be implemented soon", 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)
|
||||
}
|
||||
|
||||
func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
||||
placeholder := widget.NewLabel("Disc authoring will be implemented here.\n\nFeatures:\n• Create VIDEO_TS folder structure\n• Generate burn-ready ISO\n• NTSC/PAL selection\n• Menu creation\n• Chapter integration")
|
||||
placeholder.Wrapping = fyne.TextWrapWord
|
||||
return container.NewCenter(placeholder)
|
||||
}
|
||||
|
||||
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