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 168aab1ec8
commit 876f1f6c95

182
main.go
View File

@ -87,7 +87,7 @@ var (
{"trim", "Trim", utils.MustHex("#F9A825"), "Convert", nil}, // Dark Yellow/Gold (not implemented yet)
{"filters", "Filters", utils.MustHex("#00BCD4"), "Convert", modules.HandleFilters}, // Cyan (creative filters)
{"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)
{"rip", "Rip", utils.MustHex("#FF9800"), "Disc", modules.HandleRip}, // Orange (extraction)
{"bluray", "Blu-Ray", utils.MustHex("#2196F3"), "Disc", nil}, // Blue (not implemented yet)
@ -907,6 +907,16 @@ type appState struct {
filterInterpPreset 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
upscaleFile *videoSource
upscaleMethod string // lanczos, bicubic, spline, bilinear
@ -3083,10 +3093,10 @@ func (s *appState) batchAddToQueue(paths []string) {
"outputPath": outPath,
"outputBase": outputBase,
"selectedFormat": s.convert.SelectedFormat,
"quality": s.convert.Quality,
"mode": s.convert.Mode,
"preserveChapters": s.convert.PreserveChapters,
"videoCodec": s.convert.VideoCodec,
"quality": s.convert.Quality,
"mode": s.convert.Mode,
"preserveChapters": s.convert.PreserveChapters,
"videoCodec": s.convert.VideoCodec,
"encoderPreset": s.convert.EncoderPreset,
"crf": s.convert.CRF,
"bitrateMode": s.convert.BitrateMode,
@ -3113,7 +3123,7 @@ func (s *appState) batchAddToQueue(paths []string) {
"sourceWidth": src.Width,
"sourceHeight": src.Height,
"sourceBitrate": src.Bitrate,
"sourceDuration": src.Duration,
"sourceDuration": src.Duration,
"fieldOrder": src.FieldOrder,
}
@ -6357,18 +6367,18 @@ func buildFormatBadge(formatLabel string) fyne.CanvasObject {
} else {
badgeColor = ui.GetContainerColor(containerName)
}
// Create colored background
bg := canvas.NewRectangle(badgeColor)
bg.CornerRadius = 4
bg.SetMinSize(fyne.NewSize(120, 32))
// Create label
label := canvas.NewText(formatLabel, color.White)
label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter
label.TextSize = 13
// Stack background and 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
func buildVideoCodecBadge(codecName string) fyne.CanvasObject {
codecLower := strings.ToLower(strings.TrimSpace(codecName))
// Get codec color
badgeColor := ui.GetVideoCodecColor(codecLower)
// Create colored background
bg := canvas.NewRectangle(badgeColor)
bg.CornerRadius = 4
bg.SetMinSize(fyne.NewSize(100, 28))
// Create label
label := canvas.NewText(codecName, color.White)
label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter
label.TextSize = 12
// Stack background and 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
func buildAudioCodecBadge(codecName string) fyne.CanvasObject {
codecLower := strings.ToLower(strings.TrimSpace(codecName))
// Get codec color
badgeColor := ui.GetAudioCodecColor(codecLower)
// Create colored background
bg := canvas.NewRectangle(badgeColor)
bg.CornerRadius = 4
bg.SetMinSize(fyne.NewSize(100, 28))
// Create label
label := canvas.NewText(codecName, color.White)
label.TextStyle = fyne.TextStyle{Bold: true}
label.Alignment = fyne.TextAlignCenter
label.TextSize = 12
// Stack background and 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)
var (
bitrateModeSelect *widget.Select
bitratePresetSelect *widget.Select
crfPresetSelect *widget.Select
crfEntry *widget.Entry
manualCrfRow *fyne.Container
videoBitrateEntry *widget.Entry
manualBitrateRow *fyne.Container
targetFileSizeSelect *widget.Select
targetFileSizeEntry *widget.Entry
qualitySelectSimple *widget.Select
qualitySelectAdv *widget.Select
qualitySectionSimple fyne.CanvasObject
qualitySectionAdv fyne.CanvasObject
simpleBitrateSelect *widget.Select
crfContainer *fyne.Container
bitrateContainer *fyne.Container
targetSizeContainer *fyne.Container
resetConvertDefaults func()
tabs *container.AppTabs
bitrateModeSelect *widget.Select
bitratePresetSelect *widget.Select
crfPresetSelect *widget.Select
crfEntry *widget.Entry
manualCrfRow *fyne.Container
videoBitrateEntry *widget.Entry
manualBitrateRow *fyne.Container
targetFileSizeSelect *widget.Select
targetFileSizeEntry *widget.Entry
qualitySelectSimple *widget.Select
qualitySelectAdv *widget.Select
qualitySectionSimple fyne.CanvasObject
qualitySectionAdv fyne.CanvasObject
simpleBitrateSelect *widget.Select
crfContainer *fyne.Container
bitrateContainer *fyne.Container
targetSizeContainer *fyne.Container
resetConvertDefaults func()
tabs *container.AppTabs
simpleEncodingSection *fyne.Container
advancedVideoEncodingBlock *fyne.Container
audioEncodingSection *fyne.Container
@ -6805,7 +6815,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}, false)
}()
})
analyzeInterlaceBtn.Importance = widget.MediumImportance
analyzeInterlaceBtn.Importance = widget.HighImportance
// Auto-crop controls
autoCropCheck := widget.NewCheck("Auto-Detect Black Bars", func(checked bool) {
@ -6863,6 +6873,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}, state.window)
}()
})
detectCropBtn.Importance = widget.MediumImportance
if src == nil {
detectCropBtn.Disable()
}
@ -8372,7 +8383,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
formatContainer,
chapterWarningLabel, // Warning when converting chapters to DVD
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}),
outputEntry,
outputHintContainer,
@ -8435,7 +8446,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
formatContainer,
chapterWarningLabel, // Warning when converting chapters to DVD
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}),
outputEntry,
outputHintContainer,
@ -8793,14 +8804,15 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
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.SetMinSize(fyne.NewSize(15, 1))
horizontalSpacer.SetMinSize(fyne.NewSize(10, 1))
// Split: left side (video + metadata) takes 50% | right side (options) takes 50%
mainSplit := container.New(&fixedHSplitLayout{ratio: 0.5},
container.NewHBox(leftColumn, horizontalSpacer),
mainSplit := container.NewHSplit(
leftColumn,
optionsPanel)
mainSplit.SetOffset(0.5) // 50/50 split
// Add horizontal padding around the split (10px on each side)
mainContent := container.NewPadded(mainSplit)
@ -8889,7 +8901,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
}
state.openLogViewer("Conversion Log", state.convertActiveLog, state.convertBusy)
})
viewLogBtn.Importance = widget.LowImportance
viewLogBtn.Importance = widget.MediumImportance
if state.convertActiveLog == "" {
viewLogBtn.Disable()
}
@ -8958,6 +8970,8 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
state.convert = cfg
state.showConvertView(state.source)
})
loadCfgBtn.Importance = widget.MediumImportance
saveCfgBtn := widget.NewButton("Save Config", func() {
if err := savePersistedConvertConfig(state.convert); err != nil {
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)
})
saveCfgBtn.Importance = widget.MediumImportance
// FFmpeg Command Preview
var commandPreviewWidget *ui.FFmpegCommandWidget
@ -9509,11 +9524,12 @@ Metadata: %s`,
}()
}
}, false)
}()
})
previewBtn.Importance = widget.LowImportance
previewSection = previewBtn
}
}()
})
detectCropBtn.Importance = widget.MediumImportance
if src == nil {
detectCropBtn.Disable()
}
var sectionItems []fyne.CanvasObject
sectionItems = append(sectionItems,
@ -12893,40 +12909,40 @@ func normalizeCodecName(codec string) string {
// Map common variations to standard names
replacements := map[string]string{
"h264": "h264",
"avc": "h264",
"avc1": "h264",
"h.264": "h264",
"x264": "h264",
"h265": "h265",
"hevc": "h265",
"h.265": "h265",
"x265": "h265",
"mpeg4": "mpeg4",
"divx": "mpeg4",
"xvid": "mpeg4",
"mpeg-4": "mpeg4",
"mpeg2": "mpeg2",
"mpeg-2": "mpeg2",
"mpeg2video": "mpeg2",
"aac": "aac",
"mp3": "mp3",
"ac3": "ac3",
"a_ac3": "ac3",
"eac3": "eac3",
"vorbis": "vorbis",
"opus": "opus",
"vp8": "vp8",
"vp9": "vp9",
"av1": "av1",
"libaom-av1": "av1",
"theora": "theora",
"wmv3": "wmv",
"vc1": "vc1",
"prores": "prores",
"prores_ks": "prores",
"mjpeg": "mjpeg",
"png": "png",
"h264": "h264",
"avc": "h264",
"avc1": "h264",
"h.264": "h264",
"x264": "h264",
"h265": "h265",
"hevc": "h265",
"h.265": "h265",
"x265": "h265",
"mpeg4": "mpeg4",
"divx": "mpeg4",
"xvid": "mpeg4",
"mpeg-4": "mpeg4",
"mpeg2": "mpeg2",
"mpeg-2": "mpeg2",
"mpeg2video": "mpeg2",
"aac": "aac",
"mp3": "mp3",
"ac3": "ac3",
"a_ac3": "ac3",
"eac3": "eac3",
"vorbis": "vorbis",
"opus": "opus",
"vp8": "vp8",
"vp9": "vp9",
"av1": "av1",
"libaom-av1": "av1",
"theora": "theora",
"wmv3": "wmv",
"vc1": "vc1",
"prores": "prores",
"prores_ks": "prores",
"mjpeg": "mjpeg",
"png": "png",
}
for old, new := range replacements {