Some fixes

This commit is contained in:
2026-08-13 12:39:03 +02:00
parent 027fc683ef
commit ab46a16726
11 changed files with 373 additions and 14 deletions
+16 -1
View File
@@ -31,7 +31,10 @@
<ListView x:Name="InputList"
Height="120"
SelectionMode="Single"
SelectionChanged="InputList_SelectionChanged" />
SelectionChanged="InputList_SelectionChanged"
AllowDrop="True"
DragOver="InputList_DragOver"
Drop="InputList_Drop" />
<Button x:Name="PreviewSourceButton"
Content="Preview selected"
Click="PreviewSource_Click"
@@ -163,6 +166,18 @@
</StackPanel>
</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">
<StackPanel Spacing="10">
<TextBlock Text="FFmpeg tools" Style="{StaticResource SectionTitleStyle}" />
+35 -3
View File
@@ -7,6 +7,8 @@ using AmigaDB.VideoRenderer.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer;
using Windows.Storage;
using Windows.Storage.Pickers;
using WinRT.Interop;
@@ -39,6 +41,7 @@ public sealed partial class MainPage : Page
SelectEncoder(_loadedSettings.Encoder);
SelectTheme(_loadedSettings.Theme);
ApplyTheme(_loadedSettings.Theme);
MoveSourcesBox.IsChecked = _loadedSettings.ShouldMoveSourcesToOriginals;
}
private async void AddInputs_Click(object sender, RoutedEventArgs e)
@@ -55,6 +58,25 @@ public sealed partial class MainPage : Page
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;
IReadOnlyList<IStorageItem> items = await e.DataView.GetStorageItemsAsync();
foreach (string path in items.OfType<StorageFile>()
.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);
}
private async void BrowseEndCard_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
@@ -111,7 +133,8 @@ public sealed partial class MainPage : Page
LogBox.Text = string.Empty;
_renderCancellation = new CancellationTokenSource();
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");
OpenOutputButton.IsEnabled = true;
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
@@ -192,7 +215,15 @@ public sealed partial class MainPage : Page
Number(TrimBox.Text, "trim start"),
Number(FadeBox.Text, "fade"),
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)
@@ -260,7 +291,8 @@ public sealed partial class MainPage : Page
TrimBox.Text.Trim(),
FadeBox.Text.Trim(),
HoldBox.Text.Trim(),
IntervalBox.Text.Trim()));
IntervalBox.Text.Trim(),
MoveSourcesBox.IsChecked == true));
}
private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark";
+28
View File
@@ -1,4 +1,6 @@
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.IO;
@@ -14,6 +16,8 @@ namespace AmiReel_WinUI;
/// </summary>
public sealed partial class MainWindow : Window
{
private bool _exitConfirmed;
public MainWindow()
{
InitializeComponent();
@@ -27,5 +31,29 @@ public sealed partial class MainWindow : Window
// Navigate the root frame to the main page on startup.
RootFrame.Navigate(typeof(MainPage));
AppWindow.Closing += AppWindow_Closing;
}
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
};
ContentDialogResult result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary) return;
_exitConfirmed = true;
Close();
}
}
+4
View File
@@ -31,6 +31,10 @@
<EmbeddedResource Include="ThirdParty\ffprobe.exe" Condition="Exists('ThirdParty\ffprobe.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffprobe.exe" />
</ItemGroup>
<ItemGroup>
<Resource Include="assets\AmiReel.ico" Condition="Exists('assets\AmiReel.ico')" />
</ItemGroup>
<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\ffprobe.exe')" Text="ThirdParty\ffprobe.exe is required for a portable publish." />
+42
View File
@@ -377,5 +377,47 @@
<Style TargetType="TextBlock">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
</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>
+75 -2
View File
@@ -2,8 +2,45 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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.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.Background>
<DrawingBrush Stretch="None" Viewport="0,0,36,36" ViewportUnits="Absolute" Opacity="0.08">
@@ -43,7 +80,9 @@
<Button Content="Add AVI files…" Click="AddInputs_Click" Margin="0,0,8,0"/>
<Button Content="Clear" Click="ClearInputs_Click"/>
</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 AVI files here to add them"/>
<Button x:Name="PreviewSourceButton" Grid.Row="2" Content="Preview selected" Click="PreviewSource_Click"
HorizontalAlignment="Left" Margin="0,10,0,0" IsEnabled="False"/>
</Grid>
@@ -148,6 +187,18 @@
</Grid>
</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">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="*"/></Grid.ColumnDefinitions>
@@ -203,4 +254,26 @@
</Border>
</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>
+88 -4
View File
@@ -2,9 +2,11 @@ using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
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;
@@ -21,6 +23,8 @@ public partial class MainWindow : Window
private CancellationTokenSource? _renderCancellation;
private string? _lastRenderedFile;
private bool _logFlushScheduled;
private bool _titleBarIsLight;
private bool _exitConfirmed;
public MainWindow()
{
@@ -39,7 +43,47 @@ public partial class MainWindow : Window
SelectEncoder(_loadedSettings.Encoder);
SelectTheme(_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(f => string.Equals(Path.GetExtension(f), ".avi", StringComparison.OrdinalIgnoreCase)).OrderBy(NaturalKey))
if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file);
}
private void AddInputs_Click(object sender, RoutedEventArgs e)
@@ -100,7 +144,8 @@ public partial class MainWindow : Window
LogBox.Clear();
_renderCancellation = new CancellationTokenSource();
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");
OpenOutputButton.IsEnabled = true;
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
@@ -138,7 +183,15 @@ public partial class MainWindow : Window
EncoderMode encoder = Enum.Parse<EncoderMode>(((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);
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)
@@ -242,7 +295,8 @@ public partial class MainWindow : Window
TrimBox.Text.Trim(),
FadeBox.Text.Trim(),
HoldBox.Text.Trim(),
IntervalBox.Text.Trim()));
IntervalBox.Text.Trim(),
MoveSourcesBox.IsChecked == true));
}
private void OpenPreview(string mediaPath)
@@ -332,10 +386,40 @@ public partial class MainWindow : Window
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);
}
+8 -3
View File
@@ -11,8 +11,11 @@ public sealed record AppSettings(
string TrimStart,
string FadeSeconds,
string EndCardHoldSeconds,
string ThumbnailInterval)
string ThumbnailInterval,
bool? MoveSourcesToOriginals)
{
public bool ShouldMoveSourcesToOriginals => MoveSourcesToOriginals != false;
public static AppSettings Default(string outputFolder) => new(
outputFolder,
"",
@@ -24,7 +27,8 @@ public sealed record AppSettings(
"4.414",
"3",
"4",
"10");
"10",
true);
public AppSettings Normalize(string fallbackOutputFolder) => new(
string.IsNullOrWhiteSpace(OutputFolder) ? fallbackOutputFolder : OutputFolder,
@@ -37,5 +41,6 @@ public sealed record AppSettings(
string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
string.IsNullOrWhiteSpace(FadeSeconds) ? "3" : FadeSeconds,
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 EndCardHoldSeconds,
int ThumbnailInterval,
bool MoveSourcesToOriginals = true,
int Width = 3840,
int Height = 2160,
int FramesPerSecond = 50,
+2
View File
@@ -25,6 +25,7 @@ The repository currently contains two desktop frontends that share the same rend
- 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
@@ -167,6 +168,7 @@ This includes values such as:
- theme
- encoder selection
- timing settings
- whether source videos are moved into `originals` after a successful render
## Notes for Distribution
+74 -1
View File
@@ -15,7 +15,7 @@ public sealed class RenderPipeline
_probe = new MediaProbe(_runner, null);
}
public async Task RenderAsync(
public async Task<IReadOnlyList<string>> RenderAsync(
RenderSettings settings,
IProgress<RenderProgress> progress,
Action<string> log,
@@ -105,7 +105,16 @@ public sealed class RenderPipeline
finalArgs.AddRange(EncoderArguments(encoder));
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);
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));
return inputPaths;
}
finally
{
@@ -285,6 +294,70 @@ public sealed class RenderPipeline
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)
{
if (s.InputFiles.Count == 0) throw new ArgumentException("Select at least one AVI input file.");