Add Shorts wizard and v4 video-style pipeline

Turn the Shorts tab into a step-by-step wizard and port
amigadb-short-fx-v4.sh: Video background style, auto/manual crop,
source fade-in, and blur/opacity/margin settings.
This commit is contained in:
2026-08-15 17:59:18 +02:00
parent 58e11ba4e3
commit 8a734d58d9
10 changed files with 1911 additions and 145 deletions
+253 -15
View File
@@ -23,6 +23,10 @@ public sealed partial class MainPage : Page
private string? _lastRenderedFile;
private bool _logFlushScheduled;
private bool _isShortsTab;
private bool _isRendering;
private bool _keepRenderOutput;
private List<ShortWizardStep> _wizardSteps = [.. ShortWizard.StepsFor(ShortStyle.Brand)];
private int _wizardIndex;
public MainPage()
{
@@ -48,12 +52,23 @@ public sealed partial class MainPage : Page
ShortWebsiteBox.Text = _loadedSettings.ShortsWebsite;
ShortCrfBox.Text = _loadedSettings.ShortsCrf;
ShortBackgroundBox.Text = _loadedSettings.ShortsBackgroundImage;
ShortBackgroundVideoBox.Text = _loadedSettings.ShortsBackgroundVideo;
ShortVideoBlurBox.Text = _loadedSettings.ShortsVideoBlur;
ShortVideoOpacityBox.Text = _loadedSettings.ShortsVideoOpacity;
ShortSourceMarginBox.Text = _loadedSettings.ShortsSourceMarginX;
ShortFadeInBox.Text = _loadedSettings.ShortsSourceFadeIn;
ShortCropLeftBox.Text = _loadedSettings.ShortsCropLeft;
ShortCropRightBox.Text = _loadedSettings.ShortsCropRight;
ShortCropTopBox.Text = _loadedSettings.ShortsCropTop;
ShortCropBottomBox.Text = _loadedSettings.ShortsCropBottom;
ShortFontBox.Text = string.IsNullOrWhiteSpace(_loadedSettings.ShortsFontFile)
? ShortsPipeline.FindDefaultFont() ?? ""
: _loadedSettings.ShortsFontFile;
SelectComboTag(ShortStyleBox, _loadedSettings.ShortsStyle);
SelectComboTag(ShortPresetBox, _loadedSettings.ShortsPreset);
UpdateShortBackgroundVisibility();
SelectComboTag(ShortCropModeBox, _loadedSettings.ShortsSourceCropMode);
RefreshWizardSteps();
ShowWizardStep();
}
private void VideoTab_Click(object sender, RoutedEventArgs e) => SetActiveTab(shorts: false);
@@ -62,23 +77,135 @@ public sealed partial class MainPage : Page
private void SetActiveTab(bool shorts)
{
if (!_isRendering)
_keepRenderOutput = false;
_isShortsTab = shorts;
VideoContentPanel.Visibility = shorts ? Visibility.Collapsed : Visibility.Visible;
ShortsContentPanel.Visibility = shorts ? Visibility.Visible : Visibility.Collapsed;
Style primaryButtonStyle = (Style)Application.Current.Resources["PrimaryButtonStyle"];
VideoTabButton.Style = shorts ? null : primaryButtonStyle;
ShortsTabButton.Style = shorts ? primaryButtonStyle : null;
Style tabButtonStyle = (Style)Application.Current.Resources["TabButtonStyle"];
VideoTabButton.Style = shorts ? tabButtonStyle : primaryButtonStyle;
ShortsTabButton.Style = shorts ? primaryButtonStyle : tabButtonStyle;
RenderButton.Content = shorts ? "Render short" : "Start render";
UpdateWorkspaceVisibility();
}
private void ShortStyleBox_SelectionChanged(object sender, SelectionChangedEventArgs e) => UpdateShortBackgroundVisibility();
private void UpdateShortBackgroundVisibility()
private void ShortStyleBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
bool isBrand = string.Equals(SelectedShortStyle(), nameof(ShortStyle.Brand), StringComparison.OrdinalIgnoreCase);
ShortBackgroundRow.Visibility = isBrand ? Visibility.Visible : Visibility.Collapsed;
if (!IsLoaded) return;
RefreshWizardSteps();
ShowWizardStep();
}
private void ShortBack_Click(object sender, RoutedEventArgs e)
{
if (_wizardIndex <= 0) return;
_keepRenderOutput = false;
_wizardIndex--;
ShowWizardStep();
}
private async void ShortNext_Click(object sender, RoutedEventArgs e)
{
try
{
ValidateCurrentWizardStep();
if (_wizardIndex >= _wizardSteps.Count - 1) return;
_keepRenderOutput = false;
_wizardIndex++;
ShowWizardStep();
}
catch (Exception exception)
{
await ShowMessageAsync("Check this step", exception.Message);
}
}
private void RefreshWizardSteps()
{
ShortWizardStep current = _wizardSteps[Math.Clamp(_wizardIndex, 0, _wizardSteps.Count - 1)];
_wizardSteps = [.. ShortWizard.StepsFor(SelectedShortStyleEnum())];
int index = _wizardSteps.IndexOf(current);
_wizardIndex = index >= 0 ? index : Math.Min(_wizardIndex, _wizardSteps.Count - 1);
}
private void ShowWizardStep()
{
ShortWizardStep step = _wizardSteps[Math.Clamp(_wizardIndex, 0, _wizardSteps.Count - 1)];
ShortWizardTitleText.Text = ShortWizard.Title(step);
ShortWizardStepText.Text = $"Step {_wizardIndex + 1} of {_wizardSteps.Count}";
ShortWizardProgress.Maximum = _wizardSteps.Count;
ShortWizardProgress.Value = _wizardIndex + 1;
ShortStepClipPanel.Visibility = VisibleIf(step == ShortWizardStep.Clip);
ShortStepEffectPanel.Visibility = VisibleIf(step == ShortWizardStep.Effect);
ShortStepStyleOptionsPanel.Visibility = VisibleIf(step == ShortWizardStep.StyleOptions);
ShortStepTitlesPanel.Visibility = VisibleIf(step == ShortWizardStep.Titles);
ShortStepEncodePanel.Visibility = VisibleIf(step == ShortWizardStep.Encode);
ShortStyle style = SelectedShortStyleEnum();
ShortStyleDescriptionText.Text = ShortWizard.Description(style);
ShortBackgroundRow.Visibility = VisibleIf(style == ShortStyle.Brand);
ShortVideoOptionsPanel.Visibility = VisibleIf(style == ShortStyle.Video);
if (step == ShortWizardStep.Encode)
ShortSummaryText.Text = BuildShortSummary();
UpdateWizardNavigation();
}
private void UpdateWizardNavigation()
{
bool shorts = _isShortsTab && !_isRendering;
bool lastStep = _wizardIndex >= _wizardSteps.Count - 1;
ShortBackButton.Visibility = VisibleIf(shorts);
ShortNextButton.Visibility = VisibleIf(shorts && !lastStep);
ShortBackButton.IsEnabled = shorts && _wizardIndex > 0;
ShortNextButton.IsEnabled = shorts;
RenderButton.Visibility = VisibleIf(!_isRendering && (!shorts || lastStep));
}
private void ValidateCurrentWizardStep()
{
ShortWizardStep step = _wizardSteps[_wizardIndex];
switch (step)
{
case ShortWizardStep.Clip:
if (string.IsNullOrWhiteSpace(ShortInputBox.Text) || !File.Exists(ShortInputBox.Text.Trim()))
throw new FileNotFoundException("Select a valid source video for the Short.");
ParsePositiveDuration();
break;
case ShortWizardStep.Effect:
ParseNonNegativeNumber(ShortFadeInBox.Text, "source fade-in");
break;
case ShortWizardStep.StyleOptions:
if (SelectedShortStyleEnum() == ShortStyle.Brand
&& (string.IsNullOrWhiteSpace(ShortBackgroundBox.Text) || !File.Exists(ShortBackgroundBox.Text.Trim())))
throw new FileNotFoundException("The brand style requires a background image.");
if (SelectedShortStyleEnum() == ShortStyle.Video)
ReadVideoStyleOptions();
break;
case ShortWizardStep.Titles:
if (string.IsNullOrWhiteSpace(ShortTitleBox.Text))
throw new ArgumentException("Enter a title.");
break;
case ShortWizardStep.Encode:
if (string.IsNullOrWhiteSpace(ShortFontBox.Text) || !File.Exists(ShortFontBox.Text.Trim()))
throw new FileNotFoundException("Select a bold TrueType/OpenType font file.");
ParseCrf();
break;
}
}
private string BuildShortSummary()
{
string input = string.IsNullOrWhiteSpace(ShortInputBox.Text)
? "(no source yet)"
: Path.GetFileName(ShortInputBox.Text.Trim());
string title = string.IsNullOrWhiteSpace(ShortTitleBox.Text) ? "(no title yet)" : ShortTitleBox.Text.Trim();
return $"{SelectedShortStyleEnum()} · {input} · {ShortStartBox.Text.Trim()} + {ShortDurationBox.Text.Trim()}s · {title}";
}
private static Visibility VisibleIf(bool visible) => visible ? Visibility.Visible : Visibility.Collapsed;
private async void AddInputs_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
@@ -180,6 +307,7 @@ public sealed partial class MainPage : Page
{
try
{
_keepRenderOutput = false;
SetRendering(true);
LogBox.Text = string.Empty;
_renderCancellation = new CancellationTokenSource();
@@ -191,10 +319,12 @@ public sealed partial class MainPage : Page
}
catch (OperationCanceledException)
{
_keepRenderOutput = true;
UpdateProgress(new(RenderProgressBar.Value, "Cancelled", "The render was cancelled."));
}
catch (Exception exception)
{
_keepRenderOutput = true;
AppendLog("ERROR: " + exception);
StageText.Text = "Failed";
await ShowMessageAsync("Render failed", UserFacingErrors.Summarize(exception));
@@ -308,14 +438,15 @@ public sealed partial class MainPage : Page
private ShortSettings ReadShortSettings()
{
if (!double.TryParse(ShortDurationBox.Text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out double duration) || duration <= 0)
throw new ArgumentException("Enter a valid duration in seconds.");
if (!int.TryParse(ShortCrfBox.Text, out int crf))
throw new ArgumentException("CRF must be an integer.");
double duration = ParsePositiveDuration();
int crf = ParseCrf();
(double blur, double opacity, int marginX, ShortSourceCropMode cropMode, int left, int right, int top, int bottom)
= ReadVideoStyleOptions();
double fadeIn = ParseNonNegativeNumber(ShortFadeInBox.Text, "source fade-in");
string input = ShortInputBox.Text.Trim();
string output = ShortOutputBox.Text.Trim();
ShortStyle style = Enum.Parse<ShortStyle>(SelectedShortStyle());
ShortStyle style = SelectedShortStyleEnum();
if (string.IsNullOrWhiteSpace(output) && !string.IsNullOrWhiteSpace(input))
{
string baseName = Path.GetFileNameWithoutExtension(input);
@@ -337,6 +468,16 @@ public sealed partial class MainPage : Page
DurationSeconds = duration,
Style = style,
BackgroundImage = ShortBackgroundBox.Text.Trim(),
BackgroundVideo = ShortBackgroundVideoBox.Text.Trim(),
VideoBlur = blur,
VideoOpacity = opacity,
SourceCropMode = cropMode,
CropLeft = left,
CropRight = right,
CropTop = top,
CropBottom = bottom,
SourceMarginX = marginX,
SourceFadeInSeconds = fadeIn,
FontFile = ShortFontBox.Text.Trim(),
FfmpegPath = FfmpegPathBox.Text.Trim(),
FfprobePath = FfprobePathBox.Text.Trim(),
@@ -345,6 +486,59 @@ public sealed partial class MainPage : Page
};
}
private double ParsePositiveDuration()
{
if (!double.TryParse(ShortDurationBox.Text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out double duration) || duration <= 0)
throw new ArgumentException("Enter a valid duration in seconds.");
return duration;
}
private int ParseCrf()
{
if (!int.TryParse(ShortCrfBox.Text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int crf))
throw new ArgumentException("CRF must be an integer.");
return crf;
}
private static double ParseNonNegativeNumber(string text, string name)
{
if (!double.TryParse(text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out double value) || value < 0)
throw new ArgumentException($"Enter a valid non-negative value for {name}.");
return value;
}
private static int ParseNonNegativeInt(string text, string name)
{
if (!int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) || value < 0)
throw new ArgumentException($"{name} must be a non-negative integer.");
return value;
}
private (double Blur, double Opacity, int MarginX, ShortSourceCropMode CropMode, int Left, int Right, int Top, int Bottom)
ReadVideoStyleOptions()
{
double blur = ParseNonNegativeNumber(ShortVideoBlurBox.Text, "background blur");
double opacity = ParseNonNegativeNumber(ShortVideoOpacityBox.Text, "background opacity");
if (opacity > 1)
throw new ArgumentException("Background opacity must be between 0.0 and 1.0.");
int marginX = ParseNonNegativeInt(ShortSourceMarginBox.Text, "Side margin");
if (marginX >= 540)
throw new ArgumentException("Source side margin must be less than 540 pixels.");
if (SelectedShortStyleEnum() == ShortStyle.Video
&& (string.IsNullOrWhiteSpace(ShortBackgroundVideoBox.Text) || !File.Exists(ShortBackgroundVideoBox.Text.Trim())))
throw new FileNotFoundException("The video style requires a background video.");
return (
blur,
opacity,
marginX,
SelectedShortCropMode(),
ParseNonNegativeInt(ShortCropLeftBox.Text, "Crop left"),
ParseNonNegativeInt(ShortCropRightBox.Text, "Crop right"),
ParseNonNegativeInt(ShortCropTopBox.Text, "Crop top"),
ParseNonNegativeInt(ShortCropBottomBox.Text, "Crop bottom"));
}
private async void BrowseShortInput_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
@@ -378,6 +572,17 @@ public sealed partial class MainPage : Page
ShortBackgroundBox.Text = file.Path;
}
private async void BrowseShortBackgroundVideo_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
foreach (string extension in SupportedVideoFormats.Extensions)
picker.FileTypeFilter.Add(extension);
picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
var file = await picker.PickSingleFileAsync();
if (file is not null)
ShortBackgroundVideoBox.Text = file.Path;
}
private async void BrowseShortFont_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
@@ -396,11 +601,25 @@ public sealed partial class MainPage : Page
private void SetRendering(bool rendering)
{
_isRendering = rendering;
RenderButton.IsEnabled = !rendering;
CancelButton.IsEnabled = rendering;
OpenSettingsButton.IsEnabled = !rendering;
VideoTabButton.IsEnabled = !rendering;
ShortsTabButton.IsEnabled = !rendering;
UpdateWorkspaceVisibility();
}
private void UpdateWorkspaceVisibility()
{
bool showSetup = !_isRendering;
bool showOutput = _isRendering || _keepRenderOutput;
VideoContentPanel.Visibility = VisibleIf(showSetup && !_isShortsTab);
ShortsContentPanel.Visibility = VisibleIf(showSetup && _isShortsTab);
RenderProgressPanel.Visibility = VisibleIf(showOutput);
FfmpegLogPanel.Visibility = VisibleIf(showOutput);
UpdateWizardNavigation();
}
private void UpdateProgress(RenderProgress value)
@@ -496,6 +715,16 @@ public sealed partial class MainPage : Page
ShortsWebsite = ShortWebsiteBox.Text.Trim(),
ShortsFontFile = ShortFontBox.Text.Trim(),
ShortsBackgroundImage = ShortBackgroundBox.Text.Trim(),
ShortsBackgroundVideo = ShortBackgroundVideoBox.Text.Trim(),
ShortsVideoBlur = ShortVideoBlurBox.Text.Trim(),
ShortsVideoOpacity = ShortVideoOpacityBox.Text.Trim(),
ShortsSourceCropMode = SelectedShortCropMode().ToString(),
ShortsCropLeft = ShortCropLeftBox.Text.Trim(),
ShortsCropRight = ShortCropRightBox.Text.Trim(),
ShortsCropTop = ShortCropTopBox.Text.Trim(),
ShortsCropBottom = ShortCropBottomBox.Text.Trim(),
ShortsSourceMarginX = ShortSourceMarginBox.Text.Trim(),
ShortsSourceFadeIn = ShortFadeInBox.Text.Trim(),
ShortsCrf = ShortCrfBox.Text.Trim(),
ShortsPreset = SelectedShortPreset(),
});
@@ -507,6 +736,15 @@ public sealed partial class MainPage : Page
private string SelectedShortStyle() => (ShortStyleBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Brand";
private ShortStyle SelectedShortStyleEnum() =>
Enum.TryParse(SelectedShortStyle(), ignoreCase: true, out ShortStyle style) ? style : ShortStyle.Brand;
private ShortSourceCropMode SelectedShortCropMode()
{
string tag = (ShortCropModeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Auto";
return Enum.TryParse(tag, ignoreCase: true, out ShortSourceCropMode mode) ? mode : ShortSourceCropMode.Auto;
}
private string SelectedShortPreset() => (ShortPresetBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "slow";
private void SelectTheme(string theme) => SelectComboTag(ThemeBox, theme);