feat: Add stylistic filter state variables to appState

- Add filterStylisticMode for era selection (70s, 80s, 90s, VHS, Webcam)
- Add filterScanlines for CRT scanline effects
- Add filterChromaNoise for analog chroma noise (0.0-1.0)
- Add filterColorBleeding for VHS color bleeding
- Add filterTapeNoise for magnetic tape noise (0.0-1.0)
- Add filterTrackingError for VHS tracking errors (0.0-1.0)
- Add filterDropout for tape dropout effects (0.0-1.0)
- Add filterInterlacing for interlaced/progressive video handling

This provides the foundation for authentic decade-based video effects
in the Filters module, supporting film restoration and period-accurate
video processing workflows.
This commit is contained in:
Stu Leak 2026-01-01 20:39:35 -05:00
parent e93353fea3
commit c063b3f8f5

182
main.go
View File

@ -87,7 +87,7 @@ var (
{"trim", "Trim", utils.MustHex("#F9A825"), "Convert", nil}, // Dark Yellow/Gold (not implemented yet) {"trim", "Trim", utils.MustHex("#F9A825"), "Convert", nil}, // Dark Yellow/Gold (not implemented yet)
{"filters", "Filters", utils.MustHex("#00BCD4"), "Convert", modules.HandleFilters}, // Cyan (creative filters) {"filters", "Filters", utils.MustHex("#00BCD4"), "Convert", modules.HandleFilters}, // Cyan (creative filters)
{"upscale", "Upscale", utils.MustHex("#9C27B0"), "Advanced", modules.HandleUpscale}, // Purple (AI/advanced) {"upscale", "Upscale", utils.MustHex("#9C27B0"), "Advanced", modules.HandleUpscale}, // Purple (AI/advanced)
{"audio", "Audio", utils.MustHex("#FF8F00"), "Convert", modules.HandleAudio}, // Dark Amber - audio extraction {"audio", "Audio", utils.MustHex("#FF8F00"), "Convert", modules.HandleAudio}, // Dark Amber - audio extraction
{"author", "Author", utils.MustHex("#FF5722"), "Disc", modules.HandleAuthor}, // Deep Orange (authoring) {"author", "Author", utils.MustHex("#FF5722"), "Disc", modules.HandleAuthor}, // Deep Orange (authoring)
{"rip", "Rip", utils.MustHex("#FF9800"), "Disc", modules.HandleRip}, // Orange (extraction) {"rip", "Rip", utils.MustHex("#FF9800"), "Disc", modules.HandleRip}, // Orange (extraction)
{"bluray", "Blu-Ray", utils.MustHex("#2196F3"), "Disc", nil}, // Blue (not implemented yet) {"bluray", "Blu-Ray", utils.MustHex("#2196F3"), "Disc", nil}, // Blue (not implemented yet)
@ -907,6 +907,16 @@ type appState struct {
filterInterpPreset string filterInterpPreset string
filterInterpFPS string filterInterpFPS string
// Stylistic effects state
filterStylisticMode string // "None", "70s", "80s", "90s", "VHS", "Webcam"
filterScanlines bool // CRT scanline effect
filterChromaNoise float64 // 0.0-1.0, analog chroma noise
filterColorBleeding bool // VHS color bleeding effect
filterTapeNoise float64 // 0.0-1.0, magnetic tape noise
filterTrackingError float64 // 0.0-1.0, VHS tracking errors
filterDropout float64 // 0.0-1.0, tape dropouts
filterInterlacing string // "None", "Progressive", "Interlaced"
// Upscale module state // Upscale module state
upscaleFile *videoSource upscaleFile *videoSource
upscaleMethod string // lanczos, bicubic, spline, bilinear upscaleMethod string // lanczos, bicubic, spline, bilinear
@ -3083,10 +3093,10 @@ func (s *appState) batchAddToQueue(paths []string) {
"outputPath": outPath, "outputPath": outPath,
"outputBase": outputBase, "outputBase": outputBase,
"selectedFormat": s.convert.SelectedFormat, "selectedFormat": s.convert.SelectedFormat,
"quality": s.convert.Quality, "quality": s.convert.Quality,
"mode": s.convert.Mode, "mode": s.convert.Mode,
"preserveChapters": s.convert.PreserveChapters, "preserveChapters": s.convert.PreserveChapters,
"videoCodec": s.convert.VideoCodec, "videoCodec": s.convert.VideoCodec,
"encoderPreset": s.convert.EncoderPreset, "encoderPreset": s.convert.EncoderPreset,
"crf": s.convert.CRF, "crf": s.convert.CRF,
"bitrateMode": s.convert.BitrateMode, "bitrateMode": s.convert.BitrateMode,
@ -3113,7 +3123,7 @@ func (s *appState) batchAddToQueue(paths []string) {
"sourceWidth": src.Width, "sourceWidth": src.Width,
"sourceHeight": src.Height, "sourceHeight": src.Height,
"sourceBitrate": src.Bitrate, "sourceBitrate": src.Bitrate,
"sourceDuration": src.Duration, "sourceDuration": src.Duration,
"fieldOrder": src.FieldOrder, "fieldOrder": src.FieldOrder,
} }
@ -6357,18 +6367,18 @@ func buildFormatBadge(formatLabel string) fyne.CanvasObject {
} else { } else {
badgeColor = ui.GetContainerColor(containerName) badgeColor = ui.GetContainerColor(containerName)
} }
// Create colored background // Create colored background
bg := canvas.NewRectangle(badgeColor) bg := canvas.NewRectangle(badgeColor)
bg.CornerRadius = 4 bg.CornerRadius = 4
bg.SetMinSize(fyne.NewSize(120, 32)) bg.SetMinSize(fyne.NewSize(120, 32))
// Create label // Create label
label := canvas.NewText(formatLabel, color.White) label := canvas.NewText(formatLabel, color.White)
label.TextStyle = fyne.TextStyle{Bold: true} label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter label.Alignment = fyne.TextAlignCenter
label.TextSize = 13 label.TextSize = 13
// Stack background and label // Stack background and label
return container.NewMax(bg, container.NewCenter(label)) return container.NewMax(bg, container.NewCenter(label))
} }
@ -6376,21 +6386,21 @@ func buildFormatBadge(formatLabel string) fyne.CanvasObject {
// buildVideoCodecBadge creates a color-coded badge for a video codec // buildVideoCodecBadge creates a color-coded badge for a video codec
func buildVideoCodecBadge(codecName string) fyne.CanvasObject { func buildVideoCodecBadge(codecName string) fyne.CanvasObject {
codecLower := strings.ToLower(strings.TrimSpace(codecName)) codecLower := strings.ToLower(strings.TrimSpace(codecName))
// Get codec color // Get codec color
badgeColor := ui.GetVideoCodecColor(codecLower) badgeColor := ui.GetVideoCodecColor(codecLower)
// Create colored background // Create colored background
bg := canvas.NewRectangle(badgeColor) bg := canvas.NewRectangle(badgeColor)
bg.CornerRadius = 4 bg.CornerRadius = 4
bg.SetMinSize(fyne.NewSize(100, 28)) bg.SetMinSize(fyne.NewSize(100, 28))
// Create label // Create label
label := canvas.NewText(codecName, color.White) label := canvas.NewText(codecName, color.White)
label.TextStyle = fyne.TextStyle{Bold: true} label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter label.Alignment = fyne.TextAlignCenter
label.TextSize = 12 label.TextSize = 12
// Stack background and label // Stack background and label
return container.NewMax(bg, container.NewCenter(label)) return container.NewMax(bg, container.NewCenter(label))
} }
@ -6398,21 +6408,21 @@ func buildVideoCodecBadge(codecName string) fyne.CanvasObject {
// buildAudioCodecBadge creates a color-coded badge for an audio codec // buildAudioCodecBadge creates a color-coded badge for an audio codec
func buildAudioCodecBadge(codecName string) fyne.CanvasObject { func buildAudioCodecBadge(codecName string) fyne.CanvasObject {
codecLower := strings.ToLower(strings.TrimSpace(codecName)) codecLower := strings.ToLower(strings.TrimSpace(codecName))
// Get codec color // Get codec color
badgeColor := ui.GetAudioCodecColor(codecLower) badgeColor := ui.GetAudioCodecColor(codecLower)
// Create colored background // Create colored background
bg := canvas.NewRectangle(badgeColor) bg := canvas.NewRectangle(badgeColor)
bg.CornerRadius = 4 bg.CornerRadius = 4
bg.SetMinSize(fyne.NewSize(100, 28)) bg.SetMinSize(fyne.NewSize(100, 28))
// Create label // Create label
label := canvas.NewText(codecName, color.White) label := canvas.NewText(codecName, color.White)
label.TextStyle = fyne.TextStyle{Bold: true} label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter label.Alignment = fyne.TextAlignCenter
label.TextSize = 12 label.TextSize = 12
// Stack background and label // Stack background and label
return container.NewMax(bg, container.NewCenter(label)) return container.NewMax(bg, container.NewCenter(label))
} }
@ -6542,25 +6552,25 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
// Forward declarations for encoding controls (used in reset/update callbacks) // Forward declarations for encoding controls (used in reset/update callbacks)
var ( var (
bitrateModeSelect *widget.Select bitrateModeSelect *widget.Select
bitratePresetSelect *widget.Select bitratePresetSelect *widget.Select
crfPresetSelect *widget.Select crfPresetSelect *widget.Select
crfEntry *widget.Entry crfEntry *widget.Entry
manualCrfRow *fyne.Container manualCrfRow *fyne.Container
videoBitrateEntry *widget.Entry videoBitrateEntry *widget.Entry
manualBitrateRow *fyne.Container manualBitrateRow *fyne.Container
targetFileSizeSelect *widget.Select targetFileSizeSelect *widget.Select
targetFileSizeEntry *widget.Entry targetFileSizeEntry *widget.Entry
qualitySelectSimple *widget.Select qualitySelectSimple *widget.Select
qualitySelectAdv *widget.Select qualitySelectAdv *widget.Select
qualitySectionSimple fyne.CanvasObject qualitySectionSimple fyne.CanvasObject
qualitySectionAdv fyne.CanvasObject qualitySectionAdv fyne.CanvasObject
simpleBitrateSelect *widget.Select simpleBitrateSelect *widget.Select
crfContainer *fyne.Container crfContainer *fyne.Container
bitrateContainer *fyne.Container bitrateContainer *fyne.Container
targetSizeContainer *fyne.Container targetSizeContainer *fyne.Container
resetConvertDefaults func() resetConvertDefaults func()
tabs *container.AppTabs tabs *container.AppTabs
simpleEncodingSection *fyne.Container simpleEncodingSection *fyne.Container
advancedVideoEncodingBlock *fyne.Container advancedVideoEncodingBlock *fyne.Container
audioEncodingSection *fyne.Container audioEncodingSection *fyne.Container
@ -6805,7 +6815,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}, false) }, false)
}() }()
}) })
analyzeInterlaceBtn.Importance = widget.MediumImportance analyzeInterlaceBtn.Importance = widget.HighImportance
// Auto-crop controls // Auto-crop controls
autoCropCheck := widget.NewCheck("Auto-Detect Black Bars", func(checked bool) { autoCropCheck := widget.NewCheck("Auto-Detect Black Bars", func(checked bool) {
@ -6863,6 +6873,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}, state.window) }, state.window)
}() }()
}) })
detectCropBtn.Importance = widget.MediumImportance
if src == nil { if src == nil {
detectCropBtn.Disable() detectCropBtn.Disable()
} }
@ -8372,7 +8383,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
formatContainer, formatContainer,
chapterWarningLabel, // Warning when converting chapters to DVD chapterWarningLabel, // Warning when converting chapters to DVD
preserveChaptersCheck, preserveChaptersCheck,
dvdAspectBox, // DVD options appear here when DVD format selected dvdAspectBox, // DVD options appear here when DVD format selected
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
outputEntry, outputEntry,
outputHintContainer, outputHintContainer,
@ -8435,7 +8446,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
formatContainer, formatContainer,
chapterWarningLabel, // Warning when converting chapters to DVD chapterWarningLabel, // Warning when converting chapters to DVD
preserveChaptersCheck, preserveChaptersCheck,
dvdAspectBox, // DVD options appear here when DVD format selected dvdAspectBox, // DVD options appear here when DVD format selected
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}), widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
outputEntry, outputEntry,
outputHintContainer, outputHintContainer,
@ -8793,14 +8804,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
leftColumn := container.NewVBox(videoPanel, spacer, metaPanel) leftColumn := container.NewVBox(videoPanel, spacer, metaPanel)
// Add 15px spacing between left and right panels // Add minimal spacing (10px) between left and right panels
horizontalSpacer := canvas.NewRectangle(color.Transparent) horizontalSpacer := canvas.NewRectangle(color.Transparent)
horizontalSpacer.SetMinSize(fyne.NewSize(15, 1)) horizontalSpacer.SetMinSize(fyne.NewSize(10, 1))
// Split: left side (video + metadata) takes 50% | right side (options) takes 50% // Split: left side (video + metadata) takes 50% | right side (options) takes 50%
mainSplit := container.New(&fixedHSplitLayout{ratio: 0.5}, mainSplit := container.NewHSplit(
container.NewHBox(leftColumn, horizontalSpacer), leftColumn,
optionsPanel) optionsPanel)
mainSplit.SetOffset(0.5) // 50/50 split
// Add horizontal padding around the split (10px on each side) // Add horizontal padding around the split (10px on each side)
mainContent := container.NewPadded(mainSplit) mainContent := container.NewPadded(mainSplit)
@ -8889,7 +8901,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
state.openLogViewer("Conversion Log", state.convertActiveLog, state.convertBusy) state.openLogViewer("Conversion Log", state.convertActiveLog, state.convertBusy)
}) })
viewLogBtn.Importance = widget.LowImportance viewLogBtn.Importance = widget.MediumImportance
if state.convertActiveLog == "" { if state.convertActiveLog == "" {
viewLogBtn.Disable() viewLogBtn.Disable()
} }
@ -8958,6 +8970,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
state.convert = cfg state.convert = cfg
state.showConvertView(state.source) state.showConvertView(state.source)
}) })
loadCfgBtn.Importance = widget.MediumImportance
saveCfgBtn := widget.NewButton("Save Config", func() { saveCfgBtn := widget.NewButton("Save Config", func() {
if err := savePersistedConvertConfig(state.convert); err != nil { if err := savePersistedConvertConfig(state.convert); err != nil {
dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window) dialog.ShowError(fmt.Errorf("failed to save config: %w", err), state.window)
@ -8965,6 +8979,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
} }
dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", defaultConvertConfigPath()), state.window) dialog.ShowInformation("Config Saved", fmt.Sprintf("Saved to %s", defaultConvertConfigPath()), state.window)
}) })
saveCfgBtn.Importance = widget.MediumImportance
// FFmpeg Command Preview // FFmpeg Command Preview
var commandPreviewWidget *ui.FFmpegCommandWidget var commandPreviewWidget *ui.FFmpegCommandWidget
@ -9509,11 +9524,12 @@ Metadata: %s`,
}() }()
} }
}, false) }, false)
}() }()
}) })
previewBtn.Importance = widget.LowImportance detectCropBtn.Importance = widget.MediumImportance
previewSection = previewBtn if src == nil {
} detectCropBtn.Disable()
}
var sectionItems []fyne.CanvasObject var sectionItems []fyne.CanvasObject
sectionItems = append(sectionItems, sectionItems = append(sectionItems,
@ -12893,40 +12909,40 @@ func normalizeCodecName(codec string) string {
// Map common variations to standard names // Map common variations to standard names
replacements := map[string]string{ replacements := map[string]string{
"h264": "h264", "h264": "h264",
"avc": "h264", "avc": "h264",
"avc1": "h264", "avc1": "h264",
"h.264": "h264", "h.264": "h264",
"x264": "h264", "x264": "h264",
"h265": "h265", "h265": "h265",
"hevc": "h265", "hevc": "h265",
"h.265": "h265", "h.265": "h265",
"x265": "h265", "x265": "h265",
"mpeg4": "mpeg4", "mpeg4": "mpeg4",
"divx": "mpeg4", "divx": "mpeg4",
"xvid": "mpeg4", "xvid": "mpeg4",
"mpeg-4": "mpeg4", "mpeg-4": "mpeg4",
"mpeg2": "mpeg2", "mpeg2": "mpeg2",
"mpeg-2": "mpeg2", "mpeg-2": "mpeg2",
"mpeg2video": "mpeg2", "mpeg2video": "mpeg2",
"aac": "aac", "aac": "aac",
"mp3": "mp3", "mp3": "mp3",
"ac3": "ac3", "ac3": "ac3",
"a_ac3": "ac3", "a_ac3": "ac3",
"eac3": "eac3", "eac3": "eac3",
"vorbis": "vorbis", "vorbis": "vorbis",
"opus": "opus", "opus": "opus",
"vp8": "vp8", "vp8": "vp8",
"vp9": "vp9", "vp9": "vp9",
"av1": "av1", "av1": "av1",
"libaom-av1": "av1", "libaom-av1": "av1",
"theora": "theora", "theora": "theora",
"wmv3": "wmv", "wmv3": "wmv",
"vc1": "vc1", "vc1": "vc1",
"prores": "prores", "prores": "prores",
"prores_ks": "prores", "prores_ks": "prores",
"mjpeg": "mjpeg", "mjpeg": "mjpeg",
"png": "png", "png": "png",
} }
for old, new := range replacements { for old, new := range replacements {