Add simple bitrate/resolution/aspect controls and cache helper
This commit is contained in:
parent
2f16d4af36
commit
d094010440
|
|
@ -39,6 +39,11 @@ func formatBytes(b int64) string {
|
|||
}
|
||||
}
|
||||
|
||||
// FormatBytes exposes human-readable bytes with binary units.
|
||||
func FormatBytes(b int64) string {
|
||||
return formatBytes(b)
|
||||
}
|
||||
|
||||
// DeltaBytes renders size plus delta vs reference.
|
||||
func DeltaBytes(newBytes, refBytes int64) string {
|
||||
if newBytes <= 0 {
|
||||
|
|
@ -62,7 +67,7 @@ func DeltaBitrate(newBps, refBps int) string {
|
|||
if newBps <= 0 {
|
||||
return "--"
|
||||
}
|
||||
br := formatBitrate(newBps)
|
||||
br := formatBitrateHuman(newBps)
|
||||
if refBps <= 0 || refBps == newBps {
|
||||
return br
|
||||
}
|
||||
|
|
@ -83,3 +88,14 @@ func formatPercent(val float64) string {
|
|||
}
|
||||
return fmt.Sprintf("%.1f%%", val)
|
||||
}
|
||||
|
||||
func formatBitrateHuman(bps int) string {
|
||||
if bps <= 0 {
|
||||
return "--"
|
||||
}
|
||||
kbps := float64(bps) / 1000.0
|
||||
if kbps >= 1000 {
|
||||
return fmt.Sprintf("%.1f Mbps", kbps/1000.0)
|
||||
}
|
||||
return fmt.Sprintf("%.0f kbps", kbps)
|
||||
}
|
||||
|
|
|
|||
139
main.go
139
main.go
|
|
@ -1334,10 +1334,6 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
cfg := job.Config
|
||||
inputPath := cfg["inputPath"].(string)
|
||||
outputPath := cfg["outputPath"].(string)
|
||||
sourceBitrate := 0
|
||||
if v, ok := cfg["sourceBitrate"].(float64); ok {
|
||||
sourceBitrate = int(v)
|
||||
}
|
||||
|
||||
// If a direct conversion is running, wait until it finishes before starting queued jobs.
|
||||
for s.convertBusy {
|
||||
|
|
@ -1517,7 +1513,7 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
sourceWidth, _ := cfg["sourceWidth"].(int)
|
||||
sourceHeight, _ := cfg["sourceHeight"].(int)
|
||||
// Get source bitrate if present
|
||||
sourceBitrate = 0
|
||||
sourceBitrate := 0
|
||||
if v, ok := cfg["sourceBitrate"].(float64); ok {
|
||||
sourceBitrate = int(v)
|
||||
}
|
||||
|
|
@ -1617,6 +1613,9 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
} else if bitrateMode == "CBR" {
|
||||
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
|
||||
args = append(args, "-b:v", videoBitrate, "-minrate", videoBitrate, "-maxrate", videoBitrate, "-bufsize", videoBitrate)
|
||||
} else {
|
||||
vb := defaultBitrate(videoCodec, sourceWidth, sourceBitrate)
|
||||
args = append(args, "-b:v", vb, "-minrate", vb, "-maxrate", vb, "-bufsize", vb)
|
||||
}
|
||||
} else if bitrateMode == "VBR" {
|
||||
if videoBitrate, _ := cfg["videoBitrate"].(string); videoBitrate != "" {
|
||||
|
|
@ -2865,6 +2864,30 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
bitratePresetSelect.SetSelected(state.convert.BitratePreset)
|
||||
|
||||
// Simple bitrate selector (shares presets)
|
||||
simpleBitrateSelect := widget.NewSelect(bitratePresetLabels, func(value string) {
|
||||
state.convert.BitratePreset = value
|
||||
if applyBitratePreset != nil {
|
||||
applyBitratePreset(value)
|
||||
}
|
||||
})
|
||||
simpleBitrateSelect.SetSelected(state.convert.BitratePreset)
|
||||
|
||||
// Simple resolution selector (separate widget to avoid double-parent issues)
|
||||
resolutionSelectSimple := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
|
||||
state.convert.TargetResolution = value
|
||||
logging.Debug(logging.CatUI, "target resolution set to %s (simple)", value)
|
||||
})
|
||||
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
|
||||
|
||||
// Simple aspect selector (separate widget)
|
||||
targetAspectSelectSimple := widget.NewSelect(aspectTargets, func(value string) {
|
||||
logging.Debug(logging.CatUI, "target aspect set to %s (simple)", value)
|
||||
state.convert.OutputAspect = value
|
||||
updateAspectBoxVisibility()
|
||||
})
|
||||
targetAspectSelectSimple.SetSelected(state.convert.OutputAspect)
|
||||
|
||||
// Target File Size with smart presets + manual entry
|
||||
targetFileSizeEntry = widget.NewEntry()
|
||||
targetFileSizeEntry.SetPlaceHolder("e.g., 25MB, 100MB, 8MB")
|
||||
|
|
@ -3028,11 +3051,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
}
|
||||
updateEncodingControls()
|
||||
|
||||
// Target Resolution
|
||||
// Target Resolution (advanced)
|
||||
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
|
||||
state.convert.TargetResolution = value
|
||||
logging.Debug(logging.CatUI, "target resolution set to %s", value)
|
||||
})
|
||||
if state.convert.TargetResolution == "" {
|
||||
state.convert.TargetResolution = "Source"
|
||||
}
|
||||
resolutionSelect.SetSelected(state.convert.TargetResolution)
|
||||
|
||||
// Frame Rate with hint
|
||||
|
|
@ -3217,7 +3243,14 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
widget.NewLabel("Choose slower for better compression, faster for speed"),
|
||||
widget.NewLabelWithStyle("Encoder Preset", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
simplePresetSelect,
|
||||
widget.NewLabel("Aspect ratio will match source video"),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabelWithStyle("Bitrate (simple presets)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
simpleBitrateSelect,
|
||||
widget.NewLabelWithStyle("Target Resolution", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
resolutionSelectSimple,
|
||||
widget.NewLabelWithStyle("Target Aspect Ratio", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
targetAspectSelectSimple,
|
||||
targetAspectHint,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
|
||||
|
|
@ -3328,23 +3361,13 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
tabs.OnSelected = func(item *container.TabItem) {
|
||||
if item.Text == "Simple" {
|
||||
state.convert.Mode = "Simple"
|
||||
// Lock aspect ratio to Source in Simple mode
|
||||
state.convert.OutputAspect = "Source"
|
||||
targetAspectSelect.SetSelected("Source")
|
||||
updateAspectBoxVisibility()
|
||||
logging.Debug(logging.CatUI, "convert mode selected: Simple (aspect locked to Source)")
|
||||
logging.Debug(logging.CatUI, "convert mode selected: Simple")
|
||||
} else {
|
||||
state.convert.Mode = "Advanced"
|
||||
logging.Debug(logging.CatUI, "convert mode selected: Advanced")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Simple mode starts with Source aspect
|
||||
if state.convert.Mode == "Simple" {
|
||||
state.convert.OutputAspect = "Source"
|
||||
targetAspectSelect.SetSelected("Source")
|
||||
}
|
||||
|
||||
optionsRect := canvas.NewRectangle(utils.MustHex("#13182B"))
|
||||
optionsRect.CornerRadius = 8
|
||||
optionsRect.StrokeColor = gridColor
|
||||
|
|
@ -5307,6 +5330,7 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
|||
}
|
||||
src := s.source
|
||||
cfg := s.convert
|
||||
sourceBitrate := src.Bitrate
|
||||
isDVD := cfg.SelectedFormat.Ext == ".mpg"
|
||||
outDir := filepath.Dir(src.Path)
|
||||
outName := cfg.OutputFile()
|
||||
|
|
@ -6699,30 +6723,27 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
// File Info section
|
||||
comparisonText.WriteString("━━━ FILE INFO ━━━\n")
|
||||
|
||||
var file1SizeBytes int64
|
||||
file1Size := getField(state.compareFile1, func(src *videoSource) string {
|
||||
if fi, err := os.Stat(src.Path); err == nil {
|
||||
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
||||
if sizeMB >= 1024 {
|
||||
return fmt.Sprintf("%.2f GB", sizeMB/1024)
|
||||
}
|
||||
return fmt.Sprintf("%.2f MB", sizeMB)
|
||||
file1SizeBytes = fi.Size()
|
||||
return utils.FormatBytes(fi.Size())
|
||||
}
|
||||
return "Unknown"
|
||||
})
|
||||
file2Size := getField(state.compareFile2, func(src *videoSource) string {
|
||||
if fi, err := os.Stat(src.Path); err == nil {
|
||||
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
||||
if sizeMB >= 1024 {
|
||||
return fmt.Sprintf("%.2f GB", sizeMB/1024)
|
||||
if file1SizeBytes > 0 {
|
||||
return utils.DeltaBytes(fi.Size(), file1SizeBytes)
|
||||
}
|
||||
return fmt.Sprintf("%.2f MB", sizeMB)
|
||||
return utils.FormatBytes(fi.Size())
|
||||
}
|
||||
return "Unknown"
|
||||
})
|
||||
|
||||
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n", "File Size:", file1Size, file2Size))
|
||||
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
||||
"Format:",
|
||||
"Format Family:",
|
||||
getField(state.compareFile1, func(s *videoSource) string { return s.Format }),
|
||||
getField(state.compareFile2, func(s *videoSource) string { return s.Format })))
|
||||
|
||||
|
|
@ -6747,7 +6768,12 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
||||
"Bitrate:",
|
||||
getField(state.compareFile1, func(s *videoSource) string { return formatBitrate(s.Bitrate) }),
|
||||
getField(state.compareFile2, func(s *videoSource) string { return formatBitrate(s.Bitrate) })))
|
||||
getField(state.compareFile2, func(s *videoSource) string {
|
||||
if state.compareFile1 != nil {
|
||||
return utils.DeltaBitrate(s.Bitrate, state.compareFile1.Bitrate)
|
||||
}
|
||||
return formatBitrate(s.Bitrate)
|
||||
})))
|
||||
comparisonText.WriteString(fmt.Sprintf("%-25s | %-20s | %s\n",
|
||||
"Pixel Format:",
|
||||
getField(state.compareFile1, func(s *videoSource) string { return s.PixelFormat }),
|
||||
|
|
@ -6848,22 +6874,38 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
file2Info.Wrapping = fyne.TextWrapWord
|
||||
file2Info.TextStyle = fyne.TextStyle{} // non-selectable label
|
||||
|
||||
// Helper function to format metadata
|
||||
formatMetadata := func(src *videoSource) string {
|
||||
fileSize := "Unknown"
|
||||
// Helper function to format metadata (optionally comparing to a reference video)
|
||||
formatMetadata := func(src *videoSource, ref *videoSource) string {
|
||||
var (
|
||||
fileSize = "Unknown"
|
||||
refSize int64 = 0
|
||||
)
|
||||
if fi, err := os.Stat(src.Path); err == nil {
|
||||
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
||||
if sizeMB >= 1024 {
|
||||
fileSize = fmt.Sprintf("%.2f GB", sizeMB/1024)
|
||||
if ref != nil {
|
||||
if rfi, err := os.Stat(ref.Path); err == nil {
|
||||
refSize = rfi.Size()
|
||||
}
|
||||
}
|
||||
if refSize > 0 {
|
||||
fileSize = utils.DeltaBytes(fi.Size(), refSize)
|
||||
} else {
|
||||
fileSize = fmt.Sprintf("%.2f MB", sizeMB)
|
||||
fileSize = utils.FormatBytes(fi.Size())
|
||||
}
|
||||
}
|
||||
var bitrateStr string
|
||||
if src.Bitrate > 0 {
|
||||
bitrateStr = formatBitrate(src.Bitrate)
|
||||
} else {
|
||||
|
||||
var (
|
||||
bitrateStr = "--"
|
||||
refBitrate = 0
|
||||
)
|
||||
if ref != nil {
|
||||
refBitrate = ref.Bitrate
|
||||
}
|
||||
if src.Bitrate > 0 {
|
||||
if refBitrate > 0 {
|
||||
bitrateStr = utils.DeltaBitrate(src.Bitrate, refBitrate)
|
||||
} else {
|
||||
bitrateStr = formatBitrate(src.Bitrate)
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
|
|
@ -6944,7 +6986,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
filename := filepath.Base(state.compareFile1.Path)
|
||||
displayName := truncateFilename(filename, 35)
|
||||
file1Label.SetText(fmt.Sprintf("File 1: %s", displayName))
|
||||
file1Info.SetText(formatMetadata(state.compareFile1))
|
||||
file1Info.SetText(formatMetadata(state.compareFile1, state.compareFile2))
|
||||
// Build video player with compact size for side-by-side
|
||||
file1VideoContainer.Objects = []fyne.CanvasObject{
|
||||
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile1, nil),
|
||||
|
|
@ -6965,7 +7007,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
filename := filepath.Base(state.compareFile2.Path)
|
||||
displayName := truncateFilename(filename, 35)
|
||||
file2Label.SetText(fmt.Sprintf("File 2: %s", displayName))
|
||||
file2Info.SetText(formatMetadata(state.compareFile2))
|
||||
file2Info.SetText(formatMetadata(state.compareFile2, state.compareFile1))
|
||||
// Build video player with compact size for side-by-side
|
||||
file2VideoContainer.Objects = []fyne.CanvasObject{
|
||||
buildVideoPane(state, fyne.NewSize(320, 180), state.compareFile2, nil),
|
||||
|
|
@ -7030,7 +7072,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
if state.compareFile1 == nil {
|
||||
return
|
||||
}
|
||||
metadata := formatMetadata(state.compareFile1)
|
||||
metadata := formatMetadata(state.compareFile1, state.compareFile2)
|
||||
state.window.Clipboard().SetContent(metadata)
|
||||
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
|
||||
})
|
||||
|
|
@ -7047,7 +7089,7 @@ func buildCompareView(state *appState) fyne.CanvasObject {
|
|||
if state.compareFile2 == nil {
|
||||
return
|
||||
}
|
||||
metadata := formatMetadata(state.compareFile2)
|
||||
metadata := formatMetadata(state.compareFile2, state.compareFile1)
|
||||
state.window.Clipboard().SetContent(metadata)
|
||||
dialog.ShowInformation("Copied", "Metadata copied to clipboard", state.window)
|
||||
})
|
||||
|
|
@ -7159,19 +7201,14 @@ func buildInspectView(state *appState) fyne.CanvasObject {
|
|||
formatMetadata := func(src *videoSource) string {
|
||||
fileSize := "Unknown"
|
||||
if fi, err := os.Stat(src.Path); err == nil {
|
||||
sizeMB := float64(fi.Size()) / (1024 * 1024)
|
||||
if sizeMB >= 1024 {
|
||||
fileSize = fmt.Sprintf("%.2f GB", sizeMB/1024)
|
||||
} else {
|
||||
fileSize = fmt.Sprintf("%.2f MB", sizeMB)
|
||||
}
|
||||
fileSize = utils.FormatBytes(fi.Size())
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"━━━ FILE INFO ━━━\n"+
|
||||
"Path: %s\n"+
|
||||
"File Size: %s\n"+
|
||||
"Format: %s\n"+
|
||||
"Format Family: %s\n"+
|
||||
"\n━━━ VIDEO ━━━\n"+
|
||||
"Codec: %s\n"+
|
||||
"Resolution: %dx%d\n"+
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ set -e
|
|||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
BUILD_OUTPUT="$PROJECT_ROOT/VideoTools"
|
||||
# Extract app version from main.go
|
||||
APP_VERSION="$(grep -E 'appVersion\s*=\s*\"' "$PROJECT_ROOT/main.go" | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
|
||||
# Extract app version from main.go (avoid grep warnings on Git Bash)
|
||||
APP_VERSION="$(grep -m1 'appVersion' "$PROJECT_ROOT/main.go" | sed -E 's/.*\"([^\"]+)\".*/\1/')"
|
||||
[ -z "$APP_VERSION" ] && APP_VERSION="(version unknown)"
|
||||
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
|
|
@ -64,5 +64,10 @@ if go build -o "$BUILD_OUTPUT" .; then
|
|||
else
|
||||
echo "❌ Build failed! (VideoTools $APP_VERSION)"
|
||||
echo "Diagnostics: version=$APP_VERSION os=$(uname -s) arch=$(uname -m) go=$(go version | awk '{print $3}')"
|
||||
echo ""
|
||||
echo "Help: check the Go error messages above."
|
||||
echo " - Undefined symbol/identifier: usually a missing variable or typo in source; see the referenced file:line."
|
||||
echo " - \"C compiler not found\": install a C toolchain (e.g., build-essential on Ubuntu, Xcode CLT on macOS)."
|
||||
echo " - Cache permission denied: run 'rm -rf ~/.cache/go-build' or 'chown -R $USER ~/.cache/go-build'."
|
||||
exit 1
|
||||
fi
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ set -e
|
|||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Extract app version from main.go
|
||||
APP_VERSION="$(grep -E 'appVersion[[:space:]]*=[[:space:]]*\"' "$PROJECT_ROOT/main.go" | head -1 | sed -E 's/.*\"([^\"]+)\".*/\1/')"
|
||||
# Extract app version from main.go (avoid grep warnings on Git Bash)
|
||||
APP_VERSION="$(grep -m1 'appVersion' "$PROJECT_ROOT/main.go" | sed -E 's/.*\"([^\"]+)\".*/\1/')"
|
||||
[ -z "$APP_VERSION" ] && APP_VERSION="(version unknown)"
|
||||
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
|
|
@ -97,6 +97,11 @@ case "$OS" in
|
|||
else
|
||||
echo "❌ Build failed! (VideoTools $APP_VERSION)"
|
||||
diagnostics
|
||||
echo ""
|
||||
echo "Help: check the Go error messages above."
|
||||
echo " - Undefined symbol/identifier: usually a missing variable or typo in source; see the referenced file:line."
|
||||
echo " - \"C compiler not found\": install MinGW-w64 or MSYS2 toolchain so gcc is in PATH."
|
||||
echo " - Cache permission denied: delete or chown the Go build cache (e.g., %LOCALAPPDATA%\\go-build on Windows)."
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
|
|
|||
39
scripts/clear-go-cache.sh
Normal file
39
scripts/clear-go-cache.sh
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
# Convenience script to clear Go build/module caches.
|
||||
# Safe to run on Linux/macOS and Windows Git Bash.
|
||||
|
||||
set -e
|
||||
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo " VideoTools Go Cache Cleaner"
|
||||
echo "════════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
|
||||
if ! command -v go >/dev/null 2>&1; then
|
||||
echo "⚠️ Go is not installed or not in PATH; skipping go clean."
|
||||
else
|
||||
echo "🧹 Running: go clean -cache -modcache -testcache"
|
||||
go clean -cache -modcache -testcache || true
|
||||
echo "✓ Go clean complete"
|
||||
fi
|
||||
|
||||
OS="$(uname -s)"
|
||||
case "$OS" in
|
||||
CYGWIN*|MINGW*|MSYS*)
|
||||
# Windows paths under Git Bash
|
||||
CACHE_DIR="${LOCALAPPDATA:-$APPDATA}/go-build"
|
||||
;;
|
||||
*)
|
||||
CACHE_DIR="${GOCACHE:-$HOME/.cache/go-build}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$CACHE_DIR" ] && [ -d "$CACHE_DIR" ]; then
|
||||
echo "🗑️ Removing build cache dir: $CACHE_DIR"
|
||||
rm -rf "$CACHE_DIR" || sudo rm -rf "$CACHE_DIR" || true
|
||||
else
|
||||
echo "ℹ️ No cache directory found at $CACHE_DIR (nothing to remove)."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✅ Done. Re-run ./scripts/build.sh to rebuild VideoTools."
|
||||
Loading…
Reference in New Issue
Block a user