Compare commits

...

3 Commits

Author SHA1 Message Date
1a04cab1d6 Fix snippet duration accuracy with stream copy mode
Improves snippet timing accuracy for Default Format mode by:
- Adding -accurate_seek flag for precise keyframe seeking
- Changing from -t (duration) to -to (end time) for better accuracy
- Adding -avoid_negative_ts make_zero to fix timestamp issues with problematic containers like WMV

This should resolve issues where snippets were 1:20 or 0:21 instead of the configured length (e.g., 10s). Stream copy still uses keyframe-level precision but should now be much closer to target duration.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 23:24:29 -05:00
727bbd9097 Fix drag-and-drop workflow: load videos to memory instead of auto-queuing
Changes multi-video drag-and-drop behavior to load videos into memory instead of automatically adding them to conversion queue. This allows users to:
- Adjust conversion settings before queuing
- Generate snippets instead of converting
- Navigate through videos before deciding to convert

Creates new loadMultipleVideos() function that loads all videos into loadedVideos array and shows informative dialog. Users can now use Convert or Snippet buttons to manually process videos as needed.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 23:23:43 -05:00
6315524a6e Fix UI scaling for small laptop screens (1280x768+)
Reduces default window size from 1280x800 to 1200x700 to fit on 1280x768 laptop screens. Reduces all hardcoded MinSize values for professional cross-resolution support:
- Window default: 1200x700 (was 1280x800)
- Log scroll: 600x350 (was 700x450)
- Deinterlace preview: 640x360 (was 800x450)
- Contact sheet viewer: 700x600 with scroll (was 900x700)
- Contact sheet image: 640x480 (was 800x600)
- Filters settings: 350x400 (was 400x600)
- Upscale settings: 400x400 (was 450x600)

All content uses scrollable containers for proper scaling. Window is resizable and can be maximized via window manager controls.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-16 23:21:58 -05:00

110
main.go
View File

