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