Compare commits

..

No commits in common. "1367a7e492c2956fa4e6b7c7d58a0b7a85eafa64" and "c0081e36933039cf0b00204f90caa5513b37bebe" have entirely different histories.

4 changed files with 19 additions and 165 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(_ fyne.Position, items []fyne.URI) {
if d.onDropped != nil && len(items) > 0 {
func (d *Droppable) Dropped(pos fyne.Position, items []fyne.URI) {
_ = pos
if d.onDropped != nil {
d.onDropped(items)
}
}

View File

@ -140,7 +140,6 @@ 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
@ -274,13 +273,7 @@ func getStatusText(job *queue.Job) string {
}
return fmt.Sprintf("Status: Completed%s", duration)
case queue.JobStatusFailed:
// 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)
return fmt.Sprintf("Status: Failed | Error: %s", job.Error)
case queue.JobStatusCancelled:
return "Status: Cancelled"
default:

150
main.go
View File

@ -1018,8 +1018,7 @@ func (s *appState) refreshQueueView() {
func() { // onClearAll
s.jobQueue.ClearAll()
s.clearVideo()
// Return to main menu after clearing everything to avoid dangling in queue
s.showMainMenu()
s.refreshQueueView() // Refresh
},
func(id string) { // onCopyError
job, err := s.jobQueue.Get(id)
@ -1567,12 +1566,10 @@ func (s *appState) showMergeView() {
s.updateQueueButtonLabel()
topBar := ui.TintedBar(mergeColor, container.NewHBox(backBtn, layout.NewSpacer(), queueBtn))
bottomBar := ui.TintedBar(mergeColor, container.NewHBox(s.statsBar, layout.NewSpacer()))
bottomBar := ui.TintedBar(mergeColor, container.NewHBox(layout.NewSpacer()))
listBox := container.NewVBox()
var addFiles func([]string)
var addQueueBtn *widget.Button
var runNowBtn *widget.Button
var buildList func()
buildList = func() {
@ -1633,15 +1630,6 @@ 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) {
@ -1769,7 +1757,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
@ -1779,7 +1767,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
@ -1832,7 +1820,6 @@ 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 {
@ -1925,20 +1912,9 @@ 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
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 {
for _, rc := range rawClips {
if m, ok := rc.(map[string]interface{}); ok {
clips = append(clips, mergeClip{
Path: toString(m["path"]),
Chapter: toString(m["chapter"]),
@ -2047,69 +2023,23 @@ 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)
}
// Track total duration for progress
var totalDur float64
for _, c := range clips {
if c.Duration > 0 {
totalDur += c.Duration
}
err = cmd.Run()
if err != nil {
return fmt.Errorf("merge failed: %w\nFFmpeg output:\n%s", err, strings.TrimSpace(stderr.String()))
}
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
}
@ -6039,68 +5969,6 @@ 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,24 +29,17 @@ echo ""
cd "$PROJECT_ROOT"
echo "🧹 Cleaning previous builds and cache..."
go clean -cache -testcache 2>/dev/null || true
go clean -cache -modcache -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 (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 "⬇️ Downloading and verifying dependencies..."
go mod download
go mod verify
echo "✓ Dependencies verified"
echo ""
echo "🔨 Building VideoTools..."