Compare commits
4 Commits
19f2922366
...
2761d35ed6
| Author | SHA1 | Date | |
|---|---|---|---|
| 2761d35ed6 | |||
| f558119f4f | |||
| 601acf9ccf | |||
| e020f06873 |
4
DONE.md
4
DONE.md
|
|
@ -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
6
FyneApp.toml
Normal 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
|
||||
3
TODO.md
3
TODO.md
|
|
@ -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
10
VideoTools.desktop
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
77
main.go
|
|
@ -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)
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user