diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4a92c4a..6ac8635 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,10 +25,10 @@ jobs:
shell: pwsh
run: .\scripts\build.ps1 -Configuration Release
- - name: Upload portable executable
+ - name: Upload Legacy portable executable
uses: actions/upload-artifact@v4
with:
- name: CryptoProCleanup-win32-${{ github.sha }}
- path: build/Release/CryptoProCleanup.exe
+ name: CryptoProCleanup-Legacy-x86-${{ github.sha }}
+ path: build/Win32/Release/CryptoProCleanupLegacy.exe
if-no-files-found: error
retention-days: 14
diff --git a/.gitignore b/.gitignore
index ecccf42..0da977f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,11 @@ build/
dist/
Debug/
Release/
+packages/
+artifacts/
+.referer/
+Generated Files/
+obj/
*.obj
*.pdb
*.idb
diff --git a/App.xaml b/App.xaml
new file mode 100644
index 0000000..eae829b
--- /dev/null
+++ b/App.xaml
@@ -0,0 +1,384 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 4
+ 8
+ 12
+ 16
+ 20
+ 24
+ 32
+ 20
+ 16
+ 16,14
+ 14
+ 20,12,20,4
+ 18,16,18,12
+ 16,12
+ 16,12
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/App.xaml.cpp b/App.xaml.cpp
new file mode 100644
index 0000000..01ad3bb
--- /dev/null
+++ b/App.xaml.cpp
@@ -0,0 +1,57 @@
+#include "pch.h"
+#include "App.xaml.h"
+#include "MainWindow.xaml.h"
+#include "src/cleanup.hpp"
+
+#include
+#include
+
+namespace winrt::CryptoProCleanupModern::implementation
+{
+ App::App()
+ {
+ }
+
+ void App::OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&)
+ {
+ int argc = 0;
+ wchar_t** argv = CommandLineToArgvW(GetCommandLineW(), &argc);
+ const cpc::CommandLineOptions options = cpc::ParseCommandLine(argc, argv);
+ if (argv) LocalFree(argv);
+
+ if (options.showHelp)
+ {
+ const std::wstring heading = std::wstring(L"CryptoPro Cleanup Utility ") + cpc::kVersion + L"\r\n\r\n";
+ MessageBoxW(nullptr,
+ (heading +
+ L"--scan Safe scan only\r\n"
+ L"--offline-scan Safe disconnected-Windows scan\r\n"
+ L"--report Report path\r\n"
+ L"--lang ru|en Interface/report language\r\n"
+ L"--resume Internal restart continuation").c_str(),
+ L"CryptoPro Cleanup Utility", MB_OK | MB_ICONINFORMATION);
+ ExitProcess(0);
+ }
+ if (!options.offlineWindowsPath.empty()) ExitProcess(cpc::RunOfflineScanCommand(options));
+ if (options.scanOnly) ExitProcess(cpc::RunScanCommand(options));
+ // A resumed cleanup is an exact residual pass over the protected state.
+ // It must never fall through to the normal uninstaller confirmation UI.
+ if (!options.resumeToken.empty()) ExitProcess(cpc::RunResumeCommand(options.resumeToken, true));
+
+ try
+ {
+ auto mainWindow = winrt::make_self();
+ mainWindow->InitializeSession(options.language, {}, options.languageExplicit);
+ window = *mainWindow;
+ window.Activate();
+ }
+ catch (winrt::hresult_error const& error)
+ {
+ const std::wstring detail = std::wstring(L"Modern UI initialization failed (0x") +
+ [&]() { std::wostringstream value; value << std::hex << static_cast(error.code().value); return value.str(); }() +
+ L"): " + std::wstring(error.message().c_str());
+ OutputDebugStringW((detail + L"\r\n").c_str());
+ MessageBoxW(nullptr, detail.c_str(), L"CryptoPro Cleanup Utility", MB_OK | MB_ICONERROR);
+ }
+ }
+}
diff --git a/App.xaml.h b/App.xaml.h
new file mode 100644
index 0000000..24861b6
--- /dev/null
+++ b/App.xaml.h
@@ -0,0 +1,16 @@
+#pragma once
+
+#include "App.xaml.g.h"
+#include "pch.h"
+
+namespace winrt::CryptoProCleanupModern::implementation
+{
+ struct App : AppT
+ {
+ App();
+ void OnLaunched(Microsoft::UI::Xaml::LaunchActivatedEventArgs const&);
+
+ private:
+ Microsoft::UI::Xaml::Window window{nullptr};
+ };
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dba9d89..8b9f153 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,81 @@
All notable project changes are documented here. The project follows semantic versioning after the first generally available release.
+## [0.5.3-rc1] - 2026-08-21
+
+### Changed
+
+- Introduced shared spacing tokens and semantic content, statistic, table, callout, and navigation card styles across Modern; product and offline-result rows now use dedicated table insets instead of accumulated per-card padding.
+- Restored the studio accent palette and fixed button label inheritance in Dark, Light, System, and High Contrast resources.
+- Made the page header, backup selector, certificate toolbar, offline selector, Settings rows, Reports actions, About actions, and dialogs adapt to narrow windows without truncating known RU/EN labels.
+- Added a dedicated Legacy low-resolution layout: the minimum window is now 640×480, all six navigation entries and page actions remain visible at 800×600/1024×768, and the effective 768×528 work-area layout compacts tables, settings, reports, About links, and Offline Windows controls without overlapping buttons.
+- Replaced generic completion text with operation-specific semantic states and preserved the originating operation when asynchronous failures are reported.
+- Split backup validation into side-effect-free inspection and a debounced background write probe. The UI now shows free and required space; advanced offline cleanup estimates complete hive and verified quarantine-copy size with a safety margin.
+- Kept invalid offline diagnostics accessible, invalidated product-set confirmation on every selection change, corrected live/offline license-dialog context, and made cleanup-result wording explicitly conservative.
+- Reports now use actual stage outcomes, compact safe-scan summaries, an optional technical log, complete session-file discovery, and a warning before confidential files are opened.
+- Theme refresh updates runtime rows and badges in place without rebuilding their models, selections, or handlers. Dead retained execution state was removed.
+- Fixed stale Light-theme brushes on the compact Modern navigation rail: Dark now refreshes every button background, selected state, border, and icon foreground after startup and every theme change.
+
+### Safety and validation
+
+- Added static layout-resource checks and expanded safe native tests for no-side-effect backup inspection, path ancestry, write-probe cleanup, offline space estimation, and `NotBuilt`/`Ready`/`Stale` plan state.
+- Modern UI automation now samples compact-navigation pixels in Dark theme and fails if a button keeps a light background or its symbol loses contrast.
+- Local Release Win32 Legacy and x64 Modern builds, native x86/x64 unit tests, theme/layout checks, and non-destructive UI automation passed. Modern screenshots cover 1024×760 and 1280×720; Legacy screenshots cover 800×600, 1024×768, and a simulated 768×528 work area at the development system's 100% DPI. OS-level High Contrast and 125–200% DPI sessions were not executed.
+- No destructive CryptoPro cleanup, offline-disk write, real RunOnce registration, reboot, or live resume execution was performed during 0.5.3-rc1 validation.
+
+## [0.5.2-dev] - 2026-08-21
+
+### Changed
+
+- Replaced the forced-dark Modern theme with matching Dark, Light, and High Contrast resource dictionaries; System follows Windows at runtime, including dialogs and title bar.
+- Added generation-bound asynchronous operations, semantic status preservation, complete plan-input revisions, selected-only plan counts, detailed target categories, and stricter conflict gating.
+- Bound every offline result to its scanned path, added explicit product-set confirmation, richer certificate selection/diagnostics, another-volume backup validation, and correct offline redaction context.
+- Hardened FORCE/resume authorization, transactional helper deployment, runner identity/hash validation, bounded retries, and retained failed state for manual inspection.
+- Added real certificate-date sorting and status filters, filtered-only bulk selection, keyboard/UI Automation rows, session-specific Reports actions/timeline, emergency redacted logs, and executable signature/SHA-256 inspection in About.
+- Window placement is monitor-validated and bounded; reduced motion now also honors the Windows animation setting.
+
+### Safety and validation
+
+- Added a theme-resource consistency checker to every local build and expanded non-destructive unit tests for operation generations, plan revisions, redaction boundaries, backup validation, certificate sorting/selection, FORCE authorization, and tampered resume helpers.
+- No destructive cleanup, real RunOnce registration, reboot, or live resume execution was performed during 0.5.2-dev development validation.
+
+## [0.5.1-dev] - 2026-08-21
+
+### Changed
+
+- Stabilized the Modern x64 operation model with one explicit state gate, scan/selection plan revisions, blocked overlapping commands, and guarded window closing.
+- Restored the full uninstall-first workflow in Modern: mandatory backup, registered MSI/EXE pass, exact `FORCE` confirmation after uninstaller failure, verified residual pass, verification, redacted logs, and structured reports.
+- Added a compact native x64 `CryptoProCleanupResume.exe`. Its protected versioned state contains only the preverified residual plan; the helper never reruns registered uninstallers and never restarts Windows.
+- Expanded disconnected-Windows selection, diagnostics, certificate choices, exact `OFFLINE` confirmation, recovery-backed result logging, and post-cleanup rescan.
+- Added persisted Modern settings, dark title-bar integration, certificate validity/sort/bulk filters, a searchable categorized plan inspector, and a structured Reports page.
+
+### Safety and validation
+
+- Added isolated tests for operation gating, plan revisions, exact confirmation phrases, execution-result merging, sensitive log redaction, and resume state/runner/load/cleanup without RunOnce or HKLM writes.
+- Local Release Win32/x64 builds and non-destructive tests are required by the package script. No destructive CryptoPro, offline-disk, RunOnce, or reboot test is performed on the development machine.
+
+## [0.5.0-dev] - 2026-08-20
+
+### Added
+
+- Separate native C++/WinUI 3 x64 application for Windows 10/11, closely following the supplied dark Fluent mockups; the Windows 7-compatible x86 Win32 edition remains available as Legacy.
+- Sidebar navigation, overview/status cards, dedicated certificate, Offline Windows, reports, settings, and about pages, typed destructive confirmations, and a read-only cleanup-plan inspector.
+- Responsive layout that collapses the sidebar and stacks cards/details as the window narrows, while retaining native DPI scaling and keyboard-accessible controls.
+- Confidential full-license dialog with copy support for both the running and offline Windows scans. Full values remain excluded from JSON and the redacted log.
+- Self-contained Windows App SDK 2.4 runtime packaging, including the official runtime resource index extracted reproducibly from Microsoft's NuGet MSIX.
+
+### Fixed
+
+- Prevented early WinUI `SelectionChanged` events from accessing not-yet-connected XAML controls during startup.
+- RU/EN switching now refreshes the footer status immediately instead of retaining the language used by the completed scan.
+- XAML resources stored in project subdirectories no longer produce mismatched unpackaged `ms-appx` paths.
+
+### Validation
+
+- Modern Release x64 starts successfully in both framework-dependent diagnostic and self-contained portable layouts on Windows 11 x64.
+- Automated UI Automation checks confirmed immediate RU-to-EN status translation and opening of the confidential license dialog without emitting license values to test output.
+- Wide and 1024×760 layouts were launched and visually checked; no destructive cleanup operation was executed during development.
+
## [0.4.0-rc4] - 2026-08-20
### Added
diff --git a/CryptoProCleanup.sln b/CryptoProCleanup.sln
index 9d6e913..972fdab 100644
--- a/CryptoProCleanup.sln
+++ b/CryptoProCleanup.sln
@@ -7,20 +7,44 @@ Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "CryptoProCleanup", "CryptoP
EndProject
Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "CryptoProCleanupTests", "CryptoProCleanupTests.vcxproj", "{12A1286B-DF22-4CA5-98F8-ACAB826E64AE}"
EndProject
+Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "CryptoProCleanupModern", "CryptoProCleanupModern.vcxproj", "{75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}"
+EndProject
+Project("{BC8A1FFA-BEE3-4634-8014-F334798102B3}") = "CryptoProCleanupResume", "CryptoProCleanupResume.vcxproj", "{A9B645B3-5600-4EA8-8772-1803483B77B2}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Win32 = Debug|Win32
+ Debug|x64 = Debug|x64
Release|Win32 = Release|Win32
+ Release|x64 = Release|x64
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{4F4EAB45-CC51-451B-8298-7904B312DF4A}.Debug|Win32.ActiveCfg = Debug|Win32
{4F4EAB45-CC51-451B-8298-7904B312DF4A}.Debug|Win32.Build.0 = Debug|Win32
+ {4F4EAB45-CC51-451B-8298-7904B312DF4A}.Debug|x64.ActiveCfg = Debug|x64
{4F4EAB45-CC51-451B-8298-7904B312DF4A}.Release|Win32.ActiveCfg = Release|Win32
{4F4EAB45-CC51-451B-8298-7904B312DF4A}.Release|Win32.Build.0 = Release|Win32
+ {4F4EAB45-CC51-451B-8298-7904B312DF4A}.Release|x64.ActiveCfg = Release|x64
{12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Debug|Win32.ActiveCfg = Debug|Win32
{12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Debug|Win32.Build.0 = Debug|Win32
+ {12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Debug|x64.ActiveCfg = Debug|x64
+ {12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Debug|x64.Build.0 = Debug|x64
{12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Release|Win32.ActiveCfg = Release|Win32
{12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Release|Win32.Build.0 = Release|Win32
+ {12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Release|x64.ActiveCfg = Release|x64
+ {12A1286B-DF22-4CA5-98F8-ACAB826E64AE}.Release|x64.Build.0 = Release|x64
+ {75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}.Debug|Win32.ActiveCfg = Debug|x64
+ {75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}.Debug|x64.ActiveCfg = Debug|x64
+ {75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}.Debug|x64.Build.0 = Debug|x64
+ {75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}.Release|Win32.ActiveCfg = Release|x64
+ {75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}.Release|x64.ActiveCfg = Release|x64
+ {75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}.Release|x64.Build.0 = Release|x64
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}.Debug|Win32.ActiveCfg = Debug|x64
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}.Debug|x64.ActiveCfg = Debug|x64
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}.Debug|x64.Build.0 = Debug|x64
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}.Release|Win32.ActiveCfg = Release|x64
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}.Release|x64.ActiveCfg = Release|x64
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/CryptoProCleanup.vcxproj b/CryptoProCleanup.vcxproj
index 1f97d9b..bd20805 100644
--- a/CryptoProCleanup.vcxproj
+++ b/CryptoProCleanup.vcxproj
@@ -3,6 +3,8 @@
DebugWin32
ReleaseWin32
+ Debugx64
+ Releasex64
17.0
@@ -18,8 +20,16 @@
Applicationfalsev143trueUnicode
+
+ Applicationtruev143Unicode
+
+
+ Applicationfalsev143trueUnicode
+
- $(MSBuildThisFileDirectory)build\$(Configuration)\$(MSBuildThisFileDirectory)build\obj\$(ProjectName)\$(Configuration)\CryptoProCleanup
+ $(MSBuildThisFileDirectory)build\$(Platform)\$(Configuration)\$(MSBuildThisFileDirectory)build\obj\$(Platform)\$(ProjectName)\$(Configuration)\
+ CryptoProCleanupLegacy
+ CryptoProCleanup
Level4trueWIN32;_WINDOWS;UNICODE;_UNICODE;WINVER=0x0601;_WIN32_WINNT=0x0601;NOMINMAX;%(PreprocessorDefinitions)truestdcpp17MultiThreadedDebug/utf-8 %(AdditionalOptions)
WindowstrueRequireAdministratoradvapi32.lib;comctl32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;user32.lib;uxtheme.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:WINDOWS,6.01 %(AdditionalOptions)
@@ -30,6 +40,16 @@
WindowstruetruetrueRequireAdministratoradvapi32.lib;comctl32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;user32.lib;uxtheme.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:WINDOWS,6.01 %(AdditionalOptions)
src\app.manifest
+
+ Level4true_WINDOWS;UNICODE;_UNICODE;WINVER=0x0A00;_WIN32_WINNT=0x0A00;NOMINMAX;CPC_MODERN_UI;%(PreprocessorDefinitions)truestdcpp17MultiThreadedDebug/utf-8 %(AdditionalOptions)
+ WindowstrueRequireAdministratoradvapi32.lib;comctl32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;user32.lib;uxtheme.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:WINDOWS,10.00 %(AdditionalOptions)
+ src\app-modern.manifest
+
+
+ Level4truetruetrueNDEBUG;_WINDOWS;UNICODE;_UNICODE;WINVER=0x0A00;_WIN32_WINNT=0x0A00;NOMINMAX;CPC_MODERN_UI;%(PreprocessorDefinitions)truestdcpp17MultiThreaded/utf-8 %(AdditionalOptions)
+ WindowstruetruetrueRequireAdministratoradvapi32.lib;comctl32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;user32.lib;uxtheme.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:WINDOWS,10.00 %(AdditionalOptions)
+ src\app-modern.manifest
+
@@ -37,7 +57,10 @@
-
+
+
+
+
diff --git a/CryptoProCleanupModern.vcxproj b/CryptoProCleanupModern.vcxproj
new file mode 100644
index 0000000..7bdebc7
--- /dev/null
+++ b/CryptoProCleanupModern.vcxproj
@@ -0,0 +1,154 @@
+
+
+
+ $(MSBuildThisFileDirectory)packages
+
+
+
+
+
+
+
+
+
+ Debugx64
+ Releasex64
+
+
+ 17.0
+ {75C5D9BA-5B7D-4DD7-8E19-2785DBDCD31D}
+ CryptoProCleanupModern
+ CryptoProCleanupModern
+ CryptoProCleanup
+ ru-RU
+ 17.0
+ 10.0.26100.0
+ 10.0.17763.0
+ false
+ Windows Store
+ 10.0
+ true
+ true
+ true
+ true
+ false
+ None
+ true
+ true
+ true
+
+ true
+
+
+
+ Application
+ v143
+ Unicode
+
+
+ true
+ true
+
+
+ false
+ true
+ false
+
+
+
+ $(MSBuildThisFileDirectory)build\$(Platform)\$(Configuration)\Modern\
+ $(MSBuildThisFileDirectory)build\obj\$(Platform)\$(ProjectName)\$(Configuration)\
+
+
+
+
+ Use
+ pch.h
+ $(IntDir)pch.pch
+ Level4
+ true
+ _WINDOWS;UNICODE;_UNICODE;WINVER=0x0A00;_WIN32_WINNT=0x0A00;NOMINMAX;%(PreprocessorDefinitions)
+ true
+ stdcpp17
+ $(MSBuildThisFileDirectory);$(MSBuildThisFileDirectory)src;$(MSBuildThisFileDirectory)src\modern;$(GeneratedFilesDir);%(AdditionalIncludeDirectories)
+ /utf-8 /bigobj /FS %(AdditionalOptions)
+ true
+
+
+ Windows
+ advapi32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;user32.lib;version.lib;wintrust.lib;%(AdditionalDependencies)
+ /SUBSYSTEM:WINDOWS,10.00 %(AdditionalOptions)
+
+
+
+ _DEBUG;%(PreprocessorDefinitions)MultiThreadedDebug
+ %(IgnoreSpecificDefaultLibraries);libucrtd.lib%(AdditionalOptions) /defaultlib:ucrtd.lib
+
+
+ NDEBUG;%(PreprocessorDefinitions)MultiThreadedNone
+ truetruefalse%(IgnoreSpecificDefaultLibraries);libucrt.lib%(AdditionalOptions) /defaultlib:ucrt.lib
+
+
+
+
+
+
+
+
+
+ Code
+
+
+ NotUsing
+
+ App.xaml
+ MainWindow.xaml
+
+
+ NotUsing
+ NotUsing
+ NotUsing
+ NotUsing
+ Create
+ App.xaml
+ MainWindow.xaml
+
+
+
+
+
+
+
+
+
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ <_RuntimeMsix>$(NugetPackageDirectory)\Microsoft.WindowsAppSDK.Runtime.2.4.0\tools\MSIX\win10-x64\Microsoft.WindowsAppRuntime.2.msix
+ <_RuntimeExtractDir>$(IntDir)WindowsAppRuntimeResources
+
+
+
+
+
+
+
+
+
diff --git a/CryptoProCleanupResume.vcxproj b/CryptoProCleanupResume.vcxproj
new file mode 100644
index 0000000..a3fc5ad
--- /dev/null
+++ b/CryptoProCleanupResume.vcxproj
@@ -0,0 +1,62 @@
+
+
+
+ Debugx64
+ Releasex64
+
+
+ 17.0
+ Win32Proj
+ {A9B645B3-5600-4EA8-8772-1803483B77B2}
+ CryptoProCleanupResume
+ 10.0
+
+
+
+ Application
+ v143
+ Unicode
+
+
+ trueCryptoProCleanupResume
+
+
+ falsetrueCryptoProCleanupResume
+
+
+
+ $(MSBuildThisFileDirectory)build\$(Platform)\$(Configuration)\Resume\
+ $(MSBuildThisFileDirectory)build\obj\$(Platform)\$(ProjectName)\$(Configuration)\
+
+
+
+ Level4true
+ _WINDOWS;UNICODE;_UNICODE;WINVER=0x0601;_WIN32_WINNT=0x0601;NOMINMAX;%(PreprocessorDefinitions)
+ truestdcpp17
+ MultiThreadedDebug
+ MultiThreaded
+ /utf-8 %(AdditionalOptions)
+
+
+ WindowsRequireAdministrator
+ advapi32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;user32.lib;version.lib;wintrust.lib;%(AdditionalDependencies)
+ /SUBSYSTEM:WINDOWS,6.01 %(AdditionalOptions)
+
+ src\resume.manifest
+
+
+ truetrueNone
+ truetruefalse
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CryptoProCleanupTests.vcxproj b/CryptoProCleanupTests.vcxproj
index 41487bd..b0146ee 100644
--- a/CryptoProCleanupTests.vcxproj
+++ b/CryptoProCleanupTests.vcxproj
@@ -3,13 +3,17 @@
DebugWin32
ReleaseWin32
+ Debugx64
+ Releasex64
17.0Win32Proj{12A1286B-DF22-4CA5-98F8-ACAB826E64AE}CryptoProCleanupTests10.0
Applicationtruev143Unicode
Applicationfalsev143trueUnicode
+ Applicationtruev143Unicode
+ Applicationfalsev143trueUnicode
- $(MSBuildThisFileDirectory)build\$(Configuration)\$(MSBuildThisFileDirectory)build\obj\$(ProjectName)\$(Configuration)\
+ $(MSBuildThisFileDirectory)build\$(Platform)\$(Configuration)\$(MSBuildThisFileDirectory)build\obj\$(Platform)\$(ProjectName)\$(Configuration)\
Level4trueWIN32;_CONSOLE;UNICODE;_UNICODE;WINVER=0x0601;_WIN32_WINNT=0x0601;NOMINMAX;CPC_TEST_BUILD;%(PreprocessorDefinitions)stdcpp17MultiThreadedDebugtrue/utf-8 %(AdditionalOptions)
Consoletrueadvapi32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:CONSOLE,6.01 %(AdditionalOptions)
@@ -18,7 +22,16 @@
Level4trueWIN32;NDEBUG;_CONSOLE;UNICODE;_UNICODE;WINVER=0x0601;_WIN32_WINNT=0x0601;NOMINMAX;CPC_TEST_BUILD;%(PreprocessorDefinitions)stdcpp17MultiThreadedtrue/utf-8 %(AdditionalOptions)
Consoletrueadvapi32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:CONSOLE,6.01 %(AdditionalOptions)
+
+ Level4true_CONSOLE;UNICODE;_UNICODE;WINVER=0x0A00;_WIN32_WINNT=0x0A00;NOMINMAX;CPC_TEST_BUILD;CPC_MODERN_UI;%(PreprocessorDefinitions)stdcpp17MultiThreadedDebugtrue/utf-8 %(AdditionalOptions)
+ Consoletrueadvapi32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:CONSOLE,10.00 %(AdditionalOptions)
+
+
+ Level4trueNDEBUG;_CONSOLE;UNICODE;_UNICODE;WINVER=0x0A00;_WIN32_WINNT=0x0A00;NOMINMAX;CPC_TEST_BUILD;CPC_MODERN_UI;%(PreprocessorDefinitions)stdcpp17MultiThreadedtrue/utf-8 %(AdditionalOptions)
+ Consoletrueadvapi32.lib;crypt32.lib;msi.lib;ole32.lib;oleaut32.lib;setupapi.lib;shell32.lib;shlwapi.lib;taskschd.lib;version.lib;wintrust.lib;%(AdditionalDependencies)/SUBSYSTEM:CONSOLE,10.00 %(AdditionalOptions)
+
+
diff --git a/MainWindow.xaml b/MainWindow.xaml
new file mode 100644
index 0000000..1828a03
--- /dev/null
+++ b/MainWindow.xaml
@@ -0,0 +1,329 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/MainWindow.xaml.cpp b/MainWindow.xaml.cpp
new file mode 100644
index 0000000..3675acd
--- /dev/null
+++ b/MainWindow.xaml.cpp
@@ -0,0 +1,3668 @@
+#include "pch.h"
+#include "MainWindow.xaml.h"
+#if __has_include("MainWindow.g.cpp")
+#include "MainWindow.g.cpp"
+#endif
+
+#include
+#include
+#include
+#include
+
+namespace winrt
+{
+ using namespace Windows::ApplicationModel::DataTransfer;
+ using namespace Windows::Foundation;
+ using namespace Windows::Storage::Pickers;
+ using namespace Microsoft::UI::Xaml;
+ using namespace Microsoft::UI::Xaml::Controls;
+ using namespace Microsoft::UI::Xaml::Media;
+}
+
+namespace
+{
+ constexpr int kIconResource = 201;
+ constexpr wchar_t kSettingsKey[] = L"Software\\CodeAlexandrov\\CryptoProCleanup\\ModernWinUI";
+ std::wstring gRuntimeThemeKey = L"Light";
+
+ bool ReadSettingDword(wchar_t const* name, DWORD* value)
+ {
+ DWORD size = sizeof(*value), type = 0;
+ return RegGetValueW(HKEY_CURRENT_USER, kSettingsKey, name, RRF_RT_REG_DWORD,
+ &type, value, &size) == ERROR_SUCCESS;
+ }
+
+ std::wstring ReadSettingString(wchar_t const* name)
+ {
+ DWORD size = 0;
+ if (RegGetValueW(HKEY_CURRENT_USER, kSettingsKey, name, RRF_RT_REG_SZ,
+ nullptr, nullptr, &size) != ERROR_SUCCESS || size < sizeof(wchar_t)) return {};
+ std::wstring value(size / sizeof(wchar_t), L'\0');
+ if (RegGetValueW(HKEY_CURRENT_USER, kSettingsKey, name, RRF_RT_REG_SZ,
+ nullptr, value.data(), &size) != ERROR_SUCCESS) return {};
+ while (!value.empty() && value.back() == L'\0') value.pop_back();
+ return value;
+ }
+
+ void WriteSettingDword(HKEY key, wchar_t const* name, DWORD value)
+ {
+ RegSetValueExW(key, name, 0, REG_DWORD, reinterpret_cast(&value), sizeof(value));
+ }
+
+ void WriteSettingString(HKEY key, wchar_t const* name, std::wstring const& value)
+ {
+ RegSetValueExW(key, name, 0, REG_SZ, reinterpret_cast(value.c_str()),
+ static_cast((value.size() + 1) * sizeof(wchar_t)));
+ }
+
+ winrt::TextBlock Text(std::wstring const& value, double size = 12.0)
+ {
+ winrt::TextBlock text;
+ text.Text(value);
+ text.FontSize(size);
+ text.TextWrapping(winrt::TextWrapping::Wrap);
+ return text;
+ }
+
+ winrt::ColumnDefinition Column(double value, winrt::GridUnitType unit)
+ {
+ winrt::ColumnDefinition column;
+ column.Width(winrt::GridLength{value, unit});
+ return column;
+ }
+
+ winrt::Brush ThemeBrush(wchar_t const* key)
+ {
+ auto resources = winrt::Application::Current().Resources();
+ auto themes = resources.ThemeDictionaries();
+ auto themeKey = winrt::box_value(gRuntimeThemeKey);
+ if (themes.HasKey(themeKey))
+ {
+ auto dictionary = themes.Lookup(themeKey).as();
+ auto resourceKey = winrt::box_value(key);
+ if (dictionary.HasKey(resourceKey)) return dictionary.Lookup(resourceKey).as();
+ }
+ return resources.Lookup(winrt::box_value(key)).as();
+ }
+
+ winrt::Border PaddedDialogContent(winrt::UIElement const& child)
+ {
+ winrt::Border host;
+ auto resources = winrt::Application::Current().Resources();
+ host.Padding(winrt::unbox_value(
+ resources.Lookup(winrt::box_value(L"DialogContentPadding"))));
+ host.Child(child);
+ return host;
+ }
+
+ enum class BadgeTone { Neutral, Success, Warning, Danger };
+
+ void ApplyBadgeTheme(winrt::Border const& badge, BadgeTone tone)
+ {
+ wchar_t const* background = tone == BadgeTone::Success ? L"BadgeSuccessBackgroundBrush" :
+ tone == BadgeTone::Warning ? L"BadgeWarningBackgroundBrush" :
+ tone == BadgeTone::Danger ? L"BadgeDangerBackgroundBrush" : L"BadgeNeutralBackgroundBrush";
+ wchar_t const* foreground = tone == BadgeTone::Success ? L"BadgeSuccessForegroundBrush" :
+ tone == BadgeTone::Warning ? L"BadgeWarningForegroundBrush" :
+ tone == BadgeTone::Danger ? L"BadgeDangerForegroundBrush" : L"BadgeNeutralForegroundBrush";
+ badge.Background(ThemeBrush(background));
+ badge.BorderBrush(ThemeBrush(tone == BadgeTone::Danger ? L"DangerBorderBrush" :
+ tone == BadgeTone::Warning ? L"WarningBorderBrush" : L"SubtleBorderBrush"));
+ if (auto label = badge.Child().try_as()) label.Foreground(ThemeBrush(foreground));
+ }
+
+ winrt::Border Badge(std::wstring const& value, BadgeTone tone)
+ {
+ winrt::Border badge;
+ badge.HorizontalAlignment(winrt::HorizontalAlignment::Left);
+ badge.Padding(winrt::Thickness{9, 5, 9, 5});
+ badge.CornerRadius(winrt::CornerRadius{8});
+ badge.BorderThickness(winrt::Thickness{1});
+ badge.Tag(winrt::box_value(static_cast(tone)));
+ auto label = Text(value, 10);
+ badge.Child(label);
+ ApplyBadgeTheme(badge, tone);
+ return badge;
+ }
+
+ void RefreshBadgeThemes(winrt::DependencyObject const& root)
+ {
+ if (!root) return;
+ if (auto border = root.try_as())
+ {
+ const int32_t tone = winrt::unbox_value_or(border.Tag(), -1);
+ if (tone >= static_cast(BadgeTone::Neutral) &&
+ tone <= static_cast(BadgeTone::Danger))
+ ApplyBadgeTheme(border, static_cast(tone));
+ }
+ const int children = winrt::VisualTreeHelper::GetChildrenCount(root);
+ for (int index = 0; index < children; ++index)
+ RefreshBadgeThemes(winrt::VisualTreeHelper::GetChild(root, index));
+ }
+
+ std::wstring FormatByteCount(unsigned long long bytes)
+ {
+ constexpr unsigned long long mib = 1024ull * 1024;
+ constexpr unsigned long long gib = 1024ull * mib;
+ wchar_t buffer[64]{};
+ if (bytes >= gib) swprintf_s(buffer, L"%.1f GB", static_cast(bytes) / gib);
+ else swprintf_s(buffer, L"%llu MB", (bytes + mib - 1) / mib);
+ return buffer;
+ }
+
+ std::wstring ParentDirectory(std::wstring path)
+ {
+ if (path.empty()) return {};
+ std::vector buffer(path.begin(), path.end());
+ buffer.push_back(L'\0');
+ if (PathRemoveFileSpecW(buffer.data())) return buffer.data();
+ return path;
+ }
+
+ std::wstring NormalizeUiPath(std::wstring path)
+ {
+ path = cpc::Trim(path);
+ std::replace(path.begin(), path.end(), L'/', L'\\');
+ while (path.size() > 3 && path.back() == L'\\') path.pop_back();
+ return cpc::ToLower(path);
+ }
+
+ std::wstring VolumeRootOf(std::wstring const& path)
+ {
+ std::array root{};
+ return GetVolumePathNameW(path.c_str(), root.data(), static_cast(root.size())) ? root.data() : L"";
+ }
+
+}
+
+namespace winrt::CryptoProCleanupModern::implementation
+{
+ MainWindow::MainWindow()
+ {
+ }
+
+ void MainWindow::InitializeSession(cpc::Language language, std::wstring const& resumeToken,
+ bool languageExplicit)
+ {
+ // C++/WinRT completes XAML connection after the authored constructor
+ // returns. Named controls are therefore first accessed here.
+ ConfigureWindow();
+ uiReady_ = true;
+ WindowRoot().ActualThemeChanged([weak = get_weak()](FrameworkElement const&, IInspectable const&)
+ {
+ if (auto self = weak.get())
+ {
+ HIGHCONTRASTW contrast{sizeof(contrast)};
+ const bool highContrast = SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(contrast), &contrast, 0) &&
+ (contrast.dwFlags & HCF_HIGHCONTRASTON) != 0;
+ gRuntimeThemeKey = highContrast ? L"HighContrast" :
+ self->themeMode_ == cpc::ThemeMode::Dark ? L"Dark" :
+ self->themeMode_ == cpc::ThemeMode::Light ? L"Light" :
+ self->WindowRoot().ActualTheme() == ElementTheme::Dark ? L"Dark" : L"Light";
+ self->ApplyTitleBarTheme();
+ self->RefreshThemedVisuals();
+ }
+ });
+ try
+ {
+ accessibilitySettings_ = winrt::Windows::UI::ViewManagement::AccessibilitySettings();
+ accessibilitySettings_.HighContrastChanged([weak = get_weak()](auto const&, auto const&)
+ {
+ if (auto self = weak.get())
+ {
+ self->DispatcherQueue().TryEnqueue([weak]()
+ {
+ if (auto target = weak.get()) target->ApplyTheme();
+ });
+ }
+ });
+ uiSettings_ = winrt::Windows::UI::ViewManagement::UISettings();
+ uiSettings_.AnimationsEnabledChanged([weak = get_weak()](auto const&, auto const&)
+ {
+ if (auto self = weak.get()) self->DispatcherQueue().TryEnqueue([weak]()
+ {
+ if (auto target = weak.get()) target->SetBusy(target->busy_, L"", target->statusPercent_);
+ });
+ });
+ }
+ catch (...) {}
+ language_ = language;
+ LoadSettings(languageExplicit);
+ ApplyTheme();
+ Closed({this, &MainWindow::Window_Closed});
+ certificateFilterTimer_ = DispatcherTimer();
+ certificateFilterTimer_.Interval(std::chrono::milliseconds(250));
+ certificateFilterTimer_.Tick([weak = get_weak()](IInspectable const&, IInspectable const&)
+ {
+ if (auto self = weak.get())
+ {
+ self->certificateFilterTimer_.Stop();
+ self->PopulateCertificates();
+ }
+ });
+ backupValidationTimer_ = DispatcherTimer();
+ backupValidationTimer_.Interval(std::chrono::milliseconds(450));
+ backupValidationTimer_.Tick([weak = get_weak()](IInspectable const&, IInspectable const&)
+ {
+ if (auto self = weak.get())
+ {
+ self->backupValidationTimer_.Stop();
+ self->ProbeBackupPathAsync(self->backupValidationRevision_, self->backupValidationPath_);
+ }
+ });
+ BackupPath().TextChanged([weak = get_weak()](IInspectable const&, TextChangedEventArgs const&)
+ {
+ if (auto self = weak.get())
+ {
+ self->planRevisions_.BackupChanged();
+ self->InvalidatePlan();
+ self->ScheduleBackupValidation();
+ }
+ });
+ OfflinePath().TextChanged([weak = get_weak()](IInspectable const&, TextChangedEventArgs const&)
+ {
+ if (auto self = weak.get())
+ {
+ if (self->offlineScanRunning_ || !self->operationGate_.idle()) return;
+ const std::wstring entered = self->OfflinePath().Text().c_str();
+ if (self->offlineScanRevision_ && NormalizeUiPath(entered) != NormalizeUiPath(self->offlineInputPath_))
+ self->InvalidateOfflinePath();
+ self->offlineInputPath_ = entered;
+ }
+ });
+ ApplyAdaptiveLayout(WindowRoot().ActualWidth());
+ if (currentPage_ == L"certificates") Navigation().SelectedItem(NavCertificates());
+ else if (currentPage_ == L"offline") Navigation().SelectedItem(NavOffline());
+ else if (currentPage_ == L"reports") Navigation().SelectedItem(NavReports());
+ else if (currentPage_ == L"settings") Navigation().SelectedItem(NavSettings());
+ else if (currentPage_ == L"about") Navigation().SelectedItem(NavAbout());
+ else { currentPage_ = L"overview"; Navigation().SelectedItem(NavOverview()); }
+ NavigateTo(currentPage_);
+
+ resumeToken_ = resumeToken;
+ languageSync_ = true;
+ LanguageCombo().SelectedIndex(language_ == cpc::Language::Russian ? 0 : 1);
+ SettingsLanguageCombo().SelectedIndex(language_ == cpc::Language::Russian ? 0 : 1);
+ settingsSync_ = true;
+ ThemeCombo().SelectedIndex(std::min(static_cast(themeMode_), 1));
+ RememberWindowToggle().IsOn(rememberWindow_);
+ ReduceMotionToggle().IsOn(reduceMotion_);
+ settingsSync_ = false;
+ languageSync_ = false;
+ ApplyLanguage();
+ ApplyTheme();
+ SaveSettings();
+
+ ExecutablePathText().Text(L"");
+ ExecutablePathText().Visibility(Visibility::Collapsed);
+
+ PWSTR documents = nullptr;
+ if (BackupPath().Text().empty() && SUCCEEDED(SHGetKnownFolderPath(FOLDERID_Documents, 0, nullptr, &documents)))
+ {
+ BackupPath().Text(std::wstring(documents) + L"\\CryptoPro Backup");
+ CoTaskMemFree(documents);
+ }
+
+ if (!resumeToken_.empty())
+ {
+ // Defensive fallback for embedders: resume is always an exact
+ // residual pass and never enters the normal uninstaller dialog.
+ cpc::RunResumeCommand(resumeToken_, true);
+ Close();
+ return;
+ }
+ StartLiveScan();
+ }
+
+ HWND MainWindow::GetWindowHandle()
+ {
+ HWND hwnd = nullptr;
+ Microsoft::UI::Xaml::Window window = *this;
+ check_hresult(window.as()->get_WindowHandle(&hwnd));
+ return hwnd;
+ }
+
+ void MainWindow::ConfigureWindow()
+ {
+ Title(std::wstring(L"CryptoPro Cleanup Utility ") + cpc::kVersion);
+ const HWND hwnd = GetWindowHandle();
+ const UINT dpi = GetDpiForWindow(hwnd);
+ const int width = MulDiv(1440, static_cast(dpi), 96);
+ const int height = MulDiv(920, static_cast(dpi), 96);
+ HMONITOR monitor = MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY);
+ MONITORINFO monitorInfo{sizeof(monitorInfo)};
+ if (monitor && GetMonitorInfoW(monitor, &monitorInfo))
+ {
+ const int availableWidth = monitorInfo.rcWork.right - monitorInfo.rcWork.left;
+ const int availableHeight = monitorInfo.rcWork.bottom - monitorInfo.rcWork.top;
+ const int boundedWidth = std::min(width, availableWidth);
+ const int boundedHeight = std::min(height, availableHeight);
+ const int left = monitorInfo.rcWork.left + (availableWidth - boundedWidth) / 2;
+ const int top = monitorInfo.rcWork.top + (availableHeight - boundedHeight) / 2;
+ SetWindowPos(hwnd, nullptr, left, top, boundedWidth, boundedHeight, SWP_NOZORDER | SWP_NOACTIVATE);
+ }
+ const HICON icon = static_cast(LoadImageW(GetModuleHandleW(nullptr), MAKEINTRESOURCEW(kIconResource),
+ IMAGE_ICON, 0, 0, LR_DEFAULTSIZE));
+ if (icon)
+ {
+ SendMessageW(hwnd, WM_SETICON, ICON_SMALL, reinterpret_cast(icon));
+ SendMessageW(hwnd, WM_SETICON, ICON_BIG, reinterpret_cast(icon));
+ }
+ }
+
+ void MainWindow::LoadSettings(bool languageExplicit)
+ {
+ DWORD value = 0;
+ if (ReadSettingDword(L"RememberWindow", &value)) rememberWindow_ = value != 0;
+ if (ReadSettingDword(L"ReduceMotion", &value)) reduceMotion_ = value != 0;
+ if (ReadSettingDword(L"Theme", &value)) themeMode_ = cpc::NormalizeThemeMode(value);
+ if (!languageExplicit && ReadSettingDword(L"Language", &value))
+ language_ = value == 1 ? cpc::Language::English : cpc::Language::Russian;
+ if (rememberWindow_)
+ {
+ const std::wstring page = ReadSettingString(L"LastPage");
+ if (page == L"overview" || page == L"certificates" || page == L"offline" ||
+ page == L"reports" || page == L"settings" || page == L"about") currentPage_ = page;
+ DWORD left = 0, top = 0, right = 0, bottom = 0;
+ if (ReadSettingDword(L"WindowLeft", &left) && ReadSettingDword(L"WindowTop", &top) &&
+ ReadSettingDword(L"WindowRight", &right) && ReadSettingDword(L"WindowBottom", &bottom))
+ {
+ RECT desired{static_cast(left), static_cast(top),
+ static_cast(right), static_cast(bottom)};
+ HMONITOR monitor = MonitorFromRect(&desired, MONITOR_DEFAULTTONULL);
+ MONITORINFO monitorInfo{sizeof(monitorInfo)};
+ if (desired.right - desired.left >= 700 && desired.bottom - desired.top >= 500 &&
+ monitor && GetMonitorInfoW(monitor, &monitorInfo))
+ {
+ const LONG width = std::min(desired.right - desired.left,
+ monitorInfo.rcWork.right - monitorInfo.rcWork.left);
+ const LONG height = std::min(desired.bottom - desired.top,
+ monitorInfo.rcWork.bottom - monitorInfo.rcWork.top);
+ const LONG boundedLeft = std::clamp(desired.left, monitorInfo.rcWork.left,
+ monitorInfo.rcWork.right - width);
+ const LONG boundedTop = std::clamp(desired.top, monitorInfo.rcWork.top,
+ monitorInfo.rcWork.bottom - height);
+ SetWindowPos(GetWindowHandle(), nullptr, boundedLeft, boundedTop, width, height,
+ SWP_NOZORDER | SWP_NOACTIVATE);
+ }
+ }
+ else
+ {
+ DWORD width = 0, height = 0;
+ if (ReadSettingDword(L"WindowWidth", &width) && ReadSettingDword(L"WindowHeight", &height) &&
+ width >= 900 && height >= 650 && width <= 4096 && height <= 2160)
+ {
+ const UINT dpi = GetDpiForWindow(GetWindowHandle());
+ SetWindowPos(GetWindowHandle(), nullptr, 0, 0,
+ MulDiv(static_cast(width), static_cast(dpi), 96),
+ MulDiv(static_cast(height), static_cast(dpi), 96),
+ SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
+ }
+ }
+ if (ReadSettingDword(L"WindowMaximized", &value) && value) ShowWindow(GetWindowHandle(), SW_MAXIMIZE);
+ }
+ }
+
+ void MainWindow::SaveSettings()
+ {
+ HKEY key = nullptr;
+ if (RegCreateKeyExW(HKEY_CURRENT_USER, kSettingsKey, 0, nullptr, 0, KEY_SET_VALUE,
+ nullptr, &key, nullptr) != ERROR_SUCCESS) return;
+ WriteSettingDword(key, L"Language", language_ == cpc::Language::English ? 1 : 0);
+ WriteSettingDword(key, L"Theme", static_cast(themeMode_));
+ WriteSettingDword(key, L"RememberWindow", rememberWindow_ ? 1 : 0);
+ WriteSettingDword(key, L"ReduceMotion", reduceMotion_ ? 1 : 0);
+ if (rememberWindow_)
+ {
+ WINDOWPLACEMENT placement{sizeof(placement)};
+ if (GetWindowPlacement(GetWindowHandle(), &placement))
+ {
+ const RECT rectangle = placement.rcNormalPosition;
+ const UINT dpi = GetDpiForWindow(GetWindowHandle());
+ WriteSettingDword(key, L"WindowWidth", static_cast(MulDiv(rectangle.right - rectangle.left, 96, dpi)));
+ WriteSettingDword(key, L"WindowHeight", static_cast(MulDiv(rectangle.bottom - rectangle.top, 96, dpi)));
+ WriteSettingDword(key, L"WindowLeft", static_cast(rectangle.left));
+ WriteSettingDword(key, L"WindowTop", static_cast(rectangle.top));
+ WriteSettingDword(key, L"WindowRight", static_cast(rectangle.right));
+ WriteSettingDword(key, L"WindowBottom", static_cast(rectangle.bottom));
+ WriteSettingDword(key, L"WindowMaximized", IsZoomed(GetWindowHandle()) ? 1 : 0);
+ }
+ WriteSettingString(key, L"LastPage", currentPage_);
+ }
+ RegCloseKey(key);
+ }
+
+ void MainWindow::ApplyTheme()
+ {
+ switch (themeMode_)
+ {
+ case cpc::ThemeMode::System: WindowRoot().RequestedTheme(ElementTheme::Default); break;
+ case cpc::ThemeMode::Light: WindowRoot().RequestedTheme(ElementTheme::Light); break;
+ default: WindowRoot().RequestedTheme(ElementTheme::Dark); break;
+ }
+ HIGHCONTRASTW contrast{sizeof(contrast)};
+ const bool highContrast = SystemParametersInfoW(SPI_GETHIGHCONTRAST, sizeof(contrast), &contrast, 0) &&
+ (contrast.dwFlags & HCF_HIGHCONTRASTON) != 0;
+ gRuntimeThemeKey = highContrast ? L"HighContrast" :
+ themeMode_ == cpc::ThemeMode::Dark ? L"Dark" :
+ themeMode_ == cpc::ThemeMode::Light ? L"Light" :
+ WindowRoot().ActualTheme() == ElementTheme::Dark ? L"Dark" : L"Light";
+ HighContrastStatus().Text(highContrast
+ ? T(L"Высокий контраст Windows включён.", L"Windows high contrast is enabled.")
+ : T(L"Высокий контраст Windows выключен.", L"Windows high contrast is disabled."));
+ ApplyTitleBarTheme();
+ RefreshThemedVisuals();
+ }
+
+ void MainWindow::ApplyTitleBarTheme()
+ {
+ const BOOL dark = WindowRoot().ActualTheme() == ElementTheme::Dark ? TRUE : FALSE;
+ if (HMODULE dwm = LoadLibraryW(L"dwmapi.dll"))
+ {
+ using DwmSetWindowAttributeFn = HRESULT (WINAPI*)(HWND, DWORD, LPCVOID, DWORD);
+ if (auto setAttribute = reinterpret_cast(GetProcAddress(dwm, "DwmSetWindowAttribute")))
+ {
+ if (FAILED(setAttribute(GetWindowHandle(), 20, &dark, sizeof(dark))))
+ setAttribute(GetWindowHandle(), 19, &dark, sizeof(dark));
+ }
+ FreeLibrary(dwm);
+ }
+ }
+
+ void MainWindow::RefreshThemedVisuals()
+ {
+ if (!uiReady_) return;
+ // Static controls use ThemeResource. Runtime rows keep their model,
+ // handlers, focus, and selection; only brushes are refreshed here.
+ for (auto const& child : ProductsPanel().Children())
+ if (auto border = child.try_as()) border.BorderBrush(ThemeBrush(L"DividerBrush"));
+ for (auto const& child : OfflineProductsPanel().Children())
+ if (auto border = child.try_as()) border.BorderBrush(ThemeBrush(L"DividerBrush"));
+ for (auto const& item : CertificatesPanel().Items())
+ if (auto button = item.try_as