Compare commits

...

4 Commits

Author SHA1 Message Date
2761d35ed6 Fix stats bar UI thread updates 2025-12-20 14:14:42 -05:00
f558119f4f Add app icon support and window sizing improvements
- Update LoadAppIcon() to search for PNG first (better Linux support)
- Add FyneApp.toml for icon metadata and Windows embedding
- Create VideoTools.desktop for Linux application launcher integration
- Change default window size from 1200x700 to 800x600
- Icon now appears in taskbar, app switcher, and Windows title bar

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-20 14:13:18 -05:00
601acf9ccf Replace benchmark error dialogs with notifications 2025-12-20 14:12:33 -05:00
e020f06873 Refresh history sidebar when jobs complete 2025-12-20 14:08:03 -05:00
7 changed files with 127 additions and 44 deletions

View File

@ -768,6 +768,10 @@ This file tracks completed features, fixes, and milestones.
- ✅ Ranked benchmark results by score and added cancel confirmation
- ✅ Added estimated audio bitrate fallback when metadata is missing
- ✅ Made target file size input unit-selectable with numeric-only entry
- ✅ Prevented snippet runaway bitrates when using Match Source Format
- ✅ History sidebar refreshes when jobs complete (snippet entries now appear)
- ✅ Benchmark errors now show non-blocking notifications instead of OK popups
- ✅ Fixed stats bar updates to run on the UI thread to avoid Fyne warnings
- ✅ Stabilized video seeking and embedded rendering
- ✅ Improved player window positioning
- ✅ Fixed clear video functionality

6
FyneApp.toml Normal file
View File

@ -0,0 +1,6 @@
[Details]
Icon = "assets/logo/VT_Icon.png"
Name = "VideoTools"
ID = "com.leaktechnologies.videotools"
Version = "0.1.0-dev19"
Build = 19

View File

@ -43,6 +43,9 @@ This file tracks upcoming features, improvements, and known issues.
- Lossless + Target Size mode support
- Audio bitrate estimation when metadata is missing
- Target size unit selector and numeric entry
- Snippet history updates in sidebar
- Non-blocking benchmark error notifications
- Stats bar updates run on the UI thread
## Priority Features for dev20+

10
VideoTools.desktop Normal file
View File

@ -0,0 +1,10 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=VideoTools
Comment=Video conversion and processing tool
Exec=/home/stu/Projects/VideoTools/VideoTools
Icon=/home/stu/Projects/VideoTools/assets/logo/VT_Icon.png
Terminal=false
Categories=AudioVideo;Video;
StartupWMClass=VideoTools

View File

