From 09d779c1b2e14e70cf40bfff0f68cfce9b829e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=B8=D1=80=D0=B8=D0=BB=D0=BB=20=D0=90=D0=BB=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=D0=BE=D0=B2?= Date: Fri, 21 Aug 2026 17:18:15 +0500 Subject: [PATCH 1/2] Release CryptoPro Cleanup 0.5.3 RC1 --- .gitignore | 5 + App.xaml | 384 +++ App.xaml.cpp | 57 + App.xaml.h | 16 + CHANGELOG.md | 75 + CryptoProCleanup.sln | 24 + CryptoProCleanup.vcxproj | 27 +- CryptoProCleanupModern.vcxproj | 154 ++ CryptoProCleanupResume.vcxproj | 62 + CryptoProCleanupTests.vcxproj | 15 +- MainWindow.xaml | 329 +++ MainWindow.xaml.cpp | 3668 +++++++++++++++++++++++++ MainWindow.xaml.h | 209 ++ README.en.md | 80 +- README.md | 91 +- SECURITY.md | 2 +- docs/RELEASE_NOTES_0.5.3-rc1.md | 90 + docs/TEST_MATRIX.md | 12 + docs/images/0.5.3/about.png | Bin 0 -> 42526 bytes docs/images/0.5.3/certificates.png | Bin 0 -> 58884 bytes docs/images/0.5.3/offline-windows.png | Bin 0 -> 47158 bytes docs/images/0.5.3/overview.png | Bin 0 -> 62046 bytes docs/images/0.5.3/reports.png | Bin 0 -> 38321 bytes docs/images/0.5.3/settings.png | Bin 0 -> 33368 bytes packages.config | 13 + scripts/build.ps1 | 28 +- scripts/check-layout-resources.ps1 | 56 + scripts/check-theme-resources.ps1 | 71 + scripts/legacy-ui-smoke.ps1 | 193 ++ scripts/package.ps1 | 55 +- scripts/ui-smoke.ps1 | 426 +++ src/cleanup.hpp | 229 +- src/core.cpp | 958 ++++++- src/gui.cpp | 1915 ++++++++++++- src/main.cpp | 7 +- src/modern/Project.idl | 8 + src/modern/app.manifest | 24 + src/modern/pch.cpp | 1 + src/modern/pch.h | 50 + src/modern/resources.rc | 38 + src/offline.cpp | 54 +- src/resource.h | 77 + src/resources.rc | 197 +- src/resume.manifest | 23 + src/resume_main.cpp | 22 + src/resume_resources.rc | 34 + src/version.hpp | 14 + tests/test_core.cpp | 287 ++ 48 files changed, 9756 insertions(+), 324 deletions(-) create mode 100644 App.xaml create mode 100644 App.xaml.cpp create mode 100644 App.xaml.h create mode 100644 CryptoProCleanupModern.vcxproj create mode 100644 CryptoProCleanupResume.vcxproj create mode 100644 MainWindow.xaml create mode 100644 MainWindow.xaml.cpp create mode 100644 MainWindow.xaml.h create mode 100644 docs/RELEASE_NOTES_0.5.3-rc1.md create mode 100644 docs/images/0.5.3/about.png create mode 100644 docs/images/0.5.3/certificates.png create mode 100644 docs/images/0.5.3/offline-windows.png create mode 100644 docs/images/0.5.3/overview.png create mode 100644 docs/images/0.5.3/reports.png create mode 100644 docs/images/0.5.3/settings.png create mode 100644 packages.config create mode 100644 scripts/check-layout-resources.ps1 create mode 100644 scripts/check-theme-resources.ps1 create mode 100644 scripts/legacy-ui-smoke.ps1 create mode 100644 scripts/ui-smoke.ps1 create mode 100644 src/modern/Project.idl create mode 100644 src/modern/app.manifest create mode 100644 src/modern/pch.cpp create mode 100644 src/modern/pch.h create mode 100644 src/modern/resources.rc create mode 100644 src/resume.manifest create mode 100644 src/resume_main.cpp create mode 100644 src/resume_resources.rc create mode 100644 src/version.hpp 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