Add Shorts look preview and visual crop editor

Show opening/closing 9:16 stills from the same filtergraph, and let
users drag a keep-region on a source frame to fill the crop insets.
This commit is contained in:
2026-08-15 21:08:36 +02:00
parent 8a734d58d9
commit 323324d713
9 changed files with 958 additions and 12 deletions
+120
View File
@@ -1,11 +1,13 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Text;
using AmiReel.Models;
using AmiReel.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers;
@@ -27,6 +29,8 @@ public sealed partial class MainPage : Page
private bool _keepRenderOutput;
private List<ShortWizardStep> _wizardSteps = [.. ShortWizard.StepsFor(ShortStyle.Brand)];
private int _wizardIndex;
private int _previewGeneration;
private string? _previewDirectory;
public MainPage()
{
@@ -150,6 +154,8 @@ public sealed partial class MainPage : Page
ShortSummaryText.Text = BuildShortSummary();
UpdateWizardNavigation();
if (IsLoaded && step != ShortWizardStep.Clip)
_ = RefreshShortPreviewAsync(automatic: true);
}
private void UpdateWizardNavigation()
@@ -593,12 +599,126 @@ public sealed partial class MainPage : Page
ShortFontBox.Text = file.Path;
}
private async void OpenCropEditor_Click(object sender, RoutedEventArgs e)
{
string input = ShortInputBox.Text.Trim();
if (string.IsNullOrWhiteSpace(input) || !File.Exists(input))
{
await ShowMessageAsync("Crop source", "Select a valid source video first.");
return;
}
try
{
CropEditorDialog.XamlRoot = XamlRoot;
CropEditorDialog.RequestedTheme = ActualTheme;
await ShortCropEditor.LoadVideoFrameAsync(
input,
string.IsNullOrWhiteSpace(ShortStartBox.Text) ? "00:00:00" : ShortStartBox.Text.Trim(),
FfmpegPathBox.Text.Trim(),
FfprobePathBox.Text.Trim(),
CancellationToken.None);
ShortCropEditor.SetInsets(ReadCropInsets());
ContentDialogResult result = await CropEditorDialog.ShowAsync();
if (result != ContentDialogResult.Primary)
return;
PixelInsets insets = ShortCropEditor.Insets;
ShortCropLeftBox.Text = insets.Left.ToString(CultureInfo.InvariantCulture);
ShortCropRightBox.Text = insets.Right.ToString(CultureInfo.InvariantCulture);
ShortCropTopBox.Text = insets.Top.ToString(CultureInfo.InvariantCulture);
ShortCropBottomBox.Text = insets.Bottom.ToString(CultureInfo.InvariantCulture);
}
catch (Exception exception)
{
await ShowMessageAsync("Crop source", UserFacingErrors.Summarize(exception));
}
}
private PixelInsets ReadCropInsets()
{
static int ReadBox(string text)
{
return int.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value) && value > 0
? value
: 0;
}
return new PixelInsets(
ReadBox(ShortCropLeftBox.Text),
ReadBox(ShortCropRightBox.Text),
ReadBox(ShortCropTopBox.Text),
ReadBox(ShortCropBottomBox.Text));
}
private async void PreviewShortSource_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(ShortInputBox.Text))
await OpenPreviewAsync(ShortInputBox.Text.Trim());
}
private async void UpdateShortPreview_Click(object sender, RoutedEventArgs e) =>
await RefreshShortPreviewAsync(automatic: false);
private async Task RefreshShortPreviewAsync(bool automatic)
{
if (_isRendering)
return;
int generation = ++_previewGeneration;
ShortPreviewProgress.Visibility = Visibility.Visible;
UpdateShortPreviewButton.IsEnabled = false;
ShortPreviewStatusText.Text = "Rendering 9:16 preview frames...";
try
{
ShortSettings settings = ReadShortSettings();
ShortsPipeline.ValidatePreview(settings);
_previewDirectory ??= Path.Combine(Path.GetTempPath(), "AmiReel", "short-preview", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(_previewDirectory);
ShortPreviewFrames frames = await new ShortsPipeline().RenderPreviewAsync(
settings, _previewDirectory, _ => { }, CancellationToken.None);
if (generation != _previewGeneration)
return;
ShortPreviewOpeningImage.Source = await LoadPreviewBitmapAsync(frames.OpeningPath);
ShortPreviewClosingImage.Source = await LoadPreviewBitmapAsync(frames.ClosingPath);
bool hasFont = !string.IsNullOrWhiteSpace(settings.FontFile) && File.Exists(settings.FontFile);
ShortPreviewStatusText.Text = hasFont
? $"{settings.Style} · opening hook and closing title card"
: "Look only — pick a font on the last step to include the title-card text.";
}
catch (Exception exception)
{
if (generation != _previewGeneration)
return;
string message = UserFacingErrors.Summarize(exception);
ShortPreviewStatusText.Text = message;
if (!automatic)
await ShowMessageAsync("Preview failed", message);
}
finally
{
if (generation == _previewGeneration)
{
ShortPreviewProgress.Visibility = Visibility.Collapsed;
UpdateShortPreviewButton.IsEnabled = true;
}
}
}
private static async Task<BitmapImage> LoadPreviewBitmapAsync(string path)
{
await using FileStream stream = File.OpenRead(path);
BitmapImage bitmap = new() { DecodePixelWidth = 296 };
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
return bitmap;
}
private void SetRendering(bool rendering)
{
_isRendering = rendering;