Compare commits
13 Commits
c0081e3693
...
1367a7e492
| Author | SHA1 | Date | |
|---|---|---|---|
| 1367a7e492 | |||
| 81cb415663 | |||
| 0577491eee | |||
| d1cd0e504f | |||
| eebc68fac7 | |||
| e4b28df842 | |||
| 50a78f6a2a | |||
| 84721eb822 | |||
| 87f2d118c9 | |||
| 10c1ef04c1 | |||
| 158b4d9217 | |||
| b40129c2f9 | |||
| fb5c63cd29 |
|
|
@ -294,12 +294,12 @@ func (d *Droppable) DraggedOver(pos fyne.Position) {
|
|||
}
|
||||
|
||||
// DraggedOut clears highlight (optional)
|
||||
func (d *Droppable) DraggedOut() {}
|
||||
func (d *Droppable) DraggedOut() {
|
||||
}
|
||||
|
||||
// Dropped handles drop events
|
||||
func (d *Droppable) Dropped(pos fyne.Position, items []fyne.URI) {
|
||||
_ = pos
|
||||
if d.onDropped != nil {
|
||||
func (d *Droppable) Dropped(_ fyne.Position, items []fyne.URI) {
|
||||
if d.onDropped != nil && len(items) > 0 {
|
||||
d.onDropped(items)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ func buildJobItem(
|
|||
statusText := getStatusText(job)
|
||||
statusLabel := widget.NewLabel(statusText)
|
||||
statusLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
statusLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
// Control buttons
|
||||
var buttons []fyne.CanvasObject
|
||||
|
|
@ -273,7 +274,13 @@ func getStatusText(job *queue.Job) string {
|
|||
}
|
||||
return fmt.Sprintf("Status: Completed%s", duration)
|
||||
case queue.JobStatusFailed:
|
||||
return fmt.Sprintf("Status: Failed | Error: %s", job.Error)
|
||||
// Truncate error to prevent UI overflow
|
||||
errMsg := job.Error
|
||||
maxLen := 150
|
||||
if len(errMsg) > maxLen {
|
||||
errMsg = errMsg[:maxLen] + "… (see Copy Error button for full message)"
|
||||
}
|
||||
return fmt.Sprintf("Status: Failed | Error: %s", errMsg)
|
||||
case queue.JobStatusCancelled:
|
||||
return "Status: Cancelled"
|
||||
default:
|
||||
|
|
|
|||
150
main.go
150
main.go
|
|
@ -1018,7 +1018,8 @@ func (s *appState) refreshQueueView() {
|
|||
func() { // onClearAll
|
||||
s.jobQueue.ClearAll()
|
||||
s.clearVideo()
|
||||
s.refreshQueueView() // Refresh
|
||||
// Return to main menu after clearing everything to avoid dangling in queue
|
||||
s.showMainMenu()
|
||||
},
|
||||
func(id string) { // onCopyError
|
||||
job, err := s.jobQueue.Get(id)
|
||||
|
|
@ -1566,10 +1567,12 @@ func (s *appState) showMergeView() {
|
|||
s.updateQueueButtonLabel()
|
||||
|
||||
topBar := ui.TintedBar(mergeColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
|
||||
bottomBar := ui.TintedBar(mergeColor, container.NewHBox(layout.NewSpacer()))
|
||||
bottomBar := ui.TintedBar(mergeColor, container.NewHBox(s.statsBar, layout.NewSpacer()))
|
||||
|
||||
listBox := container.NewVBox()
|
||||
var addFiles func([]string)
|
||||
var addQueueBtn *widget.Button
|
||||
var runNowBtn *widget.Button
|
||||
|
||||
var buildList func()
|
||||
buildList = func() {
|
||||
|
|
@ -1630,6 +1633,15 @@ func (s *appState) showMergeView() {
|
|||
}
|
||||
}
|
||||
listBox.Refresh()
|
||||
if addQueueBtn != nil && runNowBtn != nil {
|
||||
if len(s.mergeClips) >= 2 {
|
||||
addQueueBtn.Enable()
|
||||
runNowBtn.Enable()
|
||||
} else {
|
||||
addQueueBtn.Disable()
|
||||
runNowBtn.Disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addFiles = func(paths []string) {
|
||||
|
|
@ -1757,7 +1769,7 @@ func (s *appState) showMergeView() {
|
|||
}, s.window)
|
||||
})
|
||||
|
||||
addQueueBtn := widget.NewButton("Add Merge to Queue", func() {
|
||||
addQueueBtn = widget.NewButton("Add Merge to Queue", func() {
|
||||
if err := s.addMergeToQueue(false); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
return
|
||||
|
|
@ -1767,7 +1779,7 @@ func (s *appState) showMergeView() {
|
|||
s.jobQueue.Start()
|
||||
}
|
||||
})
|
||||
runNowBtn := widget.NewButton("Merge Now", func() {
|
||||
runNowBtn = widget.NewButton("Merge Now", func() {
|
||||
if err := s.addMergeToQueue(true); err != nil {
|
||||
dialog.ShowError(err, s.window)
|
||||
return
|
||||
|
|
@ -1820,6 +1832,7 @@ func (s *appState) showMergeView() {
|
|||
s.setContent(container.NewBorder(topBar, bottomBar, nil, nil, container.NewPadded(content)))
|
||||
|
||||
buildList()
|
||||
s.updateStatsBar()
|
||||
}
|
||||
|
||||
func (s *appState) addMergeToQueue(startNow bool) error {
|
||||
|
|
@ -1912,9 +1925,20 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress
|
|||
outputPath, _ := cfg["outputPath"].(string)
|
||||
|
||||
rawClips, _ := cfg["clips"].([]interface{})
|
||||
rawClipMaps, _ := cfg["clips"].([]map[string]interface{})
|
||||
var clips []mergeClip
|
||||
for _, rc := range rawClips {
|
||||
if m, ok := rc.(map[string]interface{}); ok {
|
||||
if len(rawClips) > 0 {
|
||||
for _, rc := range rawClips {
|
||||
if m, ok := rc.(map[string]interface{}); ok {
|
||||
clips = append(clips, mergeClip{
|
||||
Path: toString(m["path"]),
|
||||
Chapter: toString(m["chapter"]),
|
||||
Duration: toFloat(m["duration"]),
|
||||
})
|
||||
}
|
||||
}
|
||||
} else if len(rawClipMaps) > 0 {
|
||||
for _, m := range rawClipMaps {
|
||||
clips = append(clips, mergeClip{
|
||||
Path: toString(m["path"]),
|
||||
Chapter: toString(m["chapter"]),
|
||||
|
|
@ -2023,23 +2047,69 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress
|
|||
}
|
||||
}
|
||||
|
||||
// Add progress output for live updates (must be before output path)
|
||||
args = append(args, "-progress", "pipe:1", "-nostats")
|
||||
|
||||
args = append(args, outputPath)
|
||||
|
||||
// Execute
|
||||
cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, args...)
|
||||
utils.ApplyNoWindow(cmd)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge stdout pipe: %w", err)
|
||||
}
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if progressCallback != nil {
|
||||
progressCallback(0)
|
||||
}
|
||||
err = cmd.Run()
|
||||
if err != nil {
|
||||
return fmt.Errorf("merge failed: %w\nFFmpeg output:\n%s", err, strings.TrimSpace(stderr.String()))
|
||||
|
||||
// Track total duration for progress
|
||||
var totalDur float64
|
||||
for _, c := range clips {
|
||||
if c.Duration > 0 {
|
||||
totalDur += c.Duration
|
||||
}
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("merge start failed: %w (%s)", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
|
||||
// Parse progress
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
parts := strings.SplitN(line, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
key, val := parts[0], parts[1]
|
||||
if key == "out_time_ms" && totalDur > 0 && progressCallback != nil {
|
||||
if ms, err := strconv.ParseFloat(val, 64); err == nil {
|
||||
pct := (ms / 1000.0 / totalDur) * 100
|
||||
if pct > 100 {
|
||||
pct = 100
|
||||
}
|
||||
progressCallback(pct)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
err = cmd.Wait()
|
||||
if progressCallback != nil {
|
||||
progressCallback(100)
|
||||
}
|
||||
if err != nil {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
return fmt.Errorf("merge failed: %w\nFFmpeg output:\n%s", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -5969,6 +6039,68 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
|||
return
|
||||
}
|
||||
|
||||
// If in merge module, handle multiple video files
|
||||
if s.active == "merge" {
|
||||
// Collect all video files from the dropped items
|
||||
var videoPaths []string
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() != "file" {
|
||||
continue
|
||||
}
|
||||
path := uri.Path()
|
||||
logging.Debug(logging.CatModule, "drop received path=%s", path)
|
||||
|
||||
// Check if it's a directory
|
||||
if info, err := os.Stat(path); err == nil && info.IsDir() {
|
||||
logging.Debug(logging.CatModule, "processing directory: %s", path)
|
||||
videos := s.findVideoFiles(path)
|
||||
videoPaths = append(videoPaths, videos...)
|
||||
} else if s.isVideoFile(path) {
|
||||
videoPaths = append(videoPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
if len(videoPaths) == 0 {
|
||||
logging.Debug(logging.CatUI, "no valid video files in dropped items")
|
||||
return
|
||||
}
|
||||
|
||||
// Add all videos to merge clips sequentially
|
||||
go func() {
|
||||
for _, path := range videoPaths {
|
||||
src, err := probeVideo(path)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatModule, "failed to probe %s: %v", path, err)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
dialog.ShowError(fmt.Errorf("failed to probe %s: %w", filepath.Base(path), err), s.window)
|
||||
}, false)
|
||||
continue
|
||||
}
|
||||
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.mergeClips = append(s.mergeClips, mergeClip{
|
||||
Path: path,
|
||||
Chapter: strings.TrimSuffix(filepath.Base(path), filepath.Ext(path)),
|
||||
Duration: src.Duration,
|
||||
})
|
||||
|
||||
// Set default output path if not set and we have at least 2 clips
|
||||
if len(s.mergeClips) >= 2 && strings.TrimSpace(s.mergeOutput) == "" {
|
||||
first := filepath.Dir(s.mergeClips[0].Path)
|
||||
s.mergeOutput = filepath.Join(first, "merged.mkv")
|
||||
}
|
||||
|
||||
// Refresh the merge view to show the new clips
|
||||
s.showMergeView()
|
||||
}, false)
|
||||
}
|
||||
|
||||
logging.Debug(logging.CatModule, "added %d clips to merge list", len(videoPaths))
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Other modules don't handle file drops yet
|
||||
logging.Debug(logging.CatUI, "drop ignored; module %s cannot handle files", s.active)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,17 +29,24 @@ echo ""
|
|||
cd "$PROJECT_ROOT"
|
||||
|
||||
echo "🧹 Cleaning previous builds and cache..."
|
||||
go clean -cache -modcache -testcache 2>/dev/null || true
|
||||
go clean -cache -testcache 2>/dev/null || true
|
||||
rm -f "$BUILD_OUTPUT" 2>/dev/null || true
|
||||
# Also clear build cache directory to avoid permission issues
|
||||
rm -rf "${GOCACHE:-$HOME/.cache/go-build}" 2>/dev/null || true
|
||||
echo "✓ Cache cleaned"
|
||||
echo ""
|
||||
|
||||
echo "⬇️ Downloading and verifying dependencies..."
|
||||
go mod download
|
||||
go mod verify
|
||||
echo "✓ Dependencies verified"
|
||||
echo "⬇️ Downloading and verifying dependencies (skips if already cached)..."
|
||||
if go list -m all >/dev/null 2>&1; then
|
||||
echo "✓ Dependencies already present"
|
||||
else
|
||||
if go mod download && go mod verify; then
|
||||
echo "✓ Dependencies downloaded and verified"
|
||||
else
|
||||
echo "❌ Failed to download/verify modules. Check network/GOPROXY or try again."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "🔨 Building VideoTools..."
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user