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:
parent
e93353fea3
commit
c063b3f8f5
182
main.go
182
main.go
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user