Polish AmiReel UI and improve render workflow
@@ -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 (IDE0001–IDE0090) 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 (IDE0001–IDE0090)](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.) |
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 361 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 574 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
@@ -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>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -18,6 +18,14 @@
|
|||||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
</PropertyGroup>
|
</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>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="ThirdParty\ffmpeg.exe" Condition="Exists('ThirdParty\ffmpeg.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffmpeg.exe" />
|
<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" />
|
<EmbeddedResource Include="ThirdParty\ffprobe.exe" Condition="Exists('ThirdParty\ffprobe.exe')" LogicalName="AmigaDB.VideoRenderer.Tools.ffprobe.exe" />
|
||||||
|
|||||||
@@ -1,19 +1,48 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 17
|
||||||
VisualStudioVersion = 17.0.31903.59
|
VisualStudioVersion = 17.0.31903.59
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmigaDB.VideoRenderer", "AmigaDB.VideoRenderer.csproj", "{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmigaDB.VideoRenderer", "AmigaDB.VideoRenderer.csproj", "{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AmiReel.WinUI", "AmiReel.WinUI\AmiReel.WinUI.csproj", "{7F1B3264-98F5-4B31-812F-7A4FB2A447BB}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Debug|x64 = Debug|x64
|
||||||
|
Debug|x86 = Debug|x86
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
|
Release|x64 = Release|x64
|
||||||
|
Release|x86 = Release|x86
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{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|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.ActiveCfg = Release|Any CPU
|
||||||
{7DF2BA53-4031-45EA-B4EF-27B86111FBFA}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Window x:Class="AmigaDB.VideoRenderer.MainWindow"
|
<Window x:Class="AmigaDB.VideoRenderer.MainWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
Title="AmiReel" Height="720" Width="980" MinHeight="620" MinWidth="840"
|
Title="AmiReel" Height="860" Width="1280" MinHeight="780" MinWidth="1100"
|
||||||
WindowStartupLocation="CenterScreen">
|
WindowStartupLocation="CenterScreen">
|
||||||
<Grid Background="{DynamicResource PageBrush}">
|
<Grid Background="{DynamicResource PageBrush}">
|
||||||
<Grid>
|
<Grid>
|
||||||
@@ -23,33 +23,36 @@
|
|||||||
|
|
||||||
<Grid Margin="18">
|
<Grid Margin="18">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
|
<RowDefinition Height="Auto"/>
|
||||||
<RowDefinition Height="*"/>
|
<RowDefinition Height="*"/>
|
||||||
<RowDefinition Height="Auto"/>
|
<RowDefinition Height="Auto"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="1.08*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
<ColumnDefinition Width="14"/>
|
<ColumnDefinition Width="14"/>
|
||||||
<ColumnDefinition Width="0.92*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
|
||||||
<StackPanel>
|
|
||||||
<GroupBox Header="Source recordings">
|
<GroupBox Header="Source recordings">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="110"/></Grid.RowDefinitions>
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition Height="98"/><RowDefinition Height="Auto"/></Grid.RowDefinitions>
|
||||||
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
<Button Content="Add AVI files…" Click="AddInputs_Click" Margin="0,0,8,0"/>
|
<Button Content="Add AVI files…" Click="AddInputs_Click" Margin="0,0,8,0"/>
|
||||||
<Button Content="Clear" Click="ClearInputs_Click"/>
|
<Button Content="Clear" Click="ClearInputs_Click"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<ListBox x:Name="InputList" Grid.Row="1"/>
|
<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>
|
</Grid>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
<GroupBox Header="End card and output">
|
<GroupBox Grid.Column="2" Header="End card and output">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition/><RowDefinition/><RowDefinition/></Grid.RowDefinitions>
|
<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"/>
|
<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"/>
|
<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"/>
|
<Button Grid.Row="1" Grid.Column="1" Content="Browse…" Click="BrowseEndCard_Click" Margin="0,0,0,8"/>
|
||||||
@@ -58,42 +61,39 @@
|
|||||||
<TextBox x:Name="OutputNameBox" Grid.Row="3" Grid.ColumnSpan="2" Text="amigadb_intro" ToolTip="Output filename prefix"/>
|
<TextBox x:Name="OutputNameBox" Grid.Row="3" Grid.ColumnSpan="2" Text="amigadb_intro" ToolTip="Output filename prefix"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
</StackPanel>
|
</Grid>
|
||||||
</ScrollViewer>
|
|
||||||
|
|
||||||
<ScrollViewer Grid.Column="2" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
<GroupBox Grid.Row="1" Header="Render progress" Margin="0,14,0,0">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<GroupBox Header="Render progress">
|
<StackPanel Orientation="Horizontal" Margin="0,0,0,8">
|
||||||
<StackPanel>
|
|
||||||
<DockPanel Margin="0,0,0,8">
|
|
||||||
<TextBlock x:Name="StageText" Text="Ready" FontWeight="SemiBold"/>
|
<TextBlock x:Name="StageText" Text="Ready" FontWeight="SemiBold"/>
|
||||||
<TextBlock x:Name="PercentText" Text="0%" Foreground="{DynamicResource AccentBrush}" DockPanel.Dock="Right"/>
|
<TextBlock x:Name="PercentText" Text="0%" Foreground="{DynamicResource AccentBrush}" Margin="6,0,0,0"/>
|
||||||
</DockPanel>
|
</StackPanel>
|
||||||
<ProgressBar x:Name="RenderProgress" Minimum="0" Maximum="100" Foreground="{DynamicResource AccentBrush}"/>
|
<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."
|
<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"/>
|
Foreground="{DynamicResource MutedBrush}" TextWrapping="Wrap" Margin="0,8,0,0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|
||||||
<GroupBox Header="FFmpeg log">
|
<GroupBox Grid.Row="2" Header="FFmpeg log" Margin="0,14,0,0">
|
||||||
<TextBox x:Name="LogBox" Height="220" IsReadOnly="True" AcceptsReturn="True"
|
<TextBox x:Name="LogBox" IsReadOnly="True" AcceptsReturn="True"
|
||||||
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"
|
||||||
FontFamily="Consolas" FontSize="11" TextWrapping="NoWrap"/>
|
FontFamily="Consolas" FontSize="11" TextWrapping="NoWrap"/>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
</StackPanel>
|
|
||||||
</ScrollViewer>
|
|
||||||
</Grid>
|
|
||||||
|
|
||||||
<DockPanel Grid.Row="1" Margin="0,14,0,0">
|
<DockPanel Grid.Row="3" Margin="0,12,0,0" LastChildFill="False">
|
||||||
<Button x:Name="OpenOutputButton" Content="Open output folder" Click="OpenOutput_Click" IsEnabled="False"/>
|
<StackPanel Orientation="Horizontal" DockPanel.Dock="Left">
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
<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="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="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"/>
|
<Button x:Name="RenderButton" Content="Start render" Style="{StaticResource PrimaryButton}" Click="Render_Click"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
||||||
<Border x:Name="SettingsOverlay" Grid.RowSpan="2" Background="#88060B14" Visibility="Collapsed">
|
<Border x:Name="SettingsOverlay" Grid.RowSpan="4" Background="#88060B14" Visibility="Collapsed">
|
||||||
<Grid Margin="40">
|
<Grid Margin="40">
|
||||||
<Border Width="760"
|
<Border Width="760"
|
||||||
MaxHeight="720"
|
MaxHeight="720"
|
||||||
@@ -172,13 +172,15 @@
|
|||||||
<GroupBox Header="FFmpeg tools">
|
<GroupBox Header="FFmpeg tools">
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
|
||||||
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition/><RowDefinition/></Grid.RowDefinitions>
|
<Grid.RowDefinitions><RowDefinition Height="Auto"/><RowDefinition/><RowDefinition/><RowDefinition/></Grid.RowDefinitions>
|
||||||
<TextBlock Text="Optional override. Leave blank to use embedded FFmpeg and FFprobe."
|
<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"/>
|
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"/>
|
<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"/>
|
<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"/>
|
<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"/>
|
<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>
|
</Grid>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ public partial class MainWindow : Window
|
|||||||
private readonly ObservableCollection<string> _inputs = [];
|
private readonly ObservableCollection<string> _inputs = [];
|
||||||
private readonly AppSettings _loadedSettings;
|
private readonly AppSettings _loadedSettings;
|
||||||
private CancellationTokenSource? _renderCancellation;
|
private CancellationTokenSource? _renderCancellation;
|
||||||
|
private string? _lastRenderedFile;
|
||||||
|
|
||||||
public MainWindow()
|
public MainWindow()
|
||||||
{
|
{
|
||||||
@@ -26,6 +27,7 @@ public partial class MainWindow : Window
|
|||||||
EndCardBox.Text = _loadedSettings.EndCardPath;
|
EndCardBox.Text = _loadedSettings.EndCardPath;
|
||||||
FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
|
FfmpegPathBox.Text = _loadedSettings.FfmpegPath;
|
||||||
FfprobePathBox.Text = _loadedSettings.FfprobePath;
|
FfprobePathBox.Text = _loadedSettings.FfprobePath;
|
||||||
|
PreviewPlayerPathBox.Text = _loadedSettings.PreviewPlayerPath;
|
||||||
TrimBox.Text = _loadedSettings.TrimStart;
|
TrimBox.Text = _loadedSettings.TrimStart;
|
||||||
FadeBox.Text = _loadedSettings.FadeSeconds;
|
FadeBox.Text = _loadedSettings.FadeSeconds;
|
||||||
HoldBox.Text = _loadedSettings.EndCardHoldSeconds;
|
HoldBox.Text = _loadedSettings.EndCardHoldSeconds;
|
||||||
@@ -70,6 +72,12 @@ public partial class MainWindow : Window
|
|||||||
if (dialog.ShowDialog(this) == true) FfprobePathBox.Text = dialog.FileName;
|
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 OpenSettings_Click(object sender, RoutedEventArgs e) => SettingsOverlay.Visibility = Visibility.Visible;
|
||||||
|
|
||||||
private void CloseSettings_Click(object sender, RoutedEventArgs e)
|
private void CloseSettings_Click(object sender, RoutedEventArgs e)
|
||||||
@@ -89,7 +97,9 @@ public partial class MainWindow : Window
|
|||||||
_renderCancellation = new CancellationTokenSource();
|
_renderCancellation = new CancellationTokenSource();
|
||||||
Progress<RenderProgress> progress = new(UpdateProgress);
|
Progress<RenderProgress> progress = new(UpdateProgress);
|
||||||
await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
|
await new RenderPipeline().RenderAsync(settings, progress, AppendLog, _renderCancellation.Token);
|
||||||
|
_lastRenderedFile = Path.Combine(settings.OutputDirectory, settings.OutputName + "_final.mp4");
|
||||||
OpenOutputButton.IsEnabled = true;
|
OpenOutputButton.IsEnabled = true;
|
||||||
|
PreviewRenderedButton.IsEnabled = File.Exists(_lastRenderedFile);
|
||||||
MessageBox.Show(this, "The AmiReel render completed successfully.", "Render complete",
|
MessageBox.Show(this, "The AmiReel render completed successfully.", "Render complete",
|
||||||
MessageBoxButton.OK, MessageBoxImage.Information);
|
MessageBoxButton.OK, MessageBoxImage.Information);
|
||||||
}
|
}
|
||||||
@@ -140,6 +150,27 @@ public partial class MainWindow : Window
|
|||||||
Process.Start(new ProcessStartInfo("explorer.exe", OutputFolderBox.Text) { UseShellExecute = true });
|
Process.Start(new ProcessStartInfo("explorer.exe", OutputFolderBox.Text) { UseShellExecute = true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PreviewSource_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
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)
|
private void SetRendering(bool rendering)
|
||||||
{
|
{
|
||||||
RenderButton.IsEnabled = !rendering;
|
RenderButton.IsEnabled = !rendering;
|
||||||
@@ -187,6 +218,7 @@ public partial class MainWindow : Window
|
|||||||
EndCardBox.Text.Trim(),
|
EndCardBox.Text.Trim(),
|
||||||
FfmpegPathBox.Text.Trim(),
|
FfmpegPathBox.Text.Trim(),
|
||||||
FfprobePathBox.Text.Trim(),
|
FfprobePathBox.Text.Trim(),
|
||||||
|
PreviewPlayerPathBox.Text.Trim(),
|
||||||
SelectedTheme(),
|
SelectedTheme(),
|
||||||
SelectedEncoder(),
|
SelectedEncoder(),
|
||||||
TrimBox.Text.Trim(),
|
TrimBox.Text.Trim(),
|
||||||
@@ -195,6 +227,42 @@ public partial class MainWindow : Window
|
|||||||
IntervalBox.Text.Trim()));
|
IntervalBox.Text.Trim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenPreview(string mediaPath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(mediaPath))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, "The selected media file was not found.", "Preview unavailable",
|
||||||
|
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string playerPath = PreviewPlayerPathBox.Text.Trim();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
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 SelectedTheme() => ((ComboBoxItem)ThemeBox.SelectedItem).Tag!.ToString()!;
|
||||||
private string SelectedEncoder() => ((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!;
|
private string SelectedEncoder() => ((ComboBoxItem)EncoderBox.SelectedItem).Tag!.ToString()!;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ public sealed record AppSettings(
|
|||||||
string EndCardPath,
|
string EndCardPath,
|
||||||
string FfmpegPath,
|
string FfmpegPath,
|
||||||
string FfprobePath,
|
string FfprobePath,
|
||||||
|
string PreviewPlayerPath,
|
||||||
string Theme,
|
string Theme,
|
||||||
string Encoder,
|
string Encoder,
|
||||||
string TrimStart,
|
string TrimStart,
|
||||||
@@ -17,6 +18,7 @@ public sealed record AppSettings(
|
|||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
"",
|
||||||
"Dark",
|
"Dark",
|
||||||
"Auto",
|
"Auto",
|
||||||
"4.414",
|
"4.414",
|
||||||
@@ -29,6 +31,7 @@ public sealed record AppSettings(
|
|||||||
EndCardPath ?? "",
|
EndCardPath ?? "",
|
||||||
FfmpegPath ?? "",
|
FfmpegPath ?? "",
|
||||||
FfprobePath ?? "",
|
FfprobePath ?? "",
|
||||||
|
PreviewPlayerPath ?? "",
|
||||||
string.IsNullOrWhiteSpace(Theme) ? "Dark" : Theme,
|
string.IsNullOrWhiteSpace(Theme) ? "Dark" : Theme,
|
||||||
string.IsNullOrWhiteSpace(Encoder) ? "Auto" : Encoder,
|
string.IsNullOrWhiteSpace(Encoder) ? "Auto" : Encoder,
|
||||||
string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
|
string.IsNullOrWhiteSpace(TrimStart) ? "4.414" : TrimStart,
|
||||||
|
|||||||
@@ -92,17 +92,18 @@ public sealed partial class ProcessRunner
|
|||||||
|
|
||||||
private static bool IsNoise(ProcessOutput output)
|
private static bool IsNoise(ProcessOutput output)
|
||||||
{
|
{
|
||||||
if (output.Frame is not null || output.Time is not null)
|
|
||||||
return true;
|
|
||||||
|
|
||||||
string line = output.Line.TrimStart();
|
string line = output.Line.TrimStart();
|
||||||
if (string.IsNullOrWhiteSpace(line))
|
if (string.IsNullOrWhiteSpace(line))
|
||||||
return true;
|
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)
|
if (line.StartsWith("ERROR:", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.StartsWith("WARNING:", 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("skipped", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.Contains("could not", StringComparison.OrdinalIgnoreCase)
|
|| line.Contains("could not", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.Contains("failed", StringComparison.OrdinalIgnoreCase)
|
|| line.Contains("failed", StringComparison.OrdinalIgnoreCase)
|
||||||
@@ -128,6 +129,7 @@ public sealed partial class ProcessRunner
|
|||||||
|| line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase)
|
|| line.StartsWith("Side data:", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.StartsWith("encoder :", StringComparison.OrdinalIgnoreCase)
|
|| line.StartsWith("encoder :", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.StartsWith("title :", StringComparison.OrdinalIgnoreCase)
|
|| line.StartsWith("title :", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| line.StartsWith("CPB properties:", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.StartsWith("Press [q] to stop", StringComparison.OrdinalIgnoreCase)
|
|| line.StartsWith("Press [q] to stop", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.StartsWith("[", StringComparison.OrdinalIgnoreCase)
|
|| line.StartsWith("[", StringComparison.OrdinalIgnoreCase)
|
||||||
|| line.StartsWith("Duration:", StringComparison.OrdinalIgnoreCase)
|
|| line.StartsWith("Duration:", StringComparison.OrdinalIgnoreCase)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public sealed class RenderPipeline
|
|||||||
"-r", settings.FramesPerSecond.ToString(CultureInfo.InvariantCulture)];
|
"-r", settings.FramesPerSecond.ToString(CultureInfo.InvariantCulture)];
|
||||||
joinArgs.AddRange(EncoderArguments(encoder));
|
joinArgs.AddRange(EncoderArguments(encoder));
|
||||||
joinArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", main]);
|
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 originalDuration = await probe.ProbeDurationAsync(tools, main, token);
|
||||||
double trimmedDuration = originalDuration - settings.TrimStart;
|
double trimmedDuration = originalDuration - settings.TrimStart;
|
||||||
@@ -104,7 +104,7 @@ public sealed class RenderPipeline
|
|||||||
"-map", "[v]", "-map", "[a]", "-r", settings.FramesPerSecond.ToString()]);
|
"-map", "[v]", "-map", "[a]", "-r", settings.FramesPerSecond.ToString()]);
|
||||||
finalArgs.AddRange(EncoderArguments(encoder));
|
finalArgs.AddRange(EncoderArguments(encoder));
|
||||||
finalArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-movflags", "+faststart", final]);
|
finalArgs.AddRange(["-c:a", "aac", "-b:a", "384k", "-ar", "48000", "-movflags", "+faststart", final]);
|
||||||
await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, token);
|
await RunStageAsync(tools.Ffmpeg, finalArgs, log, progress, "Final render", 60, 99, finalDuration, settings.FramesPerSecond, token);
|
||||||
progress.Report(new(100, "Complete", final));
|
progress.Report(new(100, "Complete", final));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -120,7 +120,7 @@ public sealed class RenderPipeline
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _runner.RunAsync(tools.Ffmpeg,
|
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", "-"],
|
"-frames:v", "1", "-an", "-c:v", "h264_nvenc", "-preset", "p6", "-f", "null", "-"],
|
||||||
null, null, token);
|
null, null, token);
|
||||||
nvenc = true;
|
nvenc = true;
|
||||||
@@ -129,10 +129,9 @@ public sealed class RenderPipeline
|
|||||||
{
|
{
|
||||||
nvenc = false;
|
nvenc = false;
|
||||||
}
|
}
|
||||||
if (nvenc) { log("NVIDIA NVENC selected."); return "nvenc"; }
|
if (nvenc) return "nvenc";
|
||||||
if (requested == EncoderMode.NvidiaNvenc)
|
if (requested == EncoderMode.NvidiaNvenc)
|
||||||
throw new InvalidOperationException("NVIDIA NVENC was requested but could not initialize.");
|
log("WARNING: NVIDIA NVENC was requested but could not initialize. Falling back to CPU libx264.");
|
||||||
log("NVENC unavailable; CPU libx264 selected.");
|
|
||||||
return "x264";
|
return "x264";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,7 +174,6 @@ public sealed class RenderPipeline
|
|||||||
"AmiReel found FFprobe on PATH, but it could not be started.",
|
"AmiReel found FFprobe on PATH, but it could not be started.",
|
||||||
token);
|
token);
|
||||||
|
|
||||||
log($"Configured or embedded FFmpeg tools are unusable. Falling back to FFmpeg from PATH: '{systemTools.Ffmpeg}' and '{systemTools.Ffprobe}'.");
|
|
||||||
return systemTools;
|
return systemTools;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -235,7 +233,7 @@ public sealed class RenderPipeline
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async Task RunStageAsync(string executable, IEnumerable<string> args, Action<string> log,
|
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 =>
|
await _runner.RunAsync(executable, args, log, null, token, outputHandler: output =>
|
||||||
{
|
{
|
||||||
@@ -243,22 +241,23 @@ public sealed class RenderPipeline
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
double percent = from + Math.Min(1, time.TotalSeconds / duration.Value) * (to - from);
|
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));
|
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);
|
TimeSpan total = TimeSpan.FromSeconds(durationSeconds);
|
||||||
List<string> parts = [$"{current:hh\\:mm\\:ss} / {total:hh\\:mm\\:ss}"];
|
List<string> parts = [$"{current:hh\\:mm\\:ss} / {total:hh\\:mm\\:ss}"];
|
||||||
|
int totalFrames = Math.Max(1, (int)Math.Ceiling(durationSeconds * framesPerSecond));
|
||||||
|
|
||||||
if (fps is > 0)
|
if (fps is > 0)
|
||||||
parts.Add($"{fps:0.#} fps");
|
parts.Add($"{fps:0.#} fps");
|
||||||
if (speed is > 0)
|
if (speed is > 0)
|
||||||
parts.Add($"{speed:0.##}x");
|
parts.Add($"{speed:0.##}x");
|
||||||
if (frame is > 0)
|
if (frame is > 0)
|
||||||
parts.Add($"frame {frame.Value:N0}");
|
parts.Add($"frame {frame.Value:N0} / {totalFrames:N0}");
|
||||||
|
|
||||||
return string.Join(" · ", parts);
|
return string.Join(" · ", parts);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ public static class ToolExtractor
|
|||||||
return new ToolPaths(ffmpegPath, ffprobePath);
|
return new ToolPaths(ffmpegPath, ffprobePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TryResolveFromSystemPath(out ToolPaths? systemTools) && systemTools is not null)
|
||||||
|
return systemTools;
|
||||||
|
|
||||||
return await ExtractAsync(cancellationToken);
|
return await ExtractAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||