Compare commits
9 Commits
027fc683ef
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 58e11ba4e3 | |||
| f1f1be778b | |||
| fc02cd9602 | |||
| 8f27aee11a | |||
| c412469773 | |||
| 3de3357d23 | |||
| 603af17e56 | |||
| 3e881b79b8 | |||
| ab46a16726 |
@@ -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
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
@@ -1,104 +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"/>
|
|
||||||
</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"/>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</ResourceDictionary.ThemeDictionaries>
|
|
||||||
|
|
||||||
<Style TargetType="Page">
|
|
||||||
<Setter Property="Background" Value="{ThemeResource PageBrush}" />
|
|
||||||
</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="24" />
|
|
||||||
<Setter Property="FontWeight" Value="SemiBold" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style x:Key="MutedTextStyle" TargetType="TextBlock">
|
|
||||||
<Setter Property="Foreground" Value="{ThemeResource MutedBrush}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="Button">
|
|
||||||
<Setter Property="Background" Value="{ThemeResource ButtonBrush}" />
|
|
||||||
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
|
|
||||||
</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}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="TextBox">
|
|
||||||
<Setter Property="Background" Value="{ThemeResource FieldBrush}" />
|
|
||||||
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="ComboBox">
|
|
||||||
<Setter Property="Background" Value="{ThemeResource FieldBrush}" />
|
|
||||||
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="ProgressBar">
|
|
||||||
<Setter Property="Foreground" Value="{ThemeResource AccentBrush}" />
|
|
||||||
<Setter Property="Background" Value="{ThemeResource FieldBrush}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="ListView">
|
|
||||||
<Setter Property="Background" Value="{ThemeResource FieldBrush}" />
|
|
||||||
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
|
|
||||||
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style TargetType="TextBlock">
|
|
||||||
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
|
|
||||||
</Style>
|
|
||||||
</ResourceDictionary>
|
|
||||||
</Application.Resources>
|
|
||||||
</Application>
|
|
||||||
@@ -1,47 +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; }
|
|
||||||
|
|
||||||
/// <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: 361 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 574 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 5.7 KiB |
@@ -1,190 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8" ?>
|
|
||||||
<Page
|
|
||||||
x:Class="AmiReel_WinUI.MainPage"
|
|
||||||
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
|
||||||
mc:Ignorable="d">
|
|
||||||
|
|
||||||
<Grid x:Name="RootGrid" Padding="20" RowSpacing="16" Background="{ThemeResource PageBrush}">
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid Grid.Row="0" ColumnSpacing="16">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<Border Grid.Column="0" Padding="18" Style="{StaticResource CardBorderStyle}">
|
|
||||||
<StackPanel Spacing="12">
|
|
||||||
<TextBlock Text="Source recordings" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
||||||
<Button Content="Add AVI files..." Click="AddInputs_Click" />
|
|
||||||
<Button Content="Clear" Click="ClearInputs_Click" />
|
|
||||||
</StackPanel>
|
|
||||||
<ListView x:Name="InputList"
|
|
||||||
Height="120"
|
|
||||||
SelectionMode="Single"
|
|
||||||
SelectionChanged="InputList_SelectionChanged" />
|
|
||||||
<Button x:Name="PreviewSourceButton"
|
|
||||||
Content="Preview selected"
|
|
||||||
Click="PreviewSource_Click"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
IsEnabled="False" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Grid.Column="1" Padding="18" Style="{StaticResource CardBorderStyle}">
|
|
||||||
<StackPanel Spacing="12">
|
|
||||||
<TextBlock Text="End card and output" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<TextBlock Text="Leave blank to use a built-in black end card." Style="{StaticResource MutedTextStyle}" />
|
|
||||||
|
|
||||||
<Grid ColumnSpacing="10" RowSpacing="10">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<TextBox x:Name="EndCardBox" Grid.Row="0" MinHeight="48" />
|
|
||||||
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseEndCard_Click" VerticalAlignment="Stretch" MinWidth="120" />
|
|
||||||
|
|
||||||
<TextBox x:Name="OutputFolderBox" Grid.Row="1" MinHeight="48" />
|
|
||||||
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseOutput_Click" VerticalAlignment="Stretch" MinWidth="120" />
|
|
||||||
|
|
||||||
<TextBox x:Name="OutputNameBox" Grid.Row="2" Grid.ColumnSpan="2" MinHeight="48" />
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<Border Grid.Row="1" Padding="18" Style="{StaticResource CardBorderStyle}">
|
|
||||||
<StackPanel Spacing="12">
|
|
||||||
<TextBlock Text="Render progress" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
||||||
<TextBlock x:Name="StageText" Text="Ready" FontSize="18" FontWeight="SemiBold" />
|
|
||||||
<TextBlock x:Name="PercentText" Text="0%" FontSize="18" FontWeight="SemiBold" Foreground="{ThemeResource AccentBrush}" />
|
|
||||||
</StackPanel>
|
|
||||||
<ProgressBar x:Name="RenderProgressBar" Minimum="0" Maximum="100" Height="10" />
|
|
||||||
<TextBlock x:Name="StatusText" Text="Select the recordings and start the render. The end card is optional." TextWrapping="WrapWholeWords" Style="{StaticResource MutedTextStyle}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Grid.Row="2" Padding="18" Style="{StaticResource CardBorderStyle}">
|
|
||||||
<StackPanel Spacing="12">
|
|
||||||
<TextBlock Text="FFmpeg log" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<TextBox x:Name="LogBox"
|
|
||||||
IsReadOnly="True"
|
|
||||||
AcceptsReturn="True"
|
|
||||||
TextWrapping="NoWrap"
|
|
||||||
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
|
||||||
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
|
||||||
FontFamily="Consolas"
|
|
||||||
FontSize="12"
|
|
||||||
VerticalAlignment="Stretch"
|
|
||||||
Height="260" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Grid Grid.Row="3">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="Auto" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10">
|
|
||||||
<Button x:Name="OpenOutputButton" Content="Open output folder" Click="OpenOutput_Click" IsEnabled="False" HorizontalAlignment="Left" />
|
|
||||||
<Button x:Name="PreviewRenderedButton" Content="Preview render" Click="PreviewRendered_Click" IsEnabled="False" />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
|
|
||||||
<Button x:Name="OpenSettingsButton" Content="Settings" Click="OpenSettings_Click" />
|
|
||||||
<Button x:Name="CancelButton" Content="Cancel" Click="Cancel_Click" IsEnabled="False" />
|
|
||||||
<Button x:Name="RenderButton" Content="Start render" Click="Render_Click" Style="{StaticResource PrimaryButtonStyle}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<ContentDialog x:Name="SettingsDialog"
|
|
||||||
Title="Settings"
|
|
||||||
PrimaryButtonText="Done"
|
|
||||||
CloseButtonText="Close"
|
|
||||||
DefaultButton="Primary">
|
|
||||||
<ScrollViewer MaxHeight="560" VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel Spacing="16">
|
|
||||||
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
|
||||||
<StackPanel Spacing="10">
|
|
||||||
<TextBlock Text="Appearance" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<ComboBox x:Name="ThemeBox" Header="Theme" SelectionChanged="ThemeBox_SelectionChanged">
|
|
||||||
<ComboBoxItem Content="Dark" Tag="Dark" />
|
|
||||||
<ComboBoxItem Content="Light" Tag="Light" />
|
|
||||||
</ComboBox>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
|
||||||
<StackPanel Spacing="10">
|
|
||||||
<TextBlock Text="Timing and screenshots" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<Grid ColumnSpacing="10" RowSpacing="10">
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
<ColumnDefinition Width="*" />
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
<TextBox x:Name="TrimBox" Grid.Row="0" Grid.Column="0" Header="Trim start (seconds)" />
|
|
||||||
<TextBox x:Name="FadeBox" Grid.Row="0" Grid.Column="1" Header="Fade (seconds)" />
|
|
||||||
<TextBox x:Name="HoldBox" Grid.Row="1" Grid.Column="0" Header="End-card hold (seconds)" />
|
|
||||||
<TextBox x:Name="IntervalBox" Grid.Row="1" Grid.Column="1" Header="Thumbnail interval (seconds)" />
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
|
||||||
<StackPanel Spacing="10">
|
|
||||||
<TextBlock Text="Video encoding" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<ComboBox x:Name="EncoderBox" Header="Encoder">
|
|
||||||
<ComboBoxItem Content="Auto (NVENC → CPU fallback)" Tag="Auto" />
|
|
||||||
<ComboBoxItem Content="NVIDIA NVENC" Tag="NvidiaNvenc" />
|
|
||||||
<ComboBoxItem Content="CPU libx264" Tag="CpuX264" />
|
|
||||||
</ComboBox>
|
|
||||||
<TextBlock Text="Output: 3840 × 2160 · 50 FPS" Style="{StaticResource MutedTextStyle}" />
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
|
||||||
<StackPanel Spacing="10">
|
|
||||||
<TextBlock Text="FFmpeg tools" Style="{StaticResource SectionTitleStyle}" />
|
|
||||||
<TextBlock Text="Optional override. Leave blank to auto-detect FFmpeg and FFprobe from PATH, then use the embedded fallback." TextWrapping="WrapWholeWords" Style="{StaticResource MutedTextStyle}" />
|
|
||||||
<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="FfmpegPathBox" Grid.Row="0" Header="ffmpeg.exe path" />
|
|
||||||
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseFfmpeg_Click" VerticalAlignment="Bottom" />
|
|
||||||
<TextBox x:Name="FfprobePathBox" Grid.Row="1" Header="ffprobe.exe path" />
|
|
||||||
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseFfprobe_Click" VerticalAlignment="Bottom" />
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</ContentDialog>
|
|
||||||
</Grid>
|
|
||||||
</Page>
|
|
||||||
@@ -1,345 +0,0 @@
|
|||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Globalization;
|
|
||||||
using System.Text;
|
|
||||||
using AmigaDB.VideoRenderer.Models;
|
|
||||||
using AmigaDB.VideoRenderer.Services;
|
|
||||||
using Microsoft.UI.Xaml;
|
|
||||||
using Microsoft.UI.Xaml.Controls;
|
|
||||||
using Microsoft.Win32;
|
|
||||||
using Windows.Storage.Pickers;
|
|
||||||
using WinRT.Interop;
|
|
||||||
|
|
||||||
namespace AmiReel_WinUI;
|
|
||||||
|
|
||||||
public sealed partial class MainPage : Page
|
|
||||||
{
|
|
||||||
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;
|
|
||||||
|
|
||||||
public MainPage()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
InputList.ItemsSource = _inputs;
|
|
||||||
_loadedSettings = AppSettingsStore.Load();
|
|
||||||
OutputFolderBox.Text = _loadedSettings.OutputFolder;
|
|
||||||
EndCardBox.Text = _loadedSettings.EndCardPath;
|
|
||||||
FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
|
|
||||||
FfprobePathBox.Text = _loadedSettings.FfprobePath;
|
|
||||||
OutputNameBox.Text = "amigadb_intro";
|
|
||||||
TrimBox.Text = _loadedSettings.TrimStart;
|
|
||||||
FadeBox.Text = _loadedSettings.FadeSeconds;
|
|
||||||
HoldBox.Text = _loadedSettings.EndCardHoldSeconds;
|
|
||||||
IntervalBox.Text = _loadedSettings.ThumbnailInterval;
|
|
||||||
SelectEncoder(_loadedSettings.Encoder);
|
|
||||||
SelectTheme(_loadedSettings.Theme);
|
|
||||||
ApplyTheme(_loadedSettings.Theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void AddInputs_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
FileOpenPicker picker = CreateFileOpenPicker();
|
|
||||||
picker.FileTypeFilter.Add(".avi");
|
|
||||||
picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
|
|
||||||
var files = await picker.PickMultipleFilesAsync();
|
|
||||||
if (files is null) return;
|
|
||||||
|
|
||||||
foreach (string path in files.Select(file => file.Path).OrderBy(NaturalKey))
|
|
||||||
if (!_inputs.Contains(path, StringComparer.OrdinalIgnoreCase)) _inputs.Add(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ClearInputs_Click(object sender, RoutedEventArgs e) => _inputs.Clear();
|
|
||||||
|
|
||||||
private async void BrowseEndCard_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
FileOpenPicker picker = CreateFileOpenPicker();
|
|
||||||
picker.FileTypeFilter.Add(".png");
|
|
||||||
picker.FileTypeFilter.Add(".jpg");
|
|
||||||
picker.FileTypeFilter.Add(".jpeg");
|
|
||||||
picker.FileTypeFilter.Add(".webp");
|
|
||||||
picker.FileTypeFilter.Add(".bmp");
|
|
||||||
var file = await picker.PickSingleFileAsync();
|
|
||||||
if (file is not null)
|
|
||||||
EndCardBox.Text = file.Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void BrowseOutput_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
FolderPicker picker = CreateFolderPicker();
|
|
||||||
var folder = await picker.PickSingleFolderAsync();
|
|
||||||
if (folder is not null)
|
|
||||||
OutputFolderBox.Text = folder.Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void BrowseFfmpeg_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
FileOpenPicker picker = CreateFileOpenPicker();
|
|
||||||
picker.FileTypeFilter.Add(".exe");
|
|
||||||
var file = await picker.PickSingleFileAsync();
|
|
||||||
if (file is not null)
|
|
||||||
FfmpegPathBox.Text = file.Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void BrowseFfprobe_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
FileOpenPicker picker = CreateFileOpenPicker();
|
|
||||||
picker.FileTypeFilter.Add(".exe");
|
|
||||||
var file = await picker.PickSingleFileAsync();
|
|
||||||
if (file is not null)
|
|
||||||
FfprobePathBox.Text = file.Path;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OpenSettings_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
SettingsDialog.XamlRoot = XamlRoot;
|
|
||||||
await SettingsDialog.ShowAsync();
|
|
||||||
SaveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void Render_Click(object sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
RenderSettings settings = ReadSettings();
|
|
||||||
SaveSettings();
|
|
||||||
SetRendering(true);
|
|
||||||
LogBox.Text = string.Empty;
|
|
||||||
_renderCancellation = new CancellationTokenSource();
|
|
||||||
Progress<RenderProgress> progress = new(UpdateProgress);
|
|
||||||
await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
|
|
||||||
_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.");
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
UpdateProgress(new(RenderProgressBar.Value, "Cancelled", "The render was cancelled."));
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
AppendLog("ERROR: " + exception);
|
|
||||||
StageText.Text = "Failed";
|
|
||||||
await ShowMessageAsync("Render failed", UserFacingErrors.Summarize(exception));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_renderCancellation?.Dispose();
|
|
||||||
_renderCancellation = null;
|
|
||||||
SetRendering(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (!IsLoaded) return;
|
|
||||||
string theme = SelectedTheme();
|
|
||||||
ApplyTheme(theme);
|
|
||||||
SaveSettings();
|
|
||||||
}
|
|
||||||
|
|
||||||
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>(SelectedEncoder());
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetRendering(bool rendering)
|
|
||||||
{
|
|
||||||
RenderButton.IsEnabled = !rendering;
|
|
||||||
CancelButton.IsEnabled = rendering;
|
|
||||||
OpenSettingsButton.IsEnabled = !rendering;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateProgress(RenderProgress value)
|
|
||||||
{
|
|
||||||
RenderProgressBar.Value = value.Percent;
|
|
||||||
PercentText.Text = $"{value.Percent:0}%";
|
|
||||||
StageText.Text = value.Stage;
|
|
||||||
StatusText.Text = value.Message;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OpenPreview(string mediaPath)
|
|
||||||
{
|
|
||||||
if (!File.Exists(mediaPath))
|
|
||||||
return;
|
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true });
|
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendLog(string line)
|
|
||||||
{
|
|
||||||
lock (_logLock)
|
|
||||||
{
|
|
||||||
_pendingLog.AppendLine(line);
|
|
||||||
if (_logFlushScheduled)
|
|
||||||
return;
|
|
||||||
|
|
||||||
_logFlushScheduled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = DispatcherQueue.TryEnqueue(() =>
|
|
||||||
{
|
|
||||||
string chunk;
|
|
||||||
lock (_logLock)
|
|
||||||
{
|
|
||||||
chunk = _pendingLog.ToString();
|
|
||||||
_pendingLog.Clear();
|
|
||||||
_logFlushScheduled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (chunk.Length == 0)
|
|
||||||
return;
|
|
||||||
|
|
||||||
LogBox.Text += chunk;
|
|
||||||
LogBox.Select(LogBox.Text.Length, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveSettings()
|
|
||||||
{
|
|
||||||
AppSettingsStore.Save(new AppSettings(
|
|
||||||
OutputFolderBox.Text.Trim(),
|
|
||||||
EndCardBox.Text.Trim(),
|
|
||||||
FfmpegPathBox.Text.Trim(),
|
|
||||||
FfprobePathBox.Text.Trim(),
|
|
||||||
_loadedSettings.PreviewPlayerPath,
|
|
||||||
SelectedTheme(),
|
|
||||||
SelectedEncoder(),
|
|
||||||
TrimBox.Text.Trim(),
|
|
||||||
FadeBox.Text.Trim(),
|
|
||||||
HoldBox.Text.Trim(),
|
|
||||||
IntervalBox.Text.Trim()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private string SelectedTheme() => (ThemeBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Dark";
|
|
||||||
|
|
||||||
private string SelectedEncoder() => (EncoderBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Auto";
|
|
||||||
|
|
||||||
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 InputList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
PreviewSourceButton.IsEnabled = InputList.SelectedItem is string;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ApplyTheme(string theme)
|
|
||||||
{
|
|
||||||
RequestedTheme = string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase)
|
|
||||||
? ElementTheme.Light
|
|
||||||
: ElementTheme.Dark;
|
|
||||||
}
|
|
||||||
|
|
||||||
private FileOpenPicker CreateFileOpenPicker()
|
|
||||||
{
|
|
||||||
FileOpenPicker picker = new();
|
|
||||||
InitializeWithWindow.Initialize(picker, App.MainWindowHandle);
|
|
||||||
return picker;
|
|
||||||
}
|
|
||||||
|
|
||||||
private FolderPicker CreateFolderPicker()
|
|
||||||
{
|
|
||||||
FolderPicker picker = new();
|
|
||||||
picker.FileTypeFilter.Add("*");
|
|
||||||
InitializeWithWindow.Initialize(picker, App.MainWindowHandle);
|
|
||||||
return picker;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 async Task ShowMessageAsync(string title, string message)
|
|
||||||
{
|
|
||||||
ContentDialog dialog = new()
|
|
||||||
{
|
|
||||||
Title = title,
|
|
||||||
Content = message,
|
|
||||||
CloseButtonText = "OK",
|
|
||||||
XamlRoot = XamlRoot
|
|
||||||
};
|
|
||||||
await dialog.ShowAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +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">
|
|
||||||
<Window.SystemBackdrop>
|
|
||||||
<MicaBackdrop />
|
|
||||||
</Window.SystemBackdrop>
|
|
||||||
|
|
||||||
<Grid>
|
|
||||||
<Grid.RowDefinitions>
|
|
||||||
<RowDefinition Height="Auto" />
|
|
||||||
<RowDefinition Height="*" />
|
|
||||||
</Grid.RowDefinitions>
|
|
||||||
|
|
||||||
<Grid x:Name="AppTitleBar" Height="44" Background="Transparent">
|
|
||||||
<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"
|
|
||||||
FontSize="16"
|
|
||||||
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>
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
using Microsoft.UI.Xaml;
|
|
||||||
using System;
|
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
// 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
|
|
||||||
{
|
|
||||||
public MainWindow()
|
|
||||||
{
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
ExtendsContentIntoTitleBar = true;
|
|
||||||
SetTitleBar(AppTitleBar);
|
|
||||||
|
|
||||||
string? executablePath = Environment.ProcessPath;
|
|
||||||
if (!string.IsNullOrWhiteSpace(executablePath) && File.Exists(executablePath))
|
|
||||||
AppWindow.SetIcon(executablePath);
|
|
||||||
|
|
||||||
// Navigate the root frame to the main page on startup.
|
|
||||||
RootFrame.Navigate(typeof(MainPage));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<Solution>
|
||||||
|
<Project Path="AmiReel.csproj" />
|
||||||
|
<Project Path="AmiReel.Tests/AmiReel.Tests.csproj" />
|
||||||
|
</Solution>
|
||||||
@@ -1,38 +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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
@@ -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
|
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
<Application x:Class="AmigaDB.VideoRenderer.App"
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<Application
|
||||||
|
x:Class="AmiReel.App"
|
||||||
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"
|
||||||
StartupUri="MainWindow.xaml">
|
xmlns:local="using:AmiReel">
|
||||||
<Application.Resources>
|
<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="PageBrush" Color="#0E1525"/>
|
||||||
<SolidColorBrush x:Key="PanelBrush" Color="#162033"/>
|
<SolidColorBrush x:Key="PanelBrush" Color="#162033"/>
|
||||||
<SolidColorBrush x:Key="PanelAltBrush" Color="#1A2740"/>
|
<SolidColorBrush x:Key="PanelAltBrush" Color="#1A2740"/>
|
||||||
@@ -14,368 +22,248 @@
|
|||||||
<SolidColorBrush x:Key="MutedBrush" Color="#9FB1CC"/>
|
<SolidColorBrush x:Key="MutedBrush" Color="#9FB1CC"/>
|
||||||
<SolidColorBrush x:Key="ButtonBrush" Color="#243554"/>
|
<SolidColorBrush x:Key="ButtonBrush" Color="#243554"/>
|
||||||
<SolidColorBrush x:Key="ButtonHoverBrush" Color="#2E446C"/>
|
<SolidColorBrush x:Key="ButtonHoverBrush" Color="#2E446C"/>
|
||||||
<SolidColorBrush x:Key="ButtonDisabledBrush" Color="#2A3140"/>
|
|
||||||
<SolidColorBrush x:Key="ButtonDisabledTextBrush" Color="#7E8BA1"/>
|
|
||||||
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#07111D"/>
|
<SolidColorBrush x:Key="PrimaryTextBrush" Color="#07111D"/>
|
||||||
<SolidColorBrush x:Key="SelectionBrush" Color="#295C87"/>
|
|
||||||
<SolidColorBrush x:Key="SelectionTextBrush" Color="#FFFFFF"/>
|
|
||||||
<SolidColorBrush x:Key="ScrollTrackBrush" Color="#11192B"/>
|
|
||||||
<SolidColorBrush x:Key="ScrollThumbBrush" Color="#31476D"/>
|
|
||||||
<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"/>
|
||||||
|
<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 applied to built-in Fluent controls -->
|
||||||
|
<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" />
|
<Setter Property="FontFamily" Value="Bahnschrift" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="ScrollViewer">
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ScrollViewer">
|
|
||||||
<Grid Background="{TemplateBinding Background}">
|
|
||||||
<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}"/>
|
<Style x:Key="CardBorderStyle" TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{ThemeResource PanelBrush}" />
|
||||||
<ScrollBar x:Name="PART_VerticalScrollBar"
|
<Setter Property="BorderBrush" Value="{ThemeResource BorderBrush}" />
|
||||||
Grid.Row="0"
|
|
||||||
Grid.Column="1"
|
|
||||||
Orientation="Vertical"
|
|
||||||
Value="{TemplateBinding VerticalOffset}"
|
|
||||||
Maximum="{TemplateBinding ScrollableHeight}"
|
|
||||||
ViewportSize="{TemplateBinding ViewportHeight}"
|
|
||||||
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}"/>
|
|
||||||
|
|
||||||
<ScrollBar x:Name="PART_HorizontalScrollBar"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="0"
|
|
||||||
Orientation="Horizontal"
|
|
||||||
Value="{TemplateBinding HorizontalOffset}"
|
|
||||||
Maximum="{TemplateBinding ScrollableWidth}"
|
|
||||||
ViewportSize="{TemplateBinding ViewportWidth}"
|
|
||||||
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}"/>
|
|
||||||
|
|
||||||
<Border x:Name="CornerBox"
|
|
||||||
Grid.Row="1"
|
|
||||||
Grid.Column="1"
|
|
||||||
Width="14"
|
|
||||||
Height="14"
|
|
||||||
Background="{DynamicResource ScrollTrackBrush}"
|
|
||||||
Visibility="Collapsed"/>
|
|
||||||
</Grid>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<MultiTrigger>
|
|
||||||
<MultiTrigger.Conditions>
|
|
||||||
<Condition Property="ComputedVerticalScrollBarVisibility" Value="Visible"/>
|
|
||||||
<Condition Property="ComputedHorizontalScrollBarVisibility" Value="Visible"/>
|
|
||||||
</MultiTrigger.Conditions>
|
|
||||||
<Setter TargetName="CornerBox" Property="Visibility" Value="Visible"/>
|
|
||||||
</MultiTrigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
<Style x:Key="ScrollBarThumbStyle" TargetType="Thumb">
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="Thumb">
|
|
||||||
<Border Background="{DynamicResource ScrollThumbBrush}" CornerRadius="6" Margin="2"/>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter Property="Opacity" Value="1"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
<Style TargetType="RepeatButton" x:Key="ScrollBarButtonStyle">
|
|
||||||
<Setter Property="Focusable" Value="False"/>
|
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderBrush" Value="Transparent"/>
|
|
||||||
<Setter Property="BorderThickness" Value="0"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="RepeatButton">
|
|
||||||
<Border Background="{TemplateBinding Background}" CornerRadius="6"/>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
<Style TargetType="ScrollBar">
|
|
||||||
<Setter Property="Width" Value="14"/>
|
|
||||||
<Setter Property="Background" Value="{DynamicResource ScrollTrackBrush}"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ScrollBar">
|
|
||||||
<Grid Background="Transparent" Width="{TemplateBinding Width}">
|
|
||||||
<Border Background="{DynamicResource ScrollTrackBrush}" CornerRadius="7" Margin="2"/>
|
|
||||||
<Track x:Name="PART_Track" IsDirectionReversed="True" Margin="1">
|
|
||||||
<Track.DecreaseRepeatButton>
|
|
||||||
<RepeatButton Style="{StaticResource ScrollBarButtonStyle}" Command="ScrollBar.PageUpCommand"/>
|
|
||||||
</Track.DecreaseRepeatButton>
|
|
||||||
<Track.Thumb>
|
|
||||||
<Thumb Style="{StaticResource ScrollBarThumbStyle}"/>
|
|
||||||
</Track.Thumb>
|
|
||||||
<Track.IncreaseRepeatButton>
|
|
||||||
<RepeatButton Style="{StaticResource ScrollBarButtonStyle}" Command="ScrollBar.PageDownCommand"/>
|
|
||||||
</Track.IncreaseRepeatButton>
|
|
||||||
</Track>
|
|
||||||
</Grid>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<Trigger Property="Orientation" Value="Horizontal">
|
|
||||||
<Setter Property="Height" Value="14"/>
|
|
||||||
<Setter TargetName="PART_Track" Property="IsDirectionReversed" Value="False"/>
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
|
||||||
<Style TargetType="TextBox">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
|
|
||||||
<Setter Property="CaretBrush" Value="{DynamicResource TextBrush}"/>
|
|
||||||
<Setter Property="SelectionBrush" Value="{DynamicResource SelectionBrush}"/>
|
|
||||||
<Setter Property="SelectionTextBrush" Value="{DynamicResource SelectionTextBrush}"/>
|
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="Padding" Value="9,7"/>
|
<Setter Property="CornerRadius" Value="16" />
|
||||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
|
||||||
<Setter Property="SnapsToDevicePixels" Value="True"/>
|
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="ComboBox">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/>
|
<Style x:Key="SectionTitleStyle" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
|
<Setter Property="FontSize" Value="15" />
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
<Setter Property="Padding" Value="8,0"/>
|
|
||||||
<Setter Property="Height" Value="42"/>
|
|
||||||
<Setter Property="MinHeight" Value="42"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ComboBox">
|
|
||||||
<Grid>
|
|
||||||
<Border x:Name="OuterBorder"
|
|
||||||
Background="{TemplateBinding Background}"
|
|
||||||
BorderBrush="{TemplateBinding BorderBrush}"
|
|
||||||
BorderThickness="{TemplateBinding BorderThickness}"
|
|
||||||
CornerRadius="12"
|
|
||||||
Height="{TemplateBinding Height}"
|
|
||||||
MinHeight="{TemplateBinding MinHeight}">
|
|
||||||
<Grid>
|
|
||||||
<Grid.ColumnDefinitions>
|
|
||||||
<ColumnDefinition Width="*"/>
|
|
||||||
<ColumnDefinition Width="42"/>
|
|
||||||
</Grid.ColumnDefinitions>
|
|
||||||
<ContentPresenter Margin="12,0,8,0"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
HorizontalAlignment="Left"
|
|
||||||
Content="{TemplateBinding SelectionBoxItem}"
|
|
||||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
|
||||||
ContentTemplateSelector="{TemplateBinding ItemTemplateSelector}"/>
|
|
||||||
<Border Grid.Column="1" Background="{DynamicResource AccentSoftBrush}" CornerRadius="0,12,12,0" BorderBrush="{DynamicResource BorderBrush}" BorderThickness="1,0,0,0">
|
|
||||||
<Path HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
Fill="{DynamicResource AccentBrush}"
|
|
||||||
Data="M 0 0 L 4 4 L 8 0 Z"/>
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
<Popup x:Name="PART_Popup"
|
|
||||||
Placement="Bottom"
|
|
||||||
AllowsTransparency="True"
|
|
||||||
Focusable="False"
|
|
||||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
|
||||||
PopupAnimation="Fade">
|
|
||||||
<Border Margin="0,8,0,0"
|
|
||||||
Background="{DynamicResource PopupBrush}"
|
|
||||||
BorderBrush="{DynamicResource BorderBrush}"
|
|
||||||
BorderThickness="1"
|
|
||||||
CornerRadius="14"
|
|
||||||
MinWidth="{TemplateBinding ActualWidth}">
|
|
||||||
<ScrollViewer Margin="6" SnapsToDevicePixels="True" MaxHeight="280">
|
|
||||||
<ItemsPresenter KeyboardNavigation.DirectionalNavigation="Contained"/>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Border>
|
|
||||||
</Popup>
|
|
||||||
<ToggleButton x:Name="HitTarget" Opacity="0" Focusable="False" IsChecked="{Binding IsDropDownOpen, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"/>
|
|
||||||
</Grid>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<Trigger Property="IsMouseOver" Value="True">
|
|
||||||
<Setter TargetName="OuterBorder" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsKeyboardFocusWithin" Value="True">
|
|
||||||
<Setter TargetName="OuterBorder" Property="BorderBrush" Value="{DynamicResource AccentBrush}"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsEnabled" Value="False">
|
|
||||||
<Setter Property="Opacity" Value="0.65"/>
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="ComboBoxItem">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/>
|
<Style x:Key="MutedTextStyle" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
<Setter Property="Foreground" Value="{ThemeResource MutedBrush}" />
|
||||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
<Setter Property="FontSize" Value="13" />
|
||||||
<Setter Property="Padding" Value="10,8"/>
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate TargetType="ComboBoxItem">
|
|
||||||
<Border x:Name="ItemBorder" Background="{TemplateBinding Background}" CornerRadius="10" Padding="{TemplateBinding Padding}">
|
|
||||||
<ContentPresenter/>
|
|
||||||
</Border>
|
|
||||||
<ControlTemplate.Triggers>
|
|
||||||
<Trigger Property="IsHighlighted" Value="True">
|
|
||||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource AccentSoftBrush}"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsSelected" Value="True">
|
|
||||||
<Setter TargetName="ItemBorder" Property="Background" Value="{DynamicResource AccentBrush}"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
|
|
||||||
</Trigger>
|
|
||||||
</ControlTemplate.Triggers>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsHighlighted" Value="True">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AccentSoftBrush}"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
|
||||||
</Trigger>
|
|
||||||
<Trigger Property="IsSelected" Value="True">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="ListBox">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource FieldBrush}"/>
|
<Style x:Key="FieldLabelStyle" TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
<Setter Property="Foreground" Value="{ThemeResource MutedBrush}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
|
<Setter Property="FontSize" Value="12" />
|
||||||
<Setter Property="BorderThickness" Value="1"/>
|
<Setter Property="Margin" Value="0,0,0,4" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="ListBoxItem">
|
|
||||||
<Setter Property="Padding" Value="8,6"/>
|
<Style TargetType="TextBlock">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
<Setter Property="Foreground" Value="{ThemeResource TextBrush}" />
|
||||||
<Setter Property="Background" Value="Transparent"/>
|
<Setter Property="FontSize" Value="13" />
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="IsSelected" Value="True">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AccentSoftBrush}"/>
|
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="Button">
|
<Style TargetType="Button">
|
||||||
<Setter Property="Background" Value="{DynamicResource ButtonBrush}"/>
|
<Setter Property="Padding" Value="16,9" />
|
||||||
<Setter Property="Foreground" Value="{DynamicResource TextBrush}"/>
|
<Setter Property="FontSize" Value="13" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
<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>
|
||||||
<Style x:Key="PrimaryButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource PrimaryTextBrush}"/>
|
<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="FontWeight" Value="SemiBold" />
|
||||||
<Setter Property="Padding" Value="22,11"/>
|
<Setter Property="Padding" Value="22,10" />
|
||||||
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style x:Key="IconButton" TargetType="Button" BasedOn="{StaticResource {x:Type Button}}">
|
|
||||||
<Setter Property="Width" Value="46"/>
|
<Style x:Key="IconButtonStyle" TargetType="Button">
|
||||||
<Setter Property="Height" Value="46"/>
|
<Setter Property="Width" Value="40" />
|
||||||
|
<Setter Property="Height" Value="40" />
|
||||||
<Setter Property="Padding" Value="0" />
|
<Setter Property="Padding" Value="0" />
|
||||||
<Setter Property="FontSize" Value="20"/>
|
<Setter Property="FontSize" Value="16" />
|
||||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
<Setter Property="CornerRadius" Value="10" />
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style x:Key="SecondaryPanel" TargetType="Border">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource PanelBrush}"/>
|
<!-- Explicit styles bound to ContentDialog.PrimaryButtonStyle / CloseButtonStyle so its
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource BorderBrush}"/>
|
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="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" />
|
<Setter Property="CornerRadius" Value="18" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style TargetType="GroupBox">
|
</ResourceDictionary>
|
||||||
<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>
|
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
</Application>
|
</Application>
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 130 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 109 KiB |
@@ -0,0 +1,33 @@
|
|||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Microsoft.UI.Xaml.Media;
|
||||||
|
|
||||||
|
namespace AmiReel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds ContentDialogs styled to match the app's own palette and button shapes,
|
||||||
|
/// since ContentDialog's default Fluent look (accent-pill primary button, plain
|
||||||
|
/// square close button, ungrouped theme resources) does not follow our custom brushes.
|
||||||
|
/// </summary>
|
||||||
|
internal static class DialogHelper
|
||||||
|
{
|
||||||
|
public static ContentDialog CreateStyled(XamlRoot xamlRoot, ElementTheme theme)
|
||||||
|
{
|
||||||
|
string themeKey = theme == ElementTheme.Dark ? "Dark" : "Light";
|
||||||
|
ResourceDictionary themeDictionary = (ResourceDictionary)Application.Current.Resources.ThemeDictionaries[themeKey];
|
||||||
|
|
||||||
|
return new ContentDialog
|
||||||
|
{
|
||||||
|
XamlRoot = xamlRoot,
|
||||||
|
RequestedTheme = theme,
|
||||||
|
Background = (Brush)themeDictionary["PanelBrush"],
|
||||||
|
Foreground = (Brush)themeDictionary["TextBrush"],
|
||||||
|
BorderBrush = (Brush)themeDictionary["HeroBorderBrush"],
|
||||||
|
BorderThickness = new Thickness(1.5),
|
||||||
|
CornerRadius = new CornerRadius(18),
|
||||||
|
PrimaryButtonStyle = (Style)Application.Current.Resources["DialogPrimaryButtonStyle"],
|
||||||
|
SecondaryButtonStyle = (Style)Application.Current.Resources["DialogCloseButtonStyle"],
|
||||||
|
CloseButtonStyle = (Style)Application.Current.Resources["DialogCloseButtonStyle"]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,354 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Page
|
||||||
|
x:Class="AmiReel.MainPage"
|
||||||
|
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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d">
|
||||||
|
|
||||||
|
<Grid x:Name="RootGrid" Padding="20" RowSpacing="16" Background="{ThemeResource PageBrush}">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<Border Grid.Column="0" Padding="18" Style="{StaticResource CardBorderStyle}">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Text="Source recordings" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
||||||
|
<Button Content="Add video files..." Click="AddInputs_Click" />
|
||||||
|
<Button Content="Clear" Click="ClearInputs_Click" />
|
||||||
|
</StackPanel>
|
||||||
|
<ListView x:Name="InputList"
|
||||||
|
Height="130"
|
||||||
|
SelectionMode="Single"
|
||||||
|
SelectionChanged="InputList_SelectionChanged"
|
||||||
|
AllowDrop="True"
|
||||||
|
DragOver="InputList_DragOver"
|
||||||
|
Drop="InputList_Drop"
|
||||||
|
ToolTipService.ToolTip="Drag and drop video files here to add them" />
|
||||||
|
<Button x:Name="PreviewSourceButton"
|
||||||
|
Content="Preview selected"
|
||||||
|
Click="PreviewSource_Click"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
IsEnabled="False" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Column="1" Padding="18" Style="{StaticResource CardBorderStyle}">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Text="End card and output" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<TextBlock Text="Leave blank to use a built-in black end card." Style="{StaticResource MutedTextStyle}" />
|
||||||
|
|
||||||
|
<Grid ColumnSpacing="10" RowSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<TextBox x:Name="EndCardBox" Grid.Row="0" MinHeight="40" />
|
||||||
|
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseEndCard_Click" VerticalAlignment="Stretch" MinWidth="120" />
|
||||||
|
|
||||||
|
<TextBox x:Name="OutputFolderBox" Grid.Row="1" MinHeight="40" />
|
||||||
|
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseOutput_Click" VerticalAlignment="Stretch" MinWidth="120" />
|
||||||
|
|
||||||
|
<TextBox x:Name="OutputNameBox" Grid.Row="2" Grid.ColumnSpan="2" MinHeight="40" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<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">
|
||||||
|
<TextBlock Text="Render progress" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<TextBlock x:Name="StageText" Text="Ready" FontSize="15" FontWeight="SemiBold" />
|
||||||
|
<TextBlock x:Name="PercentText" Text="0%" FontSize="15" FontWeight="SemiBold" Foreground="{ThemeResource AccentBrush}" />
|
||||||
|
</StackPanel>
|
||||||
|
<ProgressBar x:Name="RenderProgressBar" Minimum="0" Maximum="100" Height="8" />
|
||||||
|
<TextBlock x:Name="StatusText" Text="Select the recordings and start the render. The end card is optional." TextWrapping="WrapWholeWords" Style="{StaticResource MutedTextStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Grid.Row="3" Padding="18" Style="{StaticResource CardBorderStyle}">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<TextBlock Text="FFmpeg log" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<TextBox x:Name="LogBox"
|
||||||
|
IsReadOnly="True"
|
||||||
|
AcceptsReturn="True"
|
||||||
|
TextWrapping="NoWrap"
|
||||||
|
ScrollViewer.VerticalScrollBarVisibility="Auto"
|
||||||
|
ScrollViewer.HorizontalScrollBarVisibility="Auto"
|
||||||
|
FontFamily="Consolas"
|
||||||
|
FontSize="12"
|
||||||
|
VerticalAlignment="Stretch"
|
||||||
|
Height="260" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid Grid.Row="4">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10">
|
||||||
|
<Button x:Name="OpenOutputButton" Content="Open output folder" Click="OpenOutput_Click" IsEnabled="False" HorizontalAlignment="Left" />
|
||||||
|
<Button x:Name="PreviewRenderedButton" Content="Preview render" Click="PreviewRendered_Click" IsEnabled="False" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
|
||||||
|
<Button x:Name="OpenSettingsButton" Content="" FontFamily="Segoe Fluent Icons, Segoe MDL2 Assets"
|
||||||
|
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="RenderButton" Content="Start render" Click="Render_Click" Style="{StaticResource PrimaryButtonStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<ContentDialog x:Name="SettingsDialog"
|
||||||
|
Title="Settings"
|
||||||
|
PrimaryButtonText="Done"
|
||||||
|
CloseButtonText="Close"
|
||||||
|
DefaultButton="Primary"
|
||||||
|
Background="{ThemeResource PanelBrush}"
|
||||||
|
Foreground="{ThemeResource TextBrush}"
|
||||||
|
BorderBrush="{ThemeResource HeroBorderBrush}"
|
||||||
|
BorderThickness="1.5"
|
||||||
|
CornerRadius="18"
|
||||||
|
PrimaryButtonStyle="{StaticResource DialogPrimaryButtonStyle}"
|
||||||
|
CloseButtonStyle="{StaticResource DialogCloseButtonStyle}">
|
||||||
|
<ScrollViewer MaxHeight="560" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Spacing="16">
|
||||||
|
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Appearance" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<ComboBox x:Name="ThemeBox" Header="Theme" SelectionChanged="ThemeBox_SelectionChanged">
|
||||||
|
<ComboBoxItem Content="Dark" Tag="Dark" />
|
||||||
|
<ComboBoxItem Content="Light" Tag="Light" />
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Timing and screenshots" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<Grid ColumnSpacing="10" RowSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBox x:Name="TrimBox" Grid.Row="0" Grid.Column="0" Header="Trim start (seconds)" />
|
||||||
|
<TextBox x:Name="FadeBox" Grid.Row="0" Grid.Column="1" Header="Fade (seconds)" />
|
||||||
|
<TextBox x:Name="HoldBox" Grid.Row="1" Grid.Column="0" Header="End-card hold (seconds)" />
|
||||||
|
<TextBox x:Name="IntervalBox" Grid.Row="1" Grid.Column="1" Header="Thumbnail interval (seconds)" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Video encoding" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<ComboBox x:Name="EncoderBox" Header="Encoder">
|
||||||
|
<ComboBoxItem Content="Auto (NVENC → CPU fallback)" Tag="Auto" />
|
||||||
|
<ComboBoxItem Content="NVIDIA NVENC" Tag="NvidiaNvenc" />
|
||||||
|
<ComboBoxItem Content="CPU libx264" Tag="CpuX264" />
|
||||||
|
</ComboBox>
|
||||||
|
<TextBlock Text="Output: 3840 × 2160 · 50 FPS" Style="{StaticResource MutedTextStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="Source files" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<CheckBox x:Name="MoveSourcesBox"
|
||||||
|
Content="Move source videos to destination\originals"
|
||||||
|
IsChecked="True" />
|
||||||
|
<TextBlock Text="After a successful render, original recordings are moved into an originals subfolder of the output folder. Uncheck to leave them where they are."
|
||||||
|
TextWrapping="WrapWholeWords"
|
||||||
|
Style="{StaticResource MutedTextStyle}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Border Padding="14" Background="{ThemeResource PanelAltBrush}" BorderBrush="{ThemeResource BorderBrush}" BorderThickness="1" CornerRadius="12">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<TextBlock Text="FFmpeg tools" Style="{StaticResource SectionTitleStyle}" />
|
||||||
|
<TextBlock Text="Optional override. Leave blank to auto-detect FFmpeg and FFprobe from PATH, then use the embedded fallback." TextWrapping="WrapWholeWords" Style="{StaticResource MutedTextStyle}" />
|
||||||
|
<Grid ColumnSpacing="10" RowSpacing="10">
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="*" />
|
||||||
|
<ColumnDefinition Width="Auto" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<TextBox x:Name="FfmpegPathBox" Grid.Row="0" Header="ffmpeg.exe path" />
|
||||||
|
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseFfmpeg_Click" VerticalAlignment="Bottom" />
|
||||||
|
<TextBox x:Name="FfprobePathBox" Grid.Row="1" Header="ffprobe.exe path" />
|
||||||
|
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseFfprobe_Click" VerticalAlignment="Bottom" />
|
||||||
|
<TextBox x:Name="PreviewPlayerPathBox" Grid.Row="2" Header="Preview player path (optional, e.g. mpv.exe)" />
|
||||||
|
<Button Grid.Row="2" Grid.Column="1" Content="Browse..." Click="BrowsePreviewPlayer_Click" VerticalAlignment="Bottom" />
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</ContentDialog>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
@@ -0,0 +1,573 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text;
|
||||||
|
using AmiReel.Models;
|
||||||
|
using AmiReel.Services;
|
||||||
|
using Microsoft.UI.Xaml;
|
||||||
|
using Microsoft.UI.Xaml.Controls;
|
||||||
|
using Windows.ApplicationModel.DataTransfer;
|
||||||
|
using Windows.Storage;
|
||||||
|
using Windows.Storage.Pickers;
|
||||||
|
using WinRT.Interop;
|
||||||
|
|
||||||
|
namespace AmiReel;
|
||||||
|
|
||||||
|
public sealed partial class MainPage : Page
|
||||||
|
{
|
||||||
|
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 _isShortsTab;
|
||||||
|
|
||||||
|
public MainPage()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
InputList.ItemsSource = _inputs;
|
||||||
|
_loadedSettings = AppSettingsStore.Load();
|
||||||
|
OutputFolderBox.Text = _loadedSettings.OutputFolder;
|
||||||
|
EndCardBox.Text = _loadedSettings.EndCardPath;
|
||||||
|
FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
|
||||||
|
FfprobePathBox.Text = _loadedSettings.FfprobePath;
|
||||||
|
PreviewPlayerPathBox.Text = _loadedSettings.PreviewPlayerPath;
|
||||||
|
OutputNameBox.Text = "amigadb_intro";
|
||||||
|
TrimBox.Text = _loadedSettings.TrimStart;
|
||||||
|
FadeBox.Text = _loadedSettings.FadeSeconds;
|
||||||
|
HoldBox.Text = _loadedSettings.EndCardHoldSeconds;
|
||||||
|
IntervalBox.Text = _loadedSettings.ThumbnailInterval;
|
||||||
|
SelectEncoder(_loadedSettings.Encoder);
|
||||||
|
SelectTheme(_loadedSettings.Theme);
|
||||||
|
ApplyTheme(_loadedSettings.Theme);
|
||||||
|
MoveSourcesBox.IsChecked = _loadedSettings.ShouldMoveSourcesToOriginals;
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
FileOpenPicker picker = CreateFileOpenPicker();
|
||||||
|
foreach (string extension in SupportedVideoFormats.Extensions)
|
||||||
|
picker.FileTypeFilter.Add(extension);
|
||||||
|
picker.SuggestedStartLocation = PickerLocationId.VideosLibrary;
|
||||||
|
var files = await picker.PickMultipleFilesAsync();
|
||||||
|
if (files is null) return;
|
||||||
|
|
||||||
|
foreach (string path in files.Select(file => file.Path).OrderBy(NaturalKey))
|
||||||
|
if (!_inputs.Contains(path, StringComparer.OrdinalIgnoreCase)) _inputs.Add(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearInputs_Click(object sender, RoutedEventArgs e) => _inputs.Clear();
|
||||||
|
|
||||||
|
private void InputList_DragOver(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
e.AcceptedOperation = e.DataView.Contains(StandardDataFormats.StorageItems)
|
||||||
|
? DataPackageOperation.Copy
|
||||||
|
: DataPackageOperation.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void InputList_Drop(object sender, DragEventArgs e)
|
||||||
|
{
|
||||||
|
if (!e.DataView.Contains(StandardDataFormats.StorageItems)) return;
|
||||||
|
|
||||||
|
DragOperationDeferral deferral = e.GetDeferral();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IReadOnlyList<IStorageItem> items = await e.DataView.GetStorageItemsAsync();
|
||||||
|
foreach (string path in items.OfType<StorageFile>()
|
||||||
|
.Select(file => file.Path)
|
||||||
|
.Where(SupportedVideoFormats.IsSupported)
|
||||||
|
.OrderBy(NaturalKey))
|
||||||
|
if (!_inputs.Contains(path, StringComparer.OrdinalIgnoreCase)) _inputs.Add(path);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
deferral.Complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void BrowseEndCard_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
FileOpenPicker picker = CreateFileOpenPicker();
|
||||||
|
picker.FileTypeFilter.Add(".png");
|
||||||
|
picker.FileTypeFilter.Add(".jpg");
|
||||||
|
picker.FileTypeFilter.Add(".jpeg");
|
||||||
|
picker.FileTypeFilter.Add(".webp");
|
||||||
|
picker.FileTypeFilter.Add(".bmp");
|
||||||
|
var file = await picker.PickSingleFileAsync();
|
||||||
|
if (file is not null)
|
||||||
|
EndCardBox.Text = file.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void BrowseOutput_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
FolderPicker picker = CreateFolderPicker();
|
||||||
|
var folder = await picker.PickSingleFolderAsync();
|
||||||
|
if (folder is not null)
|
||||||
|
OutputFolderBox.Text = folder.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void BrowseFfmpeg_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
FileOpenPicker picker = CreateFileOpenPicker();
|
||||||
|
picker.FileTypeFilter.Add(".exe");
|
||||||
|
var file = await picker.PickSingleFileAsync();
|
||||||
|
if (file is not null)
|
||||||
|
FfmpegPathBox.Text = file.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void BrowseFfprobe_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
FileOpenPicker picker = CreateFileOpenPicker();
|
||||||
|
picker.FileTypeFilter.Add(".exe");
|
||||||
|
var file = await picker.PickSingleFileAsync();
|
||||||
|
if (file is not null)
|
||||||
|
FfprobePathBox.Text = file.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void BrowsePreviewPlayer_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
FileOpenPicker picker = CreateFileOpenPicker();
|
||||||
|
picker.FileTypeFilter.Add(".exe");
|
||||||
|
var file = await picker.PickSingleFileAsync();
|
||||||
|
if (file is not null)
|
||||||
|
PreviewPlayerPathBox.Text = file.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OpenSettings_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
SettingsDialog.XamlRoot = XamlRoot;
|
||||||
|
await SettingsDialog.ShowAsync();
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void Render_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetRendering(true);
|
||||||
|
LogBox.Text = string.Empty;
|
||||||
|
_renderCancellation = new CancellationTokenSource();
|
||||||
|
|
||||||
|
if (_isShortsTab)
|
||||||
|
await RenderShortAsync(_renderCancellation.Token);
|
||||||
|
else
|
||||||
|
await RenderVideoAsync(_renderCancellation.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
UpdateProgress(new(RenderProgressBar.Value, "Cancelled", "The render was cancelled."));
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
AppendLog("ERROR: " + exception);
|
||||||
|
StageText.Text = "Failed";
|
||||||
|
await ShowMessageAsync("Render failed", UserFacingErrors.Summarize(exception));
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_renderCancellation?.Dispose();
|
||||||
|
_renderCancellation = null;
|
||||||
|
SetRendering(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
CancelButton.IsEnabled = false;
|
||||||
|
StatusText.Text = "Stopping FFmpeg...";
|
||||||
|
_renderCancellation?.Cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenOutput_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
string? directory = !string.IsNullOrWhiteSpace(_lastRenderedFile)
|
||||||
|
? 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)
|
||||||
|
{
|
||||||
|
if (InputList.SelectedItem is string path)
|
||||||
|
await OpenPreviewAsync(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void PreviewRendered_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_lastRenderedFile) && File.Exists(_lastRenderedFile))
|
||||||
|
await OpenPreviewAsync(_lastRenderedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThemeBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!IsLoaded) return;
|
||||||
|
string theme = SelectedTheme();
|
||||||
|
ApplyTheme(theme);
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
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>(SelectedEncoder());
|
||||||
|
return new RenderSettings
|
||||||
|
{
|
||||||
|
InputFiles = _inputs.ToList(),
|
||||||
|
EndCardPath = EndCardBox.Text.Trim(),
|
||||||
|
OutputDirectory = OutputFolderBox.Text.Trim(),
|
||||||
|
OutputName = OutputNameBox.Text.Trim(),
|
||||||
|
FfmpegPath = FfmpegPathBox.Text.Trim(),
|
||||||
|
FfprobePath = FfprobePathBox.Text.Trim(),
|
||||||
|
Encoder = encoder,
|
||||||
|
TrimStart = Number(TrimBox.Text, "trim start"),
|
||||||
|
FadeSeconds = Number(FadeBox.Text, "fade"),
|
||||||
|
EndCardHoldSeconds = Number(HoldBox.Text, "end-card hold"),
|
||||||
|
ThumbnailInterval = interval,
|
||||||
|
MoveSourcesToOriginals = MoveSourcesBox.IsChecked == true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReplaceInputs(IReadOnlyList<string> paths)
|
||||||
|
{
|
||||||
|
_inputs.Clear();
|
||||||
|
foreach (string path in paths)
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
RenderButton.IsEnabled = !rendering;
|
||||||
|
CancelButton.IsEnabled = rendering;
|
||||||
|
OpenSettingsButton.IsEnabled = !rendering;
|
||||||
|
VideoTabButton.IsEnabled = !rendering;
|
||||||
|
ShortsTabButton.IsEnabled = !rendering;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateProgress(RenderProgress value)
|
||||||
|
{
|
||||||
|
RenderProgressBar.Value = value.Percent;
|
||||||
|
PercentText.Text = $"{value.Percent:0}%";
|
||||||
|
StageText.Text = value.Stage;
|
||||||
|
StatusText.Text = value.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenPreviewAsync(string mediaPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(mediaPath))
|
||||||
|
{
|
||||||
|
await ShowMessageAsync("Preview unavailable", "The selected media file was not found.");
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
await ShowMessageAsync("Preview failed", exception.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendLog(string line)
|
||||||
|
{
|
||||||
|
lock (_logLock)
|
||||||
|
{
|
||||||
|
_pendingLog.AppendLine(line);
|
||||||
|
if (_logFlushScheduled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_logFlushScheduled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = DispatcherQueue.TryEnqueue(() =>
|
||||||
|
{
|
||||||
|
string chunk;
|
||||||
|
lock (_logLock)
|
||||||
|
{
|
||||||
|
chunk = _pendingLog.ToString();
|
||||||
|
_pendingLog.Clear();
|
||||||
|
_logFlushScheduled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunk.Length == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
LogBox.Text += chunk;
|
||||||
|
LogBox.Select(LogBox.Text.Length, 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveSettings()
|
||||||
|
{
|
||||||
|
AppSettingsStore.Save(new AppSettings
|
||||||
|
{
|
||||||
|
OutputFolder = OutputFolderBox.Text.Trim(),
|
||||||
|
EndCardPath = EndCardBox.Text.Trim(),
|
||||||
|
FfmpegPath = FfmpegPathBox.Text.Trim(),
|
||||||
|
FfprobePath = FfprobePathBox.Text.Trim(),
|
||||||
|
PreviewPlayerPath = PreviewPlayerPathBox.Text.Trim(),
|
||||||
|
Theme = SelectedTheme(),
|
||||||
|
Encoder = SelectedEncoder(),
|
||||||
|
TrimStart = TrimBox.Text.Trim(),
|
||||||
|
FadeSeconds = FadeBox.Text.Trim(),
|
||||||
|
EndCardHoldSeconds = HoldBox.Text.Trim(),
|
||||||
|
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 SelectedEncoder() => (EncoderBox.SelectedItem as ComboBoxItem)?.Tag?.ToString() ?? "Auto";
|
||||||
|
|
||||||
|
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 comboBox.Items.Cast<ComboBoxItem>())
|
||||||
|
{
|
||||||
|
if (string.Equals(item.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
comboBox.SelectedItem = item;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
comboBox.SelectedIndex = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InputList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
PreviewSourceButton.IsEnabled = InputList.SelectedItem is string;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyTheme(string theme)
|
||||||
|
{
|
||||||
|
bool isDark = !string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase);
|
||||||
|
App.MainWindowInstance?.ApplyTheme(isDark);
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileOpenPicker CreateFileOpenPicker()
|
||||||
|
{
|
||||||
|
FileOpenPicker picker = new();
|
||||||
|
InitializeWithWindow.Initialize(picker, App.MainWindowHandle);
|
||||||
|
return picker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private FolderPicker CreateFolderPicker()
|
||||||
|
{
|
||||||
|
FolderPicker picker = new();
|
||||||
|
picker.FileTypeFilter.Add("*");
|
||||||
|
InitializeWithWindow.Initialize(picker, App.MainWindowHandle);
|
||||||
|
return picker;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 async Task ShowMessageAsync(string title, string message)
|
||||||
|
{
|
||||||
|
ContentDialog dialog = DialogHelper.CreateStyled(XamlRoot, ActualTheme);
|
||||||
|
dialog.Title = title;
|
||||||
|
dialog.Content = message;
|
||||||
|
dialog.CloseButtonText = "OK";
|
||||||
|
await dialog.ShowAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,206 +1,38 @@
|
|||||||
<Window x:Class="AmigaDB.VideoRenderer.MainWindow"
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<Window
|
||||||
|
x:Class="AmiReel.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="AmiReel" Height="860" Width="1280" MinHeight="780" MinWidth="1100"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
WindowStartupLocation="CenterScreen">
|
xmlns:local="using:AmiReel"
|
||||||
<Grid Background="{DynamicResource PageBrush}">
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
<Grid>
|
Title="AmiReel"
|
||||||
<Grid.Background>
|
mc:Ignorable="d">
|
||||||
<DrawingBrush Stretch="None" Viewport="0,0,36,36" ViewportUnits="Absolute" Opacity="0.08">
|
<Grid Background="{ThemeResource PageBrush}">
|
||||||
<DrawingBrush.Drawing>
|
|
||||||
<GeometryDrawing Brush="{DynamicResource PageOverlayBrush}">
|
|
||||||
<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 AVI files…" Click="AddInputs_Click" Margin="0,0,8,0"/>
|
|
||||||
<Button Content="Clear" Click="ClearInputs_Click"/>
|
|
||||||
</StackPanel>
|
|
||||||
<ListBox x:Name="InputList" Grid.Row="1" SelectionChanged="InputList_SelectionChanged"/>
|
|
||||||
<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>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
<RowDefinition Height="Auto"/>
|
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<TextBlock Text="Settings" FontSize="24" FontWeight="SemiBold" Margin="0,0,0,14"/>
|
<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 x:Name="TitleBarIcon"
|
||||||
|
Width="20"
|
||||||
|
Height="20"
|
||||||
|
Stretch="Uniform" />
|
||||||
|
<TextBlock Text="AmiReel"
|
||||||
|
FontFamily="Bahnschrift"
|
||||||
|
FontSize="14"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
<!--
|
||||||
<StackPanel>
|
The Frame hosts pages for your application content. Add your UI to
|
||||||
<GroupBox Header="Appearance">
|
MainPage.xaml rather than here so you can use Page features such as
|
||||||
<StackPanel>
|
navigation events and the Loaded lifecycle.
|
||||||
<TextBlock Text="Theme" Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,4"/>
|
-->
|
||||||
<ComboBox x:Name="ThemeBox" SelectionChanged="ThemeBox_SelectionChanged">
|
<Frame x:Name="RootFrame" Grid.Row="1" />
|
||||||
<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="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>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
@@ -1,341 +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.Text;
|
using System.Reflection;
|
||||||
using System.Windows;
|
using System.Runtime.InteropServices;
|
||||||
using System.Windows.Controls;
|
using System.Runtime.InteropServices.WindowsRuntime;
|
||||||
using AmigaDB.VideoRenderer.Models;
|
using Windows.Graphics;
|
||||||
using AmigaDB.VideoRenderer.Services;
|
using Windows.UI;
|
||||||
using Microsoft.Win32;
|
using WinRT.Interop;
|
||||||
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 bool _exitConfirmed;
|
||||||
private readonly AppSettings _loadedSettings;
|
|
||||||
private readonly StringBuilder _pendingLog = new();
|
|
||||||
private readonly object _logLock = new();
|
|
||||||
private CancellationTokenSource? _renderCancellation;
|
|
||||||
private string? _lastRenderedFile;
|
|
||||||
private bool _logFlushScheduled;
|
|
||||||
|
|
||||||
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);
|
|
||||||
Closing += (_, _) => SaveSettings();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddInputs_Click(object sender, RoutedEventArgs e)
|
/// <summary>
|
||||||
{
|
/// Sets the window's taskbar/Alt+Tab icon. <see cref="AppWindow.SetIcon(string)"/> requires an
|
||||||
OpenFileDialog dialog = new() { Filter = "AVI recordings (*.avi)|*.avi", Multiselect = true };
|
/// actual .ico file path (an .exe path is silently ignored), so the embedded .ico is extracted
|
||||||
if (dialog.ShowDialog(this) != true) return;
|
/// once to a stable file under %LOCALAPPDATA% and that path is used instead.
|
||||||
foreach (string file in dialog.FileNames.OrderBy(NaturalKey))
|
/// </summary>
|
||||||
if (!_inputs.Contains(file, StringComparer.OrdinalIgnoreCase)) _inputs.Add(file);
|
private void SetWindowIcon()
|
||||||
}
|
|
||||||
|
|
||||||
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
|
try
|
||||||
{
|
{
|
||||||
RenderSettings settings = ReadSettings();
|
string iconPath = Path.Combine(
|
||||||
SaveSettings();
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||||
SetRendering(true);
|
"AmiReel", "app-icon.ico");
|
||||||
LogBox.Clear();
|
|
||||||
_renderCancellation = new CancellationTokenSource();
|
if (!File.Exists(iconPath))
|
||||||
Progress<RenderProgress> progress = new(UpdateProgress);
|
|
||||||
await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
|
|
||||||
_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."));
|
Directory.CreateDirectory(Path.GetDirectoryName(iconPath)!);
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
using Stream? resource = assembly.GetManifestResourceStream("AmiReel.Assets.AppIcon.ico");
|
||||||
|
if (resource is null) return;
|
||||||
|
|
||||||
|
using FileStream file = File.Create(iconPath);
|
||||||
|
resource.CopyTo(file);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
|
||||||
{
|
AppWindow.SetIcon(iconPath);
|
||||||
AppendLog("ERROR: " + exception);
|
|
||||||
MessageBox.Show(this, UserFacingErrors.Summarize(exception), "Render failed", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
StageText.Text = "Failed";
|
|
||||||
}
|
}
|
||||||
finally
|
catch
|
||||||
{
|
{
|
||||||
_renderCancellation?.Dispose();
|
// Non-critical: the window simply keeps whatever default icon Windows assigned.
|
||||||
_renderCancellation = null;
|
|
||||||
SetRendering(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderSettings ReadSettings()
|
/// <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()
|
||||||
{
|
{
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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()));
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
try
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(playerPath))
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
{
|
using Stream? stream = assembly.GetManifestResourceStream("AmiReel.Assets.TitleBarIcon.png");
|
||||||
if (!File.Exists(playerPath))
|
if (stream is null) return;
|
||||||
throw new FileNotFoundException("Configured preview player was not found.", playerPath);
|
|
||||||
|
|
||||||
ProcessStartInfo customPlayer = new()
|
BitmapImage bitmap = new();
|
||||||
{
|
await bitmap.SetSourceAsync(stream.AsRandomAccessStream());
|
||||||
FileName = playerPath,
|
TitleBarIcon.Source = bitmap;
|
||||||
UseShellExecute = false
|
|
||||||
};
|
|
||||||
customPlayer.ArgumentList.Add(mediaPath);
|
|
||||||
Process.Start(customPlayer);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
catch
|
||||||
Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true });
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
{
|
||||||
MessageBox.Show(this, exception.Message, "Preview failed", MessageBoxButton.OK, MessageBoxImage.Error);
|
// Non-critical: the title bar simply shows no icon if this fails.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SelectedTheme() => ((ComboBoxItem)ThemeBox.SelectedItem).Tag!.ToString()!;
|
private void SetDefaultSizeAndPosition()
|
||||||
private string SelectedEncoder() => ((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!;
|
{
|
||||||
|
const int DefaultWidth = 1280;
|
||||||
|
const int DefaultHeight = 860;
|
||||||
|
const int MinWidth = 1100;
|
||||||
|
const int MinHeight = 780;
|
||||||
|
|
||||||
private void SelectTheme(string theme)
|
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)
|
||||||
{
|
{
|
||||||
foreach (ComboBoxItem item in ThemeBox.Items)
|
presenter.PreferredMinimumWidth = (int)(MinWidth * scale);
|
||||||
|
presenter.PreferredMinimumHeight = (int)(MinHeight * scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayArea? displayArea = DisplayArea.GetFromWindowId(Win32Interop.GetWindowIdFromWindow(hwnd), DisplayAreaFallback.Primary);
|
||||||
|
if (displayArea is not null)
|
||||||
{
|
{
|
||||||
if (string.Equals(item.Tag?.ToString(), theme, StringComparison.OrdinalIgnoreCase))
|
int centerX = displayArea.WorkArea.X + (displayArea.WorkArea.Width - width) / 2;
|
||||||
{
|
int centerY = displayArea.WorkArea.Y + (displayArea.WorkArea.Height - height) / 2;
|
||||||
ThemeBox.SelectedItem = item;
|
AppWindow.Move(new PointInt32(centerX, centerY));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ThemeBox.SelectedIndex = 0;
|
[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 void SelectEncoder(string encoder)
|
private async void AppWindow_Closing(AppWindow sender, AppWindowClosingEventArgs args)
|
||||||
{
|
{
|
||||||
foreach (ComboBoxItem item in EncoderBox.Items)
|
if (_exitConfirmed) return;
|
||||||
{
|
args.Cancel = true;
|
||||||
if (string.Equals(item.Tag?.ToString(), encoder, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
EncoderBox.SelectedItem = item;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EncoderBox.SelectedIndex = 0;
|
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;
|
||||||
|
|
||||||
private void ApplyTheme(string theme)
|
ContentDialogResult result = await dialog.ShowAsync();
|
||||||
{
|
if (result != ContentDialogResult.Primary) return;
|
||||||
bool light = string.Equals(theme, "Light", StringComparison.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
SetBrush("PageBrush", light ? "#F4F7FB" : "#0E1525");
|
_exitConfirmed = true;
|
||||||
SetBrush("PanelBrush", light ? "#FFFFFF" : "#162033");
|
Close();
|
||||||
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");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetBrush(string key, string color)
|
|
||||||
{
|
|
||||||
Application.Current.Resources[key] = new SolidColorBrush((Color)ColorConverter.ConvertFromString(color));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +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)
|
|
||||||
{
|
{
|
||||||
public static AppSettings Default(string outputFolder) => new(
|
public required string OutputFolder { get; init; }
|
||||||
outputFolder,
|
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; }
|
||||||
"Dark",
|
public required string Encoder { get; init; }
|
||||||
"Auto",
|
public required string TrimStart { get; init; }
|
||||||
"4.414",
|
public required string FadeSeconds { get; init; }
|
||||||
"3",
|
public required string EndCardHoldSeconds { get; init; }
|
||||||
"4",
|
public required string ThumbnailInterval { get; init; }
|
||||||
"10");
|
public bool? MoveSourcesToOriginals { get; init; }
|
||||||
|
|
||||||
public AppSettings Normalize(string fallbackOutputFolder) => new(
|
// Shorts tab defaults (sticky across launches, same as the fields above).
|
||||||
string.IsNullOrWhiteSpace(OutputFolder) ? fallbackOutputFolder : OutputFolder,
|
public string ShortsStyle { get; init; } = "Brand";
|
||||||
EndCardPath ?? "",
|
public string ShortsHook { get; init; } = "THIS RAN ON AN AMIGA";
|
||||||
FfmpegPath ?? "",
|
public string ShortsWebsite { get; init; } = "AMIGADB.NET";
|
||||||
FfprobePath ?? "",
|
public string ShortsFontFile { get; init; } = "";
|
||||||
PreviewPlayerPath ?? "",
|
public string ShortsBackgroundImage { get; init; } = "";
|
||||||
string.IsNullOrWhiteSpace(Theme) ? "Dark" : Theme,
|
public string ShortsCrf { get; init; } = "16";
|
||||||
string.IsNullOrWhiteSpace(Encoder) ? "Auto" : Encoder,
|
public string ShortsPreset { get; init; } = "slow";
|
||||||
string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
|
|
||||||
string.IsNullOrWhiteSpace(FadeSeconds) ? "3" : FadeSeconds,
|
public bool ShouldMoveSourcesToOriginals => MoveSourcesToOriginals != false;
|
||||||
string.IsNullOrWhiteSpace(EndCardHoldSeconds) ? "4" : EndCardHoldSeconds,
|
|
||||||
string.IsNullOrWhiteSpace(ThumbnailInterval) ? "10" : ThumbnailInterval);
|
public static AppSettings Default(string outputFolder) => new()
|
||||||
|
{
|
||||||
|
OutputFolder = outputFolder,
|
||||||
|
EndCardPath = "",
|
||||||
|
FfmpegPath = "",
|
||||||
|
FfprobePath = "",
|
||||||
|
PreviewPlayerPath = "",
|
||||||
|
Theme = "Dark",
|
||||||
|
Encoder = "Auto",
|
||||||
|
TrimStart = "4.414",
|
||||||
|
FadeSeconds = "3",
|
||||||
|
EndCardHoldSeconds = "4",
|
||||||
|
ThumbnailInterval = "10",
|
||||||
|
MoveSourcesToOriginals = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
public AppSettings Normalize(string fallbackOutputFolder) => this with
|
||||||
|
{
|
||||||
|
OutputFolder = string.IsNullOrWhiteSpace(OutputFolder) ? fallbackOutputFolder : OutputFolder,
|
||||||
|
EndCardPath = EndCardPath ?? "",
|
||||||
|
FfmpegPath = FfmpegPath ?? "",
|
||||||
|
FfprobePath = FfprobePath ?? "",
|
||||||
|
PreviewPlayerPath = PreviewPlayerPath ?? "",
|
||||||
|
Theme = string.IsNullOrWhiteSpace(Theme) ? "Dark" : Theme,
|
||||||
|
Encoder = string.IsNullOrWhiteSpace(Encoder) ? "Auto" : Encoder,
|
||||||
|
TrimStart = string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
|
||||||
|
FadeSeconds = string.IsNullOrWhiteSpace(FadeSeconds) ? "3" : FadeSeconds,
|
||||||
|
EndCardHoldSeconds = string.IsNullOrWhiteSpace(EndCardHoldSeconds) ? "4" : EndCardHoldSeconds,
|
||||||
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
namespace AmigaDB.VideoRenderer.Models;
|
namespace AmiReel.Models;
|
||||||
|
|
||||||
public enum EncoderMode
|
public enum EncoderMode
|
||||||
{
|
{
|
||||||
@@ -7,22 +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; }
|
||||||
int Width = 3840,
|
public required string OutputDirectory { get; init; }
|
||||||
int Height = 2160,
|
public required string OutputName { get; init; }
|
||||||
int FramesPerSecond = 50,
|
public required string FfmpegPath { get; init; }
|
||||||
int ThumbnailWidth = 1280,
|
public required string FfprobePath { get; init; }
|
||||||
int ThumbnailHeight = 720);
|
public required EncoderMode Encoder { get; init; }
|
||||||
|
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);
|
||||||
|
|||||||
@@ -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";
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace AmiReel.Models;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Video file extensions accepted as render sources. FFmpeg can demux far more than this,
|
||||||
|
/// but this list covers what a screen/video capture workflow is realistically going to produce.
|
||||||
|
/// </summary>
|
||||||
|
public static class SupportedVideoFormats
|
||||||
|
{
|
||||||
|
public static readonly IReadOnlyList<string> Extensions =
|
||||||
|
[
|
||||||
|
".avi", ".mp4", ".m4v", ".mov", ".mkv", ".webm",
|
||||||
|
".wmv", ".flv", ".mpg", ".mpeg", ".ts", ".3gp"
|
||||||
|
];
|
||||||
|
|
||||||
|
public static bool IsSupported(string path) =>
|
||||||
|
Extensions.Contains(Path.GetExtension(path), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
/// <summary>Picker filter string, e.g. "*.avi;*.mp4;*.m4v;...".</summary>
|
||||||
|
public static string PickerFilter => string.Join(';', Extensions.Select(e => "*" + e));
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
@@ -37,17 +36,16 @@
|
|||||||
<uap:VisualElements
|
<uap:VisualElements
|
||||||
DisplayName="AmiReel"
|
DisplayName="AmiReel"
|
||||||
Description="AmiReel"
|
Description="AmiReel"
|
||||||
BackgroundColor="transparent"
|
BackgroundColor="#0E1525"
|
||||||
Square150x150Logo="Assets\Square150x150Logo.png"
|
Square150x150Logo="Assets\Square150x150Logo.png"
|
||||||
Square44x44Logo="Assets\Square44x44Logo.png">
|
Square44x44Logo="Assets\Square44x44Logo.png">
|
||||||
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
|
||||||
<uap:SplashScreen Image="Assets\SplashScreen.png" />
|
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="#0E1525" />
|
||||||
</uap:VisualElements>
|
</uap:VisualElements>
|
||||||
</Application>
|
</Application>
|
||||||
</Applications>
|
</Applications>
|
||||||
|
|
||||||
<Capabilities>
|
<Capabilities>
|
||||||
<rescap:Capability Name="runFullTrust" />
|
<rescap:Capability Name="runFullTrust" />
|
||||||
<systemai:Capability Name="systemAIModels"/>
|
|
||||||
</Capabilities>
|
</Capabilities>
|
||||||
</Package>
|
</Package>
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,110 +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 custom `ffmpeg.exe` / `ffprobe.exe` paths
|
## Project Layout
|
||||||
- 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
|
||||||
@@ -112,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`.
|
||||||
|
|
||||||
@@ -159,40 +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
|
|
||||||
|
|
||||||
## 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.
|
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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));
|
||||||
|
}
|
||||||
@@ -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,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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
@@ -15,7 +15,7 @@ public sealed class RenderPipeline
|
|||||||
_probe = new MediaProbe(_runner, null);
|
_probe = new MediaProbe(_runner, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RenderAsync(
|
public async Task<IReadOnlyList<string>> RenderAsync(
|
||||||
RenderSettings settings,
|
RenderSettings settings,
|
||||||
IProgress<RenderProgress> progress,
|
IProgress<RenderProgress> progress,
|
||||||
Action<string> log,
|
Action<string> log,
|
||||||
@@ -105,7 +105,16 @@ public sealed class RenderPipeline
|
|||||||
finalArgs.AddRange(EncoderArguments(encoder));
|
finalArgs.AddRange(EncoderArguments(encoder));
|
||||||
finalArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-movflags", "+faststart", final]);
|
finalArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-movflags", "+faststart", final]);
|
||||||
await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, settings.FramesPerSecond, token);
|
await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, settings.FramesPerSecond, token);
|
||||||
|
|
||||||
|
IReadOnlyList<string> inputPaths = settings.InputFiles;
|
||||||
|
if (settings.MoveSourcesToOriginals)
|
||||||
|
{
|
||||||
|
progress.Report(new(99.5, "Moving sources", "Moving original recordings to originals"));
|
||||||
|
inputPaths = MoveSourceVideos(settings.InputFiles, settings.OutputDirectory, log);
|
||||||
|
}
|
||||||
|
|
||||||
progress.Report(new(100, "Complete", final));
|
progress.Report(new(100, "Complete", final));
|
||||||
|
return inputPaths;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -246,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}"];
|
||||||
@@ -285,10 +294,74 @@ public sealed class RenderPipeline
|
|||||||
return fallbackPath;
|
return fallbackPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Validate(RenderSettings s)
|
internal static IReadOnlyList<string> MoveSourceVideos(
|
||||||
|
IReadOnlyList<string> sources,
|
||||||
|
string outputDirectory,
|
||||||
|
Action<string> log)
|
||||||
{
|
{
|
||||||
if (s.InputFiles.Count == 0) throw new ArgumentException("Select at least one AVI input file.");
|
string originalsDir = Path.Combine(outputDirectory, "originals");
|
||||||
if (s.InputFiles.Any(f => !File.Exists(f))) throw new FileNotFoundException("One or more AVI files no longer exist.");
|
Directory.CreateDirectory(originalsDir);
|
||||||
|
List<string> resolved = new(sources.Count);
|
||||||
|
HashSet<string> claimed = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (string source in sources)
|
||||||
|
{
|
||||||
|
if (!File.Exists(source))
|
||||||
|
{
|
||||||
|
log($"WARNING: Source file no longer exists, skipped move: {source}");
|
||||||
|
resolved.Add(source);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string sourceFull = Path.GetFullPath(source);
|
||||||
|
string preferred = Path.GetFullPath(Path.Combine(originalsDir, Path.GetFileName(source)));
|
||||||
|
if (string.Equals(sourceFull, preferred, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
claimed.Add(preferred);
|
||||||
|
resolved.Add(preferred);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string dest = UniqueDestination(preferred, claimed);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
File.Move(source, dest);
|
||||||
|
claimed.Add(dest);
|
||||||
|
log($"Moved source to {dest}");
|
||||||
|
resolved.Add(dest);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
log($"WARNING: Could not move source '{source}' to originals: {exception.Message}");
|
||||||
|
resolved.Add(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string UniqueDestination(string dest, HashSet<string> claimed)
|
||||||
|
{
|
||||||
|
string full = Path.GetFullPath(dest);
|
||||||
|
if (!File.Exists(full) && !claimed.Contains(full))
|
||||||
|
return full;
|
||||||
|
|
||||||
|
string directory = Path.GetDirectoryName(full)!;
|
||||||
|
string name = Path.GetFileNameWithoutExtension(full);
|
||||||
|
string extension = Path.GetExtension(full);
|
||||||
|
for (int index = 2; ; index++)
|
||||||
|
{
|
||||||
|
string candidate = Path.Combine(directory, $"{name}_{index}{extension}");
|
||||||
|
if (!File.Exists(candidate) && !claimed.Contains(candidate))
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void Validate(RenderSettings s)
|
||||||
|
{
|
||||||
|
if (s.InputFiles.Count == 0) throw new ArgumentException("Select at least one video input file.");
|
||||||
|
if (s.InputFiles.Any(f => !File.Exists(f))) throw new FileNotFoundException("One or more input files no longer exist.");
|
||||||
if (!string.IsNullOrWhiteSpace(s.EndCardPath) && !File.Exists(s.EndCardPath))
|
if (!string.IsNullOrWhiteSpace(s.EndCardPath) && !File.Exists(s.EndCardPath))
|
||||||
throw new FileNotFoundException("End-card image was not found.");
|
throw new FileNotFoundException("End-card image was not found.");
|
||||||
if (string.IsNullOrWhiteSpace(s.OutputDirectory)) throw new ArgumentException("Select an output directory.");
|
if (string.IsNullOrWhiteSpace(s.OutputDirectory)) throw new ArgumentException("Select an output directory.");
|
||||||
|
|||||||
@@ -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,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.");
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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 |
@@ -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 |
@@ -1,8 +1,5 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param()
|
||||||
[ValidateSet('WinUI', 'Wpf')]
|
|
||||||
[string]$Target = 'WinUI'
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
@@ -14,10 +11,8 @@ 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'
|
|
||||||
$releaseDir = Join-Path $projectDir 'AmiReel.WinUI\bin\Release\net10.0-windows10.0.26100.0\win-x64'
|
|
||||||
$releaseExe = Join-Path $releaseDir 'AmiReel.exe'
|
$releaseExe = Join-Path $releaseDir 'AmiReel.exe'
|
||||||
$publishDir = Join-Path $releaseDir 'publish'
|
$publishDir = Join-Path $releaseDir 'publish'
|
||||||
$publishExe = Join-Path $publishDir 'AmiReel.exe'
|
$publishExe = Join-Path $publishDir 'AmiReel.exe'
|
||||||
@@ -31,24 +26,6 @@ switch ($Target) {
|
|||||||
'-p:IncludeNativeLibrariesForSelfExtract=true',
|
'-p:IncludeNativeLibrariesForSelfExtract=true',
|
||||||
'-p:EnableCompressionInSingleFile=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
|
||||||
|
|||||||