@ -468,29 +468,44 @@ func NewConversionStatsBar(onTapped func()) *ConversionStatsBar {
// UpdateStats updates the stats display
func (c *ConversionStatsBar) UpdateStats(running, pending, completed, failed, cancelled int, progress float64, jobTitle string) {
c.running = running
c.pending = pending
c.completed = completed
c.failed = failed
c.cancelled = cancelled
c.progress = progress
c.jobTitle = jobTitle
c.Refresh()
c.updateStats(func() {
c.running = running
c.pending = pending
c.completed = completed
c.failed = failed
c.cancelled = cancelled
c.progress = progress
c.jobTitle = jobTitle
})
}
// UpdateStatsWithDetails updates the stats display with detailed conversion info
func (c *ConversionStatsBar) UpdateStatsWithDetails(running, pending, completed, failed, cancelled int, progress, fps, speed float64, eta, jobTitle string) {
c.running = running
c.pending = pending
c.completed = completed
c.failed = failed
c.cancelled = cancelled
c.progress = progress
c.fps = fps
c.speed = speed
c.eta = eta
c.jobTitle = jobTitle
c.Refresh()
c.updateStats(func() {
c.running = running
c.pending = pending
c.completed = completed
c.failed = failed
c.cancelled = cancelled
c.progress = progress
c.fps = fps
c.speed = speed
c.eta = eta
c.jobTitle = jobTitle
})
}
func (c *ConversionStatsBar) updateStats(update func()) {
app := fyne.CurrentApp()
if app == nil || app.Driver() == nil {
update()
c.Refresh()
return
}
app.Driver().DoFromGoroutine(func() {
update()
c.Refresh()
}, false)
}
// CreateRenderer creates the renderer for the stats bar

View File

@ -240,13 +240,23 @@ func MakeIconButton(symbol, tooltip string, tapped func()) *widget.Button {
// LoadAppIcon loads the application icon from standard locations
func LoadAppIcon() fyne.Resource {
search := []string{
filepath.Join("assets", "logo", "VT_Icon.svg"),
// Try PNG first (better compatibility), then SVG
iconFiles := []string{"VT_Icon.png", "VT_Icon.svg"}
var search []string
// Search in current directory first
for _, iconFile := range iconFiles {
search = append(search, filepath.Join("assets", "logo", iconFile))
}
// Then search relative to executable
if exe, err := os.Executable(); err == nil {
dir := filepath.Dir(exe)
search = append(search, filepath.Join(dir, "assets", "logo", "VT_Icon.svg"))
for _, iconFile := range iconFiles {
search = append(search, filepath.Join(dir, "assets", "logo", iconFile))
}
}
for _, p := range search {
if _, err := os.Stat(p); err == nil {
res, err := fyne.LoadResourceFromPath(p)
@ -254,8 +264,10 @@ func LoadAppIcon() fyne.Resource {
logging.Debug(logging.CatUI, "failed to load icon %s: %v", p, err)
continue
}
logging.Debug(logging.CatUI, "loaded app icon from %s", p)
return res
}
}
logging.Debug(logging.CatUI, "no app icon found in search paths")
return nil
}

77
main.go
View File

@ -1789,7 +1789,10 @@ func (s *appState) showBenchmark() {
return
}
logging.Debug(logging.CatSystem, "failed to generate test video: %v", err)
dialog.ShowError(fmt.Errorf("failed to generate test video: %w", err), s.window)
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Benchmark Error",
Content: fmt.Sprintf("Failed to generate test video: %v", err),
})
s.showMainMenu()
return
}
@ -1812,7 +1815,10 @@ func (s *appState) showBenchmark() {
return
}
logging.Debug(logging.CatSystem, "benchmark failed: %v", err)
dialog.ShowError(fmt.Errorf("benchmark failed: %w", err), s.window)
fyne.CurrentApp().SendNotification(&fyne.Notification{
Title: "Benchmark Error",
Content: fmt.Sprintf("Benchmark failed: %v", err),
})
s.showMainMenu()
return
}
@ -4089,27 +4095,48 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
args = append(args, "-b:a", "192k")
}
} else {
// For non-WMV: use source codec or fallback to H.264
videoCodec := src.VideoCodec
if videoCodec == "" || strings.Contains(strings.ToLower(videoCodec), "wmv") {
// For non-WMV: match source codec where possible, but cap bitrate for snippets
videoCodec := strings.ToLower(strings.TrimSpace(src.VideoCodec))
switch {
case strings.Contains(videoCodec, "264"):
videoCodec = "libx264"
case strings.Contains(videoCodec, "265"), strings.Contains(videoCodec, "hevc"):
videoCodec = "libx265"
case strings.Contains(videoCodec, "vp9"):
videoCodec = "libvpx-vp9"
case strings.Contains(videoCodec, "av1"):
videoCodec = "libsvtav1"
default:
videoCodec = "libx264"
}
args = append(args, "-c:v", videoCodec)
// Apply encoder preset if supported codec
if strings.Contains(strings.ToLower(videoCodec), "264") ||
strings.Contains(strings.ToLower(videoCodec), "265") {
if conv.EncoderPreset != "" {
args = append(args, "-preset", conv.EncoderPreset)
} else {
args = append(args, "-preset", "slow")
}
if conv.CRF != "" {
args = append(args, "-crf", conv.CRF)
} else {
args = append(args, "-crf", "18")
}
preset := conv.EncoderPreset
if preset == "" {
preset = "slow"
}
crfVal := conv.CRF
if crfVal == "" {
crfVal = "18"
}
if strings.TrimSpace(crfVal) == "0" {
crfVal = "18"
}
targetBitrate := clampSnippetBitrate(strings.TrimSpace(conv.VideoBitrate), src.Width)
if targetBitrate == "" {
targetBitrate = clampSnippetBitrate(defaultBitrate(conv.VideoCodec, src.Width, src.Bitrate), src.Width)
}
if targetBitrate == "" {
targetBitrate = clampSnippetBitrate("3500k", src.Width)
}
if strings.Contains(videoCodec, "x264") || strings.Contains(videoCodec, "x265") {
args = append(args, "-preset", preset, "-crf", crfVal, "-maxrate", targetBitrate, "-bufsize", targetBitrate)
} else if strings.Contains(videoCodec, "vp9") || strings.Contains(videoCodec, "av1") {
args = append(args, "-crf", crfVal, "-maxrate", targetBitrate, "-bufsize", targetBitrate)
}
// Audio codec
@ -4817,12 +4844,12 @@ func runGUI() {
// 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))
// Use compact default size (800x600) that fits on any screen
// Window can be resized or maximized by user using window manager controls
w.Resize(fyne.NewSize(800, 600))
w.CenterOnScreen()
logging.Debug(logging.CatUI, "window initialized at 1200x700 (fits 1280x768+ screens), manual resizing enabled")
logging.Debug(logging.CatUI, "window initialized at 800x600 (compact default), manual resizing enabled")
state := &appState{
window: w,
@ -4907,6 +4934,7 @@ func runGUI() {
return
}
app.Driver().DoFromGoroutine(func() {
historyCount := len(state.historyEntries)
// Add completed jobs to history
jobs := state.jobQueue.List()
for _, job := range jobs {
@ -4922,6 +4950,11 @@ func runGUI() {
if state.active == "queue" {
state.refreshQueueView()
}
if state.active == "mainmenu" && state.sidebarVisible && len(state.historyEntries) != historyCount {
state.navigationHistorySuppress = true
state.showMainMenu()
state.navigationHistorySuppress = false
}
}, false)
})