Files
AmiReel/AmiReel.WinUI/MainPage.xaml.cs
T
2026-08-13 12:39:03 +02:00

378 lines
12 KiB
C#

using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
using System.Text;
using AmigaDB.VideoRenderer.Models;
using AmigaDB.VideoRenderer.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers;
using WinRT.Interop;
namespace AmiReel_WinUI;
public sealed partial class MainPage : Page
{
private readonly ObservableCollection<string> _inputs = [];
private readonly AppSettings _loadedSettings;
private readonly StringBuilder _pendingLog = new();
private readonly object _logLock = new();
private CancellationTokenSource? _renderCancellation;
private string? _lastRenderedFile;
private bool _logFlushScheduled;
public MainPage()
{
InitializeComponent();
InputList.ItemsSource = _inputs;
_loadedSettings = AppSettingsStore.Load();
OutputFolderBox.Text = _loadedSettings.OutputFolder;
EndCardBox.Text = _loadedSettings.EndCardPath;
FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
FfprobePathBox.Text = _loadedSettings.FfprobePath;
OutputNameBox.Text = "amigadb_intro";
TrimBox.Text = _loadedSettings.TrimStart;
FadeBox.Text = _loadedSettings.FadeSeconds;
HoldBox.Text = _loadedSettings.EndCardHoldSeconds;
IntervalBox.Text = _loadedSettings.ThumbnailInterval;
SelectEncoder(_loadedSettings.Encoder);
SelectTheme(_loadedSettings.Theme);
ApplyTheme(_loadedSettings.Theme);
MoveSourcesBox.IsChecked = _loadedSettings.ShouldMoveSourcesToOriginals;
}
private async void AddInputs_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
picker.FileTypeFilter.Add(".avi");
picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
var files = await picker.PickMultipleFilesAsync();
if (files is null) return;
foreach (string path in files.Select(file => file.Path).OrderBy(NaturalKey))
if (!_inputs.Contains(path, StringComparer.OrdinalIgnoreCase)) _inputs.Add(path);
}
private void ClearInputs_Click(object sender, RoutedEventArgs e) => _inputs.Clear();
private void InputList_DragOver(object sender, DragEventArgs e)
{
e.AcceptedOperation = e.DataView.Contains(StandardDataFormats.StorageItems)
? DataPackageOperation.Copy
: DataPackageOperation.None;
}
private async void InputList_Drop(object sender, DragEventArgs e)
{
if (!e.DataView.Contains(StandardDataFormats.StorageItems)) return;
IReadOnlyList<IStorageItem> items = await e.DataView.GetStorageItemsAsync();
foreach (string path in items.OfType<StorageFile>()
.Select(file => file.Path)
.Where(path => string.Equals(Path.GetExtension(path), ".avi", StringComparison.OrdinalIgnoreCase))
.OrderBy(NaturalKey))
if (!_inputs.Contains(path, StringComparer.OrdinalIgnoreCase)) _inputs.Add(path);
}
private async void BrowseEndCard_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
picker.FileTypeFilter.Add(".png");
picker.FileTypeFilter.Add(".jpg");
picker.FileTypeFilter.Add(".jpeg");
picker.FileTypeFilter.Add(".webp");
picker.FileTypeFilter.Add(".bmp");
var file = await picker.PickSingleFileAsync();
if (file is not null)
EndCardBox.Text = file.Path;
}
private async void BrowseOutput_Click(object sender, RoutedEventArgs e)
{
FolderPicker picker = CreateFolderPicker();
var folder = await picker.PickSingleFolderAsync();
if (folder is not null)
OutputFolderBox.Text = folder.Path;
}
private async void BrowseFfmpeg_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
picker.FileTypeFilter.Add(".exe");
var file = await picker.PickSingleFileAsync();
if (file is not null)
FfmpegPathBox.Text = file.Path;
}
private async void BrowseFfprobe_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
picker.FileTypeFilter.Add(".exe");
var file = await picker.PickSingleFileAsync();
if (file is not null)
FfprobePathBox.Text = file.Path;
}
private async void OpenSettings_Click(object sender, RoutedEventArgs e)
{
SettingsDialog.XamlRoot = XamlRoot;
await SettingsDialog.ShowAsync();
SaveSettings();
}
private async void Render_Click(object sender, RoutedEventArgs e)
{
try
{
RenderSettings settings = ReadSettings();
SaveSettings();
SetRendering(true);
LogBox.Text = string.Empty;
_renderCancellation = new CancellationTokenSource();
Progress<RenderProgress> progress = new(UpdateProgress);
IReadOnlyList<string> resolvedInputs = await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
ReplaceInputs(resolvedInputs);
_lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4");
OpenOutputButton.IsEnabled = true;
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
await ShowMessageAsync("Render complete", "The AmiReel render completed successfully.");
}
catch (OperationCanceledException)
{
UpdateProgress(new(RenderProgressBar.Value, "Cancelled", "The render was cancelled."));
}
catch (Exception exception)
{
AppendLog("ERROR: " + exception);
StageText.Text = "Failed";
await ShowMessageAsync("Render failed", UserFacingErrors.Summarize(exception));
}
finally
{
_renderCancellation?.Dispose();
_renderCancellation = null;
SetRendering(false);
}
}
private void Cancel_Click(object sender, RoutedEventArgs e)
{
CancelButton.IsEnabled = false;
StatusText.Text = "Stopping FFmpeg...";
_renderCancellation?.Cancel();
}
private void OpenOutput_Click(object sender, RoutedEventArgs e)
{
if (Directory.Exists(OutputFolderBox.Text))
Process.Start(new ProcessStartInfo("explorer.exe", OutputFolderBox.Text) { UseShellExecute = true });
}
private void PreviewSource_Click(object sender, RoutedEventArgs e)
{
if (InputList.SelectedItem is string path)
OpenPreview(path);
}
private void PreviewRendered_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(_lastRenderedFile) && File.Exists(_lastRenderedFile))
OpenPreview(_lastRenderedFile);
}
private void ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!IsLoaded) return;
string theme = SelectedTheme();
ApplyTheme(theme);
SaveSettings();
}
private RenderSettings ReadSettings()
{
static double Number(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;
}
if (!int.TryParse(IntervalBox.Text, out int interval) || interval < 1)
throw new ArgumentException("Thumbnail interval must be at least one second.");
EncoderMode encoder = Enum.Parse<EncoderMode>(SelectedEncoder());
return new(
_inputs.ToList(),
EndCardBox.Text.Trim(),
OutputFolderBox.Text.Trim(),
OutputNameBox.Text.Trim(),
FfmpegPathBox.Text.Trim(),
FfprobePathBox.Text.Trim(),
encoder,
Number(TrimBox.Text, "trim start"),
Number(FadeBox.Text, "fade"),
Number(HoldBox.Text, "end-card hold"),
interval,
MoveSourcesBox.IsChecked == true);
}
private void ReplaceInputs(IReadOnlyList<string> paths)
{
_inputs.Clear();
foreach (string path in paths)
_inputs.Add(path);
}
private void SetRendering(bool rendering)
{
RenderButton.IsEnabled = !rendering;
CancelButton.IsEnabled = rendering;
OpenSettingsButton.IsEnabled = !rendering;
}
private void UpdateProgress(RenderProgress value)
{
RenderProgressBar.Value = value.Percent;
PercentText.Text = $"{value.Percent:0}%";
StageText.Text = value.Stage;
StatusText.Text = value.Message;
}
private void OpenPreview(string mediaPath)
{
if (!File.Exists(mediaPath))
return;
Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true });
}
private void AppendLog(string line)
{
lock (_logLock)
{
_pendingLog.AppendLine(line);
if (_logFlushScheduled)
return;
_logFlushScheduled = true;
}
_ = DispatcherQueue.TryEnqueue(() =>
{
string chunk;
lock (_logLock)
{
chunk = _pendingLog.ToString();
_pendingLog.Clear();
_logFlushScheduled = false;
}
if (chunk.Length == 0)
return;
LogBox.Text += chunk;
LogBox.Select(LogBox.Text.Length, 0);
});
}
private void SaveSettings()
{
AppSettingsStore.Save(new AppSettings(
OutputFolderBox.Text.Trim(),
EndCardBox.Text.Trim(),
FfmpegPathBox.Text.Trim(),
FfprobePathBox.Text.Trim(),
_loadedSettings.PreviewPlayerPath,
SelectedTheme(),
SelectedEncoder(),
TrimBox.Text.Trim(),
FadeBox.Text.Trim(),
HoldBox.Text.Trim(),
IntervalBox.Text.Trim(),
MoveSourcesBox.IsChecked == true));
}
private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark";
private string SelectedEncoder() => (EncoderBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Auto";
private void SelectTheme(string theme)
{
foreach (ComboBoxItem item in ThemeBox.Items)
{
if (string.Equals(item.Tag?.ToString(), theme, StringComparison.OrdinalIgnoreCase))
{
ThemeBox.SelectedItem = item;
return;
}
}
ThemeBox.SelectedIndex = 0;
}
private void SelectEncoder(string encoder)
{
foreach (ComboBoxItem item in EncoderBox.Items)
{
if (string.Equals(item.Tag?.ToString(), encoder, StringComparison.OrdinalIgnoreCase))
{
EncoderBox.SelectedItem = item;
return;
}
}
EncoderBox.SelectedIndex = 0;
}
private void InputList_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
PreviewSourceButton.IsEnabled = InputList.SelectedItem is string;
}
private void ApplyTheme(string theme)
{
RequestedTheme = string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase)
? ElementTheme.Light
: ElementTheme.Dark;
}
private FileOpenPicker CreateFileOpenPicker()
{
FileOpenPicker picker = new();
InitializeWithWindow.Initialize(picker, App.MainWindowHandle);
return picker;
}
private FolderPicker CreateFolderPicker()
{
FolderPicker picker = new();
picker.FileTypeFilter.Add("*");
InitializeWithWindow.Initialize(picker, App.MainWindowHandle);
return picker;
}
private static string NaturalKey(string path)
{
string name = Path.GetFileNameWithoutExtension(path);
int underscore = name.LastIndexOf('_');
return underscore >= 0 && int.TryParse(name[(underscore + 1)..], out int number)
? name[..underscore] + number.ToString("D10")
: name;
}
private async Task ShowMessageAsync(string title, string message)
{
ContentDialog dialog = new()
{
Title = title,
Content = message,
CloseButtonText = "OK",
XamlRoot = XamlRoot
};
await dialog.ShowAsync();
}
}