Polish AmiReel UI and improve render workflow

This commit is contained in:
2026-08-11 13:55:44 +02:00
parent ad205bb534
commit d62ae0b9fb
38 changed files with 2711 additions and 183 deletions
@@ -0,0 +1,62 @@
---
description: 'Accessibility requirements for interactive controls, keyboard navigation, screen readers, and contrast'
applyTo: '**/*.cs, **/*.xaml'
---
# Accessibility
These rules apply to **every UI change**. They are not optional add-ons.
---
## Rules
- **Every interactive control** must have an `AutomationProperties.Name` or `AutomationProperties.LabeledBy`.
- Add a stable, unique `AutomationProperties.AutomationId` for controls targeted by UI automation tests (and for key interactive elements).
- Use semantic XAML controls — prefer `Button`, `HyperlinkButton`, `ListView` over styled `Border`/`Grid` with click handlers.
- Ensure **keyboard navigation** works for all features:
- Logical tab order via `TabIndex`.
- `AccessKey` bindings for frequently used actions.
- `KeyboardAccelerator` for shortcut keys.
- Maintain **minimum contrast ratios** (4.5:1 for normal text, 3:1 for large text) — test in High Contrast mode.
- Support **screen readers** (Narrator / NVDA): test that all content is announced correctly.
- Images must have `AutomationProperties.Name` describing the image purpose (or `AutomationProperties.AccessibilityView="Raw"` for decorative images).
- Do not rely on colour alone to convey meaning — add icons, text, or patterns.
## Anti-patterns
- Clickable `TextBlock` or `Image` without `AutomationProperties`.
- Custom controls that are not keyboard-focusable.
- Using `Visibility.Collapsed` to "hide" content from screen readers (use `AccessibilityView` instead).
## Validation
- Build & register the MSIX package — see **Build, Run & Deploy** in `.github/agents/Agents.md`.
- Test keyboard navigation: tab through every new/changed UI area.
- Test High Contrast: switch to Windows High Contrast theme and verify readability.
- Run Accessibility Insights for Windows on the app.
### Verification Checklist
- [ ] All interactive controls have `AutomationProperties.Name`
- [ ] Keyboard navigation works for the changed area
- [ ] Tested with High Contrast theme enabled
- [ ] Tab through the entire UI with keyboard only.
- [ ] Verify key interactive controls have stable, unique `AutomationProperties.AutomationId` values (especially controls used by UI automation tests).
- [ ] Switch to Windows High Contrast theme and verify readability.
- [ ] Run Narrator and verify all controls are announced correctly.
- [ ] Run **Accessibility Insights for Windows** on the app.
## Must Read & Research
> **Agent Rule:** Before any accessibility-related change, you **must** fetch and review these references using `fetch_webpage`. Apply what you learn.
| # | Reference | When to consult |
|---|---|---|
| 1 | [Accessibility in WinUI](https://learn.microsoft.com/en-us/windows/apps/design/accessibility/accessibility) | Any UI change — verify accessibility approach |
| 2 | [AutomationProperties](https://learn.microsoft.com/en-us/windows/apps/design/accessibility/basic-accessibility-information) | Adding or modifying interactive controls |
| 3 | [Accessibility Insights](https://accessibilityinsights.io/docs/windows/overview/) | Testing tool — run before finalizing UI changes |
| 4 | [Keyboard Accessibility](https://learn.microsoft.com/en-us/windows/apps/design/accessibility/keyboard-accessibility) | Adding navigation, focus management, or shortcut keys |
| 5 | [High Contrast Themes](https://learn.microsoft.com/en-us/windows/apps/design/accessibility/high-contrast-themes) | Adding custom styles, colours, or theme resources |
@@ -0,0 +1,170 @@
---
description: 'Static analysis, StyleCop, EditorConfig, naming conventions, and code cleanup rules'
applyTo: '**/*.cs, **/*.editorconfig, **/stylecop.json'
---
# Code Quality — Static Analysis, StyleCop & Code Cleanup
Maintain strict code quality through automated analysis and consistent style enforcement.
---
## 1. Static Analysis (Roslyn Analyzers)
### Required Analyzer Packages
Add these to the `.csproj` if not already present:
```xml
<ItemGroup>
<!-- Use the latest stable versions; do not hard-code version numbers in instructions -->
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="*" />
<PackageReference Include="StyleCop.Analyzers" Version="*">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
```
### Analysis Configuration in `.csproj`
```xml
<PropertyGroup>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
<Nullable>enable</Nullable>
</PropertyGroup>
```
### Rule Enforcement
Follow **all** CA* (quality) and IDE* (code style) analyzer rules at their configured severity. Do not cherry-pick — obey every warning the analyzers report. When encountering a specific rule violation, fetch the corresponding documentation from the **Must Read & Research** references below to understand and apply the correct fix.
### `.editorconfig`
The project's `.editorconfig` in the solution root is the source of truth for code style. Obey all rules defined there. When creating or modifying `.editorconfig`, fetch the [EditorConfig Reference](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options) for the full list of available settings.
Key project conventions enforced via `.editorconfig`:
- Private fields use `_camelCase` prefix (SA1101 suppressed, SA1309 suppressed).
- File-scoped namespaces are required.
- `this.` qualification is not used.
---
## 2. StyleCop Rules
### StyleCop Configuration (`stylecop.json`)
Place this file in the project root alongside the `.csproj`:
```json
{
"$schema": "https://raw.githubusercontent.com/DotNetAnalyzers/StyleCopAnalyzers/master/StyleCop.Analyzers/StyleCop.Analyzers/Settings/stylecop.schema.json",
"settings": {
"documentationRules": {
"companyName": "YourProjectName",
"copyrightText": "Copyright (c) {companyName}. All rights reserved.",
"xmlHeader": false,
"documentInterfaces": true,
"documentExposedElements": true,
"documentInternalElements": false,
"documentPrivateElements": false,
"documentPrivateFields": false
},
"orderingRules": {
"usingDirectivesPlacement": "outsideNamespace",
"systemUsingDirectivesFirst": true
},
"layoutRules": {
"newlineAtEndOfFile": "require"
},
"namingRules": {
"allowCommonHungarianPrefixes": false
}
}
}
```
### Rule Enforcement
Follow **all** SA* (StyleCop) rules at their configured severity. When encountering a specific SA* violation, fetch the [StyleCop Rules Reference](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/DOCUMENTATION.md) to understand the rule and apply the correct fix. Do not suppress rules without justification in a code comment.
---
## 3. Code Cleanup Rules (Always Enforced)
### After Every Edit
1. **Remove unused `using` statements** — No unused imports should remain.
2. **Remove commented-out code** — Version control tracks history; dead code is noise.
3. **Remove unused variables and fields** — If it's declared but never read, delete it.
4. **Remove empty methods** — If an event handler or override does nothing, remove it.
5. **Simplify code** — Apply IDE suggestions (IDE0001IDE0090) for:
- Removing unnecessary casts
- Simplifying `default` expressions
- Using pattern matching
- Using null-coalescing operators
### Naming Conventions
| Element | Convention | Example |
|---|---|---|
| Class / Struct | PascalCase | `MainViewModel` |
| Interface | I + PascalCase | `INavigationService` |
| Public method | PascalCase | `LoadDataAsync()` |
| Private method | PascalCase | `ValidateInput()` |
| Public property | PascalCase | `CurrentPage` |
| Private field | _camelCase | `_settingsService` |
| Parameter | camelCase | `userName` |
| Local variable | camelCase | `itemCount` |
| Constant | PascalCase | `MaxRetryCount` |
| Async method | Suffix `Async` | `FetchDataAsync()` |
| Boolean | Prefix `Is/Has/Can` | `IsLoading`, `HasAccess` |
### File Organization
Each `.cs` file should follow this order:
1. `using` directives (System first, then others, alphabetically)
2. Namespace declaration (file-scoped)
3. Class/struct/interface declaration
4. Inside the type:
1. Constants
2. Static fields
3. Instance fields
4. Constructors
5. Properties
6. Public methods
7. Private/internal methods
8. Event handlers
9. Nested types
---
## Validation
- Build & register the MSIX package — see **Build, Run & Deploy** in `.github/agents/Agents.md`.
- Fix **all** warnings — do not suppress without justification in a code comment.
- Verify no unused `using` statements remain after every edit.
- Verify no commented-out code remains.
- Confirm naming conventions match the table above for every new symbol.
---
## Must Read & Research
> **Agent Rule:** Before configuring analyzers, fixing warnings, or adjusting code style, you **must** fetch and review the relevant references below using `fetch_webpage`. Apply what you learn — do not skip this step.
| # | Reference | When to consult |
|---|---|---|
| 1 | [.NET Code Analysis Overview](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/overview) | Setting up or modifying analyzer configuration |
| 2 | [Code Style Rules (IDE0001IDE0090)](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/) | Resolving IDE* warnings or adjusting `.editorconfig` |
| 3 | [Quality Rules (CA*)](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/) | Resolving CA* warnings or suppressing with justification |
| 4 | [StyleCop.Analyzers GitHub](https://github.com/DotNetAnalyzers/StyleCopAnalyzers) | Adding/updating StyleCop package or configuration |
| 5 | [StyleCop Rules Reference](https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/DOCUMENTATION.md) | Understanding specific SA* rule violations |
| 6 | [EditorConfig Reference](https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/code-style-rule-options) | Modifying `.editorconfig` style or severity settings |
| 7 | [.NET Naming Conventions](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/identifier-names) | Verifying naming patterns for types, members, parameters |
@@ -0,0 +1,134 @@
---
description: 'Design principles (DRY, KISS, SOLID, YAGNI) enforced in every code change'
applyTo: '**/*.cs, **/*.xaml'
---
# Design Principles
Apply these principles in **every change** you make to this codebase. When in doubt, favour simplicity and clarity over cleverness.
---
## 1. DRY — Don't Repeat Yourself
> *"Every piece of knowledge must have a single, unambiguous, authoritative representation within a system."*
### Rules
- Before writing new code, **search** the codebase for existing implementations that solve the same problem.
- Extract shared logic into helper methods, base classes, or services.
- If you find duplicated code during a task, **refactor it** as part of the same change.
- Prefer generic/reusable components over copy-paste variations.
### Anti-patterns to avoid
- Copy-pasting code between classes/files instead of extracting a shared method.
- Creating multiple converters/helpers that do the same thing.
- Duplicating validation logic across ViewModel and Model layers.
---
## 2. KISS — Keep It Simple, Stupid
> *"Simplicity is the ultimate sophistication."*
### Rules
- Choose the **simplest approach** that meets the requirement.
- Avoid unnecessary abstractions, inheritance hierarchies, or patterns that add complexity without clear benefit.
- Write code that **reads like plain English** — favour descriptive names over comments.
- If a method is longer than ~30 lines, consider splitting it.
- If a class does more than one thing, split it (see SRP below).
### Anti-patterns to avoid
- Over-engineering with factories/builders/strategies for simple object creation.
- Creating deep inheritance trees when composition would suffice.
- Using complex LINQ chains when a simple `foreach` is clearer.
---
## 3. SOLID Principles
### 3.1 SRP — Single Responsibility Principle
> *"A class should have only one reason to change."*
- Each class/file should have **one clear responsibility**.
- ViewModels handle UI state & commands. Services handle business logic. Models hold data.
- If a class name contains "And" or "Manager", it likely violates SRP.
### 3.2 OCP — Open/Closed Principle
> *"Software entities should be open for extension, but closed for modification."*
- Use interfaces and abstract classes so behaviour can be extended without modifying existing code.
- Prefer adding new implementations over modifying existing ones.
- Use dependency injection to swap implementations.
### 3.3 LSP — Liskov Substitution Principle
> *"Objects of a superclass should be replaceable with objects of its subclasses without altering correctness."*
- Derived classes must honour the contracts of their base classes.
- Never throw `NotImplementedException` in overridden methods — if a subclass can't fulfil the contract, the inheritance is wrong.
### 3.4 ISP — Interface Segregation Principle
> *"No client should be forced to depend on methods it does not use."*
- Keep interfaces small and focused.
- Prefer multiple small interfaces over one large one.
- Example: `INavigationService`, `IDialogService`, `ISettingsService` — not `IAppService`.
### 3.5 DIP — Dependency Inversion Principle
> *"Depend upon abstractions, not concretions."*
- High-level modules must not depend on low-level modules. Both should depend on abstractions.
- Use constructor injection for dependencies.
- Register services in a DI container (e.g., `Microsoft.Extensions.DependencyInjection`).
---
## 4. YAGNI — You Aren't Gonna Need It
> *"Don't add functionality until it is necessary."*
### Rules
- Only implement what is **explicitly requested** or **clearly needed right now**.
- Do not add "just in case" parameters, methods, or abstractions.
- If you're unsure whether something is needed, **leave it out** — it can always be added later.
### Anti-patterns to avoid
- Adding unused interface methods "for future use".
- Building a generic framework when a single concrete class suffices.
- Creating configuration options nobody has asked for.
---
## Quick Reference Checklist
Before submitting any code change, verify:
- [ ] No duplicated code exists (DRY)
- [ ] The solution is as simple as possible (KISS)
- [ ] Each class has one responsibility (SRP)
- [ ] New behaviour is added via extension, not modification (OCP)
- [ ] Derived types can substitute their base types (LSP)
- [ ] Interfaces are small and focused (ISP)
- [ ] Dependencies point toward abstractions (DIP)
- [ ] No speculative features were added (YAGNI)
## Validation
- Review every new/changed class for SRP violations — ask "does this class have more than one reason to change?"
- Search the codebase for duplicate logic before adding new helpers: `grep_search` for similar method names or patterns.
- Verify no speculative code was added — every line must trace back to the original request.
- Build & register the MSIX package — see **Build, Run & Deploy** in `.github/agents/Agents.md`.
---
## Must Read & Research
> **Agent Rule:** Before making any code change related to design principles, you **must** fetch and review the relevant references below using `fetch_webpage`. Apply what you learn — do not skip this step.
| # | Reference | When to consult |
|---|---|---|
| 1 | [SOLID Principles in C# — Microsoft Learn](https://learn.microsoft.com/en-us/archive/msdn-magazine/2014/may/csharp-best-practices-dangers-of-violating-solid-principles-in-csharp) | Adding/refactoring classes, interfaces, or inheritance |
| 2 | [.NET Design Guidelines](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/) | Designing public APIs, naming, type design |
| 3 | [Framework Design Guidelines (Book)](https://learn.microsoft.com/en-us/dotnet/standard/design-guidelines/) | Deep-dive on member design, exception patterns, collections |
| 4 | [Clean Code Summary](https://gist.github.com/wojteklu/73c6914cc446146b8b533c0988cf8d29) | Code readability, function size, naming clarity |
@@ -0,0 +1,55 @@
---
description: 'Globalization & Localization requirements for user-facing strings, resource files, and culture-aware formatting'
applyTo: '**/*.cs, **/*.xaml, **/*.resw'
---
# Globalization & Localization
These rules apply to **every feature and change** involving user-facing text. They are not optional add-ons.
---
## Rules
- **All user-facing strings** (UI text, error messages, tooltips) must come from `.resw` resource files — never hard-code them in XAML or C#.
- Resource file location: `Strings/en-us/Resources.resw` (default locale).
- Use `x:Uid` in XAML to bind controls to resource keys:
```xml
<TextBlock x:Uid="WelcomeMessage" />
```
With a matching `.resw` entry: `WelcomeMessage.Text` = "Welcome!"
- In code-behind / ViewModels, use the `ResourceLoader`:
```csharp
var loader = new Microsoft.Windows.ApplicationModel.Resources.ResourceLoader();
string message = loader.GetString("ErrorFileNotFound");
```
- **Format dates, numbers, and currencies** using `CultureInfo.CurrentCulture` or `DateTimeFormatter` — never assume a specific regional format.
- Avoid concatenating translated strings — use format placeholders (`{0}`, `{1}`).
- Design UI layouts to accommodate text expansion (~30-40% longer for German vs. English).
## Anti-patterns
- Hard-coded strings in `.xaml` or `.cs` files (e.g., `Content="Save"`).
- Using `string.Format` with hard-coded ordinal assumptions.
- Fixed-width UI elements that clip translated text.
## Validation
- Build & register the MSIX package — see **Build, Run & Deploy** in `.github/agents/Agents.md`.
- Check for hard-coded strings: search `Content="` and `Text="` in `.xaml` files — replace with `x:Uid`.
### Verification Checklist
- [ ] All user-facing strings are in `.resw` resource files
## Must Read & Research
> **Agent Rule:** Before any localization-related change, you **must** fetch and review these references using `fetch_webpage`. Apply what you learn.
| # | Reference | When to consult |
|---|---|---|
| 1 | [Globalize your WinUI app](https://learn.microsoft.com/en-us/windows/apps/design/globalizing/guidelines-and-checklist-for-globalizing-your-app) | Adding any new user-facing strings or culture-aware formatting |
| 2 | [Resource Management System](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/mrtcore/localize-strings) | Setting up or modifying `.resw` files and `ResourceLoader` usage |
| 3 | [WinUI Localization with x:Uid](https://learn.microsoft.com/en-us/windows/apps/develop/ui-input/localizing-strings) | Binding XAML controls to localized resources via `x:Uid` |
@@ -0,0 +1,53 @@
---
description: 'Performance requirements for data binding, layout, threading, and collection virtualization'
applyTo: '**/*.cs, **/*.xaml'
---
# Performance
These rules apply to **every feature and change**. They are not optional add-ons.
---
## Rules
- **Use `x:Bind`** (compiled bindings) instead of `{Binding}` — it's faster and type-safe.
- Use **`x:Load`** (or `x:DeferLoadStrategy`) to defer loading of UI elements not immediately visible.
- Avoid heavy work on the UI thread — use `Task.Run` for CPU-bound work and `async/await` for I/O.
- Use **virtualizing panels** (`ItemsRepeater` with `StackLayout`, or `ListView`) for long lists — never use `StackPanel` with hundreds of items.
- **Cache** expensive computations and HTTP responses when appropriate.
- Minimize XAML visual tree depth — deep nesting hurts layout performance.
- Use **incremental loading** (`ISupportIncrementalLoading`) for large data sets.
- Profile with **Visual Studio Diagnostics Tools** and **PerfView** before and after optimizations.
- Be cautious with `DispatcherQueue.TryEnqueue` — don't flood the dispatcher queue.
## Anti-patterns
- Blocking the UI thread with `.Result` or `.GetAwaiter().GetResult()`.
- Loading all data upfront when only a subset is needed.
- Creating new `HttpClient` instances per request (use `IHttpClientFactory`).
- Using `FindName()` or `VisualTreeHelper` in tight loops.
## Validation
- Build & register the MSIX package — see **Build, Run & Deploy** in `.github/agents/Agents.md`.
### Verification Checklist
- [ ] No blocking calls on the UI thread
- [ ] `x:Bind` is used instead of `{Binding}`
- [ ] Large lists use virtualization
## Must Read & Research
> **Agent Rule:** Before any performance-sensitive change (data binding, layout, collections, async), you **must** fetch and review these references using `fetch_webpage`. Apply what you learn.
| # | Reference | When to consult |
|---|---|---|
| 1 | [Performance best practices for WinUI 3](https://learn.microsoft.com/en-us/windows/apps/performance/) | Any change touching UI rendering, data loading, or threading |
| 2 | [x:Bind markup extension](https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/x-bind-markup-extension) | Adding or modifying XAML data bindings |
| 3 | [x:Load attribute](https://learn.microsoft.com/en-us/windows/uwp/xaml-platform/x-load-attribute) | Deferring UI element loading |
| 4 | [Optimize XAML layout](https://learn.microsoft.com/en-us/windows/apps/performance/optimize-xaml-layout) | Restructuring XAML panels, reducing visual tree depth |
| 5 | [ListView optimization](https://learn.microsoft.com/en-us/windows/apps/performance/optimize-listview) | Working with lists, collections, or `ItemsRepeater` |
@@ -0,0 +1,55 @@
---
description: 'Security requirements for secrets management, input validation, permissions, and secure coding'
applyTo: '**/*.cs, **/*.appxmanifest'
---
# Security
These rules apply to **every feature and change**. They are not optional add-ons.
---
## Rules
- **Never hard-code secrets** (API keys, passwords, connection strings) — use environment variables, Windows Credential Manager, or Azure Key Vault.
- Validate and sanitize **all external input** (user input, file content, network responses).
- Use `SecureString` or `PasswordVault` for sensitive data in memory when practical.
- Follow the **principle of least privilege** — request only the permissions the app actually needs in `Package.appxmanifest`.
- Keep NuGet packages up to date — run `dotnet list package --outdated` regularly.
- Enable **code signing** for published MSIX packages. Use the `winapp` CLI rather than hand-rolling `signtool`:
- Generate a development certificate matching the manifest publisher: `winapp cert generate --manifest .\Package.appxmanifest --install`.
- Inspect a cert before signing: `winapp cert info .\devcert.pfx`.
- Sign an existing file: `winapp sign .\MyApp.msix --cert .\devcert.pfx`.
- Build + sign in one step: `winapp pack .\bin\<Platform>\Release\<TFM>\win-<rid> --cert .\devcert.pfx`.
- Production releases must be signed by a trusted certificate authority -- never ship the development cert.
- When using `HttpClient`, always validate TLS certificates and use HTTPS.
- Never log sensitive data (PII, tokens, passwords).
## Anti-patterns
- Storing secrets in `appsettings.json` committed to source control.
- Disabling TLS validation for debugging and forgetting to re-enable it.
- Using `Process.Start` with unsanitized user input.
- Broad `try { } catch (Exception) { }` that swallows errors silently without any logging.
## Validation
- Build & register the MSIX package — see **Build, Run & Deploy** in `.github/agents/Agents.md`.
- Check for hard-coded secrets: search for `password`, `apikey`, `secret`, `connectionstring` in `.cs` files.
### Verification Checklist
- [ ] No secrets are hard-coded
## Must Read & Research
> **Agent Rule:** Before any security-related change (auth, input handling, permissions, HTTP), you **must** fetch and review these references using `fetch_webpage`. Apply what you learn.
| # | Reference | When to consult |
|---|---|---|
| 1 | [.NET Security Best Practices](https://learn.microsoft.com/en-us/dotnet/standard/security/) | Any code handling credentials, tokens, or sensitive data |
| 2 | [Secure coding guidelines for .NET](https://learn.microsoft.com/en-us/dotnet/standard/security/secure-coding-guidelines) | Input validation, exception handling, type safety |
| 3 | [MSIX Security](https://learn.microsoft.com/en-us/windows/msix/msix-container) | Packaging, signing, or distribution changes |
| 4 | [Package.appxmanifest capabilities](https://learn.microsoft.com/en-us/windows/uwp/packaging/app-capability-declarations) | Adding or modifying app capabilities/permissions |
@@ -0,0 +1,284 @@
---
description: 'Unit testing standards, test project setup, naming, build & run commands'
applyTo: '**/*Tests.cs, **/*Test.cs, **/*.Tests.csproj'
---
# Testing — Unit Tests, Build & Run
Every public method and class must have corresponding unit tests. Tests are not optional.
---
## 1. Test Framework & Project Setup
### Recommended Stack
| Component | Package | Purpose |
|---|---|---|
| Test Framework | `MSTest` | Test runner & assertions |
| Mocking | `Moq` | Mock dependencies |
| UI Testing | `Microsoft.Windows.Apps.Test` | WinUI UI automation (optional) |
### Test Project Setup
Create a test project alongside the main project:
```
<SolutionRoot>/
<ProjectName>/ ← Main app project
<ProjectName>.Tests/ ← Unit test project
```
Test project `.csproj` should reference the main project:
```xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- Match TargetFramework to the main project's .csproj -->
<TargetFramework><!-- same as the main project's .csproj TargetFramework --></TargetFramework>
<UseWinUI>true</UseWinUI>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<!-- Use latest stable versions; do not hard-code version numbers in instructions -->
<PackageReference Include="MSTest.TestAdapter" Version="*" />
<PackageReference Include="MSTest.TestFramework" Version="*" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="*" />
<PackageReference Include="Moq" Version="*" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\<ProjectName>\<ProjectName>.csproj" />
</ItemGroup>
</Project>
```
---
## 2. Test Writing Rules
### What to Test
- **All public methods** in ViewModels, Services, Helpers, and Models.
- **Edge cases** — null inputs, empty collections, boundary values.
- **Error paths** — exception handling, invalid state transitions.
- **Business logic** — calculations, transformations, state management.
### What NOT to Test (Directly)
- XAML layout / visual rendering (use UI tests for that).
- Framework internals (e.g., `InitializeComponent()`).
- Private methods — test them indirectly through public methods.
### Test Naming Convention
Use the pattern: `MethodName_Scenario_ExpectedResult`
```csharp
[TestMethod]
public void CalculateTotal_WithEmptyCart_ReturnsZero() { }
[TestMethod]
public void LoadDataAsync_WhenServiceThrows_SetsErrorState() { }
[TestMethod]
public async Task SaveAsync_WithValidInput_ReturnsTrue() { }
```
### Test Structure (AAA Pattern)
Every test follows **Arrange → Act → Assert**:
```csharp
[TestMethod]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
int result = calculator.Add(2, 3);
// Assert
Assert.AreEqual(5, result);
}
```
### ViewModel Testing Example
```csharp
[TestMethod]
public async Task LoadItemsAsync_OnSuccess_PopulatesItems()
{
// Arrange
var mockService = new Mock<IDataService>();
mockService
.Setup(s => s.GetItemsAsync())
.ReturnsAsync(new List<Item> { new("Test") });
var viewModel = new MainViewModel(mockService.Object);
// Act
await viewModel.LoadItemsAsync();
// Assert
Assert.AreEqual(1, viewModel.Items.Count);
Assert.IsFalse(viewModel.IsLoading);
}
```
---
## 3. Test Organization
### File Structure
Mirror the main project's folder structure (defined in [winui-best-practices](winui-best-practices.instructions.md)) in the test project. As the main project grows with subfolders under `ViewModels/`, `Services/`, `Views/`, etc., the test project must grow organically in the same way. This alignment enables on-demand test runs scoped to the area you changed:
```
<ProjectName>/ <ProjectName>.Tests/
Models/ Models/
User.cs UserTests.cs
ViewModels/ ViewModels/
MainViewModelTests.cs MainViewModelTests.cs
Settings/ Settings/
ThemeViewModel.cs ThemeViewModelTests.cs
Services/ Services/
DataService.cs DataServiceTests.cs
Auth/ Auth/
AuthService.cs AuthServiceTests.cs
Helpers/ Helpers/
StringHelper.cs StringHelperTests.cs
Converters/ Converters/
BoolToVisibilityConverter.cs BoolToVisibilityConverterTests.cs
```
### One Test Class per Class Under Test
```csharp
namespace <RootNamespace>.Tests.ViewModels;
[TestClass]
public class MainViewModelTests
{
// All tests for MainViewModel go here
}
```
### Running Tests On-Demand
After a change, run only the tests related to the affected area instead of the full suite. Detect the platform first (matching the convention in `.github/agents/Agents.md`):
```powershell
# Run from the test project folder
cd <ProjectName>.Tests
# Detect platform once per session (AMD64 -> x64; ARM64/x86 unchanged)
$arch = $env:PROCESSOR_ARCHITECTURE
$Platform = if ($arch -eq 'AMD64') { 'x64' } else { $arch }
# Run tests for a specific class
dotnet test -c Debug -p:Platform=$Platform --filter "FullyQualifiedName~MainViewModelTests"
# Run a single test
dotnet test -c Debug -p:Platform=$Platform --filter "FullyQualifiedName~MainViewModelTests.LoadItemsAsync_OnSuccess_PopulatesItems"
# Run all tests in a namespace (e.g., all ViewModel tests)
dotnet test -c Debug -p:Platform=$Platform --filter "FullyQualifiedName~Tests.ViewModels"
# Run tests in a subfolder namespace (e.g., only Settings ViewModels)
dotnet test -c Debug -p:Platform=$Platform --filter "FullyQualifiedName~Tests.ViewModels.Settings"
# Run the full suite (for cross-cutting changes)
dotnet test -c Debug -p:Platform=$Platform
```
```
---
## 4. Test-Specific Commands
For general build and register commands, see **Build, Run & Deploy** in `.github/agents/Agents.md`.
For on-demand test filtering, see **Running Tests On-Demand** above.
Below are additional test commands:
### Build Only the Test Project
```powershell
cd <ProjectName>.Tests
$arch = $env:PROCESSOR_ARCHITECTURE
$Platform = if ($arch -eq 'AMD64') { 'x64' } else { $arch }
dotnet build -c Debug -p:Platform=$Platform
```
### Run Tests with Verbose Output
```powershell
dotnet test -c Debug -p:Platform=$Platform --verbosity normal
```
---
## 5. Agent Workflow for Tests
When you write or modify code, follow this sequence:
1. **Implement the feature or fix** in the main project.
2. **Write unit tests** for every new/changed public method.
3. **Build** — see **Build, Run & Deploy** in `.github/agents/Agents.md`. Fix all errors and warnings.
4. **Run tests**`dotnet test -c Debug -p:Platform=$Platform` (from the test project folder; detect `$Platform` as shown above) and ensure all pass.
5. **Review** — Confirm tests cover the happy path, edge cases, and error cases.
### When Modifying Existing Code
1. **Run existing tests first** to establish a baseline.
2. Make the code change.
3. **Run tests again** — fix any failures.
4. **Add new tests** if the change introduces new behaviour.
### Coverage Goals
- Aim for **80%+ code coverage** on business logic (ViewModels, Services).
- 100% coverage of utility/helper methods.
- UI code-behind is exempt from unit test coverage (tested via integration/UI tests).
---
## 6. Common Test Pitfalls
| Pitfall | Fix |
|---|---|
| Test depends on another test's state | Each test must be fully independent |
| Testing multiple things in one test | One assertion per logical concept |
| Tests pass but don't actually verify anything | Always have meaningful assertions |
| Mocking too much | Mock only external dependencies, not the class under test |
| Testing implementation details | Test behaviour and outcomes, not internal method calls |
| Async tests without `await` | Always `await` async methods and use `async Task` return type |
---
## Validation
- Build & run tests — see **Build, Run & Deploy** in `.github/agents/Agents.md`.
- Verify all tests pass — zero failures, zero skipped without justification.
- Verify naming follows `MethodName_Scenario_ExpectedResult` pattern.
- Verify AAA structure (Arrange/Act/Assert) in every test method.
- Confirm coverage goals: 80%+ on ViewModels/Services, 100% on helpers.
---
## Must Read & Research
> **Agent Rule:** Before writing or modifying tests, you **must** fetch and review the relevant references below using `fetch_webpage`. Apply what you learn — do not skip this step.
| # | Reference | When to consult |
|---|---|---|
| 1 | [Unit testing C# with MSTest](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-with-mstest) | Setting up test project, writing first tests, MSTest attributes |
| 2 | [Unit testing best practices .NET](https://learn.microsoft.com/en-us/dotnet/core/testing/unit-testing-best-practices) | Every time you write tests — naming, structure, AAA pattern |
| 3 | [Moq Quickstart](https://github.com/devlooped/moq/wiki/Quickstart) | Mocking interfaces, setting up `Returns`/`Throws`, verifying calls |
| 4 | [FluentAssertions Documentation](https://fluentassertions.com/introduction) | Writing expressive assertions (`Should().Be()`, collections, exceptions) |
| 5 | [dotnet test CLI](https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-test) | Running tests from terminal, filtering, verbosity options |
| 6 | [Test Explorer in Visual Studio](https://learn.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer) | Debugging tests, viewing coverage, understanding test output |
@@ -0,0 +1,92 @@
---
description: 'WinAppSDK & Windows Platform SDK -- API namespace catalog and lookup guidance'
applyTo: '**/*.cs, **/*.xaml, **/*.csproj'
---
# Windows APIs -- WinAppSDK & Windows Platform SDK
## Sample-First Rule
> **Agent Rule -- MANDATORY:** Before implementing **any** WinAppSDK or Windows Platform SDK API you have not used before, you **must** search the sample repositories below for a working example first. **Do not guess API usage patterns from documentation alone** -- the docs often omit critical details that only the sample code reveals. Search **all** of the following repos, not just one:
| # | Repository | What it covers |
|---|---|---|
| 1 | [WindowsAppSDK-Samples](https://github.com/microsoft/WindowsAppSDK-Samples) | All WinAppSDK features (AI, windowing, lifecycle, notifications, etc.) |
| 2 | [AI Dev Gallery](https://github.com/microsoft/ai-dev-gallery) | On-device AI/ML patterns, model usage examples |
| 3 | [WinUI-Gallery](https://github.com/microsoft/WinUI-Gallery) | UI control patterns and XAML examples |
### How to apply
1. **Find the right API** -- Translate the user's scenario/requirement into common API/programming keywords, then search the API references (Part A-B below) using those keywords to identify which API fits.
2. **Search for samples** -- Once you know which API to use, search each sample repo above for the class name to find a working example.
3. **Study the sample** -- Read the sample's Model / ViewModel / Service layer to understand how the API is actually called -- object lifetime, required parameters, data preparation, error handling.
4. **Adapt** the sample pattern into our MVVM architecture -- don't copy the sample structure wholesale, but match its API call sequence exactly.
---
> **Agent Rule:** Before implementing any feature that involves a platform capability, **consult this file** to check whether a built-in API already exists. Always verify exact class names, method signatures, and availability by following the reference links -- do not guess API shapes.
---
## Part A -- Windows App SDK APIs
**Full API reference:** <https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/>
> **Agent Rule:** Do not rely on a hardcoded namespace list -- the SDK is updated frequently. Instead, **search** the API reference above by converting the user's scenario into common programming keywords.
### How to search
1. **Translate** the user's request into API/programming terms. Examples:
- "I want to describe an image" -> search for: `image description`, `ImageDescription`, `describe image`
- "Add a notification" -> search for: `notification`, `toast`, `AppNotification`
- "Pick a file" -> search for: `file picker`, `StoragePicker`, `FileOpenPicker`
- "Make the window always on top" -> search for: `AppWindow`, `presenter`, `compact overlay`
2. **Search** the [WinAppSDK API reference](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/) using `web_search` or `web_fetch` with those keywords.
3. **Verify** the class/method exists in the SDK version used by this project (check `.csproj` `<PackageReference>` for `Microsoft.WindowsAppSDK` version).
### Key reference links
| # | Link | When to consult |
|---|---|---|
| 1 | [WinAppSDK API Reference (full)](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/) | **Always** -- search and look up exact class/method signatures here |
| 2 | [Windows App SDK overview](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/) | Feature overview, architecture |
| 3 | [Release notes (stable)](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/stable-channel) | API availability, version support, breaking changes |
| 4 | [Windows AI overview](https://learn.microsoft.com/en-us/windows/ai/) | All AI options: Windows AI APIs, Windows ML, Foundry Local |
| 5 | [Get started with Windows AI APIs](https://learn.microsoft.com/en-us/windows/ai/apis/get-started) | Prerequisites, project setup, first AI call |
| 6 | [Windows ML overview](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/overview) | Custom ONNX model inference |
| 7 | [Foundry Local](https://learn.microsoft.com/en-us/windows/ai/foundry-local/get-started) | Run OSS LLMs locally |
---
## Part B -- Windows Platform SDK (UWP / WinRT APIs)
**Full API reference:** <https://learn.microsoft.com/en-us/uwp/api/>
> **Agent Rule:** The Platform SDK (`Windows.*` namespaces) is very large and constantly evolving. Do not rely on a hardcoded list. **Search** the API reference by translating the user's requirement into programming keywords.
### How to search
1. **Translate** the user's request into API/programming terms. Examples:
- "Send a Bluetooth message" -> search for: `Bluetooth`, `RFCOMM`, `BluetoothDevice`
- "Get the user's location" -> search for: `geolocation`, `Geolocator`, `position`
- "Read text from an image" -> search for: `OCR`, `text recognition`, `OcrEngine`
- "Copy to clipboard" -> search for: `clipboard`, `DataTransfer`, `DataPackage`
2. **Search** the [Platform SDK API reference](https://learn.microsoft.com/en-us/uwp/api/) using `web_search` or `web_fetch` with those keywords.
3. **Check for WinAppSDK equivalent** -- some Platform SDK APIs have newer equivalents in Part A. Always prefer the WinAppSDK version when both exist.
### Key reference links
| # | Link | When to consult |
|---|---|---|
| 1 | [Platform SDK API Reference (full)](https://learn.microsoft.com/en-us/uwp/api/) | **Search here** for any Windows capability not in WinAppSDK |
| 2 | [Windows SDK downloads](https://developer.microsoft.com/windows/downloads/windows-sdk/) | SDK versions and downloads |
---
## Validation
- Before implementing any platform feature, confirm the API is available in the current Windows App SDK version by checking the [release notes](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/stable-channel).
- For features requiring specific hardware (NPU), provide a graceful fallback for unsupported devices.
- When both WinAppSDK and Platform SDK offer a similar API, prefer the WinAppSDK version.
@@ -0,0 +1,420 @@
---
description: 'WinUI 3 / WinAppSDK architecture, MVVM, XAML patterns, DI, theming, and controls guidance'
applyTo: '**/*.cs, **/*.xaml, **/*.csproj'
---
# WinUI 3 / WinAppSDK -- Best Practices & Patterns
This file covers WinUI 3-specific patterns, conventions, and architecture guidance for this project.
---
## 1. Architecture -- MVVM Pattern
### Overview
Use **Model-View-ViewModel (MVVM)** for all UI features:
| Layer | Responsibility | Example |
|---|---|---|
| **Model** | Data structures & business entities | `Item.cs`, `UserProfile.cs` |
| **View** | XAML UI -- layout, styles, animations | `MainPage.xaml` |
| **ViewModel** | UI state, commands, data transformation | `MainViewModel.cs` |
| **Service** | Business logic, data access, navigation | `IDataService.cs`, `NavigationService.cs` |
### Project Folder Structure
```
<ProjectName>/
Models/ -> Data classes
ViewModels/ -> ViewModels (one per page/dialog)
Views/ -> XAML pages and windows
Services/ -> Business logic & platform services
Converters/ -> IValueConverter implementations
Helpers/ -> Static utility methods
Controls/ -> Custom/reusable controls
Strings/
en-us/
Resources.resw
Assets/ -> Images, icons, splash screens
```
### ViewModel Base
Use `CommunityToolkit.Mvvm` (recommended) for boilerplate-free ViewModels:
```xml
<!-- Use latest stable version; do not hard-code version numbers in instructions -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="*" />
```
```csharp
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty]
private string _title = string.Empty;
[ObservableProperty]
private bool _isLoading;
[RelayCommand]
private async Task LoadDataAsync()
{
IsLoading = true;
try
{
// Load data
}
finally
{
IsLoading = false;
}
}
}
```
### View-ViewModel Binding
```xml
<Page
x:Class="<RootNamespace>.Views.MainPage"
xmlns:vm="using:<RootNamespace>.ViewModels">
<Page.DataContext>
<vm:MainViewModel />
</Page.DataContext>
<Grid>
<TextBlock Text="{x:Bind ViewModel.Title, Mode=OneWay}" />
<Button Command="{x:Bind ViewModel.LoadDataCommand}" Content="Load" />
</Grid>
</Page>
```
---
## 2. XAML Best Practices
### Use `x:Bind` Over `{Binding}`
| Feature | `x:Bind` | `{Binding}` |
|---|---|---|
| Compile-time check | Yes | No |
| Performance | Faster (compiled) | Slower (reflection) |
| Default mode | OneTime | OneWay |
| IntelliSense | Yes | No |
```xml
<!-- GOOD -->
<TextBlock Text="{x:Bind ViewModel.Name, Mode=OneWay}" />
<!-- AVOID -->
<TextBlock Text="{Binding Name}" />
```
### Use `x:Load` for Deferred Loading
```xml
<StackPanel x:Load="{x:Bind ViewModel.ShowAdvancedOptions, Mode=OneWay}">
<!-- Heavy content loaded only when needed -->
</StackPanel>
```
### Use WinUI Controls from the SDK
Prefer the WinUI 3 controls from `Microsoft.UI.Xaml.Controls`, **not** the older UWP `Windows.UI.Xaml.Controls`:
```csharp
// GOOD -- WinUI 3
using Microsoft.UI.Xaml.Controls;
// AVOID -- UWP (won't work in WinUI 3 desktop)
// using Windows.UI.Xaml.Controls;
```
### XAML Formatting Convention
```xml
<Button
x:Name="SaveButton"
x:Uid="SaveButton"
AutomationProperties.Name="Save"
Command="{x:Bind ViewModel.SaveCommand}"
Style="{StaticResource AccentButtonStyle}" />
```
- One attribute per line for controls with 3+ attributes.
- Order: `x:Name` -> `x:Uid` -> `AutomationProperties` -> layout -> data -> style.
---
## 3. Dependency Injection
### Setup with `Microsoft.Extensions.DependencyInjection`
```xml
<!-- Use latest stable version; do not hard-code version numbers in instructions -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="*" />
```
```csharp
// App.xaml.cs
public partial class App : Application
{
public static IServiceProvider Services { get; private set; } = null!;
public App()
{
InitializeComponent();
Services = ConfigureServices();
}
private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();
// Services
services.AddSingleton<INavigationService, NavigationService>();
services.AddTransient<IDataService, DataService>();
// ViewModels
services.AddTransient<MainViewModel>();
return services.BuildServiceProvider();
}
}
```
---
## 4. Navigation
### Frame-based Navigation
```csharp
public interface INavigationService
{
bool CanGoBack { get; }
void NavigateTo<TPage>() where TPage : Page;
void GoBack();
}
```
Use a `NavigationView` with a `Frame`:
```xml
<NavigationView SelectionChanged="OnNavigationChanged">
<NavigationView.MenuItems>
<NavigationViewItem Content="Home" Tag="Home" />
<NavigationViewItem Content="Settings" Tag="Settings" />
</NavigationView.MenuItems>
<Frame x:Name="ContentFrame" />
</NavigationView>
```
---
## 5. Windowing & Title Bar
### Custom Title Bar
```csharp
// In Window constructor or Activated handler
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar); // AppTitleBar is a UIElement in your XAML
```
```xml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="48" /> <!-- Title bar -->
<RowDefinition Height="*" /> <!-- Content -->
</Grid.RowDefinitions>
<Grid x:Name="AppTitleBar" Grid.Row="0">
<TextBlock Text="My App" VerticalAlignment="Center" Margin="16,0" />
</Grid>
<Frame Grid.Row="1" x:Name="ContentFrame" />
</Grid>
```
### Window Sizing
```csharp
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
var windowId = Win32Interop.GetWindowIdFromWindow(hwnd);
var appWindow = AppWindow.GetFromWindowId(windowId);
appWindow.Resize(new SizeInt32(1280, 720));
```
---
## 6. Theming
### Support Light/Dark/High Contrast
```xml
<!-- In App.xaml -->
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
```
### Detect Theme Changes
```csharp
if (Content is FrameworkElement rootElement)
{
rootElement.ActualThemeChanged += (s, e) =>
{
// React to theme change
};
}
```
### Use Theme Resources, Not Hard-coded Colors
```xml
<!-- GOOD -->
<TextBlock Foreground="{ThemeResource TextFillColorPrimaryBrush}" />
<!-- AVOID -->
<TextBlock Foreground="#000000" />
```
---
## 7. System Backdrop (Mica / Acrylic)
This project uses **Mica** backdrop (already configured in `MainWindow.xaml`):
```xml
<Window.SystemBackdrop>
<MicaBackdrop />
</Window.SystemBackdrop>
```
Alternatives:
```xml
<DesktopAcrylicBackdrop /> <!-- Acrylic -->
<MicaBackdrop Kind="BaseAlt" /> <!-- Mica Alt -->
```
---
## 8. Community Toolkit
### Recommended Packages
```xml
<!-- Use latest stable versions; do not hard-code version numbers in instructions -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="*" />
<PackageReference Include="CommunityToolkit.WinUI.Controls.SettingsControls" Version="*" />
<PackageReference Include="CommunityToolkit.WinUI.Helpers" Version="*" />
<PackageReference Include="CommunityToolkit.WinUI.Animations" Version="*" />
```
### Useful Toolkit Features
| Feature | Package | Use Case |
|---|---|---|
| `ObservableObject` | CommunityToolkit.Mvvm | ViewModel base class |
| `RelayCommand` | CommunityToolkit.Mvvm | Command implementation |
| `ObservableProperty` | CommunityToolkit.Mvvm | Auto INotifyPropertyChanged |
| `SettingsCard` | CommunityToolkit.WinUI.Controls | Settings pages |
| `IncrementalLoadingCollection` | CommunityToolkit.WinUI | Lazy-loading lists |
---
## 9. Common Pitfalls
| Pitfall | Solution |
|---|---|
| Using `Windows.UI.Xaml` namespace | Use `Microsoft.UI.Xaml` for WinUI 3 |
| Calling `Window.Current` | Not available in WinUI 3 -- pass window reference explicitly |
| Using `CoreDispatcher` | Use `DispatcherQueue` instead |
| `REGDB_E_CLASSNOTREG` error | Ensure Developer Mode is enabled, then re-register: run `winapp unregister` followed by `dotnet run` (or `winapp run <build-output>`) to refresh the loose-layout registration |
| Stale package state after manifest changes | Run `winapp unregister`, then `dotnet run` -- using `winapp run --clean` additionally wipes `LocalState`/settings to test first-run behavior |
| XAML Designer crashes | Clean & rebuild; ensure platform matches (x64 vs AnyCPU) |
| `{Binding}` not updating | Switch to `x:Bind` with `Mode=OneWay` or `Mode=TwoWay` |
---
## 10. Validation
Build & register the MSIX package -- see **Build, Run & Deploy** in `.github/agents/Agents.md`.
### Verify
- Run the app and verify the changed UI renders correctly on x64.
- Search XAML for `{Binding` -- replace with `x:Bind`.
- Search XAML for `Foreground="#` or `Background="#` -- replace with `{ThemeResource}`.
- Search C# for `Windows.UI.Xaml` -- replace with `Microsoft.UI.Xaml`.
- Search C# for `Window.Current` -- replace with explicit window reference.
- Search C# for `CoreDispatcher` -- replace with `DispatcherQueue`.
- Test Light, Dark, and High Contrast themes.
---
## 11. Must Read & Research
> **Agent Rule:** Before making any WinUI/WinAppSDK-related change, you **must** fetch and review the relevant references below using `fetch_webpage`. Consult the appropriate section based on the type of change. Apply what you learn -- do not skip this step.
### Official Documentation
| # | Reference | When to consult |
|---|---|---|
| 1 | [WinUI 3 Overview](https://learn.microsoft.com/en-us/windows/apps/winui/winui3/) | Starting new features, onboarding to the project |
| 2 | [Windows App SDK](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/) | Using SDK-specific APIs (windowing, lifecycle, activation) |
| 3 | [WinUI 3 API Reference](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/) | Looking up specific class/method signatures |
| 4 | [WinUI 3 Gallery App](https://learn.microsoft.com/en-us/windows/apps/design/controls/) | Choosing controls, reviewing control patterns |
| 5 | [AI Dev Gallery](https://github.com/microsoft/ai-dev-gallery) | On-device AI/ML integration patterns, model usage examples in WinUI |
| 6 | [Windows App SDK Release Notes](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/stable-channel) | Checking for breaking changes, new APIs, known issues |
### Architecture & Patterns
| # | Reference | When to consult |
|---|---|---|
| 7 | [CommunityToolkit.Mvvm Docs](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/) | Implementing ViewModels, commands, `ObservableProperty` |
| 8 | [Dependency Injection in .NET](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection) | Registering services, constructor injection, lifetime management |
| 9 | [Template Studio for WinUI](https://github.com/microsoft/TemplateStudio) | Scaffolding pages, navigation, project structure patterns |
### Controls & Design
| # | Reference | When to consult |
|---|---|---|
| 10 | [WinUI 3 Gallery (GitHub)](https://github.com/microsoft/WinUI-Gallery) | Example implementations of any WinUI control |
| 11 | [Windows Community Toolkit (GitHub)](https://github.com/CommunityToolkit/Windows) | Before building custom controls -- check if the toolkit already has one |
| 12 | [Fluent Design System](https://learn.microsoft.com/en-us/windows/apps/design/) | Spacing, typography, colour, motion, layout decisions |
| 13 | [XAML Controls Gallery](https://apps.microsoft.com/store/detail/winui-3-gallery/9P3JFPWWDZRC) | Interactive demo of all controls and their properties |
### Windows APIs & AI
| # | Reference | When to consult |
|---|---|---|
| 14 | [Windows APIs instruction file](windows-apis.instructions.md) | **First stop** -- check if a built-in API already exists for the capability you need (AI, windowing, notifications, widgets, lifecycle, etc.) |
| 15 | [Windows AI APIs](https://learn.microsoft.com/en-us/windows/ai/apis/) | On-device AI: Phi Silica (text gen), OCR, imaging (super-res, description, object extract, erase) |
| 16 | [Windows ML](https://learn.microsoft.com/en-us/windows/ai/new-windows-ml/overview) | Custom ONNX model inference on CPU/GPU/NPU |
| 17 | [Foundry Local](https://learn.microsoft.com/en-us/windows/ai/foundry-local/get-started) | Run OSS LLMs (Llama, Mistral, Phi) locally via REST API |
| 18 | [Windows AI on Windows](https://learn.microsoft.com/en-us/windows/ai/) | AI landing page -- all AI options for Windows apps |
### Samples
> **Agent Rule -- MANDATORY:** Before implementing any WinAppSDK or Platform SDK API you have not used before, **search the samples repo first** and study the working example. Do not guess API usage from docs alone -- see the [Sample-First Rule](windows-apis.instructions.md#sample-first-rule) for details and known pitfalls.
| # | Reference | When to consult |
|---|---|---|
| 19 | [Windows App SDK Samples](https://github.com/microsoft/WindowsAppSDK-Samples) | **Always search here first** before implementing any SDK API for the first time |
| 20 | [WinUI 3 Demos](https://github.com/microsoft/WinUI-Gallery) | Reference implementations and patterns |
| 21 | [Windows AI API Samples](https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/WindowsAIFoundry/cs-winui) | AI API usage with WinUI (ImageDescription, TextRecognizer, LanguageModel, etc.) |
+54
View File
@@ -0,0 +1,54 @@
## .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
+270
View File
@@ -0,0 +1,270 @@
# Copilot Agent Instructions -- WinUI 3 / WinAppSDK
## Project Overview
This is a **WinUI 3** desktop application built on the **Windows App SDK**. It uses MSIX packaging and supports x86, x64, and ARM64 architectures.
> **Source of truth for versions & names:** Always read the project `.csproj` to determine the current `TargetFramework`, `RuntimeIdentifiers`, `Platforms`, `RootNamespace`, and `Microsoft.WindowsAppSDK` package version. Never hard-code project names or version numbers in instruction files.
>
> Throughout this document and the instruction files, `<ProjectName>` is a placeholder -- replace it with the actual project folder/assembly name (derived from the `.csproj` filename).
| Property | How to determine |
|---|---|
| UI Framework | WinUI 3 (`Microsoft.UI.Xaml`) -- always used |
| App SDK | Read `Microsoft.WindowsAppSDK` version from `.csproj` `<PackageReference>` |
| Runtime / TFM | Read `<TargetFramework>` from `.csproj` (e.g., `net10.0-windows10.0.26100.0`) |
| Target OS | Derived from `<TargetFramework>` and `<TargetPlatformMinVersion>` in `.csproj` |
| Platforms | Read `<Platforms>` from `.csproj` (e.g., `x86;x64;ARM64`) |
| Packaging | MSIX (`<EnableMsixTooling>true</EnableMsixTooling>`) |
| Namespace | Read `<RootNamespace>` from `.csproj` |
| Nullable | Read `<Nullable>` from `.csproj` |
> **Default TFM:** Templates ship with `net10.0` by default. Pass
> `--dotnet-version <tfm>` (for example `net10.0`) when running `dotnet new ...`
> or edit `<TargetFramework>` inside the generated `.csproj` before the first
> build if you need a newer framework. Keep `<RuntimeIdentifiers>` synchronized
> with the framework you pick.
## Instruction Files Index
All detailed agent instructions are organized under `.github/instructions/`:
| File | Scope |
|---|---|
| [design-principles.instructions.md](.github/instructions/design-principles.instructions.md) | DRY, KISS, SOLID, YAGNI |
| [globalization.instructions.md](.github/instructions/globalization.instructions.md) | Globalization & Localization |
| [accessibility.instructions.md](.github/instructions/accessibility.instructions.md) | Accessibility |
| [security.instructions.md](.github/instructions/security.instructions.md) | Security |
| [performance.instructions.md](.github/instructions/performance.instructions.md) | Performance |
| [code-quality.instructions.md](.github/instructions/code-quality.instructions.md) | Static Analysis, StyleCop, Code Cleanup |
| [winui-best-practices.instructions.md](.github/instructions/winui-best-practices.instructions.md) | WinUI 3 / WinAppSDK patterns & references |
| [windows-apis.instructions.md](.github/instructions/windows-apis.instructions.md) | WinAppSDK & Platform SDK API namespace catalog & lookup guidance |
| [testing.instructions.md](.github/instructions/testing.instructions.md) | Unit Testing, Build & Run |
## Core Agent Workflow
Every time you work on this codebase, follow this checklist:
### Before Writing Code
1. **Review the original goal** -- Re-read the user's request and confirm you understand the intent.
2. **Check existing code** -- Search for related implementations to avoid duplication (DRY).
3. **Find the right API** -- If the task involves a platform capability (AI, UI controls, file access, notifications, windowing, widgets, sensors, etc.), first check the [Windows APIs catalog](.github/instructions/windows-apis.instructions.md) and then look up the correct API in the [WinUI 3 API Reference](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/) before writing code.
4. **Plan the approach** -- Consider SOLID principles and identify which classes/interfaces are involved.
### While Writing Code
> **Agent Rule -- MANDATORY:** Steps 5-8 are **not** passive references. You **must** actually open and read the linked instruction file before writing code that falls within its scope. Do not skip this -- these files contain rules, anti-patterns, and checklists that must be applied.
5. **Apply Design Principles** -- **Read** [design-principles](.github/instructions/design-principles.instructions.md) before adding/refactoring classes or logic. Apply DRY, KISS, SOLID, YAGNI.
6. **Follow Fundamentals** -- **Read the applicable instruction files** based on what you're changing:
- Adding or changing **UI controls / XAML**? -> Read [accessibility](.github/instructions/accessibility.instructions.md) (AutomationProperties, keyboard nav, contrast) AND [performance](.github/instructions/performance.instructions.md) (x:Bind, x:Load, virtualization).
- Adding or changing **user-facing strings** (labels, messages, tooltips)? -> Read [globalization](.github/instructions/globalization.instructions.md) (`.resw` files, `x:Uid`, `ResourceLoader`).
- Handling **secrets, user input, HTTP, or permissions**? -> Read [security](.github/instructions/security.instructions.md) (no hard-coded secrets, input validation, least privilege).
- Working on **data binding, collections, async/IO, or layout**? -> Read [performance](.github/instructions/performance.instructions.md) (x:Bind, virtualization, async patterns).
7. **Respect Code Quality Rules** -- **Read** [code-quality](.github/instructions/code-quality.instructions.md) before writing code. Follow all CA*/SA*/IDE* analyzer rules and naming conventions.
8. **Follow WinUI Patterns** -- **Read** [winui-best-practices](.github/instructions/winui-best-practices.instructions.md) for MVVM, x:Bind, community toolkit, and API verification.
### After Writing Code
9. **Remove unused code** -- Delete unused `using` statements, dead code, commented-out blocks.
10. **Write unit tests** -- Every new public method/class needs tests. **Read** [testing](.github/instructions/testing.instructions.md) for framework setup, naming conventions (`MethodName_Scenario_ExpectedResult`), AAA pattern, and `dotnet test` commands.
11. **Build the project** -- Detect the platform first (`$Platform = $env:PROCESSOR_ARCHITECTURE`), then run `dotnet build -c Debug -p:Platform=$Platform` from the project folder and fix all warnings/errors. **If build errors occur, follow the Troubleshooting Build Errors workflow below.**
12. **Run tests** -- Run tests related to the change using `--filter` (see [testing](.github/instructions/testing.instructions.md)). Run the full suite only when the change is cross-cutting.
13. **Run the app with package identity** -- Use `dotnet run` (preferred -- the project references `Microsoft.Windows.SDK.BuildTools.WinApp`, which automatically invokes `winapp run` to register a loose-layout package and launch via AUMID). See [Build, Run & Deploy](#build-run--deploy) below for advanced scenarios.
14. **Re-review against original goal** -- Confirm the implementation matches the user's request.
### Troubleshooting Build Errors
> **Agent Rule -- MANDATORY:** When a build fails due to an unknown type, missing namespace, unresolved API, or similar definition error, follow this escalation order. **Do NOT jump straight to reading `.winmd` files or using `ildasm`/decompilers** -- always try web search first.
**Step 1 -- Web Search (ALWAYS try first):**
1. Open and read [windows-apis.instructions.md](.github/instructions/windows-apis.instructions.md) -- it contains the API namespace catalog and lookup guidance.
2. Translate the unknown type/namespace into search keywords (e.g., `ImageDescription` -> "WinAppSDK ImageDescription API").
3. Use `web_search` or `web_fetch` to search the [WinAppSDK API Reference](https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/) and the [Platform SDK API Reference](https://learn.microsoft.com/en-us/uwp/api/) for the correct namespace, class name, and method signatures.
4. Check the [release notes](https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/stable-channel) to verify the API is available in the project's SDK version (read from `.csproj`).
**Step 2 -- Sample Repos:**
If web search finds the API but usage is unclear, search the sample repositories listed in [windows-apis.instructions.md](.github/instructions/windows-apis.instructions.md) for working examples.
**Step 3 -- WinMD / Decompiler (last resort only):**
Only if Steps 1-2 fail to resolve the issue, then inspect `.winmd` metadata files or use decompilation tools to discover the exact type definitions. This is a fallback, not the default approach.
## Build, Run & Deploy
This is an MSIX-packaged WinUI 3 app. You **must** pass both `-c` (Configuration) and `-p:Platform=` to every `dotnet build`/`dotnet test` command.
This template references the [`Microsoft.Windows.SDK.BuildTools.WinApp`](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools.WinApp) NuGet package, which hooks `dotnet run` to invoke the [`winapp` CLI](https://github.com/microsoft/WinAppCli). Use `dotnet run` for everyday inner-loop development -- you do not need to call `Add-AppxPackage` or `MakeAppx.exe` by hand.
### Dotnet CLI Workflow
- Prefer `dotnet new` for scaffolding projects and items so namespaces, GUIDs,
and resource wiring stay correct.
- Common commands:
- `dotnet new winui -n MyApp`
- `dotnet new winui-page -n SettingsPage --project .\MyApp\MyApp.csproj`
- `dotnet new winui-usercontrol -n ProfileCard --project .\MyApp\MyApp.csproj`
- Discover available scaffolds with `dotnet new winui --list` (shows supported
parameters such as `--dotnet-version`).
- Need a newer TFM? Supply `--dotnet-version net10.0` during scaffold or edit
`<TargetFramework>` afterward before the first build.
### Prerequisites
- **Developer Mode must be enabled** on Windows. Verify with:
```powershell
# Check developer mode
Get-WindowsDeveloperLicense
# If not enabled: Settings -> System -> For developers -> Developer Mode -> On
```
- **`winapp` CLI** -- installed transitively via the `Microsoft.Windows.SDK.BuildTools.WinApp` NuGet reference (no separate install needed for `dotnet run`). To use `winapp` directly from the terminal for advanced scenarios (manifest editing, certificate management, packaging), install it standalone:
```powershell
winget install Microsoft.WinAppCli --source winget
```
### Detect Platform
**Always detect the machine's architecture first** -- never hardcode a platform value. Run this once at the start of every build/test session:
```powershell
# Detect the current machine's CPU architecture
# (returns AMD64 on x64 boxes, ARM64 on ARM64 boxes, x86 on 32-bit boxes)
$arch = $env:PROCESSOR_ARCHITECTURE
$Platform = if ($arch -eq 'AMD64') { 'x64' } else { $arch } # MSBuild expects x64/x86/ARM64
```
Use `$Platform` in all subsequent `dotnet` commands.
### Build
```powershell
# Run from the project folder containing the .csproj
cd <ProjectName>
# Detect platform (see above)
$arch = $env:PROCESSOR_ARCHITECTURE
$Platform = if ($arch -eq 'AMD64') { 'x64' } else { $arch }
# Debug build (matches current machine)
dotnet build -c Debug -p:Platform=$Platform
# Release build
dotnet build -c Release -p:Platform=$Platform
```
### Run with Package Identity (preferred)
The template references `Microsoft.Windows.SDK.BuildTools.WinApp`, which makes `dotnet run` register a loose-layout package via `winapp run` and launch the app via AUMID activation -- giving it the same package identity it would have in production:
```powershell
$arch = $env:PROCESSOR_ARCHITECTURE
$Platform = if ($arch -eq 'AMD64') { 'x64' } else { $arch }
dotnet run -c Debug -p:Platform=$Platform
```
The CLI prints the registered package's AUMID and the launched process's PID -- attach a debugger to that PID for runtime debugging.
#### Useful MSBuild knobs (set in `.csproj` `<PropertyGroup>`)
| Property | When to set |
|---|---|
| `EnableWinAppRunSupport=false` | Disable the `dotnet run` integration entirely (e.g., to launch unpackaged) |
| `WinAppRunUseExecutionAlias=true` | For console apps -- launches via `uap5:ExecutionAlias` so stdin/stdout stay in the terminal. Add the alias first with `winapp manifest add-alias`. |
| `WinAppRunNoLaunch=true` | Register the package but don't launch (attach your IDE's debugger before launch) |
| `WinAppLaunchArgs="--flag value"` | Pass arguments to the app on launch |
#### Manual `winapp run` (when not using `dotnet run`)
```powershell
# Read <TargetFramework> from .csproj first; example uses net10.0-windows10.0.26100.0
winapp run .\bin\$Platform\Debug\<TargetFramework>
# Pass args after -- to avoid escaping
winapp run .\bin\$Platform\Debug\<TargetFramework> -- --my-flag value
# Console app: keep stdin/stdout in the current terminal (requires uap5:ExecutionAlias)
winapp run .\bin\$Platform\Debug\<TargetFramework> --with-alias
# Wipe LocalState/settings between runs to test first-run behavior
winapp run .\bin\$Platform\Debug\<TargetFramework> --clean
```
#### Run Tests
```powershell
# Run from the test project folder
cd <ProjectName>.Tests
$arch = $env:PROCESSOR_ARCHITECTURE
$Platform = if ($arch -eq 'AMD64') { 'x64' } else { $arch }
dotnet test -c Debug -p:Platform=$Platform
```
### winapp CLI command reference
The `winapp` CLI is the canonical entry point for app-identity, packaging, certificate, and asset operations. Reach for it instead of hand-rolling `MakeAppx`/`SignTool`/`Add-AppxPackage` invocations.
| Scenario | Command | Notes |
|---|---|---|
| **Run/debug with identity (loose layout)** | `dotnet run` (or `winapp run <build-output>`) | Default for inner loop. Registers full loose-layout package. |
| **Console app inner loop** | Set `WinAppRunUseExecutionAlias=true` in `.csproj`, then `dotnet run` | Requires `uap5:ExecutionAlias` -- add via `winapp manifest add-alias`. |
| **Sparse identity on a single exe** | `winapp create-debug-identity .\bin\Debug\<TFM>\<ProjectName>.exe` | Use when the exe is outside the build folder, or for IDE F5 startup debugging. |
| **Stop debugging / clean up** | `winapp unregister` | Removes dev packages registered for the current project. |
| **Generate dev signing cert** | `winapp cert generate --manifest .\Package.appxmanifest --install` | Reads publisher from manifest. Stored as `devcert.pfx` in the project. |
| **Inspect a cert** | `winapp cert info .\devcert.pfx` | Verify subject matches manifest publisher. |
| **Sign a file** | `winapp sign .\MyApp.msix --cert .\devcert.pfx` | Wraps `signtool`. |
| **Build distribution MSIX** | `winapp pack .\bin\$Platform\Release\<TFM>\win-<rid> --cert .\devcert.pfx` | Auto-resolves `$targetnametoken$`, registers third-party WinRT components. |
| **Self-contained MSIX (bundles WinAppSDK)** | `winapp pack ... --self-contained` | No runtime dependency on the framework package. |
| **Regenerate Square44/Square150/etc. icons** | `winapp manifest update-assets .\branding\logo.svg` | SVG preferred -- rendered at all 5 scale and 14 targetsize variants. |
| **Add execution alias** | `winapp manifest add-alias` | Required for console-app inline I/O via `WinAppRunUseExecutionAlias`. |
| **Underlying SDK tools** | `winapp tool signtool ...`, `winapp tool makeappx ...` | Falls back to the raw [`Microsoft.Windows.SDK.BuildTools`](https://www.nuget.org/packages/Microsoft.Windows.SDK.BuildTools/) tools when needed. |
For full reference, see the [winapp CLI usage docs](https://github.com/microsoft/WinAppCli/blob/main/docs/usage.md) and the [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md).
### Fallback: register a loose layout manually
Only use this if you've explicitly disabled the `winapp` integration (`<EnableWinAppRunSupport>false</EnableWinAppRunSupport>`) or are debugging the deployment itself. The supported path is `dotnet run` / `winapp run`.
```powershell
$arch = $env:PROCESSOR_ARCHITECTURE
$Platform = if ($arch -eq 'AMD64') { 'x64' } else { $arch }
$Rid = $Platform.ToLower() # arm64, x64, x86
# Register the built MSIX package from the build output
# Read <TargetFramework> from .csproj to build the correct path.
Add-AppxPackage -Register ".\<ProjectName>\bin\$Platform\Debug\<TargetFramework>\win-$Rid\AppxManifest.xml"
```
> **Note:** Replace `<TargetFramework>` with the actual value from `.csproj` (e.g., `net10.0-windows10.0.26100.0`).
If the launch fails because an old instance is still running, terminate it with `taskkill /IM <ProjectName>.exe /F` before re-running.
## Key Rules (Always Enforced)
- **Every change must build and pass tests** -- Run `dotnet build` and `dotnet test` (see [Build, Run & Deploy](#build-run--deploy)) before considering any task complete.
- **Follow all instruction files** -- The detailed rules in `.github/instructions/` are authoritative. **You must actually open and read them** (not just acknowledge they exist) when working within their scope. See the trigger conditions in steps 5-8 above.
- **Web search before decompilation** -- When facing unknown types or build errors, always search the web / API docs first. Only use WinMD/ILDASM as a last resort (see [Troubleshooting Build Errors](#troubleshooting-build-errors)).
- **Use `winapp` for app-identity / packaging / signing** -- Don't hand-roll `MakeAppx`/`SignTool`/`Add-AppxPackage` invocations. The CLI keeps the manifest, certificate, and registration steps in sync.
## Windows AI Prerequisites
When integrating Windows AI APIs (Phi Silica, Windows Vision -- ImageDescription,
TextRecognizer, ImageScaler, etc.) -- see
[windows-apis.instructions.md](.github/instructions/windows-apis.instructions.md):
1. **Package identity is required.** All Windows AI APIs require the app to
run with package identity. The `dotnet run` flow described above already
provides this. If you're testing outside `dotnet run`, register identity
first with `winapp run` or `winapp create-debug-identity`.
2. **Manifest capabilities.** Add the capabilities each API requires to
`Package.appxmanifest` (commonly `runFullTrust`; some scenarios additionally
need `internetClient`). Check the API's docs page for the exact list.
3. **Hardware / OS gating.** Some APIs require a Copilot+ PC (NPU) or a
minimum Windows build. Always probe availability with the API's
`IsAvailable` / `EnsureReadyAsync` pattern (or equivalent) and provide a
graceful fallback for unsupported devices.
4. **Verify locally before checking in.** After capability or manifest
changes, re-run `dotnet run` (or `winapp run`) so the registered identity
reflects the updated manifest -- a stale registration will silently use
the old capability set.
+84
View File
@@ -0,0 +1,84 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.26100.0</TargetFramework>
<TargetPlatformMinVersion>10.0.17763.0</TargetPlatformMinVersion>
<RootNamespace>AmiReel_WinUI</RootNamespace>
<AssemblyName>AmiReel.WinUI</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<Platforms>x86;x64;ARM64</Platforms>
<RuntimeIdentifier Condition="'$(RuntimeIdentifier)' == ''">win-$([System.Runtime.InteropServices.RuntimeInformation]::ProcessArchitecture.ToString().ToLowerInvariant())</RuntimeIdentifier>
<PublishProfile Condition="Exists('Properties\PublishProfiles\win-$(Platform).pubxml')">win-$(Platform).pubxml</PublishProfile>
<UseWinUI>true</UseWinUI>
<WinUISDKReferences>false</WinUISDKReferences>
<EnableMsixTooling>true</EnableMsixTooling>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Content Include="Assets\SplashScreen.scale-200.png" />
<Content Include="Assets\LockScreenLogo.scale-200.png" />
<Content Include="Assets\Square150x150Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.scale-200.png" />
<Content Include="Assets\Square44x44Logo.targetsize-24_altform-unplated.png" />
<Content Include="Assets\Square44x44Logo.targetsize-48_altform-lightunplated.png" />
<Content Include="Assets\StoreLogo.png" />
<Content Include="Assets\AppIcon.ico" />
<Content Include="Assets\Wide310x150Logo.scale-200.png" />
</ItemGroup>
<ItemGroup>
<Manifest Include="$(ApplicationManifest)" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Models\*.cs" Link="Shared\Models\%(Filename)%(Extension)" />
<Compile Include="..\Services\*.cs" Link="Shared\Services\%(Filename)%(Extension)" />
</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>
<!--
Defining the "Msix" ProjectCapability here allows the Single-project MSIX Packaging
Tools extension to be activated for this project even if the Windows App SDK Nuget
package has not yet been restored.
-->
<ItemGroup Condition="'$(DisableMsixProjectCapabilityAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<ProjectCapability Include="Msix" />
</ItemGroup>
<!--
Microsoft.Windows.SDK.BuildTools.WinApp adds first-class support for
`dotnet run` on packaged WinUI apps: it hooks the .NET CLI Run target to
register a debug identity via the winapp CLI and launch the app with
package identity (AUMID).
Set <EnableWinAppRunSupport>false</EnableWinAppRunSupport> in a
<PropertyGroup> above to opt out.
-->
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.28000.2526" />
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.3.1" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools.WinApp" Version="0.5.0" />
</ItemGroup>
<!--
Defining the "HasPackageAndPublishMenuAddedByProject" property here allows the Solution
Explorer "Package and Publish" context menu entry to be enabled for this project even if
the Windows App SDK Nuget package has not yet been restored.
-->
<PropertyGroup Condition="'$(DisableHasPackageAndPublishMenuAddedByProject)'!='true' and '$(EnableMsixTooling)'=='true'">
<HasPackageAndPublishMenu>true</HasPackageAndPublishMenu>
</PropertyGroup>
<!-- Publish Properties -->
<PropertyGroup>
<PublishReadyToRun Condition="'$(Configuration)' == 'Debug'">False</PublishReadyToRun>
<PublishReadyToRun Condition="'$(Configuration)' != 'Debug'">True</PublishReadyToRun>
<PublishTrimmed Condition="'$(Configuration)' == 'Debug'">False</PublishTrimmed>
<PublishTrimmed Condition="'$(Configuration)' != 'Debug'">True</PublishTrimmed>
</PropertyGroup>
</Project>
+16
View File
@@ -0,0 +1,16 @@
<?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" />
<!-- Other merged dictionaries here -->
</ResourceDictionary.MergedDictionaries>
<!-- Other app resources here -->
</ResourceDictionary>
</Application.Resources>
</Application>
+47
View File
@@ -0,0 +1,47 @@
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();
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 361 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

+182
View File
@@ -0,0 +1,182 @@
<?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 ApplicationPageBackgroundThemeBrush}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.1*" />
<ColumnDefinition Width="16" />
<ColumnDefinition Width="0.9*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Spacing="16">
<Border Padding="18" CornerRadius="16" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="Source recordings" FontSize="28" FontWeight="SemiBold" />
<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="220" SelectionMode="None" />
</StackPanel>
</Border>
<Border Padding="18" CornerRadius="16" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="End card and output" FontSize="28" FontWeight="SemiBold" />
<TextBlock Text="Leave blank to use a built-in black end card." Opacity="0.8" />
<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" Header="End card image" />
<Button Grid.Row="0" Grid.Column="1" Content="Browse..." Click="BrowseEndCard_Click" VerticalAlignment="Bottom" />
<TextBox x:Name="OutputFolderBox" Grid.Row="1" Header="Output folder" />
<Button Grid.Row="1" Grid.Column="1" Content="Browse..." Click="BrowseOutput_Click" VerticalAlignment="Bottom" />
<TextBox x:Name="OutputNameBox" Grid.Row="2" Grid.ColumnSpan="2" Header="Output filename prefix" />
</Grid>
</StackPanel>
</Border>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="2" Spacing="16">
<Border Padding="18" CornerRadius="16" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="Render progress" FontSize="28" FontWeight="SemiBold" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock x:Name="StageText" Text="Ready" FontSize="18" FontWeight="SemiBold" />
<TextBlock x:Name="PercentText" Grid.Column="1" Text="0%" FontSize="18" FontWeight="SemiBold" />
</Grid>
<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" Opacity="0.85" />
</StackPanel>
</Border>
<Border Padding="18" CornerRadius="16" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="FFmpeg log" FontSize="28" FontWeight="SemiBold" />
<TextBox x:Name="LogBox"
Height="360"
IsReadOnly="True"
AcceptsReturn="True"
TextWrapping="Wrap"
ScrollViewer.VerticalScrollBarVisibility="Auto"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
FontFamily="Consolas"
FontSize="12" />
</StackPanel>
</Border>
</StackPanel>
<Grid Grid.Row="1" Grid.ColumnSpan="3">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="OpenOutputButton" Grid.Column="0" Content="Open output folder" Click="OpenOutput_Click" IsEnabled="False" HorizontalAlignment="Left" />
<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" />
</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" CornerRadius="12" Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<TextBlock Text="Appearance" FontSize="24" FontWeight="SemiBold" />
<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" CornerRadius="12" Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<TextBlock Text="Timing and screenshots" FontSize="24" FontWeight="SemiBold" />
<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" CornerRadius="12" Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<TextBlock Text="Video encoding" FontSize="24" FontWeight="SemiBold" />
<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" Opacity="0.8" />
</StackPanel>
</Border>
<Border Padding="14" CornerRadius="12" Background="{ThemeResource CardBackgroundFillColorSecondaryBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="1">
<StackPanel Spacing="10">
<TextBlock Text="FFmpeg tools" FontSize="24" FontWeight="SemiBold" />
<TextBlock Text="Optional override. Leave blank to use embedded FFmpeg and FFprobe." TextWrapping="WrapWholeWords" Opacity="0.8" />
<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>
+293
View File
@@ -0,0 +1,293 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Globalization;
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 CancellationTokenSource? _renderCancellation;
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);
OpenOutputButton.IsEnabled = true;
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", exception.Message);
}
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 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 AppendLog(string line)
{
_ = DispatcherQueue.TryEnqueue(() =>
{
LogBox.Text += line + Environment.NewLine;
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(),
string.Empty,
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 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();
}
}
+34
View File
@@ -0,0 +1,34 @@
<?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>
<TitleBar x:Name="AppTitleBar" Title="AmiReel">
<TitleBar.IconSource>
<ImageIconSource ImageSource="Assets/AppIcon.ico" />
</TitleBar.IconSource>
</TitleBar>
<!--
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>
+27
View File
@@ -0,0 +1,27 @@
using Microsoft.UI.Xaml;
// 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);
AppWindow.SetIcon("Assets/AppIcon.ico");
// Navigate the root frame to the main page on startup.
RootFrame.Navigate(typeof(MainPage));
}
}
+53
View File
@@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:systemai="http://schemas.microsoft.com/appx/manifest/systemai/windows10"
IgnorableNamespaces="uap rescap systemai">
<Identity
Name="F004AE73-5989-46F3-B913-E9A3A355713F"
Publisher="CN=AppPublisher"
Version="1.0.0.0" />
<mp:PhoneIdentity PhoneProductId="F004AE73-5989-46F3-B913-E9A3A355713F" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>AmiReel.WinUI</DisplayName>
<PublisherDisplayName>AppPublisher</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26226.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.26226.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements
DisplayName="AmiReel.WinUI"
Description="AmiReel.WinUI"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
<systemai:Capability Name="systemAIModels"/>
</Capabilities>
</Package>
@@ -0,0 +1,10 @@
{
"profiles": {
"AmiReel.WinUI (Package)": {
"commandName": "MsixPackage"
},
"AmiReel.WinUI (Unpackaged)": {
"commandName": "Project"
}
}
}
+19
View File
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="AmiReel.WinUI.app"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- The ID below informs the system that this application is compatible with OS features first introduced in Windows 10.
It is necessary to support features in unpackaged applications, for example the custom titlebar implementation.
For more info see https://docs.microsoft.com/windows/apps/windows-app-sdk/use-windows-app-sdk-run-time#declare-os-compatibility-in-your-application-manifest -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
</assembly>
+8
View File
@@ -18,6 +18,14 @@
<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" />
+30 -1
View File
@@ -1,19 +1,48 @@
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
+168 -166
View File
@@ -1,7 +1,7 @@
<Window x:Class="AmigaDB.VideoRenderer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="AmiReel" Height="720" Width="980" MinHeight="620" MinWidth="840"
Title="AmiReel" Height="860" Width="1280" MinHeight="780" MinWidth="1100"
WindowStartupLocation="CenterScreen">
<Grid Background="{DynamicResource PageBrush}">
<Grid>
@@ -22,183 +22,185 @@
</Grid>
<Grid Margin="18">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1.08*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="0.92*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<GroupBox Header="Source recordings">
<Grid>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="110"/></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"/>
</Grid>
</GroupBox>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="14"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<GroupBox Header="End card and output">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition/><RowDefinition/><RowDefinition/></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>
</StackPanel>
</ScrollViewer>
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel>
<GroupBox Header="Render progress">
<StackPanel>
<DockPanel Margin="0,0,0,8">
<TextBlock x:Name="StageText" Text="Ready" FontWeight="SemiBold"/>
<TextBlock x:Name="PercentText" Text="0%" Foreground="{DynamicResource AccentBrush}" DockPanel.Dock="Right"/>
</DockPanel>
<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 Header="FFmpeg log">
<TextBox x:Name="LogBox" Height="220" IsReadOnly="True" AcceptsReturn="True"
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
FontFamily="Consolas" FontSize="11" TextWrapping="NoWrap"/>
</GroupBox>
</StackPanel>
</ScrollViewer>
</Grid>
<DockPanel Grid.Row="1" Margin="0,14,0,0">
<Button x:Name="OpenOutputButton" Content="Open output folder" Click="OpenOutput_Click" IsEnabled="False"/>
<StackPanel Orientation="Horizontal" HorizontalAlignment="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="2" 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">
<GroupBox Header="Source recordings">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="98"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
<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" MouseDoubleClick="InputList_MouseDoubleClick"/>
<Button x:Name="PreviewSourceButton" Grid.Row="2" Content="Preview selected" Click="PreviewSource_Click"
HorizontalAlignment="Left" Margin="0,10,0,0" IsEnabled="False"/>
</Grid>
</GroupBox>
<TextBlock Text="Settings" FontSize="24" FontWeight="SemiBold" Margin="0,0,0,14"/>
<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>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<GroupBox Header="Appearance">
<StackPanel>
<TextBlock Text="Theme" Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,4"/>
<ComboBox x:Name="ThemeBox" SelectionChanged="ThemeBox_SelectionChanged">
<ComboBoxItem Content="Dark" Tag="Dark"/>
<ComboBoxItem Content="Light" Tag="Light"/>
</ComboBox>
</StackPanel>
</GroupBox>
<GroupBox 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 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 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>
<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"/>
<DockPanel Grid.Row="3" Margin="0,12,0,0" LastChildFill="False">
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
<Button x:Name="OpenOutputButton" Content="Open output folder" Click="OpenOutput_Click" IsEnabled="False" Margin="0,0,10,0"/>
<Button x:Name="PreviewRenderedButton" Content="Preview render" Click="PreviewRendered_Click" IsEnabled="False"/>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
<Button x:Name="OpenSettingsButton" Content="⚙" Click="OpenSettings_Click" Style="{StaticResource IconButton}" Margin="0,0,10,0" ToolTip="Settings"/>
<Button x:Name="CancelButton" Content="Cancel" Click="Cancel_Click" IsEnabled="False" Margin="0,0,10,0"/>
<Button x:Name="RenderButton" Content="Start render" Style="{StaticResource PrimaryButton}" Click="Render_Click"/>
</StackPanel>
</DockPanel>
<Border x:Name="SettingsOverlay" Grid.RowSpan="4" Background="#88060B14" Visibility="Collapsed">
<Grid Margin="40">
<Border Width="760"
MaxHeight="720"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="{DynamicResource PanelBrush}"
BorderBrush="{DynamicResource HeroBorderBrush}"
BorderThickness="1.5"
CornerRadius="24"
Padding="20">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Settings" FontSize="24" FontWeight="SemiBold" Margin="0,0,0,14"/>
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
<StackPanel>
<GroupBox Header="Appearance">
<StackPanel>
<TextBlock Text="Theme" Foreground="{DynamicResource MutedBrush}" Margin="0,0,0,4"/>
<ComboBox x:Name="ThemeBox" SelectionChanged="ThemeBox_SelectionChanged">
<ComboBoxItem Content="Dark" Tag="Dark"/>
<ComboBoxItem Content="Light" Tag="Light"/>
</ComboBox>
</StackPanel>
<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>
<GroupBox Header="FFmpeg tools">
<Grid>
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition/><RowDefinition/></Grid.RowDefinitions>
<TextBlock Text="Optional override. Leave blank to use embedded FFmpeg and FFprobe."
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,0" ToolTip="Path to ffprobe.exe"/>
<Button Grid.Row="2" Grid.Column="1" Content="Browse…" Click="BrowseFfprobe_Click"/>
</Grid>
</GroupBox>
</StackPanel>
</ScrollViewer>
<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>
<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>
<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>
</Grid>
</Border>
</Grid>
</Border>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</Window>
+68
View File
@@ -16,6 +16,7 @@ public partial class MainWindow : Window
private readonly ObservableCollection<string> _inputs = [];
private readonly AppSettings _loadedSettings;
private CancellationTokenSource? _renderCancellation;
private string? _lastRenderedFile;
public MainWindow()
{
@@ -26,6 +27,7 @@ public partial class MainWindow : Window
EndCardBox.Text = _loadedSettings.EndCardPath;
FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
FfprobePathBox.Text = _loadedSettings.FfprobePath;
PreviewPlayerPathBox.Text = _loadedSettings.PreviewPlayerPath;
TrimBox.Text = _loadedSettings.TrimStart;
FadeBox.Text = _loadedSettings.FadeSeconds;
HoldBox.Text = _loadedSettings.EndCardHoldSeconds;
@@ -70,6 +72,12 @@ public partial class MainWindow : Window
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)
@@ -89,7 +97,9 @@ public partial class MainWindow : Window
_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);
MessageBox.Show(this, "The AmiReel render completed successfully.", "Render complete",
MessageBoxButton.OK, MessageBoxImage.Information);
}
@@ -140,6 +150,27 @@ public partial class MainWindow : Window
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 InputList_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (InputList.SelectedItem is string path)
OpenPreview(path);
}
private void SetRendering(bool rendering)
{
RenderButton.IsEnabled = !rendering;
@@ -187,6 +218,7 @@ public partial class MainWindow : Window
EndCardBox.Text.Trim(),
FfmpegPathBox.Text.Trim(),
FfprobePathBox.Text.Trim(),
PreviewPlayerPathBox.Text.Trim(),
SelectedTheme(),
SelectedEncoder(),
TrimBox.Text.Trim(),
@@ -195,6 +227,42 @@ public partial class MainWindow : Window
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
{
if (!string.IsNullOrWhiteSpace(playerPath))
{
if (!File.Exists(playerPath))
throw new FileNotFoundException("Configured preview player was not found.", playerPath);
ProcessStartInfo customPlayer = new()
{
FileName = playerPath,
UseShellExecute = false
};
customPlayer.ArgumentList.Add(mediaPath);
Process.Start(customPlayer);
return;
}
Process.Start(new ProcessStartInfo(mediaPath) { UseShellExecute = true });
}
catch (Exception exception)
{
MessageBox.Show(this, exception.Message, "Preview failed", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private string SelectedTheme() => ((ComboBoxItem)ThemeBox.SelectedItem).Tag!.ToString()!;
private string SelectedEncoder() => ((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!;
+3
View File
@@ -5,6 +5,7 @@ public sealed record AppSettings(
string EndCardPath,
string FfmpegPath,
string FfprobePath,
string PreviewPlayerPath,
string Theme,
string Encoder,
string TrimStart,
@@ -17,6 +18,7 @@ public sealed record AppSettings(
"",
"",
"",
"",
"Dark",
"Auto",
"4.414",
@@ -29,6 +31,7 @@ public sealed record AppSettings(
EndCardPath ?? "",
FfmpegPath ?? "",
FfprobePath ?? "",
PreviewPlayerPath ?? "",
string.IsNullOrWhiteSpace(Theme) ? "Dark" : Theme,
string.IsNullOrWhiteSpace(Encoder) ? "Auto" : Encoder,
string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
+7 -5
View File
@@ -92,17 +92,18 @@ public sealed partial class ProcessRunner
private static bool IsNoise(ProcessOutput output)
{
if (output.Frame is not null || output.Time is not null)
return true;
string line = output.Line.TrimStart();
if (string.IsNullOrWhiteSpace(line))
return true;
if (line.StartsWith("frame=", StringComparison.OrdinalIgnoreCase))
return true;
if (output.Frame is not null || output.Time is not null)
return true;
if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("WARNING:", StringComparison.OrdinalIgnoreCase)
|| line.Contains("selected.", StringComparison.OrdinalIgnoreCase)
|| line.Contains("Falling back to FFmpeg from PATH", StringComparison.OrdinalIgnoreCase)
|| line.Contains("skipped", StringComparison.OrdinalIgnoreCase)
|| line.Contains("could not", StringComparison.OrdinalIgnoreCase)
|| line.Contains("failed", StringComparison.OrdinalIgnoreCase)
@@ -128,6 +129,7 @@ public sealed partial class ProcessRunner
|| line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("encoder :", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("title :", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("CPB properties:", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Press [q] to stop", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("[", StringComparison.OrdinalIgnoreCase)
|| line.StartsWith("Duration:", StringComparison.OrdinalIgnoreCase)
+10 -11
View File
@@ -48,7 +48,7 @@ public sealed class RenderPipeline
"-r", settings.FramesPerSecond.ToString(CultureInfo.InvariantCulture)];
joinArgs.AddRange(EncoderArguments(encoder));
joinArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", main]);
await RunStageAsync(tools.Ffmpeg, joinArgs, log, progress, "Joining recordings", 2, 48, joinDuration, token);
await RunStageAsync(tools.Ffmpeg, joinArgs, log, progress, "Joining recordings", 2, 48, joinDuration, settings.FramesPerSecond, token);
double originalDuration = await probe.ProbeDurationAsync(tools, main, token);
double trimmedDuration = originalDuration - settings.TrimStart;
@@ -104,7 +104,7 @@ public sealed class RenderPipeline
"-map", "[v]", "-map", "[a]", "-r", settings.FramesPerSecond.ToString()]);
finalArgs.AddRange(EncoderArguments(encoder));
finalArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-movflags", "+faststart", final]);
await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, token);
await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, settings.FramesPerSecond, token);
progress.Report(new(100, "Complete", final));
}
finally
@@ -120,7 +120,7 @@ public sealed class RenderPipeline
try
{
await _runner.RunAsync(tools.Ffmpeg,
["-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "color=c=black:s=128x128:r=1",
["-hide_banner", "-loglevel", "error", "-f", "lavfi", "-i", "color=c=black:s=640x360:r=1",
"-frames:v", "1", "-an", "-c:v", "h264_nvenc", "-preset", "p6", "-f", "null", "-"],
null, null, token);
nvenc = true;
@@ -129,10 +129,9 @@ public sealed class RenderPipeline
{
nvenc = false;
}
if (nvenc) { log("NVIDIA NVENC selected."); return "nvenc"; }
if (nvenc) return "nvenc";
if (requested == EncoderMode.NvidiaNvenc)
throw new InvalidOperationException("NVIDIA NVENC was requested but could not initialize.");
log("NVENC unavailable; CPU libx264 selected.");
log("WARNING: NVIDIA NVENC was requested but could not initialize. Falling back to CPU libx264.");
return "x264";
}
@@ -175,7 +174,6 @@ public sealed class RenderPipeline
"AmiReel found FFprobe on PATH, but it could not be started.",
token);
log($"Configured or embedded FFmpeg tools are unusable. Falling back to FFmpeg from PATH: '{systemTools.Ffmpeg}' and '{systemTools.Ffprobe}'.");
return systemTools;
}
}
@@ -235,7 +233,7 @@ public sealed class RenderPipeline
}
private async Task RunStageAsync(string executable, IEnumerable<string> args, Action<string> log,
IProgress<RenderProgress> progress, string stageName, double from, double to, double? duration, CancellationToken token)
IProgress<RenderProgress> progress, string stageName, double from, double to, double? duration, int framesPerSecond, CancellationToken token)
{
await _runner.RunAsync(executable, args, log, null, token, outputHandler: output =>
{
@@ -243,22 +241,23 @@ public sealed class RenderPipeline
return;
double percent = from + Math.Min(1, time.TotalSeconds / duration.Value) * (to - from);
string message = BuildProgressMessage(time, duration.Value, output.FramesPerSecond, output.Speed, output.Frame);
string message = BuildProgressMessage(time, duration.Value, output.FramesPerSecond, output.Speed, output.Frame, framesPerSecond);
progress.Report(new(percent, stageName, message));
});
}
private static string BuildProgressMessage(TimeSpan current, double durationSeconds, double? fps, double? speed, int? frame)
private static string BuildProgressMessage(TimeSpan current, double durationSeconds, double? fps, double? speed, int? frame, int framesPerSecond)
{
TimeSpan total = TimeSpan.FromSeconds(durationSeconds);
List<string> parts = [$"{current:hh\\:mm\\:ss} / {total:hh\\:mm\\:ss}"];
int totalFrames = Math.Max(1, (int)Math.Ceiling(durationSeconds * framesPerSecond));
if (fps is > 0)
parts.Add($"{fps:0.#} fps");
if (speed is > 0)
parts.Add($"{speed:0.##}x");
if (frame is > 0)
parts.Add($"frame {frame.Value:N0}");
parts.Add($"frame {frame.Value:N0} / {totalFrames:N0}");
return string.Join(" · ", parts);
}
+3
View File
@@ -19,6 +19,9 @@ public static class ToolExtractor
return new ToolPaths(ffmpegPath, ffprobePath);
}
if (TryResolveFromSystemPath(out ToolPaths? systemTools) && systemTools is not null)
return systemTools;
return await ExtractAsync(cancellationToken);
}