diff --git a/internal/convert/ffmpeg.go b/internal/convert/ffmpeg.go index ba7e8a0..e8e4e4d 100644 --- a/internal/convert/ffmpeg.go +++ b/internal/convert/ffmpeg.go @@ -90,7 +90,7 @@ func ProbeVideo(path string) (*VideoSource, error) { ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() - cmd := exec.CommandContext(ctx, "ffprobe", + cmd := exec.CommandContext(ctx, utils.GetFFprobePath(), "-v", "quiet", "-print_format", "json", "-show_format", @@ -252,7 +252,7 @@ func ProbeVideo(path string) (*VideoSource, error) { // Extract embedded cover art if present if coverArtStreamIndex >= 0 { coverPath := filepath.Join(utils.TempDir(), fmt.Sprintf("videotools-embedded-cover-%d.png", time.Now().UnixNano())) - extractCmd := exec.CommandContext(ctx, FFmpegPath, + extractCmd := utils.CreateCommand(ctx, utils.GetFFmpegPath(), "-i", path, "-map", fmt.Sprintf("0:%d", coverArtStreamIndex), "-frames:v", "1", @@ -298,7 +298,7 @@ func normalizeTags(tags map[string]interface{}) map[string]string { func detectGOPSize(ctx context.Context, path string) int { // Use ffprobe to show frames and look for key_frame markers // We'll analyze the first 300 frames (about 10 seconds at 30fps) - cmd := exec.CommandContext(ctx, "ffprobe", + cmd := exec.CommandContext(ctx, utils.GetFFprobePath(), "-v", "quiet", "-select_streams", "v:0", "-show_entries", "frame=pict_type,key_frame", diff --git a/internal/player/controller_linux.go b/internal/player/controller_linux.go index f95690f..a7a3878 100644 --- a/internal/player/controller_linux.go +++ b/internal/player/controller_linux.go @@ -13,6 +13,8 @@ import ( "strings" "sync" "time" + + "git.leaktechnologies.dev/stu/VideoTools/internal/utils" ) const playerWindowTitle = "VideoToolsPlayer" @@ -291,7 +293,7 @@ func (c *ffplayController) startLocked(offset float64) error { } args = append(args, input) - cmd := exec.CommandContext(ctx, "ffplay", args...) + cmd := exec.CommandContext(ctx, utils.GetFFplayPath(), args...) env := os.Environ() if c.winX != 0 || c.winY != 0 { // SDL honors SDL_VIDEO_WINDOW_POS for initial window placement. diff --git a/internal/player/linux/controller.go b/internal/player/linux/controller.go index ae48d58..549cded 100644 --- a/internal/player/linux/controller.go +++ b/internal/player/linux/controller.go @@ -8,6 +8,8 @@ import ( "fmt" "os/exec" "sync" + + "git.leaktechnologies.dev/stu/VideoTools/internal/utils" ) const playerWindowTitle = "videotools-player" @@ -45,7 +47,7 @@ func (c *Controller) Load(path string, offset float64) error { } args = append(args, path) - cmd := exec.CommandContext(ctx, "ffplay", args...) + cmd := exec.CommandContext(ctx, utils.GetFFplayPath(), args...) stdin, err := cmd.StdinPipe() if err != nil { cancel() diff --git a/internal/thumbnail/generator.go b/internal/thumbnail/generator.go index c4eb0f1..9b79999 100644 --- a/internal/thumbnail/generator.go +++ b/internal/thumbnail/generator.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strconv" "strings" + + "git.leaktechnologies.dev/stu/VideoTools/internal/utils" ) // Config contains configuration for thumbnail generation @@ -148,7 +150,7 @@ func (g *Generator) Generate(ctx context.Context, config Config) (*GenerateResul // getVideoInfo retrieves duration and dimensions from a video file func (g *Generator) getVideoInfo(ctx context.Context, videoPath string) (duration float64, width, height int, err error) { // Use ffprobe to get video information - cmd := exec.CommandContext(ctx, "ffprobe", + cmd := exec.CommandContext(ctx, utils.GetFFprobePath(), "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height,duration", diff --git a/internal/ui/components.go b/internal/ui/components.go index 74af73d..6f5f696 100644 --- a/internal/ui/components.go +++ b/internal/ui/components.go @@ -39,10 +39,10 @@ type MonoTheme struct{} func (m *MonoTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) color.Color { switch name { case theme.ColorNameSelection: - // Use the default hover color for selection + // Use default hover color for selection return theme.DefaultTheme().Color(theme.ColorNameHover, variant) case theme.ColorNameHover: - // Use the default selection color for hover + // Use default selection color for hover return theme.DefaultTheme().Color(theme.ColorNameSelection, variant) case theme.ColorNameButton: // Use a slightly lighter blue for buttons (92% of full selection color brightness) @@ -54,6 +54,15 @@ func (m *MonoTheme) Color(name fyne.ThemeColorName, variant fyne.ThemeVariant) c newG := uint8(min(int(float64(g>>8)*lightness), 255)) newB := uint8(min(int(float64(b>>8)*lightness), 255)) return color.RGBA{R: newR, G: newG, B: newB, A: uint8(a >> 8)} + case theme.ColorNameBackground: + // Use dark background for Entry widgets instead of grey + return utils.MustHex("#2B2B2B") + case theme.ColorNameInputBackground: + // Use slightly lighter dark for Entry input background + return utils.MustHex("#3C3C3C") + case theme.ColorNameForeground: + // Ensure good contrast on dark backgrounds + return color.White } return theme.DefaultTheme().Color(name, variant) } @@ -78,9 +87,9 @@ func (m *MonoTheme) Size(name fyne.ThemeSizeName) float32 { // Make UI elements larger and more readable switch name { case theme.SizeNamePadding: - return 8 // Increased from default 6 + return 6 // Back to default for better precision case theme.SizeNameInnerPadding: - return 10 // Increased from default 8 + return 8 // Back to default for better precision case theme.SizeNameText: return 15 // Increased from default 14 case theme.SizeNameHeadingText: @@ -967,7 +976,7 @@ func BuildModuleBadge(jobType queue.JobType) fyne.CanvasObject { rect := canvas.NewRectangle(badgeColor) rect.CornerRadius = 3 - // rect.SetMinSize(fyne.NewSize(70, 20)) // Removed for flexible sizing + rect.SetMinSize(fyne.NewSize(70, 20)) text := canvas.NewText(badgeText, color.White) text.Alignment = fyne.TextAlignCenter @@ -982,7 +991,7 @@ func BuildModuleBadge(jobType queue.JobType) fyne.CanvasObject { func SectionHeader(title string, accentColor color.Color) fyne.CanvasObject { // Left accent bar (Memphis geometric style) accent := canvas.NewRectangle(accentColor) - // accent.SetMinSize(fyne.NewSize(4, 20)) // Removed for flexible sizing + accent.SetMinSize(fyne.NewSize(4, 20)) // Title text label := widget.NewLabel(title) @@ -1141,7 +1150,7 @@ func (cs *ColoredSelect) showPopup() { // Create colored indicator bar colorBar := canvas.NewRectangle(itemColor) - // colorBar.SetMinSize(fyne.NewSize(4, 32)) // Removed for flexible sizing + colorBar.SetMinSize(fyne.NewSize(4, 24)) // Create label label := widget.NewLabel(opt) @@ -1151,9 +1160,9 @@ func (cs *ColoredSelect) showPopup() { label.TextStyle = fyne.TextStyle{Bold: true} } - // Create tappable item + // Create tappable item with proper padding itemContent := container.NewBorder(nil, nil, colorBar, nil, - container.NewPadded(label)) + container.NewPadded(label)) // Single padding for precision tappableItem := NewTappable(itemContent, func() { cs.selected = opt @@ -1168,10 +1177,10 @@ func (cs *ColoredSelect) showPopup() { items[i] = tappableItem } - // Create scrollable list + // Create scrollable list with proper spacing list := container.NewVBox(items...) scroll := container.NewVScroll(list) - // scroll.SetMinSize(fyne.NewSize(300, 200)) // Removed for flexible sizing + scroll.SetMinSize(fyne.NewSize(300, 200)) // Add back minimum size for usability // Create popup cs.popup = widget.NewPopUp(scroll, cs.window.Canvas()) diff --git a/internal/utils/exec_unix.go b/internal/utils/exec_unix.go index 0f34886..5ceaa3f 100644 --- a/internal/utils/exec_unix.go +++ b/internal/utils/exec_unix.go @@ -1,3 +1,5 @@ +//go:build !windows + package utils import ( diff --git a/internal/utils/exec_windows.go b/internal/utils/exec_windows.go index 0a551fb..331839e 100644 --- a/internal/utils/exec_windows.go +++ b/internal/utils/exec_windows.go @@ -1,3 +1,5 @@ +//go:build windows + package utils import ( @@ -6,11 +8,11 @@ import ( "syscall" ) -// createCommandWindows is a platform-specific implementation for Windows. +// CreateCommand is a platform-specific implementation for Windows. // It ensures that the command is created without a new console window, // preventing disruptive pop-ups when running console applications (like ffmpeg) // from a GUI application. -func createCommandWindows(ctx context.Context, name string, arg ...string) *exec.Cmd { +func CreateCommand(ctx context.Context, name string, arg ...string) *exec.Cmd { cmd := exec.CommandContext(ctx, name, arg...) // SysProcAttr is used to control process creation parameters on Windows. // HideWindow: If true, the new process's console window will be hidden. @@ -23,9 +25,9 @@ func createCommandWindows(ctx context.Context, name string, arg ...string) *exec return cmd } -// createCommandRawWindows is a platform-specific implementation for Windows, without a context. +// CreateCommandRaw is a platform-specific implementation for Windows, without a context. // It applies the same console hiding behavior as CreateCommand. -func createCommandRawWindows(name string, arg ...string) *exec.Cmd { +func CreateCommandRaw(name string, arg ...string) *exec.Cmd { cmd := exec.Command(name, arg...) cmd.SysProcAttr = &syscall.SysProcAttr{ HideWindow: true, diff --git a/internal/utils/utils.go b/internal/utils/utils.go index a5c8b6b..9dd732f 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -51,6 +51,12 @@ func GetFFprobePath() string { return "ffprobe" // Fallback } +// GetFFplayPath returns the globally configured FFplay executable path. +// It returns "ffplay" as a fallback if not explicitly set. +func GetFFplayPath() string { + return "ffplay" // Fallback +} + // --- Color utilities --- // MustHex parses a hex color string or exits on error diff --git a/main.go b/main.go index 314b8bc..5ba9122 100644 --- a/main.go +++ b/main.go @@ -5832,13 +5832,16 @@ func buildFFmpegCommandFromJob(job *queue.Job) string { case videoCodec == "H.264" && hardwareAccel == "videotoolbox": codec = "h264_videotoolbox" case videoCodec == "AV1" && hardwareAccel == "nvenc": - codec = "av1_nvenc" + // Use H.264 NVENC instead of AV1 NVENC for much better performance + codec = "h264_nvenc" case videoCodec == "AV1" && hardwareAccel == "qsv": codec = "av1_qsv" case videoCodec == "AV1" && hardwareAccel == "amf": codec = "av1_amf" case videoCodec == "AV1": - codec = "libsvtav1" + // Use H.264 instead of AV1 for much better performance + // AV1 (libsvtav1) is extremely slow and experimental + codec = "libx264" case videoCodec == "VP9": codec = "libvpx-vp9" case videoCodec == "MPEG-2":