Add progress bars to In Progress tab and fix lossless quality compatibility
In Progress Tab Enhancements: - Added animated striped progress bars to in-progress jobs - Exported ModuleColor function for reuse across modules - Shows real-time progress (0-100%) with module-specific colors - Progress updates automatically as jobs run - Maintains consistent visual style with queue view Lossless Quality Preset Improvements: - H.265 and AV1 now support all bitrate modes with lossless quality - Lossless with Target Size mode now works for H.265/AV1 - H.264 and MPEG-2 no longer show "Lossless" option (codec limitation) - Dynamic quality dropdown updates based on selected codec - Automatic fallback to "Near-Lossless" when switching from lossless-capable codec to non-lossless codec Quality Options Logic: - Base options: Draft, Standard, Balanced, High, Near-Lossless - "Lossless" only appears for H.265 and AV1 - codecSupportsLossless() helper function checks compatibility - updateQualityOptions() refreshes dropdown when codec changes Lossless + Bitrate Mode Combinations: - Lossless + CRF: Forces CRF 0 for perfect quality - Lossless + CBR: Constant bitrate with lossless quality - Lossless + VBR: Variable bitrate with lossless quality - Lossless + Target Size: Calculates bitrate for exact file size with best possible quality (now allowed for H.265/AV1) Technical Implementation: - Added Progress field to ui.HistoryEntry struct - Exported StripedProgress widget and ModuleColor function - updateQualityOptions() function dynamically filters quality presets - updateEncodingControls() handles lossless modes per codec - Descriptive hints explain each lossless+bitrate combination This allows professional workflows where lossless quality is desired but file size constraints still need to be met using Target Size mode. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
12b2b221b9
commit
86d2f2b835
|
|
@ -40,6 +40,7 @@ type HistoryEntry struct {
|
|||
CompletedAt *time.Time
|
||||
Error string
|
||||
FFmpegCmd string
|
||||
Progress float64 // 0.0 to 1.0 for in-progress jobs
|
||||
}
|
||||
|
||||
// BuildMainMenu creates the main menu view with module tiles grouped by category
|
||||
|
|
@ -258,6 +259,21 @@ func buildHistoryItem(
|
|||
timeLabel := widget.NewLabel(timeStr)
|
||||
timeLabel.TextStyle = fyne.TextStyle{Monospace: true}
|
||||
|
||||
// Progress bar for in-progress jobs
|
||||
contentItems := []fyne.CanvasObject{
|
||||
container.NewHBox(headerItems...),
|
||||
titleLabel,
|
||||
timeLabel,
|
||||
}
|
||||
|
||||
if entry.Status == queue.JobStatusRunning || entry.Status == queue.JobStatusPending {
|
||||
// Add progress bar for active jobs
|
||||
moduleCol := ModuleColor(entry.Type)
|
||||
progressBar := NewStripedProgress(moduleCol)
|
||||
progressBar.SetProgress(entry.Progress)
|
||||
contentItems = append(contentItems, progressBar)
|
||||
}
|
||||
|
||||
// Status color bar
|
||||
statusColor := GetStatusColor(entry.Status)
|
||||
statusRect := canvas.NewRectangle(statusColor)
|
||||
|
|
@ -265,11 +281,7 @@ func buildHistoryItem(
|
|||
|
||||
content := container.NewBorder(
|
||||
nil, nil, statusRect, nil,
|
||||
container.NewVBox(
|
||||
container.NewHBox(headerItems...),
|
||||
titleLabel,
|
||||
timeLabel,
|
||||
),
|
||||
container.NewVBox(contentItems...),
|
||||
)
|
||||
|
||||
card := canvas.NewRectangle(bgColor)
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ import (
|
|||
"git.leaktechnologies.dev/stu/VideoTools/internal/utils"
|
||||
)
|
||||
|
||||
// stripedProgress renders a progress bar with a tinted stripe pattern.
|
||||
type stripedProgress struct {
|
||||
// StripedProgress renders a progress bar with a tinted stripe pattern.
|
||||
type StripedProgress struct {
|
||||
widget.BaseWidget
|
||||
progress float64
|
||||
color color.Color
|
||||
|
|
@ -25,8 +25,9 @@ type stripedProgress struct {
|
|||
offset float64
|
||||
}
|
||||
|
||||
func newStripedProgress(col color.Color) *stripedProgress {
|
||||
sp := &stripedProgress{
|
||||
// NewStripedProgress creates a new striped progress bar with the given color
|
||||
func NewStripedProgress(col color.Color) *StripedProgress {
|
||||
sp := &StripedProgress{
|
||||
progress: 0,
|
||||
color: col,
|
||||
bg: color.RGBA{R: 34, G: 38, B: 48, A: 255}, // dark neutral
|
||||
|
|
@ -35,7 +36,8 @@ func newStripedProgress(col color.Color) *stripedProgress {
|
|||
return sp
|
||||
}
|
||||
|
||||
func (s *stripedProgress) SetProgress(p float64) {
|
||||
// SetProgress updates the progress value (0.0 to 1.0)
|
||||
func (s *StripedProgress) SetProgress(p float64) {
|
||||
if p < 0 {
|
||||
p = 0
|
||||
}
|
||||
|
|
@ -46,7 +48,7 @@ func (s *stripedProgress) SetProgress(p float64) {
|
|||
s.Refresh()
|
||||
}
|
||||
|
||||
func (s *stripedProgress) CreateRenderer() fyne.WidgetRenderer {
|
||||
func (s *StripedProgress) CreateRenderer() fyne.WidgetRenderer {
|
||||
bgRect := canvas.NewRectangle(s.bg)
|
||||
fillRect := canvas.NewRectangle(applyAlpha(s.color, 200))
|
||||
stripes := canvas.NewRaster(func(w, h int) image.Image {
|
||||
|
|
@ -79,7 +81,7 @@ func (s *stripedProgress) CreateRenderer() fyne.WidgetRenderer {
|
|||
}
|
||||
|
||||
type stripedProgressRenderer struct {
|
||||
bar *stripedProgress
|
||||
bar *StripedProgress
|
||||
bg *canvas.Rectangle
|
||||
fill *canvas.Rectangle
|
||||
stripes *canvas.Raster
|
||||
|
|
@ -234,7 +236,7 @@ func buildJobItem(
|
|||
descLabel.Wrapping = fyne.TextWrapWord
|
||||
|
||||
// Progress bar (for running jobs)
|
||||
progress := newStripedProgress(moduleColor(job.Type))
|
||||
progress := NewStripedProgress(ModuleColor(job.Type))
|
||||
progress.SetProgress(job.Progress / 100.0)
|
||||
if job.Status == queue.JobStatusCompleted {
|
||||
progress.SetProgress(1.0)
|
||||
|
|
@ -379,7 +381,8 @@ func getStatusText(job *queue.Job) string {
|
|||
}
|
||||
|
||||
// moduleColor maps job types to distinct colors matching the main module colors
|
||||
func moduleColor(t queue.JobType) color.Color {
|
||||
// ModuleColor returns the color for a given job type
|
||||
func ModuleColor(t queue.JobType) color.Color {
|
||||
switch t {
|
||||
case queue.JobTypeConvert:
|
||||
return color.RGBA{R: 139, G: 68, B: 255, A: 255} // Violet (#8B44FF)
|
||||
|
|
|
|||
89
main.go
89
main.go
|
|
@ -1390,6 +1390,7 @@ func (s *appState) showMainMenu() {
|
|||
CreatedAt: job.CreatedAt,
|
||||
StartedAt: job.StartedAt,
|
||||
Error: job.Error,
|
||||
Progress: job.Progress / 100.0, // Convert 0-100 to 0.0-1.0
|
||||
}
|
||||
activeJobs = append(activeJobs, entry)
|
||||
}
|
||||
|
|
@ -5187,16 +5188,29 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
updateEncodingControls func()
|
||||
updateQualityVisibility func()
|
||||
buildCommandPreview func()
|
||||
updateQualityOptions func() // Update quality dropdown based on codec
|
||||
)
|
||||
|
||||
qualityOptions := []string{
|
||||
// Base quality options (without lossless)
|
||||
baseQualityOptions := []string{
|
||||
"Draft (CRF 28)",
|
||||
"Standard (CRF 23)",
|
||||
"Balanced (CRF 20)",
|
||||
"High (CRF 18)",
|
||||
"Near-Lossless (CRF 16)",
|
||||
"Lossless",
|
||||
}
|
||||
|
||||
// Helper function to check if codec supports lossless
|
||||
codecSupportsLossless := func(codec string) bool {
|
||||
return codec == "H.265" || codec == "AV1"
|
||||
}
|
||||
|
||||
// Current quality options (dynamic based on codec)
|
||||
qualityOptions := baseQualityOptions
|
||||
if codecSupportsLossless(state.convert.VideoCodec) {
|
||||
qualityOptions = append(qualityOptions, "Lossless")
|
||||
}
|
||||
|
||||
var syncingQuality bool
|
||||
|
||||
qualitySelectSimple = widget.NewSelect(qualityOptions, func(value string) {
|
||||
|
|
@ -5243,6 +5257,29 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
qualitySelectSimple.SetSelected(state.convert.Quality)
|
||||
qualitySelectAdv.SetSelected(state.convert.Quality)
|
||||
|
||||
// Update quality options based on codec
|
||||
updateQualityOptions = func() {
|
||||
var newOptions []string
|
||||
if codecSupportsLossless(state.convert.VideoCodec) {
|
||||
// H.265 and AV1 support lossless
|
||||
newOptions = append(baseQualityOptions, "Lossless")
|
||||
} else {
|
||||
// H.264, MPEG-2, etc. don't support lossless
|
||||
newOptions = baseQualityOptions
|
||||
// If currently set to Lossless, fall back to Near-Lossless
|
||||
if state.convert.Quality == "Lossless" {
|
||||
state.convert.Quality = "Near-Lossless (CRF 16)"
|
||||
}
|
||||
}
|
||||
|
||||
qualitySelectSimple.Options = newOptions
|
||||
qualitySelectAdv.Options = newOptions
|
||||
qualitySelectSimple.SetSelected(state.convert.Quality)
|
||||
qualitySelectAdv.SetSelected(state.convert.Quality)
|
||||
qualitySelectSimple.Refresh()
|
||||
qualitySelectAdv.Refresh()
|
||||
}
|
||||
|
||||
outputEntry := widget.NewEntry()
|
||||
outputEntry.SetText(state.convert.OutputBase)
|
||||
var updatingOutput bool
|
||||
|
|
@ -5528,6 +5565,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
videoCodecSelect := widget.NewSelect([]string{"H.264", "H.265", "VP9", "AV1", "MPEG-2", "Copy"}, func(value string) {
|
||||
state.convert.VideoCodec = value
|
||||
logging.Debug(logging.CatUI, "video codec set to %s", value)
|
||||
if updateQualityOptions != nil {
|
||||
updateQualityOptions()
|
||||
}
|
||||
if updateQualityVisibility != nil {
|
||||
updateQualityVisibility()
|
||||
}
|
||||
|
|
@ -6009,25 +6049,40 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
updateEncodingControls = func() {
|
||||
mode := state.convert.BitrateMode
|
||||
isLossless := state.convert.Quality == "Lossless"
|
||||
supportsLossless := codecSupportsLossless(state.convert.VideoCodec)
|
||||
|
||||
hint := ""
|
||||
|
||||
if isLossless {
|
||||
// Lossless forces CRF 0; hide bitrate/target size
|
||||
if mode != "CRF" {
|
||||
state.convert.BitrateMode = "CRF"
|
||||
bitrateModeSelect.SetSelected("CRF")
|
||||
mode = "CRF"
|
||||
if isLossless && supportsLossless {
|
||||
// Lossless with H.265/AV1: Allow all bitrate modes
|
||||
// The lossless quality affects the encoding, but bitrate/target size still control output
|
||||
switch mode {
|
||||
case "CRF", "":
|
||||
if crfEntry.Text != "0" {
|
||||
crfEntry.SetText("0")
|
||||
}
|
||||
state.convert.CRF = "0"
|
||||
crfEntry.Disable()
|
||||
crfContainer.Show()
|
||||
bitrateContainer.Hide()
|
||||
targetSizeContainer.Hide()
|
||||
hint = "Lossless mode with CRF 0. Perfect quality preservation for H.265/AV1."
|
||||
case "CBR":
|
||||
crfContainer.Hide()
|
||||
bitrateContainer.Show()
|
||||
targetSizeContainer.Hide()
|
||||
hint = "Lossless quality with constant bitrate. May achieve smaller file size than pure lossless CRF."
|
||||
case "VBR":
|
||||
crfContainer.Hide()
|
||||
bitrateContainer.Show()
|
||||
targetSizeContainer.Hide()
|
||||
hint = "Lossless quality with variable bitrate. Efficient file size while maintaining lossless quality."
|
||||
case "Target Size":
|
||||
crfContainer.Hide()
|
||||
bitrateContainer.Hide()
|
||||
targetSizeContainer.Show()
|
||||
hint = "Lossless quality with target size. Calculates bitrate to achieve exact file size with best possible quality."
|
||||
}
|
||||
if crfEntry.Text != "0" {
|
||||
crfEntry.SetText("0")
|
||||
}
|
||||
state.convert.CRF = "0"
|
||||
crfEntry.Disable()
|
||||
crfContainer.Show()
|
||||
bitrateContainer.Hide()
|
||||
targetSizeContainer.Hide()
|
||||
hint = "Lossless forces CRF 0 for H.265/AV1; bitrate and target size are ignored."
|
||||
} else {
|
||||
crfEntry.Enable()
|
||||
switch mode {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user