346 lines
11 KiB
C#
346 lines
11 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.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);
|
|
}
|
|
|
|
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 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);
|
|
await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
|
|
_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);
|
|
}
|
|
|
|
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()));
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|