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.
|
||||
|
||||
## 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
|
||||
|
||||
### 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
|
||||
- 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 |
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)
|
||||
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)
|
||||
|
|
|
|||
898
main.go
898
main.go
|
|
@ -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)
|
||||
|
|
@ -2724,12 +2739,23 @@ func (s *appState) showMergeView() {
|
|||
for _, uri := range items {
|
||||
if uri.Scheme() == "file" {
|
||||
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))
|
||||
} else {
|
||||
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
|
||||
func buildAuthorView(state *appState) fyne.CanvasObject {
|
||||
state.stopPreview()
|
||||
state.lastModule = state.active
|
||||
state.active = "author"
|
||||
|
||||
// Initialize default values
|
||||
if state.authorOutputType == "" {
|
||||
state.authorOutputType = "dvd"
|
||||
}
|
||||
if state.authorRegion == "" {
|
||||
state.authorRegion = "AUTO"
|
||||
}
|
||||
if state.authorAspectRatio == "" {
|
||||
state.authorAspectRatio = "AUTO"
|
||||
}
|
||||
|
||||
authorColor := moduleColor("author")
|
||||
|
||||
// Back button
|
||||
|
|
@ -13977,32 +14018,257 @@ func buildAuthorView(state *appState) fyne.CanvasObject {
|
|||
})
|
||||
backBtn.Importance = widget.LowImportance
|
||||
|
||||
// Title
|
||||
title := canvas.NewText("AUTHOR", authorColor)
|
||||
title.TextStyle = fyne.TextStyle{Monospace: true, Bold: true}
|
||||
title.TextSize = 20
|
||||
// Queue button
|
||||
queueBtn := widget.NewButton("View Queue", func() {
|
||||
state.showQueue()
|
||||
})
|
||||
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
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("Video Clips", buildVideoClipsTab(state)),
|
||||
container.NewTabItem("Chapters", buildChaptersTab(state)),
|
||||
container.NewTabItem("Rip DVD/ISO", buildRipTab(state)),
|
||||
container.NewTabItem("Author Disc", buildAuthorDiscTab(state)),
|
||||
container.NewTabItem("Subtitles", buildSubtitlesTab(state)),
|
||||
container.NewTabItem("Settings", buildAuthorSettingsTab(state)),
|
||||
container.NewTabItem("Generate", buildAuthorDiscTab(state)),
|
||||
)
|
||||
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 {
|
||||
// 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")
|
||||
fileLabel = widget.NewLabel("Select a single video file or use clips from Video Clips tab")
|
||||
}
|
||||
|
||||
selectBtn := widget.NewButton("Select Video", func() {
|
||||
|
|
@ -14034,12 +14300,12 @@ func buildChaptersTab(state *appState) fyne.CanvasObject {
|
|||
|
||||
// Detect scenes button
|
||||
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)
|
||||
return
|
||||
}
|
||||
// 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
|
||||
|
||||
|
|
@ -14049,13 +14315,13 @@ func buildChaptersTab(state *appState) fyne.CanvasObject {
|
|||
// 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)
|
||||
dialog.ShowInformation("Add Chapter", "Manual chapter addition will be implemented", state.window)
|
||||
})
|
||||
|
||||
// Export chapters button
|
||||
exportBtn := widget.NewButton("Export Chapters", func() {
|
||||
// TODO: Implement chapter export
|
||||
dialog.ShowInformation("Export", "Chapter export will be implemented soon", state.window)
|
||||
dialog.ShowInformation("Export", "Chapter export will be implemented", state.window)
|
||||
})
|
||||
|
||||
controls := container.NewVBox(
|
||||
|
|
@ -14081,10 +14347,572 @@ func buildRipTab(state *appState) fyne.CanvasObject {
|
|||
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 {
|
||||
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)
|
||||
// Generate DVD/ISO
|
||||
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
||||
if len(state.authorClips) == 0 && state.authorFile == nil {
|
||||
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
||||
return
|
||||
}
|
||||
|
||||
// Show compilation options
|
||||
dialog.ShowFormConfirm("Generate DVD",
|
||||
"Choose generation options:",
|
||||
func(callback bool, options map[string]interface{}) {
|
||||
if !callback {
|
||||
return
|
||||
}
|
||||
// TODO: Implement actual DVD/ISO generation
|
||||
dialog.ShowInformation("DVD Generation", "DVD/ISO generation will be implemented in next step", state.window)
|
||||
},
|
||||
map[string]string{
|
||||
"include_subtitles": "Include Subtitles",
|
||||
"include_chapters": "Include Chapters",
|
||||
"preserve_quality": "Preserve Original Quality",
|
||||
},
|
||||
map[string]interface{}{
|
||||
"include_subtitles": len(state.authorSubtitles) > 0,
|
||||
"include_chapters": len(state.authorChapters) > 0,
|
||||
"preserve_quality": true,
|
||||
},
|
||||
state.window)
|
||||
})
|
||||
generateBtn.Importance = widget.HighImportance
|
||||
|
||||
// Show summary
|
||||
summary := "Ready to generate:\n\n"
|
||||
if len(state.authorClips) > 0 {
|
||||
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
||||
for i, clip := range state.authorClips {
|
||||
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
||||
}
|
||||
} else if state.authorFile != nil {
|
||||
summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
|
||||
}
|
||||
|
||||
if len(state.authorSubtitles) > 0 {
|
||||
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
||||
for i, path := range state.authorSubtitles {
|
||||
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
||||
}
|
||||
}
|
||||
|
||||
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
||||
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
||||
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
||||
if state.authorTitle != "" {
|
||||
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
||||
}
|
||||
|
||||
summaryLabel := widget.NewLabel(summary)
|
||||
summaryLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
controls := container.NewVBox(
|
||||
widget.NewLabel("Generate DVD/ISO:"),
|
||||
widget.NewSeparator(),
|
||||
summaryLabel,
|
||||
widget.NewSeparator(),
|
||||
generateBtn,
|
||||
)
|
||||
|
||||
return container.NewPadded(controls)
|
||||
}
|
||||
|
||||
func buildVideoClipsTab(state *appState) fyne.CanvasObject {
|
||||
// Video clips list with drag-and-drop support
|
||||
list := container.NewVBox()
|
||||
|
||||
rebuildList := func() {
|
||||
list.Objects = nil
|
||||
|
||||
if len(state.authorClips) == 0 {
|
||||
emptyLabel := widget.NewLabel("Drag and drop video files here\nor click 'Add Files' to select videos")
|
||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Make empty state a drop target
|
||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
||||
var paths []string
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() == "file" {
|
||||
paths = append(paths, uri.Path())
|
||||
}
|
||||
}
|
||||
if len(paths) > 0 {
|
||||
state.addAuthorFiles(paths)
|
||||
}
|
||||
})
|
||||
|
||||
list.Add(container.NewMax(emptyDrop))
|
||||
} else {
|
||||
for i, clip := range state.authorClips {
|
||||
idx := i
|
||||
card := widget.NewCard(clip.DisplayName, fmt.Sprintf("%.2fs", clip.Duration), nil)
|
||||
|
||||
// Remove button
|
||||
removeBtn := widget.NewButton("Remove", func() {
|
||||
state.authorClips = append(state.authorClips[:idx], state.authorClips[idx+1:]...)
|
||||
rebuildList()
|
||||
})
|
||||
removeBtn.Importance = widget.MediumImportance
|
||||
|
||||
// Duration label
|
||||
durationLabel := widget.NewLabel(fmt.Sprintf("Duration: %.2f seconds", clip.Duration))
|
||||
durationLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||
|
||||
cardContent := container.NewVBox(
|
||||
durationLabel,
|
||||
widget.NewSeparator(),
|
||||
removeBtn,
|
||||
)
|
||||
card.SetContent(cardContent)
|
||||
list.Add(card)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add files button
|
||||
addBtn := widget.NewButton("Add Files", func() {
|
||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||
if err != nil || reader == nil {
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
state.addAuthorFiles([]string{reader.URI().Path()})
|
||||
}, state.window)
|
||||
})
|
||||
addBtn.Importance = widget.HighImportance
|
||||
|
||||
// Clear all button
|
||||
clearBtn := widget.NewButton("Clear All", func() {
|
||||
state.authorClips = []authorClip{}
|
||||
rebuildList()
|
||||
})
|
||||
clearBtn.Importance = widget.MediumImportance
|
||||
|
||||
// Compile button
|
||||
compileBtn := widget.NewButton("COMPILE TO DVD", func() {
|
||||
if len(state.authorClips) == 0 {
|
||||
dialog.ShowInformation("No Clips", "Please add video clips first", state.window)
|
||||
return
|
||||
}
|
||||
// TODO: Implement compilation to DVD
|
||||
dialog.ShowInformation("Compile", "DVD compilation will be implemented", state.window)
|
||||
})
|
||||
compileBtn.Importance = widget.HighImportance
|
||||
|
||||
controls := container.NewVBox(
|
||||
widget.NewLabel("Video Clips:"),
|
||||
container.NewScroll(list),
|
||||
widget.NewSeparator(),
|
||||
container.NewHBox(addBtn, clearBtn, compileBtn),
|
||||
)
|
||||
|
||||
// Initialize the list
|
||||
rebuildList()
|
||||
|
||||
return container.NewPadded(controls)
|
||||
}
|
||||
|
||||
func buildSubtitlesTab(state *appState) fyne.CanvasObject {
|
||||
// Subtitle files list with drag-and-drop support
|
||||
list := container.NewVBox()
|
||||
|
||||
rebuildSubList := func() {
|
||||
list.Objects = nil
|
||||
|
||||
if len(state.authorSubtitles) == 0 {
|
||||
emptyLabel := widget.NewLabel("Drag and drop subtitle files here\nor click 'Add Subtitles' to select")
|
||||
emptyLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Make empty state a drop target
|
||||
emptyDrop := ui.NewDroppable(container.NewCenter(emptyLabel), func(items []fyne.URI) {
|
||||
var paths []string
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() == "file" {
|
||||
paths = append(paths, uri.Path())
|
||||
}
|
||||
}
|
||||
if len(paths) > 0 {
|
||||
state.authorSubtitles = append(state.authorSubtitles, paths...)
|
||||
rebuildSubList()
|
||||
}
|
||||
})
|
||||
|
||||
list.Add(container.NewMax(emptyDrop))
|
||||
} else {
|
||||
for i, path := range state.authorSubtitles {
|
||||
idx := i
|
||||
card := widget.NewCard(filepath.Base(path), "", nil)
|
||||
|
||||
// Remove button
|
||||
removeBtn := widget.NewButton("Remove", func() {
|
||||
state.authorSubtitles = append(state.authorSubtitles[:idx], state.authorSubtitles[idx+1:]...)
|
||||
rebuildSubList()
|
||||
})
|
||||
removeBtn.Importance = widget.MediumImportance
|
||||
|
||||
cardContent := container.NewVBox(removeBtn)
|
||||
card.SetContent(cardContent)
|
||||
list.Add(card)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add subtitles button
|
||||
addBtn := widget.NewButton("Add Subtitles", func() {
|
||||
dialog.ShowFileOpen(func(reader fyne.URIReadCloser, err error) {
|
||||
if err != nil || reader == nil {
|
||||
return
|
||||
}
|
||||
defer reader.Close()
|
||||
state.authorSubtitles = append(state.authorSubtitles, reader.URI().Path())
|
||||
rebuildSubList()
|
||||
}, state.window)
|
||||
})
|
||||
addBtn.Importance = widget.HighImportance
|
||||
|
||||
// Clear all button
|
||||
clearBtn := widget.NewButton("Clear All", func() {
|
||||
state.authorSubtitles = []string{}
|
||||
rebuildSubList()
|
||||
})
|
||||
clearBtn.Importance = widget.MediumImportance
|
||||
|
||||
controls := container.NewVBox(
|
||||
widget.NewLabel("Subtitle Tracks:"),
|
||||
container.NewScroll(list),
|
||||
widget.NewSeparator(),
|
||||
container.NewHBox(addBtn, clearBtn),
|
||||
)
|
||||
|
||||
// Initialize
|
||||
rebuildSubList()
|
||||
|
||||
return container.NewPadded(controls)
|
||||
}
|
||||
|
||||
func buildAuthorSettingsTab(state *appState) fyne.CanvasObject {
|
||||
// Output type selection
|
||||
outputType := widget.NewSelect([]string{"DVD (VIDEO_TS)", "ISO Image"}, func(value string) {
|
||||
if value == "DVD (VIDEO_TS)" {
|
||||
state.authorOutputType = "dvd"
|
||||
} else {
|
||||
state.authorOutputType = "iso"
|
||||
}
|
||||
})
|
||||
if state.authorOutputType == "iso" {
|
||||
outputType.SetSelected("ISO Image")
|
||||
}
|
||||
|
||||
// Region selection
|
||||
regionSelect := widget.NewSelect([]string{"AUTO", "NTSC", "PAL"}, func(value string) {
|
||||
state.authorRegion = value
|
||||
})
|
||||
if state.authorRegion == "" {
|
||||
regionSelect.SetSelected("AUTO")
|
||||
} else {
|
||||
regionSelect.SetSelected(state.authorRegion)
|
||||
}
|
||||
|
||||
// Aspect ratio selection
|
||||
aspectSelect := widget.NewSelect([]string{"AUTO", "4:3", "16:9"}, func(value string) {
|
||||
state.authorAspectRatio = value
|
||||
})
|
||||
if state.authorAspectRatio == "" {
|
||||
aspectSelect.SetSelected("AUTO")
|
||||
} else {
|
||||
aspectSelect.SetSelected(state.authorAspectRatio)
|
||||
}
|
||||
|
||||
// DVD title entry
|
||||
titleEntry := widget.NewEntry()
|
||||
titleEntry.SetPlaceHolder("DVD Title")
|
||||
titleEntry.SetText(state.authorTitle)
|
||||
titleEntry.OnChanged = func(value string) {
|
||||
state.authorTitle = value
|
||||
}
|
||||
|
||||
// Create menu checkbox
|
||||
createMenuCheck := widget.NewCheck("Create DVD Menu", func(checked bool) {
|
||||
state.authorCreateMenu = checked
|
||||
})
|
||||
createMenuCheck.SetChecked(state.authorCreateMenu)
|
||||
|
||||
controls := container.NewVBox(
|
||||
widget.NewLabel("Output Settings:"),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel("Output Type:"),
|
||||
outputType,
|
||||
widget.NewLabel("Region:"),
|
||||
regionSelect,
|
||||
widget.NewLabel("Aspect Ratio:"),
|
||||
aspectSelect,
|
||||
widget.NewLabel("DVD Title:"),
|
||||
titleEntry,
|
||||
createMenuCheck,
|
||||
)
|
||||
|
||||
return container.NewPadded(controls)
|
||||
}
|
||||
|
||||
func buildAuthorDiscTab(state *appState) fyne.CanvasObject {
|
||||
// Generate DVD/ISO
|
||||
generateBtn := widget.NewButton("GENERATE DVD", func() {
|
||||
if len(state.authorClips) == 0 && state.authorFile == nil {
|
||||
dialog.ShowInformation("No Content", "Please add video clips or select a single video file", state.window)
|
||||
return
|
||||
}
|
||||
|
||||
// Show compilation options
|
||||
dialog.ShowInformation("DVD Generation",
|
||||
"DVD/ISO generation will be implemented in next step.\n\n"+
|
||||
"Features planned:\n"+
|
||||
"• Create VIDEO_TS folder structure\n"+
|
||||
"• Generate burn-ready ISO\n"+
|
||||
"• Include subtitle tracks\n"+
|
||||
"• Include alternate audio tracks\n"+
|
||||
"• Support for alternate camera angles", state.window)
|
||||
})
|
||||
generateBtn.Importance = widget.HighImportance
|
||||
|
||||
// Show summary
|
||||
summary := "Ready to generate:\n\n"
|
||||
if len(state.authorClips) > 0 {
|
||||
summary += fmt.Sprintf("Video Clips: %d\n", len(state.authorClips))
|
||||
for i, clip := range state.authorClips {
|
||||
summary += fmt.Sprintf(" %d. %s (%.2fs)\n", i+1, clip.DisplayName, clip.Duration)
|
||||
}
|
||||
} else if state.authorFile != nil {
|
||||
summary += fmt.Sprintf("Video File: %s\n", filepath.Base(state.authorFile.Path))
|
||||
}
|
||||
|
||||
if len(state.authorSubtitles) > 0 {
|
||||
summary += fmt.Sprintf("Subtitle Tracks: %d\n", len(state.authorSubtitles))
|
||||
for i, path := range state.authorSubtitles {
|
||||
summary += fmt.Sprintf(" %d. %s\n", i+1, filepath.Base(path))
|
||||
}
|
||||
}
|
||||
|
||||
summary += fmt.Sprintf("Output Type: %s\n", state.authorOutputType)
|
||||
summary += fmt.Sprintf("Region: %s\n", state.authorRegion)
|
||||
summary += fmt.Sprintf("Aspect Ratio: %s\n", state.authorAspectRatio)
|
||||
if state.authorTitle != "" {
|
||||
summary += fmt.Sprintf("DVD Title: %s\n", state.authorTitle)
|
||||
}
|
||||
|
||||
summaryLabel := widget.NewLabel(summary)
|
||||
summaryLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
controls := container.NewVBox(
|
||||
widget.NewLabel("Generate DVD/ISO:"),
|
||||
widget.NewSeparator(),
|
||||
summaryLabel,
|
||||
widget.NewSeparator(),
|
||||
generateBtn,
|
||||
)
|
||||
|
||||
return container.NewPadded(controls)
|
||||
}
|
||||
|
||||
// addAuthorFiles helper function
|
||||
func (s *appState) addAuthorFiles(paths []string) {
|
||||
for _, path := range paths {
|
||||
src, err := probeVideo(path)
|
||||
if err != nil {
|
||||
dialog.ShowError(fmt.Errorf("failed to load video %s: %w", filepath.Base(path), err), s.window)
|
||||
continue
|
||||
}
|
||||
|
||||
clip := authorClip{
|
||||
Path: path,
|
||||
DisplayName: filepath.Base(path),
|
||||
Duration: src.Duration,
|
||||
Chapters: []authorChapter{},
|
||||
}
|
||||
s.authorClips = append(s.authorClips, clip)
|
||||
}
|
||||
}
|
||||
|
||||
func buildUpscaleFilter(targetWidth, targetHeight int, method string, preserveAspect bool) string {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user