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