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:
+253
-15
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user