diff --git a/AmiReel.WinUI/.github/instructions/accessibility.instructions.md b/.github/instructions/accessibility.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/accessibility.instructions.md
rename to .github/instructions/accessibility.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/code-quality.instructions.md b/.github/instructions/code-quality.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/code-quality.instructions.md
rename to .github/instructions/code-quality.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/design-principles.instructions.md b/.github/instructions/design-principles.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/design-principles.instructions.md
rename to .github/instructions/design-principles.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/globalization.instructions.md b/.github/instructions/globalization.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/globalization.instructions.md
rename to .github/instructions/globalization.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/performance.instructions.md b/.github/instructions/performance.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/performance.instructions.md
rename to .github/instructions/performance.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/security.instructions.md b/.github/instructions/security.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/security.instructions.md
rename to .github/instructions/security.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/testing.instructions.md b/.github/instructions/testing.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/testing.instructions.md
rename to .github/instructions/testing.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/windows-apis.instructions.md b/.github/instructions/windows-apis.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/windows-apis.instructions.md
rename to .github/instructions/windows-apis.instructions.md
diff --git a/AmiReel.WinUI/.github/instructions/winui-best-practices.instructions.md b/.github/instructions/winui-best-practices.instructions.md
similarity index 100%
rename from AmiReel.WinUI/.github/instructions/winui-best-practices.instructions.md
rename to .github/instructions/winui-best-practices.instructions.md
diff --git a/.gitignore b/.gitignore
index 4d53f18..cd47807 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,14 +1,55 @@
-bin/
-obj/
+# Build output
+[Bb]in/
+[Oo]bj/
+[Dd]ebug/
+[Rr]elease/
+publish/
+
+# IDE / tooling
.vs/
.vscode/
+_ReSharper*/
+*.DotSettings.user
+*.dotCover
+.idea/
+
+# MSBuild / user files
+*.user
+*.suo
+*.userosscache
+*.sln.docstates
*.pdb
*.cache
*.tmp
*.log
-*.userosscache
+*.binlog
+
+# Test results
TestResults/
+[Tt]est[Rr]esult*/
+*.trx
+*.coverage
+*.coveragexml
+
+# NuGet
+*.nupkg
+*.snupkg
+*.nuget.props
+*.nuget.targets
+project.lock.json
+
+# MSIX packaging output
+AppPackages/
+BundleArtifacts/
+*.msix
+*.msixupload
+*.appx
+*.appxbundle
+*.appxupload
+
+# Local dev signing certificate
+devcert.pfx
+
+# Embedded third-party binaries (fetched separately, see ThirdParty/README.md)
ThirdParty/ffmpeg.exe
ThirdParty/ffprobe.exe
-*.user
-*.suo
diff --git a/AmiReel.WinUI/AGENTS.md b/AGENTS.md
similarity index 100%
rename from AmiReel.WinUI/AGENTS.md
rename to AGENTS.md
diff --git a/AmiReel.WinUI/.gitignore b/AmiReel.WinUI/.gitignore
deleted file mode 100644
index dd39356..0000000
--- a/AmiReel.WinUI/.gitignore
+++ /dev/null
@@ -1,54 +0,0 @@
-## .NET / Visual Studio
-[Bb]in/
-[Oo]bj/
-[Dd]ebug/
-[Rr]elease/
-.vs/
-*.user
-*.suo
-*.userosscache
-*.sln.docstates
-artifacts/
-
-# Build logs
-[Ll]og/
-[Ll]ogs/
-*.log
-*.binlog
-
-# Test results
-[Tt]est[Rr]esult*/
-*.trx
-*.coverage
-*.coveragexml
-
-# NuGet
-*.nupkg
-*.snupkg
-*.nuget.props
-*.nuget.targets
-project.lock.json
-
-# MSIX packaging output
-AppPackages/
-BundleArtifacts/
-*.msix
-*.msixupload
-*.appx
-*.appxbundle
-*.appxupload
-
-# Publish output
-publish/
-*.pubxml
-PublishScripts/
-
-# Code analysis and tooling
-_ReSharper*/
-*.DotSettings.user
-*.dotCover
-.idea/
-
-# Generated files
-Generated\ Files/
-*_wpftmp.csproj
diff --git a/AmiReel.WinUI/App.xaml b/AmiReel.WinUI/App.xaml
deleted file mode 100644
index cc8b137..0000000
--- a/AmiReel.WinUI/App.xaml
+++ /dev/null
@@ -1,269 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 10
- 18
-
- 460
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/AmiReel.WinUI/App.xaml.cs b/AmiReel.WinUI/App.xaml.cs
deleted file mode 100644
index 7e911c5..0000000
--- a/AmiReel.WinUI/App.xaml.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-using Windows.ApplicationModel;
-using Windows.ApplicationModel.Activation;
-using Windows.Foundation;
-using Windows.Foundation.Collections;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using Microsoft.UI.Xaml.Controls.Primitives;
-using Microsoft.UI.Xaml.Data;
-using Microsoft.UI.Xaml.Input;
-using Microsoft.UI.Xaml.Media;
-using Microsoft.UI.Xaml.Navigation;
-using Microsoft.UI.Xaml.Shapes;
-using WinRT.Interop;
-
-// To learn more about WinUI, the WinUI project structure,
-// and more about our project templates, see: http://aka.ms/winui-project-info.
-
-namespace AmiReel_WinUI;
-
-///
-/// Provides application-specific behavior to supplement the default Application class.
-///
-public partial class App : Application
-{
- private Window? _window;
- public static IntPtr MainWindowHandle { get; private set; }
- public static MainWindow? MainWindowInstance { get; set; }
-
- ///
- /// Initializes the singleton application object. This is the first line of authored code
- /// executed, and as such is the logical equivalent of main() or WinMain().
- ///
- public App()
- {
- InitializeComponent();
- }
-
- ///
- /// Invoked when the application is launched.
- ///
- /// Details about the launch request and process.
- protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
- {
- _window = new MainWindow();
- MainWindowHandle = WindowNative.GetWindowHandle(_window);
- _window.Activate();
- }
-}
diff --git a/AmiReel.WinUI/MainWindow.xaml b/AmiReel.WinUI/MainWindow.xaml
deleted file mode 100644
index 247e84c..0000000
--- a/AmiReel.WinUI/MainWindow.xaml
+++ /dev/null
@@ -1,38 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/AmiReel.WinUI/MainWindow.xaml.cs b/AmiReel.WinUI/MainWindow.xaml.cs
deleted file mode 100644
index 5155340..0000000
--- a/AmiReel.WinUI/MainWindow.xaml.cs
+++ /dev/null
@@ -1,116 +0,0 @@
-using Microsoft.UI;
-using Microsoft.UI.Windowing;
-using Microsoft.UI.Xaml;
-using Microsoft.UI.Xaml.Controls;
-using System;
-using System.IO;
-using System.Runtime.InteropServices;
-using Windows.Graphics;
-using Windows.UI;
-using WinRT.Interop;
-
-// To learn more about WinUI, the WinUI project structure,
-// and more about our project templates, see: http://aka.ms/winui-project-info.
-
-namespace AmiReel_WinUI;
-
-///
-/// The application window. This hosts a Frame that displays pages. Add your
-/// UI and logic to MainPage.xaml / MainPage.xaml.cs instead of here so you
-/// can use Page features such as navigation events and the Loaded lifecycle.
-///
-public sealed partial class MainWindow : Window
-{
- private bool _exitConfirmed;
-
- public MainWindow()
- {
- InitializeComponent();
- App.MainWindowInstance = this;
-
- ExtendsContentIntoTitleBar = true;
- SetTitleBar(AppTitleBar);
-
- string? executablePath = Environment.ProcessPath;
- if (!string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath))
- AppWindow.SetIcon(executablePath);
-
- SetDefaultSizeAndPosition();
-
- // Navigate the root frame to the main page on startup.
- RootFrame.Navigate(typeof(MainPage));
-
- AppWindow.Closing += AppWindow_Closing;
- }
-
- private void SetDefaultSizeAndPosition()
- {
- const int DefaultWidth = 1280;
- const int DefaultHeight = 860;
- const int MinWidth = 1100;
- const int MinHeight = 780;
-
- IntPtr hwnd = WindowNative.GetWindowHandle(this);
- double scale = GetDpiForWindow(hwnd) / 96.0;
- int width = (int)(DefaultWidth * scale);
- int height = (int)(DefaultHeight * scale);
-
- AppWindow.Resize(new SizeInt32(width, height));
-
- if (AppWindow.Presenter is OverlappedPresenter presenter)
- {
- presenter.PreferredMinimumWidth = (int)(MinWidth * scale);
- presenter.PreferredMinimumHeight = (int)(MinHeight * scale);
- }
-
- DisplayArea? displayArea = DisplayArea.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd), DisplayAreaFallback.Primary);
- if (displayArea is not null)
- {
- int centerX = displayArea.WorkArea.X + (displayArea.WorkArea.Width - width) / 2;
- int centerY = displayArea.WorkArea.Y + (displayArea.WorkArea.Height - height) / 2;
- AppWindow.Move(new PointInt32(centerX, centerY));
- }
- }
-
- [DllImport("user32.dll")]
- private static extern int GetDpiForWindow(IntPtr hwnd);
-
- public void ApplyTheme(bool isDark)
- {
- if (Content is FrameworkElement root)
- root.RequestedTheme = isDark ? ElementTheme.Dark : ElementTheme.Light;
-
- AppWindowTitleBar titleBar = AppWindow.TitleBar;
- Color foreground = isDark ? Color.FromArgb(255, 0xF5, 0xF7, 0xFB) : Color.FromArgb(255, 0x12, 0x20, 0x33);
- Color hoverBackground = isDark ? Color.FromArgb(255, 0x2E, 0x44, 0x6C) : Color.FromArgb(255, 0xD7, 0xE4, 0xF4);
-
- titleBar.ButtonBackgroundColor = Colors.Transparent;
- titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
- titleBar.ButtonForegroundColor = foreground;
- titleBar.ButtonInactiveForegroundColor = foreground;
- titleBar.ButtonHoverBackgroundColor = hoverBackground;
- titleBar.ButtonHoverForegroundColor = foreground;
- titleBar.ButtonPressedBackgroundColor = hoverBackground;
- titleBar.ButtonPressedForegroundColor = foreground;
- }
-
- private async void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
- {
- if (_exitConfirmed) return;
- args.Cancel = true;
-
- FrameworkElement root = (FrameworkElement)Content;
- ContentDialog dialog = DialogHelper.CreateStyled(RootFrame.XamlRoot, root.RequestedTheme);
- dialog.Title = "Exit AmiReel?";
- dialog.Content = "Are you sure you want to close the application?";
- dialog.PrimaryButtonText = "Exit";
- dialog.CloseButtonText = "Cancel";
- dialog.DefaultButton = ContentDialogButton.Primary;
-
- ContentDialogResult result = await dialog.ShowAsync();
- if (result != ContentDialogResult.Primary) return;
-
- _exitConfirmed = true;
- Close();
- }
-}
diff --git a/AmiReel.WinUI/AmiReel.WinUI.csproj b/AmiReel.csproj
similarity index 85%
rename from AmiReel.WinUI/AmiReel.WinUI.csproj
rename to AmiReel.csproj
index 509b078..bd370c0 100644
--- a/AmiReel.WinUI/AmiReel.WinUI.csproj
+++ b/AmiReel.csproj
@@ -3,9 +3,9 @@
WinExe
net10.0-windows10.0.26100.0
10.0.17763.0
- AmiReel_WinUI
+ AmiReel
AmiReel
- ..\assets\AmiReel.ico
+ branding\AmiReel.ico
app.manifest
x86;x64;ARM64
win-$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant())
@@ -36,13 +36,8 @@
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
-
+
+
+
+
+
+
-
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+ 18
+
+ 460
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/App.xaml.cs b/App.xaml.cs
index 2eb3309..c28bd70 100644
--- a/App.xaml.cs
+++ b/App.xaml.cs
@@ -1,5 +1,48 @@
-namespace AmigaDB.VideoRenderer;
+using Windows.ApplicationModel;
+using Windows.ApplicationModel.Activation;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+using Microsoft.UI.Xaml.Shapes;
+using WinRT.Interop;
-public partial class App : System.Windows.Application
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace AmiReel;
+
+///
+/// Provides application-specific behavior to supplement the default Application class.
+///
+public partial class App : Application
{
+ private Window? _window;
+ public static IntPtr MainWindowHandle { get; private set; }
+ public static MainWindow? MainWindowInstance { get; set; }
+
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ InitializeComponent();
+ }
+
+ ///
+ /// Invoked when the application is launched.
+ ///
+ /// Details about the launch request and process.
+ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
+ {
+ _window = new MainWindow();
+ MainWindowHandle = WindowNative.GetWindowHandle(_window);
+ _window.Activate();
+ }
}
diff --git a/AmiReel.WinUI/Assets/AppIcon.ico b/Assets/AppIcon.ico
similarity index 100%
rename from AmiReel.WinUI/Assets/AppIcon.ico
rename to Assets/AppIcon.ico
diff --git a/AmiReel.WinUI/Assets/LockScreenLogo.scale-200.png b/Assets/LockScreenLogo.scale-200.png
similarity index 100%
rename from AmiReel.WinUI/Assets/LockScreenLogo.scale-200.png
rename to Assets/LockScreenLogo.scale-200.png
diff --git a/AmiReel.WinUI/Assets/SplashScreen.scale-200.png b/Assets/SplashScreen.scale-200.png
similarity index 100%
rename from AmiReel.WinUI/Assets/SplashScreen.scale-200.png
rename to Assets/SplashScreen.scale-200.png
diff --git a/AmiReel.WinUI/Assets/Square150x150Logo.scale-200.png b/Assets/Square150x150Logo.scale-200.png
similarity index 100%
rename from AmiReel.WinUI/Assets/Square150x150Logo.scale-200.png
rename to Assets/Square150x150Logo.scale-200.png
diff --git a/AmiReel.WinUI/Assets/Square44x44Logo.scale-200.png b/Assets/Square44x44Logo.scale-200.png
similarity index 100%
rename from AmiReel.WinUI/Assets/Square44x44Logo.scale-200.png
rename to Assets/Square44x44Logo.scale-200.png
diff --git a/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
similarity index 100%
rename from AmiReel.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
rename to Assets/Square44x44Logo.targetsize-24_altform-unplated.png
diff --git a/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png b/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png
similarity index 100%
rename from AmiReel.WinUI/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png
rename to Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png
diff --git a/AmiReel.WinUI/Assets/StoreLogo.png b/Assets/StoreLogo.png
similarity index 100%
rename from AmiReel.WinUI/Assets/StoreLogo.png
rename to Assets/StoreLogo.png
diff --git a/AmiReel.WinUI/Assets/Wide310x150Logo.scale-200.png b/Assets/Wide310x150Logo.scale-200.png
similarity index 100%
rename from AmiReel.WinUI/Assets/Wide310x150Logo.scale-200.png
rename to Assets/Wide310x150Logo.scale-200.png
diff --git a/AmiReel.WinUI/DialogHelper.cs b/DialogHelper.cs
similarity index 98%
rename from AmiReel.WinUI/DialogHelper.cs
rename to DialogHelper.cs
index ae9dcab..54b2cab 100644
--- a/AmiReel.WinUI/DialogHelper.cs
+++ b/DialogHelper.cs
@@ -2,7 +2,7 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
-namespace AmiReel_WinUI;
+namespace AmiReel;
///
/// Builds ContentDialogs styled to match the app's own palette and button shapes,
diff --git a/AmiReel.WinUI/MainPage.xaml b/MainPage.xaml
similarity index 99%
rename from AmiReel.WinUI/MainPage.xaml
rename to MainPage.xaml
index 2922a3c..85fd395 100644
--- a/AmiReel.WinUI/MainPage.xaml
+++ b/MainPage.xaml
@@ -1,6 +1,6 @@
-
-
-
-
+
+
+
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 770a8be..d9e69b8 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -1,425 +1,116 @@
-using System.Collections.ObjectModel;
-using System.Diagnostics;
-using System.Globalization;
+using Microsoft.UI;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using System;
using System.IO;
using System.Runtime.InteropServices;
-using System.Text;
-using System.Windows;
-using System.Windows.Controls;
-using System.Windows.Interop;
-using AmigaDB.VideoRenderer.Models;
-using AmigaDB.VideoRenderer.Services;
-using Microsoft.Win32;
-using System.Windows.Media;
+using Windows.Graphics;
+using Windows.UI;
+using WinRT.Interop;
-namespace AmigaDB.VideoRenderer;
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
-public partial class MainWindow : Window
+namespace AmiReel;
+
+///
+/// The application window. This hosts a Frame that displays pages. Add your
+/// UI and logic to MainPage.xaml / MainPage.xaml.cs instead of here so you
+/// can use Page features such as navigation events and the Loaded lifecycle.
+///
+public sealed partial class MainWindow : Window
{
- 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 _titleBarIsLight;
private bool _exitConfirmed;
public MainWindow()
{
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;
- 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;
- Closing += MainWindow_Closing;
- SourceInitialized += (_, _) => UpdateTitleBarTheme(_titleBarIsLight);
- StateChanged += (_, _) => MaximizeButton.Content = WindowState == WindowState.Maximized ? "\uE923" : "\uE922";
+ App.MainWindowInstance = this;
+
+ ExtendsContentIntoTitleBar = true;
+ SetTitleBar(AppTitleBar);
+
+ string? executablePath = Environment.ProcessPath;
+ if (!string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath))
+ AppWindow.SetIcon(executablePath);
+
+ SetDefaultSizeAndPosition();
+
+ // Navigate the root frame to the main page on startup.
+ RootFrame.Navigate(typeof(MainPage));
+
+ AppWindow.Closing += AppWindow_Closing;
}
- private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
+ private void SetDefaultSizeAndPosition()
+ {
+ const int DefaultWidth = 1280;
+ const int DefaultHeight = 860;
+ const int MinWidth = 1100;
+ const int MinHeight = 780;
- private void MaximizeRestore_Click(object sender, RoutedEventArgs e)
- => WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
+ IntPtr hwnd = WindowNative.GetWindowHandle(this);
+ double scale = GetDpiForWindow(hwnd) / 96.0;
+ int width = (int)(DefaultWidth * scale);
+ int height = (int)(DefaultHeight * scale);
- private void CloseWindowButton_Click(object sender, RoutedEventArgs e) => Close();
+ AppWindow.Resize(new SizeInt32(width, height));
- private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
+ if (AppWindow.Presenter is OverlappedPresenter presenter)
+ {
+ presenter.PreferredMinimumWidth = (int)(MinWidth * scale);
+ presenter.PreferredMinimumHeight = (int)(MinHeight * scale);
+ }
+
+ DisplayArea? displayArea = DisplayArea.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd), DisplayAreaFallback.Primary);
+ if (displayArea is not null)
+ {
+ int centerX = displayArea.WorkArea.X + (displayArea.WorkArea.Width - width) / 2;
+ int centerY = displayArea.WorkArea.Y + (displayArea.WorkArea.Height - height) / 2;
+ AppWindow.Move(new PointInt32(centerX, centerY));
+ }
+ }
+
+ [DllImport("user32.dll")]
+ private static extern int GetDpiForWindow(IntPtr hwnd);
+
+ public void ApplyTheme(bool isDark)
+ {
+ if (Content is FrameworkElement root)
+ root.RequestedTheme = isDark ? ElementTheme.Dark : ElementTheme.Light;
+
+ AppWindowTitleBar titleBar = AppWindow.TitleBar;
+ Color foreground = isDark ? Color.FromArgb(255, 0xF5, 0xF7, 0xFB) : Color.FromArgb(255, 0x12, 0x20, 0x33);
+ Color hoverBackground = isDark ? Color.FromArgb(255, 0x2E, 0x44, 0x6C) : Color.FromArgb(255, 0xD7, 0xE4, 0xF4);
+
+ titleBar.ButtonBackgroundColor = Colors.Transparent;
+ titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
+ titleBar.ButtonForegroundColor = foreground;
+ titleBar.ButtonInactiveForegroundColor = foreground;
+ titleBar.ButtonHoverBackgroundColor = hoverBackground;
+ titleBar.ButtonHoverForegroundColor = foreground;
+ titleBar.ButtonPressedBackgroundColor = hoverBackground;
+ titleBar.ButtonPressedForegroundColor = foreground;
+ }
+
+ private async void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
if (_exitConfirmed) return;
- e.Cancel = true;
- ExitConfirmOverlay.Visibility = Visibility.Visible;
- }
+ args.Cancel = true;
- private void CancelExit_Click(object sender, RoutedEventArgs e) => ExitConfirmOverlay.Visibility = Visibility.Collapsed;
+ FrameworkElement root = (FrameworkElement)Content;
+ ContentDialog dialog = DialogHelper.CreateStyled(RootFrame.XamlRoot, root.RequestedTheme);
+ dialog.Title = "Exit AmiReel?";
+ dialog.Content = "Are you sure you want to close the application?";
+ dialog.PrimaryButtonText = "Exit";
+ dialog.CloseButtonText = "Cancel";
+ dialog.DefaultButton = ContentDialogButton.Primary;
+
+ ContentDialogResult result = await dialog.ShowAsync();
+ if (result != ContentDialogResult.Primary) return;
- private void ConfirmExit_Click(object sender, RoutedEventArgs e)
- {
_exitConfirmed = true;
- SaveSettings();
Close();
}
-
- private void InputList_DragEnter(object sender, DragEventArgs e)
- {
- e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
- e.Handled = true;
- }
-
- private void InputList_Drop(object sender, DragEventArgs e)
- {
- if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
- string[] files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
- foreach (string file in files.Where(Models.SupportedVideoFormats.IsSupported).OrderBy(NaturalKey))
- if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file);
- }
-
- private void AddInputs_Click(object sender, RoutedEventArgs e)
- {
- OpenFileDialog dialog = new() { Filter = $"Video files|{Models.SupportedVideoFormats.PickerFilter}", Multiselect = true };
- if (dialog.ShowDialog(this) != true) return;
- foreach (string file in dialog.FileNames.OrderBy(NaturalKey))
- if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file);
- }
-
- private void ClearInputs_Click(object sender, RoutedEventArgs e) => _inputs.Clear();
-
- private void BrowseEndCard_Click(object sender, RoutedEventArgs e)
- {
- OpenFileDialog dialog = new() { Filter = "Images|*.png;*.jpg;*.jpeg;*.webp;*.bmp|All files|*.*" };
- if (dialog.ShowDialog(this) == true) EndCardBox.Text = dialog.FileName;
- }
-
- private void BrowseOutput_Click(object sender, RoutedEventArgs e)
- {
- OpenFolderDialog dialog = new() { InitialDirectory = OutputFolderBox.Text };
- if (dialog.ShowDialog(this) == true) OutputFolderBox.Text = dialog.FolderName;
- }
-
- private void BrowseFfmpeg_Click(object sender, RoutedEventArgs e)
- {
- OpenFileDialog dialog = new() { Filter = "FFmpeg executable (ffmpeg.exe)|ffmpeg.exe|Executable files (*.exe)|*.exe|All files|*.*" };
- if (dialog.ShowDialog(this) == true) FfmpegPathBox.Text = dialog.FileName;
- }
-
- private void BrowseFfprobe_Click(object sender, RoutedEventArgs e)
- {
- OpenFileDialog dialog = new() { Filter = "FFprobe executable (ffprobe.exe)|ffprobe.exe|Executable files (*.exe)|*.exe|All files|*.*" };
- if (dialog.ShowDialog(this) == true) FfprobePathBox.Text = dialog.FileName;
- }
-
- private void BrowsePreviewPlayer_Click(object sender, RoutedEventArgs e)
- {
- OpenFileDialog dialog = new() { Filter = "Video player (*.exe)|*.exe|All files|*.*" };
- if (dialog.ShowDialog(this) == true) PreviewPlayerPathBox.Text = dialog.FileName;
- }
-
- private void OpenSettings_Click(object sender, RoutedEventArgs e) => SettingsOverlay.Visibility = Visibility.Visible;
-
- private void CloseSettings_Click(object sender, RoutedEventArgs e)
- {
- SettingsOverlay.Visibility = Visibility.Collapsed;
- SaveSettings();
- }
-
- private async void Render_Click(object sender, RoutedEventArgs e)
- {
- try
- {
- RenderSettings settings = ReadSettings();
- SaveSettings();
- SetRendering(true);
- LogBox.Clear();
- _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);
- MessageBox.Show(this, "The AmiReel render completed successfully.", "Render complete",
- MessageBoxButton.OK, MessageBoxImage.Information);
- }
- catch (OperationCanceledException)
- {
- UpdateProgress(new(RenderProgress.Value, "Cancelled", "The render was cancelled."));
- }
- catch (Exception exception)
- {
- AppendLog("ERROR: " + exception);
- MessageBox.Show(this, UserFacingErrors.Summarize(exception), "Render failed", MessageBoxButton.OK, MessageBoxImage.Error);
- StageText.Text = "Failed";
- }
- finally
- {
- _renderCancellation?.Dispose();
- _renderCancellation = null;
- SetRendering(false);
- }
- }
-
- 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(((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!);
- 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 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 InputList_SelectionChanged(object sender, SelectionChangedEventArgs e)
- => PreviewSourceButton.IsEnabled = InputList.SelectedItem is string;
-
- private void SetRendering(bool rendering)
- {
- RenderButton.IsEnabled = !rendering;
- CancelButton.IsEnabled = rendering;
- }
-
- private void UpdateProgress(RenderProgress value)
- {
- RenderProgress.Value = value.Percent;
- PercentText.Text = $"{value.Percent:0}%";
- StageText.Text = value.Stage;
- StatusText.Text = value.Message;
- }
-
- private void AppendLog(string line)
- {
- lock (_logLock)
- {
- _pendingLog.AppendLine(line);
- if (_logFlushScheduled)
- return;
-
- _logFlushScheduled = true;
- }
-
- _ = Dispatcher.BeginInvoke(() =>
- {
- string chunk;
- lock (_logLock)
- {
- chunk = _pendingLog.ToString();
- _pendingLog.Clear();
- _logFlushScheduled = false;
- }
-
- if (chunk.Length == 0)
- return;
-
- LogBox.AppendText(chunk);
- LogBox.ScrollToEnd();
- });
- }
-
- 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 void ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
- {
- if (!IsLoaded) return;
- string theme = SelectedTheme();
- ApplyTheme(theme);
- SaveSettings();
- }
-
- 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 void OpenPreview(string mediaPath)
- {
- if (!File.Exists(mediaPath))
- {
- MessageBox.Show(this, "The selected media file was not found.", "Preview unavailable",
- MessageBoxButton.OK, MessageBoxImage.Warning);
- 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)
- {
- MessageBox.Show(this, exception.Message, "Preview failed", MessageBoxButton.OK, MessageBoxImage.Error);
- }
- }
-
- private string SelectedTheme() => ((ComboBoxItem)ThemeBox.SelectedItem).Tag!.ToString()!;
- private string SelectedEncoder() => ((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!;
-
- 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 ApplyTheme(string theme)
- {
- bool light = string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase);
-
- SetBrush("PageBrush", light ? "#F4F7FB" : "#0E1525");
- SetBrush("PanelBrush", light ? "#FFFFFF" : "#162033");
- SetBrush("PanelAltBrush", light ? "#EEF4FB" : "#1A2740");
- SetBrush("FieldBrush", light ? "#F7FAFD" : "#1D2940");
- SetBrush("BorderBrush", light ? "#C7D3E3" : "#2B3A58");
- SetBrush("AccentBrush", light ? "#0E9AEF" : "#4AB8FF");
- SetBrush("AccentSoftBrush", light ? "#D6EEFF" : "#13324F");
- SetBrush("TextBrush", light ? "#122033" : "#F5F7FB");
- SetBrush("MutedBrush", light ? "#5F7390" : "#9FB1CC");
- SetBrush("ButtonBrush", light ? "#E5EDF7" : "#243554");
- SetBrush("ButtonHoverBrush", light ? "#D7E4F4" : "#2E446C");
- SetBrush("ButtonDisabledBrush", light ? "#EEF2F7" : "#2A3140");
- SetBrush("ButtonDisabledTextBrush", light ? "#8C99AB" : "#7E8BA1");
- SetBrush("PrimaryTextBrush", light ? "#FFFFFF" : "#07111D");
- SetBrush("SelectionBrush", light ? "#BEE4FF" : "#295C87");
- SetBrush("SelectionTextBrush", light ? "#122033" : "#FFFFFF");
-
- _titleBarIsLight = light;
- UpdateTitleBarTheme(light);
- }
-
- private void SetBrush(string key, string color)
- {
- Application.Current.Resources[key] = new SolidColorBrush((Color)ColorConverter.ConvertFromString(color));
- }
-
- private void UpdateTitleBarTheme(bool light)
- {
- IntPtr hwnd = new WindowInteropHelper(this).Handle;
- if (hwnd == IntPtr.Zero) return;
-
- int useImmersiveDarkMode = light ? 0 : 1;
- DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useImmersiveDarkMode, sizeof(int));
-
- int captionColor = ToColorRef(light ? "#F4F7FB" : "#0E1525");
- int textColor = ToColorRef(light ? "#122033" : "#F5F7FB");
- DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, ref captionColor, sizeof(int));
- DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, ref textColor, sizeof(int));
- }
-
- private static int ToColorRef(string hexColor)
- {
- Color color = (Color)ColorConverter.ConvertFromString(hexColor);
- return color.R | (color.G << 8) | (color.B << 16);
- }
-
- private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
- private const int DWMWA_CAPTION_COLOR = 35;
- private const int DWMWA_TEXT_COLOR = 36;
-
- [DllImport("dwmapi.dll", PreserveSig = true)]
- private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
}
diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs
index 0cdb574..4b2fbfe 100644
--- a/Models/AppSettings.cs
+++ b/Models/AppSettings.cs
@@ -1,4 +1,4 @@
-namespace AmigaDB.VideoRenderer.Models;
+namespace AmiReel.Models;
public sealed record AppSettings(
string OutputFolder,
diff --git a/Models/RenderSettings.cs b/Models/RenderSettings.cs
index 3e42143..099452f 100644
--- a/Models/RenderSettings.cs
+++ b/Models/RenderSettings.cs
@@ -1,4 +1,4 @@
-namespace AmigaDB.VideoRenderer.Models;
+namespace AmiReel.Models;
public enum EncoderMode
{
diff --git a/Models/SupportedVideoFormats.cs b/Models/SupportedVideoFormats.cs
index f785b74..05347c0 100644
--- a/Models/SupportedVideoFormats.cs
+++ b/Models/SupportedVideoFormats.cs
@@ -1,7 +1,7 @@
using System.IO;
using System.Linq;
-namespace AmigaDB.VideoRenderer.Models;
+namespace AmiReel.Models;
///
/// Video file extensions accepted as render sources. FFmpeg can demux far more than this,
diff --git a/AmiReel.WinUI/Package.appxmanifest b/Package.appxmanifest
similarity index 100%
rename from AmiReel.WinUI/Package.appxmanifest
rename to Package.appxmanifest
diff --git a/Properties/PublishProfiles/win-arm64.pubxml b/Properties/PublishProfiles/win-arm64.pubxml
new file mode 100644
index 0000000..8953cce
--- /dev/null
+++ b/Properties/PublishProfiles/win-arm64.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ FileSystem
+ ARM64
+ win-arm64
+ bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\
+ true
+ False
+
+
\ No newline at end of file
diff --git a/Properties/PublishProfiles/win-x64.pubxml b/Properties/PublishProfiles/win-x64.pubxml
new file mode 100644
index 0000000..cd99561
--- /dev/null
+++ b/Properties/PublishProfiles/win-x64.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ FileSystem
+ x64
+ win-x64
+ bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\
+ true
+ False
+
+
\ No newline at end of file
diff --git a/Properties/PublishProfiles/win-x86.pubxml b/Properties/PublishProfiles/win-x86.pubxml
new file mode 100644
index 0000000..a70c694
--- /dev/null
+++ b/Properties/PublishProfiles/win-x86.pubxml
@@ -0,0 +1,14 @@
+
+
+
+
+ FileSystem
+ x86
+ win-x86
+ bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\
+ true
+ False
+
+
\ No newline at end of file
diff --git a/AmiReel.WinUI/Properties/launchSettings.json b/Properties/launchSettings.json
similarity index 59%
rename from AmiReel.WinUI/Properties/launchSettings.json
rename to Properties/launchSettings.json
index cc6fcd1..58a6ceb 100644
--- a/AmiReel.WinUI/Properties/launchSettings.json
+++ b/Properties/launchSettings.json
@@ -1,9 +1,9 @@
{
"profiles": {
- "AmiReel.WinUI (Package)": {
+ "AmiReel (Package)": {
"commandName": "MsixPackage"
},
- "AmiReel.WinUI (Unpackaged)": {
+ "AmiReel (Unpackaged)": {
"commandName": "Project"
}
}
diff --git a/README.md b/README.md
index 28867ae..cc431c9 100644
--- a/README.md
+++ b/README.md
@@ -1,111 +1,90 @@
# AmiReel
-AmiReel is a Windows video-rendering tool for turning Amiga AVI captures into:
+AmiReel is a Windows desktop app for turning Amiga (or any) screen recordings into:
- a 4K 50 FPS final video,
- PNG/JPG thumbnails,
-- an animated WebP preview,
-- and a publishable Windows desktop app workflow.
+- an animated WebP preview.
-The repository currently contains two desktop frontends that share the same render pipeline:
-
-- `AmiReel.WinUI`
- The newer WinUI 3 application. This is the default publish target.
-- `AmigaDB.VideoRenderer.csproj`
- The existing WPF application.
+Built with **WinUI 3** on the Windows App SDK, driving FFmpeg under the hood.
## Features
-- multiple ordered AVI inputs
+- Multiple ordered video inputs — `.avi`, `.mp4`, `.m4v`, `.mov`, `.mkv`, `.webm`, `.wmv`, `.flv`, `.mpg`, `.mpeg`, `.ts`, `.3gp`
+- Drag-and-drop or file-picker source selection
- 3840×2160 output at 50 FPS
- NVIDIA NVENC with automatic CPU `libx264` fallback
-- configurable trim, fade, end-card hold, and thumbnail interval
-- optional end-card image
-- live render progress with frame counters
-- FFmpeg log output
-- source video preview and final render preview
-- dark and light themes
-- optional move of source recordings into `originals` in the output folder
-- optional custom `ffmpeg.exe` / `ffprobe.exe` paths
-- automatic FFmpeg detection from `PATH`
-- single-file WinUI publish for easier distribution
+- Configurable trim, fade, end-card hold, and thumbnail interval
+- Optional end-card image
+- Live render progress with frame counters and FFmpeg log output
+- Source and final-render preview (built-in or a custom player)
+- Dark and light themes
+- Optional move of source recordings into `originals/` in the output folder
+- Optional custom `ffmpeg.exe` / `ffprobe.exe` paths, with automatic detection from `PATH`
+- Self-contained single-file publish for easy distribution
+
+## Project Layout
+
+```
+AmiReel.csproj Project file
+AmiReel.slnx Solution file
+App.xaml(.cs) Application entry point
+MainWindow.xaml(.cs) Window shell: custom title bar, theming, exit confirmation
+MainPage.xaml(.cs) Main UI: source list, settings, render controls
+DialogHelper.cs Shared ContentDialog styling (Settings, exit, error dialogs)
+Package.appxmanifest MSIX packaging identity and tile/icon declarations
+app.manifest Win32 manifest (DPI awareness, OS compatibility)
+Assets/ Packaged app icons and tiles (all required sizes)
+branding/ Source artwork the Assets/ tiles are generated from
+Models/
+ AppSettings.cs Persisted user settings
+ RenderSettings.cs Render job parameters
+ SupportedVideoFormats.cs Accepted input file extensions
+Services/
+ RenderPipeline.cs Main render workflow (FFmpeg orchestration)
+ ProcessRunner.cs FFmpeg process execution and progress parsing
+ ToolExtractor.cs FFmpeg resolution, extraction, and fallback logic
+ MediaProbe.cs Duration/audio-stream probing via ffprobe
+ FfmpegLibraryProbe.cs NVENC capability probing
+ AppSettingsStore.cs Settings load/save (JSON in %LOCALAPPDATA%)
+ UserFacingErrors.cs Exception-to-message translation
+ThirdParty/ Embedded ffmpeg.exe / ffprobe.exe (not checked in, see below)
+publish-win-x64.ps1 Publish script
+```
## Requirements
For development:
- Windows 10 or Windows 11
-- .NET SDK
- - WPF project: .NET 8 SDK
- - WinUI project: .NET 10 SDK and WinUI/Windows App SDK support
-- Windows x64 FFmpeg binaries in `ThirdParty`
+- .NET 10 SDK with WinUI 3 / Windows App SDK workload
+- Windows x64 FFmpeg binaries in `ThirdParty/`:
- `ThirdParty\ffmpeg.exe`
- `ThirdParty\ffprobe.exe`
For end users:
- Windows 10 or Windows 11 x64
-- the published AmiReel executable
-- FFmpeg is not required separately if you distribute AmiReel with embedded `ThirdParty` FFmpeg binaries
-
-## Project Layout
-
-- `AmiReel.WinUI\`
- WinUI 3 frontend
-- `MainWindow.xaml` and `MainWindow.xaml.cs`
- WPF frontend
-- `Services\RenderPipeline.cs`
- Main render workflow
-- `Services\ProcessRunner.cs`
- FFmpeg process execution and progress parsing
-- `Services\ToolExtractor.cs`
- FFmpeg resolution, extraction, and fallback logic
-- `publish-win-x64.ps1`
- Publish script
+- The published `AmiReel.exe` — FFmpeg is not required separately if you distribute AmiReel with the embedded `ThirdParty` binaries
## Run in Development
-### WinUI
-
```powershell
-dotnet build .\AmiReel.WinUI\AmiReel.WinUI.csproj
-dotnet run --project .\AmiReel.WinUI\AmiReel.WinUI.csproj
-```
-
-### WPF
-
-```powershell
-dotnet build .\AmigaDB.VideoRenderer.csproj
-dotnet run --project .\AmigaDB.VideoRenderer.csproj
+dotnet build .\AmiReel.csproj
+dotnet run --project .\AmiReel.csproj
```
## Publish
-### Default: WinUI
-
-The publish script now defaults to the WinUI application:
-
```powershell
.\publish-win-x64.ps1
```
-This publishes the WinUI app as a self-contained single-file executable.
-
-At the end of the script you will see:
-
-- the release folder path
-- the final executable path
-
-Example output path:
+This publishes a self-contained, single-file `win-x64` executable. At the end of the script
+you'll see the release folder and final executable path, for example:
```text
-AmiReel.WinUI\bin\Release\net10.0-windows10.0.26100.0\win-x64\publish\AmiReel.exe
-```
-
-### Publish WPF instead
-
-```powershell
-.\publish-win-x64.ps1 -Target Wpf
+bin\Release\net10.0-windows10.0.26100.0\win-x64\publish\AmiReel.exe
```
## Installation
@@ -113,42 +92,33 @@ AmiReel.WinUI\bin\Release\net10.0-windows10.0.26100.0\win-x64\publish\AmiReel.ex
### For developers
1. Clone the repository.
-2. Add `ffmpeg.exe` and `ffprobe.exe` to `ThirdParty`.
-3. Restore and build the desired app:
+2. Add `ffmpeg.exe` and `ffprobe.exe` to `ThirdParty\`.
+3. Restore and build:
```powershell
-dotnet build .\AmigaDB.VideoRenderer.sln
+dotnet build .\AmiReel.slnx
```
-4. Run either the WinUI or WPF project.
-
### For end users
-1. Copy the published `AmiReel.exe` from the publish folder to any location, for example:
-
-```text
-C:\Apps\AmiReel\
-```
-
+1. Copy the published `AmiReel.exe` to any location, for example `C:\Apps\AmiReel\`.
2. Double-click `AmiReel.exe`.
3. If FFmpeg is embedded in the build, no extra setup is required.
-4. If you want to use your own FFmpeg build:
- open `Settings` and select:
- - `ffmpeg.exe`
- - `ffprobe.exe`
+4. To use your own FFmpeg build instead, open **Settings** and set:
+ - `ffmpeg.exe` path
+ - `ffprobe.exe` path
### Optional desktop shortcut
-1. Right-click `AmiReel.exe`.
-2. Choose `Send to` -> `Desktop (create shortcut)`.
+Right-click `AmiReel.exe` → **Send to** → **Desktop (create shortcut)**.
## FFmpeg Behavior
AmiReel resolves FFmpeg in this order:
-1. custom paths from settings
-2. `ffmpeg.exe` and `ffprobe.exe` found on system `PATH`
-3. embedded `ThirdParty` binaries
+1. Custom paths from Settings
+2. `ffmpeg.exe` / `ffprobe.exe` found on the system `PATH`
+3. Embedded `ThirdParty` binaries (extracted to `%LOCALAPPDATA%\AmiReel\tools` on first use)
If NVENC cannot be initialized, AmiReel automatically falls back to CPU `libx264`.
@@ -160,41 +130,25 @@ User settings are stored in:
%LOCALAPPDATA%\AmiReel\settings.json
```
-This includes values such as:
-
-- output folder
-- end-card path
-- FFmpeg paths
-- theme
-- encoder selection
-- timing settings
-- whether source videos are moved into `originals` after a successful render
+This includes: output folder, end-card path, FFmpeg/preview-player paths, theme, encoder
+selection, timing settings, and whether source videos are moved into `originals/` after a
+successful render.
## Notes for Distribution
-- If you distribute FFmpeg binaries with AmiReel, you are responsible for complying with the license terms of the FFmpeg build you use.
-- Keep any required notices, source offer, or attribution required by that FFmpeg distribution.
-- Windows Explorer may cache executable icons. If a freshly published build still shows an old icon, rename the file or refresh the icon cache before assuming the embed failed.
+- If you distribute FFmpeg binaries with AmiReel, you are responsible for complying with the
+ license terms of the FFmpeg build you use. Keep any required notices, source offer, or
+ attribution required by that distribution.
+- Windows Explorer may cache executable icons. If a freshly published build still shows an old
+ icon, rename the file or refresh the icon cache before assuming the embed failed.
## Troubleshooting
-### App falls back to CPU instead of NVENC
+**App falls back to CPU instead of NVENC** — FFmpeg could not initialize `h264_nvenc`. AmiReel
+continues automatically with CPU encoding.
-This usually means FFmpeg could not initialize `h264_nvenc`. AmiReel will continue with CPU encoding automatically.
+**FFmpeg tool error** — If the embedded FFmpeg is missing or broken, AmiReel tries configured
+tool paths, then FFmpeg from `PATH`. Set the paths manually in **Settings** if needed.
-### FFmpeg tool error
-
-If embedded FFmpeg is missing or broken, AmiReel will try:
-
-1. configured tool paths
-2. FFmpeg from `PATH`
-
-If needed, set the paths manually in `Settings`.
-
-### Render failed popup
-
-The popup shows a short human-readable summary. Full technical details remain available in the `FFmpeg log` panel.
-
-## Current Default Recommendation
-
-Use the WinUI build for publishing and distribution unless you specifically need the WPF variant.
+**Render failed popup** — Shows a short human-readable summary. Full technical details remain
+available in the **FFmpeg log** panel.
diff --git a/Services/AppSettingsStore.cs b/Services/AppSettingsStore.cs
index cc185bb..dd4f44e 100644
--- a/Services/AppSettingsStore.cs
+++ b/Services/AppSettingsStore.cs
@@ -1,8 +1,8 @@
using System.IO;
using System.Text.Json;
-using AmigaDB.VideoRenderer.Models;
+using AmiReel.Models;
-namespace AmigaDB.VideoRenderer.Services;
+namespace AmiReel.Services;
public static class AppSettingsStore
{
diff --git a/Services/FfmpegLibraryProbe.cs b/Services/FfmpegLibraryProbe.cs
index 86190f6..c7db1c7 100644
--- a/Services/FfmpegLibraryProbe.cs
+++ b/Services/FfmpegLibraryProbe.cs
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
-namespace AmigaDB.VideoRenderer.Services;
+namespace AmiReel.Services;
internal sealed partial class FfmpegLibraryProbe : IDisposable
{
diff --git a/Services/MediaProbe.cs b/Services/MediaProbe.cs
index 7132900..3a8c429 100644
--- a/Services/MediaProbe.cs
+++ b/Services/MediaProbe.cs
@@ -1,7 +1,7 @@
using System.Globalization;
using System.IO;
-namespace AmigaDB.VideoRenderer.Services;
+namespace AmiReel.Services;
internal sealed class MediaProbe
{
diff --git a/Services/ProcessRunner.cs b/Services/ProcessRunner.cs
index a9dd73c..a26ace3 100644
--- a/Services/ProcessRunner.cs
+++ b/Services/ProcessRunner.cs
@@ -4,7 +4,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.IO;
-namespace AmigaDB.VideoRenderer.Services;
+namespace AmiReel.Services;
public sealed partial class ProcessRunner
{
diff --git a/Services/RenderPipeline.cs b/Services/RenderPipeline.cs
index a74b2e2..b286b9d 100644
--- a/Services/RenderPipeline.cs
+++ b/Services/RenderPipeline.cs
@@ -1,9 +1,9 @@
using System.Globalization;
using System.IO;
using System.Text;
-using AmigaDB.VideoRenderer.Models;
+using AmiReel.Models;
-namespace AmigaDB.VideoRenderer.Services;
+namespace AmiReel.Services;
public sealed class RenderPipeline
{
diff --git a/Services/ToolExtractor.cs b/Services/ToolExtractor.cs
index 075cb17..42c9f3d 100644
--- a/Services/ToolExtractor.cs
+++ b/Services/ToolExtractor.cs
@@ -2,7 +2,7 @@ using System.IO;
using System.Reflection;
using System.Security.Cryptography;
-namespace AmigaDB.VideoRenderer.Services;
+namespace AmiReel.Services;
public sealed record ToolPaths(string Ffmpeg, string Ffprobe);
@@ -52,7 +52,7 @@ public static class ToolExtractor
private static async Task ExtractOneAsync(string fileName, string destination, CancellationToken token)
{
Assembly assembly = Assembly.GetExecutingAssembly();
- string resourceName = $"AmigaDB.VideoRenderer.Tools.{fileName}";
+ string resourceName = $"AmiReel.Tools.{fileName}";
await using Stream source = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException(
$"Embedded {fileName} was not found. Add it to ThirdParty and publish the application again.");
diff --git a/Services/UserFacingErrors.cs b/Services/UserFacingErrors.cs
index 32f964f..69008b4 100644
--- a/Services/UserFacingErrors.cs
+++ b/Services/UserFacingErrors.cs
@@ -1,4 +1,4 @@
-namespace AmigaDB.VideoRenderer.Services;
+namespace AmiReel.Services;
public static class UserFacingErrors
{
diff --git a/AmiReel.WinUI/app.manifest b/app.manifest
similarity index 93%
rename from AmiReel.WinUI/app.manifest
rename to app.manifest
index c9b4b0c..8b4da0b 100644
--- a/AmiReel.WinUI/app.manifest
+++ b/app.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/assets/AmiReel-master-1254.png b/branding/AmiReel-master-1254.png
similarity index 100%
rename from assets/AmiReel-master-1254.png
rename to branding/AmiReel-master-1254.png
diff --git a/assets/AmiReel.ico b/branding/AmiReel.ico
similarity index 100%
rename from assets/AmiReel.ico
rename to branding/AmiReel.ico
diff --git a/assets/README.txt b/branding/README.txt
similarity index 79%
rename from assets/README.txt
rename to branding/README.txt
index e2c91db..1cfd98f 100644
--- a/assets/README.txt
+++ b/branding/README.txt
@@ -14,9 +14,9 @@ Contents:
Individual transparent PNG files:
16, 24, 32, 48, 64, 128, 256, 512 and 1024 px.
-Recommended WPF project setting:
+Referenced from the project file as:
- Assets\AmiReel.ico
+ branding\AmiReel.ico
Product: AmiReel — Amiga Video Renderer
Brand: AmigaDB
diff --git a/assets/png/AmiReel-1024x1024.png b/branding/png/AmiReel-1024x1024.png
similarity index 100%
rename from assets/png/AmiReel-1024x1024.png
rename to branding/png/AmiReel-1024x1024.png
diff --git a/assets/png/AmiReel-128x128.png b/branding/png/AmiReel-128x128.png
similarity index 100%
rename from assets/png/AmiReel-128x128.png
rename to branding/png/AmiReel-128x128.png
diff --git a/assets/png/AmiReel-16x16.png b/branding/png/AmiReel-16x16.png
similarity index 100%
rename from assets/png/AmiReel-16x16.png
rename to branding/png/AmiReel-16x16.png
diff --git a/assets/png/AmiReel-24x24.png b/branding/png/AmiReel-24x24.png
similarity index 100%
rename from assets/png/AmiReel-24x24.png
rename to branding/png/AmiReel-24x24.png
diff --git a/assets/png/AmiReel-256x256.png b/branding/png/AmiReel-256x256.png
similarity index 100%
rename from assets/png/AmiReel-256x256.png
rename to branding/png/AmiReel-256x256.png
diff --git a/assets/png/AmiReel-32x32.png b/branding/png/AmiReel-32x32.png
similarity index 100%
rename from assets/png/AmiReel-32x32.png
rename to branding/png/AmiReel-32x32.png
diff --git a/assets/png/AmiReel-48x48.png b/branding/png/AmiReel-48x48.png
similarity index 100%
rename from assets/png/AmiReel-48x48.png
rename to branding/png/AmiReel-48x48.png
diff --git a/assets/png/AmiReel-512x512.png b/branding/png/AmiReel-512x512.png
similarity index 100%
rename from assets/png/AmiReel-512x512.png
rename to branding/png/AmiReel-512x512.png
diff --git a/assets/png/AmiReel-64x64.png b/branding/png/AmiReel-64x64.png
similarity index 100%
rename from assets/png/AmiReel-64x64.png
rename to branding/png/AmiReel-64x64.png
diff --git a/publish-win-x64.ps1 b/publish-win-x64.ps1
index 91efb81..36d69d5 100644
--- a/publish-win-x64.ps1
+++ b/publish-win-x64.ps1
@@ -1,8 +1,5 @@
[CmdletBinding()]
-param(
- [ValidateSet('WinUI', 'Wpf')]
- [string]$Target = 'WinUI'
-)
+param()
$ErrorActionPreference = 'Stop'
@@ -14,41 +11,21 @@ if (-not (Test-Path $ffmpeg) -or -not (Test-Path $ffprobe)) {
throw 'Add ffmpeg.exe and ffprobe.exe to ThirdParty before publishing.'
}
-switch ($Target) {
- 'WinUI' {
- $projectPath = Join-Path $projectDir 'AmiReel.WinUI\AmiReel.WinUI.csproj'
- $releaseDir = Join-Path $projectDir 'AmiReel.WinUI\bin\Release\net10.0-windows10.0.26100.0\win-x64'
- $releaseExe = Join-Path $releaseDir 'AmiReel.exe'
- $publishDir = Join-Path $releaseDir 'publish'
- $publishExe = Join-Path $publishDir 'AmiReel.exe'
- $publishArguments = @(
- 'publish', $projectPath,
- '-c', 'Release',
- '-r', 'win-x64',
- '--self-contained', 'true',
- '-p:WindowsPackageType=None',
- '-p:PublishSingleFile=true',
- '-p:IncludeNativeLibrariesForSelfExtract=true',
- '-p:EnableCompressionInSingleFile=true'
- )
- }
- 'Wpf' {
- $projectPath = Join-Path $projectDir 'AmigaDB.VideoRenderer.csproj'
- $releaseDir = Join-Path $projectDir 'bin\Release\net8.0-windows\win-x64'
- $releaseExe = Join-Path $releaseDir 'AmiReel.exe'
- $publishDir = Join-Path $releaseDir 'publish'
- $publishExe = Join-Path $publishDir 'AmiReel.exe'
- $publishArguments = @(
- 'publish', $projectPath,
- '-c', 'Release',
- '-r', 'win-x64',
- '--self-contained', 'true',
- '-p:PublishSingleFile=true',
- '-p:IncludeNativeLibrariesForSelfExtract=true',
- '-p:EnableCompressionInSingleFile=true'
- )
- }
-}
+$projectPath = Join-Path $projectDir 'AmiReel.csproj'
+$releaseDir = Join-Path $projectDir 'bin\Release\net10.0-windows10.0.26100.0\win-x64'
+$releaseExe = Join-Path $releaseDir 'AmiReel.exe'
+$publishDir = Join-Path $releaseDir 'publish'
+$publishExe = Join-Path $publishDir 'AmiReel.exe'
+$publishArguments = @(
+ 'publish', $projectPath,
+ '-c', 'Release',
+ '-r', 'win-x64',
+ '--self-contained', 'true',
+ '-p:WindowsPackageType=None',
+ '-p:PublishSingleFile=true',
+ '-p:IncludeNativeLibrariesForSelfExtract=true',
+ '-p:EnableCompressionInSingleFile=true'
+)
if ((Test-Path $releaseExe) -or (Test-Path $publishExe)) {
$runningReleaseInstances = Get-Process | Where-Object {
@@ -59,7 +36,7 @@ if ((Test-Path $releaseExe) -or (Test-Path $publishExe)) {
}
if ($runningReleaseInstances) {
- Write-Host "Stopping running $Target build before publish..." -ForegroundColor Yellow
+ Write-Host 'Stopping running AmiReel build before publish...' -ForegroundColor Yellow
$runningReleaseInstances | Stop-Process -Force
Start-Sleep -Milliseconds 800
}
@@ -71,6 +48,6 @@ if (Test-Path $publishDir) {
& dotnet @publishArguments
-Write-Host "Published $Target to $publishDir" -ForegroundColor Green
+Write-Host "Published AmiReel to $publishDir" -ForegroundColor Green
Write-Host "Release folder: $publishDir" -ForegroundColor Cyan
Write-Host "Executable: $publishExe" -ForegroundColor Cyan