@ -301,7 +301,8 @@ func (s *appState) openLogViewer(title, path string, live bool) {
text.Disable()
bg := canvas.NewRectangle(color.NRGBA{0x15, 0x1a, 0x24, 0xff}) // slightly lighter than app bg
scroll := container.NewVScroll(container.NewMax(bg, text))
scroll.SetMinSize(fyne.NewSize(700, 450))
// Adaptive min size - allows proper scaling on small screens
scroll.SetMinSize(fyne.NewSize(600, 350))
stop := make(chan struct{})
if live {
@ -3366,15 +3367,19 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
if useSourceFormat {
// Source format mode: Use stream copy for clean extraction
// Note: This uses keyframe cutting, so duration may not be frame-perfect
// Calculate end time for more accurate cutting
endTime := center + float64(snippetLength)
args = []string{
"-y",
"-hide_banner",
"-loglevel", "error",
"-accurate_seek", // Seek accurately to keyframes
"-ss", start,
"-i", inputPath,
"-t", fmt.Sprintf("%d", snippetLength),
"-c", "copy", // Stream copy - no re-encoding
"-map", "0", // Include all streams
"-to", fmt.Sprintf("%.2f", endTime), // Use -to instead of -t for better accuracy
"-c", "copy", // Stream copy - no re-encoding
"-map", "0", // Include all streams
"-avoid_negative_ts", "make_zero", // Fix timestamp issues
outputPath,
}
} else {
@ -3728,11 +3733,15 @@ func runGUI() {
} else {
logging.Debug(logging.CatUI, "app icon not found; continuing without custom icon")
}
// Use a generous default window size that fits typical desktops without overflowing.
w.Resize(fyne.NewSize(1280, 800))
w.SetFixedSize(false) // Allow manual resizing
// Adaptive window sizing for professional cross-resolution support
w.SetFixedSize(false) // Allow manual resizing and maximizing
// Use conservative default size that fits on small laptop screens (1280x768)
// Window can be maximized by user using window manager controls
w.Resize(fyne.NewSize(1200, 700))
w.CenterOnScreen()
logging.Debug(logging.CatUI, "window initialized with manual resizing and centering enabled")
logging.Debug(logging.CatUI, "window initialized at 1200x700 (fits 1280x768+ screens), manual resizing enabled")
state := &appState{
window: w,
@ -6078,7 +6087,8 @@ Metadata: %s`,
previewImg := canvas.NewImageFromResource(img)
previewImg.FillMode = canvas.ImageFillContain
previewImg.SetMinSize(fyne.NewSize(800, 450))
// Adaptive size for small screens
previewImg.SetMinSize(fyne.NewSize(640, 360))
infoLabel := widget.NewLabel("Left: Original | Right: Deinterlaced")
infoLabel.Alignment = fyne.TextAlignCenter
@ -6989,10 +6999,11 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
return
}
// If multiple videos, add all to queue
// Load all videos into memory (don't auto-queue)
// This allows users to adjust settings or generate snippets before manually queuing
if len(videoPaths) > 1 {
logging.Debug(logging.CatUI, "multiple videos dropped in convert module; adding all to queue")
go s.batchAddToQueue(videoPaths)
logging.Debug(logging.CatUI, "multiple videos dropped in convert module; loading all into memory")
go s.loadMultipleVideos(videoPaths)
} else {
// Single video: load it
logging.Debug(logging.CatUI, "single video dropped in convert module; loading: %s", videoPaths[0])
@ -7396,6 +7407,65 @@ func (s *appState) loadVideo(path string) {
}, false)
}
// loadMultipleVideos loads multiple videos into memory without auto-queuing
func (s *appState) loadMultipleVideos(paths []string) {
logging.Debug(logging.CatModule, "loading %d videos into memory", len(paths))
var validVideos []*videoSource
var failedFiles []string
for _, path := range paths {
src, err := probeVideo(path)
if err != nil {
logging.Debug(logging.CatFFMPEG, "ffprobe failed for %s: %v", path, err)
failedFiles = append(failedFiles, filepath.Base(path))
continue
}
validVideos = append(validVideos, src)
}
if len(validVideos) == 0 {
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
msg := fmt.Sprintf("Failed to analyze %d file(s):\n%s", len(failedFiles), strings.Join(failedFiles, ", "))
s.showErrorWithCopy("Load Failed", fmt.Errorf("%s", msg))
}, false)
return
}
// Load all videos into loadedVideos array
s.loadedVideos = validVideos
s.currentIndex = 0
// Load the first video to display
firstVideo := validVideos[0]
if frames, err := capturePreviewFrames(firstVideo.Path, firstVideo.Duration); err == nil {
firstVideo.PreviewFrames = frames
if len(frames) > 0 {
s.currentFrame = frames[0]
}
}
s.applyInverseDefaults(firstVideo)
s.convert.OutputBase = s.resolveOutputBase(firstVideo, false)
if firstVideo.EmbeddedCoverArt != "" {
s.convert.CoverArtPath = firstVideo.EmbeddedCoverArt
} else {
s.convert.CoverArtPath = ""
}
s.convert.AspectHandling = "Auto"
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
msg := fmt.Sprintf("Loaded %d video(s) into memory.\nUse arrow buttons or Convert/Snippet buttons to process.", len(validVideos))
if len(failedFiles) > 0 {
msg += fmt.Sprintf("\n\n%d file(s) failed to analyze:\n%s", len(failedFiles), strings.Join(failedFiles, ", "))
}
dialog.ShowInformation("Videos Loaded", msg, s.window)
s.showConvertView(firstVideo)
}, false)
logging.Debug(logging.CatModule, "loaded %d videos into memory", len(validVideos))
}
func (s *appState) clearVideo() {
logging.Debug(logging.CatModule, "clearing loaded video")
s.stopPlayer()
@ -10263,11 +10333,15 @@ func buildThumbView(state *appState) fyne.CanvasObject {
go func() {
img := canvas.NewImageFromFile(contactSheetPath)
img.FillMode = canvas.ImageFillContain
img.SetMinSize(fyne.NewSize(800, 600))
// Adaptive size for small screens - use scrollable dialog
img.SetMinSize(fyne.NewSize(640, 480))
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
d := dialog.NewCustom("Contact Sheet", "Close", img, state.window)
d.Resize(fyne.NewSize(900, 700))
// Wrap in scroll container for large contact sheets
scroll := container.NewScroll(img)
d := dialog.NewCustom("Contact Sheet", "Close", scroll, state.window)
// Adaptive dialog size that fits on 1280x768 screens
d.Resize(fyne.NewSize(700, 600))
d.Show()
}, false)
}()
@ -10545,7 +10619,8 @@ func buildFiltersView(state *appState) fyne.CanvasObject {
)
settingsScroll := container.NewVScroll(settingsPanel)
settingsScroll.SetMinSize(fyne.NewSize(400, 600))
// Adaptive height for small screens - allow content to flow
settingsScroll.SetMinSize(fyne.NewSize(350, 400))
mainContent := container.NewHSplit(
container.NewVBox(leftPanel, videoContainer),
@ -10850,7 +10925,8 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
)
settingsScroll := container.NewVScroll(settingsPanel)
settingsScroll.SetMinSize(fyne.NewSize(450, 600))
// Adaptive height for small screens
settingsScroll.SetMinSize(fyne.NewSize(400, 400))
mainContent := container.NewHSplit(
container.NewVBox(leftPanel, videoContainer),