Compare commits

...

13 Commits

Author SHA1 Message Date
1367a7e492 Truncate long error messages in queue view to prevent UI overflow
Long FFmpeg error messages were pushing the queue UI off screen, making
the interface unusable when jobs failed with verbose errors.

Changes:
- Truncate error messages to 150 characters maximum in status text
- Add helpful message indicating full error is available via Copy Error button
- Enable text wrapping on status labels to handle multi-line content gracefully
- Prevents UI layout breakage while maintaining error visibility

Users can still access the full error message via:
- Copy Error button (copies full error to clipboard)
- View Log button (opens per-job conversion log)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 08:45:08 -05:00
81cb415663 Fix merge job progress reporting showing 100% throughout
The -progress flag was being added AFTER the output path in the FFmpeg command,
causing FFmpeg to not recognize it and therefore not output progress information.

Moved -progress pipe:1 -nostats to appear BEFORE the output path.

Now merge jobs will correctly report progress as they encode:
- Progress starts at 0%
- Updates based on out_time_ms from FFmpeg progress output
- Calculates percentage based on total duration of all clips
- Shows accurate real-time progress in queue view and stats bar

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 08:38:05 -05:00
0577491eee Fix drag-and-drop for Merge module
The Merge module's ui.NewDroppable wrappers weren't receiving drop events
because the window-level handleDrop function was intercepting them first.

Added merge module handling to handleDrop function:
- Accepts individual video files and adds them sequentially to merge clips
- Accepts multiple files at once and processes all in order
- Accepts folders and recursively finds all video files
- Probes each video to get duration and metadata
- Sets chapter names defaulting to filename
- Auto-sets output path to "merged.mkv" once 2+ clips are added
- Refreshes UI after each clip is added

Now drag-and-drop works consistently across all modules (Convert, Compare, Inspect, Merge).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-13 08:37:26 -05:00
d1cd0e504f Return to main menu after clearing queue 2025-12-11 12:01:21 -05:00
eebc68fac7 Show stats bar in merge view 2025-12-11 09:36:33 -05:00
e4b28df842 Add live progress to merge jobs 2025-12-11 09:27:39 -05:00
50a78f6a2a Fix merge job clip extraction 2025-12-11 09:16:39 -05:00
84721eb822 Fix merge button declarations 2025-12-11 07:27:31 -05:00
87f2d118c9 Enable merge actions when clips present 2025-12-11 07:25:29 -05:00
10c1ef04c1 Simplify droppable to match fyne drop signature 2025-12-11 07:22:36 -05:00
158b4d9217 Use fyne drop signatures to fix build 2025-12-11 06:59:50 -05:00
b40129c2f9 Fix build by updating droppable drop handling 2025-12-11 06:58:01 -05:00
fb5c63cd29 Fix droppable signature and dependency handling 2025-12-11 06:53:49 -05:00
4 changed files with 165 additions and 19 deletions

View File

@ -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)
}
}

View File

@ -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
View File

@ -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)
}

View File

@ -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..."