mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-02 09:19:48 +01:00
Compare commits
14 Commits
issue/2543
...
yuleng/win
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fc6df7bd1 | ||
|
|
2d0396a9c9 | ||
|
|
c5f7c54d07 | ||
|
|
6580de3be1 | ||
|
|
67d96b0a13 | ||
|
|
c5d4f992c1 | ||
|
|
11b406feee | ||
|
|
256af8f6e0 | ||
|
|
87c65f9eec | ||
|
|
971c7e9fba | ||
|
|
83215647b7 | ||
|
|
0973810511 | ||
|
|
a7c8951f6c | ||
|
|
a35c0579f0 |
15
.github/actions/spell-check/expect.txt
vendored
15
.github/actions/spell-check/expect.txt
vendored
@@ -635,6 +635,7 @@ GMEM
|
||||
GNumber
|
||||
googleai
|
||||
googlegemini
|
||||
Gotchas
|
||||
gpedit
|
||||
gpo
|
||||
GPOCA
|
||||
@@ -647,8 +648,6 @@ GSM
|
||||
gtm
|
||||
guiddata
|
||||
GUITHREADINFO
|
||||
Gotcha
|
||||
Gotchas
|
||||
GValue
|
||||
gwl
|
||||
GWLP
|
||||
@@ -894,9 +893,9 @@ Lclean
|
||||
Ldone
|
||||
Ldr
|
||||
LEFTALIGN
|
||||
leftclick
|
||||
LEFTSCROLLBAR
|
||||
LEFTTEXT
|
||||
leftclick
|
||||
LError
|
||||
LEVELID
|
||||
LExit
|
||||
@@ -1022,9 +1021,12 @@ MENUITEMINFO
|
||||
MENUITEMINFOW
|
||||
MERGECOPY
|
||||
MERGEPAINT
|
||||
Metacharacter
|
||||
metadatamatters
|
||||
Metadatas
|
||||
Metacharacter
|
||||
metafile
|
||||
Metacharacter
|
||||
mfc
|
||||
Mgmt
|
||||
Microwaved
|
||||
@@ -1071,7 +1073,7 @@ mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
MRM
|
||||
MRT
|
||||
Mrt
|
||||
mru
|
||||
MSAL
|
||||
msc
|
||||
@@ -1489,7 +1491,9 @@ regfile
|
||||
REGISTERCLASSFAILED
|
||||
REGISTRYHEADER
|
||||
REGISTRYPREVIEWEXT
|
||||
registryroot
|
||||
regkey
|
||||
regroot
|
||||
regsvr
|
||||
REINSTALLMODE
|
||||
releaseblog
|
||||
@@ -1534,7 +1538,6 @@ riid
|
||||
RKey
|
||||
RNumber
|
||||
rollups
|
||||
ROOTOWNER
|
||||
rop
|
||||
ROUNDSMALL
|
||||
ROWSETEXT
|
||||
@@ -2171,4 +2174,4 @@ Zoneszonabletester
|
||||
Zoomin
|
||||
zoomit
|
||||
ZOOMITX
|
||||
Zorder
|
||||
Zorder
|
||||
@@ -134,8 +134,8 @@
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariables.dll",
|
||||
"WinUI3Apps\\PowerToys.EnvironmentVariables.exe",
|
||||
|
||||
"PowerToys.ImageResizer.exe",
|
||||
"PowerToys.ImageResizer.dll",
|
||||
"WinUI3Apps\\PowerToys.ImageResizer.exe",
|
||||
"WinUI3Apps\\PowerToys.ImageResizer.dll",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.exe",
|
||||
"WinUI3Apps\\PowerToys.ImageResizerCLI.dll",
|
||||
"PowerToys.ImageResizerExt.dll",
|
||||
|
||||
@@ -91,6 +91,7 @@ extends:
|
||||
official: true
|
||||
codeSign: true
|
||||
runTests: false
|
||||
buildTests: false
|
||||
signingIdentity:
|
||||
serviceName: $(SigningServiceName)
|
||||
appId: $(SigningAppId)
|
||||
|
||||
@@ -258,6 +258,7 @@ jobs:
|
||||
-restore -graph
|
||||
/p:RestorePackagesConfig=true
|
||||
/p:CIBuild=true
|
||||
/p:BuildTests=${{ parameters.buildTests }}
|
||||
/bl:$(LogOutputDirectory)\build-0-main.binlog
|
||||
${{ parameters.additionalBuildOptions }}
|
||||
$(MSBuildCacheParameters)
|
||||
|
||||
@@ -59,6 +59,7 @@ stages:
|
||||
enableMsBuildCaching: ${{ parameters.enableMsBuildCaching }}
|
||||
msBuildCacheIsReadOnly: ${{ parameters.msBuildCacheIsReadOnly }}
|
||||
runTests: ${{ parameters.runTests }}
|
||||
buildTests: true
|
||||
useVSPreview: ${{ parameters.useVSPreview }}
|
||||
useLatestWinAppSDK: ${{ parameters.useLatestWinAppSDK }}
|
||||
${{ if eq(parameters.useLatestWinAppSDK, true) }}:
|
||||
@@ -78,7 +79,9 @@ stages:
|
||||
${{ else }}:
|
||||
name: SHINE-OSS-L
|
||||
${{ if eq(parameters.useVSPreview, true) }}:
|
||||
demands: ImageOverride -equals SHINE-VS17-Preview
|
||||
demands: ImageOverride -equals SHINE-VS18-Preview
|
||||
${{ else }}:
|
||||
demands: ImageOverride -equals SHINE-VS18-Latest
|
||||
buildConfigurations: [Release]
|
||||
official: false
|
||||
codeSign: false
|
||||
|
||||
@@ -90,9 +90,15 @@ if ($noticeMatch.Success) {
|
||||
$currentNoticePackageList = ""
|
||||
}
|
||||
|
||||
# Test-only packages that are allowed to be in NOTICE.md but not in the build
|
||||
# (e.g., when BuildTests=false, these packages won't appear in the NuGet list)
|
||||
$allowedExtraPackages = @(
|
||||
"- Moq"
|
||||
)
|
||||
|
||||
if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
{
|
||||
Write-Host -ForegroundColor Red "Notice.md does not match NuGet list."
|
||||
Write-Host -ForegroundColor Yellow "Notice.md does not exactly match NuGet list. Analyzing differences..."
|
||||
|
||||
# Show detailed differences
|
||||
$generatedPackages = $returnList -split "`r`n|`n" | Where-Object { $_.Trim() -ne "" } | Sort-Object
|
||||
@@ -105,7 +111,7 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
# Find packages in proj file list but not in NOTICE.md
|
||||
$missingFromNotice = $generatedPackages | Where-Object { $noticePackages -notcontains $_ }
|
||||
if ($missingFromNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice:"
|
||||
Write-Host -ForegroundColor Red "MissingFromNotice (ERROR - these must be added to NOTICE.md):"
|
||||
foreach ($pkg in $missingFromNotice) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
@@ -114,10 +120,23 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
|
||||
# Find packages in NOTICE.md but not in proj file list
|
||||
$extraInNotice = $noticePackages | Where-Object { $generatedPackages -notcontains $_ }
|
||||
if ($extraInNotice.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Yellow "ExtraInNotice:"
|
||||
foreach ($pkg in $extraInNotice) {
|
||||
Write-Host -ForegroundColor Yellow " $pkg"
|
||||
|
||||
# Filter out allowed extra packages (test-only dependencies)
|
||||
$unexpectedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -notcontains $_ }
|
||||
$allowedExtra = $extraInNotice | Where-Object { $allowedExtraPackages -contains $_ }
|
||||
|
||||
if ($allowedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Green "ExtraInNotice (OK - allowed test-only packages):"
|
||||
foreach ($pkg in $allowedExtra) {
|
||||
Write-Host -ForegroundColor Green " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
if ($unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "ExtraInNotice (ERROR - unexpected packages in NOTICE.md):"
|
||||
foreach ($pkg in $unexpectedExtra) {
|
||||
Write-Host -ForegroundColor Red " $pkg"
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
@@ -127,10 +146,17 @@ if (!$noticeFile.Trim().EndsWith($returnList.Trim()))
|
||||
Write-Host " Proj file list has $($generatedPackages.Count) packages"
|
||||
Write-Host " NOTICE.md has $($noticePackages.Count) packages"
|
||||
Write-Host " MissingFromNotice: $($missingFromNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice: $($extraInNotice.Count) packages"
|
||||
Write-Host " ExtraInNotice (allowed): $($allowedExtra.Count) packages"
|
||||
Write-Host " ExtraInNotice (unexpected): $($unexpectedExtra.Count) packages"
|
||||
Write-Host ""
|
||||
|
||||
exit 1
|
||||
# Fail if there are missing packages OR unexpected extra packages
|
||||
if ($missingFromNotice.Count -gt 0 -or $unexpectedExtra.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "FAILED: NOTICE.md mismatch detected."
|
||||
exit 1
|
||||
} else {
|
||||
Write-Host -ForegroundColor Green "PASSED: NOTICE.md matches (with allowed test-only packages)."
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
|
||||
270
CLAUDE.md
Normal file
270
CLAUDE.md
Normal file
@@ -0,0 +1,270 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
**Microsoft PowerToys** is a collection of utilities for power users to tune and streamline their Windows experience. The codebase includes 25+ utilities like FancyZones, PowerRename, Image Resizer, Command Palette, Keyboard Manager, and more.
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Prerequisites
|
||||
- Visual Studio 2022 17.4+
|
||||
- Windows 10 1803+ (April 2018 Update or newer)
|
||||
- Initialize submodules once: `git submodule update --init --recursive`
|
||||
- Run automated setup: `.\tools\build\setup-dev-environment.ps1`
|
||||
|
||||
### Common Build Commands
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| First build / NuGet restore | `tools\build\build-essentials.cmd` |
|
||||
| Build current folder | `tools\build\build.cmd` |
|
||||
| Build with options | `.\tools\build\build.ps1 -Platform x64 -Configuration Release` |
|
||||
| Build full solution | Open `PowerToys.slnx` in VS and build |
|
||||
| Build installer (Release only) | `.\tools\build\build-installer.ps1 -Platform x64 -Configuration Release` |
|
||||
|
||||
**Important Build Rules:**
|
||||
- Exit code 0 = success; non-zero = failure
|
||||
- On failure, check `build.<config>.<platform>.errors.log` next to the solution/project
|
||||
- For first build or missing NuGet packages, run `build-essentials.cmd` first
|
||||
- Use one terminal per operation (build → test). Don't switch terminals mid-flow
|
||||
- After making changes, `cd` to the project folder (`.csproj`/`.vcxproj`) before building
|
||||
|
||||
### VS Code Tasks
|
||||
- Use `PT: Build Essentials (quick)` for fast runner + settings build
|
||||
- Use `PT: Build (quick)` to build the current directory
|
||||
|
||||
## Testing
|
||||
|
||||
### Finding Tests
|
||||
- Test projects follow the pattern: `<Product>*UnitTests`, `<Product>*UITests`, or `<Product>*FuzzTests`
|
||||
- Located as sibling folders or 1-2 levels up from product code
|
||||
- Examples: `src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj`
|
||||
|
||||
### Running Tests
|
||||
1. **Build the test project first**, wait for exit code 0
|
||||
2. Run via VS Test Explorer (`Ctrl+E, T`) or `vstest.console.exe` with filters
|
||||
3. **Avoid `dotnet test`** - use VS Test Explorer or vstest.console.exe
|
||||
|
||||
### Test Types
|
||||
- **Unit Tests**: Standard dev environment, no extra setup
|
||||
- **UI Tests**: Require WinAppDriver v1.2.1 and Developer Mode ([download](https://github.com/microsoft/WinAppDriver/releases/tag/v1.2.1))
|
||||
- **Fuzz Tests**: OneFuzz + .NET 8, required for modules handling file I/O or user input
|
||||
|
||||
### Test Discipline
|
||||
- Add or adjust tests when changing behavior
|
||||
- New modules handling file I/O or user input **must** implement fuzzing tests
|
||||
- State why tests were skipped if applicable (e.g., comment-only change)
|
||||
|
||||
## Architecture
|
||||
|
||||
### Repository Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── runner/ # Main PowerToys.exe, tray icon, module loader, hotkey management
|
||||
├── settings-ui/ # WinUI configuration app (communicates via named pipes)
|
||||
├── modules/ # Individual utilities (each in subfolder)
|
||||
│ ├── AdvancedPaste/
|
||||
│ ├── fancyzones/
|
||||
│ ├── imageresizer/
|
||||
│ ├── keyboardmanager/
|
||||
│ ├── launcher/ # PowerToys Run
|
||||
│ └── ...
|
||||
├── common/ # Shared code: logging, IPC, settings, DPI, telemetry
|
||||
└── dsc/ # Desired State Configuration support
|
||||
|
||||
tools/build/ # Build scripts and automation
|
||||
doc/devdocs/ # Developer documentation
|
||||
installer/ # WiX-based installer projects
|
||||
```
|
||||
|
||||
### Module Types
|
||||
|
||||
1. **Simple Modules** (e.g., Mouse Pointer Crosshairs, Find My Mouse)
|
||||
- Entirely contained in the module interface DLL
|
||||
- No external application
|
||||
|
||||
2. **External Application Launchers** (e.g., Color Picker)
|
||||
- Start a separate application (often WPF/WinUI)
|
||||
- Handle hotkey events
|
||||
- Communicate via named pipes or IPC
|
||||
|
||||
3. **Context Handler Modules** (e.g., PowerRename, Image Resizer)
|
||||
- Shell extensions for File Explorer
|
||||
- Add right-click context menu entries
|
||||
|
||||
4. **Registry-based Modules** (e.g., Power Preview)
|
||||
- Register preview handlers and thumbnail providers
|
||||
- Modify registry during enable/disable
|
||||
|
||||
### Module Interface
|
||||
|
||||
All PowerToys modules implement a standardized interface (`src/modules/interface/`) that defines:
|
||||
- Hotkey structure
|
||||
- Name and key for the utility
|
||||
- Enable/disable functionality
|
||||
- Configuration management
|
||||
- Telemetry settings
|
||||
- GPO configuration
|
||||
|
||||
### Settings System
|
||||
|
||||
- **Runner** (`src/runner/`) loads modules and manages their lifecycle
|
||||
- **Settings UI** (`src/settings-ui/`) is a separate process using WinUI 3
|
||||
- Communication via **named pipes** (IPC) between runner and settings
|
||||
- Settings stored as JSON files in `%LOCALAPPDATA%\Microsoft\PowerToys\`
|
||||
- Schema migrations must maintain backward compatibility
|
||||
|
||||
**Important**: When modifying IPC contracts or JSON schemas:
|
||||
- Update both runner and settings-ui
|
||||
- Maintain backward compatibility
|
||||
- See [doc/devdocs/core/settings/runner-ipc.md](doc/devdocs/core/settings/runner-ipc.md)
|
||||
|
||||
## Development Workflow
|
||||
|
||||
### Making Changes
|
||||
|
||||
1. **Before starting**: Ensure there's an issue to track the work
|
||||
2. **Read the file first**: Always use Read tool before modifying files
|
||||
3. **Follow existing patterns**: Match the style and structure of surrounding code
|
||||
4. **Atomic PRs**: One logical change per PR, no drive-by refactors
|
||||
5. **Build discipline**:
|
||||
- `cd` to project folder after making changes
|
||||
- Build using `tools/build/build.cmd`
|
||||
- Wait for exit code 0 before proceeding
|
||||
6. **Test changes**: Build and run tests for affected modules
|
||||
7. **Update signing**: Add new DLLs/executables to `.pipelines/ESRPSigning_core.json`
|
||||
|
||||
### CLI Tools
|
||||
|
||||
Several modules now have CLI support (FancyZones, Image Resizer, File Locksmith):
|
||||
- Use **System.CommandLine** library for argument parsing
|
||||
- Follow `--kebab-case` for long options, `-x` for short
|
||||
- Exit codes: 0 = success, non-zero = failure
|
||||
- Log to both console and file using `ManagedCommon.Logger`
|
||||
- Reference: [doc/devdocs/cli-conventions.md](doc/devdocs/cli-conventions.md)
|
||||
|
||||
### Localization
|
||||
|
||||
- Localization is handled exclusively by internal Microsoft team
|
||||
- **Do not** submit PRs for localization changes
|
||||
- File issues for localization bugs instead
|
||||
|
||||
## Code Style and Conventions
|
||||
|
||||
### Style Enforcement
|
||||
|
||||
- **C#**: Use `src/.editorconfig` and StyleCop.Analyzers (enforced in build)
|
||||
- **C++**: Use `.clang-format` (press `Ctrl+K Ctrl+D` in VS to format)
|
||||
- **XAML**: Use XamlStyler (`.\.pipelines\applyXamlStyling.ps1 -Main`)
|
||||
|
||||
### Formatting
|
||||
|
||||
- Follow existing patterns in the file you're editing
|
||||
- For new code, follow Modern C++ practices and [C++ Core Guidelines](https://github.com/isocpp/CppCoreGuidelines)
|
||||
- C++ formatting script: `src/codeAnalysis/format_sources.ps1`
|
||||
|
||||
### Logging
|
||||
|
||||
- **C++**: Use spdlog (SPD logs) via `src/common/logger/`
|
||||
- **C#**: Use `ManagedCommon.Logger`
|
||||
- **Critical**: Keep hot paths quiet (no logging in hooks or tight loops)
|
||||
- Detailed guidance: [doc/devdocs/development/logging.md](doc/devdocs/development/logging.md)
|
||||
|
||||
### Dependencies
|
||||
|
||||
- MIT license generally acceptable; other licenses require PM approval
|
||||
- All external packages must be listed in `NOTICE.md`
|
||||
- Update `Directory.Packages.props` for NuGet packages (centralized package management)
|
||||
- Sign new DLLs by adding to signing config
|
||||
|
||||
## Critical Areas Requiring Extra Care
|
||||
|
||||
| Area | Concern | Reference |
|
||||
|------|---------|-----------|
|
||||
| `src/common/` | ABI breaks affect all modules | [.github/instructions/common-libraries.instructions.md](.github/instructions/common-libraries.instructions.md) |
|
||||
| `src/runner/`, `src/settings-ui/` | IPC contracts, schema migrations | [.github/instructions/runner-settings-ui.instructions.md](.github/instructions/runner-settings-ui.instructions.md) |
|
||||
| Installer files | Release impact | Careful review required |
|
||||
| Elevation/GPO logic | Security implications | Confirm no policy handling regression |
|
||||
|
||||
## Key Development Rules
|
||||
|
||||
### Do
|
||||
- Add tests when changing behavior
|
||||
- Follow existing code patterns
|
||||
- Use atomic PRs (one logical change)
|
||||
- Ask for clarification when spec is ambiguous
|
||||
- Check exit codes (`0` = success)
|
||||
- Read files before modifying them
|
||||
- Update `NOTICE.md` when adding dependencies
|
||||
|
||||
### Don't
|
||||
- Don't break IPC/JSON contracts without updating both runner and settings-ui
|
||||
- Don't add noisy logs in hot paths (hooks, tight loops)
|
||||
- Don't introduce third-party dependencies without PM approval
|
||||
- Don't merge incomplete features into main (use feature branches)
|
||||
- Don't use `dotnet test` (use VS Test Explorer or vstest.console.exe)
|
||||
- Don't skip hooks (--no-verify) unless explicitly requested
|
||||
|
||||
## Special Testing Requirements
|
||||
|
||||
- **Mouse Without Borders**: Requires 2+ physical computers (not VMs)
|
||||
- **Multi-monitor utilities**: Test with 2+ monitors, different DPI settings
|
||||
- **File I/O or user input modules**: Must implement fuzzing tests
|
||||
|
||||
## Running PowerToys
|
||||
|
||||
### Debug Build
|
||||
- After building, run `x64\Debug\PowerToys.exe` directly
|
||||
- Some modules (PowerRename, ImageResizer, File Explorer extensions) require full installation
|
||||
|
||||
### Release Build
|
||||
- Build the installer: `.\tools\build\build-installer.ps1 -Platform x64 -Configuration Release`
|
||||
- Install from `installer\` output folder
|
||||
|
||||
## Common Issues
|
||||
|
||||
### Build Failures
|
||||
1. Check `build.<config>.<platform>.errors.log`
|
||||
2. Ensure submodules are initialized: `git submodule update --init --recursive`
|
||||
3. Run `build-essentials.cmd` to restore NuGet packages
|
||||
4. Check Visual Studio has required workloads (import `.vsconfig`)
|
||||
|
||||
### Missing DLLs at Runtime
|
||||
- Some modules require installation via the installer to register COM handlers/shell extensions
|
||||
- Build and install from `installer/` folder
|
||||
|
||||
## Documentation Index
|
||||
|
||||
### Essential Reading
|
||||
- [Architecture Overview](doc/devdocs/core/architecture.md)
|
||||
- [Coding Guidelines](doc/devdocs/development/guidelines.md)
|
||||
- [Coding Style](doc/devdocs/development/style.md)
|
||||
- [Build Guidelines](tools/build/BUILD-GUIDELINES.md)
|
||||
- [Module Interface](doc/devdocs/modules/interface.md)
|
||||
|
||||
### Advanced Topics
|
||||
- [Runner](doc/devdocs/core/runner.md)
|
||||
- [Settings System](doc/devdocs/core/settings/readme.md)
|
||||
- [Logging](doc/devdocs/development/logging.md)
|
||||
- [UI Tests](doc/devdocs/development/ui-tests.md)
|
||||
- [Fuzzing Tests](doc/devdocs/tools/fuzzingtesting.md)
|
||||
- [Installer](doc/devdocs/core/installer.md)
|
||||
|
||||
### Module-Specific Docs
|
||||
- Individual modules: `doc/devdocs/modules/<module-name>.md`
|
||||
- PowerToys Run plugins: `doc/devdocs/modules/launcher/plugins/`
|
||||
|
||||
## Validation Checklist
|
||||
|
||||
Before finishing work:
|
||||
- [ ] Build clean with exit code 0
|
||||
- [ ] Tests updated and passing locally
|
||||
- [ ] No unintended ABI breaks or schema changes
|
||||
- [ ] IPC contracts consistent between runner and settings-ui
|
||||
- [ ] New dependencies added to `NOTICE.md`
|
||||
- [ ] New binaries added to signing config (`.pipelines/ESRPSigning_core.json`)
|
||||
- [ ] PR is atomic (one logical change), with issue linked
|
||||
- [ ] Code follows existing patterns and style guidelines
|
||||
@@ -2,6 +2,12 @@
|
||||
<Project ToolsVersion="4.0"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
|
||||
<!-- Skip building C++ test projects when BuildTests=false -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<UsePrecompiledHeaders>false</UsePrecompiledHeaders>
|
||||
<RunCodeAnalysis>false</RunCodeAnalysis>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Project configurations -->
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
|
||||
@@ -19,6 +19,39 @@
|
||||
<PlatformTarget>$(Platform)</PlatformTarget>
|
||||
</PropertyGroup>
|
||||
|
||||
<!--
|
||||
Completely skip building test projects when BuildTests=false (e.g., Release pipeline).
|
||||
This avoids InternalsVisibleTo/signing issues by not compiling test code at all.
|
||||
Match: projects ending in Test, Tests, UnitTests, UITests, FuzzTests, or in a folder named Tests.
|
||||
Also matches projects starting with UnitTests- (e.g., UnitTests-CommonLib).
|
||||
Also removes all PackageReference/ProjectReference to prevent NuGet restore and dependency builds.
|
||||
Note: Checking both 'false' and 'False' to handle YAML boolean serialization.
|
||||
-->
|
||||
<PropertyGroup Condition="'$(BuildTests)' == 'false' or '$(BuildTests)' == 'False'">
|
||||
<_ProjectName>$(MSBuildProjectName)</_ProjectName>
|
||||
<!-- Match any project ending with "Test" or "Tests" (covers UnitTests, UITests, FuzzTests, etc.) -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Test'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.EndsWith('Tests'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects starting with UnitTests- or UITest- prefix -->
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UnitTests-'))">true</_IsSkippedTestProject>
|
||||
<_IsSkippedTestProject Condition="$(_ProjectName.StartsWith('UITest-'))">true</_IsSkippedTestProject>
|
||||
<!-- Match projects in a Tests folder -->
|
||||
<_IsSkippedTestProject Condition="$(MSBuildProjectDirectory.Contains('\Tests\'))">true</_IsSkippedTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<EnableDefaultItems>false</EnableDefaultItems>
|
||||
<EnableDefaultCompileItems>false</EnableDefaultCompileItems>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GenerateGlobalUsings>false</GenerateGlobalUsings>
|
||||
<ImplicitUsings>disable</ImplicitUsings>
|
||||
<!-- Disable all code analysis for skipped test projects -->
|
||||
<EnableNETAnalyzers>false</EnableNETAnalyzers>
|
||||
<RunAnalyzers>false</RunAnalyzers>
|
||||
<RunAnalyzersDuringBuild>false</RunAnalyzersDuringBuild>
|
||||
<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<Version>$(Version).0</Version>
|
||||
<RepositoryUrl>https://github.com/microsoft/PowerToys</RepositoryUrl>
|
||||
@@ -30,7 +63,9 @@
|
||||
<_PropertySheetDisplayName>PowerToys.Root.Props</_PropertySheetDisplayName>
|
||||
<ForceImportBeforeCppProps>$(MsbuildThisFileDirectory)\Cpp.Build.props</ForceImportBeforeCppProps>
|
||||
</PropertyGroup>
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj'">
|
||||
|
||||
|
||||
<ItemGroup Condition="'$(MSBuildProjectExtension)' == '.csproj' and '$(_IsSkippedTestProject)' != 'true'">
|
||||
<PackageReference Include="StyleCop.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -28,4 +28,41 @@
|
||||
<PropertyGroup Condition="'$(IgnoreExperimentalWarnings)' == 'true'">
|
||||
<NoWarn>$(NoWarn);CS8305;SA1500;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
<!-- Skipped test projects when BuildTests=false: no-op build and remove references.
|
||||
This must be in targets (not props) so it runs AFTER the project file adds its items. -->
|
||||
<PropertyGroup Condition="'$(_IsSkippedTestProject)' == 'true'">
|
||||
<BuildDependsOn />
|
||||
<CoreBuildDependsOn />
|
||||
<RebuildDependsOn />
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For C# projects: remove all items -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.csproj'">
|
||||
<PackageReference Remove="@(PackageReference)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<Reference Remove="@(Reference)" />
|
||||
<Compile Remove="@(Compile)" />
|
||||
<Content Remove="@(Content)" />
|
||||
<EmbeddedResource Remove="@(EmbeddedResource)" />
|
||||
<None Remove="@(None)" />
|
||||
<Using Remove="@(Using)" />
|
||||
<GlobalUsing Remove="@(GlobalUsing)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- For C++ projects (vcxproj): remove all compile/link items to prevent build -->
|
||||
<ItemGroup Condition="'$(_IsSkippedTestProject)' == 'true' and '$(MSBuildProjectExtension)' == '.vcxproj'">
|
||||
<ClCompile Remove="@(ClCompile)" />
|
||||
<ClInclude Remove="@(ClInclude)" />
|
||||
<Link Remove="@(Link)" />
|
||||
<Lib Remove="@(Lib)" />
|
||||
<ProjectReference Remove="@(ProjectReference)" />
|
||||
<None Remove="@(None)" />
|
||||
<ResourceCompile Remove="@(ResourceCompile)" />
|
||||
<Midl Remove="@(Midl)" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Note: For C++ skipped test projects, build is effectively skipped by removing all compile items above.
|
||||
We don't define empty Build/Rebuild/Clean targets here because MSBuild Target definitions with Condition
|
||||
on the Target element still override the default targets even when condition is false. -->
|
||||
</Project>
|
||||
|
||||
@@ -476,12 +476,15 @@
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
<!-- ImageResizer tests temporarily disabled - needs migration from WPF to WinUI3 APIs -->
|
||||
<!--
|
||||
<Folder Name="/modules/imageresizer/Tests/">
|
||||
<Project Path="src/modules/imageresizer/tests/ImageResizer.UnitTests.csproj">
|
||||
<Platform Solution="*|ARM64" Project="ARM64" />
|
||||
<Platform Solution="*|x64" Project="x64" />
|
||||
</Project>
|
||||
</Folder>
|
||||
-->
|
||||
<Folder Name="/modules/interface/">
|
||||
<File Path="src/modules/interface/powertoy_module_interface.h" />
|
||||
</Folder>
|
||||
|
||||
@@ -18,13 +18,28 @@ Advanced Paste is a PowerToys module that provides enhanced clipboard pasting wi
|
||||
|
||||
TODO: Add implementation details
|
||||
|
||||
### Paste with AI Preview
|
||||
|
||||
The "Show preview" setting (`ShowCustomPreview`) controls whether AI-generated results are displayed in a preview window before pasting. **The preview feature does not consume additional AI credits**—the preview displays the same AI response that was already generated, cached locally from a single API call.
|
||||
|
||||
The implementation flow:
|
||||
1. User initiates "Paste with AI" action
|
||||
2. A single AI API call is made via `ExecutePasteFormatAsync`
|
||||
3. The result is cached in `GeneratedResponses`
|
||||
4. If preview is enabled, the cached result is displayed in the preview UI
|
||||
5. User can paste the cached result without any additional API calls
|
||||
|
||||
See the `ExecutePasteFormatAsync(PasteFormat, PasteActionSource)` method in `OptionsViewModel.cs` for the implementation.
|
||||
|
||||
## Debugging
|
||||
|
||||
TODO: Add debugging information
|
||||
|
||||
## Settings
|
||||
|
||||
TODO: Add settings documentation
|
||||
| Setting | Description |
|
||||
|---------|-------------|
|
||||
| `ShowCustomPreview` | When enabled, shows AI-generated results in a preview window before pasting. Does not affect AI credit consumption. |
|
||||
|
||||
## Future Improvements
|
||||
|
||||
|
||||
@@ -367,6 +367,12 @@
|
||||
</RegistryKey>
|
||||
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
|
||||
</Component>
|
||||
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
|
||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
|
||||
</RegistryKey>
|
||||
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
|
||||
</Component>
|
||||
<?undef IdSafeLanguage?>
|
||||
<?undef CompGUIDPrefix?>
|
||||
<?endforeach?>
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
using WorkspacesEditor.Utils;
|
||||
|
||||
namespace WorkspacesEditor
|
||||
{
|
||||
/// <summary>
|
||||
@@ -11,9 +14,40 @@ namespace WorkspacesEditor
|
||||
/// </summary>
|
||||
public partial class OverlayWindow : Window
|
||||
{
|
||||
private int _targetX;
|
||||
private int _targetY;
|
||||
private int _targetWidth;
|
||||
private int _targetHeight;
|
||||
|
||||
public OverlayWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
SourceInitialized += OnWindowSourceInitialized;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the target bounds for the overlay window.
|
||||
/// The window will be positioned using DPI-unaware context after initialization.
|
||||
/// </summary>
|
||||
public void SetTargetBounds(int x, int y, int width, int height)
|
||||
{
|
||||
_targetX = x;
|
||||
_targetY = y;
|
||||
_targetWidth = width;
|
||||
_targetHeight = height;
|
||||
|
||||
// Set initial WPF properties (will be corrected after HWND creation)
|
||||
Left = x;
|
||||
Top = y;
|
||||
Width = width;
|
||||
Height = height;
|
||||
}
|
||||
|
||||
private void OnWindowSourceInitialized(object sender, EventArgs e)
|
||||
{
|
||||
// Reposition window using DPI-unaware context to match the virtual coordinates.
|
||||
// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Interop;
|
||||
|
||||
namespace WorkspacesEditor.Utils
|
||||
{
|
||||
@@ -17,6 +19,39 @@ namespace WorkspacesEditor.Utils
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||
|
||||
private const uint SWP_NOZORDER = 0x0004;
|
||||
private const uint SWP_NOACTIVATE = 0x0010;
|
||||
|
||||
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
|
||||
|
||||
/// <summary>
|
||||
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates.
|
||||
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||
/// </summary>
|
||||
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
|
||||
{
|
||||
var helper = new WindowInteropHelper(window).Handle;
|
||||
if (helper != IntPtr.Zero)
|
||||
{
|
||||
// Temporarily switch to DPI-unaware context to position window.
|
||||
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
|
||||
try
|
||||
{
|
||||
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetThreadDpiAwarenessContext(oldContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[DllImport("USER32.DLL")]
|
||||
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
|
||||
@@ -495,10 +495,10 @@ namespace WorkspacesEditor.ViewModels
|
||||
{
|
||||
var bounds = screen.Bounds;
|
||||
OverlayWindow overlayWindow = new OverlayWindow();
|
||||
overlayWindow.Top = bounds.Top;
|
||||
overlayWindow.Left = bounds.Left;
|
||||
overlayWindow.Width = bounds.Width;
|
||||
overlayWindow.Height = bounds.Height;
|
||||
|
||||
// Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups
|
||||
overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
|
||||
|
||||
overlayWindow.ShowActivated = true;
|
||||
overlayWindow.Topmost = true;
|
||||
overlayWindow.Show();
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\CoreCommonProps.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableCoreMrtTooling>false</EnableCoreMrtTooling>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
|
||||
@@ -84,7 +84,13 @@
|
||||
<WarningsNotAsErrors>IL2081;$(WarningsNotAsErrors)</WarningsNotAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- InternalsVisibleTo with public key for CI builds (signed assemblies) -->
|
||||
<ItemGroup Condition="'$(CIBuild)'=='true'">
|
||||
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests, PublicKey=002400000c80000014010000060200000024000052534131000800000100010085aad0bef0688d1b994a0d78e1fd29fc24ac34ed3d3ac3fb9b3d0c48386ba834aa880035060a8848b2d8adf58e670ed20914be3681a891c9c8c01eef2ab22872547c39be00af0e6c72485d7cfd1a51df8947d36ceba9989106b58abe79e6a3e71a01ed6bdc867012883e0b1a4d35b1b5eeed6df21e401bb0c22f2246ccb69979dc9e61eef262832ed0f2064853725a75485fa8a3efb7e027319c86dec03dc3b1bca2b5081bab52a627b9917450dfad534799e1c7af58683bdfa135f1518ff1ea60e90d7b993a6c87fd3dd93408e35d1296f9a7f9a97c5db56c0f3cc25ad11e9777f94d138b3cea53b9a8331c2e6dcb8d2ea94e18bf1163ff112a22dbd92d429a" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- InternalsVisibleTo without public key for local builds (unsigned assemblies) -->
|
||||
<ItemGroup Condition="'$(CIBuild)'!='true'">
|
||||
<InternalsVisibleTo Include="Microsoft.CommandPalette.Extensions.Toolkit.UnitTests" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -20,9 +20,4 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\ui\ImageResizerUI.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Force using WindowsDesktop runtime to ensure consistent dll versions with other projects -->
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.WindowsDesktop.App.WPF" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerContextMenu.base.rc ImageResizerContextMenu.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerContextMenu.base.rc ImageResizerContextMenu.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
@@ -50,10 +50,10 @@
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<!--<PreBuildEvent>
|
||||
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
|
||||
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
|
||||
</PreBuildEvent>
|
||||
</PreBuildEvent>-->
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
|
||||
<ClCompile>
|
||||
@@ -73,10 +73,10 @@ MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Comm
|
||||
<EnableUAC>false</EnableUAC>
|
||||
<ModuleDefinitionFile>Source.def</ModuleDefinitionFile>
|
||||
</Link>
|
||||
<PreBuildEvent>
|
||||
<!--<PreBuildEvent>
|
||||
<Command>del $(OutDir)\ImageResizerContextMenuPackage.msix /q
|
||||
MakeAppx.exe pack /d . /p $(OutDir)ImageResizerContextMenuPackage.msix /nv</Command>
|
||||
</PreBuildEvent>
|
||||
</PreBuildEvent>-->
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClInclude Include="framework.h" />
|
||||
|
||||
@@ -82,4 +82,4 @@ IDR_CONTEXTMENUHANDLER REGISTRY "ContextMenuHandler.rgs"
|
||||
|
||||
// Icon with lowest ID value placed first to ensure application icon
|
||||
// remains consistent on all systems.
|
||||
IDI_RESIZE_PICTURES ICON "..\ui\Resources\ImageResizer.ico"
|
||||
IDI_RESIZE_PICTURES ICON "..\\ui\\Assets\\ImageResizer\\ImageResizer.ico"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Target Name="GenerateResourceFiles" BeforeTargets="PrepareForBuild">
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(SolutionDir)tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerExt.base.rc ImageResizerExt.rc" />
|
||||
<Exec Command="powershell -NonInteractive -executionpolicy Unrestricted $(MSBuildThisFileDirectory)..\..\..\..\tools\build\convert-resx-to-rc.ps1 $(MSBuildThisFileDirectory) resource.base.h resource.h ImageResizerExt.base.rc ImageResizerExt.rc" />
|
||||
</Target>
|
||||
<PropertyGroup Label="Globals">
|
||||
<ProjectGuid>{0B43679E-EDFA-4DA0-AD30-F4628B308B1B}</ProjectGuid>
|
||||
|
||||
@@ -8,8 +8,11 @@
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>ImageResizer</RootNamespace>
|
||||
<AssemblyName>ImageResizer.Test</AssemblyName>
|
||||
|
||||
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\tests\$(AssemblyName)\</OutputPath>
|
||||
|
||||
<!-- Enable WPF for System.Windows.Media.Imaging support in tests -->
|
||||
<UseWPF>true</UseWPF>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
[TestClass]
|
||||
public class CustomSizeTests
|
||||
{
|
||||
[TestMethod]
|
||||
public void NameWorks()
|
||||
{
|
||||
var size = new CustomSize
|
||||
{
|
||||
Name = "Ignored",
|
||||
};
|
||||
|
||||
Assert.AreEqual(Resources.Input_Custom, size.Name);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,8 @@
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Test;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
@@ -31,27 +29,7 @@ namespace ImageResizer.Models
|
||||
Assert.AreEqual(nameof(ResizeSize.Name), e.Arguments.PropertyName);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NameReplacesTokens()
|
||||
{
|
||||
var args = new List<(string, string)>
|
||||
{
|
||||
("$small$", Resources.Small),
|
||||
("$medium$", Resources.Medium),
|
||||
("$large$", Resources.Large),
|
||||
("$phone$", Resources.Phone),
|
||||
};
|
||||
foreach (var (name, expected) in args)
|
||||
{
|
||||
var size = new ResizeSize
|
||||
{
|
||||
Name = name,
|
||||
};
|
||||
|
||||
Assert.AreEqual(expected, size.Name);
|
||||
}
|
||||
}
|
||||
|
||||
// Note: NameReplacesTokens test removed - requires WinUI ResourceLoader runtime
|
||||
[TestMethod]
|
||||
public void FitWorks()
|
||||
{
|
||||
|
||||
@@ -25,9 +25,18 @@ namespace ImageResizer.Properties
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);
|
||||
// Cache the validation message format
|
||||
private static CompositeFormat _valueMustBeBetween;
|
||||
|
||||
private static App _imageResizerApp;
|
||||
private static CompositeFormat ValueMustBeBetweenFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
// Use hardcoded string for test since ResourceLoader requires WinUI runtime
|
||||
_valueMustBeBetween ??= System.Text.CompositeFormat.Parse("Value must be between '{0}' and '{1}'.");
|
||||
return _valueMustBeBetween;
|
||||
}
|
||||
}
|
||||
|
||||
public SettingsTests()
|
||||
{
|
||||
@@ -38,8 +47,8 @@ namespace ImageResizer.Properties
|
||||
[ClassInitialize]
|
||||
public static void ClassInitialize(TestContext context)
|
||||
{
|
||||
// new App() needs to be created since Settings.Reload() uses App.Current to update properties on the UI thread. App() can be created only once otherwise it results in System.InvalidOperationException : Cannot create more than one System.Windows.Application instance in the same AppDomain.
|
||||
_imageResizerApp = new App();
|
||||
// Note: WinUI App cannot be instantiated in unit tests without the full WinUI runtime.
|
||||
// Settings.Reload() has a fallback mechanism that allows it to work without a DispatcherQueue.
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -195,7 +204,7 @@ namespace ImageResizer.Properties
|
||||
|
||||
// Using InvariantCulture since this is used internally
|
||||
Assert.AreEqual(
|
||||
string.Format(CultureInfo.InvariantCulture, ValueMustBeBetween, 1, 100),
|
||||
string.Format(CultureInfo.InvariantCulture, ValueMustBeBetweenFormat, 1, 100),
|
||||
result);
|
||||
}
|
||||
|
||||
@@ -385,8 +394,7 @@ namespace ImageResizer.Properties
|
||||
[ClassCleanup]
|
||||
public static void ClassCleanup()
|
||||
{
|
||||
_imageResizerApp.Dispose();
|
||||
_imageResizerApp = null;
|
||||
// No App instance to dispose in WinUI3 test environment
|
||||
}
|
||||
|
||||
[TestCleanup]
|
||||
|
||||
@@ -1,50 +1,118 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Converters;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
[TestClass]
|
||||
public class TimeRemainingConverterTests
|
||||
{
|
||||
[DataTestMethod]
|
||||
[DataRow("HourMinute", 1, 1, 0)]
|
||||
[DataRow("HourMinutes", 1, 2, 0)]
|
||||
[DataRow("HoursMinute", 2, 1, 0)]
|
||||
[DataRow("HoursMinutes", 2, 2, 0)]
|
||||
[DataRow("MinuteSecond", 0, 1, 1)]
|
||||
[DataRow("MinuteSeconds", 0, 1, 2)]
|
||||
[DataRow("MinutesSecond", 0, 2, 1)]
|
||||
[DataRow("MinutesSeconds", 0, 2, 2)]
|
||||
[DataRow("Second", 0, 0, 1)]
|
||||
[DataRow("Seconds", 0, 0, 2)]
|
||||
public void ConvertWorks(string resource, int hours, int minutes, int seconds)
|
||||
[TestMethod]
|
||||
public void Convert_ReturnsEmptyString_WhenTimeSpanIsMaxValue()
|
||||
{
|
||||
var timeRemaining = new TimeSpan(hours, minutes, seconds);
|
||||
var converter = new TimeRemainingConverter();
|
||||
|
||||
// Using InvariantCulture since these are internal
|
||||
var result = converter.Convert(
|
||||
TimeSpan.MaxValue,
|
||||
targetType: null,
|
||||
parameter: null,
|
||||
language: string.Empty);
|
||||
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_ReturnsEmptyString_WhenTotalSecondsLessThanOne()
|
||||
{
|
||||
var converter = new TimeRemainingConverter();
|
||||
|
||||
var result = converter.Convert(
|
||||
TimeSpan.FromSeconds(0.5),
|
||||
targetType: null,
|
||||
parameter: null,
|
||||
language: string.Empty);
|
||||
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_ReturnsEmptyString_WhenZeroTimeSpan()
|
||||
{
|
||||
var converter = new TimeRemainingConverter();
|
||||
|
||||
var result = converter.Convert(
|
||||
TimeSpan.Zero,
|
||||
targetType: null,
|
||||
parameter: null,
|
||||
language: string.Empty);
|
||||
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_ReturnsFormattedString_WhenValidTimeSpan()
|
||||
{
|
||||
var converter = new TimeRemainingConverter();
|
||||
var timeRemaining = new TimeSpan(0, 5, 30);
|
||||
|
||||
var result = converter.Convert(
|
||||
timeRemaining,
|
||||
targetType: null,
|
||||
parameter: null,
|
||||
CultureInfo.InvariantCulture);
|
||||
language: string.Empty);
|
||||
|
||||
Assert.AreEqual(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
Resources.ResourceManager.GetString("Progress_TimeRemaining_" + resource, CultureInfo.InvariantCulture),
|
||||
hours,
|
||||
minutes,
|
||||
seconds),
|
||||
result);
|
||||
// The result should contain the time remaining information
|
||||
Assert.IsNotNull(result);
|
||||
Assert.IsInstanceOfType(result, typeof(string));
|
||||
Assert.AreNotEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_ReturnsEmptyString_WhenValueIsNotTimeSpan()
|
||||
{
|
||||
var converter = new TimeRemainingConverter();
|
||||
|
||||
var result = converter.Convert(
|
||||
"not a timespan",
|
||||
targetType: null,
|
||||
parameter: null,
|
||||
language: string.Empty);
|
||||
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Convert_ReturnsEmptyString_WhenValueIsNull()
|
||||
{
|
||||
var converter = new TimeRemainingConverter();
|
||||
|
||||
var result = converter.Convert(
|
||||
null,
|
||||
targetType: null,
|
||||
parameter: null,
|
||||
language: string.Empty);
|
||||
|
||||
Assert.AreEqual(string.Empty, result);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void ConvertBack_ReturnsValueUnchanged()
|
||||
{
|
||||
var converter = new TimeRemainingConverter();
|
||||
var input = "test value";
|
||||
|
||||
var result = converter.ConvertBack(
|
||||
input,
|
||||
targetType: null,
|
||||
parameter: null,
|
||||
language: string.Empty);
|
||||
|
||||
Assert.AreEqual(input, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
<Application
|
||||
x:Class="ImageResizer.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:m="clr-namespace:ImageResizer.Models"
|
||||
xmlns:sys="clr-namespace:System;assembly=System.Runtime"
|
||||
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
|
||||
xmlns:v="clr-namespace:ImageResizer.Views">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ui:ThemesDictionary Theme="Dark" />
|
||||
<ui:ControlsDictionary />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<v:SizeTypeToVisibilityConverter x:Key="SizeTypeToVisibilityConverter" />
|
||||
<v:SizeTypeToHelpTextConverter x:Key="SizeTypeToHelpTextConverter" />
|
||||
<v:EnumValueConverter x:Key="EnumValueConverter" />
|
||||
<v:AutoDoubleConverter x:Key="AutoDoubleConverter" />
|
||||
<v:BoolValueConverter x:Key="BoolValueConverter" />
|
||||
<v:VisibilityBoolConverter x:Key="VisibilityBoolConverter" />
|
||||
<v:EnumToIntConverter x:Key="EnumToIntConverter" />
|
||||
<v:AccessTextToTextConverter x:Key="AccessTextToTextConverter" />
|
||||
<v:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
|
||||
<v:ZeroToEmptyStringNumberFormatter x:Key="ZeroToEmptyStringNumberFormatter" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
@@ -1,243 +0,0 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Windows;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Utilities;
|
||||
using ImageResizer.ViewModels;
|
||||
using ImageResizer.Views;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer
|
||||
{
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private const string LogSubFolder = "\\Image Resizer\\Logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached AI availability state, checked at app startup.
|
||||
/// Can be updated after model download completes or background initialization.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState AiAvailabilityState { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when AI initialization completes in background.
|
||||
/// Allows UI to refresh state when initialization finishes.
|
||||
/// </summary>
|
||||
public static event EventHandler<AiAvailabilityState> AiInitializationCompleted;
|
||||
|
||||
static App()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Initialize logger early (mirroring PowerOCR pattern)
|
||||
Logger.InitializeLogger(LogSubFolder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
/* swallow logger init issues silently */
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
if (!string.IsNullOrEmpty(appLanguage))
|
||||
{
|
||||
System.Threading.Thread.CurrentThread.CurrentUICulture = new CultureInfo(appLanguage);
|
||||
}
|
||||
}
|
||||
catch (CultureNotFoundException ex)
|
||||
{
|
||||
Logger.LogError("CultureNotFoundException: " + ex.Message);
|
||||
}
|
||||
|
||||
Console.InputEncoding = Encoding.Unicode;
|
||||
}
|
||||
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
|
||||
NativeMethods.SetProcessDPIAware();
|
||||
|
||||
// TODO: Re-enable AI Super Resolution in next release by removing this #if block
|
||||
// Temporarily disable AI Super Resolution feature (hide from UI but keep code)
|
||||
#if true // Set to false to re-enable AI Super Resolution
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
|
||||
// Skip AI detection mode as well
|
||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||
{
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
#else
|
||||
// Check for AI detection mode (called by Runner in background)
|
||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||
{
|
||||
RunAiDetectionMode();
|
||||
return;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
/* TODO: Add logs to ImageResizer.
|
||||
* Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
*/
|
||||
Logger.LogWarning("GPO policy disables ImageResizer. Exiting.");
|
||||
Environment.Exit(0); // Current.Exit won't work until there's a window opened.
|
||||
return;
|
||||
}
|
||||
|
||||
// AI Super Resolution is not supported on Windows 10 - skip cache check entirely
|
||||
if (OSVersionHelper.IsWindows10())
|
||||
{
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogInfo("AI Super Resolution not supported on Windows 10");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load AI availability from cache (written by Runner's background detection)
|
||||
var cachedState = Services.AiAvailabilityCacheService.LoadCache();
|
||||
|
||||
if (cachedState.HasValue)
|
||||
{
|
||||
AiAvailabilityState = cachedState.Value;
|
||||
Logger.LogInfo($"AI state loaded from cache: {AiAvailabilityState}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// No valid cache - default to NotSupported (Runner will detect and cache for next startup)
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
Logger.LogInfo("No AI cache found, defaulting to NotSupported");
|
||||
}
|
||||
|
||||
// If AI is potentially available, start background initialization (non-blocking)
|
||||
if (AiAvailabilityState == AiAvailabilityState.Ready)
|
||||
{
|
||||
_ = InitializeAiServiceAsync(); // Fire and forget - don't block UI
|
||||
}
|
||||
else
|
||||
{
|
||||
// AI not available - set NoOp service immediately
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
}
|
||||
|
||||
var batch = ResizeBatch.FromCommandLine(Console.In, e?.Args);
|
||||
|
||||
// TODO: Add command-line parameters that can be used in lieu of the input page (issue #14)
|
||||
var mainWindow = new MainWindow(new MainViewModel(batch, Settings.Default));
|
||||
mainWindow.Show();
|
||||
|
||||
// Temporary workaround for issue #1273
|
||||
WindowHelpers.BringToForeground(new System.Windows.Interop.WindowInteropHelper(mainWindow).Handle);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI detection mode: perform detection, write to cache, and exit.
|
||||
/// Called by Runner in background to avoid blocking ImageResizer UI startup.
|
||||
/// </summary>
|
||||
private void RunAiDetectionMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("Running AI detection mode...");
|
||||
|
||||
// AI Super Resolution is not supported on Windows 10
|
||||
if (OSVersionHelper.IsWindows10())
|
||||
{
|
||||
Logger.LogInfo("AI detection skipped: Windows 10 does not support AI Super Resolution");
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform detection (reuse existing logic)
|
||||
var state = CheckAiAvailability();
|
||||
|
||||
// Write result to cache file
|
||||
Services.AiAvailabilityCacheService.SaveCache(state);
|
||||
|
||||
Logger.LogInfo($"AI detection complete: {state}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"AI detection failed: {ex.Message}");
|
||||
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
}
|
||||
|
||||
// Exit silently without showing UI
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check AI Super Resolution availability on this system.
|
||||
/// Performs architecture check and model availability check.
|
||||
/// </summary>
|
||||
private static AiAvailabilityState CheckAiAvailability()
|
||||
{
|
||||
// AI feature disabled - always return NotSupported
|
||||
return AiAvailabilityState.NotSupported;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize AI Super Resolution service asynchronously in background.
|
||||
/// Runs without blocking UI startup - state change event notifies completion.
|
||||
/// </summary>
|
||||
private static async System.Threading.Tasks.Task InitializeAiServiceAsync()
|
||||
{
|
||||
AiAvailabilityState finalState;
|
||||
|
||||
try
|
||||
{
|
||||
// Create and initialize AI service using async factory
|
||||
var aiService = await Services.WinAiSuperResolutionService.CreateAsync();
|
||||
|
||||
if (aiService != null)
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||
Logger.LogInfo("AI Super Resolution service initialized successfully.");
|
||||
finalState = AiAvailabilityState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Initialization failed - use default NoOp service
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogWarning("AI Super Resolution service initialization failed. Using default service.");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Log error and use default NoOp service
|
||||
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
|
||||
// Update cached state and notify listeners
|
||||
AiAvailabilityState = finalState;
|
||||
AiInitializationCompleted?.Invoke(null, finalState);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// Dispose AI Super Resolution service
|
||||
ResizeBatch.DisposeAiSuperResolutionService();
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--destination", "-d", "/d"];
|
||||
|
||||
public DestinationOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Destination)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Destination)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--filename", "-n"];
|
||||
|
||||
public FileNameOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_FileName)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_FileName)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--fit", "-f"];
|
||||
|
||||
public FitOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Fit)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Fit)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--height", "-h"];
|
||||
|
||||
public HeightOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Height)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Height)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--help", "-?", "/?"];
|
||||
|
||||
public HelpOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Help)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Help)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--ignore-orientation"];
|
||||
|
||||
public IgnoreOrientationOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_IgnoreOrientation)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_IgnoreOrientation)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--keep-date-modified"];
|
||||
|
||||
public KeepDateModifiedOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_KeepDateModified)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_KeepDateModified)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--progress-lines", "--accessible"];
|
||||
|
||||
public ProgressLinesOption()
|
||||
: base(_aliases, "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
|
||||
: base(_aliases[0], "Use line-based progress output for screen reader accessibility (milestones: 0%, 25%, 50%, 75%, 100%)")
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--quality", "-q"];
|
||||
|
||||
public QualityOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Quality)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Quality)
|
||||
{
|
||||
AddValidator(result =>
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--remove-metadata"];
|
||||
|
||||
public RemoveMetadataOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_RemoveMetadata)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_RemoveMetadata)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--replace", "-r"];
|
||||
|
||||
public ReplaceOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Replace)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Replace)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--show-config", "--config"];
|
||||
|
||||
public ShowConfigOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_ShowConfig)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_ShowConfig)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--shrink-only"];
|
||||
|
||||
public ShrinkOnlyOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_ShrinkOnly)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_ShrinkOnly)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--size"];
|
||||
|
||||
public SizeOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Size)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Size)
|
||||
{
|
||||
AddValidator(result =>
|
||||
{
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--unit", "-u"];
|
||||
|
||||
public UnitOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Unit)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Unit)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ namespace ImageResizer.Cli.Options
|
||||
private static readonly string[] _aliases = ["--width", "-w"];
|
||||
|
||||
public WidthOption()
|
||||
: base(_aliases, Properties.Resources.CLI_Option_Width)
|
||||
: base(_aliases[0], Properties.Resources.CLI_Option_Width)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using ImageResizer.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class AutoDoubleConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is double d && (d == 0 || double.IsNaN(d)))
|
||||
{
|
||||
return ResourceLoaderInstance.ResourceLoader.GetString("Auto");
|
||||
}
|
||||
|
||||
return value?.ToString();
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
bool boolValue = value is bool b && b;
|
||||
bool invert = parameter is string param && param.Equals("Inverted", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (invert)
|
||||
{
|
||||
boolValue = !boolValue;
|
||||
}
|
||||
|
||||
return boolValue ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> value is Visibility v && v == Visibility.Visible;
|
||||
}
|
||||
}
|
||||
32
src/modules/imageresizer/ui/Converters/EnumToIntConverter.cs
Normal file
32
src/modules/imageresizer/ui/Converters/EnumToIntConverter.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class EnumToIntConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is Enum)
|
||||
{
|
||||
return System.Convert.ToInt32(value);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is int intValue && targetType.IsEnum)
|
||||
{
|
||||
return Enum.ToObject(targetType, intValue);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,21 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Windows.Data;
|
||||
using ImageResizer.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
[ValueConversion(typeof(Enum), typeof(string))]
|
||||
public class EnumValueConverter : IValueConverter
|
||||
public partial class EnumValueConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
var type = value?.GetType();
|
||||
if (!type.IsEnum)
|
||||
if (type == null || !type.IsEnum)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
@@ -44,20 +39,18 @@ namespace ImageResizer.Views
|
||||
.Append(parameter);
|
||||
}
|
||||
|
||||
// Fixes #16792 - Looks like culture defaults to en-US, so wrong resource is being fetched.
|
||||
#pragma warning disable CA1304 // Specify CultureInfo
|
||||
var targetValue = Resources.ResourceManager.GetString(builder.ToString());
|
||||
#pragma warning restore CA1304 // Specify CultureInfo
|
||||
var targetValue = ResourceLoaderInstance.ResourceLoader.GetString(builder.ToString());
|
||||
|
||||
if (toLower)
|
||||
if (toLower && !string.IsNullOrEmpty(targetValue))
|
||||
{
|
||||
var culture = string.IsNullOrEmpty(language) ? CultureInfo.CurrentCulture : new CultureInfo(language);
|
||||
targetValue = targetValue.ToLower(culture);
|
||||
}
|
||||
|
||||
return targetValue;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class NumberBoxValueConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts the underlying double value to a display-friendly format. Ensures that NaN values
|
||||
/// are not propagated to the UI.
|
||||
/// </summary>
|
||||
public object Convert(object value, Type targetType, object parameter, string language) =>
|
||||
value is double d && double.IsNaN(d) ? 0 : value;
|
||||
|
||||
/// <summary>
|
||||
/// Converts the user input back to the underlying double value. If the input is not a valid
|
||||
/// number, 0 is returned.
|
||||
/// </summary>
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
|
||||
value switch
|
||||
{
|
||||
null => 0,
|
||||
double d when double.IsNaN(d) => 0,
|
||||
string str when !double.TryParse(str, out _) => 0,
|
||||
_ => value,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using ImageResizer.Models;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public sealed partial class SizeTypeToHelpTextConverter : IValueConverter
|
||||
{
|
||||
private const char MultiplicationSign = '\u00D7';
|
||||
|
||||
private readonly EnumValueConverter _enumConverter = new();
|
||||
private readonly AutoDoubleConverter _autoDoubleConverter = new();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is not ResizeSize size)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string EnumToString(Enum value, string parameter = null) =>
|
||||
_enumConverter.Convert(value, typeof(string), parameter, language) as string;
|
||||
|
||||
string DoubleToString(double value) =>
|
||||
_autoDoubleConverter.Convert(value, typeof(string), null, language) as string;
|
||||
|
||||
var fit = EnumToString(size.Fit, "ThirdPersonSingular");
|
||||
var width = DoubleToString(size.Width);
|
||||
var unit = EnumToString(size.Unit);
|
||||
|
||||
return size.ShowHeight ?
|
||||
$"{fit} {width} {MultiplicationSign} {DoubleToString(size.Height)} {unit}" :
|
||||
$"{fit} {width} {unit}";
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using ImageResizer.Models;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class SizeTypeToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value != null && value.GetType() == typeof(CustomSize) ? Visibility.Visible : Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value is Visibility v && v == Visibility.Visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using ImageResizer.Helpers;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace ImageResizer.Converters
|
||||
{
|
||||
public partial class TimeRemainingConverter : IValueConverter
|
||||
{
|
||||
private static CompositeFormat _progressTimeRemainingFormat;
|
||||
|
||||
private static CompositeFormat ProgressTimeRemainingFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_progressTimeRemainingFormat == null)
|
||||
{
|
||||
var formatString = ResourceLoaderInstance.ResourceLoader.GetString("Progress_TimeRemaining");
|
||||
_progressTimeRemainingFormat = CompositeFormat.Parse(formatString);
|
||||
}
|
||||
|
||||
return _progressTimeRemainingFormat;
|
||||
}
|
||||
}
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is TimeSpan timeSpan)
|
||||
{
|
||||
if (timeSpan == TimeSpan.MaxValue || timeSpan.TotalSeconds < 1)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var culture = string.IsNullOrEmpty(language) ? CultureInfo.CurrentCulture : new CultureInfo(language);
|
||||
return string.Format(culture, ProgressTimeRemainingFormat, timeSpan);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace ImageResizer.Helpers
|
||||
{
|
||||
public class Observable : INotifyPropertyChanged
|
||||
{
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
protected void Set<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
|
||||
{
|
||||
if (Equals(storage, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
storage = value;
|
||||
OnPropertyChanged(propertyName);
|
||||
}
|
||||
|
||||
protected void OnPropertyChanged(string propertyName) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace ImageResizer.Helpers
|
||||
{
|
||||
public class RelayCommand : ICommand
|
||||
{
|
||||
private readonly Action _execute;
|
||||
private readonly Func<bool> _canExecute;
|
||||
|
||||
public event EventHandler CanExecuteChanged;
|
||||
|
||||
public RelayCommand(Action execute)
|
||||
: this(execute, null)
|
||||
{
|
||||
}
|
||||
|
||||
public RelayCommand(Action execute, Func<bool> canExecute)
|
||||
{
|
||||
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
_canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter) => _canExecute == null || _canExecute();
|
||||
|
||||
public void Execute(object parameter) => _execute();
|
||||
|
||||
public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "abstract T and abstract")]
|
||||
public class RelayCommand<T> : ICommand
|
||||
{
|
||||
private readonly Action<T> execute;
|
||||
|
||||
private readonly Func<T, bool> canExecute;
|
||||
|
||||
public event EventHandler CanExecuteChanged;
|
||||
|
||||
public RelayCommand(Action<T> execute)
|
||||
: this(execute, null)
|
||||
{
|
||||
}
|
||||
|
||||
public RelayCommand(Action<T> execute, Func<T, bool> canExecute)
|
||||
{
|
||||
this.execute = execute ?? throw new ArgumentNullException(nameof(execute));
|
||||
this.canExecute = canExecute;
|
||||
}
|
||||
|
||||
public bool CanExecute(object parameter) => canExecute == null || canExecute((T)parameter);
|
||||
|
||||
public void Execute(object parameter) => execute((T)parameter);
|
||||
|
||||
public void OnCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.Windows.ApplicationModel.Resources;
|
||||
|
||||
namespace ImageResizer.Helpers
|
||||
{
|
||||
internal static class ResourceLoaderInstance
|
||||
{
|
||||
internal static ResourceLoader ResourceLoader { get; private set; }
|
||||
|
||||
static ResourceLoaderInstance()
|
||||
{
|
||||
ResourceLoader = new ResourceLoader("PowerToys.ImageResizer.pri");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,82 +1,92 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<!-- Look at Directory.Build.props in root for common stuff as well -->
|
||||
<Import Project="..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\Common.SelfContained.props" />
|
||||
|
||||
|
||||
<PropertyGroup>
|
||||
<AssemblyTitle>PowerToys.ImageResizer</AssemblyTitle>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps\</OutputPath>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<UseWPF>true</UseWPF>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ProjectGuid>{2BE46397-4DFA-414C-9BD4-41E4BBF8CB34}</ProjectGuid>
|
||||
<AssemblyDescription>PowerToys Image Resizer</AssemblyDescription>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<OutputPath>..\..\..\..\$(Platform)\$(Configuration)\WinUI3Apps</OutputPath>
|
||||
<RootNamespace>ImageResizer</RootNamespace>
|
||||
<AssemblyName>PowerToys.ImageResizer</AssemblyName>
|
||||
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<UseWinUI>true</UseWinUI>
|
||||
<GenerateSatelliteAssembliesForCore>true</GenerateSatelliteAssembliesForCore>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
<EnablePreviewMsixTooling>true</EnablePreviewMsixTooling>
|
||||
<WindowsPackageType>None</WindowsPackageType>
|
||||
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
|
||||
<ApplicationIcon>Assets\ImageResizer\ImageResizer.ico</ApplicationIcon>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<NoWarn>CA1863</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||
<!-- MRT from windows app sdk will search for a pri file with the same name of the module before defaulting to resources.pri -->
|
||||
<ProjectPriFileName>PowerToys.ImageResizer.pri</ProjectPriFileName>
|
||||
<!-- Custom Main entry point -->
|
||||
<DefineConstants>DISABLE_XAML_GENERATED_MAIN,TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Page Remove="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ApplicationDefinition Include="ImageResizerXAML\App.xaml" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||
<NoWarn>0436;SA1210;SA1516;CA1305;CA1863;CA1852</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||
<!-- Allow test project to access internal types -->
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="ImageResizer.Test" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="Assets\ImageResizer\ImageResizer.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- See https://learn.microsoft.com/windows/apps/develop/platform/csharp-winrt/net-projection-from-cppwinrt-component for more info -->
|
||||
<PropertyGroup>
|
||||
<CsWinRTIncludes>PowerToys.GPOWrapper</CsWinRTIncludes>
|
||||
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
|
||||
<ErrorOnDuplicatePublishOutputFiles>false</ErrorOnDuplicatePublishOutputFiles>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Update="Properties\Resources.resx">
|
||||
<Generator>PublicResXFileCodeGenerator</Generator>
|
||||
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
|
||||
<SubType>Designer</SubType>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\ImageResizer.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\ImageResizer.png" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.Mvvm" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK" />
|
||||
<PackageReference Include="Microsoft.WindowsAppSDK.AI" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
|
||||
<PackageReference Include="Microsoft.Xaml.Behaviors.WinUI.Managed" />
|
||||
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
|
||||
<PackageReference Include="System.CommandLine" />
|
||||
<PackageReference Include="System.IO.Abstractions" />
|
||||
<PackageReference Include="WPF-UI" />
|
||||
<PackageReference Include="WinUIEx" />
|
||||
<!-- This line forces the WebView2 version used by Windows App SDK to be the one we expect from Directory.Packages.props . -->
|
||||
<PackageReference Include="Microsoft.Web.WebView2" />
|
||||
<!-- HACK: CmdPal uses CommunityToolkit.Common directly. Align the version. -->
|
||||
<PackageReference Include="CommunityToolkit.Common" />
|
||||
<Manifest Include="$(ApplicationManifest)" />
|
||||
</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 '$(EnablePreviewMsixTooling)'=='true'">
|
||||
<ProjectCapability Include="Msix" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapperProjection\GPOWrapperProjection.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<Folder Include="Properties\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- HACK: Common.UI is referenced, even if it is not used, to force dll versions to be the same as in other projects that use it. It's still unclear why this is the case, but this is need for flattening the install directory. -->
|
||||
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
|
||||
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
|
||||
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Properties\Resources.Designer.cs">
|
||||
<DesignTime>True</DesignTime>
|
||||
<AutoGen>True</AutoGen>
|
||||
<DependentUpon>Resources.resx</DependentUpon>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Ensure Resources directory and ImageResizer.png are available for dependent projects -->
|
||||
<Target Name="CopyResourcesToSharedLocation" AfterTargets="Build">
|
||||
<ItemGroup>
|
||||
<ResourceFiles Include="$(MSBuildProjectDirectory)\Resources\ImageResizer.png" />
|
||||
</ItemGroup>
|
||||
<MakeDir Directories="$(OutputPath)Resources" Condition="!Exists('$(OutputPath)Resources')" />
|
||||
<Copy SourceFiles="@(ResourceFiles)" DestinationFolder="$(OutputPath)Resources" SkipUnchangedFiles="true" />
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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="PowerToys.ImageResizer.app" />
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=PowerToys Dev, O=PowerToys, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.ImageResizerUI" />
|
||||
</assembly>
|
||||
@@ -1,8 +0,0 @@
|
||||
<?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="PowerToys.ImageResizer.app" />
|
||||
<msix xmlns="urn:schemas-microsoft-com:msix.v1"
|
||||
publisher="CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
packageName="Microsoft.PowerToys.SparseApp"
|
||||
applicationId="PowerToys.ImageResizerUI" />
|
||||
</assembly>
|
||||
35
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml
Normal file
35
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml
Normal file
@@ -0,0 +1,35 @@
|
||||
<Application
|
||||
x:Class="ImageResizer.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:ImageResizer"
|
||||
xmlns:converters="using:ImageResizer.Converters"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters">
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<ResourceDictionary.ThemeDictionaries>
|
||||
<ResourceDictionary x:Key="Default" />
|
||||
<ResourceDictionary x:Key="Light" />
|
||||
<ResourceDictionary x:Key="Dark" />
|
||||
</ResourceDictionary.ThemeDictionaries>
|
||||
|
||||
<!-- Converters -->
|
||||
<converters:AutoDoubleConverter x:Key="AutoDoubleConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<converters:EnumToIntConverter x:Key="EnumToIntConverter" />
|
||||
<converters:EnumValueConverter x:Key="EnumValueConverter" />
|
||||
<converters:NumberBoxValueConverter x:Key="NumberBoxValueConverter" />
|
||||
<converters:SizeTypeToHelpTextConverter x:Key="SizeTypeToHelpTextConverter" />
|
||||
<converters:SizeTypeToVisibilityConverter x:Key="SizeTypeToVisibilityConverter" />
|
||||
<converters:TimeRemainingConverter x:Key="TimeRemainingConverter" />
|
||||
<tkconverters:BoolToVisibilityConverter
|
||||
x:Key="InvertedBoolToVisibilityConverter"
|
||||
TrueValue="Collapsed"
|
||||
FalseValue="Visible" />
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
220
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml.cs
Normal file
220
src/modules/imageresizer/ui/ImageResizerXAML/App.xaml.cs
Normal file
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.ViewModels;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace ImageResizer
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides application-specific behavior to supplement the default Application class.
|
||||
/// </summary>
|
||||
public partial class App : Application, IDisposable
|
||||
{
|
||||
private const string LogSubFolder = "\\Image Resizer\\Logs";
|
||||
|
||||
/// <summary>
|
||||
/// Gets cached AI availability state, checked at app startup.
|
||||
/// Can be updated after model download completes or background initialization.
|
||||
/// </summary>
|
||||
public static AiAvailabilityState AiAvailabilityState { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when AI initialization completes in background.
|
||||
/// Allows UI to refresh state when initialization finishes.
|
||||
/// </summary>
|
||||
public static event EventHandler<AiAvailabilityState> AiInitializationCompleted;
|
||||
|
||||
private Window _window;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="App"/> class.
|
||||
/// </summary>
|
||||
public App()
|
||||
{
|
||||
try
|
||||
{
|
||||
string appLanguage = LanguageHelper.LoadLanguage();
|
||||
if (!string.IsNullOrEmpty(appLanguage))
|
||||
{
|
||||
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("Language initialization error: " + ex.Message);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Logger.InitializeLogger(LogSubFolder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Swallow logger init issues silently
|
||||
}
|
||||
|
||||
Console.InputEncoding = Encoding.Unicode;
|
||||
|
||||
this.InitializeComponent();
|
||||
|
||||
UnhandledException += App_UnhandledException;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the application is launched normally by the end user.
|
||||
/// </summary>
|
||||
/// <param name="args">Details about the launch request and process.</param>
|
||||
protected override void OnLaunched(LaunchActivatedEventArgs args)
|
||||
{
|
||||
// Initialize dispatcher for cross-thread property change notifications
|
||||
Settings.InitializeDispatcher();
|
||||
|
||||
// Check GPO policy
|
||||
if (PowerToys.GPOWrapper.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapper.GpoRuleConfigured.Disabled)
|
||||
{
|
||||
Logger.LogWarning("Tried to start with a GPO policy setting the utility to always be disabled. Please contact your systems administrator.");
|
||||
Environment.Exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for AI detection mode (called by Runner in background)
|
||||
var commandLineArgs = Environment.GetCommandLineArgs();
|
||||
if (commandLineArgs?.Length > 1 && commandLineArgs[1] == "--detect-ai")
|
||||
{
|
||||
RunAiDetectionMode();
|
||||
return;
|
||||
}
|
||||
|
||||
// Initialize AI availability
|
||||
InitializeAiAvailability();
|
||||
|
||||
// Create batch from command line
|
||||
var batch = ResizeBatch.FromCommandLine(Console.In, commandLineArgs);
|
||||
|
||||
// Create and show main window
|
||||
_window = new MainWindow(new MainViewModel(batch, Settings.Default));
|
||||
_window.Activate();
|
||||
}
|
||||
|
||||
private void InitializeAiAvailability()
|
||||
{
|
||||
// AI Super Resolution is currently disabled
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||
|
||||
// If AI is enabled in the future, uncomment this section:
|
||||
/*
|
||||
// AI Super Resolution is not supported on Windows 10
|
||||
if (OSVersionHelper.IsWindows10())
|
||||
{
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogInfo("AI Super Resolution not supported on Windows 10");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Load AI availability from cache
|
||||
var cachedState = AiAvailabilityCacheService.LoadCache();
|
||||
|
||||
if (cachedState.HasValue)
|
||||
{
|
||||
AiAvailabilityState = cachedState.Value;
|
||||
Logger.LogInfo($"AI state loaded from cache: {AiAvailabilityState}");
|
||||
}
|
||||
else
|
||||
{
|
||||
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||
Logger.LogInfo("No AI cache found, defaulting to NotSupported");
|
||||
}
|
||||
|
||||
// If AI is potentially available, start background initialization
|
||||
if (AiAvailabilityState == AiAvailabilityState.Ready)
|
||||
{
|
||||
_ = InitializeAiServiceAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// AI detection mode: perform detection, write to cache, and exit.
|
||||
/// </summary>
|
||||
private void RunAiDetectionMode()
|
||||
{
|
||||
try
|
||||
{
|
||||
Logger.LogInfo("Running AI detection mode...");
|
||||
|
||||
// AI is currently disabled
|
||||
AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
Logger.LogInfo("AI detection complete: NotSupported (feature disabled)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"AI detection failed: {ex.Message}");
|
||||
AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||
}
|
||||
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initialize AI Super Resolution service asynchronously in background.
|
||||
/// </summary>
|
||||
private static async Task InitializeAiServiceAsync()
|
||||
{
|
||||
AiAvailabilityState finalState;
|
||||
|
||||
try
|
||||
{
|
||||
var aiService = await WinAiSuperResolutionService.CreateAsync();
|
||||
|
||||
if (aiService != null)
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(aiService);
|
||||
Logger.LogInfo("AI Super Resolution service initialized successfully.");
|
||||
finalState = AiAvailabilityState.Ready;
|
||||
}
|
||||
else
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogWarning("AI Super Resolution service initialization failed. Using default service.");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ResizeBatch.SetAiSuperResolutionService(NoOpAiSuperResolutionService.Instance);
|
||||
Logger.LogError($"Exception during AI service initialization: {ex.Message}");
|
||||
finalState = AiAvailabilityState.NotSupported;
|
||||
}
|
||||
|
||||
AiAvailabilityState = finalState;
|
||||
AiInitializationCompleted?.Invoke(null, finalState);
|
||||
}
|
||||
|
||||
private void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
|
||||
{
|
||||
Logger.LogError("Unhandled exception", e.Exception);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
ResizeBatch.DisposeAiSuperResolutionService();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
19
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml
Normal file
19
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml
Normal file
@@ -0,0 +1,19 @@
|
||||
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
<winuiEx:WindowEx
|
||||
x:Class="ImageResizer.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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="using:ImageResizer.Views"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
xmlns:winuiEx="using:WinUIEx"
|
||||
Width="400"
|
||||
Height="506"
|
||||
IsMaximizable="False"
|
||||
IsMinimizable="False"
|
||||
IsResizable="False"
|
||||
mc:Ignorable="d">
|
||||
<ContentPresenter x:Name="contentPresenter" />
|
||||
</winuiEx:WindowEx>
|
||||
211
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml.cs
Normal file
211
src/modules/imageresizer/ui/ImageResizerXAML/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,211 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.ViewModels;
|
||||
using ImageResizer.Views;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Storage.Pickers;
|
||||
using WinRT.Interop;
|
||||
using WinUIEx;
|
||||
|
||||
namespace ImageResizer
|
||||
{
|
||||
public sealed partial class MainWindow : WindowEx, IMainView
|
||||
{
|
||||
public MainViewModel ViewModel { get; }
|
||||
|
||||
private PropertyChangedEventHandler _selectedSizeChangedHandler;
|
||||
private InputViewModel _currentInputViewModel;
|
||||
|
||||
// Window chrome height (title bar)
|
||||
private const double WindowChromeHeight = 32;
|
||||
|
||||
public MainWindow(MainViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
var title = loader.GetString("ImageResizer");
|
||||
Title = title;
|
||||
|
||||
// Center the window on screen
|
||||
this.CenterOnScreen();
|
||||
|
||||
// Set window icon
|
||||
try
|
||||
{
|
||||
var iconPath = System.IO.Path.Combine(AppContext.BaseDirectory, "Assets", "ImageResizer", "ImageResizer.ico");
|
||||
if (System.IO.File.Exists(iconPath))
|
||||
{
|
||||
this.SetIcon(iconPath); // WinUIEx extension method
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Icon loading failed, continue without icon
|
||||
}
|
||||
|
||||
// Add Mica backdrop on Windows 11
|
||||
if (Microsoft.UI.Composition.SystemBackdrops.MicaController.IsSupported())
|
||||
{
|
||||
this.SystemBackdrop = new Microsoft.UI.Xaml.Media.MicaBackdrop();
|
||||
}
|
||||
|
||||
// Listen to ViewModel property changes
|
||||
ViewModel.PropertyChanged += ViewModel_PropertyChanged;
|
||||
|
||||
// Load the ViewModel after window is ready
|
||||
ViewModel.Load(this);
|
||||
}
|
||||
|
||||
private void ViewModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ViewModel.CurrentPage))
|
||||
{
|
||||
UpdateCurrentPage();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateCurrentPage()
|
||||
{
|
||||
var page = ViewModel.CurrentPage;
|
||||
if (page == null)
|
||||
{
|
||||
contentPresenter.Content = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (page is InputViewModel inputVM)
|
||||
{
|
||||
var inputPage = new InputPage { ViewModel = inputVM, DataContext = inputVM };
|
||||
contentPresenter.Content = inputPage;
|
||||
|
||||
// Adjust window height based on selected size type
|
||||
AdjustWindowHeightForInputPage(inputVM);
|
||||
}
|
||||
else if (page is ProgressViewModel progressVM)
|
||||
{
|
||||
var progressPage = new ProgressPage { ViewModel = progressVM, DataContext = progressVM };
|
||||
contentPresenter.Content = progressPage;
|
||||
|
||||
// Size to content after layout
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
|
||||
}
|
||||
else if (page is ResultsViewModel resultsVM)
|
||||
{
|
||||
var resultsPage = new ResultsPage { ViewModel = resultsVM, DataContext = resultsVM };
|
||||
contentPresenter.Content = resultsPage;
|
||||
|
||||
// Size to content after layout
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
|
||||
}
|
||||
}
|
||||
|
||||
private void AdjustWindowHeightForInputPage(InputViewModel inputVM)
|
||||
{
|
||||
// Unsubscribe previous handler to prevent memory leak
|
||||
if (_selectedSizeChangedHandler != null && _currentInputViewModel?.Settings != null)
|
||||
{
|
||||
_currentInputViewModel.Settings.PropertyChanged -= _selectedSizeChangedHandler;
|
||||
}
|
||||
|
||||
_currentInputViewModel = inputVM;
|
||||
|
||||
// Create and store handler reference for future cleanup
|
||||
_selectedSizeChangedHandler = (s, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(inputVM.Settings.SelectedSize))
|
||||
{
|
||||
// Delay to allow layout to update
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
|
||||
}
|
||||
};
|
||||
|
||||
inputVM.Settings.PropertyChanged += _selectedSizeChangedHandler;
|
||||
|
||||
// Set initial height after layout
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Low, () => SizeToContent());
|
||||
}
|
||||
|
||||
private void SizeToContent()
|
||||
{
|
||||
// Get the content element
|
||||
var content = contentPresenter.Content as FrameworkElement;
|
||||
if (content == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Measure the content to get its desired size
|
||||
content.Measure(new Windows.Foundation.Size(double.PositiveInfinity, double.PositiveInfinity));
|
||||
var desiredHeight = content.DesiredSize.Height;
|
||||
|
||||
if (desiredHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Add window chrome height and extra padding for safety
|
||||
var totalHeight = desiredHeight + WindowChromeHeight + 16;
|
||||
|
||||
var appWindow = this.AppWindow;
|
||||
if (appWindow != null)
|
||||
{
|
||||
var scaleFactor = Content?.XamlRoot?.RasterizationScale ?? 1.0;
|
||||
var currentSize = appWindow.Size;
|
||||
var newHeightInPixels = (int)(totalHeight * scaleFactor);
|
||||
appWindow.Resize(new Windows.Graphics.SizeInt32(currentSize.Width, newHeightInPixels));
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> OpenPictureFiles()
|
||||
{
|
||||
var picker = new FileOpenPicker();
|
||||
|
||||
// Initialize the picker with the window handle
|
||||
var hwnd = WindowNative.GetWindowHandle(this);
|
||||
InitializeWithWindow.Initialize(picker, hwnd);
|
||||
|
||||
picker.ViewMode = PickerViewMode.Thumbnail;
|
||||
picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;
|
||||
picker.FileTypeFilter.Add(".bmp");
|
||||
picker.FileTypeFilter.Add(".dib");
|
||||
picker.FileTypeFilter.Add(".exif");
|
||||
picker.FileTypeFilter.Add(".gif");
|
||||
picker.FileTypeFilter.Add(".jfif");
|
||||
picker.FileTypeFilter.Add(".jpe");
|
||||
picker.FileTypeFilter.Add(".jpeg");
|
||||
picker.FileTypeFilter.Add(".jpg");
|
||||
picker.FileTypeFilter.Add(".jxr");
|
||||
picker.FileTypeFilter.Add(".png");
|
||||
picker.FileTypeFilter.Add(".rle");
|
||||
picker.FileTypeFilter.Add(".tif");
|
||||
picker.FileTypeFilter.Add(".tiff");
|
||||
picker.FileTypeFilter.Add(".wdp");
|
||||
|
||||
var files = picker.PickMultipleFilesAsync().AsTask().GetAwaiter().GetResult();
|
||||
if (files != null && files.Count > 0)
|
||||
{
|
||||
return files.Select(f => f.Path);
|
||||
}
|
||||
|
||||
return Enumerable.Empty<string>();
|
||||
}
|
||||
|
||||
void IMainView.Close()
|
||||
{
|
||||
DispatcherQueue.TryEnqueue(DispatcherQueuePriority.Normal, Close);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
@@ -10,8 +9,8 @@ namespace ImageResizer.Views
|
||||
{
|
||||
public interface IMainView
|
||||
{
|
||||
void Close();
|
||||
|
||||
IEnumerable<string> OpenPictureFiles();
|
||||
|
||||
void Close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
<Page
|
||||
x:Class="ImageResizer.Views.InputPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:ImageResizer.Converters"
|
||||
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:local="using:ImageResizer.Views"
|
||||
xmlns:m="using:ImageResizer.Models"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkconverters="using:CommunityToolkit.WinUI.Converters"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<!-- Template for normal ResizeSize presets (Small, Medium, Large, Phone) -->
|
||||
<DataTemplate x:Key="ResizeSizeTemplate" x:DataType="m:ResizeSize">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind Name}" />
|
||||
<StackPanel Grid.Row="1" Orientation="Horizontal">
|
||||
<TextBlock Text="{x:Bind Fit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ThirdPersonSingular}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{x:Bind Width, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Text="×"
|
||||
Visibility="{x:Bind ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock
|
||||
Margin="4,0,0,0"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}"
|
||||
Text="{x:Bind Height, Converter={StaticResource AutoDoubleConverter}, ConverterParameter=Auto}"
|
||||
Visibility="{x:Bind ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<TextBlock Margin="4,0,0,0" Text="{x:Bind Unit, Converter={StaticResource EnumValueConverter}, ConverterParameter=ToLower}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for CustomSize - shows only name -->
|
||||
<DataTemplate x:Key="CustomSizeTemplate" x:DataType="m:CustomSize">
|
||||
<Grid VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind Name}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for AiSize - shows name and description -->
|
||||
<DataTemplate x:Key="AiSizeTemplate" x:DataType="m:AiSize">
|
||||
<StackPanel VerticalAlignment="Center" Orientation="Vertical">
|
||||
<TextBlock Style="{StaticResource BodyStrongTextBlockStyle}" Text="{x:Bind Name}" />
|
||||
<TextBlock x:Uid="Input_AiSuperResolutionDescription" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- DataTemplateSelector to choose the right template based on item type -->
|
||||
<local:SizeDataTemplateSelector
|
||||
x:Key="SizeTemplateSelector"
|
||||
AiSizeTemplate="{StaticResource AiSizeTemplate}"
|
||||
CustomSizeTemplate="{StaticResource CustomSizeTemplate}"
|
||||
ResizeSizeTemplate="{StaticResource ResizeSizeTemplate}" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Margin="16">
|
||||
<ComboBox
|
||||
x:Name="SizeComboBox"
|
||||
Height="64"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
ItemTemplateSelector="{StaticResource SizeTemplateSelector}"
|
||||
ItemsSource="{Binding Settings.AllSizes, Mode=OneWay}"
|
||||
SelectedItem="{Binding Settings.SelectedSize, Mode=TwoWay}" />
|
||||
</StackPanel>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Border
|
||||
Grid.RowSpan="5"
|
||||
Background="{StaticResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0" />
|
||||
|
||||
<!-- AI Configuration Panel -->
|
||||
<Grid Grid.Row="0" Margin="16">
|
||||
<!-- AI Model Download Prompt -->
|
||||
<StackPanel Visibility="{Binding ShowModelDownloadPrompt, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<InfoBar
|
||||
IsClosable="False"
|
||||
IsOpen="True"
|
||||
Message="{Binding ModelStatusMessage, Mode=OneWay}"
|
||||
Severity="Informational" />
|
||||
|
||||
<Button
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{Binding DownloadModelCommand}"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
Visibility="{Binding IsModelDownloading, Mode=OneWay, Converter={StaticResource InvertedBoolToVisibilityConverter}, ConverterParameter=Inverted}">
|
||||
<TextBlock x:Uid="Input_AiModelDownloadButton" />
|
||||
</Button>
|
||||
|
||||
<StackPanel
|
||||
Margin="0,8,0,0"
|
||||
Visibility="{Binding IsModelDownloading, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<!-- ProgressRing commented out for WPF compatibility -->
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
HorizontalAlignment="Center"
|
||||
Text="{Binding ModelStatusMessage, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<!-- AI Scale Controls -->
|
||||
<StackPanel Visibility="{Binding ShowAiControls, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid>
|
||||
<TextBlock x:Uid="Input_AiCurrentLabel" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding AiScaleDisplay, Mode=OneWay}" />
|
||||
</Grid>
|
||||
|
||||
<Slider
|
||||
Margin="0,8,0,0"
|
||||
Maximum="8"
|
||||
Minimum="1"
|
||||
TickFrequency="1"
|
||||
TickPlacement="BottomRight"
|
||||
Value="{Binding AiSuperResolutionScale, Mode=TwoWay}" />
|
||||
|
||||
<StackPanel
|
||||
Margin="0,16,0,0"
|
||||
Visibility="{Binding ShowAiSizeDescriptions, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<Grid>
|
||||
<TextBlock x:Uid="Input_AiCurrentLabel" Foreground="{StaticResource TextFillColorSecondaryBrush}" />
|
||||
<TextBlock
|
||||
HorizontalAlignment="Right"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding CurrentResolutionDescription, Mode=OneWay}" />
|
||||
</Grid>
|
||||
<Grid Margin="0,8,0,0">
|
||||
<TextBlock x:Uid="Input_AiNewLabel" />
|
||||
<TextBlock HorizontalAlignment="Right" Text="{Binding NewResolutionDescription, Mode=OneWay}" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Custom input matrix -->
|
||||
<Grid
|
||||
Grid.Row="0"
|
||||
Margin="16"
|
||||
Visibility="{Binding Settings.SelectedSize, Mode=OneWay, Converter={StaticResource SizeTypeToVisibilityConverter}}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="24" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="24" />
|
||||
<ColumnDefinition Width="24" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="8" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<FontIcon
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<NumberBox
|
||||
x:Name="WidthNumberBox"
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
KeyDown="NumberBox_KeyDown"
|
||||
Minimum="0"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding Settings.SelectedSize.Width, Mode=TwoWay}" />
|
||||
|
||||
<FontIcon
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph=""
|
||||
Visibility="{Binding Settings.SelectedSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
<NumberBox
|
||||
x:Name="HeightNumberBox"
|
||||
Grid.Column="4"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
KeyDown="NumberBox_KeyDown"
|
||||
Minimum="0"
|
||||
SpinButtonPlacementMode="Inline"
|
||||
Value="{Binding Settings.SelectedSize.Height, Mode=TwoWay}"
|
||||
Visibility="{Binding Settings.SelectedSize.ShowHeight, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
|
||||
<FontIcon
|
||||
Grid.Row="2"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="1"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding ResizeFitValues, Mode=OneWay}"
|
||||
SelectedIndex="{Binding Settings.SelectedSize.Fit, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="m:ResizeFit">
|
||||
<TextBlock Padding="2,0" Text="{x:Bind Converter={StaticResource EnumValueConverter}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<FontIcon
|
||||
Grid.Row="2"
|
||||
Grid.Column="3"
|
||||
VerticalAlignment="Center"
|
||||
FontSize="20"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Glyph="" />
|
||||
<ComboBox
|
||||
Grid.Row="2"
|
||||
Grid.Column="4"
|
||||
Margin="8,0,0,0"
|
||||
HorizontalAlignment="Stretch"
|
||||
ItemsSource="{Binding ResizeUnitValues, Mode=OneWay}"
|
||||
SelectedIndex="{Binding Settings.SelectedSize.Unit, Mode=TwoWay, Converter={StaticResource EnumToIntConverter}}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="m:ResizeUnit">
|
||||
<TextBlock Padding="2,0" Text="{x:Bind Converter={StaticResource EnumValueConverter}}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<!-- CheckBoxes -->
|
||||
<StackPanel
|
||||
Grid.Row="1"
|
||||
Margin="16"
|
||||
Orientation="Vertical">
|
||||
<CheckBox IsChecked="{Binding Settings.ShrinkOnly, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_ShrinkOnly" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding Settings.IgnoreOrientation, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_IgnoreOrientation" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding Settings.Replace, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_Replace" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
|
||||
<CheckBox IsChecked="{Binding Settings.RemoveMetadata, Mode=TwoWay}">
|
||||
<TextBlock x:Uid="Input_RemoveMetadata" TextWrapping="Wrap" />
|
||||
</CheckBox>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Separator line -->
|
||||
<Border
|
||||
Grid.Row="2"
|
||||
Height="1"
|
||||
Margin="0,8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Top"
|
||||
Background="{StaticResource DividerStrokeColorDefaultBrush}" />
|
||||
|
||||
<InfoBar
|
||||
Grid.Row="3"
|
||||
Margin="16,0"
|
||||
IsClosable="False"
|
||||
IsOpen="{Binding TryingToResizeGifFiles, Mode=OneWay}"
|
||||
Severity="Warning">
|
||||
<TextBlock x:Uid="Input_GifWarning" />
|
||||
</InfoBar>
|
||||
|
||||
<!-- Buttons -->
|
||||
<Grid Grid.Row="4" Margin="16,8,16,16">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Button
|
||||
Padding="8"
|
||||
Background="Transparent"
|
||||
BorderBrush="Transparent"
|
||||
Command="{Binding OpenSettingsCommand}">
|
||||
<FontIcon FontSize="20" Glyph="" />
|
||||
<ToolTipService.ToolTip>
|
||||
<TextBlock x:Uid="Open_settings" />
|
||||
</ToolTipService.ToolTip>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
MinWidth="76"
|
||||
Command="{Binding ResizeCommand}"
|
||||
Style="{StaticResource AccentButtonStyle}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<FontIcon FontSize="16" Glyph="" />
|
||||
<TextBlock x:Uid="Input_Resize" Margin="8,0,0,0" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
Grid.Column="2"
|
||||
MinWidth="76"
|
||||
Margin="8,0,0,0"
|
||||
Command="{Binding CancelCommand}">
|
||||
<TextBlock x:Uid="Cancel" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
using ImageResizer.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
using static ImageResizer.ViewModels.InputViewModel;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
public sealed partial class InputPage : Page
|
||||
{
|
||||
public InputViewModel ViewModel { get; set; }
|
||||
|
||||
public InputPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void NumberBox_KeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if (e.Key == Windows.System.VirtualKey.Enter)
|
||||
{
|
||||
var numberBox = sender as NumberBox;
|
||||
if (numberBox != null && ViewModel != null)
|
||||
{
|
||||
KeyPressParams keyParams;
|
||||
var value = numberBox.Value;
|
||||
|
||||
if (!double.IsNaN(value))
|
||||
{
|
||||
switch (numberBox.Name)
|
||||
{
|
||||
case "WidthNumberBox":
|
||||
keyParams = new KeyPressParams
|
||||
{
|
||||
Value = value,
|
||||
Dimension = Dimension.Width,
|
||||
};
|
||||
break;
|
||||
|
||||
case "HeightNumberBox":
|
||||
keyParams = new KeyPressParams
|
||||
{
|
||||
Value = value,
|
||||
Dimension = Dimension.Height,
|
||||
};
|
||||
break;
|
||||
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.EnterKeyPressedCommand.Execute(keyParams);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ImageResizer.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
public partial class PageTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate InputTemplate { get; set; }
|
||||
|
||||
public DataTemplate ProgressTemplate { get; set; }
|
||||
|
||||
public DataTemplate ResultsTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item)
|
||||
{
|
||||
return item switch
|
||||
{
|
||||
InputViewModel => InputTemplate,
|
||||
ProgressViewModel => ProgressTemplate,
|
||||
ResultsViewModel => ResultsTemplate,
|
||||
_ => base.SelectTemplateCore(item),
|
||||
};
|
||||
}
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
return SelectTemplateCore(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
<Page
|
||||
x:Class="ImageResizer.Views.ProgressPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:ImageResizer.Converters"
|
||||
xmlns:core="using:Microsoft.Xaml.Interactions.Core"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:interactivity="using:Microsoft.Xaml.Interactivity"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
Loaded="Page_Loaded"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
x:Uid="Progress_MainInstruction"
|
||||
Margin="12,12,12,0"
|
||||
FontSize="16" />
|
||||
<TextBlock
|
||||
Margin="12,12,12,0"
|
||||
Foreground="{StaticResource TextFillColorSecondaryBrush}"
|
||||
Text="{Binding TimeRemaining, Mode=OneWay, Converter={StaticResource TimeRemainingConverter}}" />
|
||||
<ProgressBar
|
||||
Height="16"
|
||||
Margin="12,12,12,0"
|
||||
Maximum="1"
|
||||
Value="{Binding Progress, Mode=OneWay}" />
|
||||
<Border
|
||||
Margin="0,12,0,0"
|
||||
Padding="12"
|
||||
Background="{StaticResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{StaticResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button MinWidth="76" Command="{Binding StopCommand}">
|
||||
<TextBlock x:Uid="Progress_Stop" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Page>
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
using ImageResizer.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
public sealed partial class ProgressPage : Page
|
||||
{
|
||||
public ProgressViewModel ViewModel { get; set; }
|
||||
|
||||
public ProgressPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void Page_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
ViewModel?.StartCommand.Execute(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,27 @@
|
||||
<UserControl
|
||||
<!-- Copyright (c) Microsoft Corporation. All rights reserved. -->
|
||||
<!-- Licensed under the MIT License. -->
|
||||
<Page
|
||||
x:Class="ImageResizer.Views.ResultsPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:p="clr-namespace:ImageResizer.Properties">
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:m="using:ImageResizer.Models"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:ImageResizer.ViewModels"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
x:Uid="Results_MainInstruction"
|
||||
Margin="12,12,12,0"
|
||||
FontSize="16"
|
||||
Text="{x:Static p:Resources.Results_MainInstruction}" />
|
||||
<ScrollViewer HorizontalAlignment="Stretch" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl Margin="12,4,12,0" ItemsSource="{Binding Errors}">
|
||||
FontSize="16" />
|
||||
<ScrollViewer
|
||||
MaxHeight="300"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl Margin="12,4,12,0" ItemsSource="{Binding Errors, Mode=OneWay}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="ResizeError">
|
||||
<DataTemplate>
|
||||
<StackPanel>
|
||||
<TextBlock
|
||||
Margin="0,8,0,0"
|
||||
@@ -27,18 +36,15 @@
|
||||
|
||||
<Border
|
||||
Margin="0,12,0,0"
|
||||
Padding="12,12"
|
||||
Background="{DynamicResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{DynamicResource DividerStrokeColorDefaultBrush}"
|
||||
Padding="12"
|
||||
Background="{StaticResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{StaticResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0">
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
|
||||
<Button
|
||||
MinWidth="76"
|
||||
Command="{Binding CloseCommand}"
|
||||
Content="{x:Static p:Resources.Results_Close}"
|
||||
IsCancel="True"
|
||||
IsDefault="True" />
|
||||
<Button MinWidth="76" Command="{Binding CloseCommand}">
|
||||
<TextBlock x:Uid="Results_Close" />
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
</Page>
|
||||
@@ -0,0 +1,20 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
using ImageResizer.ViewModels;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
public sealed partial class ResultsPage : Page
|
||||
{
|
||||
public ResultsViewModel ViewModel { get; set; }
|
||||
|
||||
public ResultsPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ImageResizer.Models;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace ImageResizer.Views
|
||||
{
|
||||
public partial class SizeDataTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
public DataTemplate ResizeSizeTemplate { get; set; }
|
||||
public DataTemplate CustomSizeTemplate { get; set; }
|
||||
public DataTemplate AiSizeTemplate { get; set; }
|
||||
|
||||
protected override DataTemplate SelectTemplateCore(object item, DependencyObject container)
|
||||
{
|
||||
if (item is AiSize)
|
||||
{
|
||||
return AiSizeTemplate;
|
||||
}
|
||||
|
||||
if (item is CustomSize)
|
||||
{
|
||||
return CustomSizeTemplate;
|
||||
}
|
||||
|
||||
if (item is ResizeSize)
|
||||
{
|
||||
return ResizeSizeTemplate;
|
||||
}
|
||||
|
||||
return base.SelectTemplateCore(item, container);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,41 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Globalization;
|
||||
using System.Text;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ImageResizer.Helpers;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class AiSize : ResizeSize
|
||||
public partial class AiSize : ResizeSize
|
||||
{
|
||||
private static readonly CompositeFormat ScaleFormat = CompositeFormat.Parse(Resources.Input_AiScaleFormat);
|
||||
private static CompositeFormat _scaleFormat;
|
||||
|
||||
private static CompositeFormat ScaleFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_scaleFormat == null)
|
||||
{
|
||||
_scaleFormat = CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Input_AiScaleFormat"));
|
||||
}
|
||||
|
||||
return _scaleFormat;
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("scale")]
|
||||
private int _scale = 2;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the formatted scale display string (e.g., "2×").
|
||||
/// Gets the formatted scale display string (e.g., "2x").
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, _scale);
|
||||
|
||||
[JsonPropertyName("scale")]
|
||||
public int Scale
|
||||
{
|
||||
get => _scale;
|
||||
set => Set(ref _scale, value);
|
||||
}
|
||||
public string ScaleDisplay => string.Format(CultureInfo.CurrentCulture, ScaleFormat, Scale);
|
||||
|
||||
[JsonConstructor]
|
||||
public AiSize(int scale)
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Collections.ObjectModel;
|
||||
using System.CommandLine.Parsing;
|
||||
using System.Globalization;
|
||||
using ImageResizer.Cli.Commands;
|
||||
using ImageResizer.Helpers;
|
||||
|
||||
#pragma warning disable SA1649 // File name should match first type name
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
@@ -19,117 +20,51 @@ namespace ImageResizer.Models
|
||||
/// </summary>
|
||||
public class CliOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to show help information.
|
||||
/// </summary>
|
||||
public bool ShowHelp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to show current configuration.
|
||||
/// </summary>
|
||||
public bool ShowConfig { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the destination directory for resized images.
|
||||
/// </summary>
|
||||
public string DestinationDirectory { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the width of the resized image.
|
||||
/// </summary>
|
||||
public double? Width { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the height of the resized image.
|
||||
/// </summary>
|
||||
public double? Height { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resize unit (Pixel, Percent, Inch, Centimeter).
|
||||
/// </summary>
|
||||
public ResizeUnit? Unit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resize fit mode (Fill, Fit, Stretch).
|
||||
/// </summary>
|
||||
public ResizeFit? Fit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the index of the preset size to use.
|
||||
/// </summary>
|
||||
public int? SizeIndex { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to only shrink images (not enlarge).
|
||||
/// </summary>
|
||||
public bool? ShrinkOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to replace the original file.
|
||||
/// </summary>
|
||||
public bool? Replace { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to ignore orientation when resizing.
|
||||
/// </summary>
|
||||
public bool? IgnoreOrientation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to remove metadata from the resized image.
|
||||
/// </summary>
|
||||
public bool? RemoveMetadata { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the JPEG quality level (1-100).
|
||||
/// </summary>
|
||||
public int? JpegQualityLevel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to keep the date modified.
|
||||
/// </summary>
|
||||
public bool? KeepDateModified { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the output filename format.
|
||||
/// </summary>
|
||||
public string FileName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to use line-based progress output for screen reader accessibility.
|
||||
/// </summary>
|
||||
public bool? ProgressLines { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the list of files to process.
|
||||
/// </summary>
|
||||
public ICollection<string> Files { get; } = new List<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the pipe name for receiving file list.
|
||||
/// </summary>
|
||||
public string PipeName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets parse/validation errors produced by System.CommandLine.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> ParseErrors { get; private set; } = Array.Empty<string>();
|
||||
|
||||
/// <summary>
|
||||
/// Converts a boolean value to nullable bool (true -> true, false -> null).
|
||||
/// </summary>
|
||||
private static bool? ToBoolOrNull(bool value) => value ? true : null;
|
||||
|
||||
/// <summary>
|
||||
/// Parses command-line arguments into CliOptions using System.CommandLine.
|
||||
/// </summary>
|
||||
/// <param name="args">The command-line arguments.</param>
|
||||
/// <returns>A CliOptions instance with parsed values.</returns>
|
||||
public static CliOptions Parse(string[] args)
|
||||
{
|
||||
var options = new CliOptions();
|
||||
var cmd = new ImageResizerRootCommand();
|
||||
|
||||
// Parse using System.CommandLine
|
||||
var parseResult = new Parser(cmd).Parse(args);
|
||||
|
||||
if (parseResult.Errors.Count > 0)
|
||||
@@ -143,7 +78,6 @@ namespace ImageResizer.Models
|
||||
options.ParseErrors = new ReadOnlyCollection<string>(errors);
|
||||
}
|
||||
|
||||
// Extract values from parse result using strongly typed options
|
||||
options.ShowHelp = parseResult.GetValueForOption(cmd.HelpOption);
|
||||
options.ShowConfig = parseResult.GetValueForOption(cmd.ShowConfigOption);
|
||||
options.DestinationDirectory = parseResult.GetValueForOption(cmd.DestinationOption);
|
||||
@@ -153,7 +87,6 @@ namespace ImageResizer.Models
|
||||
options.Fit = parseResult.GetValueForOption(cmd.FitOption);
|
||||
options.SizeIndex = parseResult.GetValueForOption(cmd.SizeOption);
|
||||
|
||||
// Convert bool to nullable bool (true -> true, false -> null)
|
||||
options.ShrinkOnly = ToBoolOrNull(parseResult.GetValueForOption(cmd.ShrinkOnlyOption));
|
||||
options.Replace = ToBoolOrNull(parseResult.GetValueForOption(cmd.ReplaceOption));
|
||||
options.IgnoreOrientation = ToBoolOrNull(parseResult.GetValueForOption(cmd.IgnoreOrientationOption));
|
||||
@@ -165,14 +98,12 @@ namespace ImageResizer.Models
|
||||
|
||||
options.FileName = parseResult.GetValueForOption(cmd.FileNameOption);
|
||||
|
||||
// Get files from arguments
|
||||
var files = parseResult.GetValueForArgument(cmd.FilesArgument);
|
||||
if (files != null)
|
||||
{
|
||||
const string pipeNamePrefix = "\\\\.\\pipe\\";
|
||||
foreach (var file in files)
|
||||
{
|
||||
// Check for pipe name (must be at the start of the path)
|
||||
if (file.StartsWith(pipeNamePrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
options.PipeName = file.Substring(pipeNamePrefix.Length);
|
||||
@@ -187,62 +118,55 @@ namespace ImageResizer.Models
|
||||
return options;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints current configuration to the console.
|
||||
/// </summary>
|
||||
/// <param name="settings">The settings to display.</param>
|
||||
public static void PrintConfig(ImageResizer.Properties.Settings settings)
|
||||
{
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
Console.WriteLine(Properties.Resources.CLI_ConfigTitle);
|
||||
Console.WriteLine(loader.GetString("CLI_ConfigTitle"));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.CLI_ConfigGeneralSettings);
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigShrinkOnly, settings.ShrinkOnly));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigReplaceOriginal, settings.Replace));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigIgnoreOrientation, settings.IgnoreOrientation));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigRemoveMetadata, settings.RemoveMetadata));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigKeepDateModified, settings.KeepDateModified));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigJpegQuality, settings.JpegQualityLevel));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPngInterlace, settings.PngInterlaceOption));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigTiffCompress, settings.TiffCompressOption));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFilenameFormat, settings.FileName));
|
||||
Console.WriteLine(loader.GetString("CLI_ConfigGeneralSettings"));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigShrinkOnly"), settings.ShrinkOnly));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigReplaceOriginal"), settings.Replace));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigIgnoreOrientation"), settings.IgnoreOrientation));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigRemoveMetadata"), settings.RemoveMetadata));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigKeepDateModified"), settings.KeepDateModified));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigJpegQuality"), settings.JpegQualityLevel));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigPngInterlace"), settings.PngInterlaceOption));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigTiffCompress"), settings.TiffCompressOption));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigFilenameFormat"), settings.FileName));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.CLI_ConfigCustomSize);
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigWidth, settings.CustomSize.Width, settings.CustomSize.Unit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigHeight, settings.CustomSize.Height, settings.CustomSize.Unit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigFitMode, settings.CustomSize.Fit));
|
||||
Console.WriteLine(loader.GetString("CLI_ConfigCustomSize"));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigWidth"), settings.CustomSize.Width, settings.CustomSize.Unit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigHeight"), settings.CustomSize.Height, settings.CustomSize.Unit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigFitMode"), settings.CustomSize.Fit));
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.CLI_ConfigPresetSizes);
|
||||
Console.WriteLine(loader.GetString("CLI_ConfigPresetSizes"));
|
||||
for (int i = 0; i < settings.Sizes.Count; i++)
|
||||
{
|
||||
var size = settings.Sizes[i];
|
||||
var selected = i == settings.SelectedSizeIndex ? "*" : " ";
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigPresetSizeFormat, i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigPresetSizeFormat"), i, selected, size.Name, size.Width, size.Height, size.Unit, size.Fit));
|
||||
}
|
||||
|
||||
if (settings.SelectedSizeIndex >= settings.Sizes.Count)
|
||||
{
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, Properties.Resources.CLI_ConfigCustomSelected, settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
|
||||
Console.WriteLine(string.Format(CultureInfo.InvariantCulture, loader.GetString("CLI_ConfigCustomSelected"), settings.CustomSize.Width, settings.CustomSize.Height, settings.CustomSize.Unit, settings.CustomSize.Fit));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Prints usage information to the console.
|
||||
/// </summary>
|
||||
public static void PrintUsage()
|
||||
{
|
||||
var loader = ResourceLoaderInstance.ResourceLoader;
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageTitle);
|
||||
Console.WriteLine(loader.GetString("CLI_UsageTitle"));
|
||||
Console.WriteLine();
|
||||
|
||||
var cmd = new ImageResizerRootCommand();
|
||||
|
||||
// Print usage line
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageLine);
|
||||
Console.WriteLine(loader.GetString("CLI_UsageLine"));
|
||||
Console.WriteLine();
|
||||
|
||||
// Print options from the command definition
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageOptions);
|
||||
Console.WriteLine(loader.GetString("CLI_UsageOptions"));
|
||||
foreach (var option in cmd.Options)
|
||||
{
|
||||
var aliases = string.Join(", ", option.Aliases);
|
||||
@@ -251,11 +175,11 @@ namespace ImageResizer.Models
|
||||
}
|
||||
|
||||
Console.WriteLine();
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageExamples);
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageExampleHelp);
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageExampleDimensions);
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageExamplePercent);
|
||||
Console.WriteLine(Properties.Resources.CLI_UsageExamplePreset);
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExamples"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExampleHelp"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExampleDimensions"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExamplePercent"));
|
||||
Console.WriteLine(loader.GetString("CLI_UsageExamplePreset"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Helpers;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
@@ -15,7 +14,7 @@ namespace ImageResizer.Models
|
||||
[JsonIgnore]
|
||||
public override string Name
|
||||
{
|
||||
get => Resources.Input_Custom;
|
||||
get => ResourceLoaderInstance.ResourceLoader.GetString("Input_Custom");
|
||||
set { /* no-op */ }
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
@@ -14,7 +14,6 @@ using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
@@ -14,13 +14,12 @@ using System.Text;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Extensions;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Properties;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.Utilities;
|
||||
using Microsoft.VisualBasic.FileIO;
|
||||
|
||||
using FileSystem = Microsoft.VisualBasic.FileIO.FileSystem;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
@@ -35,7 +34,20 @@ namespace ImageResizer.Models
|
||||
private readonly IAISuperResolutionService _aiSuperResolutionService;
|
||||
|
||||
// Cache CompositeFormat for AI error message formatting (CA1863)
|
||||
private static readonly CompositeFormat _aiErrorFormat = CompositeFormat.Parse(Resources.Error_AiProcessingFailed);
|
||||
private static CompositeFormat _aiErrorFormat;
|
||||
|
||||
private static CompositeFormat AiErrorFormat
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_aiErrorFormat == null)
|
||||
{
|
||||
_aiErrorFormat = CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiProcessingFailed"));
|
||||
}
|
||||
|
||||
return _aiErrorFormat;
|
||||
}
|
||||
}
|
||||
|
||||
// Filenames to avoid according to https://learn.microsoft.com/windows/win32/fileio/naming-a-file#file-and-directory-names
|
||||
private static readonly string[] _avoidFilenames =
|
||||
@@ -187,8 +199,6 @@ namespace ImageResizer.Models
|
||||
double height = _settings.SelectedSize.GetPixelHeight(originalHeight, source.DpiY);
|
||||
|
||||
// Swap target width/height dimensions if orientation correction is required.
|
||||
// Ensures that we don't try to fit a landscape image into a portrait box by
|
||||
// distorting it, unless specific Auto/Percent rules are applied.
|
||||
bool canSwapDimensions = _settings.IgnoreOrientation &&
|
||||
!_settings.SelectedSize.HasAuto &&
|
||||
_settings.SelectedSize.Unit != ResizeUnit.Percent;
|
||||
@@ -214,15 +224,11 @@ namespace ImageResizer.Models
|
||||
// Normalize scales based on the chosen Fit/Fill mode.
|
||||
if (_settings.SelectedSize.Fit == ResizeFit.Fit)
|
||||
{
|
||||
// Fit: use the smaller scale to ensure the image fits within the target.
|
||||
scaleX = Math.Min(scaleX, scaleY);
|
||||
scaleY = scaleX;
|
||||
}
|
||||
else if (_settings.SelectedSize.Fit == ResizeFit.Fill)
|
||||
{
|
||||
// Fill: use the larger scale to ensure the target area is fully covered.
|
||||
// This often results in one dimension overflowing, which is handled by
|
||||
// cropping later.
|
||||
scaleX = Math.Max(scaleX, scaleY);
|
||||
scaleY = scaleX;
|
||||
}
|
||||
@@ -230,21 +236,14 @@ namespace ImageResizer.Models
|
||||
// Handle Shrink Only mode.
|
||||
if (_settings.ShrinkOnly && _settings.SelectedSize.Unit != ResizeUnit.Percent)
|
||||
{
|
||||
// Shrink Only mode should never return an image larger than the original.
|
||||
if (scaleX > 1 || scaleY > 1)
|
||||
{
|
||||
return source;
|
||||
}
|
||||
|
||||
// Allow for crop-only when in Fill mode.
|
||||
// At this point, the scale is <= 1.0. In Fill mode, it is possible for
|
||||
// the scale to be 1.0 (no resize needed) while the target dimensions are
|
||||
// smaller than the originals, requiring a crop.
|
||||
bool isFillCropRequired = _settings.SelectedSize.Fit == ResizeFit.Fill &&
|
||||
(originalWidth > width || originalHeight > height);
|
||||
|
||||
// If the scale is exactly 1.0 and a crop isn't required, we return the
|
||||
// original image to prevent a re-encode.
|
||||
if (scaleX == 1 && scaleY == 1 && !isFillCropRequired)
|
||||
{
|
||||
return source;
|
||||
@@ -254,8 +253,7 @@ namespace ImageResizer.Models
|
||||
// Apply the scaling.
|
||||
var scaledBitmap = new TransformedBitmap(source, new ScaleTransform(scaleX, scaleY));
|
||||
|
||||
// Apply the centered crop for Fill mode, if necessary. Applies when Fill
|
||||
// mode caused the scaled image to exceed the target dimensions.
|
||||
// Apply the centered crop for Fill mode, if necessary.
|
||||
if (_settings.SelectedSize.Fit == ResizeFit.Fill
|
||||
&& (scaledBitmap.PixelWidth > width
|
||||
|| scaledBitmap.PixelHeight > height))
|
||||
@@ -280,25 +278,18 @@ namespace ImageResizer.Models
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
throw new InvalidOperationException(Properties.Resources.Error_AiConversionFailed);
|
||||
throw new InvalidOperationException(ResourceLoaderInstance.ResourceLoader.GetString("Error_AiConversionFailed"));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Wrap the exception with a localized message
|
||||
// This will be caught by ResizeBatch.Process() and displayed to the user
|
||||
var errorMessage = string.Format(CultureInfo.CurrentCulture, _aiErrorFormat, ex.Message);
|
||||
var errorMessage = string.Format(CultureInfo.CurrentCulture, AiErrorFormat, ex.Message);
|
||||
throw new InvalidOperationException(errorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks original metadata by writing an image containing the given metadata into a memory stream.
|
||||
/// In case of errors, we try to rebuild the metadata object and check again.
|
||||
/// We return null if we were not able to get hold of valid metadata.
|
||||
/// </summary>
|
||||
private BitmapMetadata GetValidMetadata(BitmapMetadata originalMetadata, BitmapSource transformedBitmap, Guid containerFormat)
|
||||
{
|
||||
if (originalMetadata == null)
|
||||
@@ -306,14 +297,12 @@ namespace ImageResizer.Models
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if the original metadata is valid
|
||||
var frameWithOriginalMetadata = CreateBitmapFrame(transformedBitmap, originalMetadata);
|
||||
if (EnsureFrameIsValid(frameWithOriginalMetadata))
|
||||
{
|
||||
return originalMetadata;
|
||||
}
|
||||
|
||||
// Original metadata was invalid. We try to rebuild the metadata object from the scratch and discard invalid metadata fields
|
||||
var recreatedMetadata = BuildMetadataFromTheScratch(originalMetadata);
|
||||
var frameWithRecreatedMetadata = CreateBitmapFrame(transformedBitmap, recreatedMetadata);
|
||||
if (EnsureFrameIsValid(frameWithRecreatedMetadata))
|
||||
@@ -321,11 +310,8 @@ namespace ImageResizer.Models
|
||||
return recreatedMetadata;
|
||||
}
|
||||
|
||||
// Seems like we have an invalid metadata object. ImageResizer will fail when trying to write the image to disk. We discard all metadata to be able to save the image.
|
||||
return null;
|
||||
|
||||
// The safest way to check if the metadata object is valid is to call Save() on the encoder.
|
||||
// I tried other ways to check if metadata is valid (like calling Clone() on the metadata object) but this was not reliable resulting in a few github issues.
|
||||
bool EnsureFrameIsValid(BitmapFrame frameToBeChecked)
|
||||
{
|
||||
try
|
||||
@@ -346,9 +332,6 @@ namespace ImageResizer.Models
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read all metadata and build up metadata object from the scratch. Discard invalid (unreadable/unwritable) metadata.
|
||||
/// </summary>
|
||||
private static BitmapMetadata BuildMetadataFromTheScratch(BitmapMetadata originalMetadata)
|
||||
{
|
||||
try
|
||||
@@ -382,9 +365,9 @@ namespace ImageResizer.Models
|
||||
{
|
||||
return BitmapFrame.Create(
|
||||
transformedBitmap,
|
||||
thumbnail: null, /* should be null, see #15413 */
|
||||
thumbnail: null,
|
||||
metadata,
|
||||
colorContexts: null /* should be null, see #14866 */ );
|
||||
colorContexts: null);
|
||||
}
|
||||
|
||||
private string GetDestinationPath(BitmapEncoder encoder)
|
||||
@@ -399,8 +382,6 @@ namespace ImageResizer.Models
|
||||
extension = supportedExtensions.FirstOrDefault();
|
||||
}
|
||||
|
||||
// Remove directory characters from the size's name.
|
||||
// For AI Size, use the scale display (e.g., "2×") instead of the full name
|
||||
string sizeName = _settings.SelectedSize is AiSize aiSize
|
||||
? aiSize.ScaleDisplay
|
||||
: _settings.SelectedSize.Name;
|
||||
@@ -408,7 +389,6 @@ namespace ImageResizer.Models
|
||||
.Replace('\\', '_')
|
||||
.Replace('/', '_');
|
||||
|
||||
// Using CurrentCulture since this is user facing
|
||||
var selectedWidth = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelWidth : _settings.SelectedSize.Width;
|
||||
var selectedHeight = _settings.SelectedSize is AiSize ? encoder.Frames[0].PixelHeight : _settings.SelectedSize.Height;
|
||||
var fileName = string.Format(
|
||||
@@ -421,7 +401,6 @@ namespace ImageResizer.Models
|
||||
encoder.Frames[0].PixelWidth,
|
||||
encoder.Frames[0].PixelHeight);
|
||||
|
||||
// Remove invalid characters from the final file name.
|
||||
fileName = fileName
|
||||
.Replace(':', '_')
|
||||
.Replace('*', '_')
|
||||
@@ -431,7 +410,6 @@ namespace ImageResizer.Models
|
||||
.Replace('>', '_')
|
||||
.Replace('|', '_');
|
||||
|
||||
// Avoid creating not recommended filenames
|
||||
if (_avoidFilenames.Contains(fileName.ToUpperInvariant()))
|
||||
{
|
||||
fileName = fileName + "_";
|
||||
|
||||
@@ -1,36 +1,54 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
// Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Properties;
|
||||
using ManagedCommon;
|
||||
|
||||
namespace ImageResizer.Models
|
||||
{
|
||||
public class ResizeSize : Observable, IHasId
|
||||
public partial class ResizeSize : ObservableObject, IHasId
|
||||
{
|
||||
private static readonly Dictionary<string, string> _tokens = new Dictionary<string, string>
|
||||
{
|
||||
["$small$"] = Resources.Small,
|
||||
["$medium$"] = Resources.Medium,
|
||||
["$large$"] = Resources.Large,
|
||||
["$phone$"] = Resources.Phone,
|
||||
};
|
||||
// Lazy initialization to avoid ResourceLoader call during class loading (enables unit testing)
|
||||
private static readonly Lazy<Dictionary<string, string>> _tokens = new Lazy<Dictionary<string, string>>(() =>
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
["$small$"] = ResourceLoaderInstance.ResourceLoader.GetString("Small"),
|
||||
["$medium$"] = ResourceLoaderInstance.ResourceLoader.GetString("Medium"),
|
||||
["$large$"] = ResourceLoaderInstance.ResourceLoader.GetString("Large"),
|
||||
["$phone$"] = ResourceLoaderInstance.ResourceLoader.GetString("Phone"),
|
||||
});
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("Id")]
|
||||
private int _id;
|
||||
|
||||
private string _name;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("fit")]
|
||||
[NotifyPropertyChangedFor(nameof(ShowHeight))]
|
||||
private ResizeFit _fit = ResizeFit.Fit;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("width")]
|
||||
private double _width;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("height")]
|
||||
private double _height;
|
||||
private bool _showHeight = true;
|
||||
|
||||
[ObservableProperty]
|
||||
[JsonPropertyName("unit")]
|
||||
[NotifyPropertyChangedFor(nameof(ShowHeight))]
|
||||
private ResizeUnit _unit = ResizeUnit.Pixel;
|
||||
|
||||
public ResizeSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
|
||||
@@ -47,73 +65,18 @@ namespace ImageResizer.Models
|
||||
{
|
||||
}
|
||||
|
||||
[JsonPropertyName("Id")]
|
||||
public int Id
|
||||
{
|
||||
get => _id;
|
||||
set => Set(ref _id, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public virtual string Name
|
||||
{
|
||||
get => _name;
|
||||
set => Set(ref _name, ReplaceTokens(value));
|
||||
set => SetProperty(ref _name, ReplaceTokens(value));
|
||||
}
|
||||
|
||||
[JsonPropertyName("fit")]
|
||||
public ResizeFit Fit
|
||||
{
|
||||
get => _fit;
|
||||
set
|
||||
{
|
||||
var previousFit = _fit;
|
||||
Set(ref _fit, value);
|
||||
if (!Equals(previousFit, value))
|
||||
{
|
||||
UpdateShowHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[JsonPropertyName("width")]
|
||||
public double Width
|
||||
{
|
||||
get => _width;
|
||||
set => Set(ref _width, value);
|
||||
}
|
||||
|
||||
[JsonPropertyName("height")]
|
||||
public double Height
|
||||
{
|
||||
get => _height;
|
||||
set => Set(ref _height, value);
|
||||
}
|
||||
|
||||
public bool ShowHeight
|
||||
{
|
||||
get => _showHeight;
|
||||
set => Set(ref _showHeight, value);
|
||||
}
|
||||
public bool ShowHeight => Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
|
||||
|
||||
public bool HasAuto
|
||||
=> Width == 0 || Height == 0 || double.IsNaN(Width) || double.IsNaN(Height);
|
||||
|
||||
[JsonPropertyName("unit")]
|
||||
public ResizeUnit Unit
|
||||
{
|
||||
get => _unit;
|
||||
set
|
||||
{
|
||||
var previousUnit = _unit;
|
||||
Set(ref _unit, value);
|
||||
if (!Equals(previousUnit, value))
|
||||
{
|
||||
UpdateShowHeight();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public double GetPixelWidth(int originalWidth, double dpi)
|
||||
=> ConvertToPixels(Width, Unit, originalWidth, dpi);
|
||||
|
||||
@@ -127,15 +90,10 @@ namespace ImageResizer.Models
|
||||
dpi);
|
||||
|
||||
private static string ReplaceTokens(string text)
|
||||
=> (text != null && _tokens.TryGetValue(text, out var result))
|
||||
=> (text != null && _tokens.Value.TryGetValue(text, out var result))
|
||||
? result
|
||||
: text;
|
||||
|
||||
private void UpdateShowHeight()
|
||||
{
|
||||
ShowHeight = Fit == ResizeFit.Stretch || Unit != ResizeUnit.Percent;
|
||||
}
|
||||
|
||||
private double ConvertToPixels(double value, ResizeUnit unit, int originalValue, double dpi)
|
||||
{
|
||||
if (value == 0 || double.IsNaN(value))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
27
src/modules/imageresizer/ui/Program.cs
Normal file
27
src/modules/imageresizer/ui/Program.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.UI.Dispatching;
|
||||
using Microsoft.UI.Xaml;
|
||||
|
||||
namespace ImageResizer
|
||||
{
|
||||
public static class Program
|
||||
{
|
||||
[STAThread]
|
||||
public static void Main(string[] args)
|
||||
{
|
||||
WinRT.ComWrappersSupport.InitializeComWrappers();
|
||||
Application.Start((p) =>
|
||||
{
|
||||
var context = new DispatcherQueueSynchronizationContext(
|
||||
DispatcherQueue.GetForCurrentThread());
|
||||
SynchronizationContext.SetSynchronizationContext(context);
|
||||
_ = new App();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("ImageResizer.Test")]
|
||||
1152
src/modules/imageresizer/ui/Properties/Resources.Designer.cs
generated
1152
src/modules/imageresizer/ui/Properties/Resources.Designer.cs
generated
File diff suppressed because it is too large
Load Diff
75
src/modules/imageresizer/ui/Properties/Resources.cs
Normal file
75
src/modules/imageresizer/ui/Properties/Resources.cs
Normal file
@@ -0,0 +1,75 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ImageResizer.Helpers;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
/// <summary>
|
||||
/// Resource accessor class for compatibility with CLI code and tests.
|
||||
/// Wraps ResourceLoader for resource string access.
|
||||
/// </summary>
|
||||
internal static class Resources
|
||||
{
|
||||
// Size names (used by tests and ResizeSize token replacement)
|
||||
public static string Small => ResourceLoaderInstance.ResourceLoader.GetString("Small");
|
||||
|
||||
public static string Medium => ResourceLoaderInstance.ResourceLoader.GetString("Medium");
|
||||
|
||||
public static string Large => ResourceLoaderInstance.ResourceLoader.GetString("Large");
|
||||
|
||||
public static string Phone => ResourceLoaderInstance.ResourceLoader.GetString("Phone");
|
||||
|
||||
// Input page resources
|
||||
public static string Input_Custom => ResourceLoaderInstance.ResourceLoader.GetString("Input_Custom");
|
||||
|
||||
// Validation messages
|
||||
public static string ValueMustBeBetween => ResourceLoaderInstance.ResourceLoader.GetString("ValueMustBeBetween");
|
||||
|
||||
// CLI options
|
||||
public static string CLI_Option_Destination => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Destination");
|
||||
|
||||
public static string CLI_Option_FileName => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_FileName");
|
||||
|
||||
public static string CLI_Option_Files => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Files");
|
||||
|
||||
public static string CLI_Option_Fit => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Fit");
|
||||
|
||||
public static string CLI_Option_Height => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Height");
|
||||
|
||||
public static string CLI_Option_Help => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Help");
|
||||
|
||||
public static string CLI_Option_IgnoreOrientation => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_IgnoreOrientation");
|
||||
|
||||
public static string CLI_Option_KeepDateModified => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_KeepDateModified");
|
||||
|
||||
public static string CLI_Option_Quality => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Quality");
|
||||
|
||||
public static string CLI_Option_Replace => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Replace");
|
||||
|
||||
public static string CLI_Option_ShowConfig => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_ShowConfig");
|
||||
|
||||
public static string CLI_Option_ShrinkOnly => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_ShrinkOnly");
|
||||
|
||||
public static string CLI_Option_RemoveMetadata => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_RemoveMetadata");
|
||||
|
||||
public static string CLI_Option_Size => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Size");
|
||||
|
||||
public static string CLI_Option_Unit => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Unit");
|
||||
|
||||
public static string CLI_Option_Width => ResourceLoaderInstance.ResourceLoader.GetString("CLI_Option_Width");
|
||||
|
||||
public static string CLI_ProcessingFiles => ResourceLoaderInstance.ResourceLoader.GetString("CLI_ProcessingFiles");
|
||||
|
||||
public static string CLI_ProgressFormat => ResourceLoaderInstance.ResourceLoader.GetString("CLI_ProgressFormat");
|
||||
|
||||
public static string CLI_CompletedWithErrors => ResourceLoaderInstance.ResourceLoader.GetString("CLI_CompletedWithErrors");
|
||||
|
||||
public static string CLI_AllFilesProcessed => ResourceLoaderInstance.ResourceLoader.GetString("CLI_AllFilesProcessed");
|
||||
|
||||
public static string CLI_WarningInvalidSizeIndex => ResourceLoaderInstance.ResourceLoader.GetString("CLI_WarningInvalidSizeIndex");
|
||||
|
||||
public static string CLI_NoInputFiles => ResourceLoaderInstance.ResourceLoader.GetString("CLI_NoInputFiles");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
@@ -18,11 +18,10 @@ using System.Text.Json.Serialization;
|
||||
using System.Text.Json.Serialization.Metadata;
|
||||
using System.Threading;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Services;
|
||||
using ImageResizer.ViewModels;
|
||||
using ManagedCommon;
|
||||
using Microsoft.UI.Dispatching;
|
||||
|
||||
namespace ImageResizer.Properties
|
||||
{
|
||||
@@ -46,7 +45,23 @@ namespace ImageResizer.Properties
|
||||
TypeInfoResolver = new DefaultJsonTypeInfoResolver(),
|
||||
};
|
||||
|
||||
private static readonly CompositeFormat ValueMustBeBetween = System.Text.CompositeFormat.Parse(Properties.Resources.ValueMustBeBetween);
|
||||
// Cached UI thread DispatcherQueue for cross-thread property change notifications
|
||||
private static DispatcherQueue _uiDispatcherQueue;
|
||||
|
||||
private static CompositeFormat _valueMustBeBetween;
|
||||
|
||||
private static CompositeFormat ValueMustBeBetween
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_valueMustBeBetween == null)
|
||||
{
|
||||
_valueMustBeBetween = System.Text.CompositeFormat.Parse(ResourceLoaderInstance.ResourceLoader.GetString("ValueMustBeBetween"));
|
||||
}
|
||||
|
||||
return _valueMustBeBetween;
|
||||
}
|
||||
}
|
||||
|
||||
// Used to synchronize access to the settings.json file
|
||||
private static Mutex _jsonMutex = new Mutex();
|
||||
@@ -87,32 +102,25 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = false;
|
||||
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
|
||||
CustomSize = new CustomSize(ResizeFit.Fit, 1024, 640, ResizeUnit.Pixel);
|
||||
AiSize = new AiSize(2); // Initialize with default scale of 2
|
||||
AiSize = new AiSize(2);
|
||||
AllSizes = new AllSizesCollection(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the SelectedSizeIndex to ensure it's within the valid range.
|
||||
/// This handles cross-device migration where settings saved on ARM64 with AI selected
|
||||
/// are loaded on non-ARM64 devices.
|
||||
/// </summary>
|
||||
private void ValidateSelectedSizeIndex()
|
||||
{
|
||||
// Index structure: 0 to Sizes.Count-1 (regular), Sizes.Count (CustomSize), Sizes.Count+1 (AiSize)
|
||||
var maxIndex = ImageResizer.App.AiAvailabilityState == AiAvailabilityState.NotSupported
|
||||
? Sizes.Count // CustomSize only
|
||||
: Sizes.Count + 1; // CustomSize + AiSize
|
||||
? Sizes.Count
|
||||
: Sizes.Count + 1;
|
||||
|
||||
if (_selectedSizeIndex > maxIndex)
|
||||
{
|
||||
_selectedSizeIndex = 0; // Reset to first size
|
||||
_selectedSizeIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
[JsonIgnore]
|
||||
public IEnumerable<ResizeSize> AllSizes { get; set; }
|
||||
|
||||
// Using OrdinalIgnoreCase since this is internal and used for comparison with symbols
|
||||
public string FileNameFormat
|
||||
=> _fileNameFormat
|
||||
?? (_fileNameFormat = FileName
|
||||
@@ -144,7 +152,6 @@ namespace ImageResizer.Properties
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback to CustomSize when index is out of range or AI is not available
|
||||
return CustomSize;
|
||||
}
|
||||
}
|
||||
@@ -168,13 +175,7 @@ namespace ImageResizer.Properties
|
||||
}
|
||||
}
|
||||
|
||||
string IDataErrorInfo.Error
|
||||
{
|
||||
get
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
string IDataErrorInfo.Error => string.Empty;
|
||||
|
||||
string IDataErrorInfo.this[string columnName]
|
||||
{
|
||||
@@ -187,7 +188,6 @@ namespace ImageResizer.Properties
|
||||
|
||||
if (JpegQualityLevel < 1 || JpegQualityLevel > 100)
|
||||
{
|
||||
// Using CurrentCulture since this is user facing
|
||||
return string.Format(CultureInfo.CurrentCulture, ValueMustBeBetween, 1, 100);
|
||||
}
|
||||
|
||||
@@ -217,26 +217,20 @@ namespace ImageResizer.Properties
|
||||
if (e.PropertyName == nameof(Models.CustomSize))
|
||||
{
|
||||
_customSize = settings.CustomSize;
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Models.AiSize))
|
||||
{
|
||||
_aiSize = settings.AiSize;
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
}
|
||||
else if (e.PropertyName == nameof(Sizes))
|
||||
{
|
||||
var oldSizes = _sizes;
|
||||
|
||||
oldSizes.CollectionChanged -= HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)oldSizes).PropertyChanged -= HandlePropertyChanged;
|
||||
|
||||
_sizes = settings.Sizes;
|
||||
|
||||
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
|
||||
|
||||
_sizes.CollectionChanged += HandleCollectionChanged;
|
||||
((INotifyPropertyChanged)_sizes).PropertyChanged += HandlePropertyChanged;
|
||||
}
|
||||
@@ -244,7 +238,6 @@ namespace ImageResizer.Properties
|
||||
}
|
||||
|
||||
public event NotifyCollectionChangedEventHandler CollectionChanged;
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public int Count
|
||||
@@ -291,7 +284,6 @@ namespace ImageResizer.Properties
|
||||
private class AllSizesEnumerator : IEnumerator<ResizeSize>
|
||||
{
|
||||
private readonly AllSizesCollection _list;
|
||||
|
||||
private int _index = -1;
|
||||
|
||||
public AllSizesEnumerator(AllSizesCollection list)
|
||||
@@ -376,15 +368,6 @@ namespace ImageResizer.Properties
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether resizing images removes any metadata that doesn't affect rendering.
|
||||
/// Default is false.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Preserved Metadata:
|
||||
/// System.Photo.Orientation,
|
||||
/// System.Image.ColorSpace
|
||||
/// </remarks>
|
||||
[JsonConverter(typeof(WrappedJsonValueConverter))]
|
||||
[JsonPropertyName("imageresizer_removeMetadata")]
|
||||
public bool RemoveMetadata
|
||||
@@ -505,6 +488,15 @@ namespace ImageResizer.Properties
|
||||
|
||||
public static string SettingsPath { get => _settingsPath; set => _settingsPath = value; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the UI DispatcherQueue for cross-thread property change notifications.
|
||||
/// Must be called from the UI thread during app startup.
|
||||
/// </summary>
|
||||
public static void InitializeDispatcher()
|
||||
{
|
||||
_uiDispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
private void NotifyPropertyChanged([System.Runtime.CompilerServices.CallerMemberName] string propertyName = "")
|
||||
@@ -517,11 +509,9 @@ namespace ImageResizer.Properties
|
||||
_jsonMutex.WaitOne();
|
||||
string jsonData = JsonSerializer.Serialize(new SettingsWrapper() { Properties = this }, _jsonSerializerOptions);
|
||||
|
||||
// Create directory if it doesn't exist
|
||||
IFileInfo file = _fileSystem.FileInfo.New(SettingsPath);
|
||||
file.Directory.Create();
|
||||
|
||||
// write string to file
|
||||
_fileSystem.File.WriteAllText(SettingsPath, jsonData);
|
||||
_jsonMutex.ReleaseMutex();
|
||||
}
|
||||
@@ -554,13 +544,22 @@ namespace ImageResizer.Properties
|
||||
{
|
||||
}
|
||||
|
||||
if (App.Current?.Dispatcher != null)
|
||||
// Use cached UI DispatcherQueue for cross-thread safety
|
||||
// If we're on the UI thread, execute directly; otherwise dispatch to UI thread
|
||||
var currentDispatcher = DispatcherQueue.GetForCurrentThread();
|
||||
if (currentDispatcher != null)
|
||||
{
|
||||
// Needs to be called on the App UI thread as the properties are bound to the UI.
|
||||
App.Current.Dispatcher.Invoke(() => ReloadCore(jsonSettings));
|
||||
// Already on UI thread, execute directly
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
else if (_uiDispatcherQueue != null)
|
||||
{
|
||||
// On background thread, dispatch to UI thread
|
||||
_uiDispatcherQueue.TryEnqueue(() => ReloadCore(jsonSettings));
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: no dispatcher available (should not happen in normal operation)
|
||||
ReloadCore(jsonSettings);
|
||||
}
|
||||
|
||||
@@ -580,20 +579,16 @@ namespace ImageResizer.Properties
|
||||
KeepDateModified = jsonSettings.KeepDateModified;
|
||||
FallbackEncoder = jsonSettings.FallbackEncoder;
|
||||
CustomSize = jsonSettings.CustomSize;
|
||||
AiSize = jsonSettings.AiSize ?? new AiSize(InputViewModel.DefaultAiScale);
|
||||
AiSize = jsonSettings.AiSize ?? new AiSize(2);
|
||||
SelectedSizeIndex = jsonSettings.SelectedSizeIndex;
|
||||
|
||||
if (jsonSettings.Sizes.Count > 0)
|
||||
{
|
||||
Sizes.Clear();
|
||||
Sizes.AddRange(jsonSettings.Sizes);
|
||||
|
||||
// Ensure Ids are unique and handle missing Ids
|
||||
IdRecoveryHelper.RecoverInvalidIds(Sizes);
|
||||
}
|
||||
|
||||
// Validate SelectedSizeIndex after Sizes collection has been updated
|
||||
// This handles cross-device migration (e.g., ARM64 -> non-ARM64)
|
||||
ValidateSelectedSizeIndex();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,64 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<root>
|
||||
<!--
|
||||
Microsoft ResX Schema
|
||||
|
||||
Version 2.0
|
||||
|
||||
The primary goals of this format is to allow a simple XML format
|
||||
that is mostly human readable. The generation and parsing of the
|
||||
various data types are done through the TypeConverter classes
|
||||
associated with the data types.
|
||||
|
||||
Example:
|
||||
|
||||
... ado.net/XML headers & schema ...
|
||||
<resheader name="resmimetype">text/microsoft-resx</resheader>
|
||||
<resheader name="version">2.0</resheader>
|
||||
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
|
||||
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
|
||||
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
|
||||
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
|
||||
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
|
||||
<value>[base64 mime encoded serialized .NET Framework object]</value>
|
||||
</data>
|
||||
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
|
||||
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
|
||||
<comment>This is a comment</comment>
|
||||
</data>
|
||||
|
||||
There are any number of "resheader" rows that contain simple
|
||||
name/value pairs.
|
||||
|
||||
Each data row contains a name, and value. The row also contains a
|
||||
type or mimetype. Type corresponds to a .NET class that support
|
||||
text/value conversion through the TypeConverter architecture.
|
||||
Classes that don't support this are serialized and stored with the
|
||||
mimetype set.
|
||||
|
||||
The mimetype is used for serialized objects, and tells the
|
||||
ResXResourceReader how to depersist the object. This is currently not
|
||||
extensible. For a given mimetype the value must be set accordingly:
|
||||
|
||||
Note - application/x-microsoft.net.object.binary.base64 is the format
|
||||
that the ResXResourceWriter will generate, however the reader can
|
||||
read any of the formats listed below.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.binary.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.soap.base64
|
||||
value : The object must be serialized with
|
||||
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
|
||||
: and then encoded with base64 encoding.
|
||||
|
||||
mimetype: application/x-microsoft.net.object.bytearray.base64
|
||||
value : The object must be serialized into a byte array
|
||||
: using a System.ComponentModel.TypeConverter
|
||||
: and then encoded with base64 encoding.
|
||||
-->
|
||||
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
|
||||
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
|
||||
<xsd:element name="root" msdata:IsDataSet="true">
|
||||
@@ -117,19 +58,32 @@
|
||||
<resheader name="writer">
|
||||
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
|
||||
</resheader>
|
||||
<data name="AllFilesFilter" xml:space="preserve">
|
||||
<value>All Files</value>
|
||||
|
||||
<!-- General strings -->
|
||||
<data name="ImageResizer" xml:space="preserve">
|
||||
<value>Image Resizer</value>
|
||||
<comment>Product name, do not loc</comment>
|
||||
</data>
|
||||
<data name="Cancel" xml:space="preserve">
|
||||
<data name="Cancel.Text" xml:space="preserve">
|
||||
<value>Cancel</value>
|
||||
</data>
|
||||
<data name="Height" xml:space="preserve">
|
||||
<value>Height</value>
|
||||
</data>
|
||||
<data name="ImageResizer" xml:space="preserve">
|
||||
<value>Image Resizer</value>
|
||||
<comment>Product name, do not loc</comment>
|
||||
<data name="Width" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
</data>
|
||||
<data name="Unit" xml:space="preserve">
|
||||
<value>Unit</value>
|
||||
</data>
|
||||
<data name="AllFilesFilter" xml:space="preserve">
|
||||
<value>All Files</value>
|
||||
</data>
|
||||
<data name="PictureFilter" xml:space="preserve">
|
||||
<value>All Picture Files</value>
|
||||
</data>
|
||||
|
||||
<!-- Input page -->
|
||||
<data name="Input_Auto" xml:space="preserve">
|
||||
<value>(auto)</value>
|
||||
</data>
|
||||
@@ -139,123 +93,26 @@
|
||||
<data name="Input_Custom" xml:space="preserve">
|
||||
<value>Custom</value>
|
||||
</data>
|
||||
<data name="Input_IgnoreOrientation" xml:space="preserve">
|
||||
<value>Ignore the _orientation of pictures</value>
|
||||
<data name="Input_IgnoreOrientation.Text" xml:space="preserve">
|
||||
<value>Ignore the orientation of pictures</value>
|
||||
</data>
|
||||
<data name="Input_GifWarning" xml:space="preserve">
|
||||
<data name="Input_GifWarning.Text" xml:space="preserve">
|
||||
<value>Gif files with animations may not be correctly resized.</value>
|
||||
</data>
|
||||
<data name="Input_Replace" xml:space="preserve">
|
||||
<value>Ov_erwrite files</value>
|
||||
<data name="Input_Replace.Text" xml:space="preserve">
|
||||
<value>Overwrite files</value>
|
||||
</data>
|
||||
<data name="Input_Resize" xml:space="preserve">
|
||||
<data name="Input_Resize.Text" xml:space="preserve">
|
||||
<value>Resize</value>
|
||||
</data>
|
||||
<data name="Input_ShrinkOnly" xml:space="preserve">
|
||||
<value>_Make pictures smaller but not larger</value>
|
||||
<data name="Input_ShrinkOnly.Text" xml:space="preserve">
|
||||
<value>Make pictures smaller but not larger</value>
|
||||
</data>
|
||||
<data name="Large" xml:space="preserve">
|
||||
<value>Large</value>
|
||||
<data name="Input_RemoveMetadata.Text" xml:space="preserve">
|
||||
<value>Remove metadata that doesn't affect rendering</value>
|
||||
</data>
|
||||
<data name="Medium" xml:space="preserve">
|
||||
<value>Medium</value>
|
||||
</data>
|
||||
<data name="OK" xml:space="preserve">
|
||||
<value>OK</value>
|
||||
</data>
|
||||
<data name="OK_Tooltip" xml:space="preserve">
|
||||
<value>Apply settings</value>
|
||||
</data>
|
||||
<data name="Phone" xml:space="preserve">
|
||||
<value>Phone</value>
|
||||
</data>
|
||||
<data name="PictureFilter" xml:space="preserve">
|
||||
<value>All Picture Files</value>
|
||||
</data>
|
||||
<data name="PngInterlaceOption_Default" xml:space="preserve">
|
||||
<value>(Default)</value>
|
||||
</data>
|
||||
<data name="PngInterlaceOption_Off" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PngInterlaceOption_On" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
<data name="Progress_MainInstruction" xml:space="preserve">
|
||||
<value>Resizing your pictures...</value>
|
||||
</data>
|
||||
<data name="Progress_Stop" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_HourMinute" xml:space="preserve">
|
||||
<value>About {0} hour, {1} minute remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_HourMinutes" xml:space="preserve">
|
||||
<value>About {0} hour, {1} minutes remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_HoursMinute" xml:space="preserve">
|
||||
<value>About {0} hours, {1} minute remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_HoursMinutes" xml:space="preserve">
|
||||
<value>About {0} hours, {1} minutes remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_MinuteSecond" xml:space="preserve">
|
||||
<value>About {1} minute, {2} second remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_MinuteSeconds" xml:space="preserve">
|
||||
<value>About {1} minute, {2} seconds remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_MinutesSecond" xml:space="preserve">
|
||||
<value>About {1} minutes, {2} second remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_MinutesSeconds" xml:space="preserve">
|
||||
<value>About {1} minutes, {2} seconds remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_Second" xml:space="preserve">
|
||||
<value>About {2} second remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining_Seconds" xml:space="preserve">
|
||||
<value>About {2} seconds remaining.</value>
|
||||
<comment>"About" = Approximately, not "on the subject of"</comment>
|
||||
</data>
|
||||
<data name="ResizeFit_Fill" xml:space="preserve">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fill_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>fills</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fit" xml:space="preserve">
|
||||
<value>Fit</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fit_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>fits within</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Stretch" xml:space="preserve">
|
||||
<value>Stretch</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Stretch_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>stretches to</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Centimeter" xml:space="preserve">
|
||||
<value>Centimeters</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Inch" xml:space="preserve">
|
||||
<value>Inches</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Percent" xml:space="preserve">
|
||||
<value>Percent</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Pixel" xml:space="preserve">
|
||||
<value>Pixels</value>
|
||||
<data name="Image_Sizes" xml:space="preserve">
|
||||
<value>Image sizes</value>
|
||||
</data>
|
||||
<data name="Resize_Tooltip" xml:space="preserve">
|
||||
<value>Resize pictures</value>
|
||||
@@ -263,42 +120,17 @@
|
||||
<data name="Resize_Type" xml:space="preserve">
|
||||
<value>Resize type</value>
|
||||
</data>
|
||||
<data name="Results_Close" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
</data>
|
||||
<data name="Results_MainInstruction" xml:space="preserve">
|
||||
<value>Can't resize the following pictures</value>
|
||||
</data>
|
||||
<data name="Small" xml:space="preserve">
|
||||
<value>Small</value>
|
||||
</data>
|
||||
<data name="Unit" xml:space="preserve">
|
||||
<value>Unit</value>
|
||||
</data>
|
||||
<data name="ValueMustBeBetween" xml:space="preserve">
|
||||
<value>Value must be between '{0}' and '{1}'.</value>
|
||||
</data>
|
||||
<data name="Version" xml:space="preserve">
|
||||
<value>Version</value>
|
||||
</data>
|
||||
<data name="Width" xml:space="preserve">
|
||||
<value>Width</value>
|
||||
</data>
|
||||
<data name="Open_settings" xml:space="preserve">
|
||||
<data name="Open_settings.Text" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="Input_RemoveMetadata" xml:space="preserve">
|
||||
<value>Remove meta_data that doesn't affect rendering</value>
|
||||
</data>
|
||||
<data name="Image_Sizes" xml:space="preserve">
|
||||
<value>Image sizes</value>
|
||||
</data>
|
||||
<data name="Input_ShrinkOnly.Content" xml:space="preserve">
|
||||
<value>_Make pictures smaller but not larger</value>
|
||||
</data>
|
||||
|
||||
<!-- AI Super Resolution -->
|
||||
<data name="Input_AiSuperResolution" xml:space="preserve">
|
||||
<value>Super resolution</value>
|
||||
</data>
|
||||
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
|
||||
<value>Upscale images using on-device AI</value>
|
||||
</data>
|
||||
<data name="Input_AiUnknownSize" xml:space="preserve">
|
||||
<value>Unavailable</value>
|
||||
</data>
|
||||
@@ -308,10 +140,10 @@
|
||||
<data name="Input_AiScaleLabel" xml:space="preserve">
|
||||
<value>Scale</value>
|
||||
</data>
|
||||
<data name="Input_AiCurrentLabel" xml:space="preserve">
|
||||
<data name="Input_AiCurrentLabel.Text" xml:space="preserve">
|
||||
<value>Current:</value>
|
||||
</data>
|
||||
<data name="Input_AiNewLabel" xml:space="preserve">
|
||||
<data name="Input_AiNewLabel.Text" xml:space="preserve">
|
||||
<value>New:</value>
|
||||
</data>
|
||||
<data name="Input_AiModelChecking" xml:space="preserve">
|
||||
@@ -332,7 +164,7 @@
|
||||
<data name="Input_AiModelDownloadFailed" xml:space="preserve">
|
||||
<value>Failed to download AI model. Please try again.</value>
|
||||
</data>
|
||||
<data name="Input_AiModelDownloadButton" xml:space="preserve">
|
||||
<data name="Input_AiModelDownloadButton.Text" xml:space="preserve">
|
||||
<value>Download</value>
|
||||
</data>
|
||||
<data name="Error_AiProcessingFailed" xml:space="preserve">
|
||||
@@ -344,8 +176,84 @@
|
||||
<data name="Error_AiScalingFailed" xml:space="preserve">
|
||||
<value>AI scaling operation failed.</value>
|
||||
</data>
|
||||
<data name="Input_AiSuperResolutionDescription" xml:space="preserve">
|
||||
<value>Upscale images using on-device AI</value>
|
||||
|
||||
<!-- Progress page -->
|
||||
<data name="Progress_MainInstruction.Text" xml:space="preserve">
|
||||
<value>Resizing your pictures...</value>
|
||||
</data>
|
||||
<data name="Progress_Stop.Text" xml:space="preserve">
|
||||
<value>Stop</value>
|
||||
</data>
|
||||
<data name="Progress_TimeRemaining" xml:space="preserve">
|
||||
<value>About {0} remaining.</value>
|
||||
<comment>"About" = Approximately</comment>
|
||||
</data>
|
||||
|
||||
<!-- Results page -->
|
||||
<data name="Results_MainInstruction.Text" xml:space="preserve">
|
||||
<value>Can't resize the following pictures</value>
|
||||
</data>
|
||||
<data name="Results_Close.Text" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
</data>
|
||||
|
||||
<!-- Size names -->
|
||||
<data name="Small" xml:space="preserve">
|
||||
<value>Small</value>
|
||||
</data>
|
||||
<data name="Medium" xml:space="preserve">
|
||||
<value>Medium</value>
|
||||
</data>
|
||||
<data name="Large" xml:space="preserve">
|
||||
<value>Large</value>
|
||||
</data>
|
||||
<data name="Phone" xml:space="preserve">
|
||||
<value>Phone</value>
|
||||
</data>
|
||||
|
||||
<!-- Resize Fit options -->
|
||||
<data name="ResizeFit_Fill" xml:space="preserve">
|
||||
<value>Fill</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fill_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>fills</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fit" xml:space="preserve">
|
||||
<value>Fit</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Fit_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>fits within</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Stretch" xml:space="preserve">
|
||||
<value>Stretch</value>
|
||||
</data>
|
||||
<data name="ResizeFit_Stretch_ThirdPersonSingular" xml:space="preserve">
|
||||
<value>stretches to</value>
|
||||
</data>
|
||||
|
||||
<!-- Resize Unit options -->
|
||||
<data name="ResizeUnit_Centimeter" xml:space="preserve">
|
||||
<value>Centimeters</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Inch" xml:space="preserve">
|
||||
<value>Inches</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Percent" xml:space="preserve">
|
||||
<value>Percent</value>
|
||||
</data>
|
||||
<data name="ResizeUnit_Pixel" xml:space="preserve">
|
||||
<value>Pixels</value>
|
||||
</data>
|
||||
|
||||
<!-- PNG Interlace options -->
|
||||
<data name="PngInterlaceOption_Default" xml:space="preserve">
|
||||
<value>(Default)</value>
|
||||
</data>
|
||||
<data name="PngInterlaceOption_Off" xml:space="preserve">
|
||||
<value>Off</value>
|
||||
</data>
|
||||
<data name="PngInterlaceOption_On" xml:space="preserve">
|
||||
<value>On</value>
|
||||
</data>
|
||||
|
||||
<!-- CLI Processing messages -->
|
||||
@@ -499,4 +407,7 @@
|
||||
<data name="CLI_Option_Width" xml:space="preserve">
|
||||
<value>Set width</value>
|
||||
</data>
|
||||
</root>
|
||||
<data name="ValueMustBeBetween" xml:space="preserve">
|
||||
<value>Value must be between '{0}' and '{1}'.</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -1,4 +1,4 @@
|
||||
#pragma warning disable IDE0073
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media.Imaging;
|
||||
|
||||
using ImageResizer.Helpers;
|
||||
using ImageResizer.Models;
|
||||
using ImageResizer.Properties;
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public class AdvancedViewModel : Observable
|
||||
{
|
||||
private static Dictionary<Guid, string> InitEncoderMap()
|
||||
{
|
||||
var bmpCodec = new BmpBitmapEncoder().CodecInfo;
|
||||
var gifCodec = new GifBitmapEncoder().CodecInfo;
|
||||
var jpegCodec = new JpegBitmapEncoder().CodecInfo;
|
||||
var pngCodec = new PngBitmapEncoder().CodecInfo;
|
||||
var tiffCodec = new TiffBitmapEncoder().CodecInfo;
|
||||
var wmpCodec = new WmpBitmapEncoder().CodecInfo;
|
||||
|
||||
return new Dictionary<Guid, string>
|
||||
{
|
||||
[bmpCodec.ContainerFormat] = bmpCodec.FriendlyName,
|
||||
[gifCodec.ContainerFormat] = gifCodec.FriendlyName,
|
||||
[jpegCodec.ContainerFormat] = jpegCodec.FriendlyName,
|
||||
[pngCodec.ContainerFormat] = pngCodec.FriendlyName,
|
||||
[tiffCodec.ContainerFormat] = tiffCodec.FriendlyName,
|
||||
[wmpCodec.ContainerFormat] = wmpCodec.FriendlyName,
|
||||
};
|
||||
}
|
||||
|
||||
public AdvancedViewModel(Settings settings)
|
||||
{
|
||||
RemoveSizeCommand = new RelayCommand<ResizeSize>(RemoveSize);
|
||||
AddSizeCommand = new RelayCommand(AddSize);
|
||||
Settings = settings;
|
||||
}
|
||||
|
||||
public static IDictionary<Guid, string> EncoderMap { get; } = InitEncoderMap();
|
||||
|
||||
public Settings Settings { get; }
|
||||
|
||||
public static string Version
|
||||
=> typeof(AdvancedViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()
|
||||
?.InformationalVersion;
|
||||
|
||||
public static IEnumerable<Guid> Encoders => EncoderMap.Keys;
|
||||
|
||||
public ICommand RemoveSizeCommand { get; }
|
||||
|
||||
public ICommand AddSizeCommand { get; }
|
||||
|
||||
public void RemoveSize(ResizeSize size)
|
||||
=> Settings.Sizes.Remove(size);
|
||||
|
||||
public void AddSize()
|
||||
=> Settings.Sizes.Add(new ResizeSize());
|
||||
|
||||
public void Close(bool accepted)
|
||||
{
|
||||
if (accepted)
|
||||
{
|
||||
Settings.Save();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedSizeIndex = Settings.SelectedSizeIndex;
|
||||
var shrinkOnly = Settings.ShrinkOnly;
|
||||
var replace = Settings.Replace;
|
||||
var ignoreOrientation = Settings.IgnoreOrientation;
|
||||
|
||||
Settings.Reload();
|
||||
Settings.SelectedSizeIndex = selectedSizeIndex;
|
||||
Settings.ShrinkOnly = shrinkOnly;
|
||||
Settings.Replace = replace;
|
||||
Settings.IgnoreOrientation = ignoreOrientation;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
#pragma warning disable IDE0073
|
||||
// Copyright (c) Brice Lambson
|
||||
// The Brice Lambson licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information. Code forked from Brice Lambson's https://github.com/bricelam/ImageResizer/
|
||||
#pragma warning restore IDE0073
|
||||
|
||||
namespace ImageResizer.ViewModels
|
||||
{
|
||||
public interface ITabViewModel
|
||||
{
|
||||
string Header { get; }
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user