Compare commits

...

5 Commits

Author SHA1 Message Date
86d2f2b835 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>
2025-12-18 18:27:24 -05:00
12b2b221b9 Add 'In Progress' tab to history sidebar
Features:
- New "In Progress" tab shows running/pending jobs
- Displays active jobs without opening full queue
- Tab positioned first for quick visibility
- Shows "Running..." or "Pending" status
- No delete button on active jobs (only completed/failed)

Implementation:
- Updated BuildHistorySidebar to accept activeJobs parameter
- Converts queue.Job to ui.HistoryEntry for display
- Filters running/pending jobs from queue
- Conditional delete button (nil check)
- Dynamic status text based on job state

UX Improvements:
- Quick glance at current activity without queue view
- Three-tab layout: In Progress → Completed → Failed
- Consistent styling with existing history entries
- Tappable entries to view full job details

This allows users to monitor active conversions directly
from the history sidebar, reducing the need to constantly
check the full job queue view.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 18:02:03 -05:00
925334d8df Clean up root folder and update TODO/DONE for dev19
Root Folder Cleanup:
- Moved all documentation .md files to docs/ folder
- Kept only README.md, TODO.md, DONE.md in root
- Cleaner project structure for contributors
- Better organization of documentation

Files Moved to docs/:
- BUILD.md, BUILD_AND_RUN.md, CHANGELOG.md
- COMPLETION_SUMMARY.md, DVD_IMPLEMENTATION_SUMMARY.md
- DVD_USER_GUIDE.md, INSTALLATION.md, INTEGRATION_GUIDE.md
- LATEST_UPDATES.md, QUEUE_SYSTEM_GUIDE.md, QUICKSTART.md
- TESTING_DEV13.md, TEST_DVD_CONVERSION.md, WINDOWS_SETUP.md

DONE.md Updates:
- Added dev19 section (2025-12-18)
- Documented history sidebar delete button
- Documented command preview improvements
- Documented format options reorganization
- Documented bitrate mode descriptive labels
- Documented critical bug fixes (Convert crash, log viewer)
- Documented bitrate control improvements

TODO.md Updates:
- Updated to dev19+ plan
- Added "Current Focus: dev19" section
- Added AI frame interpolation task (RIFE, FILM, DAIN, CAIN)
- Added color space preservation tasks
- Reorganized priority structure

This establishes dev19 as the current development focus on
Convert module cleanup and polish, with clear tracking of
completed work and upcoming priorities.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 16:21:15 -05:00
f7bb87e20a Add descriptive labels to bitrate mode dropdown
Bitrate Mode Options Now Show:
- CRF (Constant Rate Factor)
- CBR (Constant Bitrate)
- VBR (Variable Bitrate)
- Target Size (Calculate from file size)

Implementation:
- Added bidirectional mapping between short codes and full labels
- Internally still uses short codes (CRF, CBR, VBR, Target Size)
- Preserves compatibility with existing config files
- Maps display label to internal code on selection
- Maps internal code to display label when loading

Makes it immediately clear what each bitrate mode does without
needing to reference documentation or tooltips.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 16:13:10 -05:00
83c8e68f80 Improve command preview button and reorganize format options
Command Preview Button:
- Disabled when no video source is loaded
- Shows "Show Preview" when preview is hidden
- Shows "Hide Preview" when preview is visible
- Makes it clear when and why the button can be used

Format Options Reorganization:
- Grouped formats by codec family for better readability
- Order: H.264 → H.265 → AV1 → VP9 → ProRes → MPEG-2
- Added comments explaining each codec family
- Makes it easier to find and compare similar codecs

This improves discoverability and reduces user confusion about
when the command preview is available and which format to choose.

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-18 16:09:55 -05:00
19 changed files with 295 additions and 54 deletions

66
DONE.md
View File

@ -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
View File

@ -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**

View File

@ -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)

View File

@ -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
View File

@ -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 {