diff --git a/AmiReel.WinUI/App.xaml b/AmiReel.WinUI/App.xaml
index dbae222..cc8b137 100644
--- a/AmiReel.WinUI/App.xaml
+++ b/AmiReel.WinUI/App.xaml
@@ -23,6 +23,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -37,11 +85,65 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 10
+ 18
+
+ 460
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
diff --git a/AmiReel.WinUI/App.xaml.cs b/AmiReel.WinUI/App.xaml.cs
index 96e1495..7e911c5 100644
--- a/AmiReel.WinUI/App.xaml.cs
+++ b/AmiReel.WinUI/App.xaml.cs
@@ -24,6 +24,7 @@ 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
diff --git a/AmiReel.WinUI/Assets/AppIcon.ico b/AmiReel.WinUI/Assets/AppIcon.ico
index 235dad2..e3e0799 100644
Binary files a/AmiReel.WinUI/Assets/AppIcon.ico and b/AmiReel.WinUI/Assets/AppIcon.ico differ
diff --git a/AmiReel.WinUI/Assets/LockScreenLogo.scale-200.png b/AmiReel.WinUI/Assets/LockScreenLogo.scale-200.png
index 33f889e..15d7ea0 100644
Binary files a/AmiReel.WinUI/Assets/LockScreenLogo.scale-200.png and b/AmiReel.WinUI/Assets/LockScreenLogo.scale-200.png differ
diff --git a/AmiReel.WinUI/Assets/SplashScreen.scale-200.png b/AmiReel.WinUI/Assets/SplashScreen.scale-200.png
index 802c79d..a31f213 100644
Binary files a/AmiReel.WinUI/Assets/SplashScreen.scale-200.png and b/AmiReel.WinUI/Assets/SplashScreen.scale-200.png differ
diff --git a/AmiReel.WinUI/Assets/Square150x150Logo.scale-200.png b/AmiReel.WinUI/Assets/Square150x150Logo.scale-200.png
index ddba42a..1e781f1 100644
Binary files a/AmiReel.WinUI/Assets/Square150x150Logo.scale-200.png and b/AmiReel.WinUI/Assets/Square150x150Logo.scale-200.png differ
diff --git a/AmiReel.WinUI/Assets/Square44x44Logo.scale-200.png b/AmiReel.WinUI/Assets/Square44x44Logo.scale-200.png
index 9327dd7..a9b1483 100644
Binary files a/AmiReel.WinUI/Assets/Square44x44Logo.scale-200.png and b/AmiReel.WinUI/Assets/Square44x44Logo.scale-200.png differ
diff --git a/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
index e51416a..ac98ace 100644
Binary files a/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png and b/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png b/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png
index bf063eb..4ebc40f 100644
Binary files a/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png and b/AmiReel.WinUI/Assets/Square44x44Logo.targetsize-48_altform-lightunplated.png differ
diff --git a/AmiReel.WinUI/Assets/StoreLogo.png b/AmiReel.WinUI/Assets/StoreLogo.png
index be865f0..b41271a 100644
Binary files a/AmiReel.WinUI/Assets/StoreLogo.png and b/AmiReel.WinUI/Assets/StoreLogo.png differ
diff --git a/AmiReel.WinUI/Assets/Wide310x150Logo.scale-200.png b/AmiReel.WinUI/Assets/Wide310x150Logo.scale-200.png
index 314d297..ab79a94 100644
Binary files a/AmiReel.WinUI/Assets/Wide310x150Logo.scale-200.png and b/AmiReel.WinUI/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/AmiReel.WinUI/DialogHelper.cs b/AmiReel.WinUI/DialogHelper.cs
new file mode 100644
index 0000000..ae9dcab
--- /dev/null
+++ b/AmiReel.WinUI/DialogHelper.cs
@@ -0,0 +1,33 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Media;
+
+namespace AmiReel_WinUI;
+
+///
+/// Builds ContentDialogs styled to match the app's own palette and button shapes,
+/// since ContentDialog's default Fluent look (accent-pill primary button, plain
+/// square close button, ungrouped theme resources) does not follow our custom brushes.
+///
+internal static class DialogHelper
+{
+ public static ContentDialog CreateStyled(XamlRoot xamlRoot, ElementTheme theme)
+ {
+ string themeKey = theme == ElementTheme.Dark ? "Dark" : "Light";
+ ResourceDictionary themeDictionary = (ResourceDictionary)Application.Current.Resources.ThemeDictionaries[themeKey];
+
+ return new ContentDialog
+ {
+ XamlRoot = xamlRoot,
+ RequestedTheme = theme,
+ Background = (Brush)themeDictionary["PanelBrush"],
+ Foreground = (Brush)themeDictionary["TextBrush"],
+ BorderBrush = (Brush)themeDictionary["HeroBorderBrush"],
+ BorderThickness = new Thickness(1.5),
+ CornerRadius = new CornerRadius(18),
+ PrimaryButtonStyle = (Style)Application.Current.Resources["DialogPrimaryButtonStyle"],
+ SecondaryButtonStyle = (Style)Application.Current.Resources["DialogCloseButtonStyle"],
+ CloseButtonStyle = (Style)Application.Current.Resources["DialogCloseButtonStyle"]
+ };
+ }
+}
diff --git a/AmiReel.WinUI/MainPage.xaml b/AmiReel.WinUI/MainPage.xaml
index df230da..2922a3c 100644
--- a/AmiReel.WinUI/MainPage.xaml
+++ b/AmiReel.WinUI/MainPage.xaml
@@ -25,16 +25,17 @@
-
+
+ Drop="InputList_Drop"
+ ToolTipService.ToolTip="Drag and drop video files here to add them" />
-
+
-
+
-
+
@@ -75,10 +76,10 @@
-
-
+
+
-
+
@@ -111,7 +112,8 @@
-
+
@@ -121,7 +123,14 @@
Title="Settings"
PrimaryButtonText="Done"
CloseButtonText="Close"
- DefaultButton="Primary">
+ DefaultButton="Primary"
+ Background="{ThemeResource PanelBrush}"
+ Foreground="{ThemeResource TextBrush}"
+ BorderBrush="{ThemeResource HeroBorderBrush}"
+ BorderThickness="1.5"
+ CornerRadius="18"
+ PrimaryButtonStyle="{StaticResource DialogPrimaryButtonStyle}"
+ CloseButtonStyle="{StaticResource DialogCloseButtonStyle}">
@@ -190,11 +199,14 @@
+
+
+
diff --git a/AmiReel.WinUI/MainPage.xaml.cs b/AmiReel.WinUI/MainPage.xaml.cs
index 030f62b..6b291f9 100644
--- a/AmiReel.WinUI/MainPage.xaml.cs
+++ b/AmiReel.WinUI/MainPage.xaml.cs
@@ -33,6 +33,7 @@ public sealed partial class MainPage : Page
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;
@@ -47,7 +48,8 @@ public sealed partial class MainPage : Page
private async void AddInputs_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
- picker.FileTypeFilter.Add(".avi");
+ foreach (string extension in SupportedVideoFormats.Extensions)
+ picker.FileTypeFilter.Add(extension);
picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
var files = await picker.PickMultipleFilesAsync();
if (files is null) return;
@@ -69,12 +71,20 @@ public sealed partial class MainPage : Page
{
if (!e.DataView.Contains(StandardDataFormats.StorageItems)) return;
- IReadOnlyList items = await e.DataView.GetStorageItemsAsync();
- foreach (string path in items.OfType()
- .Select(file => file.Path)
- .Where(path => string.Equals(Path.GetExtension(path), ".avi", StringComparison.OrdinalIgnoreCase))
- .OrderBy(NaturalKey))
- if (!_inputs.Contains(path, StringComparer.OrdinalIgnoreCase)) _inputs.Add(path);
+ 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)
@@ -116,6 +126,15 @@ public sealed partial class MainPage : Page
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;
@@ -171,16 +190,16 @@ public sealed partial class MainPage : Page
Process.Start(new ProcessStartInfo("explorer.exe", OutputFolderBox.Text) { UseShellExecute = true });
}
- private void PreviewSource_Click(object sender, RoutedEventArgs e)
+ private async void PreviewSource_Click(object sender, RoutedEventArgs e)
{
if (InputList.SelectedItem is string path)
- OpenPreview(path);
+ await OpenPreviewAsync(path);
}
- private void PreviewRendered_Click(object sender, RoutedEventArgs e)
+ private async void PreviewRendered_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(_lastRenderedFile) && File.Exists(_lastRenderedFile))
- OpenPreview(_lastRenderedFile);
+ await OpenPreviewAsync(_lastRenderedFile);
}
private void ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -241,12 +260,39 @@ public sealed partial class MainPage : Page
StatusText.Text = value.Message;
}
- private void OpenPreview(string mediaPath)
+ private async Task OpenPreviewAsync(string mediaPath)
{
if (!File.Exists(mediaPath))
+ {
+ await ShowMessageAsync("Preview unavailable", "The selected media file was not found.");
return;
+ }
- Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true });
+ 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)
@@ -285,7 +331,7 @@ public sealed partial class MainPage : Page
EndCardBox.Text.Trim(),
FfmpegPathBox.Text.Trim(),
FfprobePathBox.Text.Trim(),
- _loadedSettings.PreviewPlayerPath,
+ PreviewPlayerPathBox.Text.Trim(),
SelectedTheme(),
SelectedEncoder(),
TrimBox.Text.Trim(),
@@ -334,9 +380,8 @@ public sealed partial class MainPage : Page
private void ApplyTheme(string theme)
{
- RequestedTheme = string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase)
- ? ElementTheme.Light
- : ElementTheme.Dark;
+ bool isDark = !string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase);
+ App.MainWindowInstance?.ApplyTheme(isDark);
}
private FileOpenPicker CreateFileOpenPicker()
@@ -365,13 +410,10 @@ public sealed partial class MainPage : Page
private async Task ShowMessageAsync(string title, string message)
{
- ContentDialog dialog = new()
- {
- Title = title,
- Content = message,
- CloseButtonText = "OK",
- XamlRoot = XamlRoot
- };
+ ContentDialog dialog = DialogHelper.CreateStyled(XamlRoot, ActualTheme);
+ dialog.Title = title;
+ dialog.Content = message;
+ dialog.CloseButtonText = "OK";
await dialog.ShowAsync();
}
}
diff --git a/AmiReel.WinUI/MainWindow.xaml b/AmiReel.WinUI/MainWindow.xaml
index 01e9bd4..247e84c 100644
--- a/AmiReel.WinUI/MainWindow.xaml
+++ b/AmiReel.WinUI/MainWindow.xaml
@@ -8,24 +8,22 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="AmiReel"
mc:Ignorable="d">
-
-
-
-
-
+
-
+
diff --git a/AmiReel.WinUI/MainWindow.xaml.cs b/AmiReel.WinUI/MainWindow.xaml.cs
index 99b876a..5155340 100644
--- a/AmiReel.WinUI/MainWindow.xaml.cs
+++ b/AmiReel.WinUI/MainWindow.xaml.cs
@@ -1,8 +1,13 @@
+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.
@@ -21,6 +26,7 @@ public sealed partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
+ App.MainWindowInstance = this;
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
@@ -29,26 +35,77 @@ public sealed partial class MainWindow : Window
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;
- ContentDialog dialog = new()
- {
- Title = "Exit AmiReel?",
- Content = "Are you sure you want to close the application?",
- PrimaryButtonText = "Exit",
- CloseButtonText = "Cancel",
- DefaultButton = ContentDialogButton.Close,
- XamlRoot = RootFrame.XamlRoot
- };
+ 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;
diff --git a/AmiReel.WinUI/Package.appxmanifest b/AmiReel.WinUI/Package.appxmanifest
index e16b5d5..b3bd114 100644
--- a/AmiReel.WinUI/Package.appxmanifest
+++ b/AmiReel.WinUI/Package.appxmanifest
@@ -37,11 +37,11 @@
-
+
diff --git a/MainWindow.xaml b/MainWindow.xaml
index 2e31c92..aa992b5 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -77,12 +77,12 @@
-
+
+ ToolTip="Drag and drop video files here to add them"/>
diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs
index 55cb4ee..770a8be 100644
--- a/MainWindow.xaml.cs
+++ b/MainWindow.xaml.cs
@@ -82,13 +82,13 @@ public partial class MainWindow : Window
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
- foreach (string file in files.Where(f => string.Equals(Path.GetExtension(f), ".avi", StringComparison.OrdinalIgnoreCase)).OrderBy(NaturalKey))
+ 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 = "AVI recordings (*.avi)|*.avi", Multiselect = true };
+ 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);
diff --git a/Models/SupportedVideoFormats.cs b/Models/SupportedVideoFormats.cs
new file mode 100644
index 0000000..f785b74
--- /dev/null
+++ b/Models/SupportedVideoFormats.cs
@@ -0,0 +1,23 @@
+using System.IO;
+using System.Linq;
+
+namespace AmigaDB.VideoRenderer.Models;
+
+///
+/// Video file extensions accepted as render sources. FFmpeg can demux far more than this,
+/// but this list covers what a screen/video capture workflow is realistically going to produce.
+///
+public static class SupportedVideoFormats
+{
+ public static readonly IReadOnlyList Extensions =
+ [
+ ".avi", ".mp4", ".m4v", ".mov", ".mkv", ".webm",
+ ".wmv", ".flv", ".mpg", ".mpeg", ".ts", ".3gp"
+ ];
+
+ public static bool IsSupported(string path) =>
+ Extensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase);
+
+ /// Picker filter string, e.g. "*.avi;*.mp4;*.m4v;...".
+ public static string PickerFilter => string.Join(';', Extensions.Select(e => "*" + e));
+}
diff --git a/Services/RenderPipeline.cs b/Services/RenderPipeline.cs
index 87eaaa8..a74b2e2 100644
--- a/Services/RenderPipeline.cs
+++ b/Services/RenderPipeline.cs
@@ -360,8 +360,8 @@ public sealed class RenderPipeline
private static void Validate(RenderSettings s)
{
- if (s.InputFiles.Count == 0) throw new ArgumentException("Select at least one AVI input file.");
- if (s.InputFiles.Any(f => !File.Exists(f))) throw new FileNotFoundException("One or more AVI files no longer exist.");
+ if (s.InputFiles.Count == 0) throw new ArgumentException("Select at least one video input file.");
+ if (s.InputFiles.Any(f => !File.Exists(f))) throw new FileNotFoundException("One or more input files no longer exist.");
if (!string.IsNullOrWhiteSpace(s.EndCardPath) && !File.Exists(s.EndCardPath))
throw new FileNotFoundException("End-card image was not found.");
if (string.IsNullOrWhiteSpace(s.OutputDirectory)) throw new ArgumentException("Select an output directory.");