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; private bool _isShortsTab; private bool _isRendering; private bool _keepRenderOutput; private List _wizardSteps = [.. ShortWizard.StepsFor(ShortStyle.Brand)]; private int _wizardIndex; 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; ShortHookBox.Text = _loadedSettings.ShortsHook; 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); SelectComboTag(ShortCropModeBox, _loadedSettings.ShortsSourceCropMode); RefreshWizardSteps(); ShowWizardStep(); } private void VideoTab_Click(object sender, RoutedEventArgs e) => SetActiveTab(shorts: false); private void ShortsTab_Click(object sender, RoutedEventArgs e) => SetActiveTab(shorts: true); private void SetActiveTab(bool shorts) { if (!_isRendering) _keepRenderOutput = false; _isShortsTab = shorts; Style primaryButtonStyle = (Style)Application.Current.Resources["PrimaryButtonStyle"]; 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) { 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(); 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 { _keepRenderOutput = false; SetRendering(true); LogBox.Text = string.Empty; _renderCancellation = new CancellationTokenSource(); if (_isShortsTab) await RenderShortAsync(_renderCancellation.Token); else await RenderVideoAsync(_renderCancellation.Token); } 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)); } finally { _renderCancellation?.Dispose(); _renderCancellation = null; SetRendering(false); } } private async Task RenderVideoAsync(CancellationToken token) { RenderSettings settings = ReadSettings(); SaveSettings(); Progress progress = new(UpdateProgress); IReadOnlyList resolvedInputs = await new RenderPipeline().RenderAsync(settings, progress, AppendLog, 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."); } private async Task RenderShortAsync(CancellationToken token) { ShortSettings settings = ReadShortSettings(); SaveSettings(); Progress progress = new(UpdateProgress); await new ShortsPipeline().RenderAsync(settings, progress, AppendLog, token); _lastRenderedFile = settings.OutputPath; ShortOutputBox.Text = settings.OutputPath; OpenOutputButton.IsEnabled = true; PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile); await ShowMessageAsync("Short complete", "The YouTube Short was rendered successfully."); } 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) { string? directory = !string.IsNullOrWhiteSpace(_lastRenderedFile) ? Path.GetDirectoryName(_lastRenderedFile) : OutputFolderBox.Text; if (!string.IsNullOrWhiteSpace(directory) && Directory.Exists(directory)) Process.Start(new ProcessStartInfo("explorer.exe", directory) { 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 RenderSettings { InputFiles = _inputs.ToList(), EndCardPath = EndCardBox.Text.Trim(), OutputDirectory = OutputFolderBox.Text.Trim(), OutputName = OutputNameBox.Text.Trim(), FfmpegPath = FfmpegPathBox.Text.Trim(), FfprobePath = FfprobePathBox.Text.Trim(), Encoder = encoder, TrimStart = Number(TrimBox.Text, "trim start"), FadeSeconds = Number(FadeBox.Text, "fade"), EndCardHoldSeconds = Number(HoldBox.Text, "end-card hold"), ThumbnailInterval = interval, MoveSourcesToOriginals = MoveSourcesBox.IsChecked == true, }; } private void ReplaceInputs(IReadOnlyList paths) { _inputs.Clear(); foreach (string path in paths) _inputs.Add(path); } private ShortSettings ReadShortSettings() { 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 = SelectedShortStyleEnum(); if (string.IsNullOrWhiteSpace(output) && !string.IsNullOrWhiteSpace(input)) { string baseName = Path.GetFileNameWithoutExtension(input); string? directory = Path.GetDirectoryName(input); output = Path.Combine(directory ?? "", $"{baseName}-{style.ToString().ToLowerInvariant()}-short.mp4"); } return new ShortSettings { InputFile = input, OutputPath = output, Title = ShortTitleBox.Text.Trim(), Group = ShortGroupBox.Text.Trim(), Year = ShortYearBox.Text.Trim(), Type = ShortTypeBox.Text.Trim(), Hook = ShortHookBox.Text.Trim(), Website = ShortWebsiteBox.Text.Trim(), Start = string.IsNullOrWhiteSpace(ShortStartBox.Text) ? "00:00:00" : ShortStartBox.Text.Trim(), 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(), Crf = crf, Preset = SelectedShortPreset(), }; } 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(); foreach (string extension in SupportedVideoFormats.Extensions) picker.FileTypeFilter.Add(extension); picker.SuggestedStartLocation = PickerLocationId.VideosLibrary; var file = await picker.PickSingleFileAsync(); if (file is not null) ShortInputBox.Text = file.Path; } private async void BrowseShortOutput_Click(object sender, RoutedEventArgs e) { FileSavePicker picker = new(); picker.FileTypeChoices.Add("MP4 video", [".mp4"]); picker.SuggestedFileName = "short.mp4"; InitializeWithWindow.Initialize(picker, App.MainWindowHandle); var file = await picker.PickSaveFileAsync(); if (file is not null) ShortOutputBox.Text = file.Path; } private async void BrowseShortBackground_Click(object sender, RoutedEventArgs e) { FileOpenPicker picker = CreateFileOpenPicker(); picker.FileTypeFilter.Add(".png"); picker.FileTypeFilter.Add(".jpg"); picker.FileTypeFilter.Add(".jpeg"); var file = await picker.PickSingleFileAsync(); if (file is not null) 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(); picker.FileTypeFilter.Add(".ttf"); picker.FileTypeFilter.Add(".otf"); var file = await picker.PickSingleFileAsync(); if (file is not null) ShortFontBox.Text = file.Path; } private async void PreviewShortSource_Click(object sender, RoutedEventArgs e) { if (!string.IsNullOrWhiteSpace(ShortInputBox.Text)) await OpenPreviewAsync(ShortInputBox.Text.Trim()); } 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) { 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 { OutputFolder = OutputFolderBox.Text.Trim(), EndCardPath = EndCardBox.Text.Trim(), FfmpegPath = FfmpegPathBox.Text.Trim(), FfprobePath = FfprobePathBox.Text.Trim(), PreviewPlayerPath = PreviewPlayerPathBox.Text.Trim(), Theme = SelectedTheme(), Encoder = SelectedEncoder(), TrimStart = TrimBox.Text.Trim(), FadeSeconds = FadeBox.Text.Trim(), EndCardHoldSeconds = HoldBox.Text.Trim(), ThumbnailInterval = IntervalBox.Text.Trim(), MoveSourcesToOriginals = MoveSourcesBox.IsChecked == true, ShortsStyle = SelectedShortStyle(), ShortsHook = ShortHookBox.Text.Trim(), 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(), }); } private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark"; private string SelectedEncoder() => (EncoderBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Auto"; 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); private void SelectEncoder(string encoder) => SelectComboTag(EncoderBox, encoder); private static void SelectComboTag(ComboBox comboBox, string tag) { foreach (ComboBoxItem item in comboBox.Items.Cast()) { if (string.Equals(item.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase)) { comboBox.SelectedItem = item; return; } } comboBox.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(); } }