Compare commits

...

7 Commits

Author SHA1 Message Date
klevze 58e11ba4e3 Add YouTube Shorts tab (C# port of amigadb-short.sh)
New "Video | Shorts" tab strip on the main page. The existing Video tab and
its render pipeline are unchanged; Shorts is a new, independent workflow
sharing the same render-progress/log/output controls.

- Services/ShortsPipeline.cs: C# port of amigadb-short.sh rather than
  shelling out to bash — end users on plain Windows don't have bash/WSL, and
  shipping this as a native part of the app keeps distribution self-contained.
  All 19 visual style filtergraphs (brand, pixel, mirror, crop, workbench,
  blur, crt, copper, stars, grid, tiles, scan, starfield, plasma,
  rasterbars, vhs, monitor, split, spectrum) are copied verbatim from the
  bash heredocs. Per explicit decision, kept parity with the script's actual
  behavior where the brand-style HOOK_BOX_Y/INFO_BOX_Y/font-size variables
  are computed but never actually used in the final filter (dead code in
  the original) rather than "fixing" it and changing rendered output.
  Reuses ToolExtractor/MediaProbe/ProcessRunner exactly as RenderPipeline
  does, so FFmpeg resolution and NVENC fallback behave identically.

- Models/ShortSettings.cs: job parameters + ShortStyle enum.

- AppSettings gains sticky Shorts defaults (style, hook, website, font,
  background image, CRF, preset) alongside the existing Video defaults.

- MainPage: Video/Shorts tab buttons toggle two content panels; the
  background-image field auto-hides for non-brand styles; font field
  defaults to the first bold system font found (Segoe UI Bold, Segoe UI
  Semibold, Arial Bold, Calibri Bold); blank output path is auto-derived
  from the input filename and style, matching the script's default.

- 13 new unit tests (escaping order, meta-line joining, validation,
  every style producing a non-empty filter, the drawtext trailer's timing
  gates). Full run is 72/72 passing.

- Fixed a bug caught during manual testing: MainPage.Resources["PrimaryButtonStyle"]
  looked in the Page's own (empty) resource dictionary instead of
  Application.Current.Resources, throwing on every tab switch.

Verified end-to-end: rendered a real Short from an existing video through
the actual running app (not just unit tests) and inspected extracted
frames — hook/title/meta/website text overlays appear and disappear at the
correct timestamps with correct styling.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 17:30:25 +02:00
klevze f1f1be778b Fix missing taskbar/Alt+Tab icon
AppWindow.SetIcon(executablePath) was passing the .exe's own path, but that
API requires an actual .ico file path — passing an .exe silently does
nothing, so the window fell back to a generic placeholder icon in the
taskbar and Alt+Tab (the custom title bar icon fixed earlier is a separate,
XAML-drawn Image and was unaffected).

Embed branding/AmiReel.ico as a resource, extract it once to
%LOCALAPPDATA%\AmiReel\app-icon.ico on startup, and call
AppWindow.SetIcon() with that file path instead. Verified via WM_GETICON
against the running window's HWND that both the big and small icons now
resolve to the real AmiReel logo.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 16:38:12 +02:00
klevze fc02cd9602 Update README project layout for tests and new Services files
Reflect the current repo structure: AmiReel.Tests/, Properties/AssemblyInfo.cs,
and the FfmpegProgressParser/FfmpegOutputFilter split out of ProcessRunner.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 16:27:15 +02:00
klevze 8f27aee11a Fix title bar icon missing in single-file published builds
The title bar Image used Source="ms-appx:///Assets/Square44x44Logo.scale-200.png".
That resolves fine in a normal build (Assets/ and AmiReel.pri sit next to the exe),
but a single-file self-contained publish (IncludeAllContentForSelfExtract=true)
bundles Content items into the exe in a way ms-appx:// can no longer resolve at
runtime, leaving the title bar icon blank — reproduced with the actual
publish-win-x64.ps1 output, confirmed fixed the same way.

Load the icon from a plain embedded resource instead (same mechanism already
used for ffmpeg.exe/ffprobe.exe): the PNG is embedded via
<EmbeddedResource LogicalName="AmiReel.Assets.TitleBarIcon.png">, and
MainWindow loads it at startup via GetManifestResourceStream + BitmapImage.
SetSourceAsync. This bypasses the ms-appx/MRT resource pipeline entirely, so
it behaves identically in dotnet build, dotnet run, and a single-file publish.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 16:01:20 +02:00
klevze c412469773 Add unit tests, dedupe FFmpeg output filtering, harden settings records
Implements the suggestions from the last review pass:

- Add AmiReel.Tests (MSTest), covering the pure logic in Models/ and
  Services/: SupportedVideoFormats, AppSettings normalization,
  UserFacingErrors.Summarize, the new FfmpegProgressParser/
  FfmpegOutputFilter, RenderPipeline.Validate, BuildProgressMessage, and
  MoveSourceVideos. 59 tests, all passing. UI code-behind and anything that
  spawns an actual FFmpeg process are left to manual/integration testing.
  Exclude AmiReel.Tests\**\*.cs from AmiReel.csproj's default item glob —
  it's a subfolder of the app project now, so without the exclude the app
  itself was compiling the MSTest-only test files.

- Extract the FFmpeg version/library-banner boilerplate list that
  ProcessRunner (live log filter) and UserFacingErrors (error summarizer)
  had each duplicated into a shared FfmpegOutputFilter.IsBoilerplateLine;
  each caller keeps its own remaining context-specific checks on top.
  Also extract ProcessRunner's line-parsing regexes into a standalone
  FfmpegProgressParser so it's directly unit-testable without spawning a
  process.

- Convert AppSettings and RenderSettings from positional record
  constructors to named `required` init properties. Both records had
  runs of same-typed consecutive parameters (three string timing fields
  in AppSettings; three doubles then five ints in RenderSettings) that a
  positional constructor would let get silently transposed at a call site
  without the compiler catching it. Update the two call sites
  (MainPage.xaml.cs) to object-initializer syntax.

- Add Properties/AssemblyInfo.cs with InternalsVisibleTo("AmiReel.Tests")
  and make Validate/BuildProgressMessage/IsNoise internal so tests can
  reach them directly instead of only through process-spawning entry
  points.

- README: document `dotnet test`, and note that Package.appxmanifest's
  Identity is a local-dev placeholder that needs a real publisher/cert
  before MSIX distribution.

Verified: dotnet build (solution + test project) is 0 warnings/errors,
dotnet test is 59/59 passing, and the app still launches unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:27:57 +02:00
klevze 3de3357d23 Code review cleanup: dead code, unused capability, accessibility gap
- Remove unused `using Microsoft.Win32;` from MainPage.xaml.cs (leftover
  from the WPF OpenFileDialog era; WinUI uses Windows.Storage.Pickers)
- Trim App.xaml.cs down to the two usings it actually needs instead of the
  full WinUI3 template boilerplate list
- Change App.MainWindowInstance to an internal setter (only MainWindow
  itself should assign it)
- Add AutomationProperties.Name to the icon-only Settings button so screen
  readers announce it (it only had a mouse tooltip before)
- Delete Assets/AppIcon.ico: unreferenced by both the manifest and code,
  the real app icon comes from ApplicationIcon (branding/AmiReel.ico) and
  AppWindow.SetIcon at runtime
- Drop the systemAIModels capability from Package.appxmanifest; the app
  doesn't call any Windows AI API, so declaring it is an unnecessary
  privilege request

Verified: dotnet build produces 0 warnings/errors, app launches unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:13:22 +02:00
klevze 603af17e56 Remove legacy WPF app, flatten WinUI project to repo root, rename namespace
The repo carried two parallel UIs (WPF + WinUI) sharing Models/Services via
cross-directory Link includes. Now that WinUI is the only frontend, collapse
the structure so the WinUI project IS the repo root instead of a nested
sibling folder:

- Delete the WPF project entirely (App.xaml, MainWindow.xaml, csproj) and its
  bin/obj output
- Move AmiReel.WinUI/* up to the repo root (App, MainWindow, MainPage,
  DialogHelper, Assets, Package.appxmanifest, app.manifest, Properties,
  .github/instructions, AGENTS.md) via git mv, preserving history
- Rename AmiReel.WinUI.csproj -> AmiReel.csproj; regenerate the solution as
  AmiReel.slnx (the newer XML solution format) with a single project
- Rename namespace AmigaDB.VideoRenderer.{Models,Services} -> AmiReel.{...}
  and AmiReel_WinUI -> AmiReel across all files, including the embedded
  ffmpeg/ffprobe resource logical names in the csproj and ToolExtractor
- Models/ and Services/ no longer need the Link-based cross-directory
  <Compile Include>; they're picked up by the SDK's default globbing now
  that they live under the project directory
- Rename assets/ -> branding/ (source icon art) to avoid a case-insensitive
  collision with Assets/ (packaged tile art) once both sit at repo root
- Merge the two .gitignore files into one; track the PublishProfiles pubxml
  files instead of ignoring them (no secrets, and they keep publish
  reproducible across machines) as branding, gitignore, etc.
- Simplify publish-win-x64.ps1 (drop the -Target Wpf/WinUI switch, there's
  only one target now) and rewrite README.md to describe the single-project
  layout, build/run/publish commands, and file structure

Verified: dotnet build succeeds for both AmiReel.csproj and AmiReel.slnx, and
the built exe launches and renders identically to before the move.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-13 14:07:10 +02:00
80 changed files with 3271 additions and 2114 deletions
+46 -5
View File
@@ -1,14 +1,55 @@
bin/ # Build output
obj/ [Bb]in/
[Oo]bj/
[Dd]ebug/
[Rr]elease/
publish/
# IDE / tooling
.vs/ .vs/
.vscode/ .vscode/
_ReSharper*/
*.DotSettings.user
*.dotCover
.idea/
# MSBuild / user files
*.user
*.suo
*.userosscache
*.sln.docstates
*.pdb *.pdb
*.cache *.cache
*.tmp *.tmp
*.log *.log
*.userosscache *.binlog
# Test results
TestResults/ TestResults/
[Tt]est[Rr]esult*/
*.trx
*.coverage
*.coveragexml
# NuGet
*.nupkg
*.snupkg
*.nuget.props
*.nuget.targets
project.lock.json
# MSIX packaging output
AppPackages/
BundleArtifacts/
*.msix
*.msixupload
*.appx
*.appxbundle
*.appxupload
# Local dev signing certificate
devcert.pfx
# Embedded third-party binaries (fetched separately, see ThirdParty/README.md)
ThirdParty/ffmpeg.exe ThirdParty/ffmpeg.exe
ThirdParty/ffprobe.exe ThirdParty/ffprobe.exe
*.user
*.suo
+20
View File
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="MSTest.TestAdapter" Version="*" />
<PackageReference Include="MSTest.TestFramework" Version="*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\AmiReel.csproj" />
</ItemGroup>
</Project>
+107
View File
@@ -0,0 +1,107 @@
using AmiReel.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Models;
[TestClass]
public class AppSettingsTests
{
[TestMethod]
public void Default_SetsFallbackOutputFolderAndSensibleDefaults()
{
// Arrange
const string outputFolder = @"C:\Videos";
// Act
AppSettings settings = AppSettings.Default(outputFolder);
// Assert
Assert.AreEqual(outputFolder, settings.OutputFolder);
Assert.AreEqual("Dark", settings.Theme);
Assert.AreEqual("Auto", settings.Encoder);
Assert.IsTrue(settings.ShouldMoveSourcesToOriginals);
}
[TestMethod]
public void ShouldMoveSourcesToOriginals_WhenFlagIsNull_DefaultsToTrue()
{
// Arrange
AppSettings settings = AppSettings.Default(@"C:\Videos") with { MoveSourcesToOriginals = null };
// Act
bool result = settings.ShouldMoveSourcesToOriginals;
// Assert
Assert.IsTrue(result);
}
[TestMethod]
public void ShouldMoveSourcesToOriginals_WhenFlagIsExplicitlyFalse_ReturnsFalse()
{
// Arrange
AppSettings settings = AppSettings.Default(@"C:\Videos") with { MoveSourcesToOriginals = false };
// Act
bool result = settings.ShouldMoveSourcesToOriginals;
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void Normalize_WithBlankOutputFolder_UsesFallback()
{
// Arrange
AppSettings settings = AppSettings.Default("") with { OutputFolder = " " };
// Act
AppSettings normalized = settings.Normalize(@"C:\Fallback");
// Assert
Assert.AreEqual(@"C:\Fallback", normalized.OutputFolder);
}
[TestMethod]
public void Normalize_WithBlankTimingValues_RestoresDefaults()
{
// Arrange
AppSettings settings = AppSettings.Default(@"C:\Videos") with
{
TrimStart = "",
FadeSeconds = " ",
EndCardHoldSeconds = "",
ThumbnailInterval = "",
};
// Act
AppSettings normalized = settings.Normalize(@"C:\Videos");
// Assert
Assert.AreEqual("4.414", normalized.TrimStart);
Assert.AreEqual("3", normalized.FadeSeconds);
Assert.AreEqual("4", normalized.EndCardHoldSeconds);
Assert.AreEqual("10", normalized.ThumbnailInterval);
}
[TestMethod]
public void Normalize_WithPopulatedValues_KeepsThemUnchanged()
{
// Arrange
AppSettings settings = AppSettings.Default(@"C:\Videos") with
{
TrimStart = "1.5",
FadeSeconds = "2.5",
Theme = "Light",
Encoder = "NvidiaNvenc",
};
// Act
AppSettings normalized = settings.Normalize(@"C:\Videos");
// Assert
Assert.AreEqual("1.5", normalized.TrimStart);
Assert.AreEqual("2.5", normalized.FadeSeconds);
Assert.AreEqual("Light", normalized.Theme);
Assert.AreEqual("NvidiaNvenc", normalized.Encoder);
}
}
@@ -0,0 +1,69 @@
using AmiReel.Models;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Models;
[TestClass]
public class SupportedVideoFormatsTests
{
[TestMethod]
[DataRow(@"C:\videos\clip.avi")]
[DataRow(@"C:\videos\clip.mp4")]
[DataRow(@"C:\videos\clip.MOV")]
[DataRow(@"C:\videos\clip.mkv")]
[DataRow(@"C:\videos\clip.webm")]
public void IsSupported_WithAcceptedExtension_ReturnsTrue(string path)
{
// Act
bool result = SupportedVideoFormats.IsSupported(path);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow(@"C:\documents\report.pdf")]
[DataRow(@"C:\images\photo.png")]
[DataRow(@"C:\audio\track.mp3")]
public void IsSupported_WithUnsupportedExtension_ReturnsFalse(string path)
{
// Act
bool result = SupportedVideoFormats.IsSupported(path);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void IsSupported_IsCaseInsensitive()
{
// Arrange
const string lower = @"C:\videos\clip.avi";
const string upper = @"C:\videos\clip.AVI";
// Act & Assert
Assert.IsTrue(SupportedVideoFormats.IsSupported(lower));
Assert.IsTrue(SupportedVideoFormats.IsSupported(upper));
}
[TestMethod]
public void IsSupported_WithNoExtension_ReturnsFalse()
{
// Act
bool result = SupportedVideoFormats.IsSupported(@"C:\videos\clip");
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void PickerFilter_ContainsEveryExtensionAsAGlobPattern()
{
// Act
string filter = SupportedVideoFormats.PickerFilter;
// Assert
foreach (string extension in SupportedVideoFormats.Extensions)
StringAssert.Contains(filter, "*" + extension);
}
}
@@ -0,0 +1,48 @@
using AmiReel.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Services;
[TestClass]
public class FfmpegOutputFilterTests
{
[TestMethod]
[DataRow("ffmpeg version 6.0-full_build-www.gyan.dev")]
[DataRow("built with gcc 12.2.0")]
[DataRow("configuration: --enable-gpl")]
[DataRow("libavutil 58. 2.100 / 58. 2.100")]
[DataRow("Input #0, avi, from 'clip.avi':")]
[DataRow("Output #0, mp4, to 'out.mp4':")]
[DataRow("Stream #0:0: Video: mjpeg")]
[DataRow("Duration: 00:01:00.00, start: 0.000000, bitrate: 128 kb/s")]
[DataRow("video:1024kB audio:128kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.5%")]
public void IsBoilerplateLine_WithKnownFfmpegBanner_ReturnsTrue(string line)
{
// Act
bool result = FfmpegOutputFilter.IsBoilerplateLine(line);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("Error while opening encoder for output stream #0:0")]
[DataRow("Cannot load nvcuda.dll")]
[DataRow("Unknown encoder 'h264_nvenc'")]
public void IsBoilerplateLine_WithRealErrorText_ReturnsFalse(string line)
{
// Act
bool result = FfmpegOutputFilter.IsBoilerplateLine(line);
// Assert
Assert.IsFalse(result);
}
[TestMethod]
public void IsBoilerplateLine_IsCaseInsensitive()
{
// Act & Assert
Assert.IsTrue(FfmpegOutputFilter.IsBoilerplateLine("DURATION: 00:00:01.00"));
Assert.IsTrue(FfmpegOutputFilter.IsBoilerplateLine("duration: 00:00:01.00"));
}
}
@@ -0,0 +1,70 @@
using AmiReel.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Services;
[TestClass]
public class FfmpegProgressParserTests
{
[TestMethod]
public void Parse_WithFullProgressLine_ExtractsAllFields()
{
// Arrange
const string line = "frame= 1234 fps=49.8 q=-1.0 size= 102400kB time=00:00:24.68 bitrate=33987.6kbits/s speed=0.996x";
// Act
FfmpegOutputLine result = FfmpegProgressParser.Parse(line);
// Assert
Assert.AreEqual(new TimeSpan(0, 0, 0, 24, 680), result.Time);
Assert.AreEqual(1234, result.Frame);
Assert.AreEqual(49.8, result.FramesPerSecond);
Assert.AreEqual(0.996, result.Speed);
}
[TestMethod]
public void Parse_WithNoRecognizedFields_ReturnsAllNulls()
{
// Arrange
const string line = "Metadata:";
// Act
FfmpegOutputLine result = FfmpegProgressParser.Parse(line);
// Assert
Assert.IsNull(result.Time);
Assert.IsNull(result.Frame);
Assert.IsNull(result.FramesPerSecond);
Assert.IsNull(result.Speed);
Assert.AreEqual(line, result.Line);
}
[TestMethod]
public void Parse_WithOnlyTimeField_ExtractsTimeAndLeavesFrameFieldsNull()
{
// Arrange: has a "time=" field but no matching frame=/fps=/speed=x group
const string line = "size= 1024kB time=00:01:02.03 bitrate= 100.0kbits/s";
// Act
FfmpegOutputLine result = FfmpegProgressParser.Parse(line);
// Assert
Assert.AreEqual(new TimeSpan(0, 0, 1, 2, 30), result.Time);
Assert.IsNull(result.Frame);
Assert.IsNull(result.FramesPerSecond);
Assert.IsNull(result.Speed);
}
[TestMethod]
public void Parse_PreservesOriginalLineText()
{
// Arrange
const string line = "some arbitrary ffmpeg output";
// Act
FfmpegOutputLine result = FfmpegProgressParser.Parse(line);
// Assert
Assert.AreEqual(line, result.Line);
}
}
@@ -0,0 +1,216 @@
using AmiReel.Models;
using AmiReel.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Services;
[TestClass]
public class RenderPipelineTests
{
private readonly List<string> _tempDirectories = [];
[TestCleanup]
public void Cleanup()
{
foreach (string directory in _tempDirectories)
{
try { Directory.Delete(directory, true); } catch { }
}
}
private string CreateTempDirectory()
{
string directory = Path.Combine(Path.GetTempPath(), "AmiReelTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
_tempDirectories.Add(directory);
return directory;
}
private static RenderSettings ValidSettings(IReadOnlyList<string> inputFiles, string outputDirectory, string endCardPath = "") => new()
{
InputFiles = inputFiles,
EndCardPath = endCardPath,
OutputDirectory = outputDirectory,
OutputName = "output",
FfmpegPath = "",
FfprobePath = "",
Encoder = EncoderMode.Auto,
TrimStart = 0,
FadeSeconds = 1,
EndCardHoldSeconds = 1,
ThumbnailInterval = 10,
};
[TestMethod]
public void Validate_WithNoInputFiles_ThrowsArgumentException()
{
// Arrange
RenderSettings settings = ValidSettings([], @"C:\out");
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithMissingInputFile_ThrowsFileNotFoundException()
{
// Arrange
string directory = CreateTempDirectory();
RenderSettings settings = ValidSettings([Path.Combine(directory, "missing.avi")], directory);
// Act & Assert
Assert.ThrowsExactly<FileNotFoundException>(() => RenderPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithMissingEndCard_ThrowsFileNotFoundException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.avi");
File.WriteAllText(input, "data");
RenderSettings settings = ValidSettings([input], directory, endCardPath: Path.Combine(directory, "missing.png"));
// Act & Assert
Assert.ThrowsExactly<FileNotFoundException>(() => RenderPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithBlankOutputDirectory_ThrowsArgumentException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.avi");
File.WriteAllText(input, "data");
RenderSettings settings = ValidSettings([input], "");
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithInvalidOutputName_ThrowsArgumentException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.avi");
File.WriteAllText(input, "data");
RenderSettings settings = ValidSettings([input], directory) with { OutputName = "bad" + Path.GetInvalidFileNameChars()[0] };
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithThumbnailIntervalLessThanOne_ThrowsArgumentException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.avi");
File.WriteAllText(input, "data");
RenderSettings settings = ValidSettings([input], directory) with { ThumbnailInterval = 0 };
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => RenderPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithAllRequirementsMet_DoesNotThrow()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.avi");
File.WriteAllText(input, "data");
RenderSettings settings = ValidSettings([input], directory);
// Act & Assert (no exception)
RenderPipeline.Validate(settings);
}
[TestMethod]
public void BuildProgressMessage_WithOnlyTime_ReturnsTimeRangeOnly()
{
// Act
string message = RenderPipeline.BuildProgressMessage(TimeSpan.FromSeconds(30), 60, null, null, null, 50);
// Assert
Assert.AreEqual("00:00:30 / 00:01:00", message);
}
[TestMethod]
public void BuildProgressMessage_WithAllOptionalFields_IncludesFpsSpeedAndFrame()
{
// Arrange: numbers render using the current culture (this is user-facing text),
// so build the expected fragments the same way rather than hard-coding "." / ",".
const double fps = 49.8;
const double speed = 0.99;
const int frame = 1500;
const int totalFrames = 3000; // ceil(60s * 50fps)
// Act
string message = RenderPipeline.BuildProgressMessage(TimeSpan.FromSeconds(30), 60, fps, speed, frame, 50);
// Assert
StringAssert.Contains(message, "00:00:30 / 00:01:00");
StringAssert.Contains(message, $"{fps:0.#} fps");
StringAssert.Contains(message, $"{speed:0.##}x");
StringAssert.Contains(message, $"frame {frame:N0} / {totalFrames:N0}");
}
[TestMethod]
public void MoveSourceVideos_WithNewDestination_MovesFileIntoOriginalsSubfolder()
{
// Arrange
string outputDirectory = CreateTempDirectory();
string sourceDirectory = CreateTempDirectory();
string source = Path.Combine(sourceDirectory, "clip.avi");
File.WriteAllText(source, "data");
List<string> logs = [];
// Act
IReadOnlyList<string> result = RenderPipeline.MoveSourceVideos([source], outputDirectory, logs.Add);
// Assert
string expected = Path.Combine(outputDirectory, "originals", "clip.avi");
Assert.AreEqual(expected, result.Single());
Assert.IsTrue(File.Exists(expected));
Assert.IsFalse(File.Exists(source));
}
[TestMethod]
public void MoveSourceVideos_WithDuplicateFilename_AppendsNumericSuffix()
{
// Arrange
string outputDirectory = CreateTempDirectory();
string sourceDirectory = CreateTempDirectory();
string sourceA = Path.Combine(sourceDirectory, "a", "clip.avi");
string sourceB = Path.Combine(sourceDirectory, "b", "clip.avi");
Directory.CreateDirectory(Path.GetDirectoryName(sourceA)!);
Directory.CreateDirectory(Path.GetDirectoryName(sourceB)!);
File.WriteAllText(sourceA, "data-a");
File.WriteAllText(sourceB, "data-b");
// Act
IReadOnlyList<string> result = RenderPipeline.MoveSourceVideos([sourceA, sourceB], outputDirectory, _ => { });
// Assert
Assert.AreEqual(2, result.Distinct(StringComparer.OrdinalIgnoreCase).Count());
Assert.IsTrue(result.All(File.Exists));
}
[TestMethod]
public void MoveSourceVideos_WithMissingSourceFile_LogsWarningAndKeepsOriginalPath()
{
// Arrange
string outputDirectory = CreateTempDirectory();
string missingSource = Path.Combine(CreateTempDirectory(), "gone.avi");
List<string> logs = [];
// Act
IReadOnlyList<string> result = RenderPipeline.MoveSourceVideos([missingSource], outputDirectory, logs.Add);
// Assert
Assert.AreEqual(missingSource, result.Single());
Assert.IsTrue(logs.Any(line => line.Contains("no longer exists", StringComparison.OrdinalIgnoreCase)));
}
}
@@ -0,0 +1,223 @@
using AmiReel.Models;
using AmiReel.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Services;
[TestClass]
public class ShortsPipelineTests
{
private readonly List<string> _tempDirectories = [];
[TestCleanup]
public void Cleanup()
{
foreach (string directory in _tempDirectories)
{
try { Directory.Delete(directory, true); } catch { }
}
}
private string CreateTempDirectory()
{
string directory = Path.Combine(Path.GetTempPath(), "AmiReelTests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(directory);
_tempDirectories.Add(directory);
return directory;
}
private static ShortSettings ValidSettings(string input, string font, ShortStyle style = ShortStyle.Crop, string background = "") => new()
{
InputFile = input,
OutputPath = Path.Combine(Path.GetDirectoryName(input) ?? "", "out.mp4"),
Title = "Test Short",
Hook = "THIS RAN ON AN AMIGA",
Website = "AMIGADB.NET",
Start = "00:00:00",
DurationSeconds = 30,
Style = style,
BackgroundImage = background,
FontFile = font,
FfmpegPath = "",
FfprobePath = "",
};
[TestMethod]
public void EscapeFfmpegPath_WithBackslashesColonsAndQuotes_EscapesAllThree()
{
// Act
string result = ShortsPipeline.EscapeFfmpegPath(@"C:\Fonts\font's.ttf");
// Assert
Assert.AreEqual(@"C\:\\Fonts\\font\'s.ttf", result);
}
[TestMethod]
public void EscapeFfmpegPath_EscapesBackslashesBeforeIntroducingNewOnes()
{
// Act: if colon/quote escaping ran before backslash escaping, the backslashes
// they introduce would themselves get doubled, which must not happen.
string result = ShortsPipeline.EscapeFfmpegPath("C:\\a");
// Assert
Assert.AreEqual("C\\:\\\\a", result);
}
[TestMethod]
public void BuildMeta_WithAllFieldsPopulated_JoinsWithMiddleDot()
{
// Arrange
ShortSettings settings = ValidSettings("in.mp4", "font.ttf") with { Group = "Pirates", Year = "1991", Type = "Cracktro" };
// Act
string meta = ShortsPipeline.BuildMeta(settings);
// Assert
Assert.AreEqual("Pirates · 1991 · Cracktro", meta);
}
[TestMethod]
public void BuildMeta_WithSomeFieldsBlank_SkipsBlankFields()
{
// Arrange
ShortSettings settings = ValidSettings("in.mp4", "font.ttf") with { Group = "Pirates", Year = "", Type = " " };
// Act
string meta = ShortsPipeline.BuildMeta(settings);
// Assert
Assert.AreEqual("Pirates", meta);
}
[TestMethod]
public void BuildMeta_WithAllFieldsBlank_ReturnsEmptyString()
{
// Arrange
ShortSettings settings = ValidSettings("in.mp4", "font.ttf");
// Act
string meta = ShortsPipeline.BuildMeta(settings);
// Assert
Assert.AreEqual("", meta);
}
[TestMethod]
public void Validate_WithMissingInputFile_ThrowsFileNotFoundException()
{
// Arrange
string directory = CreateTempDirectory();
ShortSettings settings = ValidSettings(Path.Combine(directory, "missing.mp4"), "font.ttf");
// Act & Assert
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithBlankTitle_ThrowsArgumentException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
ShortSettings settings = ValidSettings(input, font) with { Title = "" };
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithZeroDuration_ThrowsArgumentException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
ShortSettings settings = ValidSettings(input, font) with { DurationSeconds = 0 };
// Act & Assert
Assert.ThrowsExactly<ArgumentException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void Validate_WithMissingFontFile_ThrowsFileNotFoundException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
ShortSettings settings = ValidSettings(input, Path.Combine(directory, "missing-font.ttf"));
// Act & Assert
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void Validate_BrandStyleWithoutBackgroundImage_ThrowsFileNotFoundException()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
ShortSettings settings = ValidSettings(input, font, ShortStyle.Brand);
// Act & Assert
Assert.ThrowsExactly<FileNotFoundException>(() => ShortsPipeline.Validate(settings));
}
[TestMethod]
public void Validate_NonBrandStyleWithoutBackgroundImage_DoesNotThrow()
{
// Arrange
string directory = CreateTempDirectory();
string input = Path.Combine(directory, "clip.mp4");
File.WriteAllText(input, "data");
string font = Path.Combine(directory, "font.ttf");
File.WriteAllText(font, "data");
ShortSettings settings = ValidSettings(input, font, ShortStyle.Crop);
// Act & Assert (no exception)
ShortsPipeline.Validate(settings);
}
[TestMethod]
public void GetBaseFilter_ForEveryStyle_ReturnsAFilterEndingInBaseTag()
{
foreach (ShortStyle style in Enum.GetValues<ShortStyle>())
{
// Act
string filter = ShortsPipeline.GetBaseFilter(style);
// Assert
Assert.IsFalse(string.IsNullOrWhiteSpace(filter), $"{style} produced an empty filter.");
StringAssert.Contains(filter, "[base]", $"{style} filter does not label an output as [base].");
}
}
[TestMethod]
public void BuildFilterComplex_IncludesAllFourTextOverlaysAndTheirTimingGates()
{
// Act
string result = ShortsPipeline.BuildFilterComplex(
"[0:v]copy[base]",
fontEsc: "C\\:\\\\font.ttf",
hookEsc: "hook.txt", titleEsc: "title.txt", metaEsc: "meta.txt", websiteEsc: "website.txt",
hookEnd: 2.7, infoStart: 23, ctaStart: 26.5);
// Assert
StringAssert.Contains(result, "textfile='hook.txt'");
StringAssert.Contains(result, "textfile='title.txt'");
StringAssert.Contains(result, "textfile='meta.txt'");
StringAssert.Contains(result, "textfile='website.txt'");
StringAssert.Contains(result, "enable='between(t,0,2.7)'");
StringAssert.Contains(result, "enable='gte(t,23)'");
StringAssert.Contains(result, "enable='gte(t,26.5)'");
StringAssert.Contains(result, "[vout]");
}
}
@@ -0,0 +1,120 @@
using AmiReel.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace AmiReel.Tests.Services;
[TestClass]
public class UserFacingErrorsTests
{
[TestMethod]
public void Summarize_WithBlankMessage_ReturnsGenericMessage()
{
// Arrange
Exception exception = new(" ");
// Act
string result = UserFacingErrors.Summarize(exception);
// Assert
Assert.AreEqual("An unexpected error occurred.", result);
}
[TestMethod]
public void Summarize_WithNonProcessExitMessage_ReturnsMessageUnchanged()
{
// Arrange
Exception exception = new("The output directory could not be created.");
// Act
string result = UserFacingErrors.Summarize(exception);
// Assert
Assert.AreEqual("The output directory could not be created.", result);
}
[TestMethod]
public void Summarize_WithProcessExitAndOnlyBoilerplateLines_ReturnsFirstLineOnly()
{
// Arrange
Exception exception = new(
"Process exited with code 1.\n" +
"ffmpeg version 6.0\n" +
"built with gcc\n" +
"Input #0, avi, from 'clip.avi':\n" +
"Duration: 00:01:00.00\n");
// Act
string result = UserFacingErrors.Summarize(exception);
// Assert
Assert.AreEqual("Process exited with code 1.", result);
}
[TestMethod]
public void Summarize_WithProcessExitAndRealErrorLines_IncludesTheRealErrorLines()
{
// Arrange
Exception exception = new(
"Process exited with code 1.\n" +
"ffmpeg version 6.0\n" +
"[h264_nvenc @ 0x1] Cannot load libnvidia-encode.so.1\n" +
"Error initializing output stream 0:0 -- Error while opening encoder\n");
// Act
string result = UserFacingErrors.Summarize(exception);
// Assert
StringAssert.Contains(result, "Process exited with code 1.");
StringAssert.Contains(result, "Cannot load libnvidia-encode.so.1");
StringAssert.Contains(result, "Error while opening encoder");
}
[TestMethod]
public void Summarize_WithMoreThanThreeErrorLines_KeepsOnlyTheLastThree()
{
// Arrange
Exception exception = new(
"Process exited with code 1.\n" +
"error line 1\n" +
"error line 2\n" +
"error line 3\n" +
"error line 4\n");
// Act
string result = UserFacingErrors.Summarize(exception);
string[] lines = result.Split(Environment.NewLine);
// Assert
Assert.AreEqual(4, lines.Length);
CollectionAssert.DoesNotContain(lines, "error line 1");
CollectionAssert.Contains(lines, "error line 4");
}
[TestMethod]
[DataRow("ffmpeg version 6.0")]
[DataRow("built with gcc 12")]
[DataRow("Input #0, avi, from 'clip.avi':")]
[DataRow("Duration: 00:01:00.00, start: 0.000000")]
[DataRow("Stream #0:0: Video: mjpeg")]
public void IsNoise_WithFfmpegBoilerplate_ReturnsTrue(string line)
{
// Act
bool result = UserFacingErrors.IsNoise(line);
// Assert
Assert.IsTrue(result);
}
[TestMethod]
[DataRow("Error while opening encoder for output stream")]
[DataRow("Cannot load libnvidia-encode.so.1")]
[DataRow("No such file or directory")]
public void IsNoise_WithRealErrorText_ReturnsFalse(string line)
{
// Act
bool result = UserFacingErrors.IsNoise(line);
// Assert
Assert.IsFalse(result);
}
}
-54
View File
@@ -1,54 +0,0 @@
## .NET / Visual Studio
[Bb]in/
[Oo]bj/
[Dd]ebug/
[Rr]elease/
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
artifacts/
# Build logs
[Ll]og/
[Ll]ogs/
*.log
*.binlog
# Test results
[Tt]est[Rr]esult*/
*.trx
*.coverage
*.coveragexml
# NuGet
*.nupkg
*.snupkg
*.nuget.props
*.nuget.targets
project.lock.json
# MSIX packaging output
AppPackages/
BundleArtifacts/
*.msix
*.msixupload
*.appx
*.appxbundle
*.appxupload
# Publish output
publish/
*.pubxml
PublishScripts/
# Code analysis and tooling
_ReSharper*/
*.DotSettings.user
*.dotCover
.idea/
# Generated files
Generated\ Files/
*_wpftmp.csproj
-269
View File
@@ -1,269 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Application
x:Class="AmiReel_WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AmiReel_WinUI">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="PageBrush" Color="#0E1525"/>
<SolidColorBrush x:Key="PanelBrush" Color="#162033"/>
<SolidColorBrush x:Key="PanelAltBrush" Color="#1A2740"/>
<SolidColorBrush x:Key="FieldBrush" Color="#1D2940"/>
<SolidColorBrush x:Key="BorderBrush" Color="#2B3A58"/>
<SolidColorBrush x:Key="AccentBrush" Color="#4AB8FF"/>
<SolidColorBrush x:Key="AccentSoftBrush" Color="#13324F"/>
<SolidColorBrush x:Key="TextBrush" Color="#F5F7FB"/>
<SolidColorBrush x:Key="MutedBrush" Color="#9FB1CC"/>
<SolidColorBrush x:Key="ButtonBrush" Color="#243554"/>
<SolidColorBrush x:Key="ButtonHoverBrush" Color="#2E446C"/>
<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 x:Key="Light">
<SolidColorBrush x:Key="PageBrush" Color="#F4F7FB"/>
<SolidColorBrush x:Key="PanelBrush" Color="#FFFFFF"/>
<SolidColorBrush x:Key="PanelAltBrush" Color="#EEF4FB"/>
<SolidColorBrush x:Key="FieldBrush" Color="#F7FAFD"/>
<SolidColorBrush x:Key="BorderBrush" Color="#C7D3E3"/>
<SolidColorBrush x:Key="AccentBrush" Color="#0E9AEF"/>
<SolidColorBrush x:Key="AccentSoftBrush" Color="#D6EEFF"/>
<SolidColorBrush x:Key="TextBrush" Color="#122033"/>
<SolidColorBrush x:Key="MutedBrush" Color="#5F7390"/>
<SolidColorBrush x:Key="ButtonBrush" Color="#E5EDF7"/>
<SolidColorBrush x:Key="ButtonHoverBrush" Color="#D7E4F4"/>
<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.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">
<Setter Property="Background" Value="{ThemeResource PageBrush}" />
<Setter Property="FontFamily" Value="Bahnschrift" />
</Style>
<Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Background" Value="{ThemeResource PanelBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
</Style>
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
<Setter Property="FontSize" Value="15" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style x:Key="MutedTextStyle" TargetType="TextBlock">
<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 TargetType="Button">
<Setter Property="Padding" Value="16,9" />
<Setter Property="FontSize" Value="13" />
<Setter Property="CornerRadius" Value="10" />
</Style>
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
<Setter Property="Background" Value="{ThemeResource AccentBrush}" />
<Setter Property="Foreground" Value="{ThemeResource PrimaryTextBrush}" />
<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 TargetType="TextBox">
<Setter Property="FontSize" Value="13" />
<Setter Property="MinHeight" Value="38" />
</Style>
<Style TargetType="ComboBox">
<Setter Property="FontSize" Value="13" />
<Setter Property="MinHeight" Value="38" />
</Style>
<Style TargetType="CheckBox">
<Setter Property="FontSize" Value="13" />
</Style>
<Style TargetType="ProgressBar">
<Setter Property="MinHeight" Value="8" />
</Style>
<Style TargetType="ListView">
<Setter Property="Background" Value="{ThemeResource FieldBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Padding" Value="4" />
</Style>
<Style TargetType="ListViewItem">
<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>
</ResourceDictionary>
</Application.Resources>
</Application>
-48
View File
@@ -1,48 +0,0 @@
using Windows.ApplicationModel;
using Windows.ApplicationModel.Activation;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using Microsoft.UI.Xaml.Shapes;
using WinRT.Interop;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace AmiReel_WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{
private Window? _window;
public static IntPtr MainWindowHandle { get; private set; }
public static MainWindow? MainWindowInstance { get; set; }
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
InitializeComponent();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
_window = new MainWindow();
MainWindowHandle = WindowNative.GetWindowHandle(_window);
_window.Activate();
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

-38
View File
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<Window
x:Class="AmiReel_WinUI.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="using:AmiReel_WinUI"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="AmiReel"
mc:Ignorable="d">
<Grid Background="{ThemeResource PageBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid x:Name="AppTitleBar" Grid.Row="0" Height="44" Background="{ThemeResource PanelBrush}">
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="14,0,0,0">
<Image Source="ms-appx:///Assets/Square44x44Logo.scale-200.png"
Width="20"
Height="20"
Stretch="Uniform" />
<TextBlock Text="AmiReel"
FontFamily="Bahnschrift"
FontSize="14"
FontWeight="SemiBold"
VerticalAlignment="Center" />
</StackPanel>
</Grid>
<!--
The Frame hosts pages for your application content. Add your UI to
MainPage.xaml rather than here so you can use Page features such as
navigation events and the Loaded lifecycle.
-->
<Frame x:Name="RootFrame" Grid.Row="1" />
</Grid>
</Window>
-116
View File
@@ -1,116 +0,0 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
using System.IO;
using System.Runtime.InteropServices;
using Windows.Graphics;
using Windows.UI;
using WinRT.Interop;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace AmiReel_WinUI;
/// <summary>
/// The application window. This hosts a Frame that displays pages. Add your
/// UI and logic to MainPage.xaml / MainPage.xaml.cs instead of here so you
/// can use Page features such as navigation events and the Loaded lifecycle.
/// </summary>
public sealed partial class MainWindow : Window
{
private bool _exitConfirmed;
public MainWindow()
{
InitializeComponent();
App.MainWindowInstance = this;
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
string? executablePath = Environment.ProcessPath;
if (!string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath))
AppWindow.SetIcon(executablePath);
SetDefaultSizeAndPosition();
// Navigate the root frame to the main page on startup.
RootFrame.Navigate(typeof(MainPage));
AppWindow.Closing += AppWindow_Closing;
}
private void SetDefaultSizeAndPosition()
{
const int DefaultWidth = 1280;
const int DefaultHeight = 860;
const int MinWidth = 1100;
const int MinHeight = 780;
IntPtr hwnd = WindowNative.GetWindowHandle(this);
double scale = GetDpiForWindow(hwnd) / 96.0;
int width = (int)(DefaultWidth * scale);
int height = (int)(DefaultHeight * scale);
AppWindow.Resize(new SizeInt32(width, height));
if (AppWindow.Presenter is OverlappedPresenter presenter)
{
presenter.PreferredMinimumWidth = (int)(MinWidth * scale);
presenter.PreferredMinimumHeight = (int)(MinHeight * scale);
}
DisplayArea? displayArea = DisplayArea.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd), DisplayAreaFallback.Primary);
if (displayArea is not null)
{
int centerX = displayArea.WorkArea.X + (displayArea.WorkArea.Width - width) / 2;
int centerY = displayArea.WorkArea.Y + (displayArea.WorkArea.Height - height) / 2;
AppWindow.Move(new PointInt32(centerX, centerY));
}
}
[DllImport("user32.dll")]
private static extern int GetDpiForWindow(IntPtr hwnd);
public void ApplyTheme(bool isDark)
{
if (Content is FrameworkElement root)
root.RequestedTheme = isDark ? ElementTheme.Dark : ElementTheme.Light;
AppWindowTitleBar titleBar = AppWindow.TitleBar;
Color foreground = isDark ? Color.FromArgb(255, 0xF5, 0xF7, 0xFB) : Color.FromArgb(255, 0x12, 0x20, 0x33);
Color hoverBackground = isDark ? Color.FromArgb(255, 0x2E, 0x44, 0x6C) : Color.FromArgb(255, 0xD7, 0xE4, 0xF4);
titleBar.ButtonBackgroundColor = Colors.Transparent;
titleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
titleBar.ButtonForegroundColor = foreground;
titleBar.ButtonInactiveForegroundColor = foreground;
titleBar.ButtonHoverBackgroundColor = hoverBackground;
titleBar.ButtonHoverForegroundColor = foreground;
titleBar.ButtonPressedBackgroundColor = hoverBackground;
titleBar.ButtonPressedForegroundColor = foreground;
}
private async void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
{
if (_exitConfirmed) return;
args.Cancel = true;
FrameworkElement root = (FrameworkElement)Content;
ContentDialog dialog = DialogHelper.CreateStyled(RootFrame.XamlRoot, root.RequestedTheme);
dialog.Title = "Exit AmiReel?";
dialog.Content = "Are you sure you want to close the application?";
dialog.PrimaryButtonText = "Exit";
dialog.CloseButtonText = "Cancel";
dialog.DefaultButton = ContentDialogButton.Primary;
ContentDialogResult result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary) return;
_exitConfirmed = true;
Close();
}
}
@@ -3,9 +3,9 @@
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework> <TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion> <TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>AmiReel_WinUI</RootNamespace> <RootNamespace>AmiReel</RootNamespace>
<AssemblyName>AmiReel</AssemblyName> <AssemblyName>AmiReel</AssemblyName>
<ApplicationIcon>..\assets\AmiReel.ico</ApplicationIcon> <ApplicationIcon>branding\AmiReel.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms> <Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant())</RuntimeIdentifier> <RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant())</RuntimeIdentifier>
@@ -19,6 +19,15 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<!-- AmiReel.Tests is a subfolder of this project's directory; exclude it from the
default SDK glob so its test files (and MSTest-only usings) aren't compiled
into the app itself. -->
<Compile Remove="AmiReel.Tests\**\*.cs" />
<EmbeddedResource Remove="AmiReel.Tests\**\*" />
<None Remove="AmiReel.Tests\**\*" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" /> <Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" /> <Content Include="Assets\LockScreenLogo.scale-200.png" />
@@ -27,7 +36,6 @@
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" /> <Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\Square44x44Logo.targetsize-48_altform-lightunplated.png" /> <Content Include="Assets\Square44x44Logo.targetsize-48_altform-lightunplated.png" />
<Content Include="Assets\StoreLogo.png" /> <Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\AppIcon.ico" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" /> <Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup> </ItemGroup>
@@ -36,13 +44,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Compile Include="..\Models\*.cs" Link="Shared\Models\%(Filename)%(Extension)" /> <EmbeddedResource Include="ThirdParty\ffmpeg.exe" Condition="Exists('ThirdParty\ffmpeg.exe')" LogicalName="AmiReel.Tools.ffmpeg.exe" />
<Compile Include="..\Services\*.cs" Link="Shared\Services\%(Filename)%(Extension)" /> <EmbeddedResource Include="ThirdParty\ffprobe.exe" Condition="Exists('ThirdParty\ffprobe.exe')" LogicalName="AmiReel.Tools.ffprobe.exe" />
</ItemGroup> <!-- Loaded at runtime via GetManifestResourceStream for the title bar icon (MainWindow.xaml.cs).
The Assets\*.png Content items above go through the ms-appx:// / MRT resource pipeline,
<ItemGroup> which a single-file self-contained publish bundles into the exe in a way that ms-appx://
<EmbeddedResource Include="..\ThirdParty\ffmpeg.exe" Condition="Exists('..\ThirdParty\ffmpeg.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffmpeg.exe" /> can no longer resolve at runtime — this embedded copy sidesteps that entirely. -->
<EmbeddedResource Include="..\ThirdParty\ffprobe.exe" Condition="Exists('..\ThirdParty\ffprobe.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffprobe.exe" /> <EmbeddedResource Include="Assets\Square44x44Logo.scale-200.png" LogicalName="AmiReel.Assets.TitleBarIcon.png" />
<!-- Loaded at runtime and extracted to a real .ico file for AppWindow.SetIcon (MainWindow.xaml.cs),
which sets the taskbar/Alt+Tab window icon and requires an actual .ico file path — passing
the .exe path there does not work. Embedding it keeps this independent of publish mode too. -->
<EmbeddedResource Include="branding\AmiReel.ico" LogicalName="AmiReel.Assets.AppIcon.ico" />
</ItemGroup> </ItemGroup>
<!-- <!--
+4
View File
@@ -0,0 +1,4 @@
<Solution>
<Project Path="AmiReel.csproj" />
<Project Path="AmiReel.Tests/AmiReel.Tests.csproj" />
</Solution>
-42
View File
@@ -1,42 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0-windows</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon Condition="Exists('assets\\AmiReel.ico')">assets\AmiReel.ico</ApplicationIcon>
<AssemblyName>AmiReel</AssemblyName>
<RootNamespace>AmigaDB.VideoRenderer</RootNamespace>
<Product>AmiReel</Product>
<Title>AmiReel</Title>
<Version>0.1.0</Version>
<PublishSingleFile>true</PublishSingleFile>
<SelfContained>true</SelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
</PropertyGroup>
<ItemGroup>
<Compile Remove="AmiReel.WinUI\**\*.cs" />
<EmbeddedResource Remove="AmiReel.WinUI\**\*" />
<None Remove="AmiReel.WinUI\**\*" />
<Page Remove="AmiReel.WinUI\**\*.xaml" />
<ApplicationDefinition Remove="AmiReel.WinUI\App.xaml" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ThirdParty\ffmpeg.exe" Condition="Exists('ThirdParty\ffmpeg.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffmpeg.exe" />
<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." />
</Target>
</Project>
-48
View File
@@ -1,48 +0,0 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmigaDB.VideoRenderer", "AmigaDB.VideoRenderer.csproj", "{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmiReel.WinUI", "AmiReel.WinUI\AmiReel.WinUI.csproj", "{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Debug|x64.ActiveCfg = Debug|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Debug|x64.Build.0 = Debug|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Debug|x86.ActiveCfg = Debug|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Debug|x86.Build.0 = Debug|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Release|Any CPU.Build.0 = Release|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Release|x64.ActiveCfg = Release|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Release|x64.Build.0 = Release|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Release|x86.ActiveCfg = Release|Any CPU
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Release|x86.Build.0 = Release|Any CPU
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Debug|Any CPU.ActiveCfg = Debug|x86
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Debug|Any CPU.Build.0 = Debug|x86
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Debug|x64.ActiveCfg = Debug|x64
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Debug|x64.Build.0 = Debug|x64
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Debug|x86.ActiveCfg = Debug|x86
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Debug|x86.Build.0 = Debug|x86
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Release|Any CPU.ActiveCfg = Release|x86
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Release|Any CPU.Build.0 = Release|x86
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Release|x64.ActiveCfg = Release|x64
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Release|x64.Build.0 = Release|x64
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Release|x86.ActiveCfg = Release|x86
{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
+261 -415
View File
@@ -1,423 +1,269 @@
<Application x:Class="AmigaDB.VideoRenderer.App" <?xml version="1.0" encoding="utf-8"?>
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <Application
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="AmiReel.App"
StartupUri="MainWindow.xaml"> xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:AmiReel">
<Application.Resources> <Application.Resources>
<SolidColorBrush x:Key="PageBrush" Color="#0E1525"/> <ResourceDictionary>
<SolidColorBrush x:Key="PanelBrush" Color="#162033"/> <ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="PanelAltBrush" Color="#1A2740"/> <XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<SolidColorBrush x:Key="FieldBrush" Color="#1D2940"/> </ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="BorderBrush" Color="#2B3A58"/> <ResourceDictionary.ThemeDictionaries>
<SolidColorBrush x:Key="AccentBrush" Color="#4AB8FF"/> <ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="AccentSoftBrush" Color="#13324F"/> <SolidColorBrush x:Key="PageBrush" Color="#0E1525"/>
<SolidColorBrush x:Key="TextBrush" Color="#F5F7FB"/> <SolidColorBrush x:Key="PanelBrush" Color="#162033"/>
<SolidColorBrush x:Key="MutedBrush" Color="#9FB1CC"/> <SolidColorBrush x:Key="PanelAltBrush" Color="#1A2740"/>
<SolidColorBrush x:Key="ButtonBrush" Color="#243554"/> <SolidColorBrush x:Key="FieldBrush" Color="#1D2940"/>
<SolidColorBrush x:Key="ButtonHoverBrush" Color="#2E446C"/> <SolidColorBrush x:Key="BorderBrush" Color="#2B3A58"/>
<SolidColorBrush x:Key="ButtonDisabledBrush" Color="#2A3140"/> <SolidColorBrush x:Key="AccentBrush" Color="#4AB8FF"/>
<SolidColorBrush x:Key="ButtonDisabledTextBrush" Color="#7E8BA1"/> <SolidColorBrush x:Key="AccentSoftBrush" Color="#13324F"/>
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#07111D"/> <SolidColorBrush x:Key="TextBrush" Color="#F5F7FB"/>
<SolidColorBrush x:Key="SelectionBrush" Color="#295C87"/> <SolidColorBrush x:Key="MutedBrush" Color="#9FB1CC"/>
<SolidColorBrush x:Key="SelectionTextBrush" Color="#FFFFFF"/> <SolidColorBrush x:Key="ButtonBrush" Color="#243554"/>
<SolidColorBrush x:Key="ScrollTrackBrush" Color="#11192B"/> <SolidColorBrush x:Key="ButtonHoverBrush" Color="#2E446C"/>
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#31476D"/> <SolidColorBrush x:Key="PrimaryTextBrush" Color="#07111D"/>
<SolidColorBrush x:Key="ScrollThumbHoverBrush" Color="#42608F"/>
<SolidColorBrush x:Key="PopupBrush" Color="#10192B"/>
<SolidColorBrush x:Key="HeroBorderBrush" Color="#35507D"/>
<LinearGradientBrush x:Key="HeroBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#122441"/>
<GradientStop Offset="0.55" Color="#0F1830"/>
<GradientStop Offset="1" Color="#1A2D4D"/>
</LinearGradientBrush>
<LinearGradientBrush x:Key="PageOverlayBrush" StartPoint="0,0" EndPoint="1,1">
<GradientStop Offset="0" Color="#12000000"/>
<GradientStop Offset="1" Color="#00000000"/>
</LinearGradientBrush>
<Style TargetType="Window"> <!-- Fluent control theme-brush overrides so built-in controls match the palette -->
<Setter Property="Background" Value="{DynamicResource PageBrush}"/> <SolidColorBrush x:Key="TextControlBackground" Color="#1D2940"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> <SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#233252"/>
<Setter Property="FontFamily" Value="Bahnschrift"/> <SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#1D2940"/>
</Style> <SolidColorBrush x:Key="TextControlBorderBrush" Color="#2B3A58"/>
<Style TargetType="ScrollViewer"> <SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#4AB8FF"/>
<Setter Property="Background" Value="Transparent"/> <SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#4AB8FF"/>
<Setter Property="Template"> <SolidColorBrush x:Key="TextControlForeground" Color="#F5F7FB"/>
<Setter.Value> <SolidColorBrush x:Key="TextControlForegroundFocused" Color="#F5F7FB"/>
<ControlTemplate TargetType="ScrollViewer"> <SolidColorBrush x:Key="TextControlHeaderForeground" Color="#9FB1CC"/>
<Grid Background="{TemplateBinding Background}"> <SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#7488A6"/>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollContentPresenter Grid.Row="0" Grid.Column="0" Margin="{TemplateBinding Padding}"/> <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"/>
<ScrollBar x:Name="PART_VerticalScrollBar" <SolidColorBrush x:Key="ButtonBackground" Color="#243554"/>
Grid.Row="0" <SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="#2E446C"/>
Grid.Column="1" <SolidColorBrush x:Key="ButtonBackgroundPressed" Color="#1B2A44"/>
Orientation="Vertical" <SolidColorBrush x:Key="ButtonForeground" Color="#F5F7FB"/>
Value="{TemplateBinding VerticalOffset}" <SolidColorBrush x:Key="ButtonForegroundPointerOver" Color="#F5F7FB"/>
Maximum="{TemplateBinding ScrollableHeight}" <SolidColorBrush x:Key="ButtonBorderBrush" Color="#2B3A58"/>
ViewportSize="{TemplateBinding ViewportHeight}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
<ScrollBar x:Name="PART_HorizontalScrollBar" <SolidColorBrush x:Key="ContentDialogBackground" Color="#162033"/>
Grid.Row="1" <SolidColorBrush x:Key="ContentDialogForeground" Color="#F5F7FB"/>
Grid.Column="0" <SolidColorBrush x:Key="ContentDialogBorderBrush" Color="#35507D"/>
Orientation="Horizontal"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
<Border x:Name="CornerBox" <SolidColorBrush x:Key="ListViewItemBackgroundSelected" Color="#13324F"/>
Grid.Row="1" <SolidColorBrush x:Key="ListViewItemBackgroundPointerOver" Color="#1A2740"/>
Grid.Column="1" <SolidColorBrush x:Key="ListViewItemForeground" Color="#F5F7FB"/>
Width="14"
Height="14" <SolidColorBrush x:Key="CheckBoxCheckBackgroundFillUnchecked" Color="#1D2940"/>
Background="{DynamicResource ScrollTrackBrush}" <SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeUnchecked" Color="#2B3A58"/>
Visibility="Collapsed"/> <SolidColorBrush x:Key="CheckBoxCheckBackgroundFillChecked" Color="#4AB8FF"/>
</Grid> <SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeChecked" Color="#4AB8FF"/>
<ControlTemplate.Triggers> <SolidColorBrush x:Key="CheckBoxCheckGlyphForegroundChecked" Color="#07111D"/>
<MultiTrigger> <SolidColorBrush x:Key="CheckBoxForegroundUnchecked" Color="#F5F7FB"/>
<MultiTrigger.Conditions> <SolidColorBrush x:Key="CheckBoxForegroundChecked" Color="#F5F7FB"/>
<Condition Property="ComputedVerticalScrollBarVisibility" Value="Visible"/>
<Condition Property="ComputedHorizontalScrollBarVisibility" Value="Visible"/> <SolidColorBrush x:Key="ProgressBarForeground" Color="#4AB8FF"/>
</MultiTrigger.Conditions> <SolidColorBrush x:Key="ProgressBarBackground" Color="#1D2940"/>
<Setter TargetName="CornerBox" Property="Visibility" Value="Visible"/>
</MultiTrigger> <SolidColorBrush x:Key="HeroBorderBrush" Color="#35507D"/>
</ControlTemplate.Triggers> </ResourceDictionary>
</ControlTemplate> <ResourceDictionary x:Key="Light">
</Setter.Value> <SolidColorBrush x:Key="PageBrush" Color="#F4F7FB"/>
</Setter> <SolidColorBrush x:Key="PanelBrush" Color="#FFFFFF"/>
</Style> <SolidColorBrush x:Key="PanelAltBrush" Color="#EEF4FB"/>
<Style x:Key="ScrollBarThumbStyle" TargetType="Thumb"> <SolidColorBrush x:Key="FieldBrush" Color="#F7FAFD"/>
<Setter Property="Template"> <SolidColorBrush x:Key="BorderBrush" Color="#C7D3E3"/>
<Setter.Value> <SolidColorBrush x:Key="AccentBrush" Color="#0E9AEF"/>
<ControlTemplate TargetType="Thumb"> <SolidColorBrush x:Key="AccentSoftBrush" Color="#D6EEFF"/>
<Border Background="{DynamicResource ScrollThumbBrush}" CornerRadius="6" Margin="2"/> <SolidColorBrush x:Key="TextBrush" Color="#122033"/>
</ControlTemplate> <SolidColorBrush x:Key="MutedBrush" Color="#5F7390"/>
</Setter.Value> <SolidColorBrush x:Key="ButtonBrush" Color="#E5EDF7"/>
</Setter> <SolidColorBrush x:Key="ButtonHoverBrush" Color="#D7E4F4"/>
<Style.Triggers> <SolidColorBrush x:Key="PrimaryTextBrush" Color="#FFFFFF"/>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Opacity" Value="1"/> <SolidColorBrush x:Key="TextControlBackground" Color="#F7FAFD"/>
</Trigger> <SolidColorBrush x:Key="TextControlBackgroundPointerOver" Color="#EEF4FB"/>
</Style.Triggers> <SolidColorBrush x:Key="TextControlBackgroundFocused" Color="#FFFFFF"/>
</Style> <SolidColorBrush x:Key="TextControlBorderBrush" Color="#C7D3E3"/>
<Style TargetType="RepeatButton" x:Key="ScrollBarButtonStyle"> <SolidColorBrush x:Key="TextControlBorderBrushPointerOver" Color="#0E9AEF"/>
<Setter Property="Focusable" Value="False"/> <SolidColorBrush x:Key="TextControlBorderBrushFocused" Color="#0E9AEF"/>
<Setter Property="Background" Value="Transparent"/> <SolidColorBrush x:Key="TextControlForeground" Color="#122033"/>
<Setter Property="BorderBrush" Value="Transparent"/> <SolidColorBrush x:Key="TextControlForegroundFocused" Color="#122033"/>
<Setter Property="BorderThickness" Value="0"/> <SolidColorBrush x:Key="TextControlHeaderForeground" Color="#5F7390"/>
<Setter Property="Template"> <SolidColorBrush x:Key="TextControlPlaceholderForeground" Color="#8194AC"/>
<Setter.Value>
<ControlTemplate TargetType="RepeatButton"> <SolidColorBrush x:Key="ComboBoxBackground" Color="#F7FAFD"/>
<Border Background="{TemplateBinding Background}" CornerRadius="6"/> <SolidColorBrush x:Key="ComboBoxBackgroundPointerOver" Color="#EEF4FB"/>
</ControlTemplate> <SolidColorBrush x:Key="ComboBoxBorderBrush" Color="#C7D3E3"/>
</Setter.Value> <SolidColorBrush x:Key="ComboBoxForeground" Color="#122033"/>
</Setter> <SolidColorBrush x:Key="ComboBoxHeaderForeground" Color="#5F7390"/>
</Style> <SolidColorBrush x:Key="ComboBoxItemBackgroundSelected" Color="#D6EEFF"/>
<Style TargetType="ScrollBar"> <SolidColorBrush x:Key="ComboBoxDropDownBackground" Color="#FFFFFF"/>
<Setter Property="Width" Value="14"/>
<Setter Property="Background" Value="{DynamicResource ScrollTrackBrush}"/> <SolidColorBrush x:Key="ButtonBackground" Color="#E5EDF7"/>
<Setter Property="Template"> <SolidColorBrush x:Key="ButtonBackgroundPointerOver" Color="#D7E4F4"/>
<Setter.Value> <SolidColorBrush x:Key="ButtonBackgroundPressed" Color="#C7D9EE"/>
<ControlTemplate TargetType="ScrollBar"> <SolidColorBrush x:Key="ButtonForeground" Color="#122033"/>
<Grid Background="Transparent" Width="{TemplateBinding Width}"> <SolidColorBrush x:Key="ButtonForegroundPointerOver" Color="#122033"/>
<Border Background="{DynamicResource ScrollTrackBrush}" CornerRadius="7" Margin="2"/> <SolidColorBrush x:Key="ButtonBorderBrush" Color="#C7D3E3"/>
<Track x:Name="PART_Track" IsDirectionReversed="True" Margin="1">
<Track.DecreaseRepeatButton> <SolidColorBrush x:Key="ContentDialogBackground" Color="#FFFFFF"/>
<RepeatButton Style="{StaticResource ScrollBarButtonStyle}" Command="ScrollBar.PageUpCommand"/> <SolidColorBrush x:Key="ContentDialogForeground" Color="#122033"/>
</Track.DecreaseRepeatButton> <SolidColorBrush x:Key="ContentDialogBorderBrush" Color="#C7D3E3"/>
<Track.Thumb>
<Thumb Style="{StaticResource ScrollBarThumbStyle}"/> <SolidColorBrush x:Key="ListViewItemBackgroundSelected" Color="#D6EEFF"/>
</Track.Thumb> <SolidColorBrush x:Key="ListViewItemBackgroundPointerOver" Color="#EEF4FB"/>
<Track.IncreaseRepeatButton> <SolidColorBrush x:Key="ListViewItemForeground" Color="#122033"/>
<RepeatButton Style="{StaticResource ScrollBarButtonStyle}" Command="ScrollBar.PageDownCommand"/>
</Track.IncreaseRepeatButton> <SolidColorBrush x:Key="CheckBoxCheckBackgroundFillUnchecked" Color="#F7FAFD"/>
</Track> <SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeUnchecked" Color="#C7D3E3"/>
</Grid> <SolidColorBrush x:Key="CheckBoxCheckBackgroundFillChecked" Color="#0E9AEF"/>
<ControlTemplate.Triggers> <SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeChecked" Color="#0E9AEF"/>
<Trigger Property="Orientation" Value="Horizontal"> <SolidColorBrush x:Key="CheckBoxCheckGlyphForegroundChecked" Color="#FFFFFF"/>
<Setter Property="Height" Value="14"/> <SolidColorBrush x:Key="CheckBoxForegroundUnchecked" Color="#122033"/>
<Setter TargetName="PART_Track" Property="IsDirectionReversed" Value="False"/> <SolidColorBrush x:Key="CheckBoxForegroundChecked" Color="#122033"/>
</Trigger>
</ControlTemplate.Triggers> <SolidColorBrush x:Key="ProgressBarForeground" Color="#0E9AEF"/>
</ControlTemplate> <SolidColorBrush x:Key="ProgressBarBackground" Color="#F7FAFD"/>
</Setter.Value>
</Setter> <SolidColorBrush x:Key="HeroBorderBrush" Color="#A9C4E4"/>
</Style> </ResourceDictionary>
<Style TargetType="TextBox"> </ResourceDictionary.ThemeDictionaries>
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> <!-- Global corner rounding applied to built-in Fluent controls -->
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/> <CornerRadius x:Key="ControlCornerRadius">10</CornerRadius>
<Setter Property="CaretBrush" Value="{DynamicResource TextBrush}"/> <CornerRadius x:Key="OverlayCornerRadius">18</CornerRadius>
<Setter Property="SelectionBrush" Value="{DynamicResource SelectionBrush}"/>
<Setter Property="SelectionTextBrush" Value="{DynamicResource SelectionTextBrush}"/> <x:Double x:Key="ContentDialogMaxWidth">460</x:Double>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="9,7"/> <Style TargetType="Page">
<Setter Property="VerticalContentAlignment" Value="Center"/> <Setter Property="Background" Value="{ThemeResource PageBrush}" />
<Setter Property="SnapsToDevicePixels" Value="True"/> <Setter Property="FontFamily" Value="Bahnschrift" />
</Style> </Style>
<Style TargetType="ComboBox">
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/> <Style x:Key="CardBorderStyle" TargetType="Border">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> <Setter Property="Background" Value="{ThemeResource PanelBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/> <Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
<Setter Property="BorderThickness" Value="1"/> <Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="8,0"/> <Setter Property="CornerRadius" Value="16" />
<Setter Property="Height" Value="42"/> </Style>
<Setter Property="MinHeight" Value="42"/>
<Setter Property="Template"> <Style x:Key="SectionTitleStyle" TargetType="TextBlock">
<Setter.Value> <Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
<ControlTemplate TargetType="ComboBox"> <Setter Property="FontSize" Value="15" />
<Grid> <Setter Property="FontWeight" Value="SemiBold" />
<Border x:Name="OuterBorder" </Style>
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}" <Style x:Key="MutedTextStyle" TargetType="TextBlock">
BorderThickness="{TemplateBinding BorderThickness}" <Setter Property="Foreground" Value="{ThemeResource MutedBrush}" />
CornerRadius="12" <Setter Property="FontSize" Value="13" />
Height="{TemplateBinding Height}" </Style>
MinHeight="{TemplateBinding MinHeight}">
<Grid> <Style x:Key="FieldLabelStyle" TargetType="TextBlock">
<Grid.ColumnDefinitions> <Setter Property="Foreground" Value="{ThemeResource MutedBrush}" />
<ColumnDefinition Width="*"/> <Setter Property="FontSize" Value="12" />
<ColumnDefinition Width="42"/> <Setter Property="Margin" Value="0,0,0,4" />
</Grid.ColumnDefinitions> </Style>
<ContentPresenter Margin="12,0,8,0"
VerticalAlignment="Center" <Style TargetType="TextBlock">
HorizontalAlignment="Left" <Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
Content="{TemplateBinding SelectionBoxItem}" <Setter Property="FontSize" Value="13" />
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}" </Style>
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
<Border Grid.Column="1" Background="{DynamicResource AccentSoftBrush}" CornerRadius="0,12,12,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1,0,0,0"> <Style TargetType="Button">
<Path HorizontalAlignment="Center" <Setter Property="Padding" Value="16,9" />
VerticalAlignment="Center" <Setter Property="FontSize" Value="13" />
Fill="{DynamicResource AccentBrush}" <Setter Property="CornerRadius" Value="10" />
Data="M 0 0 L 4 4 L 8 0 Z"/> </Style>
</Border>
</Grid> <Style x:Key="PrimaryButtonStyle" TargetType="Button">
</Border> <Setter Property="Background" Value="{ThemeResource AccentBrush}" />
<Popup x:Name="PART_Popup" <Setter Property="Foreground" Value="{ThemeResource PrimaryTextBrush}" />
Placement="Bottom" <Setter Property="BorderBrush" Value="{ThemeResource AccentBrush}" />
AllowsTransparency="True" <Setter Property="FontWeight" Value="SemiBold" />
Focusable="False" <Setter Property="Padding" Value="22,10" />
IsOpen="{TemplateBinding IsDropDownOpen}" <Setter Property="CornerRadius" Value="10" />
PopupAnimation="Fade"> </Style>
<Border Margin="0,8,0,0"
Background="{DynamicResource PopupBrush}" <Style x:Key="IconButtonStyle" TargetType="Button">
BorderBrush="{DynamicResource BorderBrush}" <Setter Property="Width" Value="40" />
BorderThickness="1" <Setter Property="Height" Value="40" />
CornerRadius="14" <Setter Property="Padding" Value="0" />
MinWidth="{TemplateBinding ActualWidth}"> <Setter Property="FontSize" Value="16" />
<ScrollViewer Margin="6" SnapsToDevicePixels="True" MaxHeight="280"> <Setter Property="CornerRadius" Value="10" />
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained"/> <Setter Property="HorizontalContentAlignment" Value="Center" />
</ScrollViewer> <Setter Property="VerticalContentAlignment" Value="Center" />
</Border> </Style>
</Popup>
<ToggleButton x:Name="HitTarget" Opacity="0" Focusable="False" IsChecked="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/> <!-- Explicit styles bound to ContentDialog.PrimaryButtonStyle / CloseButtonStyle so its
</Grid> built-in buttons match the app's own Button look instead of the default Fluent
<ControlTemplate.Triggers> accent-pill / plain styles. -->
<Trigger Property="IsMouseOver" Value="True"> <Style x:Key="DialogPrimaryButtonStyle" TargetType="Button">
<Setter TargetName="OuterBorder" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/> <Setter Property="Background" Value="{ThemeResource AccentBrush}" />
</Trigger> <Setter Property="Foreground" Value="{ThemeResource PrimaryTextBrush}" />
<Trigger Property="IsKeyboardFocusWithin" Value="True"> <Setter Property="BorderBrush" Value="{ThemeResource AccentBrush}" />
<Setter TargetName="OuterBorder" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/> <Setter Property="BorderThickness" Value="1" />
</Trigger> <Setter Property="FontWeight" Value="SemiBold" />
<Trigger Property="IsEnabled" Value="False"> <Setter Property="FontSize" Value="13" />
<Setter Property="Opacity" Value="0.65"/> <Setter Property="Padding" Value="22,10" />
</Trigger> <Setter Property="CornerRadius" Value="10" />
</ControlTemplate.Triggers> <Setter Property="MinHeight" Value="40" />
</ControlTemplate> <Setter Property="MinWidth" Value="110" />
</Setter.Value> </Style>
</Setter>
</Style> <Style x:Key="DialogCloseButtonStyle" TargetType="Button">
<Style TargetType="ComboBoxItem"> <Setter Property="Background" Value="{ThemeResource ButtonBrush}" />
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/> <Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> <Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
<Setter Property="HorizontalContentAlignment" Value="Stretch"/> <Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="10,8"/> <Setter Property="FontSize" Value="13" />
<Setter Property="Template"> <Setter Property="Padding" Value="16,9" />
<Setter.Value> <Setter Property="CornerRadius" Value="10" />
<ControlTemplate TargetType="ComboBoxItem"> <Setter Property="MinHeight" Value="40" />
<Border x:Name="ItemBorder" Background="{TemplateBinding Background}" CornerRadius="10" Padding="{TemplateBinding Padding}"> <Setter Property="MinWidth" Value="110" />
<ContentPresenter/> </Style>
</Border>
<ControlTemplate.Triggers> <Style TargetType="TextBox">
<Trigger Property="IsHighlighted" Value="True"> <Setter Property="FontSize" Value="13" />
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource AccentSoftBrush}"/> <Setter Property="MinHeight" Value="38" />
</Trigger> </Style>
<Trigger Property="IsSelected" Value="True">
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource AccentBrush}"/> <Style TargetType="ComboBox">
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/> <Setter Property="FontSize" Value="13" />
</Trigger> <Setter Property="MinHeight" Value="38" />
</ControlTemplate.Triggers> </Style>
</ControlTemplate>
</Setter.Value> <Style TargetType="CheckBox">
</Setter> <Setter Property="FontSize" Value="13" />
<Style.Triggers> </Style>
<Trigger Property="IsHighlighted" Value="True">
<Setter Property="Background" Value="{DynamicResource AccentSoftBrush}"/> <Style TargetType="ProgressBar">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> <Setter Property="MinHeight" Value="8" />
</Trigger> </Style>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/> <Style TargetType="ListView">
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/> <Setter Property="Background" Value="{ThemeResource FieldBrush}" />
</Trigger> <Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
</Style.Triggers> <Setter Property="BorderThickness" Value="1" />
</Style> <Setter Property="CornerRadius" Value="10" />
<Style TargetType="ListBox"> <Setter Property="FontSize" Value="13" />
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/> <Setter Property="Padding" Value="4" />
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> </Style>
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/> <Style TargetType="ListViewItem">
</Style> <Setter Property="MinHeight" Value="32" />
<Style TargetType="ListBoxItem"> <Setter Property="Padding" Value="8,6" />
<Setter Property="Padding" Value="8,6"/> <Setter Property="CornerRadius" Value="6" />
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> </Style>
<Setter Property="Background" Value="Transparent"/>
<Style.Triggers> <Style TargetType="ContentDialog">
<Trigger Property="IsSelected" Value="True"> <Setter Property="CornerRadius" Value="18" />
<Setter Property="Background" Value="{DynamicResource AccentSoftBrush}"/> </Style>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/> </ResourceDictionary>
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="Button">
<Setter Property="Background" Value="{DynamicResource ButtonBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="Padding" Value="14,8"/>
<Setter Property="Cursor" Value="Hand"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="10">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"
Margin="{TemplateBinding Padding}"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Background" Value="{DynamicResource ButtonHoverBrush}"/>
</Trigger>
<Trigger Property="IsEnabled" Value="False">
<Setter Property="Background" Value="{DynamicResource ButtonDisabledBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource ButtonDisabledTextBrush}"/>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="22,11"/>
</Style>
<Style x:Key="IconButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Width" Value="46"/>
<Setter Property="Height" Value="46"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="FontSize" Value="20"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style x:Key="SecondaryPanel" TargetType="Border">
<Setter Property="Background" Value="{DynamicResource PanelBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="18"/>
</Style>
<Style TargetType="GroupBox">
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
<Setter Property="Background" Value="{DynamicResource PanelBrush}"/>
<Setter Property="Padding" Value="12"/>
<Setter Property="Margin" Value="0,0,0,16"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="GroupBox">
<Border Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1"
CornerRadius="18"
Padding="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<TextBlock Text="{TemplateBinding Header}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextBrush}"
Margin="0,0,0,12"/>
<ContentPresenter Grid.Row="1"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/>
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}"/>
<Setter Property="Height" Value="12"/>
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<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.Resources>
</Application> </Application>
+31 -2
View File
@@ -1,5 +1,34 @@
namespace AmigaDB.VideoRenderer; using Microsoft.UI.Xaml;
using WinRT.Interop;
public partial class App : System.Windows.Application namespace AmiReel;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : Application
{ {
private Window? _window;
public static IntPtr MainWindowHandle { get; private set; }
public static MainWindow? MainWindowInstance { get; internal set; }
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
InitializeComponent();
}
/// <summary>
/// Invoked when the application is launched.
/// </summary>
/// <param name="args">Details about the launch request and process.</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
_window = new MainWindow();
MainWindowHandle = WindowNative.GetWindowHandle(_window);
_window.Activate();
}
} }

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 109 KiB

@@ -2,7 +2,7 @@ using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media;
namespace AmiReel_WinUI; namespace AmiReel;
/// <summary> /// <summary>
/// Builds ContentDialogs styled to match the app's own palette and button shapes, /// Builds ContentDialogs styled to match the app's own palette and button shapes,
+143 -6
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8" ?> <?xml version="1.0" encoding="utf-8" ?>
<Page <Page
x:Class="AmiReel_WinUI.MainPage" x:Class="AmiReel.MainPage"
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"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -9,13 +9,19 @@
<Grid x:Name="RootGrid" Padding="20" RowSpacing="16" Background="{ThemeResource PageBrush}"> <Grid x:Name="RootGrid" Padding="20" RowSpacing="16" Background="{ThemeResource PageBrush}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Grid.Row="0" ColumnSpacing="16"> <StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8">
<Button x:Name="VideoTabButton" Content="Video" Click="VideoTab_Click" Style="{StaticResource PrimaryButtonStyle}" MinWidth="120" />
<Button x:Name="ShortsTabButton" Content="Shorts" Click="ShortsTab_Click" MinWidth="120" />
</StackPanel>
<Grid Grid.Row="1" ColumnSpacing="16" x:Name="VideoContentPanel">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
@@ -72,7 +78,137 @@
</Border> </Border>
</Grid> </Grid>
<Border Grid.Row="1" Padding="18" Style="{StaticResource CardBorderStyle}"> <Grid Grid.Row="1" RowSpacing="16" x:Name="ShortsContentPanel" Visibility="Collapsed">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Padding="18" Style="{StaticResource CardBorderStyle}">
<StackPanel Spacing="12">
<TextBlock Text="Short source and clip" Style="{StaticResource SectionTitleStyle}" />
<Grid ColumnSpacing="10" RowSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBox x:Name="ShortInputBox" Grid.Row="0" MinHeight="40" ToolTipService.ToolTip="Source video for this Short" />
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseShortInput_Click" VerticalAlignment="Stretch" MinWidth="120" />
<TextBox x:Name="ShortOutputBox" Grid.Row="1" MinHeight="40" ToolTipService.ToolTip="Output .mp4 path" />
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseShortOutput_Click" VerticalAlignment="Stretch" MinWidth="120" />
</Grid>
<Grid ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<TextBlock Text="Start (HH:MM:SS)" Style="{StaticResource FieldLabelStyle}" />
<TextBox x:Name="ShortStartBox" Text="00:00:00" />
</StackPanel>
<StackPanel Grid.Column="1">
<TextBlock Text="Duration (seconds)" Style="{StaticResource FieldLabelStyle}" />
<TextBox x:Name="ShortDurationBox" Text="30" />
</StackPanel>
</Grid>
<Button x:Name="PreviewShortSourceButton" Content="Preview source" Click="PreviewShortSource_Click" HorizontalAlignment="Left" />
</StackPanel>
</Border>
<Border Grid.Column="1" Padding="18" Style="{StaticResource CardBorderStyle}">
<StackPanel Spacing="12">
<TextBlock Text="Title card" Style="{StaticResource SectionTitleStyle}" />
<TextBox x:Name="ShortTitleBox" Header="Title" ToolTipService.ToolTip="Production title, e.g. Ami-Back V1.04A" />
<Grid ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBox x:Name="ShortGroupBox" Grid.Column="0" Header="Group" />
<TextBox x:Name="ShortYearBox" Grid.Column="1" Header="Year" />
<TextBox x:Name="ShortTypeBox" Grid.Column="2" Header="Type" />
</Grid>
<TextBox x:Name="ShortHookBox" Header="Opening hook" />
<TextBox x:Name="ShortWebsiteBox" Header="Closing website text" />
</StackPanel>
</Border>
</Grid>
<Border Grid.Row="1" Padding="18" Style="{StaticResource CardBorderStyle}">
<StackPanel Spacing="12">
<TextBlock Text="Style and encoding" Style="{StaticResource SectionTitleStyle}" />
<Grid ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="ShortStyleBox" Grid.Column="0" Header="Style" SelectionChanged="ShortStyleBox_SelectionChanged" HorizontalAlignment="Stretch">
<ComboBoxItem Content="Brand (title card image)" Tag="Brand" />
<ComboBoxItem Content="Pixel" Tag="Pixel" />
<ComboBoxItem Content="Mirror" Tag="Mirror" />
<ComboBoxItem Content="Crop" Tag="Crop" />
<ComboBoxItem Content="Workbench" Tag="Workbench" />
<ComboBoxItem Content="Blur" Tag="Blur" />
<ComboBoxItem Content="CRT" Tag="Crt" />
<ComboBoxItem Content="Copper bars" Tag="Copper" />
<ComboBoxItem Content="Stars" Tag="Stars" />
<ComboBoxItem Content="Grid" Tag="Grid" />
<ComboBoxItem Content="Tiles" Tag="Tiles" />
<ComboBoxItem Content="Scanlines" Tag="Scan" />
<ComboBoxItem Content="Starfield" Tag="Starfield" />
<ComboBoxItem Content="Plasma" Tag="Plasma" />
<ComboBoxItem Content="Rasterbars" Tag="Rasterbars" />
<ComboBoxItem Content="VHS" Tag="Vhs" />
<ComboBoxItem Content="Monitor" Tag="Monitor" />
<ComboBoxItem Content="Split" Tag="Split" />
<ComboBoxItem Content="Spectrum (needs audio)" Tag="Spectrum" />
</ComboBox>
<ComboBox x:Name="ShortPresetBox" Grid.Column="1" Header="x264 preset" HorizontalAlignment="Stretch">
<ComboBoxItem Content="ultrafast" Tag="ultrafast" />
<ComboBoxItem Content="superfast" Tag="superfast" />
<ComboBoxItem Content="veryfast" Tag="veryfast" />
<ComboBoxItem Content="faster" Tag="faster" />
<ComboBoxItem Content="fast" Tag="fast" />
<ComboBoxItem Content="medium" Tag="medium" />
<ComboBoxItem Content="slow" Tag="slow" />
<ComboBoxItem Content="slower" Tag="slower" />
<ComboBoxItem Content="veryslow" Tag="veryslow" />
</ComboBox>
</Grid>
<Grid x:Name="ShortBackgroundRow" ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox x:Name="ShortBackgroundBox" Grid.Column="0" Header="Background image (brand style)" />
<Button Grid.Column="1" Content="Browse..." Click="BrowseShortBackground_Click" VerticalAlignment="Bottom" MinWidth="120" />
</Grid>
<Grid ColumnSpacing="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="120" />
</Grid.ColumnDefinitions>
<TextBox x:Name="ShortFontBox" Grid.Column="0" Header="Bold font file (.ttf/.otf)" />
<Button Grid.Column="1" Content="Browse..." Click="BrowseShortFont_Click" VerticalAlignment="Bottom" MinWidth="120" />
<TextBox x:Name="ShortCrfBox" Grid.Column="2" Header="CRF" />
</Grid>
</StackPanel>
</Border>
</Grid>
<Border Grid.Row="2" Padding="18" Style="{StaticResource CardBorderStyle}">
<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">
@@ -84,7 +220,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border Grid.Row="2" Padding="18" Style="{StaticResource CardBorderStyle}"> <Border Grid.Row="3" Padding="18" Style="{StaticResource CardBorderStyle}">
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<TextBlock Text="FFmpeg log" Style="{StaticResource SectionTitleStyle}" /> <TextBlock Text="FFmpeg log" Style="{StaticResource SectionTitleStyle}" />
<TextBox x:Name="LogBox" <TextBox x:Name="LogBox"
@@ -100,7 +236,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Grid Grid.Row="3"> <Grid Grid.Row="4">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
@@ -113,7 +249,8 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10"> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
<Button x:Name="OpenSettingsButton" Content="&#xE713;" FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets" <Button x:Name="OpenSettingsButton" Content="&#xE713;" FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
Click="OpenSettings_Click" Style="{StaticResource IconButtonStyle}" ToolTipService.ToolTip="Settings" /> Click="OpenSettings_Click" Style="{StaticResource IconButtonStyle}"
ToolTipService.ToolTip="Settings" AutomationProperties.Name="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>
@@ -2,17 +2,16 @@ using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.Text; using System.Text;
using AmigaDB.VideoRenderer.Models; using AmiReel.Models;
using AmigaDB.VideoRenderer.Services; using AmiReel.Services;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Controls;
using Microsoft.Win32;
using Windows.ApplicationModel.DataTransfer; using Windows.ApplicationModel.DataTransfer;
using Windows.Storage; using Windows.Storage;
using Windows.Storage.Pickers; using Windows.Storage.Pickers;
using WinRT.Interop; using WinRT.Interop;
namespace AmiReel_WinUI; namespace AmiReel;
public sealed partial class MainPage : Page public sealed partial class MainPage : Page
{ {
@@ -23,6 +22,7 @@ public sealed partial class MainPage : Page
private CancellationTokenSource? _renderCancellation; private CancellationTokenSource? _renderCancellation;
private string? _lastRenderedFile; private string? _lastRenderedFile;
private bool _logFlushScheduled; private bool _logFlushScheduled;
private bool _isShortsTab;
public MainPage() public MainPage()
{ {
@@ -43,6 +43,40 @@ public sealed partial class MainPage : Page
SelectTheme(_loadedSettings.Theme); SelectTheme(_loadedSettings.Theme);
ApplyTheme(_loadedSettings.Theme); ApplyTheme(_loadedSettings.Theme);
MoveSourcesBox.IsChecked = _loadedSettings.ShouldMoveSourcesToOriginals; MoveSourcesBox.IsChecked = _loadedSettings.ShouldMoveSourcesToOriginals;
ShortHookBox.Text = _loadedSettings.ShortsHook;
ShortWebsiteBox.Text = _loadedSettings.ShortsWebsite;
ShortCrfBox.Text = _loadedSettings.ShortsCrf;
ShortBackgroundBox.Text = _loadedSettings.ShortsBackgroundImage;
ShortFontBox.Text = string.IsNullOrWhiteSpace(_loadedSettings.ShortsFontFile)
? ShortsPipeline.FindDefaultFont() ?? ""
: _loadedSettings.ShortsFontFile;
SelectComboTag(ShortStyleBox, _loadedSettings.ShortsStyle);
SelectComboTag(ShortPresetBox, _loadedSettings.ShortsPreset);
UpdateShortBackgroundVisibility();
}
private void VideoTab_Click(object sender, RoutedEventArgs e) => SetActiveTab(shorts: false);
private void ShortsTab_Click(object sender, RoutedEventArgs e) => SetActiveTab(shorts: true);
private void SetActiveTab(bool shorts)
{
_isShortsTab = shorts;
VideoContentPanel.Visibility = shorts ? Visibility.Collapsed : Visibility.Visible;
ShortsContentPanel.Visibility = shorts ? Visibility.Visible : Visibility.Collapsed;
Style primaryButtonStyle = (Style)Application.Current.Resources["PrimaryButtonStyle"];
VideoTabButton.Style = shorts ? null : primaryButtonStyle;
ShortsTabButton.Style = shorts ? primaryButtonStyle : null;
RenderButton.Content = shorts ? "Render short" : "Start render";
}
private void ShortStyleBox_SelectionChanged(object sender, SelectionChangedEventArgs e) => UpdateShortBackgroundVisibility();
private void UpdateShortBackgroundVisibility()
{
bool isBrand = string.Equals(SelectedShortStyle(), nameof(ShortStyle.Brand), StringComparison.OrdinalIgnoreCase);
ShortBackgroundRow.Visibility = isBrand ? Visibility.Visible : Visibility.Collapsed;
} }
private async void AddInputs_Click(object sender, RoutedEventArgs e) private async void AddInputs_Click(object sender, RoutedEventArgs e)
@@ -146,18 +180,14 @@ public sealed partial class MainPage : Page
{ {
try try
{ {
RenderSettings settings = ReadSettings();
SaveSettings();
SetRendering(true); SetRendering(true);
LogBox.Text = string.Empty; LogBox.Text = string.Empty;
_renderCancellation = new CancellationTokenSource(); _renderCancellation = new CancellationTokenSource();
Progress<RenderProgress> progress = new(UpdateProgress);
IReadOnlyList<string> resolvedInputs = await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token); if (_isShortsTab)
ReplaceInputs(resolvedInputs); await RenderShortAsync(_renderCancellation.Token);
_lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4"); else
OpenOutputButton.IsEnabled = true; await RenderVideoAsync(_renderCancellation.Token);
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
await ShowMessageAsync("Render complete", "The AmiReel render completed successfully.");
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -177,6 +207,32 @@ public sealed partial class MainPage : Page
} }
} }
private async Task RenderVideoAsync(CancellationToken token)
{
RenderSettings settings = ReadSettings();
SaveSettings();
Progress<RenderProgress> progress = new(UpdateProgress);
IReadOnlyList<string> resolvedInputs = await new RenderPipeline().RenderAsync(settings, progress, AppendLog, token);
ReplaceInputs(resolvedInputs);
_lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4");
OpenOutputButton.IsEnabled = true;
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
await ShowMessageAsync("Render complete", "The AmiReel render completed successfully.");
}
private async Task RenderShortAsync(CancellationToken token)
{
ShortSettings settings = ReadShortSettings();
SaveSettings();
Progress<RenderProgress> progress = new(UpdateProgress);
await new ShortsPipeline().RenderAsync(settings, progress, AppendLog, token);
_lastRenderedFile = settings.OutputPath;
ShortOutputBox.Text = settings.OutputPath;
OpenOutputButton.IsEnabled = true;
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
await ShowMessageAsync("Short complete", "The YouTube Short was rendered successfully.");
}
private void Cancel_Click(object sender, RoutedEventArgs e) private void Cancel_Click(object sender, RoutedEventArgs e)
{ {
CancelButton.IsEnabled = false; CancelButton.IsEnabled = false;
@@ -186,8 +242,11 @@ public sealed partial class MainPage : Page
private void OpenOutput_Click(object sender, RoutedEventArgs e) private void OpenOutput_Click(object sender, RoutedEventArgs e)
{ {
if (Directory.Exists(OutputFolderBox.Text)) string? directory = !string.IsNullOrWhiteSpace(_lastRenderedFile)
Process.Start(new ProcessStartInfo("explorer.exe", OutputFolderBox.Text) { UseShellExecute = true }); ? Path.GetDirectoryName(_lastRenderedFile)
: OutputFolderBox.Text;
if (!string.IsNullOrWhiteSpace(directory) && Directory.Exists(directory))
Process.Start(new ProcessStartInfo("explorer.exe", directory) { UseShellExecute = true });
} }
private async void PreviewSource_Click(object sender, RoutedEventArgs e) private async void PreviewSource_Click(object sender, RoutedEventArgs e)
@@ -223,19 +282,21 @@ public sealed partial class MainPage : Page
throw new ArgumentException("Thumbnail interval must be at least one second."); throw new ArgumentException("Thumbnail interval must be at least one second.");
EncoderMode encoder = Enum.Parse<EncoderMode>(SelectedEncoder()); EncoderMode encoder = Enum.Parse<EncoderMode>(SelectedEncoder());
return new( return new RenderSettings
_inputs.ToList(), {
EndCardBox.Text.Trim(), InputFiles = _inputs.ToList(),
OutputFolderBox.Text.Trim(), EndCardPath = EndCardBox.Text.Trim(),
OutputNameBox.Text.Trim(), OutputDirectory = OutputFolderBox.Text.Trim(),
FfmpegPathBox.Text.Trim(), OutputName = OutputNameBox.Text.Trim(),
FfprobePathBox.Text.Trim(), FfmpegPath = FfmpegPathBox.Text.Trim(),
encoder, FfprobePath = FfprobePathBox.Text.Trim(),
Number(TrimBox.Text, "trim start"), Encoder = encoder,
Number(FadeBox.Text, "fade"), TrimStart = Number(TrimBox.Text, "trim start"),
Number(HoldBox.Text, "end-card hold"), FadeSeconds = Number(FadeBox.Text, "fade"),
interval, EndCardHoldSeconds = Number(HoldBox.Text, "end-card hold"),
MoveSourcesBox.IsChecked == true); ThumbnailInterval = interval,
MoveSourcesToOriginals = MoveSourcesBox.IsChecked == true,
};
} }
private void ReplaceInputs(IReadOnlyList<string> paths) private void ReplaceInputs(IReadOnlyList<string> paths)
@@ -245,11 +306,101 @@ public sealed partial class MainPage : Page
_inputs.Add(path); _inputs.Add(path);
} }
private ShortSettings ReadShortSettings()
{
if (!double.TryParse(ShortDurationBox.Text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out double duration) || duration <= 0)
throw new ArgumentException("Enter a valid duration in seconds.");
if (!int.TryParse(ShortCrfBox.Text, out int crf))
throw new ArgumentException("CRF must be an integer.");
string input = ShortInputBox.Text.Trim();
string output = ShortOutputBox.Text.Trim();
ShortStyle style = Enum.Parse<ShortStyle>(SelectedShortStyle());
if (string.IsNullOrWhiteSpace(output) && !string.IsNullOrWhiteSpace(input))
{
string baseName = Path.GetFileNameWithoutExtension(input);
string? directory = Path.GetDirectoryName(input);
output = Path.Combine(directory ?? "", $"{baseName}-{style.ToString().ToLowerInvariant()}-short.mp4");
}
return new ShortSettings
{
InputFile = input,
OutputPath = output,
Title = ShortTitleBox.Text.Trim(),
Group = ShortGroupBox.Text.Trim(),
Year = ShortYearBox.Text.Trim(),
Type = ShortTypeBox.Text.Trim(),
Hook = ShortHookBox.Text.Trim(),
Website = ShortWebsiteBox.Text.Trim(),
Start = string.IsNullOrWhiteSpace(ShortStartBox.Text) ? "00:00:00" : ShortStartBox.Text.Trim(),
DurationSeconds = duration,
Style = style,
BackgroundImage = ShortBackgroundBox.Text.Trim(),
FontFile = ShortFontBox.Text.Trim(),
FfmpegPath = FfmpegPathBox.Text.Trim(),
FfprobePath = FfprobePathBox.Text.Trim(),
Crf = crf,
Preset = SelectedShortPreset(),
};
}
private async void BrowseShortInput_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
foreach (string extension in SupportedVideoFormats.Extensions)
picker.FileTypeFilter.Add(extension);
picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
var file = await picker.PickSingleFileAsync();
if (file is not null)
ShortInputBox.Text = file.Path;
}
private async void BrowseShortOutput_Click(object sender, RoutedEventArgs e)
{
FileSavePicker picker = new();
picker.FileTypeChoices.Add("MP4 video", [".mp4"]);
picker.SuggestedFileName = "short.mp4";
InitializeWithWindow.Initialize(picker, App.MainWindowHandle);
var file = await picker.PickSaveFileAsync();
if (file is not null)
ShortOutputBox.Text = file.Path;
}
private async void BrowseShortBackground_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
picker.FileTypeFilter.Add(".png");
picker.FileTypeFilter.Add(".jpg");
picker.FileTypeFilter.Add(".jpeg");
var file = await picker.PickSingleFileAsync();
if (file is not null)
ShortBackgroundBox.Text = file.Path;
}
private async void BrowseShortFont_Click(object sender, RoutedEventArgs e)
{
FileOpenPicker picker = CreateFileOpenPicker();
picker.FileTypeFilter.Add(".ttf");
picker.FileTypeFilter.Add(".otf");
var file = await picker.PickSingleFileAsync();
if (file is not null)
ShortFontBox.Text = file.Path;
}
private async void PreviewShortSource_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(ShortInputBox.Text))
await OpenPreviewAsync(ShortInputBox.Text.Trim());
}
private void SetRendering(bool rendering) private void SetRendering(bool rendering)
{ {
RenderButton.IsEnabled = !rendering; RenderButton.IsEnabled = !rendering;
CancelButton.IsEnabled = rendering; CancelButton.IsEnabled = rendering;
OpenSettingsButton.IsEnabled = !rendering; OpenSettingsButton.IsEnabled = !rendering;
VideoTabButton.IsEnabled = !rendering;
ShortsTabButton.IsEnabled = !rendering;
} }
private void UpdateProgress(RenderProgress value) private void UpdateProgress(RenderProgress value)
@@ -326,51 +477,54 @@ public sealed partial class MainPage : Page
private void SaveSettings() private void SaveSettings()
{ {
AppSettingsStore.Save(new AppSettings( AppSettingsStore.Save(new AppSettings
OutputFolderBox.Text.Trim(), {
EndCardBox.Text.Trim(), OutputFolder = OutputFolderBox.Text.Trim(),
FfmpegPathBox.Text.Trim(), EndCardPath = EndCardBox.Text.Trim(),
FfprobePathBox.Text.Trim(), FfmpegPath = FfmpegPathBox.Text.Trim(),
PreviewPlayerPathBox.Text.Trim(), FfprobePath = FfprobePathBox.Text.Trim(),
SelectedTheme(), PreviewPlayerPath = PreviewPlayerPathBox.Text.Trim(),
SelectedEncoder(), Theme = SelectedTheme(),
TrimBox.Text.Trim(), Encoder = SelectedEncoder(),
FadeBox.Text.Trim(), TrimStart = TrimBox.Text.Trim(),
HoldBox.Text.Trim(), FadeSeconds = FadeBox.Text.Trim(),
IntervalBox.Text.Trim(), EndCardHoldSeconds = HoldBox.Text.Trim(),
MoveSourcesBox.IsChecked == true)); ThumbnailInterval = IntervalBox.Text.Trim(),
MoveSourcesToOriginals = MoveSourcesBox.IsChecked == true,
ShortsStyle = SelectedShortStyle(),
ShortsHook = ShortHookBox.Text.Trim(),
ShortsWebsite = ShortWebsiteBox.Text.Trim(),
ShortsFontFile = ShortFontBox.Text.Trim(),
ShortsBackgroundImage = ShortBackgroundBox.Text.Trim(),
ShortsCrf = ShortCrfBox.Text.Trim(),
ShortsPreset = SelectedShortPreset(),
});
} }
private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark"; private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark";
private string SelectedEncoder() => (EncoderBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Auto"; private string SelectedEncoder() => (EncoderBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Auto";
private void SelectTheme(string theme) private string SelectedShortStyle() => (ShortStyleBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Brand";
private string SelectedShortPreset() => (ShortPresetBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "slow";
private void SelectTheme(string theme) => SelectComboTag(ThemeBox, theme);
private void SelectEncoder(string encoder) => SelectComboTag(EncoderBox, encoder);
private static void SelectComboTag(ComboBox comboBox, string tag)
{ {
foreach (ComboBoxItem item in ThemeBox.Items) foreach (ComboBoxItem item in comboBox.Items.Cast<ComboBoxItem>())
{ {
if (string.Equals(item.Tag?.ToString(), theme, StringComparison.OrdinalIgnoreCase)) if (string.Equals(item.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase))
{ {
ThemeBox.SelectedItem = item; comboBox.SelectedItem = item;
return; return;
} }
} }
ThemeBox.SelectedIndex = 0; comboBox.SelectedIndex = 0;
}
private void SelectEncoder(string encoder)
{
foreach (ComboBoxItem item in EncoderBox.Items)
{
if (string.Equals(item.Tag?.ToString(), encoder, StringComparison.OrdinalIgnoreCase))
{
EncoderBox.SelectedItem = item;
return;
}
}
EncoderBox.SelectedIndex = 0;
} }
private void InputList_SelectionChanged(object sender, SelectionChangedEventArgs e) private void InputList_SelectionChanged(object sender, SelectionChangedEventArgs e)
+31 -272
View File
@@ -1,279 +1,38 @@
<Window x:Class="AmigaDB.VideoRenderer.MainWindow" <?xml version="1.0" encoding="utf-8" ?>
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <Window
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="AmiReel.MainWindow"
Title="AmiReel" Height="860" Width="1280" MinHeight="780" MinWidth="1100" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
WindowStartupLocation="CenterScreen" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
WindowStyle="None" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
ResizeMode="CanResize" xmlns:local="using:AmiReel"
Icon="assets/AmiReel.ico"> xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
<WindowChrome.WindowChrome> Title="AmiReel"
<WindowChrome CaptionHeight="40" mc:Ignorable="d">
ResizeBorderThickness="6" <Grid Background="{ThemeResource PageBrush}">
GlassFrameThickness="0"
UseAeroCaptionButtons="False"/>
</WindowChrome.WindowChrome>
<Grid Background="{DynamicResource PageBrush}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto" />
<RowDefinition Height="*"/> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Grid.Row="0" Background="{DynamicResource PanelBrush}" Height="40"> <Grid x:Name="AppTitleBar" Grid.Row="0" Height="44" Background="{ThemeResource PanelBrush}">
<Grid.ColumnDefinitions> <StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="14,0,0,0">
<ColumnDefinition Width="Auto"/> <Image x:Name="TitleBarIcon"
<ColumnDefinition Width="*"/> Width="20"
<ColumnDefinition Width="Auto"/> Height="20"
<ColumnDefinition Width="Auto"/> Stretch="Uniform" />
<ColumnDefinition Width="Auto"/> <TextBlock Text="AmiReel"
</Grid.ColumnDefinitions> FontFamily="Bahnschrift"
FontSize="14"
<Image Source="assets/AmiReel.ico" Width="18" Height="18" Margin="14,0,8,0" VerticalAlignment="Center" FontWeight="SemiBold"
WindowChrome.IsHitTestVisibleInChrome="False"/> VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Text="AmiReel" VerticalAlignment="Center" FontWeight="SemiBold" </StackPanel>
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 Grid.Row="1"> <!--
<Grid> The Frame hosts pages for your application content. Add your UI to
<Grid.Background> MainPage.xaml rather than here so you can use Page features such as
<DrawingBrush Stretch="None" Viewport="0,0,36,36" ViewportUnits="Absolute" Opacity="0.08"> navigation events and the Loaded lifecycle.
<DrawingBrush.Drawing> -->
<GeometryDrawing Brush="{DynamicResource PageOverlayBrush}"> <Frame x:Name="RootFrame" Grid.Row="1" />
<GeometryDrawing.Geometry>
<GeometryGroup>
<EllipseGeometry Center="10,10" RadiusX="1.2" RadiusY="1.2"/>
<EllipseGeometry Center="28,18" RadiusX="1.2" RadiusY="1.2"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Grid.Background>
</Grid>
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<GroupBox Header="Source recordings">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="98"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<Button Content="Add video 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"
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"
HorizontalAlignment="Left" Margin="0,10,0,0" IsEnabled="False"/>
</Grid>
</GroupBox>
<GroupBox Grid.Column="2" Header="End card and output">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="55"/><RowDefinition Height="55"/><RowDefinition Height="55"/></Grid.RowDefinitions>
<TextBlock Text="Leave blank to use a built-in black end card." Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,6"/>
<TextBox x:Name="EndCardBox" Grid.Row="1" Margin="0,0,8,8" ToolTip="Optional end-card image"/>
<Button Grid.Row="1" Grid.Column="1" Content="Browse…" Click="BrowseEndCard_Click" Margin="0,0,0,8"/>
<TextBox x:Name="OutputFolderBox" Grid.Row="2" Margin="0,0,8,8" ToolTip="Output folder"/>
<Button Grid.Row="2" Grid.Column="1" Content="Browse…" Click="BrowseOutput_Click" Margin="0,0,0,8"/>
<TextBox x:Name="OutputNameBox" Grid.Row="3" Grid.ColumnSpan="2" Text="amigadb_intro" ToolTip="Output filename prefix"/>
</Grid>
</GroupBox>
</Grid>
<GroupBox Grid.Row="1" Header="Render progress" Margin="0,14,0,0">
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<TextBlock x:Name="StageText" Text="Ready" FontWeight="SemiBold"/>
<TextBlock x:Name="PercentText" Text="0%" Foreground="{DynamicResource AccentBrush}" Margin="6,0,0,0"/>
</StackPanel>
<ProgressBar x:Name="RenderProgress" Minimum="0" Maximum="100" Foreground="{DynamicResource AccentBrush}"/>
<TextBlock x:Name="StatusText" Text="Select the recordings and start the render. The end card is optional."
Foreground="{DynamicResource MutedBrush}" TextWrapping="Wrap" Margin="0,8,0,0"/>
</StackPanel>
</GroupBox>
<GroupBox Grid.Row="2" Header="FFmpeg log" Margin="0,14,0,0">
<TextBox x:Name="LogBox" IsReadOnly="True" AcceptsReturn="True"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
FontFamily="Consolas" FontSize="11" TextWrapping="NoWrap"/>
</GroupBox>
<DockPanel Grid.Row="3" Margin="0,12,0,0" LastChildFill="False">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<Button x:Name="OpenOutputButton" Content="Open output folder" Click="OpenOutput_Click" IsEnabled="False" Margin="0,0,10,0"/>
<Button x:Name="PreviewRenderedButton" Content="Preview render" Click="PreviewRendered_Click" IsEnabled="False"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
<Button x:Name="OpenSettingsButton" Content="⚙" Click="OpenSettings_Click" Style="{StaticResource IconButton}" Margin="0,0,10,0" ToolTip="Settings"/>
<Button x:Name="CancelButton" Content="Cancel" Click="Cancel_Click" IsEnabled="False" Margin="0,0,10,0"/>
<Button x:Name="RenderButton" Content="Start render" Style="{StaticResource PrimaryButton}" Click="Render_Click"/>
</StackPanel>
</DockPanel>
<Border x:Name="SettingsOverlay" Grid.RowSpan="4" Background="#88060B14" Visibility="Collapsed">
<Grid Margin="40">
<Border Width="760"
MaxHeight="720"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource PanelBrush}"
BorderBrush="{DynamicResource HeroBorderBrush}"
BorderThickness="1.5"
CornerRadius="24"
Padding="20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Settings" FontSize="24" FontWeight="SemiBold" Margin="0,0,0,14"/>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<GroupBox Header="Appearance">
<StackPanel>
<TextBlock Text="Theme" Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,4"/>
<ComboBox x:Name="ThemeBox" SelectionChanged="ThemeBox_SelectionChanged">
<ComboBoxItem Content="Dark" Tag="Dark"/>
<ComboBoxItem Content="Light" Tag="Light"/>
</ComboBox>
</StackPanel>
</GroupBox>
<GroupBox Header="Timing and screenshots">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition/><ColumnDefinition/></Grid.ColumnDefinitions>
<Grid.RowDefinitions><RowDefinition/><RowDefinition/></Grid.RowDefinitions>
<StackPanel Margin="0,0,8,10">
<TextBlock Text="Trim start (seconds)" Foreground="{DynamicResource MutedBrush}"/>
<TextBox x:Name="TrimBox"/>
</StackPanel>
<StackPanel Grid.Column="1" Margin="8,0,0,10">
<TextBlock Text="Fade (seconds)" Foreground="{DynamicResource MutedBrush}"/>
<TextBox x:Name="FadeBox"/>
</StackPanel>
<StackPanel Grid.Row="1" Margin="0,0,8,0">
<TextBlock Text="End-card hold (seconds)" Foreground="{DynamicResource MutedBrush}"/>
<TextBox x:Name="HoldBox"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" Margin="8,0,0,0">
<TextBlock Text="Thumbnail interval (seconds)" Foreground="{DynamicResource MutedBrush}"/>
<TextBox x:Name="IntervalBox"/>
</StackPanel>
</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>
<Grid.RowDefinitions><RowDefinition/><RowDefinition/></Grid.RowDefinitions>
<StackPanel Margin="0,0,8,8">
<TextBlock Text="Encoder" Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,4"/>
<ComboBox x:Name="EncoderBox" SelectedIndex="0">
<ComboBoxItem Content="Auto (NVENC → CPU fallback)" Tag="Auto"/>
<ComboBoxItem Content="NVIDIA NVENC" Tag="NvidiaNvenc"/>
<ComboBoxItem Content="CPU libx264" Tag="CpuX264"/>
</ComboBox>
</StackPanel>
<StackPanel Grid.Column="1" Margin="8,0,0,8">
<TextBlock Text="Output" Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,4"/>
<TextBox Text="3840 × 2160 · 50 FPS" IsReadOnly="True"/>
</StackPanel>
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" Foreground="{DynamicResource MutedBrush}"
Text="NVENC: preset P6 / CQ 16 · CPU: slow / CRF 13"/>
</Grid>
</GroupBox>
<GroupBox Header="FFmpeg tools">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition/><RowDefinition/><RowDefinition/></Grid.RowDefinitions>
<TextBlock Text="Optional override. Leave blank to auto-detect FFmpeg and FFprobe from PATH, then use the embedded fallback."
Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,6"/>
<TextBox x:Name="FfmpegPathBox" Grid.Row="1" Margin="0,0,8,8" ToolTip="Path to ffmpeg.exe"/>
<Button Grid.Row="1" Grid.Column="1" Content="Browse…" Click="BrowseFfmpeg_Click" Margin="0,0,0,8"/>
<TextBox x:Name="FfprobePathBox" Grid.Row="2" Margin="0,0,8,8" ToolTip="Path to ffprobe.exe"/>
<Button Grid.Row="2" Grid.Column="1" Content="Browse…" Click="BrowseFfprobe_Click" Margin="0,0,0,8"/>
<TextBox x:Name="PreviewPlayerPathBox" Grid.Row="3" Margin="0,0,8,0" ToolTip="Optional path to mpv.exe or another video player"/>
<Button Grid.Row="3" Grid.Column="1" Content="Browse…" Click="BrowsePreviewPlayer_Click"/>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="2" Margin="0,14,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<TextBlock Text="Advanced settings are saved automatically." Foreground="{DynamicResource MutedBrush}" VerticalAlignment="Center"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="Close" Click="CloseSettings_Click" MinWidth="140" Margin="0,0,10,0"/>
<Button Content="Done" Style="{StaticResource PrimaryButton}" Click="CloseSettings_Click" MinWidth="140"/>
</StackPanel>
</Grid>
</Grid>
</Border>
</Grid>
</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> </Grid>
</Window> </Window>
+150 -402
View File
@@ -1,425 +1,173 @@
using System.Collections.ObjectModel; using Microsoft.UI;
using System.Diagnostics; using Microsoft.UI.Windowing;
using System.Globalization; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using System;
using System.IO; using System.IO;
using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Runtime.InteropServices.WindowsRuntime;
using System.Windows; using Windows.Graphics;
using System.Windows.Controls; using Windows.UI;
using System.Windows.Interop; using WinRT.Interop;
using AmigaDB.VideoRenderer.Models;
using AmigaDB.VideoRenderer.Services;
using Microsoft.Win32;
using System.Windows.Media;
namespace AmigaDB.VideoRenderer; // To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
public partial class MainWindow : Window namespace AmiReel;
/// <summary>
/// The application window. This hosts a Frame that displays pages. Add your
/// UI and logic to MainPage.xaml / MainPage.xaml.cs instead of here so you
/// can use Page features such as navigation events and the Loaded lifecycle.
/// </summary>
public sealed partial class MainWindow : Window
{ {
private readonly ObservableCollection<string> _inputs = [];
private readonly AppSettings _loadedSettings;
private readonly StringBuilder _pendingLog = new();
private readonly object _logLock = new();
private CancellationTokenSource? _renderCancellation;
private string? _lastRenderedFile;
private bool _logFlushScheduled;
private bool _titleBarIsLight;
private bool _exitConfirmed; private bool _exitConfirmed;
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
InputList.ItemsSource = _inputs; App.MainWindowInstance = this;
_loadedSettings = AppSettingsStore.Load();
OutputFolderBox.Text = _loadedSettings.OutputFolder; ExtendsContentIntoTitleBar = true;
EndCardBox.Text = _loadedSettings.EndCardPath; SetTitleBar(AppTitleBar);
FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
FfprobePathBox.Text = _loadedSettings.FfprobePath; SetWindowIcon();
PreviewPlayerPathBox.Text = _loadedSettings.PreviewPlayerPath; SetDefaultSizeAndPosition();
TrimBox.Text = _loadedSettings.TrimStart; LoadTitleBarIcon();
FadeBox.Text = _loadedSettings.FadeSeconds;
HoldBox.Text = _loadedSettings.EndCardHoldSeconds; // Navigate the root frame to the main page on startup.
IntervalBox.Text = _loadedSettings.ThumbnailInterval; RootFrame.Navigate(typeof(MainPage));
SelectEncoder(_loadedSettings.Encoder);
SelectTheme(_loadedSettings.Theme); AppWindow.Closing += AppWindow_Closing;
ApplyTheme(_loadedSettings.Theme);
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; /// <summary>
/// Sets the window's taskbar/Alt+Tab icon. <see cref="AppWindow.SetIcon(string)"/> requires an
/// actual .ico file path (an .exe path is silently ignored), so the embedded .ico is extracted
/// once to a stable file under %LOCALAPPDATA% and that path is used instead.
/// </summary>
private void SetWindowIcon()
{
try
{
string iconPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"AmiReel", "app-icon.ico");
private void MaximizeRestore_Click(object sender, RoutedEventArgs e) if (!File.Exists(iconPath))
=> WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized; {
Directory.CreateDirectory(Path.GetDirectoryName(iconPath)!);
Assembly assembly = Assembly.GetExecutingAssembly();
using Stream? resource = assembly.GetManifestResourceStream("AmiReel.Assets.AppIcon.ico");
if (resource is null) return;
private void CloseWindowButton_Click(object sender, RoutedEventArgs e) => Close(); using FileStream file = File.Create(iconPath);
resource.CopyTo(file);
}
private void MainWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) AppWindow.SetIcon(iconPath);
}
catch
{
// Non-critical: the window simply keeps whatever default icon Windows assigned.
}
}
/// <summary>
/// Loads the title bar icon from an embedded resource instead of an "ms-appx:///Assets/..."
/// URI. A single-file self-contained publish bundles Content/Assets items into the exe in a
/// way the ms-appx resource pipeline can no longer resolve at runtime, leaving the title bar
/// blank; a plain embedded resource stream works the same in every publish mode.
/// </summary>
private async void LoadTitleBarIcon()
{
try
{
Assembly assembly = Assembly.GetExecutingAssembly();
using Stream? stream = assembly.GetManifestResourceStream("AmiReel.Assets.TitleBarIcon.png");
if (stream is null) return;
BitmapImage bitmap = new();
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
TitleBarIcon.Source = bitmap;
}
catch
{
// Non-critical: the title bar simply shows no icon if this fails.
}
}
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; if (_exitConfirmed) return;
e.Cancel = true; args.Cancel = true;
ExitConfirmOverlay.Visibility = Visibility.Visible;
}
private void CancelExit_Click(object sender, RoutedEventArgs e) => ExitConfirmOverlay.Visibility = Visibility.Collapsed; FrameworkElement root = (FrameworkElement)Content;
ContentDialog dialog = DialogHelper.CreateStyled(RootFrame.XamlRoot, root.RequestedTheme);
dialog.Title = "Exit AmiReel?";
dialog.Content = "Are you sure you want to close the application?";
dialog.PrimaryButtonText = "Exit";
dialog.CloseButtonText = "Cancel";
dialog.DefaultButton = ContentDialogButton.Primary;
ContentDialogResult result = await dialog.ShowAsync();
if (result != ContentDialogResult.Primary) return;
private void ConfirmExit_Click(object sender, RoutedEventArgs e)
{
_exitConfirmed = true; _exitConfirmed = true;
SaveSettings();
Close(); Close();
} }
private void InputList_DragEnter(object sender, DragEventArgs e)
{
e.Effects = e.Data.GetDataPresent(DataFormats.FileDrop) ? DragDropEffects.Copy : DragDropEffects.None;
e.Handled = true;
}
private void InputList_Drop(object sender, DragEventArgs e)
{
if (!e.Data.GetDataPresent(DataFormats.FileDrop)) return;
string[] files = (string[])e.Data.GetData(DataFormats.FileDrop)!;
foreach (string file in files.Where(Models.SupportedVideoFormats.IsSupported).OrderBy(NaturalKey))
if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file);
}
private void AddInputs_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new() { Filter = $"Video files|{Models.SupportedVideoFormats.PickerFilter}", Multiselect = true };
if (dialog.ShowDialog(this) != true) return;
foreach (string file in dialog.FileNames.OrderBy(NaturalKey))
if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file);
}
private void ClearInputs_Click(object sender, RoutedEventArgs e) => _inputs.Clear();
private void BrowseEndCard_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new() { Filter = "Images|*.png;*.jpg;*.jpeg;*.webp;*.bmp|All files|*.*" };
if (dialog.ShowDialog(this) == true) EndCardBox.Text = dialog.FileName;
}
private void BrowseOutput_Click(object sender, RoutedEventArgs e)
{
OpenFolderDialog dialog = new() { InitialDirectory = OutputFolderBox.Text };
if (dialog.ShowDialog(this) == true) OutputFolderBox.Text = dialog.FolderName;
}
private void BrowseFfmpeg_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new() { Filter = "FFmpeg executable (ffmpeg.exe)|ffmpeg.exe|Executable files (*.exe)|*.exe|All files|*.*" };
if (dialog.ShowDialog(this) == true) FfmpegPathBox.Text = dialog.FileName;
}
private void BrowseFfprobe_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new() { Filter = "FFprobe executable (ffprobe.exe)|ffprobe.exe|Executable files (*.exe)|*.exe|All files|*.*" };
if (dialog.ShowDialog(this) == true) FfprobePathBox.Text = dialog.FileName;
}
private void BrowsePreviewPlayer_Click(object sender, RoutedEventArgs e)
{
OpenFileDialog dialog = new() { Filter = "Video player (*.exe)|*.exe|All files|*.*" };
if (dialog.ShowDialog(this) == true) PreviewPlayerPathBox.Text = dialog.FileName;
}
private void OpenSettings_Click(object sender, RoutedEventArgs e) => SettingsOverlay.Visibility = Visibility.Visible;
private void CloseSettings_Click(object sender, RoutedEventArgs e)
{
SettingsOverlay.Visibility = Visibility.Collapsed;
SaveSettings();
}
private async void Render_Click(object sender, RoutedEventArgs e)
{
try
{
RenderSettings settings = ReadSettings();
SaveSettings();
SetRendering(true);
LogBox.Clear();
_renderCancellation = new CancellationTokenSource();
Progress<RenderProgress> progress = new(UpdateProgress);
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);
MessageBox.Show(this, "The AmiReel render completed successfully.", "Render complete",
MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (OperationCanceledException)
{
UpdateProgress(new(RenderProgress.Value, "Cancelled", "The render was cancelled."));
}
catch (Exception exception)
{
AppendLog("ERROR: " + exception);
MessageBox.Show(this, UserFacingErrors.Summarize(exception), "Render failed", MessageBoxButton.OK, MessageBoxImage.Error);
StageText.Text = "Failed";
}
finally
{
_renderCancellation?.Dispose();
_renderCancellation = null;
SetRendering(false);
}
}
private RenderSettings ReadSettings()
{
static double Number(string text, string name)
{
if (!double.TryParse(text.Replace(',', '.'), NumberStyles.Float, CultureInfo.InvariantCulture, out double value) || value < 0)
throw new ArgumentException($"Enter a valid non-negative value for {name}.");
return value;
}
if (!int.TryParse(IntervalBox.Text, out int interval) || interval < 1)
throw new ArgumentException("Thumbnail interval must be at least one second.");
EncoderMode encoder = Enum.Parse<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,
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)
{
CancelButton.IsEnabled = false;
StatusText.Text = "Stopping FFmpeg…";
_renderCancellation?.Cancel();
}
private void OpenOutput_Click(object sender, RoutedEventArgs e)
{
if (Directory.Exists(OutputFolderBox.Text))
Process.Start(new ProcessStartInfo("explorer.exe", OutputFolderBox.Text) { UseShellExecute = true });
}
private void PreviewSource_Click(object sender, RoutedEventArgs e)
{
if (InputList.SelectedItem is string path)
OpenPreview(path);
}
private void PreviewRendered_Click(object sender, RoutedEventArgs e)
{
if (!string.IsNullOrWhiteSpace(_lastRenderedFile) && File.Exists(_lastRenderedFile))
OpenPreview(_lastRenderedFile);
}
private void InputList_SelectionChanged(object sender, SelectionChangedEventArgs e)
=> PreviewSourceButton.IsEnabled = InputList.SelectedItem is string;
private void SetRendering(bool rendering)
{
RenderButton.IsEnabled = !rendering;
CancelButton.IsEnabled = rendering;
}
private void UpdateProgress(RenderProgress value)
{
RenderProgress.Value = value.Percent;
PercentText.Text = $"{value.Percent:0}%";
StageText.Text = value.Stage;
StatusText.Text = value.Message;
}
private void AppendLog(string line)
{
lock (_logLock)
{
_pendingLog.AppendLine(line);
if (_logFlushScheduled)
return;
_logFlushScheduled = true;
}
_ = Dispatcher.BeginInvoke(() =>
{
string chunk;
lock (_logLock)
{
chunk = _pendingLog.ToString();
_pendingLog.Clear();
_logFlushScheduled = false;
}
if (chunk.Length == 0)
return;
LogBox.AppendText(chunk);
LogBox.ScrollToEnd();
});
}
private static string NaturalKey(string path)
{
string name = Path.GetFileNameWithoutExtension(path);
int underscore = name.LastIndexOf('_');
return underscore >= 0 && int.TryParse(name[(underscore + 1)..], out int number)
? name[..underscore] + number.ToString("D10")
: name;
}
private void ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (!IsLoaded) return;
string theme = SelectedTheme();
ApplyTheme(theme);
SaveSettings();
}
private void SaveSettings()
{
AppSettingsStore.Save(new AppSettings(
OutputFolderBox.Text.Trim(),
EndCardBox.Text.Trim(),
FfmpegPathBox.Text.Trim(),
FfprobePathBox.Text.Trim(),
PreviewPlayerPathBox.Text.Trim(),
SelectedTheme(),
SelectedEncoder(),
TrimBox.Text.Trim(),
FadeBox.Text.Trim(),
HoldBox.Text.Trim(),
IntervalBox.Text.Trim(),
MoveSourcesBox.IsChecked == true));
}
private void OpenPreview(string mediaPath)
{
if (!File.Exists(mediaPath))
{
MessageBox.Show(this, "The selected media file was not found.", "Preview unavailable",
MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
string playerPath = PreviewPlayerPathBox.Text.Trim();
try
{
if (!string.IsNullOrWhiteSpace(playerPath))
{
if (!File.Exists(playerPath))
throw new FileNotFoundException("Configured preview player was not found.", playerPath);
ProcessStartInfo customPlayer = new()
{
FileName = playerPath,
UseShellExecute = false
};
customPlayer.ArgumentList.Add(mediaPath);
Process.Start(customPlayer);
return;
}
Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true });
}
catch (Exception exception)
{
MessageBox.Show(this, exception.Message, "Preview failed", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private string SelectedTheme() => ((ComboBoxItem)ThemeBox.SelectedItem).Tag!.ToString()!;
private string SelectedEncoder() => ((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!;
private void SelectTheme(string theme)
{
foreach (ComboBoxItem item in ThemeBox.Items)
{
if (string.Equals(item.Tag?.ToString(), theme, StringComparison.OrdinalIgnoreCase))
{
ThemeBox.SelectedItem = item;
return;
}
}
ThemeBox.SelectedIndex = 0;
}
private void SelectEncoder(string encoder)
{
foreach (ComboBoxItem item in EncoderBox.Items)
{
if (string.Equals(item.Tag?.ToString(), encoder, StringComparison.OrdinalIgnoreCase))
{
EncoderBox.SelectedItem = item;
return;
}
}
EncoderBox.SelectedIndex = 0;
}
private void ApplyTheme(string theme)
{
bool light = string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase);
SetBrush("PageBrush", light ? "#F4F7FB" : "#0E1525");
SetBrush("PanelBrush", light ? "#FFFFFF" : "#162033");
SetBrush("PanelAltBrush", light ? "#EEF4FB" : "#1A2740");
SetBrush("FieldBrush", light ? "#F7FAFD" : "#1D2940");
SetBrush("BorderBrush", light ? "#C7D3E3" : "#2B3A58");
SetBrush("AccentBrush", light ? "#0E9AEF" : "#4AB8FF");
SetBrush("AccentSoftBrush", light ? "#D6EEFF" : "#13324F");
SetBrush("TextBrush", light ? "#122033" : "#F5F7FB");
SetBrush("MutedBrush", light ? "#5F7390" : "#9FB1CC");
SetBrush("ButtonBrush", light ? "#E5EDF7" : "#243554");
SetBrush("ButtonHoverBrush", light ? "#D7E4F4" : "#2E446C");
SetBrush("ButtonDisabledBrush", light ? "#EEF2F7" : "#2A3140");
SetBrush("ButtonDisabledTextBrush", light ? "#8C99AB" : "#7E8BA1");
SetBrush("PrimaryTextBrush", light ? "#FFFFFF" : "#07111D");
SetBrush("SelectionBrush", light ? "#BEE4FF" : "#295C87");
SetBrush("SelectionTextBrush", light ? "#122033" : "#FFFFFF");
_titleBarIsLight = light;
UpdateTitleBarTheme(light);
}
private void SetBrush(string key, string color)
{
Application.Current.Resources[key] = new SolidColorBrush((Color)ColorConverter.ConvertFromString(color));
}
private void UpdateTitleBarTheme(bool light)
{
IntPtr hwnd = new WindowInteropHelper(this).Handle;
if (hwnd == IntPtr.Zero) return;
int useImmersiveDarkMode = light ? 0 : 1;
DwmSetWindowAttribute(hwnd, DWMWA_USE_IMMERSIVE_DARK_MODE, ref useImmersiveDarkMode, sizeof(int));
int captionColor = ToColorRef(light ? "#F4F7FB" : "#0E1525");
int textColor = ToColorRef(light ? "#122033" : "#F5F7FB");
DwmSetWindowAttribute(hwnd, DWMWA_CAPTION_COLOR, ref captionColor, sizeof(int));
DwmSetWindowAttribute(hwnd, DWMWA_TEXT_COLOR, ref textColor, sizeof(int));
}
private static int ToColorRef(string hexColor)
{
Color color = (Color)ColorConverter.ConvertFromString(hexColor);
return color.R | (color.G << 8) | (color.B << 16);
}
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;
private const int DWMWA_CAPTION_COLOR = 35;
private const int DWMWA_TEXT_COLOR = 36;
[DllImport("dwmapi.dll", PreserveSig = true)]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attribute, ref int value, int size);
} }
+68 -40
View File
@@ -1,46 +1,74 @@
namespace AmigaDB.VideoRenderer.Models; namespace AmiReel.Models;
public sealed record AppSettings( /// <summary>
string OutputFolder, /// Persisted user settings (%LOCALAPPDATA%\AmiReel\settings.json). Uses named init
string EndCardPath, /// properties rather than a positional constructor: several members share the same
string FfmpegPath, /// <see langword="string"/> type (e.g. TrimStart/FadeSeconds/EndCardHoldSeconds), so a
string FfprobePath, /// positional record would let two arguments be silently transposed at a call site
string PreviewPlayerPath, /// without the compiler catching it.
string Theme, /// </summary>
string Encoder, public sealed record AppSettings
string TrimStart,
string FadeSeconds,
string EndCardHoldSeconds,
string ThumbnailInterval,
bool? MoveSourcesToOriginals)
{ {
public required string OutputFolder { get; init; }
public required string EndCardPath { get; init; }
public required string FfmpegPath { get; init; }
public required string FfprobePath { get; init; }
public required string PreviewPlayerPath { get; init; }
public required string Theme { get; init; }
public required string Encoder { get; init; }
public required string TrimStart { get; init; }
public required string FadeSeconds { get; init; }
public required string EndCardHoldSeconds { get; init; }
public required string ThumbnailInterval { get; init; }
public bool? MoveSourcesToOriginals { get; init; }
// Shorts tab defaults (sticky across launches, same as the fields above).
public string ShortsStyle { get; init; } = "Brand";
public string ShortsHook { get; init; } = "THIS RAN ON AN AMIGA";
public string ShortsWebsite { get; init; } = "AMIGADB.NET";
public string ShortsFontFile { get; init; } = "";
public string ShortsBackgroundImage { get; init; } = "";
public string ShortsCrf { get; init; } = "16";
public string ShortsPreset { get; init; } = "slow";
public bool ShouldMoveSourcesToOriginals => MoveSourcesToOriginals != false; public bool ShouldMoveSourcesToOriginals => MoveSourcesToOriginals != false;
public static AppSettings Default(string outputFolder) => new( public static AppSettings Default(string outputFolder) => new()
outputFolder, {
"", OutputFolder = outputFolder,
"", EndCardPath = "",
"", FfmpegPath = "",
"", FfprobePath = "",
"Dark", PreviewPlayerPath = "",
"Auto", Theme = "Dark",
"4.414", Encoder = "Auto",
"3", TrimStart = "4.414",
"4", FadeSeconds = "3",
"10", EndCardHoldSeconds = "4",
true); ThumbnailInterval = "10",
MoveSourcesToOriginals = true,
};
public AppSettings Normalize(string fallbackOutputFolder) => new( public AppSettings Normalize(string fallbackOutputFolder) => this with
string.IsNullOrWhiteSpace(OutputFolder) ? fallbackOutputFolder : OutputFolder, {
EndCardPath ?? "", OutputFolder = string.IsNullOrWhiteSpace(OutputFolder) ? fallbackOutputFolder : OutputFolder,
FfmpegPath ?? "", EndCardPath = EndCardPath ?? "",
FfprobePath ?? "", FfmpegPath = FfmpegPath ?? "",
PreviewPlayerPath ?? "", FfprobePath = FfprobePath ?? "",
string.IsNullOrWhiteSpace(Theme) ? "Dark" : Theme, PreviewPlayerPath = PreviewPlayerPath ?? "",
string.IsNullOrWhiteSpace(Encoder) ? "Auto" : Encoder, Theme = string.IsNullOrWhiteSpace(Theme) ? "Dark" : Theme,
string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart, Encoder = string.IsNullOrWhiteSpace(Encoder) ? "Auto" : Encoder,
string.IsNullOrWhiteSpace(FadeSeconds) ? "3" : FadeSeconds, TrimStart = string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
string.IsNullOrWhiteSpace(EndCardHoldSeconds) ? "4" : EndCardHoldSeconds, FadeSeconds = string.IsNullOrWhiteSpace(FadeSeconds) ? "3" : FadeSeconds,
string.IsNullOrWhiteSpace(ThumbnailInterval) ? "10" : ThumbnailInterval, EndCardHoldSeconds = string.IsNullOrWhiteSpace(EndCardHoldSeconds) ? "4" : EndCardHoldSeconds,
MoveSourcesToOriginals ?? true); ThumbnailInterval = string.IsNullOrWhiteSpace(ThumbnailInterval) ? "10" : ThumbnailInterval,
MoveSourcesToOriginals = MoveSourcesToOriginals ?? true,
ShortsStyle = string.IsNullOrWhiteSpace(ShortsStyle) ? "Brand" : ShortsStyle,
ShortsHook = ShortsHook ?? "THIS RAN ON AN AMIGA",
ShortsWebsite = ShortsWebsite ?? "AMIGADB.NET",
ShortsFontFile = ShortsFontFile ?? "",
ShortsBackgroundImage = ShortsBackgroundImage ?? "",
ShortsCrf = string.IsNullOrWhiteSpace(ShortsCrf) ? "16" : ShortsCrf,
ShortsPreset = string.IsNullOrWhiteSpace(ShortsPreset) ? "slow" : ShortsPreset,
};
} }
+29 -19
View File
@@ -1,4 +1,4 @@
namespace AmigaDB.VideoRenderer.Models; namespace AmiReel.Models;
public enum EncoderMode public enum EncoderMode
{ {
@@ -7,23 +7,33 @@ public enum EncoderMode
CpuX264 CpuX264
} }
public sealed record RenderSettings( /// <summary>
IReadOnlyList<string> InputFiles, /// One render job's parameters. Uses named init properties rather than a positional
string EndCardPath, /// constructor: TrimStart/FadeSeconds/EndCardHoldSeconds are three consecutive
string OutputDirectory, /// <see langword="double"/> members (and Width/Height/FramesPerSecond/ThumbnailWidth/
string OutputName, /// ThumbnailHeight five consecutive <see langword="int"/> members), so a positional
string FfmpegPath, /// record would let arguments be silently transposed at a call site without the
string FfprobePath, /// compiler catching it.
EncoderMode Encoder, /// </summary>
double TrimStart, public sealed record RenderSettings
double FadeSeconds, {
double EndCardHoldSeconds, public required IReadOnlyList<string> InputFiles { get; init; }
int ThumbnailInterval, public required string EndCardPath { get; init; }
bool MoveSourcesToOriginals = true, public required string OutputDirectory { get; init; }
int Width = 3840, public required string OutputName { get; init; }
int Height = 2160, public required string FfmpegPath { get; init; }
int FramesPerSecond = 50, public required string FfprobePath { get; init; }
int ThumbnailWidth = 1280, public required EncoderMode Encoder { get; init; }
int ThumbnailHeight = 720); public required double TrimStart { get; init; }
public required double FadeSeconds { get; init; }
public required double EndCardHoldSeconds { get; init; }
public required int ThumbnailInterval { get; init; }
public bool MoveSourcesToOriginals { get; init; } = true;
public int Width { get; init; } = 3840;
public int Height { get; init; } = 2160;
public int FramesPerSecond { get; init; } = 50;
public int ThumbnailWidth { get; init; } = 1280;
public int ThumbnailHeight { get; init; } = 720;
}
public sealed record RenderProgress(double Percent, string Stage, string Message); public sealed record RenderProgress(double Percent, string Stage, string Message);
+47
View File
@@ -0,0 +1,47 @@
namespace AmiReel.Models;
/// <summary>Visual style applied to a YouTube Short. Mirrors the --style values of amigadb-short.sh.</summary>
public enum ShortStyle
{
Brand,
Pixel,
Mirror,
Crop,
Workbench,
Blur,
Crt,
Copper,
Stars,
Grid,
Tiles,
Scan,
Starfield,
Plasma,
Rasterbars,
Vhs,
Monitor,
Split,
Spectrum,
}
/// <summary>One YouTube Shorts render job's parameters (port of amigadb-short.sh's arguments).</summary>
public sealed record ShortSettings
{
public required string InputFile { get; init; }
public required string OutputPath { get; init; }
public required string Title { get; init; }
public string Group { get; init; } = "";
public string Year { get; init; } = "";
public string Type { get; init; } = "";
public required string Hook { get; init; }
public required string Website { get; init; }
public required string Start { get; init; }
public required double DurationSeconds { get; init; }
public required ShortStyle Style { get; init; }
public string BackgroundImage { get; init; } = "";
public required string FontFile { get; init; }
public required string FfmpegPath { get; init; }
public required string FfprobePath { get; init; }
public int Crf { get; init; } = 16;
public string Preset { get; init; } = "slow";
}
+1 -1
View File
@@ -1,7 +1,7 @@
using System.IO; using System.IO;
using System.Linq; using System.Linq;
namespace AmigaDB.VideoRenderer.Models; namespace AmiReel.Models;
/// <summary> /// <summary>
/// Video file extensions accepted as render sources. FFmpeg can demux far more than this, /// Video file extensions accepted as render sources. FFmpeg can demux far more than this,
@@ -5,8 +5,7 @@
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest" xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10" IgnorableNamespaces="uap rescap">
IgnorableNamespaces="uap rescap systemai">
<Identity <Identity
Name="F004AE73-5989-46F3-B913-E9A3A355713F" Name="F004AE73-5989-46F3-B913-E9A3A355713F"
@@ -48,6 +47,5 @@
<Capabilities> <Capabilities>
<rescap:Capability Name="runFullTrust" /> <rescap:Capability Name="runFullTrust" />
<systemai:Capability Name="systemAIModels"/>
</Capabilities> </Capabilities>
</Package> </Package>
+3
View File
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("AmiReel.Tests")]
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>ARM64</Platform>
<RuntimeIdentifier>win-arm64</RuntimeIdentifier>
<PublishDir>bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>False</PublishSingleFile>
</PropertyGroup>
</Project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>x64</Platform>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<PublishDir>bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>False</PublishSingleFile>
</PropertyGroup>
</Project>
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Platform>x86</Platform>
<RuntimeIdentifier>win-x86</RuntimeIdentifier>
<PublishDir>bin\$(Configuration)\$(TargetFramework)\$(RuntimeIdentifier)\publish\</PublishDir>
<SelfContained>true</SelfContained>
<PublishSingleFile>False</PublishSingleFile>
</PropertyGroup>
</Project>
@@ -1,9 +1,9 @@
{ {
"profiles": { "profiles": {
"AmiReel.WinUI (Package)": { "AmiReel (Package)": {
"commandName": "MsixPackage" "commandName": "MsixPackage"
}, },
"AmiReel.WinUI (Unpackaged)": { "AmiReel (Unpackaged)": {
"commandName": "Project" "commandName": "Project"
} }
} }
+115 -116
View File
@@ -1,111 +1,130 @@
# AmiReel # AmiReel
AmiReel is a Windows video-rendering tool for turning Amiga AVI captures into: AmiReel is a Windows desktop app for turning Amiga (or any) screen recordings into:
- a 4K 50 FPS final video, - a 4K 50 FPS final video,
- PNG/JPG thumbnails, - PNG/JPG thumbnails,
- an animated WebP preview, - an animated WebP preview,
- and a publishable Windows desktop app workflow. - a vertical (1080×1920, 50 FPS) YouTube Short with a styled title card.
The repository currently contains two desktop frontends that share the same render pipeline: Built with **WinUI 3** on the Windows App SDK, driving FFmpeg under the hood. The app has two
tabs — **Video** (the full-length render pipeline above) and **Shorts** (see below) — sharing
- `AmiReel.WinUI` the same render progress, log, and output controls.
The newer WinUI 3 application. This is the default publish target.
- `AmigaDB.VideoRenderer.csproj`
The existing WPF application.
## Features ## Features
- multiple ordered AVI inputs ### Video tab
- Multiple ordered video inputs — `.avi`, `.mp4`, `.m4v`, `.mov`, `.mkv`, `.webm`, `.wmv`, `.flv`, `.mpg`, `.mpeg`, `.ts`, `.3gp`
- Drag-and-drop or file-picker source selection
- 3840×2160 output at 50 FPS - 3840×2160 output at 50 FPS
- Configurable trim, fade, end-card hold, and thumbnail interval
- Optional end-card image
- Optional move of source recordings into `originals/` in the output folder
### Shorts tab
- Turns one source video into a vertical YouTube Short with an opening hook, title/group/year/
type card, and closing website call-to-action, all as timed text overlays
- 19 visual styles ported from `amigadb-short.sh``brand` (title card image), `pixel`,
`mirror`, `crop`, `workbench`, `blur`, `crt`, `copper`, `stars`, `grid`, `tiles`, `scan`,
`starfield`, `plasma`, `rasterbars`, `vhs`, `monitor`, `split`, `spectrum` (needs audio)
— see [`Services/ShortsPipeline.cs`](Services/ShortsPipeline.cs) for the exact FFmpeg filtergraphs
- Configurable clip start/duration, CRF, and x264 preset
- Style, font, background image, hook, and website text are sticky across launches
### Shared
- NVIDIA NVENC with automatic CPU `libx264` fallback - NVIDIA NVENC with automatic CPU `libx264` fallback
- configurable trim, fade, end-card hold, and thumbnail interval - Live render progress with frame counters and FFmpeg log output
- optional end-card image - Source and final-render preview (built-in or a custom player)
- live render progress with frame counters - Dark and light themes
- FFmpeg log output - Optional custom `ffmpeg.exe` / `ffprobe.exe` paths, with automatic detection from `PATH`
- source video preview and final render preview - Self-contained single-file publish for easy distribution
- dark and light themes
- optional move of source recordings into `originals` in the output folder ## Project Layout
- optional custom `ffmpeg.exe` / `ffprobe.exe` paths
- automatic FFmpeg detection from `PATH` ```
- single-file WinUI publish for easier distribution AmiReel.csproj Project file
AmiReel.slnx Solution file
App.xaml(.cs) Application entry point
MainWindow.xaml(.cs) Window shell: custom title bar, theming, exit confirmation
MainPage.xaml(.cs) Main UI: source list, settings, render controls
DialogHelper.cs Shared ContentDialog styling (Settings, exit, error dialogs)
Package.appxmanifest MSIX packaging identity and tile/icon declarations
app.manifest Win32 manifest (DPI awareness, OS compatibility)
Assets/ Packaged app icons and tiles (all required sizes)
branding/ Source artwork the Assets/ tiles are generated from
Models/
AppSettings.cs Persisted user settings (Video + Shorts defaults)
RenderSettings.cs Video render job parameters
ShortSettings.cs Shorts render job parameters, ShortStyle enum
SupportedVideoFormats.cs Accepted input file extensions
Services/
RenderPipeline.cs Video render workflow (FFmpeg orchestration)
ShortsPipeline.cs Shorts render workflow; the 19 style filtergraphs live here
ProcessRunner.cs FFmpeg process execution and progress parsing
FfmpegProgressParser.cs Parses time=/frame=/fps=/speed= from FFmpeg output lines
FfmpegOutputFilter.cs Shared FFmpeg banner/boilerplate line filtering
ToolExtractor.cs FFmpeg resolution, extraction, and fallback logic
MediaProbe.cs Duration/audio-stream probing via ffprobe
FfmpegLibraryProbe.cs NVENC capability probing
AppSettingsStore.cs Settings load/save (JSON in %LOCALAPPDATA%)
UserFacingErrors.cs Exception-to-message translation
Properties/
AssemblyInfo.cs InternalsVisibleTo AmiReel.Tests
launchSettings.json
PublishProfiles/ win-x64/x86/arm64 publish profiles
AmiReel.Tests/ Unit tests (MSTest), mirrors Models/ and Services/
amigadb-short.sh Original bash/FFmpeg script that ShortsPipeline.cs is a C# port of
ThirdParty/ Embedded ffmpeg.exe / ffprobe.exe (not checked in, see below)
publish-win-x64.ps1 Publish script
```
## Requirements ## Requirements
For development: For development:
- Windows 10 or Windows 11 - Windows 10 or Windows 11
- .NET SDK - .NET 10 SDK with WinUI 3 / Windows App SDK workload
- WPF project: .NET 8 SDK - Windows x64 FFmpeg binaries in `ThirdParty/`:
- WinUI project: .NET 10 SDK and WinUI/Windows App SDK support
- Windows x64 FFmpeg binaries in `ThirdParty`
- `ThirdParty\ffmpeg.exe` - `ThirdParty\ffmpeg.exe`
- `ThirdParty\ffprobe.exe` - `ThirdParty\ffprobe.exe`
For end users: For end users:
- Windows 10 or Windows 11 x64 - Windows 10 or Windows 11 x64
- the published AmiReel executable - The published `AmiReel.exe` — FFmpeg is not required separately if you distribute AmiReel with the embedded `ThirdParty` binaries
- FFmpeg is not required separately if you distribute AmiReel with embedded `ThirdParty` FFmpeg binaries
## Project Layout
- `AmiReel.WinUI\`
WinUI 3 frontend
- `MainWindow.xaml` and `MainWindow.xaml.cs`
WPF frontend
- `Services\RenderPipeline.cs`
Main render workflow
- `Services\ProcessRunner.cs`
FFmpeg process execution and progress parsing
- `Services\ToolExtractor.cs`
FFmpeg resolution, extraction, and fallback logic
- `publish-win-x64.ps1`
Publish script
## Run in Development ## Run in Development
### WinUI
```powershell ```powershell
dotnet build .\AmiReel.WinUI\AmiReel.WinUI.csproj dotnet build .\AmiReel.csproj
dotnet run --project .\AmiReel.WinUI\AmiReel.WinUI.csproj dotnet run --project .\AmiReel.csproj
``` ```
### WPF ## Run Tests
Unit tests live in `AmiReel.Tests/` (MSTest), covering the pure logic in `Models/` and
`Services/` — settings normalization, supported-format detection, FFmpeg output parsing/
filtering, render-settings validation, moving source files into `originals/`, and the Shorts
filtergraph/escaping/validation logic.
```powershell ```powershell
dotnet build .\AmigaDB.VideoRenderer.csproj dotnet test .\AmiReel.Tests\AmiReel.Tests.csproj
dotnet run --project .\AmigaDB.VideoRenderer.csproj
``` ```
UI code-behind (`App`, `MainWindow`, `MainPage`) and anything that spawns an actual FFmpeg
process are intentionally left to manual/integration testing rather than unit tests.
## Publish ## Publish
### Default: WinUI
The publish script now defaults to the WinUI application:
```powershell ```powershell
.\publish-win-x64.ps1 .\publish-win-x64.ps1
``` ```
This publishes the WinUI app as a self-contained single-file executable. This publishes a self-contained, single-file `win-x64` executable. At the end of the script
you'll see the release folder and final executable path, for example:
At the end of the script you will see:
- the release folder path
- the final executable path
Example output path:
```text ```text
AmiReel.WinUI\bin\Release\net10.0-windows10.0.26100.0\win-x64\publish\AmiReel.exe bin\Release\net10.0-windows10.0.26100.0\win-x64\publish\AmiReel.exe
```
### Publish WPF instead
```powershell
.\publish-win-x64.ps1 -Target Wpf
``` ```
## Installation ## Installation
@@ -113,42 +132,33 @@ AmiReel.WinUI\bin\Release\net10.0-windows10.0.26100.0\win-x64\publish\AmiReel.ex
### For developers ### For developers
1. Clone the repository. 1. Clone the repository.
2. Add `ffmpeg.exe` and `ffprobe.exe` to `ThirdParty`. 2. Add `ffmpeg.exe` and `ffprobe.exe` to `ThirdParty\`.
3. Restore and build the desired app: 3. Restore and build:
```powershell ```powershell
dotnet build .\AmigaDB.VideoRenderer.sln dotnet build .\AmiReel.slnx
``` ```
4. Run either the WinUI or WPF project.
### For end users ### For end users
1. Copy the published `AmiReel.exe` from the publish folder to any location, for example: 1. Copy the published `AmiReel.exe` to any location, for example `C:\Apps\AmiReel\`.
```text
C:\Apps\AmiReel\
```
2. Double-click `AmiReel.exe`. 2. Double-click `AmiReel.exe`.
3. If FFmpeg is embedded in the build, no extra setup is required. 3. If FFmpeg is embedded in the build, no extra setup is required.
4. If you want to use your own FFmpeg build: 4. To use your own FFmpeg build instead, open **Settings** and set:
open `Settings` and select: - `ffmpeg.exe` path
- `ffmpeg.exe` - `ffprobe.exe` path
- `ffprobe.exe`
### Optional desktop shortcut ### Optional desktop shortcut
1. Right-click `AmiReel.exe`. Right-click `AmiReel.exe`**Send to****Desktop (create shortcut)**.
2. Choose `Send to` -> `Desktop (create shortcut)`.
## FFmpeg Behavior ## FFmpeg Behavior
AmiReel resolves FFmpeg in this order: AmiReel resolves FFmpeg in this order:
1. custom paths from settings 1. Custom paths from Settings
2. `ffmpeg.exe` and `ffprobe.exe` found on system `PATH` 2. `ffmpeg.exe` / `ffprobe.exe` found on the system `PATH`
3. embedded `ThirdParty` binaries 3. Embedded `ThirdParty` binaries (extracted to `%LOCALAPPDATA%\AmiReel\tools` on first use)
If NVENC cannot be initialized, AmiReel automatically falls back to CPU `libx264`. If NVENC cannot be initialized, AmiReel automatically falls back to CPU `libx264`.
@@ -160,41 +170,30 @@ User settings are stored in:
%LOCALAPPDATA%\AmiReel\settings.json %LOCALAPPDATA%\AmiReel\settings.json
``` ```
This includes values such as: This includes: output folder, end-card path, FFmpeg/preview-player paths, theme, encoder
selection, timing settings, whether source videos are moved into `originals/` after a
- output folder successful render, and the Shorts tab's sticky defaults (style, font, background image, CRF,
- end-card path preset, hook, and website text).
- FFmpeg paths
- theme
- encoder selection
- timing settings
- whether source videos are moved into `originals` after a successful render
## Notes for Distribution ## Notes for Distribution
- If you distribute FFmpeg binaries with AmiReel, you are responsible for complying with the license terms of the FFmpeg build you use. - If you distribute FFmpeg binaries with AmiReel, you are responsible for complying with the
- Keep any required notices, source offer, or attribution required by that FFmpeg distribution. license terms of the FFmpeg build you use. Keep any required notices, source offer, or
- Windows Explorer may cache executable icons. If a freshly published build still shows an old icon, rename the file or refresh the icon cache before assuming the embed failed. attribution required by that distribution.
- Windows Explorer may cache executable icons. If a freshly published build still shows an old
icon, rename the file or refresh the icon cache before assuming the embed failed.
- `Package.appxmanifest` currently has a placeholder `Identity` (a random GUID `Name` and
`Publisher="CN=AppPublisher"`). That's fine for local unpackaged builds, but before signing
an MSIX for real distribution, replace them with a real publisher identity and generate a
matching signing certificate (`winapp cert generate`, see `AGENTS.md`).
## Troubleshooting ## Troubleshooting
### App falls back to CPU instead of NVENC **App falls back to CPU instead of NVENC** — FFmpeg could not initialize `h264_nvenc`. AmiReel
continues automatically with CPU encoding.
This usually means FFmpeg could not initialize `h264_nvenc`. AmiReel will continue with CPU encoding automatically. **FFmpeg tool error** — If the embedded FFmpeg is missing or broken, AmiReel tries configured
tool paths, then FFmpeg from `PATH`. Set the paths manually in **Settings** if needed.
### FFmpeg tool error **Render failed popup** — Shows a short human-readable summary. Full technical details remain
available in the **FFmpeg log** panel.
If embedded FFmpeg is missing or broken, AmiReel will try:
1. configured tool paths
2. FFmpeg from `PATH`
If needed, set the paths manually in `Settings`.
### Render failed popup
The popup shows a short human-readable summary. Full technical details remain available in the `FFmpeg log` panel.
## Current Default Recommendation
Use the WinUI build for publishing and distribution unless you specifically need the WPF variant.
+2 -2
View File
@@ -1,8 +1,8 @@
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
using AmigaDB.VideoRenderer.Models; using AmiReel.Models;
namespace AmigaDB.VideoRenderer.Services; namespace AmiReel.Services;
public static class AppSettingsStore public static class AppSettingsStore
{ {
+1 -1
View File
@@ -4,7 +4,7 @@ using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace AmigaDB.VideoRenderer.Services; namespace AmiReel.Services;
internal sealed partial class FfmpegLibraryProbe : IDisposable internal sealed partial class FfmpegLibraryProbe : IDisposable
{ {
+40
View File
@@ -0,0 +1,40 @@
namespace AmiReel.Services;
/// <summary>
/// Recognizes the FFmpeg/FFprobe startup banner and stream-info boilerplate that both the
/// live log filter (<see cref="ProcessRunner"/>) and the error summarizer
/// (<see cref="UserFacingErrors"/>) want to hide — the version/library banner, input/output
/// stream dumps, and the final size/duration summary line are never useful to a user.
/// </summary>
public static class FfmpegOutputFilter
{
private static readonly string[] BoilerplatePrefixes =
[
"ffmpeg version ",
"built with ",
"configuration:",
"libavutil",
"libavcodec",
"libavformat",
"libavdevice",
"libavfilter",
"libswscale",
"libswresample",
"Input #",
"Output #",
"Stream mapping:",
"Stream #",
"Metadata:",
"Duration:",
"Press [q] to stop",
"video:",
"audio:",
"subtitle:",
"other streams:",
"global headers:",
"muxing overhead:",
];
public static bool IsBoilerplateLine(string line) =>
BoilerplatePrefixes.Any(prefix => line.StartsWith(prefix, StringComparison.OrdinalIgnoreCase));
}
+44
View File
@@ -0,0 +1,44 @@
using System.Globalization;
using System.Text.RegularExpressions;
namespace AmiReel.Services;
public sealed record FfmpegOutputLine(string Line, TimeSpan? Time, double? FramesPerSecond, double? Speed, int? Frame);
/// <summary>
/// Parses a single line of FFmpeg stdout/stderr for the `time=`/`frame=`/`fps=`/`speed=`
/// progress fields FFmpeg prints while encoding.
/// </summary>
public static partial class FfmpegProgressParser
{
public static FfmpegOutputLine Parse(string line)
{
TimeSpan? time = null;
double? fps = null;
double? speed = null;
int? frame = null;
Match timeMatch = TimeRegex().Match(line);
if (timeMatch.Success && TimeSpan.TryParse(timeMatch.Groups[1].Value, CultureInfo.InvariantCulture, out TimeSpan parsedTime))
time = parsedTime;
Match frameMatch = FrameRegex().Match(line);
if (frameMatch.Success)
{
if (int.TryParse(frameMatch.Groups["frame"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedFrame))
frame = parsedFrame;
if (double.TryParse(frameMatch.Groups["fps"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedFps))
fps = parsedFps;
if (double.TryParse(frameMatch.Groups["speed"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedSpeed))
speed = parsedSpeed;
}
return new FfmpegOutputLine(line, time, fps, speed, frame);
}
[GeneratedRegex(@"time=(\d{2}:\d{2}:\d{2}(?:\.\d+)?)", RegexOptions.CultureInvariant)]
private static partial Regex TimeRegex();
[GeneratedRegex(@"frame=\s*(?<frame>\d+).*?fps=\s*(?<fps>\d+(?:\.\d+)?).*?speed=\s*(?<speed>\d+(?:\.\d+)?)x", RegexOptions.CultureInvariant)]
private static partial Regex FrameRegex();
}
+1 -1
View File
@@ -1,7 +1,7 @@
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
namespace AmigaDB.VideoRenderer.Services; namespace AmiReel.Services;
internal sealed class MediaProbe internal sealed class MediaProbe
{ {
+8 -65
View File
@@ -1,15 +1,11 @@
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.Text; using System.Text;
using System.Text.RegularExpressions;
using System.IO; using System.IO;
namespace AmigaDB.VideoRenderer.Services; namespace AmiReel.Services;
public sealed partial class ProcessRunner public sealed class ProcessRunner
{ {
public sealed record ProcessOutput(string Line, TimeSpan? Time, double? FramesPerSecond, double? Speed, int? Frame);
public async Task<string> RunAsync( public async Task<string> RunAsync(
string executable, string executable,
IEnumerable<string> arguments, IEnumerable<string> arguments,
@@ -17,7 +13,7 @@ public sealed partial class ProcessRunner
Action<TimeSpan>? position, Action<TimeSpan>? position,
CancellationToken token, CancellationToken token,
bool allowFailure = false, bool allowFailure = false,
Action<ProcessOutput>? outputHandler = null) Action<FfmpegOutputLine>? outputHandler = null)
{ {
ProcessStartInfo start = new() ProcessStartInfo start = new()
{ {
@@ -51,12 +47,12 @@ public sealed partial class ProcessRunner
private static async Task PumpAsync( private static async Task PumpAsync(
StreamReader reader, StringBuilder output, Action<string>? log, StreamReader reader, StringBuilder output, Action<string>? log,
Action<TimeSpan>? position, CancellationToken token, Action<ProcessOutput>? outputHandler) Action<TimeSpan>? position, CancellationToken token, Action<FfmpegOutputLine>? outputHandler)
{ {
while (await reader.ReadLineAsync(token) is { } line) while (await reader.ReadLineAsync(token) is { } line)
{ {
output.AppendLine(line); output.AppendLine(line);
ProcessOutput parsed = ParseOutput(line); FfmpegOutputLine parsed = FfmpegProgressParser.Parse(line);
if (!IsNoise(parsed)) if (!IsNoise(parsed))
log?.Invoke(line); log?.Invoke(line);
if (parsed.Time is { } time) if (parsed.Time is { } time)
@@ -65,32 +61,7 @@ public sealed partial class ProcessRunner
} }
} }
private static ProcessOutput ParseOutput(string line) internal static bool IsNoise(FfmpegOutputLine output)
{
TimeSpan? time = null;
double? fps = null;
double? speed = null;
int? frame = null;
Match timeMatch = TimeRegex().Match(line);
if (timeMatch.Success && TimeSpan.TryParse(timeMatch.Groups[1].Value, CultureInfo.InvariantCulture, out TimeSpan parsedTime))
time = parsedTime;
Match frameMatch = FrameRegex().Match(line);
if (frameMatch.Success)
{
if (int.TryParse(frameMatch.Groups["frame"].Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out int parsedFrame))
frame = parsedFrame;
if (double.TryParse(frameMatch.Groups["fps"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedFps))
fps = parsedFps;
if (double.TryParse(frameMatch.Groups["speed"].Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedSpeed))
speed = parsedSpeed;
}
return new ProcessOutput(line, time, fps, speed, frame);
}
private static bool IsNoise(ProcessOutput output)
{ {
string line = output.Line.TrimStart(); string line = output.Line.TrimStart();
if (string.IsNullOrWhiteSpace(line)) if (string.IsNullOrWhiteSpace(line))
@@ -111,39 +82,11 @@ public sealed partial class ProcessRunner
|| line.Contains("Output file is empty", StringComparison.OrdinalIgnoreCase)) || line.Contains("Output file is empty", StringComparison.OrdinalIgnoreCase))
return false; return false;
return line.StartsWith("ffmpeg version ", StringComparison.OrdinalIgnoreCase) return FfmpegOutputFilter.IsBoilerplateLine(line)
|| line.StartsWith("built with ", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("configuration:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavutil", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavcodec", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavformat", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavdevice", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavfilter", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libswscale", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libswresample", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Input #", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Output #", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Stream mapping:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Stream #", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Metadata:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("encoder :", StringComparison.OrdinalIgnoreCase) || line.StartsWith("encoder :", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("title :", StringComparison.OrdinalIgnoreCase) || line.StartsWith("title :", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("CPB properties:", StringComparison.OrdinalIgnoreCase) || line.StartsWith("CPB properties:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Press [q] to stop", StringComparison.OrdinalIgnoreCase) || line.StartsWith("[", StringComparison.OrdinalIgnoreCase);
|| line.StartsWith("[", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Duration:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("video:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("audio:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("subtitle:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("other streams:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("global headers:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("muxing overhead:", StringComparison.OrdinalIgnoreCase);
} }
[GeneratedRegex(@"time=(\d{2}:\d{2}:\d{2}(?:\.\d+)?)", RegexOptions.CultureInvariant)]
private static partial Regex TimeRegex();
[GeneratedRegex(@"frame=\s*(?<frame>\d+).*?fps=\s*(?<fps>\d+(?:\.\d+)?).*?speed=\s*(?<speed>\d+(?:\.\d+)?)x", RegexOptions.CultureInvariant)]
private static partial Regex FrameRegex();
} }
+4 -4
View File
@@ -1,9 +1,9 @@
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Text; using System.Text;
using AmigaDB.VideoRenderer.Models; using AmiReel.Models;
namespace AmigaDB.VideoRenderer.Services; namespace AmiReel.Services;
public sealed class RenderPipeline public sealed class RenderPipeline
{ {
@@ -255,7 +255,7 @@ public sealed class RenderPipeline
}); });
} }
private static string BuildProgressMessage(TimeSpan current, double durationSeconds, double? fps, double? speed, int? frame, int framesPerSecond) internal static string BuildProgressMessage(TimeSpan current, double durationSeconds, double? fps, double? speed, int? frame, int framesPerSecond)
{ {
TimeSpan total = TimeSpan.FromSeconds(durationSeconds); TimeSpan total = TimeSpan.FromSeconds(durationSeconds);
List<string> parts = [$"{current:hh\\:mm\\:ss} / {total:hh\\:mm\\:ss}"]; List<string> parts = [$"{current:hh\\:mm\\:ss} / {total:hh\\:mm\\:ss}"];
@@ -358,7 +358,7 @@ public sealed class RenderPipeline
} }
} }
private static void Validate(RenderSettings s) internal static void Validate(RenderSettings s)
{ {
if (s.InputFiles.Count == 0) throw new ArgumentException("Select at least one video 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 input files no longer exist."); if (s.InputFiles.Any(f => !File.Exists(f))) throw new FileNotFoundException("One or more input files no longer exist.");
+386
View File
@@ -0,0 +1,386 @@
using System.Globalization;
using System.IO;
using System.Text;
using AmiReel.Models;
namespace AmiReel.Services;
/// <summary>
/// Renders a vertical (1080x1920, 50 FPS) YouTube Short from an existing video, with a chosen
/// visual style and title-card text overlays. A C# port of amigadb-short.sh so end users don't
/// need bash/WSL — the FFmpeg filtergraphs below are copied verbatim from that script.
/// </summary>
public sealed class ShortsPipeline
{
private readonly ProcessRunner _runner = new();
public async Task RenderAsync(ShortSettings settings, IProgress<RenderProgress> progress, Action<string> log, CancellationToken token)
{
Validate(settings);
ToolPaths tools = await ToolExtractor.ResolveAsync(settings.FfmpegPath, settings.FfprobePath, token);
if (settings.Style == ShortStyle.Spectrum)
{
MediaProbe probe = new(_runner, log);
bool hasAudio = await probe.HasAudioAsync(tools, settings.InputFile, token);
if (!hasAudio)
throw new InvalidOperationException("The spectrum style requires an audio stream in the source video.");
}
string? outputDirectory = Path.GetDirectoryName(Path.GetFullPath(settings.OutputPath));
if (!string.IsNullOrEmpty(outputDirectory))
Directory.CreateDirectory(outputDirectory);
string workDir = Path.Combine(Path.GetTempPath(), "AmiReel", "shorts", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(workDir);
try
{
string hookFile = WriteTextFile(workDir, "hook.txt", settings.Hook);
string titleFile = WriteTextFile(workDir, "title.txt", settings.Title);
string metaFile = WriteTextFile(workDir, "meta.txt", BuildMeta(settings));
string websiteFile = WriteTextFile(workDir, "website.txt", settings.Website);
double duration = settings.DurationSeconds;
double infoStart = Math.Max(0, duration - 7);
double ctaStart = Math.Max(0, duration - 3.5);
double hookEnd = Math.Min(2.7, duration);
string filterComplex = BuildFilterComplex(
GetBaseFilter(settings.Style),
fontEsc: EscapeFfmpegPath(settings.FontFile),
hookEsc: EscapeFfmpegPath(hookFile),
titleEsc: EscapeFfmpegPath(titleFile),
metaEsc: EscapeFfmpegPath(metaFile),
websiteEsc: EscapeFfmpegPath(websiteFile),
hookEnd, infoStart, ctaStart);
List<string> args = ["-hide_banner", "-y", "-ss", settings.Start, "-t", F(duration), "-i", settings.InputFile];
if (settings.Style == ShortStyle.Brand)
args.AddRange(["-loop", "1", "-i", settings.BackgroundImage]);
args.AddRange([
"-filter_complex", filterComplex,
"-map", "[vout]",
"-map", "0:a?",
"-c:v", "libx264",
"-preset", settings.Preset,
"-crf", settings.Crf.ToString(CultureInfo.InvariantCulture),
"-profile:v", "high",
"-level:v", "4.2",
"-pix_fmt", "yuv420p",
"-c:a", "aac",
"-b:a", "320k",
"-ar", "48000",
"-movflags", "+faststart",
"-shortest",
settings.OutputPath,
]);
progress.Report(new(2, "Rendering short", $"Applying the '{settings.Style}' style"));
TimeSpan totalSpan = TimeSpan.FromSeconds(duration);
await _runner.RunAsync(tools.Ffmpeg, args, log, null, token, outputHandler: output =>
{
if (output.Time is not { } time) return;
double percent = Math.Min(98, 2 + time.TotalSeconds / duration * 96);
progress.Report(new(percent, "Rendering short", $"{time:hh\\:mm\\:ss} / {totalSpan:hh\\:mm\\:ss}"));
});
progress.Report(new(100, "Complete", settings.OutputPath));
}
finally
{
try { Directory.Delete(workDir, true); } catch { }
}
}
/// <summary>Looks for a common bold system font to pre-fill the Shorts font field with.</summary>
public static string? FindDefaultFont()
{
string fontsDir = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
foreach (string name in new[] { "segoeuib.ttf", "seguisb.ttf", "arialbd.ttf", "calibrib.ttf" })
{
string candidate = Path.Combine(fontsDir, name);
if (File.Exists(candidate)) return candidate;
}
return null;
}
internal static void Validate(ShortSettings s)
{
if (string.IsNullOrWhiteSpace(s.InputFile) || !File.Exists(s.InputFile))
throw new FileNotFoundException("Select a valid source video for the Short.");
if (string.IsNullOrWhiteSpace(s.Title))
throw new ArgumentException("Enter a title.");
if (string.IsNullOrWhiteSpace(s.OutputPath))
throw new ArgumentException("Select an output file.");
if (s.DurationSeconds <= 0)
throw new ArgumentException("Duration must be greater than zero.");
if (string.IsNullOrWhiteSpace(s.FontFile) || !File.Exists(s.FontFile))
throw new FileNotFoundException("Select a bold TrueType/OpenType font file.");
if (s.Style == ShortStyle.Brand && (string.IsNullOrWhiteSpace(s.BackgroundImage) || !File.Exists(s.BackgroundImage)))
throw new FileNotFoundException("The brand style requires a background image.");
}
internal static string BuildMeta(ShortSettings s)
{
List<string> parts = [];
if (!string.IsNullOrWhiteSpace(s.Group)) parts.Add(s.Group.Trim());
if (!string.IsNullOrWhiteSpace(s.Year)) parts.Add(s.Year.Trim());
if (!string.IsNullOrWhiteSpace(s.Type)) parts.Add(s.Type.Trim());
return string.Join(" · ", parts);
}
internal static string EscapeFfmpegPath(string value) =>
value.Replace("\\", "\\\\").Replace(":", "\\:").Replace("'", "\\'");
private static string WriteTextFile(string directory, string fileName, string content)
{
string path = Path.Combine(directory, fileName);
File.WriteAllText(path, content + "\n", new UTF8Encoding(false));
return path;
}
internal static string BuildFilterComplex(
string baseFilter, string fontEsc, string hookEsc, string titleEsc, string metaEsc, string websiteEsc,
double hookEnd, double infoStart, double ctaStart)
{
string hookEndStr = F(hookEnd);
string infoStartStr = F(infoStart);
string ctaStartStr = F(ctaStart);
return baseFilter + ";" +
"[base]" +
$"drawbox=x=0:y=0:w=1080:h=245:color=black@0.50:t=fill:enable='between(t,0,{hookEndStr})'," +
$"drawtext=fontfile='{fontEsc}':textfile='{hookEsc}':fontcolor=white:fontsize=70:line_spacing=10:x=(w-text_w)/2:y=70:fix_bounds=true:shadowcolor=black@0.75:shadowx=4:shadowy=4:enable='between(t,0,{hookEndStr})'," +
$"drawbox=x=0:y=1490:w=1080:h=430:color=black@0.62:t=fill:enable='gte(t,{infoStartStr})'," +
$"drawtext=fontfile='{fontEsc}':textfile='{titleEsc}':fontcolor=white:fontsize=62:line_spacing=8:x=(w-text_w)/2:y=1560:fix_bounds=true:shadowcolor=black@0.80:shadowx=4:shadowy=4:enable='gte(t,{infoStartStr})'," +
$"drawtext=fontfile='{fontEsc}':textfile='{metaEsc}':fontcolor=white@0.92:fontsize=42:x=(w-text_w)/2:y=1655:fix_bounds=true:shadowcolor=black@0.80:shadowx=3:shadowy=3:enable='gte(t,{infoStartStr})'," +
$"drawtext=fontfile='{fontEsc}':textfile='{websiteEsc}':fontcolor=0x55c8ff:fontsize=52:x=(w-text_w)/2:y=1780:fix_bounds=true:shadowcolor=black@0.85:shadowx=4:shadowy=4:enable='gte(t,{ctaStartStr})'," +
"fps=50,format=yuv420p[vout]";
}
private static string F(double value) => value.ToString("0.###", CultureInfo.InvariantCulture);
internal static string GetBaseFilter(ShortStyle style) => style switch
{
ShortStyle.Brand => """
[1:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1[bg];
[0:v]scale=920:700:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x07111f,setsar=1,eq=contrast=1.02:saturation=1.02[fg];
[bg]drawbox=x=65:y=505:w=950:h=730:color=black@0.22:t=fill[bg2];
[bg2][fg]overlay=x=(W-w)/2:y=520:format=auto[base]
""",
ShortStyle.Pixel => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=270:480:force_original_aspect_ratio=increase,crop=270:480,scale=1080:1920:flags=neighbor,eq=brightness=-0.36:saturation=1.18,gblur=sigma=2,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=0:y='300+20*sin(t*1.70)':w=1080:h=8:color=0x55c8ff@0.72:t=fill,
drawbox=x=0:y='1610+18*sin(t*1.35)':w=1080:h=8:color=0xff7020@0.72:t=fill[base]
""",
ShortStyle.Mirror => """
[0:v]split=3[fg_src][top_src][bottom_src];
[top_src]scale=1080:620:force_original_aspect_ratio=increase,crop=1080:620,vflip,eq=brightness=-0.34:saturation=0.82,gblur=sigma=9,setsar=1[top];
[bottom_src]scale=1080:620:force_original_aspect_ratio=increase,crop=1080:620,vflip,eq=brightness=-0.34:saturation=0.82,gblur=sigma=9,setsar=1[bottom];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=white@0.70,setsar=1[fg];
[top]pad=1080:1920:0:0:color=0x050616[canvas];
[canvas][bottom]overlay=x=0:y=1300[tmp];
[tmp][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Crop => """
[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,eq=brightness=-0.04:saturation=1.05[base]
""",
ShortStyle.Workbench => """
[0:v]split=2[canvas_src][screen_src];
[canvas_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x090c20:t=fill,
drawbox=x=55:y=410:w=970:h=930:color=0xc4c8d0:t=fill,
drawbox=x=70:y=425:w=940:h=900:color=0x1b4f9c:t=fill,
drawbox=x=70:y=425:w=940:h=74:color=0xf28c28:t=fill,
drawbox=x=88:y=445:w=34:h=34:color=0x111111:t=fill[window];
[screen_src]scale=960:760:force_original_aspect_ratio=decrease,pad=iw+10:ih+10:5:5:color=0xffffff,setsar=1[screen];
[window][screen]overlay=x=(W-w)/2:y=525:format=auto[base]
""",
ShortStyle.Blur => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=35,eq=brightness=-0.22:saturation=0.85,setsar=1[bg];
[fg_src]scale=1080:900:force_original_aspect_ratio=decrease,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Crt => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=28,eq=brightness=-0.45:saturation=0.55,setsar=1[bg];
[fg_src]scale=980:800:force_original_aspect_ratio=decrease,lenscorrection=k1=-0.060:k2=0.020,vignette=angle=PI/5,eq=contrast=1.08:saturation=1.10,setsar=1,
drawgrid=width=iw:height=4:thickness=1:color=black@0.18[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=50:y='220+mod(t*340,1480)':w=980:h=4:color=white@0.08:t=fill[base]
""",
ShortStyle.Copper => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,eq=brightness=-0.50:saturation=0.80,gblur=sigma=18,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x061a5f@0.55:t=fill,
drawbox=x=0:y='220+20*sin(t*1.00)':w=1080:h=140:color=0x123fcb@0.28:t=fill,
drawbox=x=0:y='420+24*sin(t*1.20)':w=1080:h=100:color=0x2b8cff@0.22:t=fill,
drawbox=x=0:y='660+16*sin(t*1.50)':w=1080:h=120:color=0x00b8ff@0.18:t=fill,
drawbox=x=0:y='1110+22*sin(t*1.10)':w=1080:h=150:color=0xff7a1f@0.20:t=fill,
drawbox=x=0:y='1360+18*sin(t*1.30)':w=1080:h=110:color=0xffc120@0.18:t=fill,
drawbox=x=0:y='1660+15*sin(t*1.70)':w=1080:h=130:color=0x58d4ff@0.18:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Stars => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.72:saturation=0.28,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x05081b@0.75:t=fill,
drawbox=x='mod(100+80*t,1080)':y='mod(80+210*t,1920)':w=3:h=3:color=white@0.95:t=fill,
drawbox=x='mod(880-55*t,1080)':y='mod(220+170*t,1920)':w=2:h=2:color=white@0.85:t=fill,
drawbox=x='mod(300+120*t,1080)':y='mod(520+140*t,1920)':w=3:h=3:color=0x9ad8ff@0.90:t=fill,
drawbox=x='mod(980-75*t,1080)':y='mod(710+160*t,1920)':w=2:h=2:color=white@0.80:t=fill,
drawbox=x='mod(420+65*t,1080)':y='mod(1040+200*t,1920)':w=4:h=4:color=0xffd46b@0.85:t=fill,
drawbox=x='mod(750-90*t,1080)':y='mod(1280+110*t,1920)':w=2:h=2:color=white@0.88:t=fill,
drawbox=x='mod(210+75*t,1080)':y='mod(1440+180*t,1920)':w=3:h=3:color=0x9ad8ff@0.90:t=fill,
drawbox=x='mod(1030-60*t,1080)':y='mod(1680+140*t,1920)':w=2:h=2:color=white@0.82:t=fill,
drawbox=x='mod(560+95*t,1080)':y='mod(260+260*t,1920)':w=2:h=2:color=white@0.92:t=fill,
drawbox=x='mod(60+110*t,1080)':y='mod(860+120*t,1920)':w=3:h=3:color=0xffd46b@0.82:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Grid => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=24,eq=brightness=-0.62:saturation=0.36,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x090b17@0.72:t=fill,
drawgrid=width=90:height=90:thickness=2:color=0x39d2ff@0.18,
drawbox=x=0:y=0:w=1080:h=1120:color=0x050814@0.80:t=fill,
drawbox=x=0:y=1120:w=1080:h=4:color=0xff54d9@0.70:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xff54d9,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Tiles => """
[0:v]split=5[fg_src][a][b][c][d];
[a]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[ta];
[b]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,hflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[tb];
[c]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,vflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[tc];
[d]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,hflip,vflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[td];
[ta][tb]hstack=inputs=2[top];
[tc][td]hstack=inputs=2[bottom];
[top][bottom]vstack=inputs=2[bg];
[fg_src]scale=980:800:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Scan => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,scale=360:640:flags=neighbor,scale=1080:1920:flags=neighbor,eq=brightness=-0.44:saturation=1.05,gblur=sigma=2,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=40:y='220+mod(t*380,1480)':w=1000:h=8:color=white@0.14:t=fill,
drawbox=x=0:y='280+20*sin(t*1.40)':w=1080:h=6:color=0x55c8ff@0.45:t=fill,
drawbox=x=0:y='1640+15*sin(t*1.20)':w=1080:h=6:color=0xff7020@0.45:t=fill[base]
""",
ShortStyle.Starfield => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=32,eq=brightness=-0.78:saturation=0.22,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x020511@0.82:t=fill,
drawbox=x='mod(120+75*t,1080)':y='mod(80+520*t,1920)':w=3:h=18:color=white@0.92:t=fill,
drawbox=x='mod(970-55*t,1080)':y='mod(250+430*t,1920)':w=2:h=14:color=0xa4dcff@0.85:t=fill,
drawbox=x='mod(370+95*t,1080)':y='mod(430+600*t,1920)':w=4:h=24:color=white@0.90:t=fill,
drawbox=x='mod(810-80*t,1080)':y='mod(640+470*t,1920)':w=2:h=17:color=0xffdc83@0.85:t=fill,
drawbox=x='mod(230+65*t,1080)':y='mod(900+540*t,1920)':w=3:h=20:color=white@0.88:t=fill,
drawbox=x='mod(1020-100*t,1080)':y='mod(1110+650*t,1920)':w=4:h=26:color=0x8fd5ff@0.90:t=fill,
drawbox=x='mod(560+85*t,1080)':y='mod(1320+490*t,1920)':w=2:h=16:color=white@0.86:t=fill,
drawbox=x='mod(70+110*t,1080)':y='mod(1510+590*t,1920)':w=3:h=22:color=0xffdc83@0.82:t=fill,
drawbox=x='mod(720-60*t,1080)':y='mod(1720+450*t,1920)':w=2:h=15:color=white@0.88:t=fill,
drawbox=x='mod(440+105*t,1080)':y='mod(180+700*t,1920)':w=3:h=28:color=0xa4dcff@0.88:t=fill,
drawbox=x='mod(900-72*t,1080)':y='mod(780+560*t,1920)':w=2:h=18:color=white@0.84:t=fill,
drawbox=x='mod(310+90*t,1080)':y='mod(1240+620*t,1920)':w=4:h=24:color=white@0.90:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0x70cfff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Plasma => """
[0:v]split=2[dim_src][fg_src];
nullsrc=size=1080x1920:rate=50,format=rgb24,
geq=r='128+105*sin(X/68+T*1.9)+22*sin(Y/91-T*1.1)':g='128+96*sin(Y/75+T*1.5)+26*sin((X+Y)/120+T*1.3)':b='128+110*sin((X-Y)/82-T*1.7)',
gblur=sigma=18,eq=brightness=-0.16:saturation=1.22[plasma];
[dim_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=26,eq=brightness=-0.70:saturation=0.35,setsar=1[dim];
[plasma][dim]blend=all_mode=overlay:all_opacity=0.35[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Rasterbars => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.70:saturation=0.35,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x05061b@0.80:t=fill,
drawbox=x=0:y='250+90*sin(t*1.25)':w=1080:h=30:color=0xff3b7f@0.78:t=fill,
drawbox=x=0:y='300+90*sin(t*1.25)':w=1080:h=24:color=0xff7b32@0.72:t=fill,
drawbox=x=0:y='345+90*sin(t*1.25)':w=1080:h=18:color=0xffd12b@0.66:t=fill,
drawbox=x=0:y='1510+85*sin(t*1.10+1.2)':w=1080:h=18:color=0x55e6ff@0.68:t=fill,
drawbox=x=0:y='1550+85*sin(t*1.10+1.2)':w=1080:h=24:color=0x4b8cff@0.72:t=fill,
drawbox=x=0:y='1600+85*sin(t*1.10+1.2)':w=1080:h=30:color=0x8b55ff@0.76:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Vhs => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=26,eq=brightness=-0.44:saturation=0.55,noise=alls=10:allf=t+u,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,chromashift=cbh=2:crh=-2,noise=alls=5:allf=t+u,eq=contrast=1.08:saturation=0.92,vignette=angle=PI/4.5,setsar=1,
drawgrid=width=iw:height=5:thickness=1:color=black@0.13[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=0:y='300+mod(t*510,1320)':w=1080:h=10:color=white@0.08:t=fill,
drawbox=x=0:y='420+mod(t*320,1080)':w=1080:h=3:color=0x7bd7ff@0.10:t=fill[base]
""",
ShortStyle.Monitor => """
[0:v]split=2[bg_src][screen_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=34,eq=brightness=-0.68:saturation=0.32,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x11131a@0.78:t=fill,
drawbox=x=42:y=365:w=996:h=1000:color=0xaaa69a:t=fill,
drawbox=x=68:y=392:w=944:h=944:color=0x2a2926:t=fill,
drawbox=x=105:y=438:w=870:h=790:color=0x050505:t=fill,
drawbox=x=792:y=1260:w=44:h=18:color=0x6aff73:t=fill,
drawbox=x=852:y=1253:w=30:h=30:color=0x232323:t=fill,
drawbox=x=910:y=1253:w=30:h=30:color=0x232323:t=fill,
drawbox=x=380:y=1365:w=320:h=75:color=0x8f8b80:t=fill,
drawbox=x=290:y=1438:w=500:h=36:color=0x77736b:t=fill[monitor];
[screen_src]scale=840:750:force_original_aspect_ratio=decrease,lenscorrection=k1=-0.045:k2=0.012,vignette=angle=PI/5,eq=contrast=1.06:saturation=1.06,setsar=1[screen];
[monitor][screen]overlay=x=(W-w)/2:y=458:format=auto[base]
""",
ShortStyle.Split => """
[0:v]split=4[bg_src][main_src][top_src][bottom_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=34,eq=brightness=-0.55:saturation=0.55,setsar=1[bg];
[top_src]scale=1080:520:force_original_aspect_ratio=increase,crop=1080:360:y='(ih-360)/2',eq=brightness=-0.28:saturation=0.88,gblur=sigma=2,setsar=1[top];
[bottom_src]scale=1080:520:force_original_aspect_ratio=increase,crop=1080:360:y='(ih-360)/2',hflip,eq=brightness=-0.32:saturation=0.80,gblur=sigma=2,setsar=1[bottom];
[main_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[main];
[bg][top]overlay=x=0:y=275[tmp1];
[tmp1][bottom]overlay=x=0:y=1285[tmp2];
[tmp2][main]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
""",
ShortStyle.Spectrum => """
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.62:saturation=0.40,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x040717@0.68:t=fill[bg];
[fg_src]scale=1000:780:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=0x55c8ff,setsar=1[fg];
[0:a]aformat=sample_fmts=fltp:channel_layouts=stereo,showspectrum=s=1000x250:mode=combined:color=rainbow:scale=sqrt:fscale=log:slide=scroll:win_func=hann:fps=50:opacity=0.85,format=rgba[spec];
[bg][spec]overlay=x=(W-w)/2:y=1360:format=auto[tmp];
[tmp][fg]overlay=x=(W-w)/2:y=480:format=auto[base]
""",
_ => throw new ArgumentOutOfRangeException(nameof(style), style, "Unknown short style."),
};
}
+2 -2
View File
@@ -2,7 +2,7 @@ using System.IO;
using System.Reflection; using System.Reflection;
using System.Security.Cryptography; using System.Security.Cryptography;
namespace AmigaDB.VideoRenderer.Services; namespace AmiReel.Services;
public sealed record ToolPaths(string Ffmpeg, string Ffprobe); public sealed record ToolPaths(string Ffmpeg, string Ffprobe);
@@ -52,7 +52,7 @@ public static class ToolExtractor
private static async Task<string> ExtractOneAsync(string fileName, string destination, CancellationToken token) private static async Task<string> ExtractOneAsync(string fileName, string destination, CancellationToken token)
{ {
Assembly assembly = Assembly.GetExecutingAssembly(); Assembly assembly = Assembly.GetExecutingAssembly();
string resourceName = $"AmigaDB.VideoRenderer.Tools.{fileName}"; string resourceName = $"AmiReel.Tools.{fileName}";
await using Stream source = assembly.GetManifestResourceStream(resourceName) await using Stream source = assembly.GetManifestResourceStream(resourceName)
?? throw new InvalidOperationException( ?? throw new InvalidOperationException(
$"Embedded {fileName} was not found. Add it to ThirdParty and publish the application again."); $"Embedded {fileName} was not found. Add it to ThirdParty and publish the application again.");
+4 -26
View File
@@ -1,4 +1,4 @@
namespace AmigaDB.VideoRenderer.Services; namespace AmiReel.Services;
public static class UserFacingErrors public static class UserFacingErrors
{ {
@@ -39,38 +39,16 @@ public static class UserFacingErrors
return string.Join(Environment.NewLine, summary); return string.Join(Environment.NewLine, summary);
} }
private static bool IsNoise(string line) internal static bool IsNoise(string line)
{ {
return line.StartsWith("ffmpeg version ", StringComparison.OrdinalIgnoreCase) return FfmpegOutputFilter.IsBoilerplateLine(line)
|| line.StartsWith("built with ", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("configuration:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavutil", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavcodec", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavformat", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavdevice", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libavfilter", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libswscale", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("libswresample", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Input #", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Output #", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Stream mapping:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Stream #", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Metadata:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("major_brand", StringComparison.OrdinalIgnoreCase) || line.StartsWith("major_brand", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("minor_version", StringComparison.OrdinalIgnoreCase) || line.StartsWith("minor_version", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("compatible_brands", StringComparison.OrdinalIgnoreCase) || line.StartsWith("compatible_brands", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("encoder", StringComparison.OrdinalIgnoreCase) || line.StartsWith("encoder", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Duration:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Press [q] to stop", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("[in#", StringComparison.OrdinalIgnoreCase) || line.StartsWith("[in#", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("[out#", StringComparison.OrdinalIgnoreCase) || line.StartsWith("[out#", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Last message repeated", StringComparison.OrdinalIgnoreCase) || line.StartsWith("Last message repeated", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("frame=", StringComparison.OrdinalIgnoreCase) || line.StartsWith("frame=", StringComparison.OrdinalIgnoreCase);
|| line.StartsWith("video:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("audio:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("subtitle:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("other streams:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("global headers:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("muxing overhead:", StringComparison.OrdinalIgnoreCase);
} }
} }
+674
View File
@@ -0,0 +1,674 @@
#!/usr/bin/env bash
set -Eeuo pipefail
# AmigaDB YouTube Shorts generator
# Creates a 1080x1920, 50 FPS vertical MP4 from an existing Amiga video.
# V3 expanded style pack for 4:3 Amiga footage, with demoscene-inspired FX and optional branded background image.
SCRIPT_NAME="$(basename "$0")"
INPUT=""
OUTPUT=""
START="00:00:00"
DURATION="30"
TITLE=""
GROUP=""
YEAR=""
TYPE=""
HOOK="THIS RAN ON AN AMIGA"
WEBSITE="AMIGADB.NET"
FONT_FILE=""
STYLE="brand"
BACKGROUND_IMAGE="amigadb_short.png"
CRF="16"
PRESET="slow"
usage() {
cat <<USAGE
Usage:
$SCRIPT_NAME --input VIDEO --title TITLE [options]
Required:
--input FILE Source video
--title TEXT Production title
Production metadata:
--group TEXT Group / creator
--year YEAR Release year
--type TEXT Demo, Intro, Cracktro, Music Disk, ...
--hook TEXT Opening hook (default: THIS RAN ON AN AMIGA)
--website TEXT Closing website text (default: AMIGADB.NET)
Clip settings:
--start TIME Start position, e.g. 00:01:23.500 (default: 00:00:00)
--duration SECONDS Output duration in seconds (default: 30)
--output FILE Output MP4 path
Visual style:
--style NAME brand, pixel, mirror, crop, workbench, blur,
crt, copper, stars, grid, tiles, scan,
starfield, plasma, rasterbars, vhs,
monitor, split, spectrum
default: brand
--background-image Background image for brand style
default: amigadb_short.png
Encoding:
--font FILE Bold TrueType/OpenType font file
--crf NUMBER H.264 quality; lower is better (default: 16)
--preset NAME x264 preset (default: slow)
Other:
-h, --help Show this help
Examples:
$SCRIPT_NAME \
--input "ami-back.mp4" \
--start "00:00:34" \
--duration 30 \
--title "Ami-Back V1.04A" \
--group "Pirates" \
--year "1991" \
--type "Cracktro" \
--hook "A 1991 AMIGA CRACKTRO" \
--style brand \
--background-image "amigadb_short.png" \
--output "ami-back-brand-short.mp4"
$SCRIPT_NAME \
--input "demo.mp4" \
--title "Demo Title" \
--group "Demo Group" \
--year "1992" \
--type "Demo" \
--style crt
USAGE
}
fail() {
printf 'Error: %s\n' "$*" >&2
exit 1
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
find_default_font() {
local candidate=""
if command_exists fc-match; then
candidate="$(fc-match -f '%{file}\n' 'DejaVu Sans:style=Bold' 2>/dev/null | head -n 1 || true)"
if [[ -n "$candidate" && -f "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
fi
for candidate in \
/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf \
/usr/share/fonts/truetype/liberation2/LiberationSans-Bold.ttf \
/usr/share/fonts/opentype/noto/NotoSans-Bold.ttf; do
if [[ -f "$candidate" ]]; then
printf '%s\n' "$candidate"
return 0
fi
done
return 1
}
while (($#)); do
case "$1" in
--input)
(($# >= 2)) || fail "Missing value after --input"
INPUT="$2"
shift 2
;;
--output)
(($# >= 2)) || fail "Missing value after --output"
OUTPUT="$2"
shift 2
;;
--start)
(($# >= 2)) || fail "Missing value after --start"
START="$2"
shift 2
;;
--duration)
(($# >= 2)) || fail "Missing value after --duration"
DURATION="$2"
shift 2
;;
--title)
(($# >= 2)) || fail "Missing value after --title"
TITLE="$2"
shift 2
;;
--group)
(($# >= 2)) || fail "Missing value after --group"
GROUP="$2"
shift 2
;;
--year)
(($# >= 2)) || fail "Missing value after --year"
YEAR="$2"
shift 2
;;
--type)
(($# >= 2)) || fail "Missing value after --type"
TYPE="$2"
shift 2
;;
--hook)
(($# >= 2)) || fail "Missing value after --hook"
HOOK="$2"
shift 2
;;
--website)
(($# >= 2)) || fail "Missing value after --website"
WEBSITE="$2"
shift 2
;;
--font)
(($# >= 2)) || fail "Missing value after --font"
FONT_FILE="$2"
shift 2
;;
--background-image|--background)
(($# >= 2)) || fail "Missing value after --background-image"
BACKGROUND_IMAGE="$2"
shift 2
;;
--style)
(($# >= 2)) || fail "Missing value after --style"
STYLE="${2,,}"
shift 2
;;
--crf)
(($# >= 2)) || fail "Missing value after --crf"
CRF="$2"
shift 2
;;
--preset)
(($# >= 2)) || fail "Missing value after --preset"
PRESET="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
fail "Unknown argument: $1"
;;
esac
done
command_exists ffmpeg || fail "ffmpeg is not installed or not in PATH"
[[ -n "$INPUT" ]] || fail "--input is required"
[[ -f "$INPUT" ]] || fail "Input file not found: $INPUT"
[[ -n "$TITLE" ]] || fail "--title is required"
[[ "$DURATION" =~ ^[0-9]+([.][0-9]+)?$ ]] || fail "--duration must be a positive number"
awk -v d="$DURATION" 'BEGIN { exit !(d > 0) }' || fail "--duration must be greater than zero"
[[ "$CRF" =~ ^[0-9]+$ ]] || fail "--crf must be an integer"
case "$STYLE" in
brand|pixel|mirror|crop|workbench|blur|crt|copper|stars|grid|tiles|scan|starfield|plasma|rasterbars|vhs|monitor|split|spectrum)
;;
*)
fail "Unknown style '$STYLE'. Use brand, pixel, mirror, crop, workbench, blur, crt, copper, stars, grid, tiles, scan, starfield, plasma, rasterbars, vhs, monitor, split, or spectrum"
;;
esac
if [[ -z "$FONT_FILE" ]]; then
FONT_FILE="$(find_default_font || true)"
fi
[[ -n "$FONT_FILE" && -f "$FONT_FILE" ]] || fail "No usable font found. Install fonts-dejavu-core or use --font FILE"
if [[ -z "$OUTPUT" ]]; then
base="$(basename "$INPUT")"
base="${base%.*}"
OUTPUT="${base}-${STYLE}-short.mp4"
fi
mkdir -p "$(dirname "$OUTPUT")"
TMP_DIR="$(mktemp -d -t amigadb-short.XXXXXX)"
cleanup() {
rm -rf "$TMP_DIR"
}
trap cleanup EXIT
printf '%s\n' "$HOOK" > "$TMP_DIR/hook.txt"
printf '%s\n' "$TITLE" > "$TMP_DIR/title.txt"
META_PARTS=()
[[ -n "$GROUP" ]] && META_PARTS+=("$GROUP")
[[ -n "$YEAR" ]] && META_PARTS+=("$YEAR")
[[ -n "$TYPE" ]] && META_PARTS+=("$TYPE")
META=""
if ((${#META_PARTS[@]})); then
for part in "${META_PARTS[@]}"; do
if [[ -n "$META" ]]; then
META+=" · "
fi
META+="$part"
done
fi
printf '%s\n' "$META" > "$TMP_DIR/meta.txt"
printf '%s\n' "$WEBSITE" > "$TMP_DIR/website.txt"
INFO_START="$(awk -v d="$DURATION" 'BEGIN { v=d-7; if (v<0) v=0; printf "%.3f", v }')"
CTA_START="$(awk -v d="$DURATION" 'BEGIN { v=d-3.5; if (v<0) v=0; printf "%.3f", v }')"
HOOK_END="$(awk -v d="$DURATION" 'BEGIN { v=2.7; if (d<v) v=d; printf "%.3f", v }')"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
resolve_path() {
local p="$1"
if [[ -f "$p" ]]; then
printf "%s\n" "$p"
return 0
fi
if [[ -f "$SCRIPT_DIR/$p" ]]; then
printf "%s\n" "$SCRIPT_DIR/$p"
return 0
fi
return 1
}
if [[ "$STYLE" == "brand" ]]; then
BACKGROUND_IMAGE="$(resolve_path "$BACKGROUND_IMAGE" || true)"
[[ -n "$BACKGROUND_IMAGE" && -f "$BACKGROUND_IMAGE" ]] || fail "Brand style requires background image. Put amigadb_short.png next to the script or pass --background-image FILE"
fi
# Escape paths for FFmpeg filter option parsing.
ffmpeg_escape_path() {
local value="$1"
value="${value//\\/\\\\}"
value="${value//:/\\:}"
value="${value//\'/\\\'}"
printf '%s' "$value"
}
FONT_ESC="$(ffmpeg_escape_path "$FONT_FILE")"
HOOK_ESC="$(ffmpeg_escape_path "$TMP_DIR/hook.txt")"
TITLE_ESC="$(ffmpeg_escape_path "$TMP_DIR/title.txt")"
META_ESC="$(ffmpeg_escape_path "$TMP_DIR/meta.txt")"
WEBSITE_ESC="$(ffmpeg_escape_path "$TMP_DIR/website.txt")"
BACKGROUND_ESC=""
if [[ "$STYLE" == "brand" ]]; then
BACKGROUND_ESC="$(ffmpeg_escape_path "$BACKGROUND_IMAGE")"
fi
HOOK_BOX_Y=0
HOOK_BOX_H=245
HOOK_TEXT_Y=70
HOOK_FONT_SIZE=70
INFO_BOX_Y=1490
INFO_BOX_H=430
TITLE_TEXT_Y=1560
TITLE_FONT_SIZE=62
META_TEXT_Y=1655
META_FONT_SIZE=42
WEBSITE_TEXT_Y=1780
WEBSITE_FONT_SIZE=52
if [[ "$STYLE" == "brand" ]]; then
HOOK_BOX_Y=250
HOOK_BOX_H=140
HOOK_TEXT_Y=285
HOOK_FONT_SIZE=60
INFO_BOX_Y=1310
INFO_BOX_H=270
TITLE_TEXT_Y=1360
TITLE_FONT_SIZE=56
META_TEXT_Y=1435
META_FONT_SIZE=38
WEBSITE_TEXT_Y=1510
WEBSITE_FONT_SIZE=48
fi
HAS_AUDIO=0
if command_exists ffprobe && ffprobe -v error -select_streams a:0 -show_entries stream=index -of csv=p=0 "$INPUT" | grep -q .; then
HAS_AUDIO=1
fi
if [[ "$STYLE" == "spectrum" && "$HAS_AUDIO" -ne 1 ]]; then
fail "The spectrum style requires an audio stream in the source video"
fi
case "$STYLE" in
brand)
BASE_FILTER=$(cat <<'FILTER'
[1:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1[bg];
[0:v]scale=920:700:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x07111f,setsar=1,eq=contrast=1.02:saturation=1.02[fg];
[bg]drawbox=x=65:y=505:w=950:h=730:color=black@0.22:t=fill[bg2];
[bg2][fg]overlay=x=(W-w)/2:y=520:format=auto[base]
FILTER
)
;;
pixel)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=270:480:force_original_aspect_ratio=increase,crop=270:480,scale=1080:1920:flags=neighbor,eq=brightness=-0.36:saturation=1.18,gblur=sigma=2,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=0:y='300+20*sin(t*1.70)':w=1080:h=8:color=0x55c8ff@0.72:t=fill,
drawbox=x=0:y='1610+18*sin(t*1.35)':w=1080:h=8:color=0xff7020@0.72:t=fill[base]
FILTER
)
;;
mirror)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=3[fg_src][top_src][bottom_src];
[top_src]scale=1080:620:force_original_aspect_ratio=increase,crop=1080:620,vflip,eq=brightness=-0.34:saturation=0.82,gblur=sigma=9,setsar=1[top];
[bottom_src]scale=1080:620:force_original_aspect_ratio=increase,crop=1080:620,vflip,eq=brightness=-0.34:saturation=0.82,gblur=sigma=9,setsar=1[bottom];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=white@0.70,setsar=1[fg];
[top]pad=1080:1920:0:0:color=0x050616[canvas];
[canvas][bottom]overlay=x=0:y=1300[tmp];
[tmp][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
crop)
BASE_FILTER=$(cat <<'FILTER'
[0:v]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,eq=brightness=-0.04:saturation=1.05[base]
FILTER
)
;;
workbench)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[canvas_src][screen_src];
[canvas_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x090c20:t=fill,
drawbox=x=55:y=410:w=970:h=930:color=0xc4c8d0:t=fill,
drawbox=x=70:y=425:w=940:h=900:color=0x1b4f9c:t=fill,
drawbox=x=70:y=425:w=940:h=74:color=0xf28c28:t=fill,
drawbox=x=88:y=445:w=34:h=34:color=0x111111:t=fill[window];
[screen_src]scale=960:760:force_original_aspect_ratio=decrease,pad=iw+10:ih+10:5:5:color=0xffffff,setsar=1[screen];
[window][screen]overlay=x=(W-w)/2:y=525:format=auto[base]
FILTER
)
;;
blur)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=35,eq=brightness=-0.22:saturation=0.85,setsar=1[bg];
[fg_src]scale=1080:900:force_original_aspect_ratio=decrease,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
crt)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=28,eq=brightness=-0.45:saturation=0.55,setsar=1[bg];
[fg_src]scale=980:800:force_original_aspect_ratio=decrease,lenscorrection=k1=-0.060:k2=0.020,vignette=angle=PI/5,eq=contrast=1.08:saturation=1.10,setsar=1,
drawgrid=width=iw:height=4:thickness=1:color=black@0.18[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=50:y='220+mod(t*340,1480)':w=980:h=4:color=white@0.08:t=fill[base]
FILTER
)
;;
copper)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,eq=brightness=-0.50:saturation=0.80,gblur=sigma=18,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x061a5f@0.55:t=fill,
drawbox=x=0:y='220+20*sin(t*1.00)':w=1080:h=140:color=0x123fcb@0.28:t=fill,
drawbox=x=0:y='420+24*sin(t*1.20)':w=1080:h=100:color=0x2b8cff@0.22:t=fill,
drawbox=x=0:y='660+16*sin(t*1.50)':w=1080:h=120:color=0x00b8ff@0.18:t=fill,
drawbox=x=0:y='1110+22*sin(t*1.10)':w=1080:h=150:color=0xff7a1f@0.20:t=fill,
drawbox=x=0:y='1360+18*sin(t*1.30)':w=1080:h=110:color=0xffc120@0.18:t=fill,
drawbox=x=0:y='1660+15*sin(t*1.70)':w=1080:h=130:color=0x58d4ff@0.18:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
stars)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.72:saturation=0.28,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x05081b@0.75:t=fill,
drawbox=x='mod(100+80*t,1080)':y='mod(80+210*t,1920)':w=3:h=3:color=white@0.95:t=fill,
drawbox=x='mod(880-55*t,1080)':y='mod(220+170*t,1920)':w=2:h=2:color=white@0.85:t=fill,
drawbox=x='mod(300+120*t,1080)':y='mod(520+140*t,1920)':w=3:h=3:color=0x9ad8ff@0.90:t=fill,
drawbox=x='mod(980-75*t,1080)':y='mod(710+160*t,1920)':w=2:h=2:color=white@0.80:t=fill,
drawbox=x='mod(420+65*t,1080)':y='mod(1040+200*t,1920)':w=4:h=4:color=0xffd46b@0.85:t=fill,
drawbox=x='mod(750-90*t,1080)':y='mod(1280+110*t,1920)':w=2:h=2:color=white@0.88:t=fill,
drawbox=x='mod(210+75*t,1080)':y='mod(1440+180*t,1920)':w=3:h=3:color=0x9ad8ff@0.90:t=fill,
drawbox=x='mod(1030-60*t,1080)':y='mod(1680+140*t,1920)':w=2:h=2:color=white@0.82:t=fill,
drawbox=x='mod(560+95*t,1080)':y='mod(260+260*t,1920)':w=2:h=2:color=white@0.92:t=fill,
drawbox=x='mod(60+110*t,1080)':y='mod(860+120*t,1920)':w=3:h=3:color=0xffd46b@0.82:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
grid)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=24,eq=brightness=-0.62:saturation=0.36,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x090b17@0.72:t=fill,
drawgrid=width=90:height=90:thickness=2:color=0x39d2ff@0.18,
drawbox=x=0:y=0:w=1080:h=1120:color=0x050814@0.80:t=fill,
drawbox=x=0:y=1120:w=1080:h=4:color=0xff54d9@0.70:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xff54d9,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
tiles)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=5[fg_src][a][b][c][d];
[a]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[ta];
[b]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,hflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[tb];
[c]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,vflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[tc];
[d]scale=540:960:force_original_aspect_ratio=increase,crop=540:960,hflip,vflip,eq=brightness=-0.45:saturation=0.68,gblur=sigma=4,setsar=1[td];
[ta][tb]hstack=inputs=2[top];
[tc][td]hstack=inputs=2[bottom];
[top][bottom]vstack=inputs=2[bg];
[fg_src]scale=980:800:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
scan)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,scale=360:640:flags=neighbor,scale=1080:1920:flags=neighbor,eq=brightness=-0.44:saturation=1.05,gblur=sigma=2,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0x55c8ff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=40:y='220+mod(t*380,1480)':w=1000:h=8:color=white@0.14:t=fill,
drawbox=x=0:y='280+20*sin(t*1.40)':w=1080:h=6:color=0x55c8ff@0.45:t=fill,
drawbox=x=0:y='1640+15*sin(t*1.20)':w=1080:h=6:color=0xff7020@0.45:t=fill[base]
FILTER
)
;;
starfield)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=32,eq=brightness=-0.78:saturation=0.22,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x020511@0.82:t=fill,
drawbox=x='mod(120+75*t,1080)':y='mod(80+520*t,1920)':w=3:h=18:color=white@0.92:t=fill,
drawbox=x='mod(970-55*t,1080)':y='mod(250+430*t,1920)':w=2:h=14:color=0xa4dcff@0.85:t=fill,
drawbox=x='mod(370+95*t,1080)':y='mod(430+600*t,1920)':w=4:h=24:color=white@0.90:t=fill,
drawbox=x='mod(810-80*t,1080)':y='mod(640+470*t,1920)':w=2:h=17:color=0xffdc83@0.85:t=fill,
drawbox=x='mod(230+65*t,1080)':y='mod(900+540*t,1920)':w=3:h=20:color=white@0.88:t=fill,
drawbox=x='mod(1020-100*t,1080)':y='mod(1110+650*t,1920)':w=4:h=26:color=0x8fd5ff@0.90:t=fill,
drawbox=x='mod(560+85*t,1080)':y='mod(1320+490*t,1920)':w=2:h=16:color=white@0.86:t=fill,
drawbox=x='mod(70+110*t,1080)':y='mod(1510+590*t,1920)':w=3:h=22:color=0xffdc83@0.82:t=fill,
drawbox=x='mod(720-60*t,1080)':y='mod(1720+450*t,1920)':w=2:h=15:color=white@0.88:t=fill,
drawbox=x='mod(440+105*t,1080)':y='mod(180+700*t,1920)':w=3:h=28:color=0xa4dcff@0.88:t=fill,
drawbox=x='mod(900-72*t,1080)':y='mod(780+560*t,1920)':w=2:h=18:color=white@0.84:t=fill,
drawbox=x='mod(310+90*t,1080)':y='mod(1240+620*t,1920)':w=4:h=24:color=white@0.90:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0x70cfff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
plasma)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[dim_src][fg_src];
nullsrc=size=1080x1920:rate=50,format=rgb24,
geq=r='128+105*sin(X/68+T*1.9)+22*sin(Y/91-T*1.1)':g='128+96*sin(Y/75+T*1.5)+26*sin((X+Y)/120+T*1.3)':b='128+110*sin((X-Y)/82-T*1.7)',
gblur=sigma=18,eq=brightness=-0.16:saturation=1.22[plasma];
[dim_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=26,eq=brightness=-0.70:saturation=0.35,setsar=1[dim];
[plasma][dim]blend=all_mode=overlay:all_opacity=0.35[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+16:ih+16:8:8:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
rasterbars)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.70:saturation=0.35,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x05061b@0.80:t=fill,
drawbox=x=0:y='250+90*sin(t*1.25)':w=1080:h=30:color=0xff3b7f@0.78:t=fill,
drawbox=x=0:y='300+90*sin(t*1.25)':w=1080:h=24:color=0xff7b32@0.72:t=fill,
drawbox=x=0:y='345+90*sin(t*1.25)':w=1080:h=18:color=0xffd12b@0.66:t=fill,
drawbox=x=0:y='1510+85*sin(t*1.10+1.2)':w=1080:h=18:color=0x55e6ff@0.68:t=fill,
drawbox=x=0:y='1550+85*sin(t*1.10+1.2)':w=1080:h=24:color=0x4b8cff@0.72:t=fill,
drawbox=x=0:y='1600+85*sin(t*1.10+1.2)':w=1080:h=30:color=0x8b55ff@0.76:t=fill[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
vhs)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=26,eq=brightness=-0.44:saturation=0.55,noise=alls=10:allf=t+u,setsar=1[bg];
[fg_src]scale=1000:820:force_original_aspect_ratio=decrease,chromashift=cbh=2:crh=-2,noise=alls=5:allf=t+u,eq=contrast=1.08:saturation=0.92,vignette=angle=PI/4.5,setsar=1,
drawgrid=width=iw:height=5:thickness=1:color=black@0.13[fg];
[bg][fg]overlay=x=(W-w)/2:y=(H-h)/2:format=auto,
drawbox=x=0:y='300+mod(t*510,1320)':w=1080:h=10:color=white@0.08:t=fill,
drawbox=x=0:y='420+mod(t*320,1080)':w=1080:h=3:color=0x7bd7ff@0.10:t=fill[base]
FILTER
)
;;
monitor)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][screen_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=34,eq=brightness=-0.68:saturation=0.32,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x11131a@0.78:t=fill,
drawbox=x=42:y=365:w=996:h=1000:color=0xaaa69a:t=fill,
drawbox=x=68:y=392:w=944:h=944:color=0x2a2926:t=fill,
drawbox=x=105:y=438:w=870:h=790:color=0x050505:t=fill,
drawbox=x=792:y=1260:w=44:h=18:color=0x6aff73:t=fill,
drawbox=x=852:y=1253:w=30:h=30:color=0x232323:t=fill,
drawbox=x=910:y=1253:w=30:h=30:color=0x232323:t=fill,
drawbox=x=380:y=1365:w=320:h=75:color=0x8f8b80:t=fill,
drawbox=x=290:y=1438:w=500:h=36:color=0x77736b:t=fill[monitor];
[screen_src]scale=840:750:force_original_aspect_ratio=decrease,lenscorrection=k1=-0.045:k2=0.012,vignette=angle=PI/5,eq=contrast=1.06:saturation=1.06,setsar=1[screen];
[monitor][screen]overlay=x=(W-w)/2:y=458:format=auto[base]
FILTER
)
;;
split)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=4[bg_src][main_src][top_src][bottom_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=34,eq=brightness=-0.55:saturation=0.55,setsar=1[bg];
[top_src]scale=1080:520:force_original_aspect_ratio=increase,crop=1080:360:y='(ih-360)/2',eq=brightness=-0.28:saturation=0.88,gblur=sigma=2,setsar=1[top];
[bottom_src]scale=1080:520:force_original_aspect_ratio=increase,crop=1080:360:y='(ih-360)/2',hflip,eq=brightness=-0.32:saturation=0.80,gblur=sigma=2,setsar=1[bottom];
[main_src]scale=1000:820:force_original_aspect_ratio=decrease,pad=iw+14:ih+14:7:7:color=0xffffff,setsar=1[main];
[bg][top]overlay=x=0:y=275[tmp1];
[tmp1][bottom]overlay=x=0:y=1285[tmp2];
[tmp2][main]overlay=x=(W-w)/2:y=(H-h)/2:format=auto[base]
FILTER
)
;;
spectrum)
BASE_FILTER=$(cat <<'FILTER'
[0:v]split=2[bg_src][fg_src];
[bg_src]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=30,eq=brightness=-0.62:saturation=0.40,setsar=1,
drawbox=x=0:y=0:w=1080:h=1920:color=0x040717@0.68:t=fill[bg];
[fg_src]scale=1000:780:force_original_aspect_ratio=decrease,pad=iw+12:ih+12:6:6:color=0x55c8ff,setsar=1[fg];
[0:a]aformat=sample_fmts=fltp:channel_layouts=stereo,showspectrum=s=1000x250:mode=combined:color=rainbow:scale=sqrt:fscale=log:slide=scroll:win_func=hann:fps=50:opacity=0.85,format=rgba[spec];
[bg][spec]overlay=x=(W-w)/2:y=1360:format=auto[tmp];
[tmp][fg]overlay=x=(W-w)/2:y=480:format=auto[base]
FILTER
)
;;
esac
FILTER_COMPLEX=$(cat <<FILTER
${BASE_FILTER};
[base]
drawbox=x=0:y=0:w=1080:h=245:color=black@0.50:t=fill:enable='between(t,0,${HOOK_END})',
drawtext=fontfile='${FONT_ESC}':textfile='${HOOK_ESC}':fontcolor=white:fontsize=70:line_spacing=10:x=(w-text_w)/2:y=70:fix_bounds=true:shadowcolor=black@0.75:shadowx=4:shadowy=4:enable='between(t,0,${HOOK_END})',
drawbox=x=0:y=1490:w=1080:h=430:color=black@0.62:t=fill:enable='gte(t,${INFO_START})',
drawtext=fontfile='${FONT_ESC}':textfile='${TITLE_ESC}':fontcolor=white:fontsize=62:line_spacing=8:x=(w-text_w)/2:y=1560:fix_bounds=true:shadowcolor=black@0.80:shadowx=4:shadowy=4:enable='gte(t,${INFO_START})',
drawtext=fontfile='${FONT_ESC}':textfile='${META_ESC}':fontcolor=white@0.92:fontsize=42:x=(w-text_w)/2:y=1655:fix_bounds=true:shadowcolor=black@0.80:shadowx=3:shadowy=3:enable='gte(t,${INFO_START})',
drawtext=fontfile='${FONT_ESC}':textfile='${WEBSITE_ESC}':fontcolor=0x55c8ff:fontsize=52:x=(w-text_w)/2:y=1780:fix_bounds=true:shadowcolor=black@0.85:shadowx=4:shadowy=4:enable='gte(t,${CTA_START})',
fps=50,format=yuv420p[vout]
FILTER
)
printf 'Creating AmigaDB YouTube Short...\n'
printf ' Input: %s\n' "$INPUT"
printf ' Segment: %s + %ss\n' "$START" "$DURATION"
printf ' Style: %s\n' "$STYLE"
printf ' Output: %s\n' "$OUTPUT"
printf ' Font: %s\n' "$FONT_FILE"
if [[ "$STYLE" == "brand" ]]; then
printf ' Bg image: %s\n' "$BACKGROUND_IMAGE"
fi
FFMPEG_ARGS=(
-hide_banner -y
-ss "$START"
-t "$DURATION"
-i "$INPUT"
)
if [[ "$STYLE" == "brand" ]]; then
FFMPEG_ARGS+=( -loop 1 -i "$BACKGROUND_IMAGE" )
fi
FFMPEG_ARGS+=(
-filter_complex "$FILTER_COMPLEX"
-map "[vout]"
-map 0:a?
-c:v libx264
-preset "$PRESET"
-crf "$CRF"
-profile:v high
-level:v 4.2
-pix_fmt yuv420p
-c:a aac
-b:a 320k
-ar 48000
-movflags +faststart
-shortest
"$OUTPUT"
)
ffmpeg "${FFMPEG_ARGS[@]}"
printf '\nDone: %s\n' "$OUTPUT"
+1 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"> <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="AmiReel.WinUI.app"/> <assemblyIdentity version="1.0.0.0" name="AmiReel.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1"> <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application> <application>

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 200 KiB

+2 -2
View File
@@ -14,9 +14,9 @@ Contents:
Individual transparent PNG files: Individual transparent PNG files:
16, 24, 32, 48, 64, 128, 256, 512 and 1024 px. 16, 24, 32, 48, 64, 128, 256, 512 and 1024 px.
Recommended WPF project setting: Referenced from the project file as:
<ApplicationIcon>Assets\AmiReel.ico</ApplicationIcon> <ApplicationIcon>branding\AmiReel.ico</ApplicationIcon>
Product: AmiReel — Amiga Video Renderer Product: AmiReel — Amiga Video Renderer
Brand: AmigaDB Brand: AmigaDB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Before

Width:  |  Height:  |  Size: 330 KiB

After

Width:  |  Height:  |  Size: 330 KiB

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

+18 -41
View File
@@ -1,8 +1,5 @@
[CmdletBinding()] [CmdletBinding()]
param( param()
[ValidateSet('WinUI', 'Wpf')]
[string]$Target = 'WinUI'
)
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@@ -14,41 +11,21 @@ if (-not (Test-Path $ffmpeg) -or -not (Test-Path $ffprobe)) {
throw 'Add ffmpeg.exe and ffprobe.exe to ThirdParty before publishing.' throw 'Add ffmpeg.exe and ffprobe.exe to ThirdParty before publishing.'
} }
switch ($Target) { $projectPath = Join-Path $projectDir 'AmiReel.csproj'
'WinUI' { $releaseDir = Join-Path $projectDir 'bin\Release\net10.0-windows10.0.26100.0\win-x64'
$projectPath = Join-Path $projectDir 'AmiReel.WinUI\AmiReel.WinUI.csproj' $releaseExe = Join-Path $releaseDir 'AmiReel.exe'
$releaseDir = Join-Path $projectDir 'AmiReel.WinUI\bin\Release\net10.0-windows10.0.26100.0\win-x64' $publishDir = Join-Path $releaseDir 'publish'
$releaseExe = Join-Path $releaseDir 'AmiReel.exe' $publishExe = Join-Path $publishDir 'AmiReel.exe'
$publishDir = Join-Path $releaseDir 'publish' $publishArguments = @(
$publishExe = Join-Path $publishDir 'AmiReel.exe' 'publish', $projectPath,
$publishArguments = @( '-c', 'Release',
'publish', $projectPath, '-r', 'win-x64',
'-c', 'Release', '--self-contained', 'true',
'-r', 'win-x64', '-p:WindowsPackageType=None',
'--self-contained', 'true', '-p:PublishSingleFile=true',
'-p:WindowsPackageType=None', '-p:IncludeNativeLibrariesForSelfExtract=true',
'-p:PublishSingleFile=true', '-p:EnableCompressionInSingleFile=true'
'-p:IncludeNativeLibrariesForSelfExtract=true', )
'-p:EnableCompressionInSingleFile=true'
)
}
'Wpf' {
$projectPath = Join-Path $projectDir 'AmigaDB.VideoRenderer.csproj'
$releaseDir = Join-Path $projectDir 'bin\Release\net8.0-windows\win-x64'
$releaseExe = Join-Path $releaseDir 'AmiReel.exe'
$publishDir = Join-Path $releaseDir 'publish'
$publishExe = Join-Path $publishDir 'AmiReel.exe'
$publishArguments = @(
'publish', $projectPath,
'-c', 'Release',
'-r', 'win-x64',
'--self-contained', 'true',
'-p:PublishSingleFile=true',
'-p:IncludeNativeLibrariesForSelfExtract=true',
'-p:EnableCompressionInSingleFile=true'
)
}
}
if ((Test-Path $releaseExe) -or (Test-Path $publishExe)) { if ((Test-Path $releaseExe) -or (Test-Path $publishExe)) {
$runningReleaseInstances = Get-Process | Where-Object { $runningReleaseInstances = Get-Process | Where-Object {
@@ -59,7 +36,7 @@ if ((Test-Path $releaseExe) -or (Test-Path $publishExe)) {
} }
if ($runningReleaseInstances) { if ($runningReleaseInstances) {
Write-Host "Stopping running $Target build before publish..." -ForegroundColor Yellow Write-Host 'Stopping running AmiReel build before publish...' -ForegroundColor Yellow
$runningReleaseInstances | Stop-Process -Force $runningReleaseInstances | Stop-Process -Force
Start-Sleep -Milliseconds 800 Start-Sleep -Milliseconds 800
} }
@@ -71,6 +48,6 @@ if (Test-Path $publishDir) {
& dotnet @publishArguments & dotnet @publishArguments
Write-Host "Published $Target to $publishDir" -ForegroundColor Green Write-Host "Published AmiReel to $publishDir" -ForegroundColor Green
Write-Host "Release folder: $publishDir" -ForegroundColor Cyan Write-Host "Release folder: $publishDir" -ForegroundColor Cyan
Write-Host "Executable: $publishExe" -ForegroundColor Cyan Write-Host "Executable: $publishExe" -ForegroundColor Cyan