Compare commits

...

2 Commits

Author SHA1 Message Date
klevze 3e881b79b8 Bring WinUI app to feature/visual parity with WPF, fix drag-and-drop and branding assets
- Restyle Fluent controls (buttons, fields, dialogs, ListView) to match the
  app's navy dark/light palette and consistent corner radii/sizing
- Fix ContentDialog (Settings, exit-confirm, error) styling by binding
  PrimaryButtonStyle/CloseButtonStyle explicitly via new DialogHelper,
  since ContentDialog ignores implicit Button styles and forces accent
  color onto whichever button is DefaultButton
- Remove MicaBackdrop and theme the title bar directly so it no longer
  shows a gray tint mismatched with the app's navy background
- Replace placeholder Assets/*.png (unused VS template art) with the real
  AmiReel logo at all required tile/splash/store sizes
- Set explicit default window size (1280x860, matching the old WPF app)
  with DPI-aware centering and a minimum size, instead of sizing to content
- Fix drag-and-drop silently failing: InputList_Drop needs a
  DragOperationDeferral before the first await or the DataView is torn
  down before GetStorageItemsAsync completes
- Add Preview player path setting (was present in WPF, missing in WinUI)
  and error handling for preview playback
- Accept all ffmpeg-readable video formats (mp4, mov, mkv, webm, ...) for
  file picker and drag-and-drop, not just .avi
- Add exit-confirmation dialog to WinUI, mirroring the WPF app

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 13:53:55 +02:00
klevze ab46a16726 Some fixes 2026-08-13 12:39:03 +02:00
26 changed files with 760 additions and 70 deletions
+180 -15
View File
@@ -23,6 +23,54 @@
<SolidColorBrush x:Key="ButtonBrush" Color="#243554"/> <SolidColorBrush x:Key="ButtonBrush" Color="#243554"/>
<SolidColorBrush x:Key="ButtonHoverBrush" Color="#2E446C"/> <SolidColorBrush x:Key="ButtonHoverBrush" Color="#2E446C"/>
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#07111D"/> <SolidColorBrush x:Key="PrimaryTextBrush" Color="#07111D"/>
<!-- Fluent control theme-brush overrides so built-in controls match the palette -->
<SolidColorBrush x:Key="TextControlBackground" Color="#1D2940"/>
<SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#233252"/>
<SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#1D2940"/>
<SolidColorBrush x:Key="TextControlBorderBrush" Color="#2B3A58"/>
<SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#4AB8FF"/>
<SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#4AB8FF"/>
<SolidColorBrush x:Key="TextControlForeground" Color="#F5F7FB"/>
<SolidColorBrush x:Key="TextControlForegroundFocused" Color="#F5F7FB"/>
<SolidColorBrush x:Key="TextControlHeaderForeground" Color="#9FB1CC"/>
<SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#7488A6"/>
<SolidColorBrush x:Key="ComboBoxBackground" Color="#1D2940"/>
<SolidColorBrush x:Key="ComboBoxBackgroundPointerOver" Color="#233252"/>
<SolidColorBrush x:Key="ComboBoxBorderBrush" Color="#2B3A58"/>
<SolidColorBrush x:Key="ComboBoxForeground" Color="#F5F7FB"/>
<SolidColorBrush x:Key="ComboBoxHeaderForeground" Color="#9FB1CC"/>
<SolidColorBrush x:Key="ComboBoxItemBackgroundSelected" Color="#13324F"/>
<SolidColorBrush x:Key="ComboBoxDropDownBackground" Color="#10192B"/>
<SolidColorBrush x:Key="ButtonBackground" Color="#243554"/>
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="#2E446C"/>
<SolidColorBrush x:Key="ButtonBackgroundPressed" Color="#1B2A44"/>
<SolidColorBrush x:Key="ButtonForeground" Color="#F5F7FB"/>
<SolidColorBrush x:Key="ButtonForegroundPointerOver" Color="#F5F7FB"/>
<SolidColorBrush x:Key="ButtonBorderBrush" Color="#2B3A58"/>
<SolidColorBrush x:Key="ContentDialogBackground" Color="#162033"/>
<SolidColorBrush x:Key="ContentDialogForeground" Color="#F5F7FB"/>
<SolidColorBrush x:Key="ContentDialogBorderBrush" Color="#35507D"/>
<SolidColorBrush x:Key="ListViewItemBackgroundSelected" Color="#13324F"/>
<SolidColorBrush x:Key="ListViewItemBackgroundPointerOver" Color="#1A2740"/>
<SolidColorBrush x:Key="ListViewItemForeground" Color="#F5F7FB"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundFillUnchecked" Color="#1D2940"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeUnchecked" Color="#2B3A58"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundFillChecked" Color="#4AB8FF"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeChecked" Color="#4AB8FF"/>
<SolidColorBrush x:Key="CheckBoxCheckGlyphForegroundChecked" Color="#07111D"/>
<SolidColorBrush x:Key="CheckBoxForegroundUnchecked" Color="#F5F7FB"/>
<SolidColorBrush x:Key="CheckBoxForegroundChecked" Color="#F5F7FB"/>
<SolidColorBrush x:Key="ProgressBarForeground" Color="#4AB8FF"/>
<SolidColorBrush x:Key="ProgressBarBackground" Color="#1D2940"/>
<SolidColorBrush x:Key="HeroBorderBrush" Color="#35507D"/>
</ResourceDictionary> </ResourceDictionary>
<ResourceDictionary x:Key="Light"> <ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="PageBrush" Color="#F4F7FB"/> <SolidColorBrush x:Key="PageBrush" Color="#F4F7FB"/>
@@ -37,11 +85,65 @@
<SolidColorBrush x:Key="ButtonBrush" Color="#E5EDF7"/> <SolidColorBrush x:Key="ButtonBrush" Color="#E5EDF7"/>
<SolidColorBrush x:Key="ButtonHoverBrush" Color="#D7E4F4"/> <SolidColorBrush x:Key="ButtonHoverBrush" Color="#D7E4F4"/>
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#FFFFFF"/> <SolidColorBrush x:Key="PrimaryTextBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="TextControlBackground" Color="#F7FAFD"/>
<SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#EEF4FB"/>
<SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#FFFFFF"/>
<SolidColorBrush x:Key="TextControlBorderBrush" Color="#C7D3E3"/>
<SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#0E9AEF"/>
<SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#0E9AEF"/>
<SolidColorBrush x:Key="TextControlForeground" Color="#122033"/>
<SolidColorBrush x:Key="TextControlForegroundFocused" Color="#122033"/>
<SolidColorBrush x:Key="TextControlHeaderForeground" Color="#5F7390"/>
<SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#8194AC"/>
<SolidColorBrush x:Key="ComboBoxBackground" Color="#F7FAFD"/>
<SolidColorBrush x:Key="ComboBoxBackgroundPointerOver" Color="#EEF4FB"/>
<SolidColorBrush x:Key="ComboBoxBorderBrush" Color="#C7D3E3"/>
<SolidColorBrush x:Key="ComboBoxForeground" Color="#122033"/>
<SolidColorBrush x:Key="ComboBoxHeaderForeground" Color="#5F7390"/>
<SolidColorBrush x:Key="ComboBoxItemBackgroundSelected" Color="#D6EEFF"/>
<SolidColorBrush x:Key="ComboBoxDropDownBackground" Color="#FFFFFF"/>
<SolidColorBrush x:Key="ButtonBackground" Color="#E5EDF7"/>
<SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="#D7E4F4"/>
<SolidColorBrush x:Key="ButtonBackgroundPressed" Color="#C7D9EE"/>
<SolidColorBrush x:Key="ButtonForeground" Color="#122033"/>
<SolidColorBrush x:Key="ButtonForegroundPointerOver" Color="#122033"/>
<SolidColorBrush x:Key="ButtonBorderBrush" Color="#C7D3E3"/>
<SolidColorBrush x:Key="ContentDialogBackground" Color="#FFFFFF"/>
<SolidColorBrush x:Key="ContentDialogForeground" Color="#122033"/>
<SolidColorBrush x:Key="ContentDialogBorderBrush" Color="#C7D3E3"/>
<SolidColorBrush x:Key="ListViewItemBackgroundSelected" Color="#D6EEFF"/>
<SolidColorBrush x:Key="ListViewItemBackgroundPointerOver" Color="#EEF4FB"/>
<SolidColorBrush x:Key="ListViewItemForeground" Color="#122033"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundFillUnchecked" Color="#F7FAFD"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeUnchecked" Color="#C7D3E3"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundFillChecked" Color="#0E9AEF"/>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeChecked" Color="#0E9AEF"/>
<SolidColorBrush x:Key="CheckBoxCheckGlyphForegroundChecked" Color="#FFFFFF"/>
<SolidColorBrush x:Key="CheckBoxForegroundUnchecked" Color="#122033"/>
<SolidColorBrush x:Key="CheckBoxForegroundChecked" Color="#122033"/>
<SolidColorBrush x:Key="ProgressBarForeground" Color="#0E9AEF"/>
<SolidColorBrush x:Key="ProgressBarBackground" Color="#F7FAFD"/>
<SolidColorBrush x:Key="HeroBorderBrush" Color="#A9C4E4"/>
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>
<!-- Global corner rounding so built-in Fluent controls match the WPF app's rounded look -->
<CornerRadius x:Key="ControlCornerRadius">10</CornerRadius>
<CornerRadius x:Key="OverlayCornerRadius">18</CornerRadius>
<x:Double x:Key="ContentDialogMaxWidth">460</x:Double>
<Style TargetType="Page"> <Style TargetType="Page">
<Setter Property="Background" Value="{ThemeResource PageBrush}" /> <Setter Property="Background" Value="{ThemeResource PageBrush}" />
<Setter Property="FontFamily" Value="Bahnschrift" />
</Style> </Style>
<Style x:Key="CardBorderStyle" TargetType="Border"> <Style x:Key="CardBorderStyle" TargetType="Border">
@@ -53,51 +155,114 @@
<Style x:Key="SectionTitleStyle" TargetType="TextBlock"> <Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" /> <Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
<Setter Property="FontSize" Value="24" /> <Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontWeight" Value="SemiBold" />
</Style> </Style>
<Style x:Key="MutedTextStyle" TargetType="TextBlock"> <Style x:Key="MutedTextStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{ThemeResource MutedBrush}" /> <Setter Property="Foreground" Value="{ThemeResource MutedBrush}" />
<Setter Property="FontSize" Value="13" />
</Style>
<Style x:Key="FieldLabelStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{ThemeResource MutedBrush}" />
<Setter Property="FontSize" Value="12" />
<Setter Property="Margin" Value="0,0,0,4" />
</Style>
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
<Setter Property="FontSize" Value="13" />
</Style> </Style>
<Style TargetType="Button"> <Style TargetType="Button">
<Setter Property="Background" Value="{ThemeResource ButtonBrush}" /> <Setter Property="Padding" Value="16,9" />
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" /> <Setter Property="FontSize" Value="13" />
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" /> <Setter Property="CornerRadius" Value="10" />
</Style> </Style>
<Style x:Key="PrimaryButtonStyle" TargetType="Button"> <Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource AccentBrush}" /> <Setter Property="Background" Value="{ThemeResource AccentBrush}" />
<Setter Property="Foreground" Value="{ThemeResource PrimaryTextBrush}" /> <Setter Property="Foreground" Value="{ThemeResource PrimaryTextBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource AccentBrush}" /> <Setter Property="BorderBrush" Value="{ThemeResource AccentBrush}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Padding" Value="22,10" />
<Setter Property="CornerRadius" Value="10" />
</Style>
<Style x:Key="IconButtonStyle" TargetType="Button">
<Setter Property="Width" Value="40" />
<Setter Property="Height" Value="40" />
<Setter Property="Padding" Value="0" />
<Setter Property="FontSize" Value="16" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="HorizontalContentAlignment" Value="Center" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
<!-- Explicit styles bound to ContentDialog.PrimaryButtonStyle / CloseButtonStyle so its
built-in buttons match the app's own Button look instead of the default Fluent
accent-pill / plain styles. -->
<Style x:Key="DialogPrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource AccentBrush}" />
<Setter Property="Foreground" Value="{ThemeResource PrimaryTextBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource AccentBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="22,10" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="MinHeight" Value="40" />
<Setter Property="MinWidth" Value="110" />
</Style>
<Style x:Key="DialogCloseButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource ButtonBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="16,9" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="MinHeight" Value="40" />
<Setter Property="MinWidth" Value="110" />
</Style> </Style>
<Style TargetType="TextBox"> <Style TargetType="TextBox">
<Setter Property="Background" Value="{ThemeResource FieldBrush}" /> <Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" /> <Setter Property="MinHeight" Value="38" />
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
</Style> </Style>
<Style TargetType="ComboBox"> <Style TargetType="ComboBox">
<Setter Property="Background" Value="{ThemeResource FieldBrush}" /> <Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" /> <Setter Property="MinHeight" Value="38" />
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" /> </Style>
<Style TargetType="CheckBox">
<Setter Property="FontSize" Value="13" />
</Style> </Style>
<Style TargetType="ProgressBar"> <Style TargetType="ProgressBar">
<Setter Property="Foreground" Value="{ThemeResource AccentBrush}" /> <Setter Property="MinHeight" Value="8" />
<Setter Property="Background" Value="{ThemeResource FieldBrush}" />
</Style> </Style>
<Style TargetType="ListView"> <Style TargetType="ListView">
<Setter Property="Background" Value="{ThemeResource FieldBrush}" /> <Setter Property="Background" Value="{ThemeResource FieldBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" /> <Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="4" />
</Style> </Style>
<Style TargetType="TextBlock"> <Style TargetType="ListViewItem">
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" /> <Setter Property="MinHeight" Value="32" />
<Setter Property="Padding" Value="8,6" />
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style TargetType="ContentDialog">
<Setter Property="CornerRadius" Value="18" />
</Style> </Style>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
+1
View File
@@ -24,6 +24,7 @@ public partial class App : Application
{ {
private Window? _window; private Window? _window;
public static IntPtr MainWindowHandle { get; private set; } public static IntPtr MainWindowHandle { get; private set; }
public static MainWindow? MainWindowInstance { get; set; }
/// <summary> /// <summary>
/// Initializes the singleton application object. This is the first line of authored code /// Initializes the singleton application object. This is the first line of authored code
Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 109 KiB

+33
View File
@@ -0,0 +1,33 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace AmiReel_WinUI;
/// <summary>
/// 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.
/// </summary>
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"]
};
}
}
+38 -11
View File
@@ -25,13 +25,17 @@
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<TextBlock Text="Source recordings" Style="{StaticResource SectionTitleStyle}" /> <TextBlock Text="Source recordings" Style="{StaticResource SectionTitleStyle}" />
<StackPanel Orientation="Horizontal" Spacing="10"> <StackPanel Orientation="Horizontal" Spacing="10">
<Button Content="Add AVI files..." Click="AddInputs_Click" /> <Button Content="Add video files..." Click="AddInputs_Click" />
<Button Content="Clear" Click="ClearInputs_Click" /> <Button Content="Clear" Click="ClearInputs_Click" />
</StackPanel> </StackPanel>
<ListView x:Name="InputList" <ListView x:Name="InputList"
Height="120" Height="130"
SelectionMode="Single" SelectionMode="Single"
SelectionChanged="InputList_SelectionChanged" /> SelectionChanged="InputList_SelectionChanged"
AllowDrop="True"
DragOver="InputList_DragOver"
Drop="InputList_Drop"
ToolTipService.ToolTip="Drag and drop video files here to add them" />
<Button x:Name="PreviewSourceButton" <Button x:Name="PreviewSourceButton"
Content="Preview selected" Content="Preview selected"
Click="PreviewSource_Click" Click="PreviewSource_Click"
@@ -56,13 +60,13 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBox x:Name="EndCardBox" Grid.Row="0" MinHeight="48" /> <TextBox x:Name="EndCardBox" Grid.Row="0" MinHeight="40" />
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseEndCard_Click" VerticalAlignment="Stretch" MinWidth="120" /> <Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseEndCard_Click" VerticalAlignment="Stretch" MinWidth="120" />
<TextBox x:Name="OutputFolderBox" Grid.Row="1" MinHeight="48" /> <TextBox x:Name="OutputFolderBox" Grid.Row="1" MinHeight="40" />
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseOutput_Click" VerticalAlignment="Stretch" MinWidth="120" /> <Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseOutput_Click" VerticalAlignment="Stretch" MinWidth="120" />
<TextBox x:Name="OutputNameBox" Grid.Row="2" Grid.ColumnSpan="2" MinHeight="48" /> <TextBox x:Name="OutputNameBox" Grid.Row="2" Grid.ColumnSpan="2" MinHeight="40" />
</Grid> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -72,10 +76,10 @@
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<TextBlock Text="Render progress" Style="{StaticResource SectionTitleStyle}" /> <TextBlock Text="Render progress" Style="{StaticResource SectionTitleStyle}" />
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock x:Name="StageText" Text="Ready" FontSize="18" FontWeight="SemiBold" /> <TextBlock x:Name="StageText" Text="Ready" FontSize="15" FontWeight="SemiBold" />
<TextBlock x:Name="PercentText" Text="0%" FontSize="18" FontWeight="SemiBold" Foreground="{ThemeResource AccentBrush}" /> <TextBlock x:Name="PercentText" Text="0%" FontSize="15" FontWeight="SemiBold" Foreground="{ThemeResource AccentBrush}" />
</StackPanel> </StackPanel>
<ProgressBar x:Name="RenderProgressBar" Minimum="0" Maximum="100" Height="10" /> <ProgressBar x:Name="RenderProgressBar" Minimum="0" Maximum="100" Height="8" />
<TextBlock x:Name="StatusText" Text="Select the recordings and start the render. The end card is optional." TextWrapping="WrapWholeWords" Style="{StaticResource MutedTextStyle}" /> <TextBlock x:Name="StatusText" Text="Select the recordings and start the render. The end card is optional." TextWrapping="WrapWholeWords" Style="{StaticResource MutedTextStyle}" />
</StackPanel> </StackPanel>
</Border> </Border>
@@ -108,7 +112,8 @@
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10"> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
<Button x:Name="OpenSettingsButton" Content="Settings" Click="OpenSettings_Click" /> <Button x:Name="OpenSettingsButton" Content="&#xE713;" FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
Click="OpenSettings_Click" Style="{StaticResource IconButtonStyle}" ToolTipService.ToolTip="Settings" />
<Button x:Name="CancelButton" Content="Cancel" Click="Cancel_Click" IsEnabled="False" /> <Button x:Name="CancelButton" Content="Cancel" Click="Cancel_Click" IsEnabled="False" />
<Button x:Name="RenderButton" Content="Start render" Click="Render_Click" Style="{StaticResource PrimaryButtonStyle}" /> <Button x:Name="RenderButton" Content="Start render" Click="Render_Click" Style="{StaticResource PrimaryButtonStyle}" />
</StackPanel> </StackPanel>
@@ -118,7 +123,14 @@
Title="Settings" Title="Settings"
PrimaryButtonText="Done" PrimaryButtonText="Done"
CloseButtonText="Close" 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}">
<ScrollViewer MaxHeight="560" VerticalScrollBarVisibility="Auto"> <ScrollViewer MaxHeight="560" VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="16"> <StackPanel Spacing="16">
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12"> <Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
@@ -163,6 +175,18 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
<StackPanel Spacing="10">
<TextBlock Text="Source files" Style="{StaticResource SectionTitleStyle}" />
<CheckBox x:Name="MoveSourcesBox"
Content="Move source videos to destination\originals"
IsChecked="True" />
<TextBlock Text="After a successful render, original recordings are moved into an originals subfolder of the output folder. Uncheck to leave them where they are."
TextWrapping="WrapWholeWords"
Style="{StaticResource MutedTextStyle}" />
</StackPanel>
</Border>
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12"> <Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
<StackPanel Spacing="10"> <StackPanel Spacing="10">
<TextBlock Text="FFmpeg tools" Style="{StaticResource SectionTitleStyle}" /> <TextBlock Text="FFmpeg tools" Style="{StaticResource SectionTitleStyle}" />
@@ -175,11 +199,14 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBox x:Name="FfmpegPathBox" Grid.Row="0" Header="ffmpeg.exe path" /> <TextBox x:Name="FfmpegPathBox" Grid.Row="0" Header="ffmpeg.exe path" />
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseFfmpeg_Click" VerticalAlignment="Bottom" /> <Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseFfmpeg_Click" VerticalAlignment="Bottom" />
<TextBox x:Name="FfprobePathBox" Grid.Row="1" Header="ffprobe.exe path" /> <TextBox x:Name="FfprobePathBox" Grid.Row="1" Header="ffprobe.exe path" />
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseFfprobe_Click" VerticalAlignment="Bottom" /> <Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseFfprobe_Click" VerticalAlignment="Bottom" />
<TextBox x:Name="PreviewPlayerPathBox" Grid.Row="2" Header="Preview player path (optional, e.g. mpv.exe)" />
<Button Grid.Row="2" Grid.Column="1" Content="Browse..." Click="BrowsePreviewPlayer_Click" VerticalAlignment="Bottom" />
</Grid> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
+94 -20
View File
@@ -7,6 +7,8 @@ using AmigaDB.VideoRenderer.Services;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Win32; using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers; using Windows.Storage.Pickers;
using WinRT.Interop; using WinRT.Interop;
@@ -31,6 +33,7 @@ public sealed partial class MainPage : Page
EndCardBox.Text = _loadedSettings.EndCardPath; EndCardBox.Text = _loadedSettings.EndCardPath;
FfmpegPathBox.Text = _loadedSettings.FfmpegPath; FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
FfprobePathBox.Text = _loadedSettings.FfprobePath; FfprobePathBox.Text = _loadedSettings.FfprobePath;
PreviewPlayerPathBox.Text = _loadedSettings.PreviewPlayerPath;
OutputNameBox.Text = "amigadb_intro"; OutputNameBox.Text = "amigadb_intro";
TrimBox.Text = _loadedSettings.TrimStart; TrimBox.Text = _loadedSettings.TrimStart;
FadeBox.Text = _loadedSettings.FadeSeconds; FadeBox.Text = _loadedSettings.FadeSeconds;
@@ -39,12 +42,14 @@ public sealed partial class MainPage : Page
SelectEncoder(_loadedSettings.Encoder); SelectEncoder(_loadedSettings.Encoder);
SelectTheme(_loadedSettings.Theme); SelectTheme(_loadedSettings.Theme);
ApplyTheme(_loadedSettings.Theme); ApplyTheme(_loadedSettings.Theme);
MoveSourcesBox.IsChecked = _loadedSettings.ShouldMoveSourcesToOriginals;
} }
private async void AddInputs_Click(object sender, RoutedEventArgs e) private async void AddInputs_Click(object sender, RoutedEventArgs e)
{ {
FileOpenPicker picker = CreateFileOpenPicker(); FileOpenPicker picker = CreateFileOpenPicker();
picker.FileTypeFilter.Add(".avi"); foreach (string extension in SupportedVideoFormats.Extensions)
picker.FileTypeFilter.Add(extension);
picker.SuggestedStartLocation = PickerLocationId.VideosLibrary; picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
var files = await picker.PickMultipleFilesAsync(); var files = await picker.PickMultipleFilesAsync();
if (files is null) return; if (files is null) return;
@@ -55,6 +60,33 @@ public sealed partial class MainPage : Page
private void ClearInputs_Click(object sender, RoutedEventArgs e) => _inputs.Clear(); 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<IStorageItem> items = await e.DataView.GetStorageItemsAsync();
foreach (string path in items.OfType<StorageFile>()
.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) private async void BrowseEndCard_Click(object sender, RoutedEventArgs e)
{ {
FileOpenPicker picker = CreateFileOpenPicker(); FileOpenPicker picker = CreateFileOpenPicker();
@@ -94,6 +126,15 @@ public sealed partial class MainPage : Page
FfprobePathBox.Text = file.Path; 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) private async void OpenSettings_Click(object sender, RoutedEventArgs e)
{ {
SettingsDialog.XamlRoot = XamlRoot; SettingsDialog.XamlRoot = XamlRoot;
@@ -111,7 +152,8 @@ public sealed partial class MainPage : Page
LogBox.Text = string.Empty; LogBox.Text = string.Empty;
_renderCancellation = new CancellationTokenSource(); _renderCancellation = new CancellationTokenSource();
Progress<RenderProgress> progress = new(UpdateProgress); Progress<RenderProgress> progress = new(UpdateProgress);
await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token); IReadOnlyList<string> resolvedInputs = await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
ReplaceInputs(resolvedInputs);
_lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4"); _lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4");
OpenOutputButton.IsEnabled = true; OpenOutputButton.IsEnabled = true;
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile); PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
@@ -148,16 +190,16 @@ public sealed partial class MainPage : Page
Process.Start(new ProcessStartInfo("explorer.exe", OutputFolderBox.Text) { UseShellExecute = true }); 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) 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)) if (!string.IsNullOrWhiteSpace(_lastRenderedFile) && File.Exists(_lastRenderedFile))
OpenPreview(_lastRenderedFile); await OpenPreviewAsync(_lastRenderedFile);
} }
private void ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e) private void ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
@@ -192,7 +234,15 @@ public sealed partial class MainPage : Page
Number(TrimBox.Text, "trim start"), Number(TrimBox.Text, "trim start"),
Number(FadeBox.Text, "fade"), Number(FadeBox.Text, "fade"),
Number(HoldBox.Text, "end-card hold"), Number(HoldBox.Text, "end-card hold"),
interval); interval,
MoveSourcesBox.IsChecked == true);
}
private void ReplaceInputs(IReadOnlyList<string> paths)
{
_inputs.Clear();
foreach (string path in paths)
_inputs.Add(path);
} }
private void SetRendering(bool rendering) private void SetRendering(bool rendering)
@@ -210,13 +260,40 @@ public sealed partial class MainPage : Page
StatusText.Text = value.Message; StatusText.Text = value.Message;
} }
private void OpenPreview(string mediaPath) private async Task OpenPreviewAsync(string mediaPath)
{ {
if (!File.Exists(mediaPath)) if (!File.Exists(mediaPath))
{
await ShowMessageAsync("Preview unavailable", "The selected media file was not found.");
return; 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 }); Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true });
} }
catch (Exception exception)
{
await ShowMessageAsync("Preview failed", exception.Message);
}
}
private void AppendLog(string line) private void AppendLog(string line)
{ {
@@ -254,13 +331,14 @@ public sealed partial class MainPage : Page
EndCardBox.Text.Trim(), EndCardBox.Text.Trim(),
FfmpegPathBox.Text.Trim(), FfmpegPathBox.Text.Trim(),
FfprobePathBox.Text.Trim(), FfprobePathBox.Text.Trim(),
_loadedSettings.PreviewPlayerPath, PreviewPlayerPathBox.Text.Trim(),
SelectedTheme(), SelectedTheme(),
SelectedEncoder(), SelectedEncoder(),
TrimBox.Text.Trim(), TrimBox.Text.Trim(),
FadeBox.Text.Trim(), FadeBox.Text.Trim(),
HoldBox.Text.Trim(), HoldBox.Text.Trim(),
IntervalBox.Text.Trim())); IntervalBox.Text.Trim(),
MoveSourcesBox.IsChecked == true));
} }
private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark"; private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark";
@@ -302,9 +380,8 @@ public sealed partial class MainPage : Page
private void ApplyTheme(string theme) private void ApplyTheme(string theme)
{ {
RequestedTheme = string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase) bool isDark = !string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase);
? ElementTheme.Light App.MainWindowInstance?.ApplyTheme(isDark);
: ElementTheme.Dark;
} }
private FileOpenPicker CreateFileOpenPicker() private FileOpenPicker CreateFileOpenPicker()
@@ -333,13 +410,10 @@ public sealed partial class MainPage : Page
private async Task ShowMessageAsync(string title, string message) private async Task ShowMessageAsync(string title, string message)
{ {
ContentDialog dialog = new() ContentDialog dialog = DialogHelper.CreateStyled(XamlRoot, ActualTheme);
{ dialog.Title = title;
Title = title, dialog.Content = message;
Content = message, dialog.CloseButtonText = "OK";
CloseButtonText = "OK",
XamlRoot = XamlRoot
};
await dialog.ShowAsync(); await dialog.ShowAsync();
} }
} }
+5 -7
View File
@@ -8,24 +8,22 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="AmiReel" Title="AmiReel"
mc:Ignorable="d"> mc:Ignorable="d">
<Window.SystemBackdrop> <Grid Background="{ThemeResource PageBrush}">
<MicaBackdrop />
</Window.SystemBackdrop>
<Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid x:Name="AppTitleBar" Height="44" Background="Transparent"> <Grid x:Name="AppTitleBar" Grid.Row="0" Height="44" Background="{ThemeResource PanelBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="14,0,0,0"> <StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="14,0,0,0">
<Image Source="ms-appx:///Assets/Square44x44Logo.scale-200.png" <Image Source="ms-appx:///Assets/Square44x44Logo.scale-200.png"
Width="20" Width="20"
Height="20" Height="20"
Stretch="Uniform" /> Stretch="Uniform" />
<TextBlock Text="AmiReel" <TextBlock Text="AmiReel"
FontSize="16" FontFamily="Bahnschrift"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</StackPanel> </StackPanel>
</Grid> </Grid>
+85
View File
@@ -1,6 +1,13 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System; using System;
using System.IO; 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, // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info. // and more about our project templates, see: http://aka.ms/winui-project-info.
@@ -14,9 +21,12 @@ namespace AmiReel_WinUI;
/// </summary> /// </summary>
public sealed partial class MainWindow : Window public sealed partial class MainWindow : Window
{ {
private bool _exitConfirmed;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
App.MainWindowInstance = this;
ExtendsContentIntoTitleBar = true; ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar); SetTitleBar(AppTitleBar);
@@ -25,7 +35,82 @@ public sealed partial class MainWindow : Window
if (!string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath)) if (!string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath))
AppWindow.SetIcon(executablePath); AppWindow.SetIcon(executablePath);
SetDefaultSizeAndPosition();
// Navigate the root frame to the main page on startup. // Navigate the root frame to the main page on startup.
RootFrame.Navigate(typeof(MainPage)); 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();
} }
} }
+2 -2
View File
@@ -37,11 +37,11 @@
<uap:VisualElements <uap:VisualElements
DisplayName="AmiReel" DisplayName="AmiReel"
Description="AmiReel" Description="AmiReel"
BackgroundColor="transparent" BackgroundColor="#0E1525"
Square150x150Logo="Assets\Square150x150Logo.png" Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png"> Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" /> <uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" /> <uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#0E1525" />
</uap:VisualElements> </uap:VisualElements>
</Application> </Application>
</Applications> </Applications>
+4
View File
@@ -31,6 +31,10 @@
<EmbeddedResource Include="ThirdParty\ffprobe.exe" Condition="Exists('ThirdParty\ffprobe.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffprobe.exe" /> <EmbeddedResource Include="ThirdParty\ffprobe.exe" Condition="Exists('ThirdParty\ffprobe.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffprobe.exe" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Resource Include="assets\AmiReel.ico" Condition="Exists('assets\AmiReel.ico')" />
</ItemGroup>
<Target Name="RequireFfmpegForPublish" BeforeTargets="Publish"> <Target Name="RequireFfmpegForPublish" BeforeTargets="Publish">
<Error Condition="!Exists('ThirdParty\ffmpeg.exe')" Text="ThirdParty\ffmpeg.exe is required for a portable publish." /> <Error Condition="!Exists('ThirdParty\ffmpeg.exe')" Text="ThirdParty\ffmpeg.exe is required for a portable publish." />
<Error Condition="!Exists('ThirdParty\ffprobe.exe')" Text="ThirdParty\ffprobe.exe is required for a portable publish." /> <Error Condition="!Exists('ThirdParty\ffprobe.exe')" Text="ThirdParty\ffprobe.exe is required for a portable publish." />
+42
View File
@@ -377,5 +377,47 @@
<Style TargetType="TextBlock"> <Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> <Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
</Style> </Style>
<Style x:Key="TitleBarButton" TargetType="Button">
<Setter Property="Width" Value="46"/>
<Setter Property="Height" Value="40"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontFamily" Value="Segoe Fluent Icons, Segoe MDL2 Assets"/>
<Setter Property="FontSize" Value="11"/>
<Setter Property="Cursor" Value="Arrow"/>
<Setter Property="Focusable" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bg" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bg" Property="Background" Value="{DynamicResource ButtonHoverBrush}"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TitleBarCloseButton" TargetType="Button" BasedOn="{StaticResource TitleBarButton}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border x:Name="Bg" Background="{TemplateBinding Background}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Bg" Property="Background" Value="#E81123"/>
<Setter Property="Foreground" Value="White"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources> </Application.Resources>
</Application> </Application>
+76 -3
View File
@@ -2,8 +2,45 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AmiReel" Height="860" Width="1280" MinHeight="780" MinWidth="1100" Title="AmiReel" Height="860" Width="1280" MinHeight="780" MinWidth="1100"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen"
WindowStyle="None"
ResizeMode="CanResize"
Icon="assets/AmiReel.ico">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="40"
ResizeBorderThickness="6"
GlassFrameThickness="0"
UseAeroCaptionButtons="False"/>
</WindowChrome.WindowChrome>
<Grid Background="{DynamicResource PageBrush}"> <Grid Background="{DynamicResource PageBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid Grid.Row="0" Background="{DynamicResource PanelBrush}" Height="40">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Image Source="assets/AmiReel.ico" Width="18" Height="18" Margin="14,0,8,0" VerticalAlignment="Center"
WindowChrome.IsHitTestVisibleInChrome="False"/>
<TextBlock Grid.Column="1" Text="AmiReel" VerticalAlignment="Center" FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}" WindowChrome.IsHitTestVisibleInChrome="False"/>
<Button x:Name="MinimizeButton" Grid.Column="2" Content="&#xE921;" Style="{StaticResource TitleBarButton}"
Click="Minimize_Click" ToolTip="Minimize" WindowChrome.IsHitTestVisibleInChrome="True"/>
<Button x:Name="MaximizeButton" Grid.Column="3" Content="&#xE922;" Style="{StaticResource TitleBarButton}"
Click="MaximizeRestore_Click" ToolTip="Maximize" WindowChrome.IsHitTestVisibleInChrome="True"/>
<Button x:Name="CloseWindowButton" Grid.Column="4" Content="&#xE8BB;" Style="{StaticResource TitleBarCloseButton}"
Click="CloseWindowButton_Click" ToolTip="Close" WindowChrome.IsHitTestVisibleInChrome="True"/>
</Grid>
<Grid Grid.Row="1">
<Grid> <Grid>
<Grid.Background> <Grid.Background>
<DrawingBrush Stretch="None" Viewport="0,0,36,36" ViewportUnits="Absolute" Opacity="0.08"> <DrawingBrush Stretch="None" Viewport="0,0,36,36" ViewportUnits="Absolute" Opacity="0.08">
@@ -40,10 +77,12 @@
<Grid> <Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="98"/><RowDefinition Height="Auto"/></Grid.RowDefinitions> <Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="98"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8"> <StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<Button Content="Add AVI files…" Click="AddInputs_Click" Margin="0,0,8,0"/> <Button Content="Add video files…" Click="AddInputs_Click" Margin="0,0,8,0"/>
<Button Content="Clear" Click="ClearInputs_Click"/> <Button Content="Clear" Click="ClearInputs_Click"/>
</StackPanel> </StackPanel>
<ListBox x:Name="InputList" Grid.Row="1" SelectionChanged="InputList_SelectionChanged"/> <ListBox x:Name="InputList" Grid.Row="1" SelectionChanged="InputList_SelectionChanged"
AllowDrop="True" DragEnter="InputList_DragEnter" DragOver="InputList_DragEnter" Drop="InputList_Drop"
ToolTip="Drag and drop video files here to add them"/>
<Button x:Name="PreviewSourceButton" Grid.Row="2" Content="Preview selected" Click="PreviewSource_Click" <Button x:Name="PreviewSourceButton" Grid.Row="2" Content="Preview selected" Click="PreviewSource_Click"
HorizontalAlignment="Left" Margin="0,10,0,0" IsEnabled="False"/> HorizontalAlignment="Left" Margin="0,10,0,0" IsEnabled="False"/>
</Grid> </Grid>
@@ -148,6 +187,18 @@
</Grid> </Grid>
</GroupBox> </GroupBox>
<GroupBox Header="Source files">
<StackPanel>
<CheckBox x:Name="MoveSourcesBox"
Content="Move source videos to destination\originals"
IsChecked="True"
Margin="0,0,0,6"/>
<TextBlock Foreground="{DynamicResource MutedBrush}"
TextWrapping="Wrap"
Text="After a successful render, original recordings are moved into an originals subfolder of the output folder. Uncheck to leave them where they are."/>
</StackPanel>
</GroupBox>
<GroupBox Header="Video encoding"> <GroupBox Header="Video encoding">
<Grid> <Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions> <Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
@@ -203,4 +254,26 @@
</Border> </Border>
</Grid> </Grid>
</Grid> </Grid>
<Border x:Name="ExitConfirmOverlay" Grid.RowSpan="2" Background="#88060B14" Visibility="Collapsed">
<Border Width="420"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource PanelBrush}"
BorderBrush="{DynamicResource HeroBorderBrush}"
BorderThickness="1.5"
CornerRadius="20"
Padding="24">
<StackPanel>
<TextBlock Text="Exit AmiReel?" FontSize="20" FontWeight="SemiBold" Margin="0,0,0,10"/>
<TextBlock Text="Are you sure you want to close the application?"
Foreground="{DynamicResource MutedBrush}" TextWrapping="Wrap" Margin="0,0,0,20"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Cancel" Click="CancelExit_Click" MinWidth="110" Margin="0,0,10,0"/>
<Button Content="Exit" Style="{StaticResource PrimaryButton}" Click="ConfirmExit_Click" MinWidth="110"/>
</StackPanel>
</StackPanel>
</Border>
</Border>
</Grid>
</Window> </Window>
+89 -5
View File
@@ -2,9 +2,11 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Interop;
using AmigaDB.VideoRenderer.Models; using AmigaDB.VideoRenderer.Models;
using AmigaDB.VideoRenderer.Services; using AmigaDB.VideoRenderer.Services;
using Microsoft.Win32; using Microsoft.Win32;
@@ -21,6 +23,8 @@ public partial class MainWindow : Window
private CancellationTokenSource? _renderCancellation; private CancellationTokenSource? _renderCancellation;
private string? _lastRenderedFile; private string? _lastRenderedFile;
private bool _logFlushScheduled; private bool _logFlushScheduled;
private bool _titleBarIsLight;
private bool _exitConfirmed;
public MainWindow() public MainWindow()
{ {
@@ -39,12 +43,52 @@ public partial class MainWindow : Window
SelectEncoder(_loadedSettings.Encoder); SelectEncoder(_loadedSettings.Encoder);
SelectTheme(_loadedSettings.Theme); SelectTheme(_loadedSettings.Theme);
ApplyTheme(_loadedSettings.Theme); ApplyTheme(_loadedSettings.Theme);
Closing += (_, _) => SaveSettings(); MoveSourcesBox.IsChecked = _loadedSettings.ShouldMoveSourcesToOriginals;
Closing += MainWindow_Closing;
SourceInitialized += (_, _) => UpdateTitleBarTheme(_titleBarIsLight);
StateChanged += (_, _) => MaximizeButton.Content = WindowState == WindowState.Maximized ? "\uE923" : "\uE922";
}
private void Minimize_Click(object sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
private void MaximizeRestore_Click(object sender, RoutedEventArgs e)
=> WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
private void CloseWindowButton_Click(object sender, RoutedEventArgs e) => Close();
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
if (_exitConfirmed) return;
e.Cancel = true;
ExitConfirmOverlay.Visibility = Visibility.Visible;
}
private void CancelExit_Click(object sender, RoutedEventArgs e) => ExitConfirmOverlay.Visibility = Visibility.Collapsed;
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) 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; if (dialog.ShowDialog(this) != true) return;
foreach (string file in dialog.FileNames.OrderBy(NaturalKey)) foreach (string file in dialog.FileNames.OrderBy(NaturalKey))
if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file); if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file);
@@ -100,7 +144,8 @@ public partial class MainWindow : Window
LogBox.Clear(); LogBox.Clear();
_renderCancellation = new CancellationTokenSource(); _renderCancellation = new CancellationTokenSource();
Progress<RenderProgress> progress = new(UpdateProgress); Progress<RenderProgress> progress = new(UpdateProgress);
await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token); IReadOnlyList<string> resolvedInputs = await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
ReplaceInputs(resolvedInputs);
_lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4"); _lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4");
OpenOutputButton.IsEnabled = true; OpenOutputButton.IsEnabled = true;
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile); PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
@@ -138,7 +183,15 @@ public partial class MainWindow : Window
EncoderMode encoder = Enum.Parse<EncoderMode>(((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!); EncoderMode encoder = Enum.Parse<EncoderMode>(((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!);
return new(_inputs.ToList(), EndCardBox.Text.Trim(), OutputFolderBox.Text.Trim(), OutputNameBox.Text.Trim(), return new(_inputs.ToList(), EndCardBox.Text.Trim(), OutputFolderBox.Text.Trim(), OutputNameBox.Text.Trim(),
FfmpegPathBox.Text.Trim(), FfprobePathBox.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); encoder, Number(TrimBox.Text, "trim start"), Number(FadeBox.Text, "fade"), Number(HoldBox.Text, "end-card hold"), interval,
MoveSourcesBox.IsChecked == true);
}
private void ReplaceInputs(IReadOnlyList<string> paths)
{
_inputs.Clear();
foreach (string path in paths)
_inputs.Add(path);
} }
private void Cancel_Click(object sender, RoutedEventArgs e) private void Cancel_Click(object sender, RoutedEventArgs e)
@@ -242,7 +295,8 @@ public partial class MainWindow : Window
TrimBox.Text.Trim(), TrimBox.Text.Trim(),
FadeBox.Text.Trim(), FadeBox.Text.Trim(),
HoldBox.Text.Trim(), HoldBox.Text.Trim(),
IntervalBox.Text.Trim())); IntervalBox.Text.Trim(),
MoveSourcesBox.IsChecked == true));
} }
private void OpenPreview(string mediaPath) private void OpenPreview(string mediaPath)
@@ -332,10 +386,40 @@ public partial class MainWindow : Window
SetBrush("PrimaryTextBrush", light ? "#FFFFFF" : "#07111D"); SetBrush("PrimaryTextBrush", light ? "#FFFFFF" : "#07111D");
SetBrush("SelectionBrush", light ? "#BEE4FF" : "#295C87"); SetBrush("SelectionBrush", light ? "#BEE4FF" : "#295C87");
SetBrush("SelectionTextBrush", light ? "#122033" : "#FFFFFF"); SetBrush("SelectionTextBrush", light ? "#122033" : "#FFFFFF");
_titleBarIsLight = light;
UpdateTitleBarTheme(light);
} }
private void SetBrush(string key, string color) private void SetBrush(string key, string color)
{ {
Application.Current.Resources[key] = new SolidColorBrush((Color)ColorConverter.ConvertFromString(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);
} }
+8 -3
View File
@@ -11,8 +11,11 @@ public sealed record AppSettings(
string TrimStart, string TrimStart,
string FadeSeconds, string FadeSeconds,
string EndCardHoldSeconds, string EndCardHoldSeconds,
string ThumbnailInterval) string ThumbnailInterval,
bool? MoveSourcesToOriginals)
{ {
public bool ShouldMoveSourcesToOriginals => MoveSourcesToOriginals != false;
public static AppSettings Default(string outputFolder) => new( public static AppSettings Default(string outputFolder) => new(
outputFolder, outputFolder,
"", "",
@@ -24,7 +27,8 @@ public sealed record AppSettings(
"4.414", "4.414",
"3", "3",
"4", "4",
"10"); "10",
true);
public AppSettings Normalize(string fallbackOutputFolder) => new( public AppSettings Normalize(string fallbackOutputFolder) => new(
string.IsNullOrWhiteSpace(OutputFolder) ? fallbackOutputFolder : OutputFolder, string.IsNullOrWhiteSpace(OutputFolder) ? fallbackOutputFolder : OutputFolder,
@@ -37,5 +41,6 @@ public sealed record AppSettings(
string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart, string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
string.IsNullOrWhiteSpace(FadeSeconds) ? "3" : FadeSeconds, string.IsNullOrWhiteSpace(FadeSeconds) ? "3" : FadeSeconds,
string.IsNullOrWhiteSpace(EndCardHoldSeconds) ? "4" : EndCardHoldSeconds, string.IsNullOrWhiteSpace(EndCardHoldSeconds) ? "4" : EndCardHoldSeconds,
string.IsNullOrWhiteSpace(ThumbnailInterval) ? "10" : ThumbnailInterval); string.IsNullOrWhiteSpace(ThumbnailInterval) ? "10" : ThumbnailInterval,
MoveSourcesToOriginals ?? true);
} }
+1
View File
@@ -19,6 +19,7 @@ public sealed record RenderSettings(
double FadeSeconds, double FadeSeconds,
double EndCardHoldSeconds, double EndCardHoldSeconds,
int ThumbnailInterval, int ThumbnailInterval,
bool MoveSourcesToOriginals = true,
int Width = 3840, int Width = 3840,
int Height = 2160, int Height = 2160,
int FramesPerSecond = 50, int FramesPerSecond = 50,
+23
View File
@@ -0,0 +1,23 @@
using System.IO;
using System.Linq;
namespace AmigaDB.VideoRenderer.Models;
/// <summary>
/// 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.
/// </summary>
public static class SupportedVideoFormats
{
public static readonly IReadOnlyList<string> 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);
/// <summary>Picker filter string, e.g. "*.avi;*.mp4;*.m4v;...".</summary>
public static string PickerFilter => string.Join(';', Extensions.Select(e => "*" + e));
}
+2
View File
@@ -25,6 +25,7 @@ The repository currently contains two desktop frontends that share the same rend
- FFmpeg log output - FFmpeg log output
- source video preview and final render preview - source video preview and final render preview
- dark and light themes - dark and light themes
- optional move of source recordings into `originals` in the output folder
- optional custom `ffmpeg.exe` / `ffprobe.exe` paths - optional custom `ffmpeg.exe` / `ffprobe.exe` paths
- automatic FFmpeg detection from `PATH` - automatic FFmpeg detection from `PATH`
- single-file WinUI publish for easier distribution - single-file WinUI publish for easier distribution
@@ -167,6 +168,7 @@ This includes values such as:
- theme - theme
- encoder selection - encoder selection
- timing settings - timing settings
- whether source videos are moved into `originals` after a successful render
## Notes for Distribution ## Notes for Distribution
+76 -3
View File
@@ -15,7 +15,7 @@ public sealed class RenderPipeline
_probe = new MediaProbe(_runner, null); _probe = new MediaProbe(_runner, null);
} }
public async Task RenderAsync( public async Task<IReadOnlyList<string>> RenderAsync(
RenderSettings settings, RenderSettings settings,
IProgress<RenderProgress> progress, IProgress<RenderProgress> progress,
Action<string> log, Action<string> log,
@@ -105,7 +105,16 @@ public sealed class RenderPipeline
finalArgs.AddRange(EncoderArguments(encoder)); finalArgs.AddRange(EncoderArguments(encoder));
finalArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-movflags", "+faststart", final]); finalArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-movflags", "+faststart", final]);
await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, settings.FramesPerSecond, token); await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, settings.FramesPerSecond, token);
IReadOnlyList<string> inputPaths = settings.InputFiles;
if (settings.MoveSourcesToOriginals)
{
progress.Report(new(99.5, "Moving sources", "Moving original recordings to originals"));
inputPaths = MoveSourceVideos(settings.InputFiles, settings.OutputDirectory, log);
}
progress.Report(new(100, "Complete", final)); progress.Report(new(100, "Complete", final));
return inputPaths;
} }
finally finally
{ {
@@ -285,10 +294,74 @@ public sealed class RenderPipeline
return fallbackPath; return fallbackPath;
} }
internal static IReadOnlyList<string> MoveSourceVideos(
IReadOnlyList<string> sources,
string outputDirectory,
Action<string> log)
{
string originalsDir = Path.Combine(outputDirectory, "originals");
Directory.CreateDirectory(originalsDir);
List<string> resolved = new(sources.Count);
HashSet<string> claimed = new(StringComparer.OrdinalIgnoreCase);
foreach (string source in sources)
{
if (!File.Exists(source))
{
log($"WARNING: Source file no longer exists, skipped move: {source}");
resolved.Add(source);
continue;
}
string sourceFull = Path.GetFullPath(source);
string preferred = Path.GetFullPath(Path.Combine(originalsDir, Path.GetFileName(source)));
if (string.Equals(sourceFull, preferred, StringComparison.OrdinalIgnoreCase))
{
claimed.Add(preferred);
resolved.Add(preferred);
continue;
}
string dest = UniqueDestination(preferred, claimed);
try
{
File.Move(source, dest);
claimed.Add(dest);
log($"Moved source to {dest}");
resolved.Add(dest);
}
catch (Exception exception)
{
log($"WARNING: Could not move source '{source}' to originals: {exception.Message}");
resolved.Add(source);
}
}
return resolved;
}
private static string UniqueDestination(string dest, HashSet<string> claimed)
{
string full = Path.GetFullPath(dest);
if (!File.Exists(full) && !claimed.Contains(full))
return full;
string directory = Path.GetDirectoryName(full)!;
string name = Path.GetFileNameWithoutExtension(full);
string extension = Path.GetExtension(full);
for (int index = 2; ; index++)
{
string candidate = Path.Combine(directory, $"{name}_{index}{extension}");
if (!File.Exists(candidate) && !claimed.Contains(candidate))
return candidate;
}
}
private static void Validate(RenderSettings s) private static void Validate(RenderSettings s)
{ {
if (s.InputFiles.Count == 0) throw new ArgumentException("Select at least one AVI input file."); 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 AVI files no longer exist."); 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)) if (!string.IsNullOrWhiteSpace(s.EndCardPath) && !File.Exists(s.EndCardPath))
throw new FileNotFoundException("End-card image was not found."); throw new FileNotFoundException("End-card image was not found.");
if (string.IsNullOrWhiteSpace(s.OutputDirectory)) throw new ArgumentException("Select an output directory."); if (string.IsNullOrWhiteSpace(s.OutputDirectory)) throw new ArgumentException("Select an output directory.");