using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Text; using AmiReel.Models; using AmiReel.Services; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Windows.ApplicationModel.DataTransfer; using Windows.Storage; using Windows.Storage.Pickers; using WinRT.Interop; namespace AmiReel; public sealed partial class MainPage : Page { private readonly ObservableCollection _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; PreviewPlayerPathBox.Text = _loadedSettings.PreviewPlayerPath; 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(); foreach (string extension in SupportedVideoFormats.Extensions) picker.FileTypeFilter.Add(extension); 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; DragOperationDeferral deferral = e.GetDeferral(); try { IReadOnlyList items = await e.DataView.GetStorageItemsAsync(); foreach (string path in items.OfType() .Select(file => file.Path) .Where(SupportedVideoFormats.IsSupported) .OrderBy(NaturalKey)) if (!_inputs.Contains(path, StringComparer.OrdinalIgnoreCase)) _inputs.Add(path); } finally { deferral.Complete(); } } 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 BrowsePreviewPlayer_Click(object sender, RoutedEventArgs e) { FileOpenPicker picker = CreateFileOpenPicker(); picker.FileTypeFilter.Add(".exe"); var file = await picker.PickSingleFileAsync(); if (file is not null) PreviewPlayerPathBox.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 progress = new(UpdateProgress); IReadOnlyList 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 async void PreviewSource_Click(object sender, RoutedEventArgs e) { if (InputList.SelectedItem is string path) await OpenPreviewAsync(path); } private async void PreviewRendered_Click(object sender, RoutedEventArgs e) { if (!string.IsNullOrWhiteSpace(_lastRenderedFile) && File.Exists(_lastRenderedFile)) await OpenPreviewAsync(_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(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 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 async Task OpenPreviewAsync(string mediaPath) { if (!File.Exists(mediaPath)) { await ShowMessageAsync("Preview unavailable", "The selected media file was not found."); return; } string playerPath = PreviewPlayerPathBox.Text.Trim(); try { if (!string.IsNullOrWhiteSpace(playerPath)) { if (!File.Exists(playerPath)) throw new FileNotFoundException("Configured preview player was not found.", playerPath); ProcessStartInfo customPlayer = new() { FileName = playerPath, UseShellExecute = false }; customPlayer.ArgumentList.Add(mediaPath); Process.Start(customPlayer); return; } Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true }); } catch (Exception exception) { await ShowMessageAsync("Preview failed", exception.Message); } } 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(), PreviewPlayerPathBox.Text.Trim(), 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) { bool isDark = !string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase); App.MainWindowInstance?.ApplyTheme(isDark); } 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 = DialogHelper.CreateStyled(XamlRoot, ActualTheme); dialog.Title = title; dialog.Content = message; dialog.CloseButtonText = "OK"; await dialog.ShowAsync(); } }