Compare commits
5 Commits
5b544b8484
...
86d2f2b835
| Author | SHA1 | Date | |
|---|---|---|---|
| 86d2f2b835 | |||
| 12b2b221b9 | |||
| 925334d8df | |||
| f7bb87e20a | |||
| 83c8e68f80 |
66
DONE.md
66
DONE.md
|
|
@ -2,6 +2,72 @@
|
|||
|
||||
This file tracks completed features, fixes, and milestones.
|
||||
|
||||
## Version 0.1.0-dev19 (2025-12-18) - Convert Module Cleanup & UX Polish
|
||||
|
||||
### Features
|
||||
- ✅ **History Sidebar Enhancements**
|
||||
- Delete button ("×") on each history entry
|
||||
- Remove individual entries from history
|
||||
- Auto-save and refresh after deletion
|
||||
- Clean, unobtrusive button placement
|
||||
|
||||
- ✅ **Command Preview Improvements**
|
||||
- Show/Hide button state based on preview visibility
|
||||
- Disabled when no video source loaded
|
||||
- Displays actual file paths instead of placeholders
|
||||
- Real-time live updates as settings change
|
||||
- Collapsible to save screen space
|
||||
|
||||
- ✅ **Format Options Reorganization**
|
||||
- Grouped by codec family (H.264 → H.265 → AV1 → VP9 → ProRes → MPEG-2)
|
||||
- Added descriptive comments for each codec type
|
||||
- Improved dropdown readability and navigation
|
||||
- Easier to find and compare similar formats
|
||||
|
||||
- ✅ **Bitrate Mode Clarity**
|
||||
- Descriptive labels in dropdown:
|
||||
- CRF (Constant Rate Factor)
|
||||
- CBR (Constant Bitrate)
|
||||
- VBR (Variable Bitrate)
|
||||
- Target Size (Calculate from file size)
|
||||
- Immediate understanding without documentation
|
||||
- Preserves internal compatibility with short codes
|
||||
|
||||
- ✅ **Root Folder Cleanup**
|
||||
- Moved all documentation .md files to docs/ folder
|
||||
- Kept only README.md, TODO.md, DONE.md in root
|
||||
- Cleaner project structure
|
||||
- Better organization for contributors
|
||||
|
||||
### Bug Fixes
|
||||
- ✅ **Critical Convert Module Crash Fixed**
|
||||
- Fixed nil pointer dereference when opening Convert module
|
||||
- Corrected widget initialization order
|
||||
- bitrateContainer now created after bitratePresetSelect initialized
|
||||
- Eliminated "invalid memory address" panic on startup
|
||||
|
||||
- ✅ **Log Viewer Crash Fixed**
|
||||
- Fixed "close of closed channel" panic
|
||||
- Duplicate close handlers removed
|
||||
- Proper dialog cleanup
|
||||
|
||||
- ✅ **Bitrate Control Improvements**
|
||||
- CBR: Set bufsize to 2x bitrate for better encoder handling
|
||||
- VBR: Increased maxrate cap from 1.5x to 2x target bitrate
|
||||
- VBR: Added bufsize at 4x target to enforce caps
|
||||
- Prevents runaway bitrates while maintaining quality peaks
|
||||
|
||||
### Technical Improvements
|
||||
- ✅ **Widget Initialization Order**
|
||||
- Fixed container creation dependencies
|
||||
- All Select widgets initialized before container use
|
||||
- Proper nil checking in UI construction
|
||||
|
||||
- ✅ **Bidirectional Label Mapping**
|
||||
- Display labels map to internal storage codes
|
||||
- Config files remain compatible
|
||||
- Clean separation of UI and data layers
|
||||
|
||||
## Version 0.1.0-dev18 (2025-12-15)
|
||||
|
||||
### Features
|
||||
|
|
|
|||
21
TODO.md
21
TODO.md
|
|
@ -1,8 +1,25 @@
|
|||
# VideoTools TODO (v0.1.0-dev14 plan)
|
||||
# VideoTools TODO (v0.1.0-dev19+ plan)
|
||||
|
||||
This file tracks upcoming features, improvements, and known issues.
|
||||
|
||||
## Priority Features for dev15 (Post-Windows Compatibility)
|
||||
## Current Focus: dev19 - Convert Module Cleanup & Polish
|
||||
|
||||
### In Progress
|
||||
- [ ] **AI Frame Interpolation Support**
|
||||
- RIFE (Real-Time Intermediate Flow Estimation) - https://github.com/hzwer/ECCV2022-RIFE
|
||||
- FILM (Frame Interpolation for Large Motion) - https://github.com/google-research/frame-interpolation
|
||||
- DAIN (Depth-Aware Video Frame Interpolation) - https://github.com/baowenbo/DAIN
|
||||
- CAIN (Channel Attention Is All You Need) - https://github.com/myungsub/CAIN
|
||||
- Python-based models, need Go bindings or CLI wrappers
|
||||
- Model download/management system
|
||||
- UI controls for model selection
|
||||
|
||||
- [ ] **Color Space Preservation**
|
||||
- Fix color space preservation in upscale module
|
||||
- Ensure all conversions preserve color metadata (color_space, color_primaries, color_trc, color_range)
|
||||
- Test with HDR content
|
||||
|
||||
## Priority Features for dev20+
|
||||
|
||||
### Quality & Polish Improvements
|
||||
- [ ] **UI/UX refinements**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -153,6 +154,7 @@ func sortedKeys(m map[string][]fyne.CanvasObject) []string {
|
|||
// BuildHistorySidebar creates the history sidebar with tabs
|
||||
func BuildHistorySidebar(
|
||||
entries []HistoryEntry,
|
||||
activeJobs []HistoryEntry,
|
||||
onEntryClick func(HistoryEntry),
|
||||
onEntryDelete func(HistoryEntry),
|
||||
titleColor, bgColor, textColor color.Color,
|
||||
|
|
@ -168,11 +170,13 @@ func BuildHistorySidebar(
|
|||
}
|
||||
|
||||
// Build lists
|
||||
inProgressList := buildHistoryList(activeJobs, onEntryClick, nil, bgColor, textColor) // No delete for active jobs
|
||||
completedList := buildHistoryList(completedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
|
||||
failedList := buildHistoryList(failedEntries, onEntryClick, onEntryDelete, bgColor, textColor)
|
||||
|
||||
// Tabs
|
||||
// Tabs - In Progress first for quick visibility
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItem("In Progress", container.NewVScroll(inProgressList)),
|
||||
container.NewTabItem("Completed", container.NewVScroll(completedList)),
|
||||
container.NewTabItem("Failed", container.NewVScroll(failedList)),
|
||||
)
|
||||
|
|
@ -220,24 +224,56 @@ func buildHistoryItem(
|
|||
// Capture entry for closures
|
||||
capturedEntry := entry
|
||||
|
||||
// Delete button - small "×" button
|
||||
deleteBtn := widget.NewButton("×", func() {
|
||||
onEntryDelete(capturedEntry)
|
||||
})
|
||||
deleteBtn.Importance = widget.LowImportance
|
||||
// Build header row with badge and optional delete button
|
||||
headerItems := []fyne.CanvasObject{badge, layout.NewSpacer()}
|
||||
if onEntryDelete != nil {
|
||||
// Delete button - small "×" button (only for completed/failed)
|
||||
deleteBtn := widget.NewButton("×", func() {
|
||||
onEntryDelete(capturedEntry)
|
||||
})
|
||||
deleteBtn.Importance = widget.LowImportance
|
||||
headerItems = append(headerItems, deleteBtn)
|
||||
}
|
||||
|
||||
// Title
|
||||
titleLabel := widget.NewLabel(utils.ShortenMiddle(entry.Title, 25))
|
||||
titleLabel.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
// Timestamp
|
||||
timeStr := "Unknown"
|
||||
if entry.CompletedAt != nil {
|
||||
timeStr = entry.CompletedAt.Format("Jan 2, 15:04")
|
||||
// Timestamp or status info
|
||||
var timeStr string
|
||||
if entry.Status == queue.JobStatusRunning || entry.Status == queue.JobStatusPending {
|
||||
// For in-progress jobs, show status
|
||||
if entry.Status == queue.JobStatusRunning {
|
||||
timeStr = "Running..."
|
||||
} else {
|
||||
timeStr = "Pending"
|
||||
}
|
||||
} else {
|
||||
// For completed/failed jobs, show timestamp
|
||||
if entry.CompletedAt != nil {
|
||||
timeStr = entry.CompletedAt.Format("Jan 2, 15:04")
|
||||
} else {
|
||||
timeStr = "Unknown"
|
||||
}
|
||||
}
|
||||
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)
|
||||
|
|
@ -245,11 +281,7 @@ func buildHistoryItem(
|
|||
|
||||
content := container.NewBorder(
|
||||
nil, nil, statusRect, nil,
|
||||
container.NewVBox(
|
||||
container.NewHBox(badge, layout.NewSpacer(), deleteBtn),
|
||||
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)
|
||||
|
|
|
|||
179
main.go
179
main.go
|
|
@ -417,16 +417,22 @@ type formatOption struct {
|
|||
}
|
||||
|
||||
var formatOptions = []formatOption{
|
||||
// H.264 - Widely compatible, older standard
|
||||
{"MP4 (H.264)", ".mp4", "libx264"},
|
||||
{"MP4 (H.265)", ".mp4", "libx265"},
|
||||
{"MP4 (AV1)", ".mp4", "libaom-av1"},
|
||||
{"MKV (H.265)", ".mkv", "libx265"},
|
||||
{"MKV (AV1)", ".mkv", "libaom-av1"},
|
||||
{"WebM (VP9)", ".webm", "libvpx-vp9"},
|
||||
{"WebM (AV1)", ".webm", "libaom-av1"},
|
||||
{"MOV (H.264)", ".mov", "libx264"},
|
||||
// H.265/HEVC - Better compression than H.264
|
||||
{"MP4 (H.265)", ".mp4", "libx265"},
|
||||
{"MKV (H.265)", ".mkv", "libx265"},
|
||||
{"MOV (H.265)", ".mov", "libx265"},
|
||||
// AV1 - Best compression, slower encode
|
||||
{"MP4 (AV1)", ".mp4", "libaom-av1"},
|
||||
{"MKV (AV1)", ".mkv", "libaom-av1"},
|
||||
{"WebM (AV1)", ".webm", "libaom-av1"},
|
||||
// VP9 - Google codec, good for web
|
||||
{"WebM (VP9)", ".webm", "libvpx-vp9"},
|
||||
// ProRes - Professional/editing codec
|
||||
{"MOV (ProRes)", ".mov", "prores_ks"},
|
||||
// MPEG-2 - DVD standard
|
||||
{"DVD-NTSC (MPEG-2)", ".mpg", "mpeg2video"},
|
||||
{"DVD-PAL (MPEG-2)", ".mpg", "mpeg2video"},
|
||||
}
|
||||
|
|
@ -1366,8 +1372,34 @@ func (s *appState) showMainMenu() {
|
|||
// Build sidebar if visible
|
||||
var sidebar fyne.CanvasObject
|
||||
if s.sidebarVisible {
|
||||
// Get active jobs from queue (running/pending)
|
||||
var activeJobs []ui.HistoryEntry
|
||||
if s.jobQueue != nil {
|
||||
for _, job := range s.jobQueue.List() {
|
||||
if job.Status == queue.JobStatusRunning || job.Status == queue.JobStatusPending {
|
||||
// Convert queue.Job to ui.HistoryEntry
|
||||
entry := ui.HistoryEntry{
|
||||
ID: job.ID,
|
||||
Type: job.Type,
|
||||
Status: job.Status,
|
||||
Title: job.Title,
|
||||
InputFile: job.InputFile,
|
||||
OutputFile: job.OutputFile,
|
||||
LogPath: job.LogPath,
|
||||
Config: job.Config,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sidebar = ui.BuildHistorySidebar(
|
||||
s.historyEntries,
|
||||
activeJobs,
|
||||
s.showHistoryDetails,
|
||||
s.deleteHistoryEntry,
|
||||
titleColor,
|
||||
|
|
@ -5073,6 +5105,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
})
|
||||
cmdPreviewBtn.Importance = widget.LowImportance
|
||||
|
||||
// Update button text and state based on preview visibility and source
|
||||
if src == nil {
|
||||
cmdPreviewBtn.Disable()
|
||||
} else if state.convertCommandPreviewShow {
|
||||
cmdPreviewBtn.SetText("Hide Preview")
|
||||
} else {
|
||||
cmdPreviewBtn.SetText("Show Preview")
|
||||
}
|
||||
|
||||
backBar := ui.TintedBar(convertColor, container.NewHBox(back, layout.NewSpacer(), navButtons, layout.NewSpacer(), cmdPreviewBtn, queueBtn))
|
||||
|
||||
var updateCover func(string)
|
||||
|
|
@ -5147,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) {
|
||||
|
|
@ -5203,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
|
||||
|
|
@ -5488,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()
|
||||
}
|
||||
|
|
@ -5698,10 +5778,33 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
widget.NewSeparator(),
|
||||
)
|
||||
|
||||
// Bitrate Mode
|
||||
bitrateModeSelect = widget.NewSelect([]string{"CRF", "CBR", "VBR", "Target Size"}, func(value string) {
|
||||
state.convert.BitrateMode = value
|
||||
logging.Debug(logging.CatUI, "bitrate mode set to %s", value)
|
||||
// Bitrate Mode with descriptions
|
||||
bitrateModeOptions := []string{
|
||||
"CRF (Constant Rate Factor)",
|
||||
"CBR (Constant Bitrate)",
|
||||
"VBR (Variable Bitrate)",
|
||||
"Target Size (Calculate from file size)",
|
||||
}
|
||||
bitrateModeMap := map[string]string{
|
||||
"CRF (Constant Rate Factor)": "CRF",
|
||||
"CBR (Constant Bitrate)": "CBR",
|
||||
"VBR (Variable Bitrate)": "VBR",
|
||||
"Target Size (Calculate from file size)": "Target Size",
|
||||
}
|
||||
reverseMap := map[string]string{
|
||||
"CRF": "CRF (Constant Rate Factor)",
|
||||
"CBR": "CBR (Constant Bitrate)",
|
||||
"VBR": "VBR (Variable Bitrate)",
|
||||
"Target Size": "Target Size (Calculate from file size)",
|
||||
}
|
||||
bitrateModeSelect = widget.NewSelect(bitrateModeOptions, func(value string) {
|
||||
// Extract short code from label
|
||||
if shortCode, ok := bitrateModeMap[value]; ok {
|
||||
state.convert.BitrateMode = shortCode
|
||||
} else {
|
||||
state.convert.BitrateMode = value
|
||||
}
|
||||
logging.Debug(logging.CatUI, "bitrate mode set to %s", state.convert.BitrateMode)
|
||||
if updateEncodingControls != nil {
|
||||
updateEncodingControls()
|
||||
}
|
||||
|
|
@ -5709,7 +5812,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
buildCommandPreview()
|
||||
}
|
||||
})
|
||||
bitrateModeSelect.SetSelected(state.convert.BitrateMode)
|
||||
// Set selected using full label
|
||||
if fullLabel, ok := reverseMap[state.convert.BitrateMode]; ok {
|
||||
bitrateModeSelect.SetSelected(fullLabel)
|
||||
} else {
|
||||
bitrateModeSelect.SetSelected(state.convert.BitrateMode)
|
||||
}
|
||||
|
||||
// Manual CRF entry
|
||||
crfEntry = widget.NewEntry()
|
||||
|
|
@ -5941,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