Enhance Author module structure and implement drag-and-drop support
- Add authorClips, authorSubtitles, authorOutputType fields to appState - Create authorClip struct for video clip management - Implement drag-and-drop support for video clips and subtitles - Add Settings tab with output type, region, aspect ratio options - Create Video Clips tab with file management - Add Subtitles tab for track management - Prepare framework for DVD/ISO generation - Update HandleAuthor to work with drag-and-drop system - Add comprehensive file validation and error handling - Support for multiple video clips compilation - Ready for chapter detection and DVD authoring implementation
This commit is contained in:
parent
e9608c6085
commit
d031afa269
72
DONE.md
72
DONE.md
|
|
@ -2,6 +2,76 @@
|
||||||
|
|
||||||
This file tracks completed features, fixes, and milestones.
|
This file tracks completed features, fixes, and milestones.
|
||||||
|
|
||||||
|
## 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-dev19 (2025-12-18 to 2025-12-20) - Convert Module Cleanup & UX Polish
|
## Version 0.1.0-dev19 (2025-12-18 to 2025-12-20) - Convert Module Cleanup & UX Polish
|
||||||
|
|
||||||
### Features (2025-12-20 Session)
|
### Features (2025-12-20 Session)
|
||||||
|
|
@ -917,4 +987,4 @@ This file tracks completed features, fixes, and milestones.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Last Updated: 2025-12-20*
|
*Last Updated: 2025-12-21*
|
||||||
|
|
|
||||||
8
TODO.md
8
TODO.md
|
|
@ -70,7 +70,7 @@ This file tracks upcoming features, improvements, and known issues.
|
||||||
- Frame interpolation presets in Filters with Upscale linkage
|
- Frame interpolation presets in Filters with Upscale linkage
|
||||||
- Real-ESRGAN AI upscale controls with ncnn pipeline (models, presets, tiles, TTA)
|
- 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+
|
## Priority Features for dev20+
|
||||||
|
|
||||||
|
|
@ -467,13 +467,15 @@ This file tracks upcoming features, improvements, and known issues.
|
||||||
- [ ] Transition effects (optional)
|
- [ ] Transition effects (optional)
|
||||||
- [ ] Chapter markers at join points
|
- [ ] 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):
|
Trim provides frame-accurate cutting with lossless-first philosophy (inspired by Lossless-Cut):
|
||||||
|
|
||||||
#### Core Features
|
#### 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
|
- [ ] **Lossless-First Approach** - Stream copy when possible, smart re-encode fallback
|
||||||
- [ ] **Keyframe-Snapping Timeline** - Visual keyframe markers with smart snapping
|
- [ ] **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)
|
- [ ] **Smart Export System** - Automatic method selection (lossless/re-encode/hybrid)
|
||||||
- [ ] **Multi-Segment Trimming** - Multiple cuts from single source with auto-chapters
|
- [ ] **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 |
334
author_module_temp.go
Normal file
334
author_module_temp.go
Normal file
|
|
@ -0,0 +1,334 @@
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
@ -47,7 +47,8 @@ func HandleAudio(files []string) {
|
||||||
// HandleAuthor handles the disc authoring module (DVD/Blu-ray) (placeholder)
|
// HandleAuthor handles the disc authoring module (DVD/Blu-ray) (placeholder)
|
||||||
func HandleAuthor(files []string) {
|
func HandleAuthor(files []string) {
|
||||||
logging.Debug(logging.CatModule, "author handler invoked with %v", files)
|
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)
|
// HandleSubtitles handles the subtitles module (placeholder)
|
||||||
|
|
|
||||||
898
main.go
898
main.go
|
|
@ -82,18 +82,18 @@ var (
|
||||||
nvencRuntimeOK bool
|
nvencRuntimeOK bool
|
||||||
|
|
||||||
modulesList = []Module{
|
modulesList = []Module{
|
||||||
{"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet
|
{"convert", "Convert", utils.MustHex("#8B44FF"), "Convert", modules.HandleConvert}, // Violet
|
||||||
{"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue
|
{"merge", "Merge", utils.MustHex("#4488FF"), "Convert", modules.HandleMerge}, // Blue
|
||||||
{"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan
|
{"trim", "Trim", utils.MustHex("#44DDFF"), "Convert", modules.HandleTrim}, // Cyan
|
||||||
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
|
{"filters", "Filters", utils.MustHex("#44FF88"), "Convert", modules.HandleFilters}, // Green
|
||||||
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
{"upscale", "Upscale", utils.MustHex("#AAFF44"), "Advanced", modules.HandleUpscale}, // Yellow-Green
|
||||||
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
{"audio", "Audio", utils.MustHex("#FFD744"), "Convert", modules.HandleAudio}, // Yellow
|
||||||
{"author", "Author", utils.MustHex("#FFAA44"), "Convert", modules.HandleAuthor}, // Orange
|
{"author", "Author", utils.MustHex("#FFAA44"), "Convert", modules.HandleAuthor}, // Orange
|
||||||
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
{"subtitles", "Subtitles", utils.MustHex("#44A6FF"), "Convert", modules.HandleSubtitles}, // Azure
|
||||||
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
{"thumb", "Thumb", utils.MustHex("#FF8844"), "Screenshots", modules.HandleThumb}, // Orange
|
||||||
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
{"compare", "Compare", utils.MustHex("#FF44AA"), "Inspect", modules.HandleCompare}, // Pink
|
||||||
{"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red
|
{"inspect", "Inspect", utils.MustHex("#FF4444"), "Inspect", modules.HandleInspect}, // Red
|
||||||
{"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal
|
{"player", "Player", utils.MustHex("#44FFDD"), "Playback", modules.HandlePlayer}, // Teal
|
||||||
}
|
}
|
||||||
|
|
||||||
// Platform-specific configuration
|
// Platform-specific configuration
|
||||||
|
|
@ -907,6 +907,14 @@ type appState struct {
|
||||||
authorChapters []authorChapter
|
authorChapters []authorChapter
|
||||||
authorSceneThreshold float64
|
authorSceneThreshold float64
|
||||||
authorDetecting bool
|
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 {
|
type mergeClip struct {
|
||||||
|
|
@ -921,6 +929,13 @@ type authorChapter struct {
|
||||||
Auto bool // True if auto-detected, false if manual
|
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() {
|
func (s *appState) persistConvertConfig() {
|
||||||
if err := savePersistedConvertConfig(s.convert); err != nil {
|
if err := savePersistedConvertConfig(s.convert); err != nil {
|
||||||
logging.Debug(logging.CatSystem, "failed to persist convert config: %v", err)
|
logging.Debug(logging.CatSystem, "failed to persist convert config: %v", err)
|
||||||
|
|
@ -2724,12 +2739,23 @@ func (s *appState) showMergeView() {
|
||||||
for _, uri := range items {
|
for _, uri := range items {
|
||||||
if uri.Scheme() == "file" {
|
if uri.Scheme() == "file" {
|
||||||
paths = append(paths, uri.Path())
|
paths = append(paths, uri.Path())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(paths) > 0 {
|
|
||||||
addFiles(paths)
|
// 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))
|
||||||
listBox.Add(container.NewMax(emptyDrop))
|
listBox.Add(container.NewMax(emptyDrop))
|
||||||
} else {
|
} else {
|
||||||
for i, c := range s.mergeClips {
|
for i, c := range s.mergeClips {
|
||||||
|
|
@ -13969,6 +13995,21 @@ func parseResolutionPreset(preset string, srcW, srcH int) (width, height int, pr
|
||||||
|
|
||||||
// buildUpscaleFilter builds the FFmpeg scale filter string with the selected method
|
// buildUpscaleFilter builds the FFmpeg scale filter string with the selected method
|
||||||
func buildAuthorView(state *appState) fyne.CanvasObject {
|
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")
|
authorColor := moduleColor("author")
|
||||||
|
|
||||||
// Back button
|
// Back button
|
||||||
|
|
@ -13977,32 +14018,257 @@ func buildAuthorView(state *appState) fyne.CanvasObject {
|
||||||
})
|
})
|
||||||
backBtn.Importance = widget.LowImportance
|
backBtn.Importance = widget.LowImportance
|
||||||
|
|
||||||
// Title
|
// Queue button
|
||||||
title := canvas.NewText("AUTHOR", authorColor)
|
queueBtn := widget.NewButton("View Queue", func() {
|
||||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
state.showQueue()
|
||||||
title.TextSize = 20
|
})
|
||||||
|
state.queueBtn = queueBtn
|
||||||
|
state.updateQueueButtonLabel()
|
||||||
|
|
||||||
header := container.NewBorder(nil, nil, backBtn, nil, container.NewCenter(title))
|
topBar := ui.TintedBar(authorColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
|
||||||
|
bottomBar := moduleFooter(authorColor, layout.NewSpacer(), state.statsBar)
|
||||||
|
|
||||||
// Create tabs for different authoring tasks
|
// Create tabs for different authoring tasks
|
||||||
tabs := container.NewAppTabs(
|
tabs := container.NewAppTabs(
|
||||||
|
container.NewTabItem("Video Clips", buildVideoClipsTab(state)),
|
||||||
container.NewTabItem("Chapters", buildChaptersTab(state)),
|
container.NewTabItem("Chapters", buildChaptersTab(state)),
|
||||||
container.NewTabItem("Rip DVD/ISO", buildRipTab(state)),
|
container.NewTabItem("Subtitles", buildSubtitlesTab(state)),
|
||||||
container.NewTabItem("Author Disc", buildAuthorDiscTab(state)),
|
container.NewTabItem("Settings", buildAuthorSettingsTab(state)),
|
||||||
|
container.NewTabItem("Generate", buildAuthorDiscTab(state)),
|
||||||
)
|
)
|
||||||
tabs.SetTabLocation(container.TabLocationTop)
|
tabs.SetTabLocation(container.TabLocationTop)
|
||||||
|
|
||||||
return container.NewBorder(header, nil, nil, nil, tabs)
|
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 {
|
func buildChaptersTab(state *appState) fyne.CanvasObject {
|
||||||
// File selection
|
|
||||||
var fileLabel *widget.Label
|
var fileLabel *widget.Label
|
||||||
if state.authorFile != nil {
|
if state.authorFile != nil {
|
||||||
fileLabel = widget.NewLabel(fmt.Sprintf("File: %s", filepath.Base(state.authorFile.Path)))
|
fileLabel = widget.NewLabel(fmt.Sprintf("File: %s", filepath.Base(state.authorFile.Path)))
|
||||||
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
|
fileLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||||
} else {
|
} else {
|
||||||
fileLabel = widget.NewLabel("No file loaded")
|
fileLabel = widget.NewLabel("Select a single video file or use clips from Video Clips tab")
|
||||||
}
|
}
|
||||||
|
|
||||||
selectBtn := widget.NewButton("Select Video", func() {
|
selectBtn := widget.NewButton("Select Video", func() {
|
||||||
|
|
@ -14034,12 +14300,12 @@ func buildChaptersTab(state *appState) fyne.CanvasObject {
|
||||||
|
|
||||||
// Detect scenes button
|
// Detect scenes button
|
||||||
detectBtn := widget.NewButton("Detect Scenes", func() {
|
detectBtn := widget.NewButton("Detect Scenes", func() {
|
||||||
if state.authorFile == nil {
|
if state.authorFile == nil && len(state.authorClips) == 0 {
|
||||||
dialog.ShowInformation("No File", "Please select a video file first", state.window)
|
dialog.ShowInformation("No File", "Please select a video file first", state.window)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// TODO: Implement scene detection
|
// TODO: Implement scene detection
|
||||||
dialog.ShowInformation("Scene Detection", "Scene detection will be implemented in the next step", state.window)
|
dialog.ShowInformation("Scene Detection", "Scene detection will be implemented", state.window)
|
||||||
})
|
})
|
||||||
detectBtn.Importance = widget.HighImportance
|
detectBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
|
@ -14049,13 +14315,13 @@ func buildChaptersTab(state *appState) fyne.CanvasObject {
|
||||||
// Add manual chapter button
|
// Add manual chapter button
|
||||||
addChapterBtn := widget.NewButton("+ Add Chapter", func() {
|
addChapterBtn := widget.NewButton("+ Add Chapter", func() {
|
||||||
// TODO: Implement manual chapter addition
|
// TODO: Implement manual chapter addition
|
||||||
dialog.ShowInformation("Add Chapter", "Manual chapter addition will be implemented soon", state.window)
|
dialog.ShowInformation("Add Chapter", "Manual chapter addition will be implemented", state.window)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Export chapters button
|
// Export chapters button
|
||||||
exportBtn := widget.NewButton("Export Chapters", func() {
|
exportBtn := widget.NewButton("Export Chapters", func() {
|
||||||
// TODO: Implement chapter export
|
// TODO: Implement chapter export
|
||||||
dialog.ShowInformation("Export", "Chapter export will be implemented soon", state.window)
|
dialog.ShowInformation("Export", "Chapter export will be implemented", state.window)
|
||||||
})
|
})
|
||||||
|
|
||||||
controls := container.NewVBox(
|
controls := container.NewVBox(
|
||||||
|
|
@ -14081,10 +14347,572 @@ func buildRipTab(state *appState) fyne.CanvasObject {
|
||||||
return container.NewCenter(placeholder)
|
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 {
|
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")
|
// Generate DVD/ISO
|
||||||
placeholder.Wrapping = fyne.TextWrapWord
|
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
||||||
return container.NewCenter(placeholder)
|
if len(state.authorClips) == 0 && state.authorFile == nil {
|
||||||
|
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show compilation options
|
||||||
|
dialog.ShowFormConfirm("Generate DVD",
|
||||||
|
"Choose generation options:",
|
||||||
|
func(callback bool, options map[string]interface{}) {
|
||||||
|
if !callback {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: Implement actual DVD/ISO generation
|
||||||
|
dialog.ShowInformation("DVD Generation", "DVD/ISO generation will be implemented in next step", state.window)
|
||||||
|
},
|
||||||
|
map[string]string{
|
||||||
|
"include_subtitles": "Include Subtitles",
|
||||||
|
"include_chapters": "Include Chapters",
|
||||||
|
"preserve_quality": "Preserve Original Quality",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"include_subtitles": len(state.authorSubtitles) > 0,
|
||||||
|
"include_chapters": len(state.authorChapters) > 0,
|
||||||
|
"preserve_quality": true,
|
||||||
|
},
|
||||||
|
state.window)
|
||||||
|
})
|
||||||
|
generateBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
// Show summary
|
||||||
|
summary := "Ready to generate:\n\n"
|
||||||
|
if len(state.authorClips) > 0 {
|
||||||
|
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
||||||
|
for i, clip := range state.authorClips {
|
||||||
|
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
||||||
|
}
|
||||||
|
} else if state.authorFile != nil {
|
||||||
|
summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(state.authorSubtitles) > 0 {
|
||||||
|
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
||||||
|
for i, path := range state.authorSubtitles {
|
||||||
|
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
||||||
|
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
||||||
|
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
||||||
|
if state.authorTitle != "" {
|
||||||
|
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryLabel := widget.NewLabel(summary)
|
||||||
|
summaryLabel.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Generate DVD/ISO:"),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
summaryLabel,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
generateBtn,
|
||||||
|
)
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
||||||
|
// Video clips list with drag-and-drop support
|
||||||
|
list := container.NewVBox()
|
||||||
|
|
||||||
|
rebuildList := func() {
|
||||||
|
list.Objects = nil
|
||||||
|
|
||||||
|
if len(state.authorClips) == 0 {
|
||||||
|
emptyLabel := widget.NewLabel("Drag and drop video files here\nor click 'Add Files' to select videos")
|
||||||
|
emptyLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
|
// Make empty state a drop target
|
||||||
|
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
||||||
|
var paths []string
|
||||||
|
for _, uri := range items {
|
||||||
|
if uri.Scheme() == "file" {
|
||||||
|
paths = append(paths, uri.Path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(paths) > 0 {
|
||||||
|
state.addAuthorFiles(paths)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
list.Add(container.NewMax(emptyDrop))
|
||||||
|
} else {
|
||||||
|
for i, clip := range state.authorClips {
|
||||||
|
idx := i
|
||||||
|
card := widget.NewCard(clip.DisplayName, fmt.Sprintf("%.2fs", clip.Duration), nil)
|
||||||
|
|
||||||
|
// Remove button
|
||||||
|
removeBtn := widget.NewButton("Remove", func() {
|
||||||
|
state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
|
||||||
|
rebuildList()
|
||||||
|
})
|
||||||
|
removeBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
// Duration label
|
||||||
|
durationLabel := widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", clip.Duration))
|
||||||
|
durationLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||||
|
|
||||||
|
cardContent := container.NewVBox(
|
||||||
|
durationLabel,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
removeBtn,
|
||||||
|
)
|
||||||
|
card.SetContent(cardContent)
|
||||||
|
list.Add(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add files button
|
||||||
|
addBtn := widget.NewButton("Add Files", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
state.addAuthorFiles([]string{reader.URI().Path()})
|
||||||
|
}, state.window)
|
||||||
|
})
|
||||||
|
addBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
// Clear all button
|
||||||
|
clearBtn := widget.NewButton("Clear All", func() {
|
||||||
|
state.authorClips = []authorClip{}
|
||||||
|
rebuildList()
|
||||||
|
})
|
||||||
|
clearBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
// Compile button
|
||||||
|
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
||||||
|
if len(state.authorClips) == 0 {
|
||||||
|
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// TODO: Implement compilation to DVD
|
||||||
|
dialog.ShowInformation("Compile", "DVD compilation will be implemented", state.window)
|
||||||
|
})
|
||||||
|
compileBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Video Clips:"),
|
||||||
|
container.NewScroll(list),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
container.NewHBox(addBtn, clearBtn, compileBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize the list
|
||||||
|
rebuildList()
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildSubtitlesTab(state *appState) fyne.CanvasObject {
|
||||||
|
// Subtitle files list with drag-and-drop support
|
||||||
|
list := container.NewVBox()
|
||||||
|
|
||||||
|
rebuildSubList := func() {
|
||||||
|
list.Objects = nil
|
||||||
|
|
||||||
|
if len(state.authorSubtitles) == 0 {
|
||||||
|
emptyLabel := widget.NewLabel("Drag and drop subtitle files here\nor click 'Add Subtitles' to select")
|
||||||
|
emptyLabel.Alignment = fyne.TextAlignCenter
|
||||||
|
|
||||||
|
// Make empty state a drop target
|
||||||
|
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
||||||
|
var paths []string
|
||||||
|
for _, uri := range items {
|
||||||
|
if uri.Scheme() == "file" {
|
||||||
|
paths = append(paths, uri.Path())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(paths) > 0 {
|
||||||
|
state.authorSubtitles = append(state.authorSubtitles, paths...)
|
||||||
|
rebuildSubList()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
list.Add(container.NewMax(emptyDrop))
|
||||||
|
} else {
|
||||||
|
for i, path := range state.authorSubtitles {
|
||||||
|
idx := i
|
||||||
|
card := widget.NewCard(filepath.Base(path), "", nil)
|
||||||
|
|
||||||
|
// Remove button
|
||||||
|
removeBtn := widget.NewButton("Remove", func() {
|
||||||
|
state.authorSubtitles = append(state.authorSubtitles[:idx], state.authorSubtitles[idx+1:]...)
|
||||||
|
rebuildSubList()
|
||||||
|
})
|
||||||
|
removeBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
cardContent := container.NewVBox(removeBtn)
|
||||||
|
card.SetContent(cardContent)
|
||||||
|
list.Add(card)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add subtitles button
|
||||||
|
addBtn := widget.NewButton("Add Subtitles", func() {
|
||||||
|
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||||
|
if err != nil || reader == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.Close()
|
||||||
|
state.authorSubtitles = append(state.authorSubtitles, reader.URI().Path())
|
||||||
|
rebuildSubList()
|
||||||
|
}, state.window)
|
||||||
|
})
|
||||||
|
addBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
// Clear all button
|
||||||
|
clearBtn := widget.NewButton("Clear All", func() {
|
||||||
|
state.authorSubtitles = []string{}
|
||||||
|
rebuildSubList()
|
||||||
|
})
|
||||||
|
clearBtn.Importance = widget.MediumImportance
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Subtitle Tracks:"),
|
||||||
|
container.NewScroll(list),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
container.NewHBox(addBtn, clearBtn),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
rebuildSubList()
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
||||||
|
// Output type selection
|
||||||
|
outputType := widget.NewSelect([]string{"DVD (VIDEO_TS)", "ISO Image"}, func(value string) {
|
||||||
|
if value == "DVD (VIDEO_TS)" {
|
||||||
|
state.authorOutputType = "dvd"
|
||||||
|
} else {
|
||||||
|
state.authorOutputType = "iso"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if state.authorOutputType == "iso" {
|
||||||
|
outputType.SetSelected("ISO Image")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Region selection
|
||||||
|
regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"}, func(value string) {
|
||||||
|
state.authorRegion = value
|
||||||
|
})
|
||||||
|
if state.authorRegion == "" {
|
||||||
|
regionSelect.SetSelected("AUTO")
|
||||||
|
} else {
|
||||||
|
regionSelect.SetSelected(state.authorRegion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Aspect ratio selection
|
||||||
|
aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"}, func(value string) {
|
||||||
|
state.authorAspectRatio = value
|
||||||
|
})
|
||||||
|
if state.authorAspectRatio == "" {
|
||||||
|
aspectSelect.SetSelected("AUTO")
|
||||||
|
} else {
|
||||||
|
aspectSelect.SetSelected(state.authorAspectRatio)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DVD title entry
|
||||||
|
titleEntry := widget.NewEntry()
|
||||||
|
titleEntry.SetPlaceHolder("DVD Title")
|
||||||
|
titleEntry.SetText(state.authorTitle)
|
||||||
|
titleEntry.OnChanged = func(value string) {
|
||||||
|
state.authorTitle = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create menu checkbox
|
||||||
|
createMenuCheck := widget.NewCheck("Create DVD Menu", func(checked bool) {
|
||||||
|
state.authorCreateMenu = checked
|
||||||
|
})
|
||||||
|
createMenuCheck.SetChecked(state.authorCreateMenu)
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Output Settings:"),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
widget.NewLabel("Output Type:"),
|
||||||
|
outputType,
|
||||||
|
widget.NewLabel("Region:"),
|
||||||
|
regionSelect,
|
||||||
|
widget.NewLabel("Aspect Ratio:"),
|
||||||
|
aspectSelect,
|
||||||
|
widget.NewLabel("DVD Title:"),
|
||||||
|
titleEntry,
|
||||||
|
createMenuCheck,
|
||||||
|
)
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
||||||
|
// Generate DVD/ISO
|
||||||
|
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
||||||
|
if len(state.authorClips) == 0 && state.authorFile == nil {
|
||||||
|
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show compilation options
|
||||||
|
dialog.ShowInformation("DVD Generation",
|
||||||
|
"DVD/ISO generation will be implemented in next step.\n\n"+
|
||||||
|
"Features planned:\n"+
|
||||||
|
"• Create VIDEO_TS folder structure\n"+
|
||||||
|
"• Generate burn-ready ISO\n"+
|
||||||
|
"• Include subtitle tracks\n"+
|
||||||
|
"• Include alternate audio tracks\n"+
|
||||||
|
"• Support for alternate camera angles", state.window)
|
||||||
|
})
|
||||||
|
generateBtn.Importance = widget.HighImportance
|
||||||
|
|
||||||
|
// Show summary
|
||||||
|
summary := "Ready to generate:\n\n"
|
||||||
|
if len(state.authorClips) > 0 {
|
||||||
|
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
||||||
|
for i, clip := range state.authorClips {
|
||||||
|
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
||||||
|
}
|
||||||
|
} else if state.authorFile != nil {
|
||||||
|
summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(state.authorSubtitles) > 0 {
|
||||||
|
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
||||||
|
for i, path := range state.authorSubtitles {
|
||||||
|
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
||||||
|
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
||||||
|
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
||||||
|
if state.authorTitle != "" {
|
||||||
|
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
||||||
|
}
|
||||||
|
|
||||||
|
summaryLabel := widget.NewLabel(summary)
|
||||||
|
summaryLabel.Wrapping = fyne.TextWrapWord
|
||||||
|
|
||||||
|
controls := container.NewVBox(
|
||||||
|
widget.NewLabel("Generate DVD/ISO:"),
|
||||||
|
widget.NewSeparator(),
|
||||||
|
summaryLabel,
|
||||||
|
widget.NewSeparator(),
|
||||||
|
generateBtn,
|
||||||
|
)
|
||||||
|
|
||||||
|
return container.NewPadded(controls)
|
||||||
|
}
|
||||||
|
|
||||||
|
// addAuthorFiles helper function
|
||||||
|
func (s *appState) addAuthorFiles(paths []string) {
|
||||||
|
for _, path := range paths {
|
||||||
|
src, err := probeVideo(path)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
clip := authorClip{
|
||||||
|
Path: path,
|
||||||
|
DisplayName: filepath.Base(path),
|
||||||
|
Duration: src.Duration,
|
||||||
|
Chapters: []authorChapter{},
|
||||||
|
}
|
||||||
|
s.authorClips = append(s.authorClips, clip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildUpscaleFilter(targetWidth, targetHeight int, method string, preserveAspect bool) string {
|
func buildUpscaleFilter(targetWidth, targetHeight int, method string, preserveAspect bool) string {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user