136 Commits
Author SHA1 Message Date
Stone_Red 930222f364 Merge pull request #21 from Stone-Red-Code/develop
Version 1.3.3.0
2026-06-27 20:02:14 +02:00
Stone_Red f4ac2c65f3 Update version and installer 2026-06-27 20:00:14 +02:00
Stone_Red 5fcbe2af79 Add plugins search box to main page 2026-06-27 19:52:02 +02:00
Stone_Red 33962cd68b Move SetIsLocked call from WindowPos to PluginWindow initialization 2026-06-27 19:17:49 +02:00
Stone_Red c6b973b111 Rename local variables for consistency and clarity 2026-06-27 19:11:44 +02:00
Stone_Red d43f1ed949 Add color conversion methods with defaults and refactor ColorPicker parsing for web plugins 2026-06-27 19:08:36 +02:00
Stone_Red 4999ae6e88 Add hot reload support for web plugins with file watcher and debounce 2026-06-27 19:07:53 +02:00
Stone_Red 6061efba02 Add web plugin creation flow and settings bridge to WebPluginWindow 2026-06-27 15:53:27 +02:00
Stone_Red 8e333df71b Add window layer setting with Always on Bottom and Always on Top options 2026-06-27 14:32:56 +02:00
Stone_Red 9b3b7aa4b6 Merge pull request #17 from Stone-Red-Code/develop
Version 1.3.2.0
2026-04-21 16:50:20 +02:00
Stone_Red 2c82c9373f Update version in manifest and update installer 2026-04-21 16:43:46 +02:00
Stone_Red 51b4f15cf1 Set TextWrapping to NoWrap and enforce MaxWidth of 200 on controls 2026-04-21 16:41:33 +02:00
Stone_Red 2916f0983a Add meeting countdown and agenda plugins 2026-04-21 16:07:47 +02:00
Stone_Red 3683e75a17 Add update dialog showing latest release info after app update 2026-04-21 01:49:47 +02:00
Stone_Red 3d98c8b196 Set MainWindow.Topmost based on editMode in SetEditMode method 2026-04-21 01:26:30 +02:00
Stone_Red 7604acf115 Merge pull request #16 from Stone-Red-Code/develop
Version 1.3.1.0
2026-02-07 00:09:58 +01:00
Stone_Red 8c97f6b56a Update version and installer 2026-02-07 00:09:04 +01:00
Stone_Red 01a1d109ff Add weekend color override settings to DatePlugin 2026-02-07 00:00:44 +01:00
Stone_Red d2f123e0f5 Make sure underscore is properly escaped 2026-02-06 23:45:34 +01:00
Stone_Red f217574022 Add FPS limit setting and throttle Main() calls in MusicVisualizerPlugin 2026-01-23 21:53:54 +01:00
Stone_Red 1e9c74af52 Optimize bitmap rendering in PluginWindow 2026-01-23 21:13:40 +01:00
Stone_Red 2853d49dd9 Add rotation setting to plugins 2026-01-23 20:35:15 +01:00
Stone_Red 4133f61a5c Add customizable date format options 2026-01-23 00:33:46 +01:00
Stone_Red e3056a398a Update default and post-load info messages for GIF plugin 2026-01-22 17:54:23 +01:00
Stone_Red cd0bcad181 Add submenu options for bug reporting and feature requests via GitHub or email 2026-01-22 17:53:20 +01:00
Stone_Red fec26069be Make sure date plugin updates when settings change 2026-01-22 15:29:35 +01:00
Stone_Red f84dd20eb9 Update NuGet packages 2026-01-22 15:29:35 +01:00
Stone_Red 9581cc185c Replace all Buttons with icons with ui:Button 2026-01-22 15:29:35 +01:00
Stone_Red a438ab5475 Add Documentation navigation item with click handler linking to GitHub 2026-01-22 15:29:35 +01:00
Stone_Red 1b496db094 Replace TextBox with FileSelector for gif-path plugin setting 2026-01-22 15:29:35 +01:00
Stone_Red 3dce9ab59a Add CreatePluginDialog for plugin creation and update search boxes with icons 2026-01-22 15:29:35 +01:00
Stone_Red a85e57ad58 Fix titles of dialogs 2026-01-22 15:29:34 +01:00
Stone_Red 1cafc01cd7 Merge pull request #13 from Stone-Red-Code/develop
Update README
2026-01-22 00:10:14 +01:00
Stone_Red d319b32263 Update third party licenses in README.md 2026-01-22 00:09:26 +01:00
Stone_Red ca83e5585f Replace old screenshots with new ones
Updated preview images in README with new screenshots.
2026-01-22 00:08:01 +01:00
Stone_Red 68ca15a81f Merge pull request #12 from Stone-Red-Code/develop
Version 1.3.0.0
2026-01-21 23:35:55 +01:00
Stone_Red 0b278ed31a Update installer and manifest to version 1.3.0.0 2026-01-21 23:35:36 +01:00
Stone_Red 346a7ad5a1 Remove unused methods from MainWindow.xaml.cs 2026-01-21 21:32:40 +01:00
Stone_Red ea07f0db3a Show loading indicator during plugin creation 2026-01-21 21:26:05 +01:00
Stone_Red 667c913e57 Make plugin creation and setup steps async 2026-01-21 21:25:44 +01:00
Stone_Red 4d0119e4b3 Cleanup plugin template 2026-01-21 21:22:40 +01:00
Stone_Red f957edd886 Add hot reload support for plugins using FileSystemWatcher 2026-01-21 21:20:27 +01:00
Stone_Red 1a6cd5a744 Add reload plugins button and open plugins in IDE support in plugin manager 2026-01-21 19:42:01 +01:00
Stone_Red deddea2888 Remove Card container for plugin logos 2026-01-20 21:11:33 +01:00
Stone_Red e18ab1ec56 Make UI structure more flat 2026-01-20 20:52:39 +01:00
Stone_Red a8236bd8da Make sure default layout gets loaded when deleting current layout 2026-01-20 19:22:09 +01:00
Stone_Red cb8df79a5f Reset CurrentLayoutName if it does not match any existing layout names 2026-01-20 15:51:48 +01:00
Stone_Red 907d743c0d Make sure newly created layout gets loaded 2026-01-20 15:51:32 +01:00
Stone_Red 835ef23719 Fix margin in MainPage 2026-01-19 22:47:44 +01:00
Stone_Red d2da426baf Update project version to 1.3.0.0 2026-01-19 22:35:06 +01:00
Stone_Red 82fe362559 Add theme-aware Modio icon and resource for PluginManager page 2026-01-17 00:39:53 +01:00
Stone_Red 439eca0ef0 Remove unused NotifyIcon field and its disposal on window close 2026-01-17 00:22:54 +01:00
Stone_Red fca25aeb59 Show error dialogs on UI thread using Dispatcher.InvokeAsync 2026-01-17 00:16:53 +01:00
Stone_Red dd9953cb7d Replace Rectangle fill with Border background for color selection UI 2026-01-17 00:16:34 +01:00
Stone_Red 990793fb01 Add ColorPicker and FileSelector settings controls and API classes 2026-01-17 00:15:57 +01:00
Stone_Red c4517fb8ac Display error message box on UI thread 2026-01-13 23:23:39 +01:00
Stone_Red 92ecd2e93d Add cancellation support to AsyncPlugin methods 2026-01-13 22:42:15 +01:00
Stone_Red 056c42d4df Update API version to 1.0.0.0 and add README and package icon to API project 2026-01-01 22:53:29 +01:00
Stone_Red c64b70eb92 Update installer script and installer 2026-01-01 22:53:07 +01:00
Stone_Red 0804738e45 Update message box style to new style 2025-12-29 00:35:23 +01:00
Stone_Red 727324ca9a Overhaul theme creation and editing UI 2025-12-28 23:20:50 +01:00
Stone_Red 389256a380 Improve performance of plugin window bitmap rendering 2025-12-28 01:47:38 +01:00
Stone_Red ec7917625c UI overhaul 2025-12-28 01:46:16 +01:00
Stone_Red 6aa107233c Add reference to WebView2 feedback issue in workaround comment 2025-12-25 20:03:26 +01:00
Stone_Red 3edb2cff84 Fix WebView2 interaction on transparent window with opaque rectangle 2025-12-25 19:57:39 +01:00
Stone_Red 0bb5b10dba Update AsyncPlugin XML docs for clarity and integration details 2025-12-25 18:20:53 +01:00
Stone_Red 2b363bcd18 Update WeatherPlugin default city 2025-12-25 18:16:27 +01:00
Stone_Red 03da9189a5 Move MouseButton to parent directory and update namespace 2025-12-25 18:16:07 +01:00
Stone_Red 0a57b8a3d6 Fix logging and dialogs in PluginData 2025-12-25 18:14:57 +01:00
Stone_Red 16c5c98f1a Add plugin state persistence via PersistStateAttribute serialization 2025-12-25 18:14:01 +01:00
Stone_Red 9e9c49214a Add support for async plugins 2025-12-25 01:23:03 +01:00
Stone_Red 43c77e6a36 Add plugin methods for changed themes, settings showing message boxes and logging 2025-12-25 01:22:39 +01:00
Stone_Red 4778f5673f Pass theme properties to web plugin 2025-12-24 23:59:08 +01:00
Stone_Red e3d6eb4c8c Add ThemeChanged event to IPluginData 2025-12-24 23:58:53 +01:00
Stone_Red c90be28f38 Add basic support for web based plugins 2025-12-24 17:49:04 +01:00
Stone_Red a564b4db5e Add better error handling and logs to plugin manager 2025-12-20 16:50:33 +01:00
Stone_Red 2cece7dbdc Update WeatherPlugin city name setting label text 2025-12-20 16:35:44 +01:00
Stone_Red ba5bfc7df3 Improve plugin metadata 2025-12-20 16:34:42 +01:00
Stone_Red d316108868 Fix Cpu Monitor plugin percentage alignment 2025-12-20 16:20:05 +01:00
Stone_Red 9a96c60af0 Add confirmation when deleting themes/layouts 2025-12-20 16:17:45 +01:00
Stone_Red a8fe2c80f1 Update plugins when opening plugin manager 2025-12-20 16:08:45 +01:00
Stone_Red ac7826898a Check "Does Not Support Unloading" tag to not use AssemblyLoadContext 2025-12-20 16:08:30 +01:00
Stone_Red 780b506103 Add collectible AssemblyLoadContext to support plugin unloading #5 2025-12-20 00:23:48 +01:00
Stone_Red 10733383d3 Add basic weather plugin 2025-12-20 00:20:31 +01:00
Stone_Red 09ce072ec6 Add try catch when stopping plugin 2025-12-09 17:55:05 +01:00
Stone_Red fcacf36187 Merge pull request #11 from Stone-Red-Code/develop
Update installer
2025-12-09 00:13:59 +01:00
Stone_Red 980ef6473e Update installer 2025-12-09 00:09:27 +01:00
Stone_Red 7755ece3cd Merge pull request #10 from Stone-Red-Code/develop
Develop
2025-12-09 00:05:39 +01:00
Stone_Red b380e35189 Update version 2025-12-09 00:05:27 +01:00
Stone_Red 3fcda8e64e Add per plugin theme override 2025-12-08 20:37:01 +01:00
Stone_Red 0bc7033196 Fix some issues with the new theme support 2025-12-08 20:11:12 +01:00
Stone_Red 10fe4c773b Add better theme support 2025-11-04 19:00:32 +01:00
Stone_Red 13ed2f6a22 Merge pull request #9 from Stone-Red-Code/develop
Update auto start step in README.md
2025-07-07 11:41:15 +02:00
Stone_Red b7139a8821 Update auto start step in README.md 2025-07-07 11:41:01 +02:00
Stone_Red 302ff3aa05 Merge pull request #8 from Stone-Red-Code/develop
Develop
2025-07-05 17:42:00 +02:00
Stone_Red d285bdc489 Update version 2025-07-05 17:39:07 +02:00
Stone_Red 0a64184c32 Fix plugin manager crash when opening it 2025-05-17 15:45:43 +02:00
Stone_Red 2df00fbeeb Properly remove the plugin window from the lists in the main window when an exception occurs 2025-02-26 20:22:42 +01:00
Stone_Red 088f51410a Send plugin windows to front when edit mode enabled and to back when disabled 2025-02-26 19:28:42 +01:00
Stone_Red 70d08aa4c3 Set WindowState of main window to Minimized when closing 2025-02-25 23:04:44 +01:00
Stone_Red af9aa60bd9 Rename ModEntry UserControl to PluginEntry 2025-02-25 23:03:14 +01:00
Stone_Red 695cb867ee Add autostart checkbox 2025-02-25 19:20:02 +01:00
Stone_Red 376176460b Update modio.net library links in README.md 2025-02-25 03:09:35 +01:00
Stone_Red 327b79d69c Remove Extended.Wpf.Toolkit from thrid party licenses section in README.md 2025-02-25 03:04:43 +01:00
Stone_Red c74bc60209 Add login/logout button to plugin manger to sync subscriptions 2025-02-24 22:02:58 +01:00
Stone_Red 930da5d679 Correctly update .csproj file when creating new plugin 2025-02-23 15:53:41 +01:00
Stone_Red ffd92d2259 Add create new plugin button to plugin manager 2025-02-23 15:32:56 +01:00
Stone_Red 6100f444a8 Center dialogs on parent window 2025-02-23 15:32:25 +01:00
Stone_Red ead20d1279 Change GetLanguageDictionary method to LanguageDictionary property 2024-12-30 16:54:04 +01:00
Stone_Red b638eff451 Add missing translations 2024-12-28 23:16:23 +01:00
Stone_Red 793dcbad38 Small fixes 2024-12-28 16:40:06 +01:00
Stone_Red c2474e6425 Plugin manager layout improvments 2024-12-05 15:00:48 +01:00
Stone_Red cbeaaee88e Standardize README 2024-12-03 00:43:51 +01:00
Stone_Red 92c13c5ed0 Change app name from "Desktop Magic" to "DesktopMagic" and some QOL improvments 2024-12-03 00:16:37 +01:00
Stone_Red 26e979af79 Standardize default plugin setting names 2024-12-03 00:16:37 +01:00
Stone_Red 29619dbdda Rename download plugins to browse plugins in README.md 2024-11-30 00:38:56 +01:00
Stone_Red bc11832f9f Add right click actions to tray icon 2024-11-29 18:40:00 +01:00
Stone_Red f8f06a9e2c Improve plugin window performance by not constantly updating properties of UI elements 2024-11-29 17:55:33 +01:00
Stone_Red 35d64ef2c3 Don't unnecessarily redraw time plugin bitmap every second 2024-11-29 17:55:33 +01:00
Stone_Red 3b9d49e8da Update README.md 2024-11-29 00:57:02 +01:00
Stone_Red aada1b8807 Add Microsoft Store link and fix twitter/x link 2024-11-28 17:47:36 +01:00
Stone_Red 9abfb9cb66 Merge pull request #7 from Stone-Red-Code/main
Readme updates
2024-11-18 23:00:21 +01:00
Stone_Red ce1c181b14 Add MsixPackaging project 2024-11-16 14:36:04 +01:00
Stone_Red e6be5c011b Replace deprecated Modio library with alternative library 2024-11-15 19:55:12 +01:00
Stone_Red c6e858764f Remove unnecessary files 2024-11-15 19:53:19 +01:00
Stone_Red f684cdb051 Show error when failing to delete plugin files 2024-11-15 19:41:34 +01:00
Stone_Red 3993e6d612 Fix multiple warnings 2024-11-15 19:41:16 +01:00
Stone_Red 8e187f91fa Update README.md 2024-10-31 13:19:28 +01:00
Stone_Red 566bcee426 Update README.md 2024-10-30 21:51:00 +01:00
Stone_Red 038074cd87 Update README.md 2024-10-30 21:50:22 +01:00
Stone_Red 7e1736696d Rename BuiltInWindowElements to BuiltInPlugins 2024-10-26 14:43:14 +02:00
Stone_Red e545f27461 Replace Xceed.Wpf.Toolkit.IntegerUpDown with MaterialDesignThemes.Wpf.NumericUpDown 2024-10-26 00:25:53 +02:00
Stone_Red d458feca1e Add DesktopMagicPluginAPI.xml to .gitignore 2024-10-25 23:28:52 +02:00
Stone_Red f899fd3e9b Merge branch 'main' into develop 2024-10-25 23:22:23 +02:00
Stone_Red c3b867ff67 Merge pull request #6 from dylanrenwick/main
Updated documentation file path to be relative to project
2024-10-25 23:19:29 +02:00
drenwick 1fe59e6338 Updated documentation file path to be relative to project 2024-10-25 15:10:58 -04:00
137 changed files with 6842 additions and 1408 deletions
+3
View File
@@ -6,6 +6,9 @@
bin/ bin/
obj/ obj/
# Project-specific files
/src/DesktopMagicPluginAPI/DesktopMagicPluginAPI.xml
# User-specific files # User-specific files
*.rsuser *.rsuser
*.suo *.suo
+18 -15
View File
@@ -1,31 +1,34 @@
# DesktopMagic ![DesktopMagic](https://socialify.git.ci/Stone-Red-Code/DesktopMagic/image?description=1&descriptionEditable=DesktopMagic%20allows%20you%20to%20improve%20the%20looks%20of%20your%20desktop!&forks=1&issues=1&logo=https%3A%2F%2Fgithub.com%2Fuser-attachments%2Fassets%2F34f3c473-a29a-4d71-9ec0-1387b04ad2c5&name=1&pattern=Formal%20Invitation&pulls=1&stargazers=1&theme=Auto)
> DesktopMagic allows you to improve the looks of your Windows desktop!\ > You can add useful widgets to your desktop like a clock, an audio visualizer or even your own plugins using mod.io!
> You can add useful elements to your desktop like a clock, an audio visualizer or even your own plugins using mod.io!
- [Releases](https://github.com/Stone-Red-Code/DesktopMagic/releases) - [Releases](https://github.com/Stone-Red-Code/DesktopMagic/releases)
- [Download plugins](https://mod.io/g/desktopmagic) - [Browse plugins](https://mod.io/g/desktopmagic)
- [Plugin creation guide](https://mod.io/g/desktopmagic/r/plugin-creation-guide) - [Plugin creation guide](https://mod.io/g/desktopmagic/r/plugin-creation-guide)
- [Licence](https://github.com/Stone-Red-Code/DesktopMagic/blob/main/LICENSE) - [Licence](https://github.com/Stone-Red-Code/DesktopMagic/blob/main/LICENSE)
## Setup ## Usage
1. Download one of the [releases](https://github.com/Stone-Red-Code/DesktopMagic/releases). 1. Download & install the latest release
1. Execute the `DesktopMagic-Installer.exe` file. - Microsoft Store: [Store Page](https://www.microsoft.com/store/productId/9NPV8F9K1CWM?ocid=pdpshare)
1. (Optional) Put a shortcut to the `DesktopMagic.exe` file in the install directory into the `shell:startup` directory. - GitHub: [Releases](https://github.com/Stone-Red-Code/DesktopMagic/releases)
1. Start the app and select the widgets you want to see on your desktop.
1. (Optional) Check "Launch on startup" so DesktopMagic automatically starts when you log in.
1. Check the "Edit" Checkbox to move the widgets.
1. Open the plugin manager to install third-party plugins.
## Follow the development ## Follow the development
[![Twitter(X)](http://img.shields.io/badge/Twitter-black.svg?&logo=x&style=for-the-badge&logoColor=white)](https://twitter.com/search?q=%23DesktopMagic%20%40StoneRedCode&f=live)
[![Twitter(X)](http://img.shields.io/badge/Twitter-black.svg?&logo=x&style=for-the-badge&logoColor=white)](https://x.com/search?q=from%3A%40StoneRedCode%20%23DesktopMagic)
## Preview ## Preview
![Screenshot 2023-12-31 164947](https://github.com/Stone-Red-Code/DesktopMagic/assets/56473591/191ac76f-264d-4e9f-8979-7a13b2a1cd12) ![Screenshot 2026-01-22 000348](https://github.com/user-attachments/assets/4617330d-4c4b-4509-80d7-9f6db413e84f)
![Screenshot 2023-12-31 164853](https://github.com/Stone-Red-Code/DesktopMagic/assets/56473591/22de55d2-dde3-43a5-b77b-c773a7697e8c) ![Screenshot 2026-01-22 000434](https://github.com/user-attachments/assets/aebeca87-b305-40a4-a942-d78fce0e9e97)
## Third party licenses ## Third party licenses
- [BusyIndicator](https://github.com/moh3ngolshani/BusyIndicator) - [MIT](https://github.com/Moh3nGolshani/BusyIndicator/blob/master/LICENSE) - [BusyIndicator](https://github.com/moh3ngolshani/BusyIndicator) - [MIT](https://github.com/Moh3nGolshani/BusyIndicator/blob/master/LICENSE)
- [Extended.Wpf.Toolkit](https://github.com/xceedsoftware/wpftoolkit) - [XCEED SOFTWARE COMMUNITY LICENSE AGREEMENT](https://github.com/xceedsoftware/wpftoolkit/blob/master/license.md) - [WPF UI](https://github.com/lepoco/wpfui) - [MIT](https://github.com/lepoco/wpfui/blob/main/LICENSE.md)
- [Material Design In XAML Toolkit](https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit) - [MIT](https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE) - [Modio.NET](https://github.com/nickelc/modio.net) - [Apache-2.0](https://github.com/nickelc/modio.net/blob/master/LICENSE-APACHE), [MIT](https://github.com/nickelc/modio.net/blob/master/LICENSE-MIT)
- [Modio.NET](https://github.com/thetestgame/modio.net) - [Apache-2.0](https://github.com/thetestgame/Modio.NET/blob/main/LICENSE-APACHE), [MIT](https://github.com/thetestgame/Modio.NET/blob/main/LICENSE-MIT)
- [NAudio](https://github.com/naudio/NAudio) - [MIT](https://github.com/naudio/NAudio/blob/master/license.txt) - [NAudio](https://github.com/naudio/NAudio) - [MIT](https://github.com/naudio/NAudio/blob/master/license.txt)
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
; Script generated by the Inno Setup Script Wizard. ; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#expr Exec('cmd.exe /C ', 'dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\DesktopMagic\') #expr Exec('cmd.exe', '/C dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\DesktopMagic\')
#define MyAppName "DesktopMagic" #define MyAppName "DesktopMagic"
#define MyAppVersion GetStringFileInfo("/publish/DesktopMagic.exe","ProductVersion") #define MyAppVersion GetStringFileInfo("/publish/DesktopMagic.exe","ProductVersion")
+92 -4
View File
@@ -9,24 +9,112 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopMagic.Api", "Desktop
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopMagic.PluginTest", "DesktopMagicPlugin.Test\DesktopMagic.PluginTest.csproj", "{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DesktopMagic.PluginTest", "DesktopMagicPlugin.Test\DesktopMagic.PluginTest.csproj", "{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}"
EndProject EndProject
Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "MsixPackaging", "MsixPackaging\MsixPackaging.wapproj", "{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
Debug|ARM = Debug|ARM
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU Release|Any CPU = Release|Any CPU
Release|ARM = Release|ARM
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution GlobalSection(ProjectConfigurationPlatforms) = postSolution
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|Any CPU.ActiveCfg = Debug|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|Any CPU.Build.0 = Debug|Any CPU {B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|Any CPU.Build.0 = Debug|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|Any CPU.ActiveCfg = Release|Any CPU {B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|ARM.ActiveCfg = Debug|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|Any CPU.Build.0 = Release|Any CPU {B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|ARM.Build.0 = Debug|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|ARM64.Build.0 = Debug|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|x64.ActiveCfg = Debug|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|x64.Build.0 = Debug|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|x86.ActiveCfg = Debug|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Debug|x86.Build.0 = Debug|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|Any CPU.ActiveCfg = Release|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|Any CPU.Build.0 = Release|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|ARM.ActiveCfg = Release|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|ARM.Build.0 = Release|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|ARM64.ActiveCfg = Release|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|ARM64.Build.0 = Release|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|x64.ActiveCfg = Release|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|x64.Build.0 = Release|x64
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|x86.ActiveCfg = Release|Any CPU
{B56B4CCC-CF92-4C05-9DEA-57666D81889A}.Release|x86.Build.0 = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|Any CPU.Build.0 = Debug|Any CPU {9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|ARM.ActiveCfg = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|ARM.Build.0 = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|ARM64.Build.0 = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|x64.ActiveCfg = Debug|x64
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|x64.Build.0 = Debug|x64
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|x86.ActiveCfg = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Debug|x86.Build.0 = Debug|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|Any CPU.ActiveCfg = Release|Any CPU {9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|Any CPU.Build.0 = Release|Any CPU {9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|Any CPU.Build.0 = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|ARM.ActiveCfg = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|ARM.Build.0 = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|ARM64.ActiveCfg = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|ARM64.Build.0 = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|x64.ActiveCfg = Release|x64
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|x64.Build.0 = Release|x64
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|x86.ActiveCfg = Release|Any CPU
{9D193A10-3E8E-447F-89F1-4B316C2EE37D}.Release|x86.Build.0 = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|Any CPU.Build.0 = Debug|Any CPU {EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|ARM.ActiveCfg = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|ARM.Build.0 = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|ARM64.ActiveCfg = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|ARM64.Build.0 = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|x64.ActiveCfg = Debug|x64
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|x64.Build.0 = Debug|x64
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|x86.ActiveCfg = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Debug|x86.Build.0 = Debug|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|Any CPU.ActiveCfg = Release|Any CPU {EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|Any CPU.Build.0 = Release|Any CPU {EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|Any CPU.Build.0 = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|ARM.ActiveCfg = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|ARM.Build.0 = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|ARM64.ActiveCfg = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|ARM64.Build.0 = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|x64.ActiveCfg = Release|x64
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|x64.Build.0 = Release|x64
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|x86.ActiveCfg = Release|Any CPU
{EF8407AB-28AE-4B5E-AC2E-D4C4A575FBC7}.Release|x86.Build.0 = Release|Any CPU
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|ARM.ActiveCfg = Debug|ARM
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|ARM.Build.0 = Debug|ARM
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|ARM.Deploy.0 = Debug|ARM
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|ARM64.ActiveCfg = Debug|ARM64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|ARM64.Build.0 = Debug|ARM64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|ARM64.Deploy.0 = Debug|ARM64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|x64.ActiveCfg = Debug|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|x64.Build.0 = Debug|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|x64.Deploy.0 = Debug|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|x86.ActiveCfg = Debug|x86
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|x86.Build.0 = Debug|x86
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Debug|x86.Deploy.0 = Debug|x86
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|Any CPU.ActiveCfg = Release|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|Any CPU.Build.0 = Release|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|Any CPU.Deploy.0 = Release|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|ARM.ActiveCfg = Release|ARM
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|ARM.Build.0 = Release|ARM
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|ARM.Deploy.0 = Release|ARM
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|ARM64.ActiveCfg = Release|ARM64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|ARM64.Build.0 = Release|ARM64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|ARM64.Deploy.0 = Release|ARM64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|x64.ActiveCfg = Release|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|x64.Build.0 = Release|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|x64.Deploy.0 = Release|x64
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|x86.ActiveCfg = Release|x86
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|x86.Build.0 = Release|x86
{CC2850EF-1451-4D0C-9474-4CBEBB28A2C8}.Release|x86.Deploy.0 = Release|x86
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
+16 -8
View File
@@ -1,20 +1,28 @@
<Application x:Class="DesktopMagic.App" <Application x:Class="DesktopMagic.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:md="http://schemas.lepo.co/wpfui/2022/xaml/markdown"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:helpers="clr-namespace:DesktopMagic.Helpers"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources\Strings\StringResources.en.xaml" /> <ResourceDictionary Source="Resources\Strings\StringResources.en.xaml" />
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="Grey" SecondaryColor="LightBlue" /> <ui:ThemesDictionary Theme="Light" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" /> <ui:ControlsDictionary />
<md:ThemesDictionary Theme="Light" />
<md:ControlsDictionary />
<ResourceDictionary Source="pack://application:,,,/BusyIndicator;component/Theme/Default.xaml" /> <ResourceDictionary Source="pack://application:,,,/BusyIndicator;component/Theme/Default.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteBrushes.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteStyles.xaml" />
<ResourceDictionary Source="Resources\Images\ImageResources.xaml" /> <ResourceDictionary Source="Resources\Images\ImageResources.xaml" />
<ResourceDictionary Source="Resources\Styles\ToggleSwitchContentLeftStyle.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<Thickness x:Key="DefaultMargin" Top="6" Left="6" Right="6" Bottom ="6"></Thickness>
<!-- Converters -->
<helpers:UnderscoreEscapingConverter x:Key="UnderscoreEscapingConverter" />
<!-- Margins -->
<Thickness x:Key="DefaultMargin" Top="5" Left="5" Right="5" Bottom ="5" />
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>
+67 -23
View File
@@ -5,6 +5,8 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Windows; using System.Windows;
using Wpf.Ui;
namespace DesktopMagic; namespace DesktopMagic;
/// <summary> /// <summary>
@@ -14,13 +16,19 @@ public partial class App : Application
{ {
public const string AppGuid = "{{61FE5CE9-47C3-4255-A1F4-5BCF4ACA0879}"; public const string AppGuid = "{{61FE5CE9-47C3-4255-A1F4-5BCF4ACA0879}";
public const string AppName = "Desktop Magic"; public const string AppName = "DesktopMagic";
// This is the previous name of the application, used to migrate the application data folder
private const string PreviousAppName = "Desktop Magic";
private static readonly string logFilePath = Path.Combine(ApplicationDataPath, $"{AppName}.log"); private static readonly string logFilePath = Path.Combine(ApplicationDataPath, $"{AppName}.log");
private readonly Thread? eventThread; private readonly Thread? eventThread;
private readonly EventWaitHandle eventWaitHandle; private readonly EventWaitHandle eventWaitHandle;
public static string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "StoneRed", AppName);
public static string PluginsPath => Path.Combine(ApplicationDataPath, "Plugins");
public static IContentDialogService DialogService { get; } = new ContentDialogService();
public static Logger Logger { get; } = new Logger() public static Logger Logger { get; } = new Logger()
{ {
@@ -62,33 +70,51 @@ public partial class App : Application
} }
}; };
public static string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "StoneRed", AppName); public static ResourceDictionary LanguageDictionary
{
get
{
ResourceDictionary dict = [];
string currentCulture = Thread.CurrentThread.CurrentUICulture.ToString();
if (currentCulture.Contains("de"))
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.de.xaml", UriKind.Relative);
}
else
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.en.xaml", UriKind.Relative);
}
return dict;
}
}
private static string PreviousApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "StoneRed", PreviousAppName);
public App() public App()
{ {
// Setup global event handler // Setup global event handler
eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, AppGuid, out bool createdNew); eventWaitHandle = new EventWaitHandle(false, EventResetMode.AutoReset, AppGuid, out bool createdNew);
//check if creating new was successful // Check if creating new was successful
if (!createdNew) if (!createdNew)
{ {
Setup(false); Setup(false);
Logger.LogWarn("Shutting down because other instance already running.", source: "Setup"); Logger.LogWarn("Shutting down because other instance already running.", source: "Setup");
//Shutdown Application // Shutdown Application
_ = eventWaitHandle.Set(); _ = eventWaitHandle.Set();
Current.Shutdown(); Current.Shutdown();
} }
else else
{ {
eventThread = new Thread( eventThread = new Thread(() =>
() => {
while (eventWaitHandle.WaitOne())
{ {
while (eventWaitHandle.WaitOne()) _ = Current.Dispatcher.BeginInvoke(() => ((MainWindow)Current.MainWindow).RestoreWindow());
{ }
_ = Current.Dispatcher.BeginInvoke( });
() => ((MainWindow)Current.MainWindow).RestoreWindow());
}
});
eventThread.Start(); eventThread.Start();
Setup(true); Setup(true);
@@ -102,21 +128,45 @@ public partial class App : Application
eventThread?.Interrupt(); eventThread?.Interrupt();
} }
private void Setup(bool clearLogFile) private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Exception exception = (Exception)e.ExceptionObject;
Logger.LogFatal(exception + (e.IsTerminating ? "\t Process terminating!" : ""), source: exception.Source ?? "Unknown");
}
private static async void Setup(bool clearLogFile)
{ {
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
try try
{ {
if (Directory.Exists(PreviousApplicationDataPath) && !Directory.Exists(ApplicationDataPath))
{
Directory.Move(PreviousApplicationDataPath, ApplicationDataPath);
Logger.LogInfo("Migrated ApplicationData folder", source: "Setup");
}
if (!Directory.Exists(ApplicationDataPath)) if (!Directory.Exists(ApplicationDataPath))
{ {
_ = Directory.CreateDirectory(ApplicationDataPath); _ = Directory.CreateDirectory(ApplicationDataPath);
Logger.LogInfo("Created ApplicationData folder", source: "Setup");
}
if (!Directory.Exists(PluginsPath))
{
_ = Directory.CreateDirectory(PluginsPath);
Logger.LogInfo("Created Plugins folder", source: "Setup");
} }
Logger.LogInfo("Created ApplicationData Folder", source: "Main");
} }
catch (Exception ex) catch (Exception ex)
{ {
_ = MessageBox.Show(ex.ToString()); Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = AppName,
Content = ex.ToString(),
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
Logger.Log(ex.Message, "Setup", LogSeverity.Error); Logger.Log(ex.Message, "Setup", LogSeverity.Error);
} }
@@ -127,10 +177,4 @@ public partial class App : Application
Logger.LogInfo("Setup complete", source: "Setup"); Logger.LogInfo("Setup complete", source: "Setup");
} }
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
{
Exception exception = (Exception)e.ExceptionObject;
Logger.LogFatal(exception + (e.IsTerminating ? "\t Process terminating!" : ""), source: exception.Source ?? "Unknown");
}
} }
@@ -0,0 +1,279 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using DesktopMagic.Helpers;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Text;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.BuiltInPlugins;
internal class AgendaPlugin : AsyncPlugin
{
[Setting("agenda-calendar-url", "iCal URL")]
private readonly TextBox calendarUrl = new TextBox(string.Empty);
[Setting("agenda-refresh-button", "Refresh Calendar")]
private readonly Button refreshButton = new Button("Refresh");
[Setting("agenda-refresh-interval", "Refresh Every (minutes)")]
private readonly IntegerUpDown refreshMinutes = new IntegerUpDown(1, 120, 10);
[Setting("agenda-lookahead-hours", "Look Ahead (hours)")]
private readonly IntegerUpDown lookAheadHours = new IntegerUpDown(1, 336, 24);
[Setting("agenda-max-events", "Maximum Events")]
private readonly IntegerUpDown maxEvents = new IntegerUpDown(1, 15, 5);
[Setting("agenda-include-all-day", "Include All-Day Events")]
private readonly CheckBox includeAllDayEvents = new CheckBox(true);
[Setting("agenda-show-date", "Show Date")]
private readonly CheckBox showDate = new CheckBox(true);
public override int UpdateInterval { get; set; } = 30000;
private List<CalendarEventEntry> upcomingEvents = [];
private DateTime nextRefreshTime = DateTime.MinValue;
private bool forceRefresh = true;
private bool themeChanged = true;
private string? errorText;
public override Task StartAsync(CancellationToken cancellationToken)
{
refreshButton.OnClick += () =>
{
forceRefresh = true;
Application.UpdateWindow();
};
calendarUrl.OnValueChanged += MarkForRefresh;
refreshMinutes.OnValueChanged += MarkForRefresh;
lookAheadHours.OnValueChanged += MarkForRefresh;
maxEvents.OnValueChanged += MarkForRefresh;
includeAllDayEvents.OnValueChanged += MarkForRefresh;
showDate.OnValueChanged += Application.UpdateWindow;
return Task.CompletedTask;
}
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
{
if (themeChanged || forceRefresh || DateTime.Now >= nextRefreshTime)
{
await RefreshEventsAsync(cancellationToken);
themeChanged = false;
}
return RenderAgendaBitmap();
}
public override void OnThemeChanged()
{
themeChanged = true;
}
public override void OnSettingsChanged()
{
Application.UpdateWindow();
}
private void MarkForRefresh()
{
forceRefresh = true;
Application.UpdateWindow();
}
private async Task RefreshEventsAsync(CancellationToken cancellationToken)
{
forceRefresh = false;
nextRefreshTime = DateTime.Now.AddMinutes(Math.Max(1, refreshMinutes.Value));
if (string.IsNullOrWhiteSpace(calendarUrl.Value))
{
upcomingEvents = [];
errorText = "Set iCal URL in settings";
return;
}
try
{
DateTime now = DateTime.Now;
DateTime windowEnd = now.AddHours(Math.Max(1, lookAheadHours.Value));
upcomingEvents = await IcsCalendarHelper.GetUpcomingEventsAsync(
calendarUrl.Value.Trim(),
now,
windowEnd,
maxEvents.Value,
includeAllDayEvents.Value,
cancellationToken);
errorText = null;
}
catch (HttpRequestException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
catch (TaskCanceledException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
catch (InvalidOperationException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
}
private Bitmap RenderAgendaBitmap()
{
List<(string Day, string Date, string Time, string Title, bool IsMessage)> rows = [];
if (!string.IsNullOrWhiteSpace(errorText))
{
rows.Add((string.Empty, string.Empty, string.Empty, errorText, true));
}
else if (upcomingEvents.Count == 0)
{
rows.Add((string.Empty, string.Empty, string.Empty, "No events in selected window", true));
}
else
{
foreach (CalendarEventEntry eventEntry in upcomingEvents)
{
string dayPart = showDate.Value ? eventEntry.StartLocal.ToString("ddd") : string.Empty;
string datePart = showDate.Value ? eventEntry.StartLocal.ToString("dd MMM yyyy") : string.Empty;
string timePart = eventEntry.IsAllDay ? "All day" : eventEntry.StartLocal.ToShortTimeString();
rows.Add((dayPart, datePart, timePart, eventEntry.Title, false));
}
}
using Font font = new Font(Application.Theme.Font, 70);
using Bitmap measureBitmap = new Bitmap(1, 1);
using Graphics measureGraphics = Graphics.FromImage(measureBitmap);
measureGraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
bool hasDayColumn = rows.Any(row => !row.IsMessage && !string.IsNullOrWhiteSpace(row.Day));
bool hasDateColumn = rows.Any(row => !row.IsMessage && !string.IsNullOrWhiteSpace(row.Date));
bool hasTimeColumn = rows.Any(row => !row.IsMessage && !string.IsNullOrWhiteSpace(row.Time));
float maxDayWidth = 0;
float maxDateWidth = 0;
float maxTimeWidth = 0;
float maxTitleWidth = 0;
float lineHeight = 0;
foreach ((string day, string date, string time, string title, bool isMessage) in rows)
{
if (isMessage)
{
SizeF messageSize = measureGraphics.MeasureString(title, font);
maxTitleWidth = Math.Max(maxTitleWidth, messageSize.Width);
lineHeight = Math.Max(lineHeight, messageSize.Height);
continue;
}
if (hasDayColumn)
{
SizeF daySize = measureGraphics.MeasureString(day, font);
maxDayWidth = Math.Max(maxDayWidth, daySize.Width);
lineHeight = Math.Max(lineHeight, daySize.Height);
}
if (hasDateColumn)
{
SizeF dateSize = measureGraphics.MeasureString(date, font);
maxDateWidth = Math.Max(maxDateWidth, dateSize.Width);
lineHeight = Math.Max(lineHeight, dateSize.Height);
}
if (hasTimeColumn)
{
SizeF timeSize = measureGraphics.MeasureString(time, font);
maxTimeWidth = Math.Max(maxTimeWidth, timeSize.Width);
lineHeight = Math.Max(lineHeight, timeSize.Height);
}
SizeF titleSize = measureGraphics.MeasureString(title, font);
maxTitleWidth = Math.Max(maxTitleWidth, titleSize.Width);
lineHeight = Math.Max(lineHeight, titleSize.Height);
}
float columnSpacing = measureGraphics.MeasureString(" ", font).Width;
float prefixWidth = 0;
bool hasAnyPrefix = false;
if (hasDayColumn)
{
prefixWidth += maxDayWidth;
hasAnyPrefix = true;
}
if (hasDateColumn)
{
prefixWidth += (hasAnyPrefix ? columnSpacing : 0) + maxDateWidth;
hasAnyPrefix = true;
}
if (hasTimeColumn)
{
prefixWidth += (hasAnyPrefix ? columnSpacing : 0) + maxTimeWidth;
hasAnyPrefix = true;
}
float totalWidth = (hasAnyPrefix ? prefixWidth + columnSpacing : 0) + maxTitleWidth;
int width = Math.Max(1, (int)Math.Ceiling(totalWidth));
int height = Math.Max(1, (int)Math.Ceiling(lineHeight * rows.Count));
Bitmap bitmap = new Bitmap(width, height);
bitmap.SetResolution(100, 100);
using Graphics graphics = Graphics.FromImage(bitmap);
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
graphics.Clear(Color.Transparent);
float dayColumnX = 0;
float dateColumnX = hasDayColumn ? dayColumnX + maxDayWidth + columnSpacing : dayColumnX;
float timeColumnX = hasDateColumn ? dateColumnX + maxDateWidth + columnSpacing : dateColumnX;
float titleColumnX = hasAnyPrefix ? timeColumnX + (hasTimeColumn ? maxTimeWidth : 0) + columnSpacing : 0;
for (int index = 0; index < rows.Count; index++)
{
(string day, string date, string time, string title, bool isMessage) = rows[index];
float y = index * lineHeight;
if (isMessage)
{
graphics.DrawString(title, font, brush, 0, y);
continue;
}
if (hasDayColumn && !string.IsNullOrWhiteSpace(day))
{
graphics.DrawString(day, font, brush, dayColumnX, y);
}
if (hasDateColumn && !string.IsNullOrWhiteSpace(date))
{
graphics.DrawString(date, font, brush, dateColumnX, y);
}
if (hasTimeColumn && !string.IsNullOrWhiteSpace(time))
{
graphics.DrawString(time, font, brush, timeColumnX, y);
}
graphics.DrawString(title, font, brush, titleColumnX, y);
}
return bitmap;
}
}
@@ -7,7 +7,7 @@ using System.Drawing.Text;
using SettingAttribute = DesktopMagic.Api.Settings.SettingAttribute; using SettingAttribute = DesktopMagic.Api.Settings.SettingAttribute;
namespace DesktopMagic.BuiltInWindowElements; namespace DesktopMagic.BuiltInPlugins;
internal class CpuMonitorPlugin : Plugin internal class CpuMonitorPlugin : Plugin
{ {
@@ -56,7 +56,7 @@ internal class CpuMonitorPlugin : Plugin
} }
else else
{ {
gr.DrawString(cpuUsageString, font, new SolidBrush(Application.Theme.PrimaryColor), 0, (size.Width / 2) - size.Height); gr.DrawString(cpuUsageString, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
} }
return bmp; return bmp;
@@ -0,0 +1,167 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using System;
using System.Drawing;
using System.Drawing.Text;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
namespace DesktopMagic.BuiltInPlugins;
internal partial class DatePlugin : Plugin
{
[Setting("week-day-style", "Week Day Style")]
public ComboBox weekDayStyleComboBox = new ComboBox([.. Enum.GetValues<WeekdayStyle>().Select(e => e.ToString())]);
[Setting("day-style", "Day Style")]
public ComboBox dayStyleComboBox = new ComboBox([.. Enum.GetValues<DayStyle>().Select(e => e.ToString())]);
[Setting("month-style", "Month Style")]
public ComboBox monthStyleComboBox = new ComboBox([.. Enum.GetValues<MonthStyle>().Select(e => e.ToString())]);
[Setting("year-style", "Year Style")]
public ComboBox yearStyleComboBox = new ComboBox([.. Enum.GetValues<YearStyle>().Select(e => e.ToString())]);
[Setting("use-date-separators", "Use Date Separators")]
public CheckBox useDateSeparators = new CheckBox(true);
[Setting("use-color-override-for-weekend", "Use Color Override For Weekend")]
public CheckBox useColorOverrideForWeekend = new CheckBox(false);
[Setting("weekend-color-override", "Weekend Color Override")]
public ColorPicker weekendColorOverride = new ColorPicker(Color.Red);
private DateTime oldDateTime = DateTime.MinValue;
private bool themeChanged = false;
public override int UpdateInterval => 1000;
public override Bitmap? Main()
{
if (oldDateTime.Date == DateTime.Now.Date && !themeChanged)
{
return null;
}
oldDateTime = DateTime.Now;
themeChanged = false;
CultureInfo culture = CultureInfo.CurrentCulture;
DateTime dateTime = DateTime.Now;
DateTimeFormatInfo dtf = culture.DateTimeFormat;
string pattern = dtf.LongDatePattern;
WeekdayStyle weekdayStyle = Enum.Parse<WeekdayStyle>(weekDayStyleComboBox.Value);
DayStyle dayStyle = Enum.Parse<DayStyle>(dayStyleComboBox.Value);
MonthStyle monthStyle = Enum.Parse<MonthStyle>(monthStyleComboBox.Value);
YearStyle yearStyle = Enum.Parse<YearStyle>(yearStyleComboBox.Value);
// Weekday: ddd, dddd
pattern = WeekdayRegex().Replace(pattern, weekdayStyle switch
{
WeekdayStyle.Long => "dddd",
WeekdayStyle.Short => "ddd",
_ => ""
});
// Day: d, dd
pattern = DayRegex().Replace(pattern, dayStyle switch
{
DayStyle.Numeric => "dd",
_ => ""
});
// Month: M, MM, MMM, MMMM
pattern = MonthRegex().Replace(pattern, monthStyle switch
{
MonthStyle.Long => "MMMM",
MonthStyle.Short => "MMM",
MonthStyle.Numeric => "MM",
_ => ""
});
// Year: yy, yyyy
pattern = YearRegex().Replace(pattern, yearStyle switch
{
YearStyle.FourDigit => "yyyy",
YearStyle.TwoDigit => "yy",
_ => ""
});
// Clean up leftover punctuation/spacing
pattern = CleanupRegex().Replace(pattern, " ").Trim();
if (string.IsNullOrWhiteSpace(pattern))
{
pattern = dtf.LongDatePattern;
}
string date = dateTime.ToString(pattern, culture);
if (useDateSeparators.Value)
{
date = DateSeparatorsRegex().Replace(date, dtf.DateSeparator);
}
Color color = useColorOverrideForWeekend.Value && (dateTime.DayOfWeek == DayOfWeek.Saturday || dateTime.DayOfWeek == DayOfWeek.Sunday)
? weekendColorOverride.Value
: Application.Theme.PrimaryColor;
using Font font = new Font(Application.Theme.Font, 200);
Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100);
using Graphics tmpGr = Graphics.FromImage(bmp);
tmpGr.TextRenderingHint = TextRenderingHint.AntiAlias;
SizeF size = tmpGr.MeasureString(date, font);
bmp = new Bitmap((int)size.Width, (int)size.Height);
bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp);
using SolidBrush brush = new SolidBrush(color);
gr.TextRenderingHint = TextRenderingHint.AntiAlias;
gr.DrawString(date, font, brush, 0, 0);
return bmp;
}
public override void OnThemeChanged()
{
themeChanged = true;
}
public override void OnSettingsChanged()
{
themeChanged = true;
}
[GeneratedRegex(@"d{3,4}")]
private static partial Regex WeekdayRegex();
[GeneratedRegex(@"\b(?<!d)d{1,2}\b")]
private static partial Regex DayRegex();
[GeneratedRegex(@"M{1,4}")]
private static partial Regex MonthRegex();
[GeneratedRegex(@"y{2,4}")]
private static partial Regex YearRegex();
[GeneratedRegex(@"[\s,./\-]+")]
private static partial Regex CleanupRegex();
[GeneratedRegex(@"(?<=\d)\s+(?=\d)")]
private static partial Regex DateSeparatorsRegex();
public enum WeekdayStyle { None, Short, Long }
public enum DayStyle { None, Numeric }
public enum MonthStyle { None, Numeric, Short, Long }
public enum YearStyle { None, TwoDigit, FourDigit }
}
@@ -9,7 +9,7 @@ using System.Collections.Generic;
using System.Drawing; using System.Drawing;
using System.Linq; using System.Linq;
namespace DesktopMagic.BuiltInWindowElements; namespace DesktopMagic.BuiltInPlugins;
internal class MusicVisualizerPlugin : Plugin internal class MusicVisualizerPlugin : Plugin
{ {
@@ -20,6 +20,11 @@ internal class MusicVisualizerPlugin : Plugin
private readonly Bitmap output = new Bitmap(880, 300); private readonly Bitmap output = new Bitmap(880, 300);
private readonly System.Diagnostics.Stopwatch frameStopwatch = System.Diagnostics.Stopwatch.StartNew();
[Setting("fps-limit", "FPS Limit")]
private readonly IntegerUpDown fpsLimit = new IntegerUpDown(1, 144, 60);
[Setting("mirror", "Mirror")] [Setting("mirror", "Mirror")]
private readonly CheckBox mirrorMode = new CheckBox(false); private readonly CheckBox mirrorMode = new CheckBox(false);
@@ -51,8 +56,16 @@ internal class MusicVisualizerPlugin : Plugin
waveIn.StartRecording(); waveIn.StartRecording();
} }
public override Bitmap Main() public override Bitmap? Main()
{ {
double msPerFrame = 1000.0 / Math.Max(1, fpsLimit.Value);
if (frameStopwatch.Elapsed.TotalMilliseconds < msPerFrame)
{
return null; // Exit early if we haven't reached the time threshold
}
frameStopwatch.Restart(); // Reset timer for the next frame
return output; return output;
} }
@@ -0,0 +1,233 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using DesktopMagic.Helpers;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Text;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.BuiltInPlugins;
internal class NextMeetingCountdownPlugin : AsyncPlugin
{
[Setting("meeting-calendar-url", "iCal URL")]
private readonly TextBox calendarUrl = new TextBox(string.Empty);
[Setting("meeting-refresh-button", "Refresh Calendar")]
private readonly Button refreshButton = new Button("Refresh");
[Setting("meeting-refresh-interval", "Refresh Every (minutes)")]
private readonly IntegerUpDown refreshMinutes = new IntegerUpDown(1, 120, 10);
[Setting("meeting-lookahead-hours", "Look Ahead (hours)")]
private readonly IntegerUpDown lookAheadHours = new IntegerUpDown(1, 336, 48);
[Setting("meeting-include-all-day", "Include All-Day Events")]
private readonly CheckBox includeAllDayEvents = new CheckBox(false);
[Setting("meeting-time-display", "Start Time Format")]
private readonly ComboBox startTimeDisplay = new ComboBox("None", "Date and time", "Time only");
[Setting("meeting-show-location", "Show Location")]
private readonly CheckBox showLocation = new CheckBox(false);
[Setting("meeting-center-lines", "Center Lines")]
private readonly CheckBox centerLines = new CheckBox(false);
public override int UpdateInterval { get; set; } = 1000;
private List<CalendarEventEntry> upcomingEvents = [];
private DateTime nextRefreshTime = DateTime.MinValue;
private bool forceRefresh = true;
private bool themeChanged = true;
private string? errorText;
public override Task StartAsync(CancellationToken cancellationToken)
{
refreshButton.OnClick += () =>
{
forceRefresh = true;
Application.UpdateWindow();
};
calendarUrl.OnValueChanged += MarkForRefresh;
refreshMinutes.OnValueChanged += MarkForRefresh;
lookAheadHours.OnValueChanged += MarkForRefresh;
includeAllDayEvents.OnValueChanged += MarkForRefresh;
startTimeDisplay.OnValueChanged += Application.UpdateWindow;
showLocation.OnValueChanged += Application.UpdateWindow;
centerLines.OnValueChanged += Application.UpdateWindow;
return Task.CompletedTask;
}
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
{
if (themeChanged || forceRefresh || DateTime.Now >= nextRefreshTime)
{
await RefreshEventsAsync(cancellationToken);
themeChanged = false;
}
return RenderCountdownBitmap();
}
public override void OnThemeChanged()
{
themeChanged = true;
}
public override void OnSettingsChanged()
{
Application.UpdateWindow();
}
private void MarkForRefresh()
{
forceRefresh = true;
Application.UpdateWindow();
}
private async Task RefreshEventsAsync(CancellationToken cancellationToken)
{
forceRefresh = false;
nextRefreshTime = DateTime.Now.AddMinutes(Math.Max(1, refreshMinutes.Value));
if (string.IsNullOrWhiteSpace(calendarUrl.Value))
{
upcomingEvents = [];
errorText = "Set iCal URL in settings";
return;
}
try
{
DateTime now = DateTime.Now;
DateTime windowEnd = now.AddHours(Math.Max(1, lookAheadHours.Value));
upcomingEvents = await IcsCalendarHelper.GetUpcomingEventsAsync(
calendarUrl.Value.Trim(),
now,
windowEnd,
20,
includeAllDayEvents.Value,
cancellationToken);
errorText = null;
}
catch (HttpRequestException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
catch (TaskCanceledException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
catch (InvalidOperationException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
}
private Bitmap RenderCountdownBitmap()
{
List<string> lines = [];
if (!string.IsNullOrWhiteSpace(errorText))
{
lines.Add(errorText);
}
else
{
DateTime now = DateTime.Now;
CalendarEventEntry? nextEvent = upcomingEvents.FirstOrDefault(eventEntry => eventEntry.EndLocal > now);
if (nextEvent is null)
{
lines.Add("No upcoming meetings");
}
else
{
lines.Add(nextEvent.Title);
if (startTimeDisplay.Value != "None")
{
string timeText = nextEvent.IsAllDay
? "All day"
: startTimeDisplay.Value == "Time only"
? nextEvent.StartLocal.ToString("t")
: $"{nextEvent.StartLocal:D} {nextEvent.StartLocal:t}";
lines.Add(timeText);
}
if (showLocation.Value && !string.IsNullOrWhiteSpace(nextEvent.Location))
{
lines.Add(nextEvent.Location!);
}
if (now < nextEvent.StartLocal)
{
lines.Add($"Starts in {FormatCountdown(nextEvent.StartLocal - now)}");
}
else
{
lines.Add("In progress");
}
}
}
using Font font = new Font(Application.Theme.Font, 90);
using Bitmap measureBitmap = new Bitmap(1, 1);
using Graphics measureGraphics = Graphics.FromImage(measureBitmap);
measureGraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
float maxWidth = 0;
float lineHeight = 0;
foreach (string line in lines)
{
SizeF size = measureGraphics.MeasureString(line, font);
maxWidth = Math.Max(maxWidth, size.Width);
lineHeight = Math.Max(lineHeight, size.Height);
}
int width = Math.Max(1, (int)Math.Ceiling(maxWidth));
int height = Math.Max(1, (int)Math.Ceiling(lineHeight * lines.Count));
Bitmap bitmap = new Bitmap(width, height);
bitmap.SetResolution(100, 100);
using Graphics graphics = Graphics.FromImage(bitmap);
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
graphics.Clear(Color.Transparent);
for (int index = 0; index < lines.Count; index++)
{
string line = lines[index];
float x = 0;
if (centerLines.Value)
{
SizeF lineSize = graphics.MeasureString(line, font);
x = Math.Max(0, (width - lineSize.Width) / 2f);
}
graphics.DrawString(line, font, brush, x, index * lineHeight);
}
return bitmap;
}
private static string FormatCountdown(TimeSpan timeSpan)
{
int totalHours = (int)timeSpan.TotalHours;
return $"{totalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}";
}
}
@@ -5,20 +5,31 @@ using System;
using System.Drawing; using System.Drawing;
using System.Drawing.Text; using System.Drawing.Text;
namespace DesktopMagic.BuiltInWindowElements; namespace DesktopMagic.BuiltInPlugins;
internal class TimePlugin : Plugin internal class TimePlugin : Plugin
{ {
[Setting("display-seconds", "Display Seconds")] [Setting("display-seconds", "Show Seconds")]
private readonly CheckBox displaySecondsCheckBox = new CheckBox(true); private readonly CheckBox displaySecondsCheckBox = new CheckBox(true);
private string oldTime = string.Empty;
private bool themeChanged;
public override int UpdateInterval => 1000; public override int UpdateInterval => 1000;
public override Bitmap Main() public override Bitmap? Main()
{ {
string time = displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString(); string time = displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString();
Font font = new Font(Application.Theme.Font, 200); if (oldTime == time && !themeChanged)
{
return null;
}
oldTime = time;
themeChanged = false;
using Font font = new Font(Application.Theme.Font, 200);
Bitmap bmp = new Bitmap(1, 1); Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100); bmp.SetResolution(100, 100);
@@ -31,10 +42,21 @@ internal class TimePlugin : Plugin
bmp.SetResolution(100, 100); bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp); using Graphics gr = Graphics.FromImage(bmp);
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
gr.TextRenderingHint = TextRenderingHint.AntiAlias; gr.TextRenderingHint = TextRenderingHint.AntiAlias;
gr.DrawString(time, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0); gr.DrawString(time, font, brush, 0, 0);
return bmp; return bmp;
} }
public override void OnThemeChanged()
{
themeChanged = true;
}
public override void OnSettingsChanged()
{
themeChanged = true;
}
} }
@@ -0,0 +1,208 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.BuiltInPlugins;
public class WeatherPlugin : AsyncPlugin
{
[Setting("city-name", "City Name")]
private readonly TextBox cityInput = new TextBox("Tokyo");
[Setting("search-btn", "Update Location")]
private readonly Button searchButton = new Button("Update Location");
[Setting("show-city", "Show City Name")]
private readonly CheckBox showCity = new CheckBox(true);
[Setting("show-temp", "Show Temperature")]
private readonly CheckBox showTemp = new CheckBox(true);
[Setting("show-status", "Show Weather Status")]
private readonly CheckBox showStatus = new CheckBox(true);
[Setting("show-time", "Show Last Updated")]
private readonly CheckBox showTime = new CheckBox(false);
[Setting("font-size", "Base Font Size")]
private readonly Slider fontSizeSlider = new Slider(10, 100, 30);
public override int UpdateInterval { get; set; } = 900000; // 15 Minutes
private readonly HttpClient httpClient = new HttpClient();
private string? cachedLat;
private string? cachedLon;
private string currentTemp = "--";
private string weatherStatus = "Enter city and search";
private string foundCityName = "No City Selected";
private string lastUpdated = "";
private bool isLoading = false;
public override async Task StartAsync(CancellationToken cancellationToken)
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (DesktopMagic)");
// Setup Event Handlers for instant UI updates when settings change
showCity.OnValueChanged += Application.UpdateWindow;
showTemp.OnValueChanged += Application.UpdateWindow;
showStatus.OnValueChanged += Application.UpdateWindow;
showTime.OnValueChanged += Application.UpdateWindow;
fontSizeSlider.OnValueChanged += Application.UpdateWindow;
await UpdateLocationAndWeather();
Application.UpdateWindow();
searchButton.OnClick += () =>
{
isLoading = true;
Application.UpdateWindow();
_ = Task.Run(async () =>
{
await UpdateLocationAndWeather();
isLoading = false;
Application.UpdateWindow();
});
};
}
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
{
// Periodic background update
if (!isLoading && cachedLat != null && cachedLon != null)
{
await UpdateWeatherOnly(cachedLat, cachedLon);
}
Bitmap bmp = new Bitmap(1200, 800);
bmp.SetResolution(300, 300);
using (Graphics g = Graphics.FromImage(bmp))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
g.Clear(Color.Transparent);
int lineCount = 0;
int baseSize = (int)fontSizeSlider.Value;
if (isLoading)
{
DrawLine(bmp, g, "Fetching...", ref lineCount, baseSize, FontStyle.Italic);
}
else
{
if (showCity.Value)
{
DrawLine(bmp, g, foundCityName.ToUpper(), ref lineCount, baseSize, FontStyle.Bold);
}
if (showTemp.Value)
{
DrawLine(bmp, g, currentTemp, ref lineCount, baseSize, FontStyle.Bold);
}
if (showStatus.Value)
{
DrawLine(bmp, g, weatherStatus, ref lineCount, baseSize, FontStyle.Regular);
}
if (showTime.Value && !string.IsNullOrEmpty(lastUpdated))
{
DrawLine(bmp, g, $"Updated: {lastUpdated}", ref lineCount, baseSize, FontStyle.Italic);
}
}
}
return bmp;
}
private void DrawLine(Bitmap bitmap, Graphics graphics, string text, ref int lineCount, int fontSize, FontStyle style)
{
using Font font = new Font(Application.Theme.Font, fontSize, style);
SizeF textSize = graphics.MeasureString(text, font);
float x = (bitmap.Width - textSize.Width) / 2;
// Adjust vertical spacing based on font size
float y = (lineCount == 0) ? 60 : (lineCount * (textSize.Height + 10)) + 60;
using Brush brush = new SolidBrush(Application.Theme.PrimaryColor);
graphics.DrawString(text, font, brush, x, y);
lineCount++;
}
private async Task UpdateLocationAndWeather()
{
try
{
string geoUrl = $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(cityInput.Value.Trim())}&count=1&language=en&format=json";
string geoJson = await httpClient.GetStringAsync(geoUrl);
using JsonDocument geoDoc = JsonDocument.Parse(geoJson);
if (!geoDoc.RootElement.TryGetProperty("results", out JsonElement results) || results.GetArrayLength() == 0)
{
foundCityName = cityInput.Value;
currentTemp = "--";
weatherStatus = "City not found";
return;
}
JsonElement location = results[0];
cachedLat = location.GetProperty("latitude").GetDouble().ToString(CultureInfo.InvariantCulture);
cachedLon = location.GetProperty("longitude").GetDouble().ToString(CultureInfo.InvariantCulture);
foundCityName = location.GetProperty("name").GetString() ?? cityInput.Value;
// We update the textbox to the "official" name found by the API
cityInput.Value = foundCityName;
await UpdateWeatherOnly(cachedLat, cachedLon);
}
catch (Exception ex)
{
Debug.WriteLine($"Error: {ex.Message}");
weatherStatus = "Connection Error";
}
}
private async Task UpdateWeatherOnly(string lat, string lon)
{
try
{
string weatherUrl = $"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current_weather=true";
string weatherJson = await httpClient.GetStringAsync(weatherUrl);
using JsonDocument weatherDoc = JsonDocument.Parse(weatherJson);
JsonElement current = weatherDoc.RootElement.GetProperty("current_weather");
currentTemp = $"{current.GetProperty("temperature").GetDouble()}°C";
weatherStatus = MapWeatherCode(current.GetProperty("weathercode").GetInt32());
lastUpdated = DateTime.Now.ToString("HH:mm");
}
catch { /* Ignore background update errors */ }
}
private string MapWeatherCode(int code)
{
return code switch
{
0 => "Clear Sky",
1 or 2 or 3 => "Partly Cloudy",
45 or 48 => "Foggy",
51 or 53 or 55 => "Drizzle",
61 or 63 or 65 => "Rainy",
71 or 73 or 75 => "Snowy",
95 or 96 or 99 => "Thunderstorm",
_ => "Cloudy"
};
}
}
@@ -1,54 +0,0 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using System;
using System.Drawing;
using System.Drawing.Text;
namespace DesktopMagic.BuiltInWindowElements;
internal class DatePlugin : Plugin
{
[Setting("short-date", "Short date")]
private readonly CheckBox shortDatecheckBox = new CheckBox(true);
private DateTime oldDateTime = DateTime.MinValue;
private Color oldColor = Color.White;
private string oldFont = string.Empty;
private bool oldShortDatecheckBoxValue;
public override int UpdateInterval => 1000;
public override Bitmap? Main()
{
if (oldDateTime.Date == DateTime.Now.Date && oldColor == Application.Theme.PrimaryColor && oldFont == Application.Theme.Font && oldShortDatecheckBoxValue == shortDatecheckBox.Value)
{
return null;
}
oldDateTime = DateTime.Now;
oldColor = Application.Theme.PrimaryColor;
oldFont = Application.Theme.Font;
oldShortDatecheckBoxValue = shortDatecheckBox.Value;
string date = shortDatecheckBox.Value ? DateTime.Now.ToShortDateString() : DateTime.Now.ToLongDateString();
Font font = new Font(Application.Theme.Font, 200);
Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100);
using Graphics tmpGr = Graphics.FromImage(bmp);
tmpGr.TextRenderingHint = TextRenderingHint.AntiAlias;
SizeF size = tmpGr.MeasureString(date, font);
bmp = new Bitmap((int)size.Width, (int)size.Height);
bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp);
gr.TextRenderingHint = TextRenderingHint.AntiAlias;
gr.DrawString(date, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
return bmp;
}
}
@@ -1,4 +1,5 @@
using DesktopMagic.Settings; using DesktopMagic.Helpers;
using DesktopMagic.Settings;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -10,7 +11,18 @@ internal class MainWindowDataContext : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private static DesktopMagicSettings settings = new(); private static DesktopMagicSettings settings = new();
private bool isLoading = true; private bool isLoading = true;
private string? pluginsSearchText;
public string Title =>
#if DEBUG
$"{App.AppName} - Dev {System.Windows.Forms.Application.ProductVersion}";
#else
$"{App.AppName} - {System.Windows.Forms.Application.ProductVersion}";
#endif
public string AppName => App.AppName;
public DesktopMagicSettings Settings public DesktopMagicSettings Settings
{ {
@@ -32,6 +44,33 @@ internal class MainWindowDataContext : INotifyPropertyChanged
} }
} }
public string? PluginsSearchText
{
get => pluginsSearchText;
set
{
pluginsSearchText = value;
OnPropertyChanged();
}
}
public bool IsAutoStartEnabled
{
get => StartupManager.IsAutoStartEnabled();
set
{
if (value)
{
_ = StartupManager.EnableAutoStart();
}
else
{
_ = StartupManager.DisableAutoStart();
}
OnPropertyChanged();
}
}
public static DesktopMagicSettings GetSettings() public static DesktopMagicSettings GetSettings()
{ {
return settings; return settings;
@@ -1,14 +1,19 @@
using DesktopMagic.Plugins; using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using Wpf.Ui.Controls;
namespace DesktopMagic.DataContexts; namespace DesktopMagic.DataContexts;
internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand command, bool installed = false) : INotifyPropertyChanged internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand command, PluginEntryDataContext.Mode mode, string? path = null, string? csprojPath = null) : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@@ -16,20 +21,58 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
public string Name => pluginMetadata.Name; public string Name => pluginMetadata.Name;
public string? Description => pluginMetadata.Description; public string? Description => pluginMetadata.Description?.Trim();
public string Author => pluginMetadata.Author ?? (string)App.LanguageDictionary["unknown"];
public string? Version => pluginMetadata.Version ?? (string)App.LanguageDictionary["unknown"];
public string? Logo => pluginMetadata.IconUri?.ToString(); public string? Logo => pluginMetadata.IconUri?.ToString();
public DateTime? FormattedDateAdded => pluginMetadata.Added; public DateTime? DateAdded => pluginMetadata.Added;
public DateTime? FormattedDateUpdated => pluginMetadata.Updated; public DateTime? DateUpdated => pluginMetadata.Updated;
public uint Id => pluginMetadata.Id; public uint Id => pluginMetadata.Id;
public string? Path => path;
public bool IsLocalPlugin => pluginMetadata.IsLocalPlugin;
public ICommand Command => command; public ICommand Command => command;
public Visibility InstallButtonVisibility => installed ? Visibility.Collapsed : Visibility.Visible; public ButtonData InstallUninstallButtonData
{
get
{
if (mode == Mode.Install)
{
return new(new SymbolIcon(SymbolRegular.ArrowDownload24), GetInstallUninstallButtonText(), true, Command);
}
else
{
return new(new SymbolIcon(SymbolRegular.Delete24), GetInstallUninstallButtonText(), true, Command);
}
}
}
public Visibility RemoveButtonVisibility => installed ? Visibility.Visible : Visibility.Collapsed; public ButtonData OpenButtonData
{
get
{
if (File.Exists(csprojPath))
{
return new(new SymbolIcon(SymbolRegular.Code24), "IDE", true, new CommandHandler(OpenCsprojInIDE));
}
else if (string.IsNullOrWhiteSpace(pluginMetadata.ProfileUri?.ToString()))
{
return new(new SymbolIcon(SymbolRegular.Folder24), (string)App.LanguageDictionary["folder"], path is not null, new CommandHandler(() => Process.Start("explorer.exe", path!)));
}
else
{
return new(new SymbolIcon(SymbolRegular.Open24), "mod.io", true, new CommandHandler(OpenModIoPage));
}
}
}
public bool IsVisible public bool IsVisible
{ {
@@ -44,8 +87,65 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
public Visibility Visibility => IsVisible ? Visibility.Visible : Visibility.Collapsed; public Visibility Visibility => IsVisible ? Visibility.Visible : Visibility.Collapsed;
private void OpenModIoPage()
{
ProcessStartInfo psi = new ProcessStartInfo
{
UseShellExecute = true,
FileName = pluginMetadata.ProfileUri?.ToString() ?? path
};
_ = Process.Start(psi);
}
private void OpenCsprojInIDE()
{
string? associatedProgram = FileUtilities.GetAssociatedProgram(".csproj");
string pluginProjectPath = System.IO.Path.GetDirectoryName(csprojPath)!;
if (string.IsNullOrWhiteSpace(pluginProjectPath))
{
App.Logger.LogError("Could not determine plugin project directory.", source: "PluginManager");
return;
}
if (associatedProgram is null)
{
App.Logger.LogInfo($"Opening plugin project in Explorer: {pluginProjectPath}", source: "PluginManager");
_ = Process.Start("explorer.exe", pluginProjectPath);
return;
}
App.Logger.LogInfo($"Opening plugin project in IDE: {associatedProgram}", source: "PluginManager");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = associatedProgram,
Arguments = csprojPath,
};
_ = Process.Start(psi);
}
private string GetInstallUninstallButtonText()
{
return mode switch
{
Mode.Install => (string)App.LanguageDictionary["install"],
Mode.Uninstall => (string)App.LanguageDictionary["uninstall"],
_ => throw new NotImplementedException()
};
}
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) private void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
} }
public record ButtonData(SymbolIcon Icon, string Text, bool IsEnabled, ICommand Command);
public enum Mode
{
Install,
Uninstall
}
} }
@@ -1,6 +1,10 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media.Imaging;
using Wpf.Ui.Appearance;
namespace DesktopMagic.DataContexts; namespace DesktopMagic.DataContexts;
@@ -12,6 +16,7 @@ internal class PluginManagerDataContext : INotifyPropertyChanged
private string installedPluginsSearchText = string.Empty; private string installedPluginsSearchText = string.Empty;
private bool isLoading = true; private bool isLoading = true;
private bool isSearching; private bool isSearching;
private bool isAuthenticated = false;
public ObservableCollection<PluginEntryDataContext> AllPlugins { get; } = []; public ObservableCollection<PluginEntryDataContext> AllPlugins { get; } = [];
public ObservableCollection<PluginEntryDataContext> InstalledPlugins { get; } = []; public ObservableCollection<PluginEntryDataContext> InstalledPlugins { get; } = [];
@@ -58,6 +63,36 @@ internal class PluginManagerDataContext : INotifyPropertyChanged
} }
} }
public bool IsAuthenticated
{
get => isAuthenticated;
set
{
isAuthenticated = value;
OnPropertyChanged();
OnPropertyChanged(nameof(LoginButtonText));
}
}
public BitmapImage ModIoIcon
{
get
{
ApplicationTheme theme = ApplicationThemeManager.GetAppTheme();
if (theme == ApplicationTheme.Light)
{
return Application.Current.Resources["ModioLogoBlueDark"] as BitmapImage ?? new BitmapImage();
}
else
{
return Application.Current.Resources["ModioLogoBlueLight"] as BitmapImage ?? new BitmapImage();
}
}
}
public string LoginButtonText => isAuthenticated ? (string)App.LanguageDictionary["logout"] : (string)App.LanguageDictionary["login"];
protected void OnPropertyChanged([CallerMemberName] string? name = null) protected void OnPropertyChanged([CallerMemberName] string? name = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
+27 -8
View File
@@ -7,26 +7,34 @@
<ApplicationIcon>icon.ico</ApplicationIcon> <ApplicationIcon>icon.ico</ApplicationIcon>
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent> <RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
<Authors>Stone_Red</Authors> <Authors>Stone_Red</Authors>
<Version>1.0.1.240501</Version> <Version>1.3.3.0</Version>
<PackageProjectUrl>https://github.com/Stone-Red-Code/DesktopMagic</PackageProjectUrl> <PackageProjectUrl>https://github.com/Stone-Red-Code/DesktopMagic</PackageProjectUrl>
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl> <RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
<AssemblyVersion>1.0.1.0501</AssemblyVersion> <AssemblyVersion>1.3.3.0</AssemblyVersion>
<FileVersion>1.0.1.0501</FileVersion> <FileVersion>1.3.3.0</FileVersion>
<TargetFramework>net8.0-windows7.0</TargetFramework> <TargetFramework>net8.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>DEBUG;TRACE</DefineConstants> <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<None Remove="icon.ico" /> <None Remove="icon.ico" />
<None Remove="icons8_volunteering_26_85L_icon.ico" /> <None Remove="icons8_volunteering_26_85L_icon.ico" />
<None Remove="icon_Dark.ico" /> <None Remove="icon_Dark.ico" />
<None Remove="Resources\Images\modio-cog-blue.png" /> <None Remove="Resources\Images\modio-cog-blue.png" />
<None Remove="Resources\Images\modio-logo-bluedark.png" /> <None Remove="Resources\Images\modio-logo-bluedark.png" />
<None Remove="Resources\Images\modio-logo-bluelight.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -36,13 +44,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BusyIndicators" Version="2.1.2" /> <PackageReference Include="BusyIndicators" Version="2.1.3" />
<PackageReference Include="CuteUtils" Version="1.0.0" /> <PackageReference Include="CuteUtils" Version="1.0.0" />
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.6.0" /> <PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.0.0" /> <PackageReference Include="Ical.Net" Version="4.3.1" />
<PackageReference Include="Modio.NET" Version="1.1.1" /> <PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
<PackageReference Include="Modio" Version="1.0.0" />
<PackageReference Include="NAudio" Version="2.2.1" /> <PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="System.Management" Version="8.0.0" /> <PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="WPF-UI" Version="4.2.0" />
<PackageReference Include="WPF-UI.Markdown" Version="4.0.2" />
<PackageReference Include="WPF-UI.Tray" Version="4.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -57,5 +69,12 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource> </Resource>
<Resource Include="Resources\Images\modio-logo-bluedark.png" /> <Resource Include="Resources\Images\modio-logo-bluedark.png" />
<Resource Include="Resources\Images\modio-logo-bluelight.png" />
</ItemGroup> </ItemGroup>
</Project>
<ItemGroup>
<None Update="AutoStart.ps1">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
+65 -40
View File
@@ -1,49 +1,74 @@
<Window x:Class="DesktopMagic.Dialogs.ColorDialog" <ui:FluentWindow x:Class="DesktopMagic.Dialogs.ColorDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
xmlns:local="clr-namespace:DesktopMagic.Dialogs" d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
mc:Ignorable="d" ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ResizeMode="NoResize" ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Title="ColorDialog" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
SizeToContent="WidthAndHeight"> WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="300"
Width="450"
MinHeight="300"
MinWidth="450"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
<Grid> <Grid>
<StackPanel Margin="15"> <Grid.RowDefinitions>
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top"/> <RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<DockPanel> <ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
<Label Content="A:" Width="20" /> <ui:TitleBar.Icon>
<Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#00030000" /> <ui:ImageIcon Source="{StaticResource Icon}" />
</DockPanel> </ui:TitleBar.Icon>
<DockPanel> </ui:TitleBar>
<Label Content="R:" Width="20" />
<Slider x:Name="redSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#00030000" />
</DockPanel>
<DockPanel>
<Label Content="G:" Width="20" />
<Slider x:Name="greenSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#0000FF00" />
</DockPanel>
<DockPanel>
<Label Content="B:" Width="20" />
<Slider x:Name="blueSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#000000FF" />
</DockPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Grid.Row="1" Margin="15">
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0"> <Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
<Rectangle x:Name="colorRechtangle" Fill="White" /> <Grid Margin="{StaticResource DefaultMargin}">
</Border> <Grid.RowDefinitions>
<TextBox x:Name="colorHexTextBox" MaxLength="9" CharacterCasing="Upper" Height="23" TextWrapping="Wrap" Width="350" TextChanged="ColorHexTextBox_TextChanged" RenderTransformOrigin="0.5,0.5" VerticalContentAlignment="Center"> <RowDefinition Height="*"/>
<materialDesign:TextFieldAssist.CharacterCounterStyle> <RowDefinition Height="*"/>
<Style TargetType="TextBlock" /> <RowDefinition Height="*"/>
</materialDesign:TextFieldAssist.CharacterCounterStyle> <RowDefinition Height="*"/>
</TextBox> </Grid.RowDefinitions>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0"> <DockPanel Grid.Row="0">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0,0,10,0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand"/> <Label Content="A:" Width="20" />
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click"/> <Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</StackPanel> </DockPanel>
<DockPanel Grid.Row="1">
<Label Content="R:" Width="20" />
<Slider x:Name="redSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</DockPanel>
<DockPanel Grid.Row="2">
<Label Content="G:" Width="20" />
<Slider x:Name="greenSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</DockPanel>
<DockPanel Grid.Row="3">
<Label Content="B:" Width="20" />
<Slider x:Name="blueSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</DockPanel>
</Grid>
<DockPanel Margin="0 5 0 0">
<Border x:Name="colorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<TextBox x:Name="colorHexTextBox" MaxLength="9" CharacterCasing="Upper" TextWrapping="Wrap" TextChanged="ColorHexTextBox_TextChanged" RenderTransformOrigin="0.5,0.5" VerticalContentAlignment="Center"/>
</DockPanel>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Window> </ui:FluentWindow>
+11 -24
View File
@@ -1,28 +1,31 @@
using DesktopMagic.Helpers; using DesktopMagic.Helpers;
using System;
using System.Threading;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
using Wpf.Ui.Controls;
namespace DesktopMagic.Dialogs; namespace DesktopMagic.Dialogs;
/// <summary> /// <summary>
/// Interaction logic for ColorDialog.xaml /// Interaction logic for ColorDialog.xaml
/// </summary> /// </summary>
public partial class ColorDialog : Window public partial class ColorDialog : FluentWindow
{ {
public System.Drawing.Color ResultColor { get; private set; } public System.Drawing.Color ResultColor { get; private set; }
public Brush ResultBrush { get; private set; } = Brushes.Transparent; public Brush ResultBrush { get; private set; } = Brushes.Transparent;
public ColorDialog(string content, System.Drawing.Color defaultColor, string title = "ColorDialog") public ColorDialog(string content, System.Drawing.Color defaultColor, string title = App.AppName)
{ {
InitializeComponent(); InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
label.Content = content; label.Content = content;
titleBar.Title = title;
Title = title; Title = title;
SetLanguageDictionary();
alphaSlider.Value = defaultColor.A; alphaSlider.Value = defaultColor.A;
redSlider.Value = defaultColor.R; redSlider.Value = defaultColor.R;
@@ -86,8 +89,8 @@ public partial class ColorDialog : Window
ResultBrush = brush; ResultBrush = brush;
ResultColor = systemColor; ResultColor = systemColor;
colorRechtangle.Fill = brush; colorRechtangle.Background = brush;
colorHexTextBox.Foreground = Brushes.Black; colorHexTextBox.Foreground = FindResource("TextFillColorPrimaryBrush") as Brush;
} }
else else
{ {
@@ -97,7 +100,7 @@ public partial class ColorDialog : Window
private void SetColorText() private void SetColorText()
{ {
if (alphaSlider.Value == 255) if ((int)alphaSlider.Value == 255)
{ {
colorHexTextBox.Text = "#"; colorHexTextBox.Text = "#";
} }
@@ -108,20 +111,4 @@ public partial class ColorDialog : Window
colorHexTextBox.Text += ((int)redSlider.Value).ToString("X2") + ((int)greenSlider.Value).ToString("X2") + ((int)blueSlider.Value).ToString("X2"); colorHexTextBox.Text += ((int)redSlider.Value).ToString("X2") + ((int)greenSlider.Value).ToString("X2") + ((int)blueSlider.Value).ToString("X2");
} }
private void SetLanguageDictionary()
{
ResourceDictionary dict = [];
string currentCulture = Thread.CurrentThread.CurrentCulture.ToString();
if (currentCulture.Contains("de"))
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.de.xaml", UriKind.Relative);
}
else
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.en.xaml", UriKind.Relative);
}
Resources.MergedDictionaries.Add(dict);
}
} }
@@ -0,0 +1,56 @@
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.CreatePluginDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="230"
Width="320"
MinHeight="160"
MinWidth="320"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar Title="Create New Plugin" ShowMinimize="False" ShowMaximize="False">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="{StaticResource Icon}" />
</ui:TitleBar.Icon>
</ui:TitleBar>
<StackPanel Grid.Row="1" Margin="{StaticResource DefaultMargin}" Orientation="Horizontal" HorizontalAlignment="Center">
<ui:TextBlock TextWrapping="Wrap" VerticalAlignment="Center" Foreground="{DynamicResource TextFillColorSecondaryBrush}">
Please check out the
</ui:TextBlock>
<ui:HyperlinkButton Padding="0" Content=" Plugin Creation Guide" VerticalAlignment="Center" VerticalContentAlignment="Center" NavigateUri="https://mod.io/g/desktopmagic/r/plugin-creation-guide/" />
</StackPanel>
<StackPanel Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Stretch" Margin="15">
<Label Content="{DynamicResource enterPluginName}" HorizontalAlignment="Left" VerticalAlignment="Top" />
<ui:TextBox x:Name="textBox" HorizontalAlignment="Stretch" Margin="5" TextWrapping="Wrap" VerticalAlignment="Top" VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
<Button x:Name="okButton" Content=".NET Plugin" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="DotNetButton_Click" Cursor="Hand" />
<Button x:Name="webPluginButton" Content="Web Plugin" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="WebPluginButton_Click" Cursor="Hand" />
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</ui:FluentWindow>
@@ -0,0 +1,38 @@
using System.Windows;
namespace DesktopMagic.Dialogs;
public partial class CreatePluginDialog : Wpf.Ui.Controls.FluentWindow
{
public string ResponseText
{
get => textBox.Text;
set => textBox.Text = value;
}
public bool IsWebPlugin { get; private set; }
public CreatePluginDialog()
{
InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
}
private void DotNetButton_Click(object sender, RoutedEventArgs e)
{
IsWebPlugin = false;
DialogResult = true;
}
private void WebPluginButton_Click(object sender, RoutedEventArgs e)
{
IsWebPlugin = true;
DialogResult = true;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
+40 -14
View File
@@ -1,21 +1,47 @@
<Window x:Class="DesktopMagic.Dialogs.InputDialog" <ui:FluentWindow x:Class="DesktopMagic.Dialogs.InputDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopMagic.Dialogs" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
Title="InputDialog" ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ResizeMode="NoResize" ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
SizeToContent="WidthAndHeight"> Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="200"
Width="300"
MinHeight="160"
MinWidth="300"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
<Grid> <Grid>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Margin="15"> <Grid.RowDefinitions>
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top"/> <RowDefinition Height="Auto" />
<TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="280" VerticalContentAlignment="Center"/> <RowDefinition Height="*" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0"> <RowDefinition Height="Auto" />
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0,0,10,0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand"/> </Grid.RowDefinitions>
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0,0,0,0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click"/>
</StackPanel> <ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="{StaticResource Icon}" />
</ui:TitleBar.Icon>
</ui:TitleBar>
<StackPanel Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Stretch" Margin="15">
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
<ui:TextBox x:Name="textBox" HorizontalAlignment="Stretch" Margin="5" TextWrapping="Wrap" VerticalAlignment="Top" VerticalContentAlignment="Center" />
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Window> </ui:FluentWindow>
+7 -22
View File
@@ -1,10 +1,8 @@
using System; using System.Windows;
using System.Threading;
using System.Windows;
namespace DesktopMagic.Dialogs; namespace DesktopMagic.Dialogs;
public partial class InputDialog : Window public partial class InputDialog : Wpf.Ui.Controls.FluentWindow
{ {
public string ResponseText public string ResponseText
{ {
@@ -12,12 +10,15 @@ public partial class InputDialog : Window
set => textBox.Text = value; set => textBox.Text = value;
} }
public InputDialog(string content, string title = "InputDialog") public InputDialog(string content, string title = App.AppName)
{ {
InitializeComponent(); InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
label.Content = content; label.Content = content;
titleBar.Title = title;
Title = title; Title = title;
SetLanguageDictionary();
} }
private void OkButton_Click(object sender, RoutedEventArgs e) private void OkButton_Click(object sender, RoutedEventArgs e)
@@ -29,20 +30,4 @@ public partial class InputDialog : Window
{ {
DialogResult = false; DialogResult = false;
} }
private void SetLanguageDictionary()
{
ResourceDictionary dict = [];
string currentCulture = Thread.CurrentThread.CurrentCulture.ToString();
if (currentCulture.Contains("de"))
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.de.xaml", UriKind.Relative);
}
else
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.en.xaml", UriKind.Relative);
}
Resources.MergedDictionaries.Add(dict);
}
} }
@@ -0,0 +1,48 @@
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ReleaseInfoDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:md="http://schemas.lepo.co/wpfui/2022/xaml/markdown"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="700"
Width="900"
MinHeight="500"
MinWidth="700"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="{StaticResource Icon}" />
</ui:TitleBar.Icon>
</ui:TitleBar>
<StackPanel Grid.Row="1" Margin="14,10,14,0">
<TextBlock x:Name="releaseNameTextBlock" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="publishedAtTextBlock" Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<Border Grid.Row="2" Margin="14,12,14,12" BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8">
<md:MarkdownViewer x:Name="releaseMarkdownViewer" Margin="8" />
</Border>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="14,0,14,12">
<Button x:Name="openButton" Width="150" Margin="0,0,8,0" Click="OpenButton_Click" />
<Button x:Name="okButton" Width="100" Click="OkButton_Click" />
</StackPanel>
</Grid>
</ui:FluentWindow>
@@ -0,0 +1,40 @@
using System.Windows;
namespace DesktopMagic.Dialogs;
public partial class ReleaseInfoDialog : Wpf.Ui.Controls.FluentWindow
{
public bool OpenReleaseRequested { get; private set; }
public ReleaseInfoDialog(
string title,
string releaseName,
string publishedAt,
string markdown,
string openButtonText,
string okButtonText)
{
InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
titleBar.Title = title;
Title = title;
releaseNameTextBlock.Text = releaseName;
publishedAtTextBlock.Text = publishedAt;
releaseMarkdownViewer.Markdown = string.IsNullOrWhiteSpace(markdown) ? "-" : markdown;
openButton.Content = openButtonText;
okButton.Content = okButtonText;
}
private void OpenButton_Click(object sender, RoutedEventArgs e)
{
OpenReleaseRequested = true;
DialogResult = true;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
+92
View File
@@ -0,0 +1,92 @@
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ThemeDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="290"
Width="550"
MinHeight="290"
MinWidth="550"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="{StaticResource Icon}" />
</ui:TitleBar.Icon>
</ui:TitleBar>
<StackPanel Grid.Row="1" Margin="15">
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Display Font: " Grid.Column="0" Grid.Row="0" VerticalAlignment="Center" Loaded="TextBlock_Loaded"/>
<ComboBox x:Name="fontComboBox" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" SelectionChanged="FontComboBox_SelectionChanged" Margin="0,0,0,5" />
<TextBlock Text="Corner Radius: " Grid.Column="0" Grid.Row="1" VerticalAlignment="Center"></TextBlock>
<ui:NumberBox x:Name="cornerRadiusTextBox" Minimum="0" Grid.Column="1" Grid.Row="1" ValueChanged="CornerRadiusTextBox_ValueChanged" PlaceholderText="Enter a number" Margin="0,0,0,5" />
<TextBlock Text="Margin: " Grid.Column="0" Grid.Row="3" VerticalAlignment="Center"></TextBlock>
<ui:NumberBox x:Name="marginTextBox" Minimum="0" Grid.Column="1" Grid.Row="3" ValueChanged="MarginTextBox_ValueChanged" PlaceholderText="Enter a number"/>
</Grid>
<Grid Grid.Column="1" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0">
<Border x:Name="primaryColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Button Content="Change Primary Color" FontWeight="Regular" Click="ChangePrimaryColorButton_Click" HorizontalAlignment="Stretch" />
</DockPanel>
<DockPanel Grid.Row="1">
<Border x:Name="secondaryColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Button Content="Change Secondary Color" FontWeight="Regular" Click="ChangeSecondaryColorButton_Click" HorizontalAlignment="Stretch" />
</DockPanel>
<DockPanel Grid.Row="2">
<Border x:Name="backgroundColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Button Content="Change Background Color" FontWeight="Regular" Click="ChangeBackgroundColorButton_Click" HorizontalAlignment="Stretch" />
</DockPanel>
</Grid>
</Grid>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</ui:FluentWindow>
@@ -0,0 +1,132 @@
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace DesktopMagic.Dialogs;
/// <summary>
/// Interaction logic for ColorDialog.xaml
/// </summary>
public partial class ThemeDialog : Wpf.Ui.Controls.FluentWindow
{
private readonly Theme theme;
public ThemeDialog(string content, Theme theme, string title = App.AppName)
{
this.theme = theme;
InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
cornerRadiusTextBox.Value = theme.CornerRadius;
marginTextBox.Value = theme.Margin;
primaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor));
secondaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.SecondaryColor));
backgroundColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor));
label.Content = content;
titleBar.Title = title;
Title = title;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
private void TextBlock_Loaded(object sender, RoutedEventArgs e)
{
int index = 0;
foreach (FontFamily ff in Fonts.SystemFontFamilies)
{
ComboBoxItem comboBoxItem = new()
{
FontFamily = ff,
Content = ff.ToString()
};
_ = fontComboBox.Items.Add(comboBoxItem);
if (ff.ToString() == theme.Font)
{
fontComboBox.SelectedIndex = index;
}
index++;
}
if (fontComboBox.SelectedIndex == -1)
{
fontComboBox.SelectedIndex = 0;
}
}
private void FontComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
theme.Font = fontComboBox.SelectedValue.ToString()!.Replace("System.Windows.Controls.ComboBoxItem: ", "");
}
private void ChangePrimaryColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Primary Color", theme.PrimaryColor)
{
Owner = this
};
if (colorDialog.ShowDialog() == true)
{
theme.PrimaryColor = colorDialog.ResultColor;
primaryColorRechtangle.Background = colorDialog.ResultBrush;
}
}
private void ChangeSecondaryColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Secondary Color", theme.SecondaryColor)
{
Owner = this
};
if (colorDialog.ShowDialog() == true)
{
theme.SecondaryColor = colorDialog.ResultColor;
secondaryColorRechtangle.Background = colorDialog.ResultBrush;
}
}
private void ChangeBackgroundColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Background Color", theme.BackgroundColor)
{
Owner = this
};
if (colorDialog.ShowDialog() == true)
{
theme.BackgroundColor = colorDialog.ResultColor;
backgroundColorRechtangle.Background = colorDialog.ResultBrush;
}
}
private void MarginTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{
if (args.NewValue is >= 0)
{
theme.Margin = (int)args.NewValue;
}
}
private void CornerRadiusTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{
if (args.NewValue is >= 0)
{
theme.CornerRadius = (int)args.NewValue;
}
}
}
+4
View File
@@ -9,4 +9,8 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "No need to")] [assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "No need to")]
[assembly: SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "<Pending>")] [assembly: SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "<Pending>")]
[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.Settings")] [assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.Settings")]
[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.DialogService")]
[assembly: SuppressMessage("Major Code Smell", "S3885:\"Assembly.Load\" should be used", Justification = "Assembly.Load does not load dependencies", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.ExecuteSource")] [assembly: SuppressMessage("Major Code Smell", "S3885:\"Assembly.Load\" should be used", Justification = "Assembly.Load does not load dependencies", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.ExecuteSource")]
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "member", Target = "~M:DesktopMagic.MainWindow.OpenPluginsFolderButton_Click(System.Object,System.Windows.RoutedEventArgs)")]
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "member", Target = "~M:DesktopMagic.MainWindow.ScrollViewer_PreviewMouseWheel(System.Object,System.Windows.Input.MouseWheelEventArgs)")]
[assembly: SuppressMessage("Major Code Smell", "S6966:Awaitable method should be used", Justification = "Breaks stuff", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.UpdateTimer_Elapsed(System.Object,System.Timers.ElapsedEventArgs)")]
+36 -2
View File
@@ -6,16 +6,26 @@ namespace DesktopMagic.Helpers;
internal static partial class MultiColorConverter internal static partial class MultiColorConverter
{ {
public static string ConvertToHex(System.Drawing.Color color) public static string ConvertToHexArgb(System.Drawing.Color color)
{ {
return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}";
} }
public static string ConvertToHex(System.Windows.Media.Color color) public static string ConvertToHexArgb(System.Windows.Media.Color color)
{ {
return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}";
} }
public static string ConvertToHexRgba(System.Drawing.Color color)
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
}
public static string ConvertToHexRgba(System.Windows.Media.Color color)
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
}
public static bool TryConvertToSystemColor(string hex, out System.Drawing.Color color) public static bool TryConvertToSystemColor(string hex, out System.Drawing.Color color)
{ {
hex = hex.Replace("#", ""); hex = hex.Replace("#", "");
@@ -79,11 +89,35 @@ internal static partial class MultiColorConverter
return System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B); return System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B);
} }
public static System.Windows.Media.Color ConvertToMediaColor(string hex, System.Windows.Media.Color defaultColor)
{
if (TryConvertToMediaColor(hex, out System.Windows.Media.Color color))
{
return color;
}
else
{
return defaultColor;
}
}
public static System.Drawing.Color ConvertToSystemColor(System.Windows.Media.Color color) public static System.Drawing.Color ConvertToSystemColor(System.Windows.Media.Color color)
{ {
return System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); return System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B);
} }
public static System.Drawing.Color ConvertToSystemColor(string hex, System.Drawing.Color defaultColor)
{
if (TryConvertToSystemColor(hex, out System.Drawing.Color color))
{
return color;
}
else
{
return defaultColor;
}
}
[GeneratedRegex("(?:[0-9a-fA-F]{8})")] [GeneratedRegex("(?:[0-9a-fA-F]{8})")]
private static partial Regex Hex8(); private static partial Regex Hex8();
+93
View File
@@ -0,0 +1,93 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
namespace DesktopMagic.Helpers;
internal static class FileUtilities
{
public static bool ExistsOnPath(string fileName)
{
return GetFullPath(fileName) is not null;
}
public static string? GetFullPath(string fileName)
{
if (File.Exists(fileName))
{
return Path.GetFullPath(fileName);
}
string? values = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrEmpty(values))
{
return null;
}
foreach (string path in values.Split(Path.PathSeparator))
{
string fullPath = Path.Combine(path, fileName);
if (File.Exists(fullPath))
{
return fullPath;
}
}
return null;
}
public static bool HasAssociatedProgram(string extension)
{
return GetAssociatedProgram(extension) is not null;
}
public static string? GetAssociatedProgram(string extension)
{
uint pcchOut = 0;
_ = AssocQueryString(AssocF.Verify, AssocStr.Executable, extension, null, null, ref pcchOut);
StringBuilder pszOut = new StringBuilder((int)pcchOut);
_ = AssocQueryString(AssocF.Verify, AssocStr.Executable, extension, null, pszOut, ref pcchOut);
if (File.Exists(pszOut.ToString()))
{
return pszOut.ToString();
}
return null;
}
[DllImport("Shlwapi.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern uint AssocQueryString(AssocF flags, AssocStr str, string pszAssoc, string? pszExtra, [Out] StringBuilder? pszOut, [In][Out] ref uint pcchOut);
[Flags]
public enum AssocF
{
Init_NoRemapCLSID = 0x1,
Init_ByExeName = 0x2,
Open_ByExeName = 0x2,
Init_DefaultToStar = 0x4,
Init_DefaultToFolder = 0x8,
NoUserSettings = 0x10,
NoTruncate = 0x20,
Verify = 0x40,
RemapRunDll = 0x80,
NoFixUps = 0x100,
IgnoreBaseClass = 0x200
}
public enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDETopic
}
}
@@ -0,0 +1,51 @@
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace DesktopMagic.Helpers;
public sealed class GitHubLatestRelease
{
[JsonPropertyName("tag_name")]
public string TagName { get; init; } = "Unknown";
[JsonPropertyName("name")]
public string Name { get; init; } = "Unknown";
[JsonPropertyName("body")]
public string Body { get; init; } = string.Empty;
[JsonPropertyName("published_at")]
public DateTimeOffset? PublishedAt { get; init; }
[JsonPropertyName("html_url")]
public string HtmlUrl { get; init; } = "https://github.com/Stone-Red-Code/DesktopMagic/releases/latest";
}
public static class GitHubReleaseService
{
private const string LatestReleaseApiUrl = "https://api.github.com/repos/Stone-Red-Code/DesktopMagic/releases/latest";
private static readonly HttpClient _httpClient = CreateReleaseHttpClient();
private static HttpClient CreateReleaseHttpClient()
{
HttpClient client = new()
{
Timeout = TimeSpan.FromSeconds(10)
};
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
client.DefaultRequestHeaders.UserAgent.ParseAdd("DesktopMagic");
return client;
}
public static async Task<GitHubLatestRelease?> GetLatestReleaseInfoAsync()
{
using HttpResponseMessage response = await _httpClient.GetAsync(LatestReleaseApiUrl);
response.EnsureSuccessStatusCode();
await using var responseStream = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync<GitHubLatestRelease>(responseStream);
}
}
@@ -0,0 +1,89 @@
using Ical.Net;
using Ical.Net.CalendarComponents;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.Helpers;
public sealed class CalendarEventEntry
{
public string Title { get; init; } = "Untitled";
public string? Location { get; init; }
public DateTime StartLocal { get; init; }
public DateTime EndLocal { get; init; }
public bool IsAllDay { get; init; }
}
public static class IcsCalendarHelper
{
private static readonly HttpClient httpClient = new()
{
Timeout = TimeSpan.FromSeconds(12)
};
static IcsCalendarHelper()
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("DesktopMagic");
httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/calendar");
}
public static async Task<List<CalendarEventEntry>> GetUpcomingEventsAsync(
string calendarUrl,
DateTime windowStartLocal,
DateTime windowEndLocal,
int maxEvents,
bool includeAllDayEvents,
CancellationToken cancellationToken)
{
using HttpResponseMessage response = await httpClient.GetAsync(calendarUrl, cancellationToken);
response.EnsureSuccessStatusCode();
string icsContent = await response.Content.ReadAsStringAsync(cancellationToken);
Calendar calendar = Calendar.Load(icsContent);
DateTime windowStartUtc = windowStartLocal.ToUniversalTime();
DateTime windowEndUtc = windowEndLocal.ToUniversalTime();
List<CalendarEventEntry> results = [];
foreach (var occurrence in calendar.GetOccurrences(windowStartUtc, windowEndUtc))
{
if (occurrence.Source is not CalendarEvent calendarEvent)
{
continue;
}
DateTime startLocal = occurrence.Period.StartTime.AsSystemLocal;
DateTime endLocal = occurrence.Period.EndTime?.AsSystemLocal ?? startLocal.AddHours(1);
bool isAllDay = !occurrence.Period.StartTime.HasTime;
if (!includeAllDayEvents && isAllDay)
{
continue;
}
if (endLocal <= windowStartLocal)
{
continue;
}
results.Add(new CalendarEventEntry
{
Title = string.IsNullOrWhiteSpace(calendarEvent.Summary) ? "Untitled" : calendarEvent.Summary,
Location = string.IsNullOrWhiteSpace(calendarEvent.Location) ? null : calendarEvent.Location,
StartLocal = startLocal,
EndLocal = endLocal,
IsAllDay = isAllDay
});
}
return results
.OrderBy(eventEntry => eventEntry.StartLocal)
.Take(Math.Max(1, maxEvents))
.ToList();
}
}
+1 -1
View File
@@ -47,7 +47,7 @@ internal class SampleAggregator
} }
} }
private bool IsPowerOfTwo(int x) private static bool IsPowerOfTwo(int x)
{ {
return (x & (x - 1)) == 0; return (x & (x - 1)) == 0;
} }
@@ -1,19 +1,19 @@
using DesktopMagic.Plugins; using DesktopMagic.Dialogs;
using DesktopMagic.Plugins;
using Microsoft.Win32;
using System; using System;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media;
namespace DesktopMagic.Helpers; namespace DesktopMagic.Helpers;
internal class SettingElementGenerator(ComboBox optionsComboBox) internal class SettingElementGenerator(uint pluginId)
{ {
private readonly ComboBox optionsComboBox = optionsComboBox; public FrameworkElement? Generate(SettingElement settingElement, System.Windows.Controls.TextBlock textBlock)
public void Generate(SettingElement settingElement, DockPanel dockPanel, TextBlock textBlock)
{ {
dockPanel.UpdateLayout();
textBlock.UpdateLayout();
if (settingElement.Input is DesktopMagic.Api.Settings.Label eLabel) if (settingElement.Input is DesktopMagic.Api.Settings.Label eLabel)
{ {
textBlock.Text = eLabel.Value; textBlock.Text = eLabel.Value;
@@ -32,16 +32,13 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBlock.Text = eLabel.Value; textBlock.Text = eLabel.Value;
}); });
}; };
return null;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.Button eButton) else if (settingElement.Input is DesktopMagic.Api.Settings.Button eButton)
{ {
Button button = new() Wpf.Ui.Controls.Button button = new()
{ {
Content = eButton.Value, Content = eButton.Value,
FontSize = 10,
Height = 20,
Margin = new Thickness(0, 10, 0, 10),
Padding = new Thickness(0),
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch
}; };
@@ -64,16 +61,18 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
}); });
}; };
_ = dockPanel.Children.Add(button); return button;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.CheckBox eCheckBox) else if (settingElement.Input is DesktopMagic.Api.Settings.CheckBox eCheckBox)
{ {
CheckBox checkBox = new() Wpf.Ui.Controls.ToggleSwitch checkBox = new()
{ {
IsChecked = eCheckBox.Value, IsChecked = eCheckBox.Value,
Style = (Style)dockPanel.FindResource("MaterialDesignDarkCheckBox"),
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Right,
Height = 30,
Style = Application.Current.FindResource("ToggleSwitchContentLeftStyle") as Style
}; };
checkBox.Click += (_s, _e) => checkBox.Click += (_s, _e) =>
{ {
@@ -94,14 +93,14 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
}); });
}; };
_ = dockPanel.Children.Add(checkBox); return checkBox;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.TextBox eTextBox) else if (settingElement.Input is DesktopMagic.Api.Settings.TextBox eTextBox)
{ {
TextBox textBox = new() Wpf.Ui.Controls.TextBox textBox = new()
{ {
Text = eTextBox.Value, Text = eTextBox.Value,
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.NoWrap,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch
}; };
@@ -123,11 +122,11 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBox.Text = eTextBox.Value; textBox.Text = eTextBox.Value;
}); });
}; };
_ = dockPanel.Children.Add(textBox); return textBox;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.IntegerUpDown eIntegerUpDown) else if (settingElement.Input is DesktopMagic.Api.Settings.IntegerUpDown eIntegerUpDown)
{ {
Xceed.Wpf.Toolkit.IntegerUpDown integerUpDown = new() Wpf.Ui.Controls.NumberBox numberBox = new()
{ {
Value = eIntegerUpDown.Value, Value = eIntegerUpDown.Value,
Minimum = eIntegerUpDown.Minimum, Minimum = eIntegerUpDown.Minimum,
@@ -135,11 +134,11 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch
}; };
integerUpDown.ValueChanged += (_s, _e) => numberBox.ValueChanged += (_s, _e) =>
{ {
try try
{ {
eIntegerUpDown.Value = integerUpDown.Value.GetValueOrDefault(); eIntegerUpDown.Value = (int)numberBox.Value;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -148,12 +147,12 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
}; };
eIntegerUpDown.OnValueChanged += () => eIntegerUpDown.OnValueChanged += () =>
{ {
integerUpDown.Dispatcher.Invoke(() => numberBox.Dispatcher.Invoke(() =>
{ {
integerUpDown.Value = eIntegerUpDown.Value; numberBox.Value = eIntegerUpDown.Value;
}); });
}; };
_ = dockPanel.Children.Add(integerUpDown); return numberBox;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.Slider eSlider) else if (settingElement.Input is DesktopMagic.Api.Settings.Slider eSlider)
{ {
@@ -165,7 +164,8 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
TickFrequency = 1, TickFrequency = 1,
IsSnapToTickEnabled = true, IsSnapToTickEnabled = true,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 5, 0, 5)
}; };
slider.ValueChanged += (_s, _e) => slider.ValueChanged += (_s, _e) =>
{ {
@@ -186,7 +186,7 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
}); });
}; };
_ = dockPanel.Children.Add(slider); return slider;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.ComboBox eComboBox) else if (settingElement.Input is DesktopMagic.Api.Settings.ComboBox eComboBox)
{ {
@@ -203,7 +203,8 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
{ {
try try
{ {
eComboBox.Value = comboBox.SelectedItem.ToString() ?? string.Empty; comboBox.SelectedItem ??= eComboBox.Value;
eComboBox.Value = comboBox.SelectedItem?.ToString() ?? eComboBox.Value;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -216,17 +217,170 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
comboBox.SelectedItem = eComboBox.Value; comboBox.SelectedItem = eComboBox.Value;
}; };
_ = dockPanel.Children.Add(comboBox); return comboBox;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.ColorPicker eColorPicker)
{
Grid panel = new()
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
panel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
panel.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
Border colorPreview = new()
{
Width = 30,
Height = 30,
CornerRadius = new CornerRadius(4),
Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B)),
Margin = new Thickness(0, 0, 10, 0),
BorderBrush = Application.Current.FindResource("ControlElevationBorderBrush") as Brush,
BorderThickness = new Thickness(1)
};
Grid.SetColumn(colorPreview, 0);
Wpf.Ui.Controls.Button browseButton = new()
{
Content = "Select Color",
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
Grid.SetColumn(browseButton, 1);
browseButton.Click += (_s, _e) =>
{
try
{
ColorDialog colorDialog = new("Select a color", eColorPicker.Value)
{
Owner = Application.Current.MainWindow
};
if (colorDialog.ShowDialog() == true)
{
eColorPicker.Value = colorDialog.ResultColor;
colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
}
}
catch (Exception ex)
{
DisplayException(ex.Message);
}
};
eColorPicker.OnValueChanged += () =>
{
colorPreview.Dispatcher.Invoke(() =>
{
colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
});
};
_ = panel.Children.Add(colorPreview);
_ = panel.Children.Add(browseButton);
return panel;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.FileSelector eFileSelector)
{
Grid grid = new()
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Wpf.Ui.Controls.TextBox pathTextBox = new()
{
Text = eFileSelector.Value,
IsReadOnly = true,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 0, 5, 0)
};
Grid.SetColumn(pathTextBox, 0);
Wpf.Ui.Controls.Button browseButton = new()
{
Content = "Browse",
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetColumn(browseButton, 1);
browseButton.Click += (_s, _e) =>
{
try
{
if (eFileSelector.SelectFolder)
{
OpenFolderDialog folderDialog = new()
{
Title = eFileSelector.Title,
InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
};
if (folderDialog.ShowDialog() == true)
{
eFileSelector.Value = folderDialog.FolderName;
pathTextBox.Text = eFileSelector.Value;
}
}
else
{
OpenFileDialog fileDialog = new()
{
Title = eFileSelector.Title,
Filter = eFileSelector.Filter,
InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
};
if (fileDialog.ShowDialog() == true)
{
eFileSelector.Value = fileDialog.FileName;
pathTextBox.Text = eFileSelector.Value;
}
}
}
catch (Exception ex)
{
DisplayException(ex.Message);
}
};
eFileSelector.OnValueChanged += () =>
{
pathTextBox.Dispatcher.Invoke(() =>
{
pathTextBox.Text = eFileSelector.Value;
});
};
_ = grid.Children.Add(pathTextBox);
_ = grid.Children.Add(browseButton);
return grid;
}
return null;
} }
private void DisplayException(string message) private async void DisplayException(string message)
{ {
App.Logger.LogInfo(message, source: "PluginInput"); App.Logger.LogInfo(message, source: "PluginInput");
_ = MessageBox.Show("File execution error:\n" + message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
int index = MainWindow.WindowNames.IndexOf(optionsComboBox.SelectedItem.ToString() ?? string.Empty); {
Title = "Error",
Content = "File execution error:\n" + message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
int index = Manager.Instance.PluginWindows.FindIndex(p => p.PluginMetadata.Id == pluginId);
PluginWindow window = MainWindow.Windows[index]; if (index >= 0 && index < Manager.Instance.PluginWindows.Count)
window?.Exit(); {
IPluginWindow window = Manager.Instance.PluginWindows[index];
window?.Exit();
}
} }
} }
@@ -0,0 +1,89 @@
using IWshRuntimeLibrary;
using System;
using System.IO;
using System.Reflection;
namespace DesktopMagic.Helpers;
public enum AutoStartStatus
{
Success = 0,
InvalidParameters = 1,
AlreadyEnabled = 2,
AlreadyDisabled = 3,
UnexpectedError = 4
}
public static class StartupManager
{
private static readonly string appPath = Path.ChangeExtension(Assembly.GetEntryAssembly()?.Location ?? throw new InvalidOperationException("Failed to get the application path."), ".exe");
private static readonly string startupFolder = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
private static readonly string shortcutPath = Path.Combine(startupFolder, Path.GetFileNameWithoutExtension(appPath) + ".lnk");
public static bool IsAutoStartEnabled()
{
return System.IO.File.Exists(shortcutPath);
}
public static AutoStartStatus EnableAutoStart()
{
if (IsAutoStartEnabled())
{
return AutoStartStatus.AlreadyEnabled;
}
try
{
WshShell shell = new WshShell();
IWshShortcut shortcut = (IWshShortcut)shell.CreateShortcut(shortcutPath);
shortcut.TargetPath = appPath;
shortcut.WorkingDirectory = Path.GetDirectoryName(appPath);
shortcut.Save();
return AutoStartStatus.Success;
}
catch
{
return AutoStartStatus.UnexpectedError;
}
}
public static AutoStartStatus DisableAutoStart()
{
if (!IsAutoStartEnabled())
{
return AutoStartStatus.AlreadyDisabled;
}
try
{
System.IO.File.Delete(shortcutPath);
return AutoStartStatus.Success;
}
catch
{
return AutoStartStatus.UnexpectedError;
}
}
public static async void ToggleAutoStart()
{
AutoStartStatus status = IsAutoStartEnabled() ? DisableAutoStart() : EnableAutoStart();
string message = status switch
{
AutoStartStatus.Success => "Auto-start setting changed successfully.",
AutoStartStatus.AlreadyEnabled => "Auto-start is already enabled.",
AutoStartStatus.AlreadyDisabled => "Auto-start is already disabled.",
_ => "Failed to change auto-start setting.",
};
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
}
@@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace DesktopMagic.Helpers;
public class UnderscoreEscapingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string text)
{
// Replaces single underscores with double underscores to escape them in labels
return text.Replace("_", "__");
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
+15
View File
@@ -76,6 +76,21 @@ namespace DesktopMagic
DependencyProperty.RegisterAttached("IsHooked", typeof(WindowLockHook), typeof(WindowPos), DependencyProperty.RegisterAttached("IsHooked", typeof(WindowLockHook), typeof(WindowPos),
new PropertyMetadata(null)); new PropertyMetadata(null));
public static void SetWindowLayer(Window window, string layer)
{
switch (layer)
{
case "Always on Bottom":
window.Topmost = false;
SendWpfWindowBack(window);
SendWpfWindowBack(window);
break;
case "Always on Top":
window.Topmost = true;
break;
}
}
private class WindowLockHook private class WindowLockHook
{ {
private readonly Window Window; private readonly Window Window;
+117 -137
View File
@@ -1,153 +1,133 @@
<Window x:Class="DesktopMagic.MainWindow" <ui:FluentWindow x:Class="DesktopMagic.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator" xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DesktopMagic" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}" d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Closing="Window_Closing" Closing="Window_Closing"
Closed="Window_Closed" Closed="Window_Closed"
ShowInTaskbar="True" Height="560"
Icon="{StaticResource Icon}" Width="900"
Background="{DynamicResource MaterialDesignPaper}" MinHeight="600"
FontFamily="{DynamicResource MaterialDesignFont}" MinWidth="940"
Height="520" ExtendsContentIntoTitleBar="True"
Width="570"
MinHeight="520"
MinWidth="570"
WindowState="Minimized" WindowState="Minimized"
StateChanged="Window_StateChanged"
Loaded="Window_Loaded"> Loaded="Window_Loaded">
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False"> <Grid>
<Grid> <Grid.RowDefinitions>
<TextBlock TextWrapping="Wrap" Text="0" VerticalAlignment="Top" FontFamily="Comic Sans MS" Loaded="TextBlock_Loaded" Foreground="Transparent" /> <RowDefinition Height="Auto" />
<Grid> <RowDefinition Height="*" />
<Grid.RowDefinitions> </Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="1*" /> <ui:TitleBar x:Name="TitleBar" Title="{Binding Title}">
</Grid.RowDefinitions> <ui:TitleBar.Icon>
<Grid Margin="{StaticResource DefaultMargin}"> <ui:ImageIcon Source="{StaticResource Icon}" />
</ui:TitleBar.Icon>
</ui:TitleBar>
<tray:NotifyIcon Grid.Row="0" FocusOnLeftClick="True" MenuOnRightClick="True" TooltipText="{Binding AppName}" Icon="{StaticResource Icon}" LeftClick="NotifyIcon_LeftClick">
<tray:NotifyIcon.Menu>
<ContextMenu>
<MenuItem Header="Settings" Click="SettingsMenuItem_Click" />
<MenuItem Header="Edit Layout" Click="EditLayoutMenuItem_Click" />
<MenuItem Header="Quit" Click="QuitMenuItem_Click" />
</ContextMenu>
</tray:NotifyIcon.Menu>
</tray:NotifyIcon>
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ui:NavigationView x:Name="NavigationView" Grid.Row="0" IsBackButtonVisible="Collapsed" IsPaneToggleVisible="True" OpenPaneLength="200" Loaded="NavigationView_Loaded">
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem Content="{DynamicResource plugins}" TargetPageType="{x:Type pages:MainPage}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Apps24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource themes}" TargetPageType="{x:Type pages:ThemePage}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Color24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource pluginManager}" TargetPageType="{x:Type plugins:PluginManager}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="AppsAddIn24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
</ui:NavigationView.MenuItems>
<ui:NavigationView.FooterMenuItems>
<ui:NavigationViewItem Content="Documentation" Click="DocumentationNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="LightbulbFilament24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource requestFeature}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="ChatHelp24"/>
</ui:NavigationViewItem.Icon>
<ui:NavigationViewItem.MenuItems>
<ui:NavigationViewItem Content="GitHub" Click="RequestFeatureNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Globe24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource email}" Click="RequestFeatureEmailNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Mail24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
</ui:NavigationViewItem.MenuItems>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource reportBug}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Bug24"/>
</ui:NavigationViewItem.Icon>
<ui:NavigationViewItem.MenuItems>
<ui:NavigationViewItem Content="GitHub" Click="ReportBugNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Globe24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource email}" Click="ReportBugEmailNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Mail24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
</ui:NavigationViewItem.MenuItems>
</ui:NavigationViewItem>
</ui:NavigationView.FooterMenuItems>
</ui:NavigationView>
<Border Grid.Row="1" Background="{DynamicResource ControlFillColorDefaultBrush}" BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" Padding="5 2 5 5">
<Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" /> <ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="1*" /> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<DockPanel Margin="0,0,2.5,0">
<Grid DockPanel.Dock="Top">
<Rectangle Grid.Row="1" Fill="#FFECECEC" Height="24" />
<CheckBox x:Name="EditCheckBox" VerticalAlignment="Center" Grid.Row="1" Content="{DynamicResource edit}" Click="EditCheckBox_Click" Foreground="Black" BorderBrush="#FF323232" Style="{StaticResource MaterialDesignDarkCheckBox}" IsChecked="True" />
</Grid>
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<ItemsControl ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Click="PluginCheckBox_Click" Content="{Binding Value.Name}" Tag="{Binding Key}" IsChecked="{Binding Value.Enabled}" Style="{StaticResource MaterialDesignDarkCheckBox}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<DockPanel Grid.Column="1" Margin="2.5,0,0,0"> <ui:HyperlinkButton FontWeight="Bold" Content="{DynamicResource developedBy}" NavigateUri="https://me.stone-red.net"/>
<ComboBox x:Name="optionsComboBox" DisplayMemberPath="Name" DockPanel.Dock="Top" Height="24" SelectedIndex="0" SelectionChanged="OptionsComboBox_SelectionChanged" Background="#FFECECEC" Padding="4" VerticalAlignment="Center" /> <ui:ToggleSwitch x:Name="autoStartCheckBox" Style="{StaticResource ToggleSwitchContentLeftStyle}" Grid.Column="2" Margin="0 0 5 0" Content="{DynamicResource autoStart}" IsChecked="{Binding IsAutoStartEnabled}" VerticalAlignment="Center"/>
<ScrollViewer Background="#FFBBBBBB">
<Grid>
<StackPanel x:Name="optionsPanel" Margin="3,3,3,0" HorizontalAlignment="Stretch" Visibility="Collapsed">
</StackPanel>
</Grid>
</ScrollViewer>
</DockPanel>
</Grid> </Grid>
<Grid Grid.Row="1"> </Border>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="110" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Display Font: " Grid.Column="0" Grid.Row="0" VerticalAlignment="Center"></TextBlock>
<ComboBox x:Name="fontComboBox" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" SelectionChanged="FontComboBox_SelectionChanged" Margin="0,0,0,5" />
<TextBlock Text="Corner Radius: " Grid.Column="0" Grid.Row="1" VerticalAlignment="Center"></TextBlock>
<TextBox x:Name="cornerRadiusTextBox" Grid.Column="1" Grid.Row="1" TextChanged="CornerRadiusTextBox_TextChanged" materialDesign:HintAssist.Hint="Enter a number">
<materialDesign:TextFieldAssist.CharacterCounterStyle>
<Style TargetType="TextBlock" />
</materialDesign:TextFieldAssist.CharacterCounterStyle>
</TextBox>
<TextBlock Text="Margin: " Grid.Column="0" Grid.Row="3" VerticalAlignment="Center"></TextBlock>
<TextBox x:Name="marginTextBox" Grid.Column="1" Grid.Row="3" TextChanged="MarginTextBox_TextChanged" materialDesign:HintAssist.Hint="Enter a number">
<materialDesign:TextFieldAssist.CharacterCounterStyle>
<Style TargetType="TextBlock" />
</materialDesign:TextFieldAssist.CharacterCounterStyle>
</TextBox>
</Grid>
<StackPanel Grid.Column="1" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
<DockPanel Margin="0,0,0,5">
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
<Rectangle x:Name="primaryColorRechtangle" Fill="White" />
</Border>
<Button Content="Change Primary Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangePrimaryColorButton_Click" />
</DockPanel>
<DockPanel Margin="0,0,0,5">
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
<Rectangle x:Name="secondaryColorRechtangle" Fill="White" />
</Border>
<Button Content="Change Secondary Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangeSecondaryColorButton_Click" />
</DockPanel>
<DockPanel>
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
<Rectangle x:Name="backgroundColorRechtangle" Fill="White" />
</Border>
<Button Content="Change Background Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangeBackgroundColorButton_Click" />
</DockPanel>
</StackPanel>
<Grid Grid.Row="1" Grid.ColumnSpan="2" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Rectangle Fill="#FFC5C5C5" Stroke="#FFC5C5C5" Grid.ColumnSpan="3" />
<StackPanel Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
<ComboBox x:Name="layoutsComboBox" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayoutName}" SelectedValuePath="Name" HorizontalAlignment="Left" Margin="0,0,0,5" Width="170" Height="20" SelectionChanged="LayoutsComboBox_SelectionChanged" />
<Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Margin="0,0,0,5" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="170" Click="NewLayoutButton_Click" FontWeight="Regular" />
<Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="170" Click="RemoveLayoutButton_Click" FontWeight="Regular" />
</StackPanel>
<StackPanel Grid.Column="1" Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
<Button x:Name="updatePluginsButton" Content="{DynamicResource updatePlugins}" Margin="0,0,0,5" HorizontalAlignment="Center" Width="170" Click="UpdatePluginsButton_Click" FontWeight="Regular" />
<Button x:Name="githubButton" Content="GitHub" HorizontalAlignment="Center" Width="170" Click="GitHubButton_Click" FontWeight="Regular" Cursor="Hand" />
</StackPanel>
<StackPanel Grid.Column="2" Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
<Button x:Name="downloadPluginsButton" Content="{DynamicResource pluginManager}" Margin="0,0,0,5" HorizontalAlignment="Right" Width="170" Click="PluginManagerButton_Click" FontWeight="Regular" Cursor="Hand" />
<Button x:Name="openPluginsFolderButton" Content="{DynamicResource openPluginsFolder}" HorizontalAlignment="Right" Width="170" Click="OpenPluginsFolderButton_Click" FontWeight="Regular" />
</StackPanel>
</Grid>
</Grid>
</Grid>
</Grid> </Grid>
</busyIndicator:BusyMask> <ContentPresenter x:Name="rootContentDialog" Grid.Row="1" Grid.RowSpan="2" />
</Window> </Grid>
</ui:FluentWindow>
+226 -635
View File
@@ -1,680 +1,271 @@
using DesktopMagic.BuiltInWindowElements;
using DesktopMagic.DataContexts; using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs; using DesktopMagic.Dialogs;
using DesktopMagic.Helpers; using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.Net.Http;
using System.IO.Packaging;
using System.Linq;
using System.Reflection; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace DesktopMagic using Wpf.Ui.Appearance;
using Wpf.Ui.Controls;
namespace DesktopMagic;
public partial class MainWindow : FluentWindow
{ {
public partial class MainWindow : Window private readonly Manager _manager = Manager.Instance;
private readonly MainWindowDataContext _mainWindowDataContext = new();
[Obsolete]
public MainWindow()
{ {
private readonly System.Windows.Forms.NotifyIcon notifyIcon = new(); SystemThemeWatcher.Watch(this);
private readonly MainWindowDataContext mainWindowDataContext = new(); InitializeComponent();
private readonly Dictionary<PluginMetadata, Type> builtInPlugins = new() DataContext = _mainWindowDataContext;
Resources.MergedDictionaries.Add(App.LanguageDictionary);
App.DialogService.SetDialogHost(rootContentDialog);
}
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
try
{ {
{new("Music Visualizer", 1), typeof(MusicVisualizerPlugin)}, App.Logger.LogInfo("Loading application", source: "MainWindow");
{new("Time",2), typeof(TimePlugin)},
{new("Date",3), typeof(DatePlugin)},
{new("Cpu Usage", 4), typeof(CpuMonitorPlugin)}
};
private bool loaded = false; _mainWindowDataContext.IsLoading = true;
private bool blockWindowsClosing = true;
public static List<PluginWindow> Windows { get; } = [];
public static List<string> WindowNames { get; } = [];
internal static bool EditMode { get; set; } = false;
private DesktopMagicSettings Settings // Load plugins and settings through manager
{ _manager.LoadSettings();
get => mainWindowDataContext.Settings; _mainWindowDataContext.Settings = _manager.Settings;
set => mainWindowDataContext.Settings = value; _manager.LoadPlugins();
_manager.LoadLayout();
_manager.IsLoaded = true;
_mainWindowDataContext.IsLoading = false;
await ShowLatestReleaseInfoAfterUpdateAsync();
App.Logger.LogInfo("Application loaded", source: "MainWindow");
} }
catch (Exception ex)
public MainWindow()
{ {
DataContext = mainWindowDataContext; App.Logger.LogError(ex.Message, source: "MainWindow");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
try
{ {
Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/DesktopMagic;component/icon.ico")).Stream; Title = App.AppName,
notifyIcon.Click += TaskbarIcon_TrayLeftClick; Content = ex.ToString(),
notifyIcon.Visible = true; CloseButtonText = "Ok"
notifyIcon.Text = App.AppName;
notifyIcon.Icon = new System.Drawing.Icon(iconStream);
InitializeComponent();
SetLanguageDictionary();
#if DEBUG
Title = $"{App.AppName} - Dev {System.Windows.Forms.Application.ProductVersion}";
#else
Title = $"{App.AppName} - {System.Windows.Forms.Application.ProductVersion}";
#endif
}
catch (Exception ex)
{
_ = MessageBox.Show(ex.ToString());
}
}
#region Load
private readonly Dictionary<uint, InternalPluginData> plugins = [];
private void Window_Loaded(object sender, RoutedEventArgs e)
{
try
{
if (!Directory.Exists(App.ApplicationDataPath + "\\Plugins"))
{
_ = Directory.CreateDirectory(App.ApplicationDataPath + "\\Plugins");
}
App.Logger.LogInfo("Created Plugins Folder", source: "Main");
//Write To Log File and Load Elements
App.Logger.LogInfo("Loading Plugin names", source: "Main");
LoadPlugins();
App.Logger.LogInfo("Loading Layout names", source: "Main");
LoadSettings();
App.Logger.LogInfo("Loading Layout", source: "Main");
LoadLayout();
loaded = true;
App.Logger.LogInfo("Window Loaded", source: "Main");
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "Main");
_ = MessageBox.Show(ex.ToString());
}
}
private void LoadPlugins()
{
mainWindowDataContext.IsLoading = true;
plugins.Clear();
foreach (var buildInPlugin in builtInPlugins.Keys)
{
plugins.Add(buildInPlugin.Id, new(buildInPlugin, string.Empty));
}
string pluginsPath = App.ApplicationDataPath + "\\Plugins";
foreach (string directory in Directory.GetDirectories(pluginsPath))
{
string? pluginDllPath = Directory.GetFiles(directory, "main.dll").FirstOrDefault();
string? pluginMetadataPath = Directory.GetFiles(directory, "metadata.json").FirstOrDefault();
if (pluginDllPath is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no \"main.dll\"", source: "Main");
continue;
}
if (pluginMetadataPath is null)
{
App.Logger.LogWarn($"Plugin \"{directory}\" has no \"metadata.json\"", source: "Main");
continue;
}
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(pluginMetadataPath));
if (pluginMetadata is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no valid \"metadata.json\"", source: "Main");
continue;
}
if (plugins.ContainsKey(pluginMetadata.Id))
{
App.Logger.LogError($"Plugin \"{directory}\" has the same id as another plugin", source: "Main");
continue;
}
plugins.Add(pluginMetadata.Id, new(pluginMetadata, directory));
}
mainWindowDataContext.IsLoading = false;
}
#endregion Load
#region Windows
private void EditCheckBox_Click(object? sender, RoutedEventArgs? e)
{
EditMode = EditCheckBox.IsChecked == true;
SaveSettings();
}
private void PluginCheckBox_Click(object sender, RoutedEventArgs? e)
{
if (sender is not CheckBox checkBox)
{
return;
}
LoadPlugin(uint.Parse(checkBox.Tag.ToString()!));
}
private void LoadPlugin(uint pluginId)
{
if (!plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
{
return;
}
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
{
pluginSettings = new PluginSettings();
Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
}
if (WindowNames.Contains(internalPluginData.Metadata.Id.ToString()) || !pluginSettings.Enabled)
{
int index = WindowNames.IndexOf(internalPluginData.Metadata.Id.ToString());
if (index >= 0)
{
try
{
blockWindowsClosing = false;
Windows[index].Close();
blockWindowsClosing = true;
Windows.RemoveAt(index);
WindowNames.RemoveAt(index);
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "Main");
}
}
return;
}
PluginWindow window;
if (builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
{
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
else
{
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
Action? onPluginLoaded = null;
onPluginLoaded = () =>
{
Dispatcher.Invoke(() =>
{
if (!optionsComboBox.Items.Contains(internalPluginData.Metadata))
{
_ = optionsComboBox.Items.Add(internalPluginData.Metadata);
}
optionsComboBox.SelectedIndex = -1;
optionsComboBox.SelectedIndex = optionsComboBox.Items.IndexOf(internalPluginData.Metadata);
window.PluginLoaded -= onPluginLoaded;
});
}; };
window.OnExit += () => _ = await messageBox.ShowDialogAsync();
{
pluginSettings.Enabled = false;
};
window.PluginLoaded += onPluginLoaded;
window.ShowInTaskbar = false;
window.Show();
window.ContentRendered += DisplayWindow_ContentRendered;
window.Closing += DisplayWindow_Closing;
Windows.Add(window);
WindowNames.Add(window.Title);
} }
}
private void DisplayWindow_ContentRendered(object? sender, EventArgs e) private async void NavigationView_Loaded(object sender, RoutedEventArgs e)
{ {
if (sender is not Window window) _ = NavigationView.Navigate(typeof(Pages.MainPage));
{ }
return;
}
WindowPos.SendWpfWindowBack(window); private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
WindowPos.SendWpfWindowBack(window); {
} _manager.SaveSettings();
private void DisplayWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e) if (_manager.BlockWindowsClosing)
{
e.Cancel = blockWindowsClosing;
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
MessageBoxResult msbRes = MessageBox.Show((string)FindResource("wantToCloseProgram"), App.AppName, MessageBoxButton.YesNo);
e.Cancel = msbRes != MessageBoxResult.Yes;
SaveSettings();
}
private void Window_Closed(object sender, EventArgs e)
{ {
e.Cancel = true;
_manager.SetEditMode(false);
ShowInTaskbar = false;
WindowState = WindowState.Minimized;
Visibility = Visibility.Collapsed; Visibility = Visibility.Collapsed;
UpdateLayout();
foreach (Window window in Windows)
{
window.Hide();
}
Environment.Exit(0);
} }
}
private void Window_StateChanged(object? sender, EventArgs? e) private void Window_Closed(object sender, EventArgs e)
{ {
if (WindowState == WindowState.Minimized) Visibility = Visibility.Collapsed;
{ UpdateLayout();
EditCheckBox.IsChecked = false; _manager.CloseAllPluginWindows();
EditCheckBox_Click(null, null); Environment.Exit(0);
ShowInTaskbar = false; }
Visibility = Visibility.Collapsed;
foreach (Window item in Windows)
{
WindowPos.SendWpfWindowBack(item);
WindowPos.SendWpfWindowBack(item);
}
}
}
#endregion Windows private void NotifyIcon_MouseClick(object? sender, System.Windows.Forms.MouseEventArgs e)
{
#region Options if (e.Button == System.Windows.Forms.MouseButtons.Left)
private void FontComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
Settings.CurrentLayout.Theme.Font = fontComboBox.SelectedValue.ToString()!.Replace("System.Windows.Controls.ComboBoxItem: ", "");
}
private void OptionsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
optionsPanel.Visibility = Visibility.Visible;
optionsPanel.Children.Clear();
optionsPanel.UpdateLayout();
if (optionsComboBox.SelectedItem is null)
{
return;
}
bool success = Settings.CurrentLayout.Plugins.TryGetValue(((PluginMetadata)optionsComboBox.SelectedItem).Id, out Settings.PluginSettings? pluginSettings);
if (!success || pluginSettings is null || pluginSettings.Settings.Count == 0)
{
_ = optionsPanel.Children.Add(new TextBlock() { Text = (string)FindResource("noOptions") });
return;
}
SettingElementGenerator settingElementGenerator = new SettingElementGenerator(optionsComboBox);
foreach (SettingElement settingElement in pluginSettings.Settings)
{
DockPanel dockPanel = new()
{
LastChildFill = true,
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 0, 0, 5)
};
_ = optionsPanel.Children.Add(dockPanel);
TextBlock textBlock = new()
{
Text = $"{settingElement.Name}:",
Padding = new Thickness(0, 0, 3, 0),
VerticalAlignment = VerticalAlignment.Center
};
_ = dockPanel.Children.Add(textBlock);
settingElementGenerator.Generate(settingElement, dockPanel, textBlock);
}
}
#endregion Options
internal void RestoreWindow()
{
for (int i = 0; i < 10; i++)
{
ShowInTaskbar = true;
Visibility = Visibility.Visible;
SystemCommands.RestoreWindow(this);
Topmost = true;
_ = Activate();
Topmost = false;
}
}
private void TextBlock_Loaded(object sender, RoutedEventArgs e)
{
int index = 0;
foreach (FontFamily ff in Fonts.SystemFontFamilies)
{
ComboBoxItem comboBoxItem = new()
{
FontFamily = ff,
Content = ff.ToString()
};
_ = fontComboBox.Items.Add(comboBoxItem);
if (ff.ToString() == Settings.CurrentLayout.Theme.Font)
{
fontComboBox.SelectedIndex = index;
}
index++;
}
if (fontComboBox.SelectedIndex == -1)
{
fontComboBox.SelectedIndex = 0;
}
}
private void ChangePrimaryColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Primary Color", Settings.CurrentLayout.Theme.PrimaryColor);
if (colorDialog.ShowDialog() == true)
{
Settings.CurrentLayout.Theme.PrimaryColor = colorDialog.ResultColor;
primaryColorRechtangle.Fill = colorDialog.ResultBrush;
}
}
private void ChangeSecondaryColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Secondary Color", Settings.CurrentLayout.Theme.SecondaryColor);
if (colorDialog.ShowDialog() == true)
{
Settings.CurrentLayout.Theme.SecondaryColor = colorDialog.ResultColor;
secondaryColorRechtangle.Fill = colorDialog.ResultBrush;
}
}
private void ChangeBackgroundColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Background Color", Settings.CurrentLayout.Theme.BackgroundColor);
if (colorDialog.ShowDialog() == true)
{
Settings.CurrentLayout.Theme.BackgroundColor = colorDialog.ResultColor;
backgroundColorRechtangle.Fill = colorDialog.ResultBrush;
}
}
private void CornerRadiusTextBox_TextChanged(object? sender, TextChangedEventArgs? e)
{
bool success = int.TryParse(cornerRadiusTextBox.Text, out int cornerRadius);
if (success)
{
cornerRadiusTextBox.Foreground = Brushes.Black;
Settings.CurrentLayout.Theme.CornerRadius = cornerRadius;
}
else
{
cornerRadiusTextBox.Foreground = Brushes.Red;
}
}
private void MarginTextBox_TextChanged(object? sender, TextChangedEventArgs? e)
{
bool success = int.TryParse(marginTextBox.Text, out int margin);
if (success)
{
marginTextBox.Foreground = Brushes.Black;
Settings.CurrentLayout.Theme.Margin = margin;
}
else
{
marginTextBox.Foreground = Brushes.Red;
}
}
#region Layout
private readonly JsonSerializerOptions jsonSettingsOptions = new()
{
Converters =
{
new ColorJsonConverter()
}
};
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (loaded)
{
SaveSettings();
LoadLayout(false);
}
}
private void NewLayoutButton_Click(object sender, RoutedEventArgs e)
{
InputDialog inputDialog = new((string)FindResource("enterLayoutName"));
if (inputDialog.ShowDialog() == true)
{
if (Settings.Layouts.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
{
_ = MessageBox.Show((string)FindResource("layoutAlreadyExists"));
return;
}
Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
SaveSettings();
}
}
private void RemoveLayoutButton_Click(object sender, RoutedEventArgs e)
{
Settings.Layouts.Remove(Settings.CurrentLayout);
SaveSettings();
}
private void SaveSettings()
{
if (!loaded)
{
return;
}
string json = JsonSerializer.Serialize(Settings, jsonSettingsOptions);
File.WriteAllText(Path.Combine(App.ApplicationDataPath, "settings.json"), json);
}
private void LoadSettings()
{
if (!File.Exists(Path.Combine(App.ApplicationDataPath, "settings.json")))
{
Settings = new DesktopMagicSettings()
{
Layouts =
[
new Layout((string)FindResource("default"))
]
};
return;
}
string json = File.ReadAllText(Path.Combine(App.ApplicationDataPath, "settings.json"));
Settings = JsonSerializer.Deserialize<DesktopMagicSettings>(json, jsonSettingsOptions) ?? new DesktopMagicSettings()
{
Layouts =
[
new Layout((string)FindResource("default"))
]
};
}
private void LoadLayout(bool minimize = true)
{
mainWindowDataContext.IsLoading = true;
cornerRadiusTextBox.Text = Settings.CurrentLayout.Theme.CornerRadius.ToString();
marginTextBox.Text = Settings.CurrentLayout.Theme.Margin.ToString();
blockWindowsClosing = false;
primaryColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(Settings.CurrentLayout.Theme.PrimaryColor));
secondaryColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(Settings.CurrentLayout.Theme.SecondaryColor));
backgroundColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(Settings.CurrentLayout.Theme.BackgroundColor));
CornerRadiusTextBox_TextChanged(null, null);
MarginTextBox_TextChanged(null, null);
foreach (Window window in Windows)
{
window.Close();
}
EditCheckBox.IsChecked = false;
EditMode = false;
blockWindowsClosing = true;
Windows.Clear();
WindowNames.Clear();
optionsComboBox.Items.Clear();
bool showWindow = true;
// Load plugins
foreach (uint pluginId in plugins.Keys)
{
InternalPluginData internalPluginData = plugins[pluginId];
// Add plugin to layout if it doesn't exist
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
{
Settings.CurrentLayout.Plugins.Add(pluginId, new PluginSettings() { Name = internalPluginData.Metadata.Name });
continue;
}
pluginSettings.Name = internalPluginData.Metadata.Name;
if (pluginSettings.Enabled)
{
LoadPlugin(pluginId);
}
if (showWindow && pluginSettings.Enabled)
{
showWindow = false;
}
}
// Remove plugins that are not loaded anymore
foreach (uint pluginId in Settings.CurrentLayout.Plugins.Keys)
{
if (!plugins.ContainsKey(pluginId))
{
Settings.CurrentLayout.Plugins.Remove(pluginId);
}
}
Settings.CurrentLayout.UpdatePlugins();
if (!showWindow && minimize)
{
WindowState = WindowState.Minimized;
Window_StateChanged(null, null);
}
else
{
WindowState = WindowState.Normal;
Window_StateChanged(null, null);
}
mainWindowDataContext.IsLoading = false;
}
#endregion Layout
private void UpdatePluginsButton_Click(object sender, RoutedEventArgs e)
{
LoadPlugins();
LoadLayout(false);
}
private void OpenPluginsFolderButton_Click(object sender, RoutedEventArgs e)
{
_ = Process.Start("explorer.exe", App.ApplicationDataPath + "\\Plugins");
}
private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
private void TaskbarIcon_TrayLeftClick(object? sender, EventArgs e)
{ {
RestoreWindow(); RestoreWindow();
} }
}
private void GitHubButton_Click(object sender, RoutedEventArgs e) internal void RestoreWindow()
{
for (int i = 0; i < 10; i++)
{ {
string uri = "https://github.com/Stone-Red-Code/DesktopMagic"; ShowInTaskbar = true;
ProcessStartInfo psi = new ProcessStartInfo Visibility = Visibility.Visible;
SystemCommands.RestoreWindow(this);
Topmost = true;
_ = Activate();
Topmost = false;
}
}
private void Quit()
{
_manager.BlockWindowsClosing = false;
Close();
}
private void ReportBugNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "https://github.com/Stone-Red-Code/DesktopMagic/issues/new?template=bug_report.md";
ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void ReportBugEmailNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "mailto:[email protected]";
ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void RequestFeatureNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "https://github.com/Stone-Red-Code/DesktopMagic/issues/new?template=feature_request.md";
ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void RequestFeatureEmailNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "mailto:[email protected]";
ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void DocumentationNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "https://github.com/Stone-Red-Code/DesktopMagic";
ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void NotifyIcon_LeftClick(Wpf.Ui.Tray.Controls.NotifyIcon sender, RoutedEventArgs e)
{
RestoreWindow();
}
private void SettingsMenuItem_Click(object sender, RoutedEventArgs e)
{
RestoreWindow();
_ = NavigationView.Navigate(typeof(Pages.MainPage));
}
private void EditLayoutMenuItem_Click(object sender, RoutedEventArgs e)
{
_manager.SetEditMode(!_manager.IsEditMode);
}
private void QuitMenuItem_Click(object sender, RoutedEventArgs e)
{
Quit();
}
private async Task ShowLatestReleaseInfoAfterUpdateAsync()
{
string currentVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0.0";
string? lastShownVersion = _manager.Settings.ReleaseInfoLastAppVersion;
if (string.Equals(lastShownVersion, currentVersion, StringComparison.Ordinal))
{
return;
}
GitHubLatestRelease? latestRelease = await GetLatestReleaseInfoAsync();
if (latestRelease is null)
{
return;
}
string releaseName = !string.IsNullOrWhiteSpace(latestRelease.Name) ? latestRelease.Name : latestRelease.TagName;
string publishedAt = latestRelease.PublishedAt?.ToLocalTime().ToString("g") ?? (string)FindResource("unknown");
string publishedAtText = string.Format((string)FindResource("releaseInfoPublishedFormat"), publishedAt);
string releaseNotesMarkdown = string.IsNullOrWhiteSpace(latestRelease.Body) ? "-" : latestRelease.Body.Trim();
ReleaseInfoDialog releaseInfoDialog = new(
(string)FindResource("releaseInfoTitle"),
releaseName,
publishedAtText,
releaseNotesMarkdown,
(string)FindResource("openReleasePage"),
(string)FindResource("ok"))
{
Owner = this,
Topmost = true
};
_ = releaseInfoDialog.ShowDialog();
if (releaseInfoDialog.OpenReleaseRequested)
{
ProcessStartInfo processStartInfo = new()
{ {
UseShellExecute = true, UseShellExecute = true,
FileName = uri FileName = latestRelease.HtmlUrl
}; };
_ = Process.Start(psi); _ = Process.Start(processStartInfo);
} }
private void PluginManagerButton_Click(object sender, RoutedEventArgs e) _manager.Settings.ReleaseInfoLastAppVersion = currentVersion;
{ _manager.SaveSettings();
PluginManager pluginManager = new PluginManager();
pluginManager.ShowDialog();
LoadPlugins();
LoadLayout(false);
}
private void SetLanguageDictionary()
{
ResourceDictionary dict = [];
string currentCulture = Thread.CurrentThread.CurrentUICulture.ToString();
if (currentCulture.Contains("de"))
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.de.xaml", UriKind.Relative);
}
else
{
dict.Source = new Uri("..\\Resources\\Strings\\StringResources.en.xaml", UriKind.Relative);
}
Resources.MergedDictionaries.Add(dict);
}
} }
internal class InternalPluginData(PluginMetadata pluginMetadata, string directoryPath) private static async Task<GitHubLatestRelease?> GetLatestReleaseInfoAsync()
{ {
public PluginMetadata Metadata { get; set; } = pluginMetadata; try
public string DirectoryPath { get; set; } = directoryPath; {
return await GitHubReleaseService.GetLatestReleaseInfoAsync();
}
catch (HttpRequestException ex)
{
App.Logger.LogError($"Failed to retrieve latest release information: {ex.Message}", source: "MainWindow");
}
catch (TaskCanceledException ex)
{
App.Logger.LogWarn($"Timed out while retrieving latest release information: {ex.Message}", source: "MainWindow");
}
catch (JsonException ex)
{
App.Logger.LogError($"Failed to parse latest release information: {ex.Message}", source: "MainWindow");
}
return null;
} }
} }
+378
View File
@@ -0,0 +1,378 @@
using DesktopMagic.BuiltInPlugins;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
namespace DesktopMagic;
/// <summary>
/// Singleton Manager class that handles global state, plugin management, and shared operations
/// </summary>
public sealed class Manager
{
private static Manager? _instance;
private static readonly object _lock = new();
public static Manager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
_instance ??= new Manager();
}
}
return _instance;
}
}
// Plugin management
private readonly Dictionary<uint, InternalPluginData> _plugins = [];
private readonly Dictionary<PluginMetadata, Type> _builtInPlugins = new()
{
{new((string)App.LanguageDictionary["musicVisualizer"], 1) { Author = "Stone_Red" }, typeof(MusicVisualizerPlugin)},
{new((string)App.LanguageDictionary["time"],2) { Author = "Stone_Red" }, typeof(TimePlugin)},
{new((string)App.LanguageDictionary["date"],3) { Author = "Stone_Red" }, typeof(DatePlugin)},
{new((string)App.LanguageDictionary["cpuUsage"], 4) { Author = "Stone_Red" }, typeof(CpuMonitorPlugin)},
{new((string)App.LanguageDictionary["weather"], 5) { Author = "Stone_Red" }, typeof(WeatherPlugin)},
{new((string)App.LanguageDictionary["nextMeetingCountdown"], 6) { Author = "Stone_Red" }, typeof(NextMeetingCountdownPlugin)},
{new((string)App.LanguageDictionary["agenda"], 7) { Author = "Stone_Red" }, typeof(AgendaPlugin)},
};
// Window management
public List<IPluginWindow> PluginWindows { get; } = [];
public IReadOnlyDictionary<uint, InternalPluginData> Plugins => _plugins;
public bool BlockWindowsClosing { get; set; } = true;
// Edit mode tracking
private bool _isEditMode = false;
public bool IsEditMode => _isEditMode;
// Settings
public DesktopMagicSettings Settings { get; set; } = new();
public bool IsLoaded { get; set; } = false;
private readonly JsonSerializerOptions _jsonSettingsOptions = new()
{
Converters = { new ColorJsonConverter() }
};
// Events
public event Action? PluginsChanged;
public event Action? SettingsChanged;
public event Action<bool>? EditModeChanged;
private Manager()
{
// Private constructor for singleton
}
#region Plugin Management
public Dictionary<uint, InternalPluginData> GetPlugins() => new(_plugins);
public void LoadPlugins()
{
App.Logger.LogInfo("Loading plugins", source: "Manager");
_plugins.Clear();
// Load built-in plugins
foreach (PluginMetadata builtInPlugin in _builtInPlugins.Keys)
{
_plugins.Add(builtInPlugin.Id, new(builtInPlugin, PluginType.DotNet, string.Empty));
}
// Load external plugins
foreach (string directory in Directory.GetDirectories(App.PluginsPath))
{
string? pluginDllPath = Directory.GetFiles(directory, "main.dll").FirstOrDefault();
string? pluginHtmlPath = Directory.GetFiles(directory, "main.html").FirstOrDefault();
string? pluginMetadataPath = Directory.GetFiles(directory, "metadata.json").FirstOrDefault();
if (pluginDllPath is null && pluginHtmlPath is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no \"main.dll\" or \"main.html\"", source: "Manager");
continue;
}
if (pluginMetadataPath is null)
{
App.Logger.LogWarn($"Plugin \"{directory}\" has no \"metadata.json\"", source: "Manager");
continue;
}
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(pluginMetadataPath));
if (pluginMetadata is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no valid \"metadata.json\"", source: "Manager");
continue;
}
if (_plugins.ContainsKey(pluginMetadata.Id))
{
App.Logger.LogError($"Plugin \"{directory}\" has the same id as another plugin", source: "Manager");
continue;
}
PluginType pluginType = pluginHtmlPath is not null ? PluginType.Web : PluginType.DotNet;
_plugins.Add(pluginMetadata.Id, new(pluginMetadata, pluginType, directory));
}
PluginsChanged?.Invoke();
App.Logger.LogInfo($"Loaded {_plugins.Count} plugins", source: "Manager");
}
public void LoadPlugin(uint pluginId, Action<InternalPluginData>? onPluginLoaded = null)
{
if (!_plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
{
return;
}
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
{
pluginSettings = new PluginSettings();
Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
}
IPluginWindow? existingWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id);
if (existingWindow is not null || !pluginSettings.Enabled)
{
// Close the window if it's already open or disabled
if (existingWindow is not null)
{
try
{
BlockWindowsClosing = false;
existingWindow.Close();
BlockWindowsClosing = true;
PluginWindows.Remove(existingWindow);
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "Manager");
}
}
return;
}
IPluginWindow window;
if (_builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
{
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
else if (internalPluginData.Type == PluginType.Web)
{
window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
else
{
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
Action? pluginLoadedHandler = null;
pluginLoadedHandler = () =>
{
onPluginLoaded?.Invoke(internalPluginData);
window.PluginLoaded -= pluginLoadedHandler;
};
Action exitHandler = () =>
{
PluginWindows.Remove(window);
BlockWindowsClosing = false;
window.Close();
BlockWindowsClosing = true;
pluginSettings.Enabled = false;
};
window.PluginLoaded += pluginLoadedHandler;
window.OnExit += exitHandler;
window.Show();
window.SetEditMode(_isEditMode);
PluginWindows.Add(window);
}
public void ReloadPlugins()
{
App.Logger.LogInfo("Reloading plugins", source: "PluginManager");
LoadPlugins();
LoadLayout(false);
}
public void SetEditMode(bool editMode)
{
_isEditMode = editMode;
foreach (IPluginWindow window in PluginWindows)
{
window.SetEditMode(editMode);
}
Application.Current.MainWindow.Topmost = editMode;
EditModeChanged?.Invoke(editMode);
SaveSettings();
}
#endregion
#region Settings Management
public void LoadSettings()
{
App.Logger.LogInfo("Loading settings", source: "Manager");
if (!File.Exists(Path.Combine(App.ApplicationDataPath, "settings.json")))
{
Settings = new DesktopMagicSettings();
Settings.Layouts.Add(new Layout("Default"));
Settings.Themes.Add(new Theme("Default"));
return;
}
string json = File.ReadAllText(Path.Combine(App.ApplicationDataPath, "settings.json"));
Settings = JsonSerializer.Deserialize<DesktopMagicSettings>(json, _jsonSettingsOptions) ?? new DesktopMagicSettings();
if (Settings.Layouts.Count == 0)
{
Settings.Layouts.Add(new Layout("Default"));
}
if (Settings.Themes.Count == 0)
{
Settings.Themes.Add(new Theme("Default"));
}
SettingsChanged?.Invoke();
}
public void SaveSettings()
{
if (!IsLoaded)
{
return;
}
string json = JsonSerializer.Serialize(Settings, _jsonSettingsOptions);
File.WriteAllText(Path.Combine(App.ApplicationDataPath, "settings.json"), json);
App.Logger.LogInfo("Settings saved", source: "Manager");
SettingsChanged?.Invoke();
}
#endregion
#region Layout Management
public void LoadLayout(bool minimize = true, Action? onComplete = null)
{
App.Logger.LogInfo("Loading layout", source: "Manager");
BlockWindowsClosing = false;
foreach (IPluginWindow window in PluginWindows)
{
window.Close();
}
BlockWindowsClosing = true;
PluginWindows.Clear();
bool showWindow = true;
// Load plugins
foreach (uint pluginId in _plugins.Keys)
{
InternalPluginData internalPluginData = _plugins[pluginId];
// Add plugin to layout if it doesn't exist
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
{
Settings.CurrentLayout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata });
continue;
}
pluginSettings.Metadata = internalPluginData.Metadata;
if (pluginSettings.Enabled)
{
LoadPlugin(pluginId);
}
if (showWindow && pluginSettings.Enabled)
{
showWindow = false;
}
}
// Remove plugins that are not loaded anymore
List<uint> pluginIdsToRemove = Settings.CurrentLayout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
foreach (uint pluginId in pluginIdsToRemove)
{
Settings.CurrentLayout.Plugins.Remove(pluginId);
}
Settings.CurrentLayout.UpdatePlugins();
if (minimize && !showWindow)
{
Application.Current.MainWindow.WindowState = WindowState.Minimized;
Application.Current.MainWindow.ShowInTaskbar = false;
}
onComplete?.Invoke();
App.Logger.LogInfo("Layout loaded", source: "Manager");
}
#endregion
#region Cleanup
public void CloseAllPluginWindows()
{
foreach (IPluginWindow window in PluginWindows)
{
window.Hide();
}
}
#endregion
}
public class InternalPluginData(PluginMetadata pluginMetadata, PluginType pluginType, string directoryPath)
{
public PluginMetadata Metadata { get; set; } = pluginMetadata;
public PluginType Type { get; set; } = pluginType;
public string DirectoryPath { get; set; } = directoryPath;
}
public enum PluginType
{
DotNet,
Web
}
+87
View File
@@ -0,0 +1,87 @@
<Page x:Class="DesktopMagic.Pages.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DesktopMagic.Pages"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
d:DesignHeight="450" d:DesignWidth="800"
ScrollViewer.CanContentScroll="False"
Title="MainPage">
<Grid Margin="{StaticResource DefaultMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ui:Card Padding="5" Margin="0 0 0 5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="10" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ui:ToggleSwitch x:Name="editCheckBox" Height="30" VerticalAlignment="Center" Margin="5 0 0 0" Content="{DynamicResource editLayout}" Click="EditCheckBox_Click" />
<ui:TextBox Grid.Column="2" Text="{Binding PluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalledPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
</Grid>
</ui:Card>
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel" Grid.Row="1" VerticalAlignment="Stretch" VerticalContentAlignment="Top">
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
<ui:CardExpander.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0" Grid.Column="0" FontTypography="Body" Text="{Binding Value.Metadata.Name}" />
<Label Grid.Row="1" Grid.Column="0" Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}" FontSize="12" ContentStringFormat="{DynamicResource authorFormat}">
<Label.Style>
<Style TargetType="Label" BasedOn="{StaticResource {x:Type Label}}">
<Setter Property="Content" Value="{Binding Value.Metadata.Author, Converter={StaticResource UnderscoreEscapingConverter}}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Value.Metadata.Author}" Value="{x:Null}">
<Setter Property="Content" Value="{DynamicResource unknown}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<ui:ToggleSwitch Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Click="PluginCheckBox_Click" IsChecked="{Binding Value.Enabled}" Tag="{Binding Key}" Margin="0,0,16,0" VerticalAlignment="Center" />
</Grid>
</ui:CardExpander.Header>
</ui:CardExpander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="layoutsComboBox" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayoutName}" SelectedValuePath="Name" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="4" SelectionChanged="LayoutsComboBox_SelectionChanged" />
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Grid.Column="2" Icon="{ui:SymbolIcon Add24}" HorizontalAlignment="Stretch" Click="NewLayoutButton_Click" FontWeight="Regular"/>
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="RemoveLayoutButton_Click" FontWeight="Regular"/>
</Grid>
</ui:Card>
</Grid>
</Page>
+318
View File
@@ -0,0 +1,318 @@
using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DesktopMagic.Pages;
/// <summary>
/// Interaction logic for MainPage.xaml
/// </summary>
public partial class MainPage : Page
{
private readonly Manager _manager = Manager.Instance;
private readonly MainWindowDataContext _dataContext;
private bool _isLoadingLayout = false;
public MainPage()
{
InitializeComponent();
_dataContext = new MainWindowDataContext
{
Settings = _manager.Settings
};
DataContext = _dataContext;
// Subscribe to manager events
_manager.PluginsChanged += OnPluginsChanged;
_manager.EditModeChanged += OnEditModeChanged;
Loaded += MainPage_Loaded;
Unloaded += MainPage_Unloaded;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// Initialize edit checkbox state
editCheckBox.IsChecked = _manager.IsEditMode;
}
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
{
// Unsubscribe from events
_manager.PluginsChanged -= OnPluginsChanged;
_manager.EditModeChanged -= OnEditModeChanged;
}
private void OnPluginsChanged()
{
Dispatcher.Invoke(() =>
{
_dataContext.Settings = _manager.Settings;
ApplyPluginsFilter();
});
}
private void OnEditModeChanged(bool editMode)
{
Dispatcher.Invoke(() =>
{
editCheckBox.IsChecked = editMode;
});
}
private void EditCheckBox_Click(object sender, RoutedEventArgs e)
{
_manager.SetEditMode(editCheckBox.IsChecked == true);
}
private void PluginCheckBox_Click(object sender, RoutedEventArgs e)
{
if (sender is not Control checkBox)
{
return;
}
uint pluginId = uint.Parse(checkBox.Tag.ToString()!);
_manager.LoadPlugin(pluginId, (internalPluginData) =>
{
Dispatcher.Invoke(() =>
{
// Update the card expander to show options when enabling a plugin
Wpf.Ui.Controls.CardExpander? cardExpander = ((checkBox.Parent as FrameworkElement)?.Parent) as Wpf.Ui.Controls.CardExpander;
if (cardExpander is not null)
{
OptionsCardExpander_Expanded(cardExpander, new RoutedEventArgs());
}
});
});
}
private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
if (sender is ScrollViewer scv)
{
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
}
#region Plugin Search
private void AllPluginsSearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
ApplyPluginsFilter();
}
private void ApplyPluginsFilter()
{
string? searchText = _dataContext.PluginsSearchText;
System.ComponentModel.ICollectionView view = CollectionViewSource.GetDefaultView(pluginsItemsControl.ItemsSource);
if (string.IsNullOrWhiteSpace(searchText))
{
view.Filter = null;
}
else
{
view.Filter = (item) =>
{
if (item is KeyValuePair<uint, PluginSettings> kvp)
{
return kvp.Value.Metadata.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase);
}
return false;
};
}
}
#endregion
#region Layout Management
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Prevent recursive calls and only process if fully loaded
if (_isLoadingLayout || !_manager.IsLoaded || !IsLoaded)
{
return;
}
// Check if this is actually a user-initiated change
// by verifying that the removed and added items are different
if (e.RemovedItems.Count > 0 && e.AddedItems.Count > 0)
{
if (e.RemovedItems[0] == e.AddedItems[0])
{
return;
}
}
try
{
_isLoadingLayout = true;
_manager.SaveSettings();
_manager.LoadLayout(false);
}
finally
{
_isLoadingLayout = false;
}
}
private async void NewLayoutButton_Click(object sender, RoutedEventArgs e)
{
InputDialog inputDialog = new((string)FindResource("enterLayoutName"))
{
Owner = Window.GetWindow(this)
};
if (inputDialog.ShowDialog() == true)
{
if (_manager.Settings.Layouts.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("layoutAlreadyExists"),
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
try
{
_isLoadingLayout = true;
_manager.Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
_manager.Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
_manager.SaveSettings();
_manager.LoadLayout(false);
}
finally
{
_isLoadingLayout = false;
}
}
}
private async void RemoveLayoutButton_Click(object sender, RoutedEventArgs e)
{
if (_manager.Settings.Layouts.Count <= 1)
{
Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("cannotDeleteLastLayout"),
CloseButtonText = "Ok"
};
_ = await cannotDeleteMessageBox.ShowDialogAsync();
return;
}
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("confirmDeleteLayout"),
PrimaryButtonText = "Yes",
SecondaryButtonText = "No",
IsCloseButtonEnabled = false
};
Wpf.Ui.Controls.MessageBoxResult result = await messageBox.ShowDialogAsync();
if (result != Wpf.Ui.Controls.MessageBoxResult.Primary) // Primary is "Yes"
{
return;
}
try
{
_isLoadingLayout = true;
_ = _manager.Settings.Layouts.Remove(_manager.Settings.CurrentLayout);
_manager.SaveSettings();
_manager.LoadLayout(false);
}
finally
{
_isLoadingLayout = false;
}
}
#endregion
private void OptionsCardExpander_Expanded(object sender, RoutedEventArgs e)
{
if (sender is not Wpf.Ui.Controls.CardExpander expander || expander.Tag is not KeyValuePair<uint, PluginSettings> keyValuePair)
{
return;
}
PluginSettings? pluginSettings = keyValuePair.Value;
uint pluginId = keyValuePair.Key;
StackPanel optionsPanel = new StackPanel
{
Visibility = Visibility.Visible
};
expander.Content = optionsPanel;
optionsPanel.UpdateLayout();
// s.Input being null means the plugins has not been loaded yet but the settings are present in the saved configuration.
if (pluginSettings is null || pluginSettings.Settings.Count == 0 || pluginSettings.Settings.All(s => s.Input is null))
{
_ = optionsPanel.Children.Add(new System.Windows.Controls.TextBlock()
{
Text = (string)FindResource(pluginSettings?.Enabled == true ? "noOptions" : "enablePluginToConfigure")
});
return;
}
SettingElementGenerator settingElementGenerator = new SettingElementGenerator(pluginId);
foreach (SettingElement settingElement in pluginSettings.Settings)
{
Wpf.Ui.Controls.CardControl card = new()
{
Margin = new Thickness(0, 0, 0, 5),
Padding = new Thickness(5)
};
System.Windows.Controls.TextBlock textBlock = new()
{
Text = string.IsNullOrWhiteSpace(settingElement.Name) ? string.Empty : settingElement.Name,
Padding = new Thickness(0, 0, 3, 0),
VerticalAlignment = VerticalAlignment.Center,
FontWeight = FontWeights.SemiBold
};
card.Header = textBlock;
_ = optionsPanel.Children.Add(card);
FrameworkElement? control = settingElementGenerator.Generate(settingElement, textBlock);
if (control is not null)
{
control.MinWidth = 200;
control.MaxWidth = 200;
card.Content = control;
}
}
optionsPanel.UpdateLayout();
}
}
+52
View File
@@ -0,0 +1,52 @@
<Page x:Class="DesktopMagic.Pages.ThemePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DesktopMagic.Pages"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="ThemePage">
<Grid Margin="{StaticResource DefaultMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ui:ListView x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" ItemsSource="{Binding Settings.Themes}" SelectedValue="{Binding Settings.CurrentLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
<ui:ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Margin="8" VerticalAlignment="Center" FontWeight="SemiBold" Text="{Binding Name, Mode=OneWay}" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal" Margin="{StaticResource DefaultMargin}">
<ui:Button x:Name="editThemeButton" Content="{DynamicResource editTheme}" Icon="{ui:SymbolIcon Color24}" HorizontalAlignment="Stretch" Click="EditThemeButton_Click" FontWeight="Regular" Tag="{Binding}">
</ui:Button>
</StackPanel>
</Grid>
</DataTemplate>
</ui:ListView.ItemTemplate>
</ui:ListView>
<ui:Card Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<ui:Button x:Name="newThemeButton" Content="{DynamicResource newTheme}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="AddThemeButton_Click" FontWeight="Regular"/>
<ui:Button x:Name="deleteThemeButton" Content="{DynamicResource deleteTheme}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="DeleteThemeButton_Click" FontWeight="Regular"/>
</Grid>
</ui:Card>
</Grid>
</Page>
+126
View File
@@ -0,0 +1,126 @@
using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs;
using DesktopMagic.Plugins;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace DesktopMagic.Pages;
/// <summary>
/// Interaction logic for ThemePage.xaml
/// </summary>
public partial class ThemePage : Page
{
private readonly Manager _manager = Manager.Instance;
private readonly MainWindowDataContext _dataContext;
public ThemePage()
{
InitializeComponent();
_dataContext = new MainWindowDataContext
{
Settings = _manager.Settings
};
DataContext = _dataContext;
// Subscribe to manager events
_manager.SettingsChanged += OnSettingsChanged;
Unloaded += ThemePage_Unloaded;
}
private void ThemePage_Unloaded(object sender, RoutedEventArgs e)
{
// Unsubscribe from events
_manager.SettingsChanged -= OnSettingsChanged;
}
private void OnSettingsChanged()
{
Dispatcher.Invoke(() =>
{
_dataContext.Settings = _manager.Settings;
});
}
private async void AddThemeButton_Click(object sender, RoutedEventArgs e)
{
InputDialog inputDialog = new((string)FindResource("enterThemeName"))
{
Owner = Window.GetWindow(this)
};
if (inputDialog.ShowDialog() == true)
{
if (_manager.Settings.Themes.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("themeAlreadyExists"),
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
_manager.Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
_manager.Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
_manager.SaveSettings();
}
}
private async void DeleteThemeButton_Click(object sender, RoutedEventArgs e)
{
if (_manager.Settings.Themes.Count <= 1)
{
Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("cannotDeleteLastTheme"),
CloseButtonText = "Ok"
};
_ = await cannotDeleteMessageBox.ShowDialogAsync();
return;
}
var messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("confirmDeleteTheme"),
PrimaryButtonText = "Yes",
SecondaryButtonText = "No",
IsCloseButtonEnabled = false
};
Wpf.Ui.Controls.MessageBoxResult result = await messageBox.ShowDialogAsync();
if (result != Wpf.Ui.Controls.MessageBoxResult.Primary) // Primary is "Yes"
{
return;
}
if (themesListBox.SelectedItem is Theme theme)
{
_ = _manager.Settings.Themes.Remove(theme);
_manager.SaveSettings();
}
}
private void EditThemeButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not System.Windows.Controls.Button button || button.Tag is not Theme theme)
{
return;
}
ThemeDialog themeDialog = new(theme.Name, theme, App.AppName)
{
Owner = Window.GetWindow(this)
};
_ = themeDialog.ShowDialog();
}
}
+24
View File
@@ -0,0 +1,24 @@
using DesktopMagic.Plugins;
using System;
namespace DesktopMagic;
public interface IPluginWindow
{
event Action? PluginLoaded;
event Action? OnExit;
bool IsRunning { get; }
PluginMetadata PluginMetadata { get; }
string PluginFolderPath { get; }
string Title { get; set; }
void Exit();
void SetEditMode(bool enabled);
void Show();
void Hide();
void Close();
event System.ComponentModel.CancelEventHandler Closing;
}
-43
View File
@@ -1,43 +0,0 @@
<UserControl x:Class="DesktopMagic.Plugins.ModEntry"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:DesktopMagic.Plugins"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:PluginEntryDataContext}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}"
Visibility="{Binding Visibility}">
<Grid Background="White" Margin="10">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Border HorizontalAlignment="Left" Width="100" Height="100" BorderThickness="1" BorderBrush="LightGray">
<Image Source="{Binding Logo, FallbackValue={StaticResource ModioCogBlue}, TargetNullValue={StaticResource ModioCogBlue}}" Width="100" Height="100" />
</Border>
<StackPanel Grid.Column="1" HorizontalAlignment="Stretch">
<Label Content="{Binding FormattedDateAdded}" HorizontalAlignment="Center" ContentStringFormat="Added: {0}" />
<Label Content="{Binding FormattedDateUpdated}" HorizontalAlignment="Center" ContentStringFormat="Updated: {0}" />
<Button Command="{Binding Command}" Visibility="{Binding InstallButtonVisibility}" HorizontalAlignment="Stretch" Margin="10" Content="Install" />
<Button Command="{Binding Command}" Visibility="{Binding RemoveButtonVisibility}" HorizontalAlignment="Stretch" Margin="10" Content="Remove" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.ColumnSpan="2">
<Label Content="{Binding Name}" FontSize="20" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</UserControl>
+54 -2
View File
@@ -1,15 +1,19 @@
using DesktopMagic.Api; using CuteUtils.Logging;
using DesktopMagic.Api;
using DesktopMagic.Settings; using DesktopMagic.Settings;
using System.Drawing; using System.Drawing;
using Wpf.Ui.Controls;
namespace DesktopMagic.Plugins; namespace DesktopMagic.Plugins;
internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : IPluginData internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : IPluginData
{ {
private readonly PluginWindow window = window; private readonly PluginWindow window = window;
public ITheme Theme { get; } = pluginSettings.Theme; public ITheme Theme => pluginSettings.Theme;
public Size WindowSize => new Size((int)window.ActualWidth, (int)window.ActualHeight); public Size WindowSize => new Size((int)window.ActualWidth, (int)window.ActualHeight);
@@ -19,8 +23,56 @@ internal class PluginData(PluginWindow window, PluginSettings pluginSettings) :
public string PluginPath => window.PluginFolderPath; public string PluginPath => window.PluginFolderPath;
public void Log(string message, LogLevel level = LogLevel.Info)
{
LogSeverity severity = level switch
{
LogLevel.Info => LogSeverity.Info,
LogLevel.Warning => LogSeverity.Warn,
LogLevel.Error => LogSeverity.Error,
_ => LogSeverity.Info,
};
App.Logger.Log($"\"{PluginName}\" - {message}", "Plugin", severity);
}
public void ShowMessage(string message, string? title = null)
{
title ??= PluginName;
_ = window.Dispatcher.Invoke(async () =>
{
System.Windows.Window temporaryOwner = new()
{
AllowsTransparency = true,
ShowInTaskbar = false,
WindowStyle = System.Windows.WindowStyle.None,
Background = System.Windows.Media.Brushes.Transparent,
Topmost = true,
};
temporaryOwner.Show();
MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Owner = temporaryOwner,
Title = $"{App.AppName} - {title}",
Content = message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
temporaryOwner.Close();
});
}
public void UpdateWindow() public void UpdateWindow()
{ {
window.UpdatePluginWindow(); window.UpdatePluginWindow();
} }
public void SaveState()
{
window.SavePluginState();
}
} }
+53
View File
@@ -0,0 +1,53 @@
<UserControl x:Class="DesktopMagic.Plugins.ModEntry"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DesktopMagic.Plugins"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:PluginEntryDataContext}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}"
Visibility="{Binding Visibility}">
<ui:Card Padding="5">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="auto" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Image Width="100" Height="100" Margin="5" Source="{Binding Logo, FallbackValue={StaticResource ModioCogBlue}, TargetNullValue={StaticResource ModioCogBlue}}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<Label Content="{Binding Author, Converter={StaticResource UnderscoreEscapingConverter}}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource authorFormat}" />
<Label Content="{Binding DateAdded}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource addedFormat}" />
<Label Content="{Binding DateUpdated}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource updatedFormat}" />
<Label Content="{Binding Version}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource versionFormat}" />
</StackPanel>
<Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ui:Button Content="{Binding InstallUninstallButtonData.Text}" Icon="{Binding InstallUninstallButtonData.Icon}" Grid.Column="0" Command="{Binding InstallUninstallButtonData.Command}" IsEnabled="{Binding InstallUninstallButtonData.IsEnabled}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
<ui:Button Content="{Binding OpenButtonData.Text}" Icon="{Binding OpenButtonData.Icon}" Grid.Column="1" Command="{Binding OpenButtonData.Command}" IsEnabled="{Binding OpenButtonData.IsEnabled}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Margin="5 0 5 0"/>
</Grid>
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="5">
<ui:TextBlock Text="{Binding Name, Converter={StaticResource UnderscoreEscapingConverter}}" FontTypography="Title" Padding="0" Margin="0 0 0 5" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" Margin="0" />
</StackPanel>
</Grid>
</ui:Card>
</UserControl>
@@ -1,6 +1,7 @@
using System.Windows.Controls; using System.Windows.Controls;
namespace DesktopMagic.Plugins; namespace DesktopMagic.Plugins;
/// <summary> /// <summary>
/// Interaction logic for ModEntry.xaml /// Interaction logic for ModEntry.xaml
/// </summary> /// </summary>
@@ -10,4 +11,4 @@ public partial class ModEntry : UserControl
{ {
InitializeComponent(); InitializeComponent();
} }
} }
+45 -32
View File
@@ -1,25 +1,21 @@
<Window x:Class="DesktopMagic.Plugins.PluginManager" <Page x:Class="DesktopMagic.Plugins.PluginManager"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator" xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:local="clr-namespace:DesktopMagic.Plugins" xmlns:local="clr-namespace:DesktopMagic.Plugins"
xmlns:dataContexts="clr-namespace:DesktopMagic.DataContexts" xmlns:dataContexts="clr-namespace:DesktopMagic.DataContexts"
mc:Ignorable="d" mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=dataContexts:PluginManagerDataContext}" d:DataContext="{d:DesignInstance Type=dataContexts:PluginManagerDataContext}"
Title="PluginManager" d:DesignHeight="450"
Height="450" d:DesignWidth="800"
Width="800" ScrollViewer.CanContentScroll="False"
MinHeight="520" Title="{DynamicResource pluginManager}">
MinWidth="700"
WindowStyle="ToolWindow"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}">
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False"> <busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="ThreeDots" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
<Grid Margin="5"> <Grid Margin="{StaticResource DefaultMargin}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="auto" /> <RowDefinition Height="auto" />
<RowDefinition Height="auto" /> <RowDefinition Height="auto" />
@@ -28,21 +24,29 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition Width="5" />
<ColumnDefinition /> <ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Label Content="All plugins" FontSize="20" /> <ui:Card Grid.ColumnSpan="3" Padding="5" Margin="0 0 0 5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="10" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBox Grid.Row="1" Text="{Binding AllPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" materialDesign:HintAssist.Hint="Search..." materialDesign:TextFieldAssist.HasLeadingIcon="True" materialDesign:TextFieldAssist.LeadingIcon="Search" /> <ui:TextBox Grid.Column="0" Text="{Binding AllPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchAllPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
<ui:TextBox Grid.Column="2" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalledPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
</Grid>
</ui:Card>
<busyIndicator:BusyMask Grid.Row="2" IsBusy="{Binding IsSearching}" IndicatorType="Ring" BusyContent="Searching..." BusyContentMargin="0,50,0,0" IsBusyAtStartup="False"> <busyIndicator:BusyMask Grid.Row="2" IsBusy="{Binding IsSearching}" IndicatorType="ThreeDots" BusyContent="Searching..." BusyContentMargin="0,50,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top" Margin="0 0 2.5 0">
<ItemsControl ItemsSource="{Binding AllPlugins}"> <ItemsControl ItemsSource="{Binding AllPlugins}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1"> <local:ModEntry Margin="0 0 0 5" />
<local:ModEntry />
</Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
<ItemsControl.Style> <ItemsControl.Style>
@@ -64,17 +68,11 @@
</ScrollViewer> </ScrollViewer>
</busyIndicator:BusyMask> </busyIndicator:BusyMask>
<Label Grid.Column="1" Content="Installed plugins" FontSize="20" /> <ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="2" Grid.Column="2" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top">
<ItemsControl ItemsSource="{Binding InstalledPlugins}" Margin="2.5 0 0 0">
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" materialDesign:HintAssist.Hint="Search..." materialDesign:TextFieldAssist.HasLeadingIcon="True" materialDesign:TextFieldAssist.LeadingIcon="Search" />
<ScrollViewer Grid.Row="2" Grid.Column="1" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding InstalledPlugins}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1"> <local:ModEntry Margin="0 0 0 5"/>
<local:ModEntry />
</Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
<ItemsControl.Style> <ItemsControl.Style>
@@ -95,9 +93,24 @@
</ItemsControl> </ItemsControl>
</ScrollViewer> </ScrollViewer>
<Border Grid.Row="3" Grid.ColumnSpan="2" Padding="0 5 0 0" BorderThickness="0 2 0 0" BorderBrush="DarkGray"> <ui:Card Grid.Row="3" Grid.ColumnSpan="3" Padding="5" Margin="0 5 0 0">
<Image Source="{StaticResource ModioLogoBlueDark}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" /> <Grid>
</Border> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="15" />
<ColumnDefinition Width="1.05*" />
<ColumnDefinition Width="5.5" />
<ColumnDefinition Width="1.05*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="1.05*" />
</Grid.ColumnDefinitions>
<Image Source="{Binding ModIoIcon}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
<ui:Button Content="{DynamicResource createNewPlugin}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="CreatePluginButton_Click"/>
<ui:Button Content="{DynamicResource reloadPlugins}" Icon="{ui:SymbolIcon ArrowClockwise24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="ReloadPluginsButton_Click"/>
<ui:Button Content="{Binding LoginButtonText}" Icon="{ui:SymbolIcon PersonKey20}" Grid.Column="6" HorizontalAlignment="Stretch" Click="LogInButton_Click"/>
</Grid>
</ui:Card>
</Grid> </Grid>
</busyIndicator:BusyMask> </busyIndicator:BusyMask>
</Window> </Page>
+663 -75
View File
@@ -1,22 +1,26 @@
using DesktopMagic.DataContexts; using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs;
using DesktopMagic.Helpers; using DesktopMagic.Helpers;
using Modio.NET; using Modio;
using Modio.NET.Filters; using Modio.Filters;
using Modio.NET.Models; using Modio.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading; using System.Windows.Threading;
using System.Xml.Linq;
using File = System.IO.File; using File = System.IO.File;
using Path = System.IO.Path; using Path = System.IO.Path;
@@ -26,22 +30,43 @@ namespace DesktopMagic.Plugins;
/// <summary> /// <summary>
/// Interaction logic for PluginManager.xaml /// Interaction logic for PluginManager.xaml
/// </summary> /// </summary>
public partial class PluginManager : Window public partial class PluginManager : Page
{ {
private const int modIoGameId = 5665; private const int ModIoGameId = 5665;
private readonly Client client = new Client(new Credentials("88e6ea774c3a502b06114e7fee0829ac")); private const string ModIoApiKey = "88e6ea774c3a502b06114e7fee0829ac";
private readonly HttpClient httpClient = new(); private readonly HttpClient httpClient = new();
private readonly PluginManagerDataContext pluginManagerDataContext = new(); private readonly PluginManagerDataContext pluginManagerDataContext = new();
private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins"); private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins");
private readonly string pluginDevelopmentPath = Path.Combine(App.ApplicationDataPath, "PluginDevelopment");
private readonly Manager _manager = Manager.Instance;
private bool changed = false;
private readonly DispatcherTimer searchTimer = new() private readonly DispatcherTimer searchTimer = new()
{ {
Interval = TimeSpan.FromMilliseconds(300), Interval = TimeSpan.FromMilliseconds(300),
}; };
private Client modIoClient;
public PluginManager() public PluginManager()
{ {
Resources.MergedDictionaries.Add(App.LanguageDictionary);
string? modIoAccessToken = MainWindowDataContext.GetSettings().ModIoAccessToken;
if (modIoAccessToken is null)
{
modIoClient = new Client(new Credentials(ModIoApiKey));
App.Logger.LogInfo("Initialized mod.io client without authentication", source: "PluginManager");
}
else
{
modIoClient = new Client(new Credentials(ModIoApiKey, modIoAccessToken));
pluginManagerDataContext.IsAuthenticated = true;
App.Logger.LogInfo("Initialized mod.io client with authentication", source: "PluginManager");
}
InitializeComponent(); InitializeComponent();
DataContext = pluginManagerDataContext; DataContext = pluginManagerDataContext;
@@ -50,77 +75,172 @@ public partial class PluginManager : Window
searchTimer.Stop(); searchTimer.Stop();
await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText); await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText);
}; };
Loaded += PluginManager_Loaded;
Unloaded += PluginManager_Unloaded;
} }
public void Remove(string pluginPath, uint? id) private async void PluginManager_Loaded(object sender, RoutedEventArgs e)
{ {
pluginManagerDataContext.IsLoading = true; await InitializePluginManager();
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
}
if (Directory.Exists(pluginPath))
{
Directory.Delete(pluginPath, true);
}
pluginManagerDataContext.IsLoading = false;
} }
protected override async void OnInitialized(EventArgs e) private void PluginManager_Unloaded(object sender, RoutedEventArgs e)
{ {
base.OnInitialized(e); searchTimer.Stop();
if (changed)
{
_manager.ReloadPlugins();
}
}
private async Task InitializePluginManager()
{
App.Logger.LogInfo("Initializing Plugin Manager", source: "PluginManager");
HashSet<uint> pluginIds = []; HashSet<uint> pluginIds = [];
pluginManagerDataContext.IsLoading = true;
pluginManagerDataContext.InstalledPlugins.Clear();
pluginManagerDataContext.AllPlugins.Clear();
foreach (string pluginPath in Directory.GetDirectories(pluginsPath)) foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
{ {
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json"); string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
if (!File.Exists(pluginMetadataPath)) if (!File.Exists(pluginMetadataPath))
{
App.Logger.LogWarn($"Plugin metadata not found at: {pluginMetadataPath}", source: "PluginManager");
continue;
}
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(await File.ReadAllTextAsync(pluginMetadataPath));
if (pluginMetadata is null)
{
App.Logger.LogWarn($"Failed to deserialize plugin metadata from: {pluginMetadataPath}", source: "PluginManager");
continue;
}
string csprojPath = GetCsprojPath(pluginMetadata.Name);
App.Logger.LogInfo($"Loaded plugin: {pluginMetadata.Name} (ID: {pluginMetadata.Id})", source: "PluginManager");
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(pluginMetadata, new CommandHandler(async () => await Remove(pluginPath, pluginMetadata.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath, csprojPath));
_ = pluginIds.Add(pluginMetadata.Id);
if (pluginMetadata.IsLocalPlugin)
{ {
continue; continue;
} }
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(pluginMetadataPath)); try
if (pluginMetadata is not null)
{ {
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(pluginMetadata, new CommandHandler(() => Remove(pluginPath, pluginMetadata.Id)), true)); Mod mod = await modIoClient.Games[ModIoGameId].Mods[pluginMetadata.Id].Get();
_ = pluginIds.Add(pluginMetadata.Id);
if (DateTimeOffset.FromUnixTimeSeconds(mod.DateUpdated).DateTime > pluginMetadata.Updated && pluginMetadata.SupportsUnloading)
{
App.Logger.LogInfo($"Plugin {pluginMetadata.Id} has an update available, reinstalling", source: "PluginManager");
await Remove(pluginPath, pluginMetadata.Id);
await Install(mod);
pluginManagerDataContext.IsLoading = true;
}
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to check for plugin {pluginMetadata.Id} updates: {ex.Message}", source: "PluginManager");
} }
} }
Filter filter = ModFilter.Popular.Desc().Limit(100); Filter filter = ModFilter.Popular.Desc().Limit(100);
IAsyncEnumerable<Mod> mods = client.Games[modIoGameId].Mods.Search(filter).ToEnumerableAsync(); try
await foreach (Mod mod in mods)
{ {
if (pluginIds.Contains(mod.Id)) App.Logger.LogInfo("Fetching popular plugins from mod.io", source: "PluginManager");
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods)
{ {
continue; if (pluginIds.Contains(mod.Id))
} {
continue;
}
pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)))); pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
}
App.Logger.LogInfo($"Loaded {pluginManagerDataContext.AllPlugins.Count} available plugins", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to fetch plugins from mod.io: {ex.Message}", source: "PluginManager");
}
await SyncPlugins();
pluginManagerDataContext.IsLoading = false;
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
}
public async Task Remove(string pluginPath, uint id)
{
App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
changed = true;
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
if (Directory.Exists(pluginPath))
{
try
{
Directory.Delete(pluginPath, true);
App.Logger.LogInfo($"Successfully deleted plugin directory: {pluginPath}", source: "PluginManager");
}
catch (Exception ex)
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
App.Logger.LogError(ex.Message, source: "PluginManager");
}
}
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
}
if (pluginManagerDataContext.IsAuthenticated)
{
try
{
await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
App.Logger.LogInfo($"Unsubscribed from plugin {id} on mod.io", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to unsubscribe from plugin {id}: {ex.Message}", source: "PluginManager");
}
} }
pluginManagerDataContext.IsLoading = false; pluginManagerDataContext.IsLoading = false;
} }
protected override void OnClosing(CancelEventArgs e) [GeneratedRegex(@"[^a-zA-Z0-9]")]
{ private static partial Regex IdentifierNameRegex();
e.Cancel = pluginManagerDataContext.IsLoading;
}
private async Task Install(Mod mod) private async Task Install(Mod mod)
{ {
App.Logger.LogInfo($"Installing plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
pluginManagerDataContext.IsLoading = true; pluginManagerDataContext.IsLoading = true;
changed = true;
if (mod.Modfile?.Download?.BinaryUrl is null) if (mod.Modfile?.Download?.BinaryUrl is null)
{ {
App.Logger.LogWarn($"Plugin {mod.Id} has no download URL", source: "PluginManager");
return; return;
} }
@@ -128,40 +248,77 @@ public partial class PluginManager : Window
string pluginPath = Path.Combine(pluginsPath, pluginGuid); string pluginPath = Path.Combine(pluginsPath, pluginGuid);
string zipFilePath = Path.Combine(pluginsPath, pluginGuid + ".zip"); string zipFilePath = Path.Combine(pluginsPath, pluginGuid + ".zip");
using (Stream fileStream = await httpClient.GetStreamAsync(mod.Modfile.Download.BinaryUrl)) try
{ {
using FileStream outputFileStream = new FileStream(zipFilePath, FileMode.Create); App.Logger.LogInfo($"Downloading plugin from: {mod.Modfile.Download.BinaryUrl}", source: "PluginManager");
using (Stream fileStream = await httpClient.GetStreamAsync(mod.Modfile.Download.BinaryUrl))
{
using FileStream outputFileStream = new FileStream(zipFilePath, FileMode.Create);
await fileStream.CopyToAsync(outputFileStream);
}
App.Logger.LogInfo($"Plugin downloaded to: {zipFilePath}", source: "PluginManager");
await fileStream.CopyToAsync(outputFileStream); using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
{
zipArchive.ExtractToDirectory(pluginPath);
}
App.Logger.LogInfo($"Plugin extracted to: {pluginPath}", source: "PluginManager");
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(new PluginMetadata(mod)));
File.Delete(zipFilePath);
if (!File.Exists(Path.Combine(pluginPath, "main.dll")) && !File.Exists(Path.Combine(pluginPath, "main.html")))
{
App.Logger.LogError($"Plugin {mod.Id} does not contain main.dll or main.html", source: "PluginManager");
await Remove(pluginPath, mod.Id);
pluginManagerDataContext.IsLoading = false;
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = "The plugin you are trying to install does not contain a \"main.dll\" or \"main.html\" file. Please contact the plugin author.",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.AllPlugins.FirstOrDefault(p => p.Id == mod.Id);
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.AllPlugins.Remove(pluginEntryDataContext);
}
if (pluginManagerDataContext.IsAuthenticated)
{
try
{
await modIoClient.Games[ModIoGameId].Mods.Subscribe(mod.Id);
App.Logger.LogInfo($"Subscribed to plugin {mod.Id} on mod.io", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to subscribe to plugin {mod.Id}: {ex.Message}", source: "PluginManager");
}
}
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(new PluginMetadata(mod), new CommandHandler(async () => await Remove(pluginPath, mod.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
App.Logger.LogInfo($"Successfully installed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
} }
catch (Exception ex)
using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
{ {
zipArchive.ExtractToDirectory(pluginPath); App.Logger.LogError($"Failed to install plugin {mod.Id}: {ex.Message}", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = $"Failed to install plugin: {ex.Message}",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
} }
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
File.WriteAllText(pluginMetadataPath, JsonSerializer.Serialize(new PluginMetadata(mod)));
File.Delete(zipFilePath);
if (!File.Exists(Path.Combine(pluginPath, "main.dll")))
{
Remove(pluginPath, mod.Id);
pluginManagerDataContext.IsLoading = false;
_ = MessageBox.Show("The plugin you are trying to install does not contain a \"main.dll\" file. Please contact the plugin author.", "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
return;
}
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.AllPlugins.FirstOrDefault(p => p.Id == mod.Id);
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.AllPlugins.Remove(pluginEntryDataContext);
}
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(new PluginMetadata(mod), new CommandHandler(() => Remove(pluginPath, mod.Id)), true));
pluginManagerDataContext.IsLoading = false; pluginManagerDataContext.IsLoading = false;
} }
@@ -174,6 +331,7 @@ public partial class PluginManager : Window
FileName = uri FileName = uri
}; };
_ = Process.Start(psi); _ = Process.Start(psi);
App.Logger.LogInfo($"Opened mod.io page: {uri}", source: "PluginManager");
} }
private void AllPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) private void AllPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
@@ -203,6 +361,7 @@ public partial class PluginManager : Window
private async Task SearchAllPlugins(string searchString) private async Task SearchAllPlugins(string searchString)
{ {
App.Logger.LogInfo($"Searching plugins with query: {searchString}", source: "PluginManager");
pluginManagerDataContext.AllPlugins.Clear(); pluginManagerDataContext.AllPlugins.Clear();
if (!searchString.Contains('*')) if (!searchString.Contains('*'))
@@ -212,17 +371,446 @@ public partial class PluginManager : Window
Filter filter = ModFilter.Name.Like($"{searchString}").And(ModFilter.Popular.Desc()).Limit(100); Filter filter = ModFilter.Name.Like($"{searchString}").And(ModFilter.Popular.Desc()).Limit(100);
IAsyncEnumerable<Mod> mods = client.Games[modIoGameId].Mods.Search(filter).ToEnumerableAsync(); try
await foreach (Mod mod in mods)
{ {
if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == mod.Id)) IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods)
{ {
continue; if (pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == mod.Id))
} {
continue;
}
pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)))); pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
}
App.Logger.LogInfo($"Search completed: {pluginManagerDataContext.AllPlugins.Count} plugins found", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Plugin search failed: {ex.Message}", source: "PluginManager");
} }
pluginManagerDataContext.IsSearching = false; pluginManagerDataContext.IsSearching = false;
} }
private async void ReloadPluginsButton_Click(object sender, RoutedEventArgs e)
{
_manager.ReloadPlugins();
await InitializePluginManager();
changed = false;
}
private async void CreatePluginButton_Click(object sender, RoutedEventArgs e)
{
try
{
await CreateNewPlugin();
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
}
private async Task CreateNewPlugin()
{
App.Logger.LogInfo("Creating new plugin", source: "PluginManager");
if (!FileUtilities.ExistsOnPath("dotnet.exe"))
{
App.Logger.LogError(".NET SDK not found on PATH", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = "The .NET SDK is required to create a plugin. Please install it and try again.",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
string pluginGuid = Guid.NewGuid().ToString();
string pluginPath = Path.Combine(pluginsPath, pluginGuid);
uint pluginId = (uint)Random.Shared.Next(1000, 9999);
CreatePluginDialog inputDialog = new()
{
Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
App.Logger.LogInfo("Plugin creation cancelled by user", source: "PluginManager");
return;
}
string pluginName = inputDialog.ResponseText;
string pluginSafeName = GetPluginSafeName(pluginName);
App.Logger.LogInfo($"Creating plugin with name: {pluginName} (safe name: {pluginSafeName})", source: "PluginManager");
if (!Directory.Exists(pluginDevelopmentPath))
{
_ = Directory.CreateDirectory(pluginDevelopmentPath);
}
string pluginProjectPath = Path.Combine(pluginDevelopmentPath, pluginSafeName);
if (Directory.Exists(pluginProjectPath))
{
App.Logger.LogWarn($"Plugin project already exists: {pluginProjectPath}", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = "A plugin with the same name already exists. Please choose a different name.",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
pluginManagerDataContext.IsLoading = true;
PluginMetadata pluginMetadata = new(pluginName, pluginId);
_ = Directory.CreateDirectory(pluginPath);
_ = Directory.CreateDirectory(pluginProjectPath);
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(pluginMetadata));
if (inputDialog.IsWebPlugin)
{
await CreateNewWebPlugin(pluginPath, pluginName);
changed = true;
await InitializePluginManager();
pluginManagerDataContext.IsLoading = false;
return;
}
App.Logger.LogInfo($"Creating .NET project at: {pluginProjectPath}", source: "PluginManager");
string cmd = $"new classlib -n {pluginSafeName} -o {pluginProjectPath} -f net8.0 --target-framework-override net8.0-windows7";
Process process = Process.Start("dotnet", cmd);
await process.WaitForExitAsync();
App.Logger.LogInfo("Installing NuGet package: DesktopMagic.Api", source: "PluginManager");
process = Process.Start("dotnet", $"add {pluginProjectPath} package DesktopMagic.Api");
await process.WaitForExitAsync();
File.Move(Path.Combine(pluginProjectPath, "Class1.cs"), Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"));
string code = $@"using DesktopMagic.Api;
using System.Drawing;
namespace {pluginSafeName};
public class {pluginSafeName}Plugin : Plugin
{{
public override Bitmap Main()
{{
Bitmap bmp = new Bitmap(2000, 1000);
bmp.SetResolution(100, 100); // Set DPI to avoid scaling issues.
using (Graphics g = Graphics.FromImage(bmp))
{{
g.DrawString(""Hello World!"", new Font(Application.Theme.Font, 100), new SolidBrush(Application.Theme.PrimaryColor), new PointF(0, 0)); // Draw ""Hello World"" to the image.
}}
return bmp; // Return the image.
}}
}}
";
string csprojPath = Path.Combine(pluginProjectPath, $"{pluginSafeName}.csproj");
await File.WriteAllTextAsync(Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"), code);
XDocument doc = XDocument.Load(csprojPath);
XElement? propertyGroup = doc.Root?.Element("PropertyGroup");
if (propertyGroup is not null)
{
XElement? outputPathElement = propertyGroup.Element("OutputPath");
if (outputPathElement is not null)
{
outputPathElement.Value = pluginPath;
}
else
{
propertyGroup.Add(new XElement("OutputPath", pluginPath));
}
XElement? appendTargetFrameworkToOutputPathElement = propertyGroup.Element("AppendTargetFrameworkToOutputPath");
if (appendTargetFrameworkToOutputPathElement is not null)
{
appendTargetFrameworkToOutputPathElement.Value = "false";
}
else
{
propertyGroup.Add(new XElement("AppendTargetFrameworkToOutputPath", "false"));
}
XElement? targetNameElement = propertyGroup.Element("TargetName");
if (targetNameElement is not null)
{
targetNameElement.Value = "main";
}
else
{
propertyGroup.Add(new XElement("TargetName", "main"));
}
doc.Save(csprojPath);
App.Logger.LogInfo("Updated .csproj file with custom build settings", source: "PluginManager");
}
else
{
App.Logger.LogError("PropertyGroup element not found in .csproj", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Error",
Content = "PropertyGroup element not found in .csproj",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
App.Logger.LogInfo("Building plugin project", source: "PluginManager");
process = Process.Start("dotnet", $"build {csprojPath} -c Release");
await process.WaitForExitAsync();
changed = true;
await InitializePluginManager();
string? associatedProgram = FileUtilities.GetAssociatedProgram(".csproj");
if (associatedProgram is null)
{
App.Logger.LogInfo($"Opening plugin project in Explorer: {pluginProjectPath}", source: "PluginManager");
_ = Process.Start("explorer.exe", pluginProjectPath);
}
else
{
App.Logger.LogInfo($"Opening plugin project in IDE: {associatedProgram}", source: "PluginManager");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = associatedProgram,
Arguments = csprojPath,
};
_ = Process.Start(psi);
}
App.Logger.LogInfo($"Successfully created plugin: {pluginName}", source: "PluginManager");
pluginManagerDataContext.IsLoading = false;
}
private async Task CreateNewWebPlugin(string pluginPath, string pluginName)
{
App.Logger.LogInfo($"Creating web plugin at: {pluginPath}", source: "PluginManager");
string mainHtml = $@"<!DOCTYPE html>
<html>
<head>
<meta charset=""utf-8"">
<style>
body {{ margin: 0; padding: 16px; font-family: var(--font-family); }}
h1 {{ color: var(--primary-color); }}
</style>
</head>
<body>
<h1 id=""greeting"">{pluginName}</h1>
<script>
function render() {{
const s = window.desktopMagic.getSettings();
document.getElementById('greeting').innerHTML = s.message;
document.getElementById('greeting').style.color = s.color;
document.getElementById('greeting').style.fontWeight = s.bold ? 'bold' : 'normal';
}}
window.desktopMagic.onSettingChanged = (id, value) => render();
render();
</script>
</body>
</html>";
var settings = new List<Dictionary<string, object?>>
{
new() { ["id"] = "message", ["name"] = "Greeting Message", ["type"] = "textbox", ["default"] = "Hello, World!" },
new() { ["id"] = "bold", ["name"] = "Bold Text", ["type"] = "checkbox", ["default"] = true },
new() { ["id"] = "color", ["name"] = "Text Color", ["type"] = "colorpicker", ["default"] = "#FF5722" }
};
string settingsJson = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(Path.Combine(pluginPath, "settings.json"), settingsJson);
await File.WriteAllTextAsync(Path.Combine(pluginPath, "main.html"), mainHtml);
App.Logger.LogInfo($"Successfully created web plugin: {pluginName}", source: "PluginManager");
}
private async void LogInButton_Click(object sender, RoutedEventArgs e)
{
if (pluginManagerDataContext.IsAuthenticated)
{
App.Logger.LogInfo("Logging out from mod.io", source: "PluginManager");
MainWindowDataContext.GetSettings().ModIoAccessToken = null;
pluginManagerDataContext.IsAuthenticated = false;
return;
}
App.Logger.LogInfo("Starting mod.io authentication", source: "PluginManager");
try
{
InputDialog inputDialog = new((string)FindResource("enterModIoEmail"), "Plugin Manager")
{
Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return;
}
App.Logger.LogInfo($"Requesting authentication code for email: {inputDialog.ResponseText}", source: "PluginManager");
await modIoClient.Auth.RequestCode(ModIoApiKey, inputDialog.ResponseText);
inputDialog = new((string)FindResource("enterModIoAccessToken"), "Plugin Manager")
{
Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return;
}
pluginManagerDataContext.IsLoading = true;
AccessToken accessToken = await modIoClient.Auth.SecurityCode(ModIoApiKey, inputDialog.ResponseText);
if (accessToken.Value is not null)
{
modIoClient = new Client(new Credentials(ModIoApiKey, accessToken.Value));
App.Logger.LogInfo("Successfully authenticated with mod.io", source: "PluginManager");
}
MainWindowDataContext.GetSettings().ModIoAccessToken = accessToken.Value;
pluginManagerDataContext.IsAuthenticated = true;
App.Logger.LogInfo("Subscribing to installed plugins on mod.io", source: "PluginManager");
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{
if (!plugin.IsLocalPlugin)
{
try
{
await modIoClient.Games[ModIoGameId].Mods.Subscribe(plugin.Id);
App.Logger.LogInfo($"Subscribed to plugin {plugin.Id}", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to subscribe to plugin {plugin.Id}: {ex.Message}", source: "PluginManager");
}
}
}
await SyncPlugins();
}
catch (Exception ex)
{
App.Logger.LogError($"Authentication failed: {ex.Message}", source: "PluginManager");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
pluginManagerDataContext.IsLoading = false;
}
private async Task SyncPlugins()
{
if (!pluginManagerDataContext.IsAuthenticated)
{
return;
}
App.Logger.LogInfo("Syncing plugins with mod.io subscriptions", source: "PluginManager");
try
{
IReadOnlyList<Mod> mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
App.Logger.LogInfo($"Found {mods.Count} subscribed plugins on mod.io", source: "PluginManager");
List<PluginEntryDataContext> pluginsToRemove = [];
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{
if (!plugin.IsLocalPlugin && !mods.Any(m => m.Id == plugin.Id))
{
App.Logger.LogInfo($"Plugin {plugin.Id} is no longer subscribed, marking for removal", source: "PluginManager");
pluginsToRemove.Add(plugin);
}
}
foreach (PluginEntryDataContext plugin in pluginsToRemove)
{
if (plugin.Path is null)
{
continue;
}
await Remove(plugin.Path, plugin.Id);
}
IEnumerable<Mod> notInstalledMods = mods.Where(m => !pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == m.Id));
foreach (Mod mod in notInstalledMods)
{
App.Logger.LogInfo($"Installing subscribed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
await Install(mod);
}
App.Logger.LogInfo("Plugin sync completed", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Plugin sync failed: {ex.Message}", source: "PluginManager");
}
}
private static string GetPluginSafeName(string pluginName)
{
string pluginSafeName = pluginName.ToLower().Replace("_", " ");
TextInfo info = CultureInfo.CurrentCulture.TextInfo;
pluginSafeName = info.ToTitleCase(pluginSafeName);
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
return pluginSafeName;
}
private string GetCsprojPath(string pluginName)
{
string pluginSafeName = GetPluginSafeName(pluginName);
return Path.Combine(pluginDevelopmentPath, pluginSafeName, $"{pluginSafeName}.csproj");
}
} }
+18 -1
View File
@@ -1,6 +1,8 @@
using Modio.NET.Models; using Modio.Models;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DesktopMagic.Plugins; namespace DesktopMagic.Plugins;
@@ -15,24 +17,39 @@ public class PluginMetadata
public Uri? IconUri { get; set; } public Uri? IconUri { get; set; }
public Uri? ProfileUri { get; set; }
public DateTime? Added { get; set; } public DateTime? Added { get; set; }
public DateTime? Updated { get; set; } public DateTime? Updated { get; set; }
public string? Description { get; set; } public string? Description { get; set; }
public string? Summary { get; set; }
public string? Version { get; set; } public string? Version { get; set; }
public List<string> Tags { get; set; } = [];
[JsonIgnore]
public bool SupportsUnloading => !Tags.Contains("Does Not Support Unloading");
[JsonIgnore]
public bool IsLocalPlugin => string.IsNullOrWhiteSpace(ProfileUri?.ToString());
public PluginMetadata(Mod mod) public PluginMetadata(Mod mod)
{ {
Name = mod.Name ?? mod.Id.ToString(); Name = mod.Name ?? mod.Id.ToString();
Id = mod.Id; Id = mod.Id;
Author = mod.SubmittedBy?.Username; Author = mod.SubmittedBy?.Username;
IconUri = mod.Logo?.Thumb320x180; IconUri = mod.Logo?.Thumb320x180;
ProfileUri = mod.ProfileUrl;
Added = DateTimeOffset.FromUnixTimeSeconds(mod.DateAdded).DateTime; Added = DateTimeOffset.FromUnixTimeSeconds(mod.DateAdded).DateTime;
Updated = DateTimeOffset.FromUnixTimeSeconds(mod.DateUpdated).DateTime; Updated = DateTimeOffset.FromUnixTimeSeconds(mod.DateUpdated).DateTime;
Description = mod.DescriptionPlaintext; Description = mod.DescriptionPlaintext;
Summary = mod.Summary;
Version = mod.Modfile?.Version; Version = mod.Modfile?.Version;
Tags = mod.Tags.Select(t => t.Name).Where(t => t is not null).ToList()!;
} }
public PluginMetadata(string name, uint id) public PluginMetadata(string name, uint id)
+16 -12
View File
@@ -1,6 +1,7 @@
<Window x:Class="DesktopMagic.PluginWindow" <Window x:Class="DesktopMagic.PluginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopMagic" xmlns:local="clr-namespace:DesktopMagic"
@@ -8,25 +9,28 @@
Title="PluginWindow" Height="450" Width="800" Title="PluginWindow" Height="450" Width="800"
Background="Transparent" Background="Transparent"
WindowStyle="None" WindowStyle="None"
ShowInTaskbar="False"
AllowsTransparency="True" AllowsTransparency="True"
LocationChanged="Window_LocationChanged" LocationChanged="Window_LocationChanged"
SizeChanged="Window_SizeChanged" SizeChanged="Window_SizeChanged"
Closing="Window_Closing" Closing="Window_Closing"
ContentRendered="Window_ContentRendered" ContentRendered="Window_ContentRendered"
x:Name="window"> x:Name="window">
<Grid> <busyIndicator:BusyMask x:Name="busyMask" IsBusy="True" Background="Transparent" BusyContent="" BusyContentMargin="-5" IndicatorType="Cogs">
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" /> <Grid>
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
<Border x:Name="border" /> <Border SizeChanged="Border_SizeChanged" x:Name="border" />
<Viewbox x:Name="viewBox" StretchDirection="Both" Stretch="Uniform"> <Viewbox SizeChanged="ViewBox_SizeChanged" x:Name="viewBox" StretchDirection="Both" Stretch="Uniform">
<Viewbox.Clip> <Viewbox.Clip>
<RectangleGeometry x:Name="rectangleGeometry" RadiusX="{Binding ElementName=border, Path=CornerRadius.TopLeft}" RadiusY="{Binding ElementName=border, Path=CornerRadius.TopLeft}" /> <RectangleGeometry x:Name="rectangleGeometry" RadiusX="{Binding ElementName=border, Path=CornerRadius.TopLeft}" RadiusY="{Binding ElementName=border, Path=CornerRadius.TopLeft}" />
</Viewbox.Clip> </Viewbox.Clip>
<Border x:Name="imageBorder" BorderBrush="#BAFFEF"> <Border x:Name="imageBorder" BorderBrush="#BAFFEF">
<Image x:Name="image" RenderOptions.EdgeMode="Aliased" Margin="0,0,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" Stretch="Uniform" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseWheel="Window_MouseWheel" /> <Image x:Name="image" RenderOptions.EdgeMode="Aliased" Margin="0,0,0,0" VerticalAlignment="Top" RenderTransformOrigin="0.5,0.5" Stretch="Uniform" MouseDown="Window_MouseDown" MouseMove="Window_MouseMove" MouseWheel="Window_MouseWheel" />
</Border> </Border>
</Viewbox> </Viewbox>
</Grid> </Grid>
</busyIndicator:BusyMask>
<WindowChrome.WindowChrome> <WindowChrome.WindowChrome>
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" /> <WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome> </WindowChrome.WindowChrome>
+643 -104
View File
@@ -1,6 +1,7 @@
using DesktopMagic.Api; using DesktopMagic.Api;
using DesktopMagic.Api.Drawing; using DesktopMagic.Api.Drawing;
using DesktopMagic.Api.Settings; using DesktopMagic.Api.Settings;
using DesktopMagic.DataContexts;
using DesktopMagic.Helpers; using DesktopMagic.Helpers;
using DesktopMagic.Plugins; using DesktopMagic.Plugins;
using DesktopMagic.Settings; using DesktopMagic.Settings;
@@ -12,7 +13,10 @@ using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Timers; using System.Timers;
using System.Windows; using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
@@ -21,17 +25,25 @@ using System.Windows.Media.Imaging;
namespace DesktopMagic; namespace DesktopMagic;
public partial class PluginWindow : Window public partial class PluginWindow : Window, IPluginWindow
{ {
public event Action? PluginLoaded; public event Action? PluginLoaded;
public event Action? OnExit; public event Action? OnExit;
private readonly PluginSettings settings; private readonly PluginSettings settings;
private readonly System.Timers.Timer? updateTimer;
private Thread? pluginThread; private Thread? pluginThread;
private System.Timers.Timer? valueTimer; private System.Timers.Timer? updateTimer;
private Plugin? pluginClassInstance; private Plugin? pluginClassInstance;
private AssemblyLoadContext assemblyLoadContext;
private CancellationTokenSource? pluginCancellationTokenSource;
private FileSystemWatcher? pluginFileWatcher;
private System.Timers.Timer? reloadDebounceTimer;
private bool isReloading = false;
private WriteableBitmap? writeableBitmap;
private BitmapScalingMode lastBitmapScalingMode = BitmapScalingMode.Unspecified;
public bool IsRunning { get; private set; } = true; public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; } public PluginMetadata PluginMetadata { get; private set; }
@@ -51,16 +63,28 @@ public partial class PluginWindow : Window
WindowStyle = WindowStyle.ToolWindow, WindowStyle = WindowStyle.ToolWindow,
ShowInTaskbar = false ShowInTaskbar = false
}; };
w.Show();
Owner = w;
w.Hide();
updateTimer = new System.Timers.Timer WindowInteropHelper helper = new WindowInteropHelper(w);
_ = helper.EnsureHandle();
Owner = w;
settings.PropertyChanged += (e, s) =>
{ {
Interval = 100 if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
{
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
ThemeChanged();
}
};
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
}; };
updateTimer.Elapsed += UpdateTimer_Elapsed;
updateTimer.Start();
PluginMetadata = pluginMetadata; PluginMetadata = pluginMetadata;
this.settings = settings; this.settings = settings;
@@ -71,6 +95,14 @@ public partial class PluginWindow : Window
Height = settings.Size.Y; Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath; PluginFolderPath = pluginFolderPath;
assemblyLoadContext = CreateAssemblyLoadContext();
// Initialize hot reload watcher if plugin supports unloading and is external
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
{
InitializeHotReload();
}
} }
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty) public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty)
@@ -78,9 +110,188 @@ public partial class PluginWindow : Window
this.pluginClassInstance = pluginClassInstance; this.pluginClassInstance = pluginClassInstance;
} }
private AssemblyLoadContext CreateAssemblyLoadContext()
{
AssemblyLoadContext context = new(PluginMetadata.Name, isCollectible: true);
context.Resolving += (ctx, assemblyName) =>
{
string assemblyPath = Path.Combine(PluginFolderPath, assemblyName.Name + ".dll");
if (File.Exists(assemblyPath))
{
return ctx.LoadFromAssemblyPath(assemblyPath);
}
else
{
_ = ctx.LoadFromAssemblyName(assemblyName);
}
return null;
};
return context;
}
private void InitializeHotReload()
{
try
{
pluginFileWatcher = new FileSystemWatcher(PluginFolderPath)
{
Filter = "main.dll",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = false
};
pluginFileWatcher.Changed += OnPluginFileChanged;
reloadDebounceTimer = new System.Timers.Timer(500); // 500ms debounce
reloadDebounceTimer.Elapsed += async (s, e) =>
{
reloadDebounceTimer!.Stop();
await ReloadPlugin();
};
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "Plugin");
}
}
private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
{
if (isReloading)
{
return;
}
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin file changed, scheduling reload", source: "Plugin");
// Debounce multiple file change events
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Start();
}
private async Task StopPlugin(bool unloadAssembly = true)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
try
{
pluginCancellationTokenSource?.Cancel();
if (pluginClassInstance is AsyncPlugin asyncPluginStop)
{
try
{
await asyncPluginStop.StopAsync(pluginCancellationTokenSource?.Token ?? CancellationToken.None);
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping async plugin: {ex.Message}", source: "Plugin");
}
}
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
pluginClassInstance.Stop();
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping plugin: {ex}", source: "Plugin");
}
if (unloadAssembly)
{
assemblyLoadContext.Unload();
}
pluginCancellationTokenSource?.Dispose();
pluginCancellationTokenSource = null;
}
private async Task ReloadPlugin()
{
if (isReloading || !IsRunning)
{
return;
}
isReloading = true;
try
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading plugin", source: "Plugin");
// Stop the file watcher during reload
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = false;
}
updateTimer?.Stop();
await StopPlugin(unloadAssembly: true);
// Wait for file to be fully written and released
await Task.Delay(200);
// Clear the current instance
pluginClassInstance = null;
// Create a new AssemblyLoadContext since the old one was unloaded
assemblyLoadContext = CreateAssemblyLoadContext();
// Show busy indicator
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = true);
// Reload the plugin
await ExecuteSource();
// Hide busy indicator
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin reloaded successfully", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload plugin: {ex}", source: "Plugin");
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Reload Error \"{PluginMetadata.Name}\"",
Content = $"Failed to reload plugin:\n{ex.Message}",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
}
finally
{
isReloading = false;
// Re-enable file watcher
if (pluginFileWatcher != null && IsRunning)
{
pluginFileWatcher.EnableRaisingEvents = true;
}
}
}
public void UpdatePluginWindow() public void UpdatePluginWindow()
{ {
ValueTimer_Elapsed(valueTimer, null); UpdateTimer_Elapsed(updateTimer, null);
}
public void SavePluginState()
{
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
}
} }
public void Exit() public void Exit()
@@ -93,6 +304,31 @@ public partial class PluginWindow : Window
}); });
} }
public void SetEditMode(bool enabled)
{
if (enabled)
{
Topmost = true;
panel.Visibility = Visibility.Visible;
imageBorder.BorderThickness = new Thickness(3);
image.Margin = new(-3);
WindowPos.SetIsLocked(this, false);
tileBar.CaptionHeight = tileBar.CaptionHeight = ActualHeight - 10 < 0 ? 0 : ActualHeight - 10;
ResizeMode = ResizeMode.CanResize;
}
else
{
Topmost = false;
panel.Visibility = Visibility.Collapsed;
imageBorder.BorderThickness = new Thickness(0);
image.Margin = new Thickness(0);
WindowPos.SetWindowLayer(this, pluginClassInstance?.windowLayer.Value ?? "Always on Bottom");
WindowPos.SetIsLocked(window, true);
tileBar.CaptionHeight = 0;
ResizeMode = ResizeMode.NoResize;
}
}
protected override void OnSourceInitialized(EventArgs e) protected override void OnSourceInitialized(EventArgs e)
{ {
base.OnSourceInitialized(e); base.OnSourceInitialized(e);
@@ -103,81 +339,78 @@ public partial class PluginWindow : Window
WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE); WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE);
} }
private static BitmapSource BitmapToImageSource(Bitmap bitmap) private void UpdateImageFromBitmap(Bitmap bitmap, BitmapScalingMode scalingMode)
{ {
BitmapData bitmapData = bitmap.LockBits( BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly, bitmap.PixelFormat);
BitmapSource bitmapSource = BitmapSource.Create( try
bitmapData.Width, bitmapData.Height, {
bitmap.HorizontalResolution, bitmap.VerticalResolution, Dispatcher.Invoke(() =>
PixelFormats.Bgra32, null, {
bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride); if (lastBitmapScalingMode != scalingMode)
{
RenderOptions.SetBitmapScalingMode(image, scalingMode);
lastBitmapScalingMode = scalingMode;
}
bitmap.UnlockBits(bitmapData); if (writeableBitmap == null || writeableBitmap.PixelWidth != bitmapData.Width || writeableBitmap.PixelHeight != bitmapData.Height)
return bitmapSource; {
writeableBitmap = new WriteableBitmap(
bitmapData.Width, bitmapData.Height,
bitmap.HorizontalResolution, bitmap.VerticalResolution,
PixelFormats.Bgra32, null);
image.Source = writeableBitmap;
}
writeableBitmap.Lock();
try
{
unsafe
{
Buffer.MemoryCopy(
bitmapData.Scan0.ToPointer(),
writeableBitmap.BackBuffer.ToPointer(),
writeableBitmap.BackBufferStride * writeableBitmap.PixelHeight,
bitmapData.Stride * bitmapData.Height);
}
writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, bitmapData.Width, bitmapData.Height));
}
finally
{
writeableBitmap.Unlock();
}
});
}
finally
{
bitmap.UnlockBits(bitmapData);
}
} }
private void Window_ContentRendered(object? sender, EventArgs e) private void Window_ContentRendered(object? sender, EventArgs e)
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting plugin thread", source: "Plugin"); App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting plugin thread", source: "Plugin");
pluginThread = new Thread(LoadPlugin); pluginThread = new Thread(async () => await LoadPlugin());
pluginThread.Start(); pluginThread.Start();
} }
private void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs e) private async Task LoadPlugin()
{
Dispatcher.Invoke(() =>
{
if (MainWindow.EditMode)
{
panel.Visibility = Visibility.Visible;
imageBorder.BorderThickness = new Thickness(3);
image.Margin = new(-3);
WindowPos.SetIsLocked(this, false);
tileBar.CaptionHeight = tileBar.CaptionHeight = ActualHeight - 10 < 0 ? 0 : ActualHeight - 10;
ResizeMode = ResizeMode.CanResize;
}
else
{
panel.Visibility = Visibility.Collapsed;
imageBorder.BorderThickness = new Thickness(0);
image.Margin = new Thickness(0);
WindowPos.SetIsLocked(this, true);
tileBar.CaptionHeight = 0;
ResizeMode = ResizeMode.NoResize;
}
if (!IsRunning)
{
(sender as System.Timers.Timer)?.Stop();
}
else
{
viewBox.Margin = new Thickness(settings.Theme.Margin);
border.Width = viewBox.ActualWidth + (settings.Theme.Margin * 2);
border.Height = viewBox.ActualHeight + (settings.Theme.Margin * 2);
rectangleGeometry.Rect = new Rect(-settings.Theme.Margin, -settings.Theme.Margin, border.ActualWidth, border.ActualHeight);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
}
});
if (!IsRunning)
{
updateTimer?.Stop();
}
}
private void LoadPlugin()
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin", source: "Plugin"); App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin", source: "Plugin");
if (pluginClassInstance is null && !File.Exists($"{PluginFolderPath}\\main.dll")) if (pluginClassInstance is null && !File.Exists($"{PluginFolderPath}\\main.dll"))
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.dll\" does not exist", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.dll\" does not exist", source: "Plugin");
_ = MessageBox.Show("File \"main.dll\" does not exist!", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File \"main.dll\" does not exist!",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
@@ -185,31 +418,84 @@ public partial class PluginWindow : Window
try try
{ {
ExecuteSource(); await ExecuteSource();
} }
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
PluginLoaded?.Invoke(); PluginLoaded?.Invoke();
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
// Enable hot reload after initial load
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = true;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload monitoring started", source: "Plugin");
}
} }
private void ExecuteSource() private void ThemeChanged()
{
viewBox.Margin = new Thickness(settings.Theme.Margin);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
pluginClassInstance?.OnThemeChanged();
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
{
pluginClassInstance.Application.UpdateWindow();
}
}
private async Task ExecuteSource()
{ {
object? instance = pluginClassInstance; object? instance = pluginClassInstance;
if (instance is null) if (instance is null)
{ {
Assembly dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll"); Assembly dll;
if (PluginMetadata.SupportsUnloading)
{
byte[] assemblyData = await File.ReadAllBytesAsync($"{PluginFolderPath}\\main.dll");
using MemoryStream assemblyStream = new(assemblyData);
dll = assemblyLoadContext.LoadFromStream(assemblyStream);
}
else
{
dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll");
}
Type? instanceType = Array.Find(dll.GetTypes(), type => type.GetTypeInfo().BaseType == typeof(Plugin)); Type? instanceType = Array.Find(dll.GetTypes(), type => type.GetTypeInfo().BaseType == typeof(Plugin));
if (instanceType is null) if (instanceType is null)
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
_ = MessageBox.Show($"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = $"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
@@ -219,32 +505,49 @@ public partial class PluginWindow : Window
if (instance is Plugin plugin) if (instance is Plugin plugin)
{ {
pluginClassInstance = plugin; pluginClassInstance = plugin;
pluginClassInstance.Application = new Plugins.PluginData(this, settings); pluginClassInstance.Application = new PluginData(this, settings);
} }
else else
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
_ = MessageBox.Show($"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = $"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
LoadOptions(pluginClassInstance); LoadState(pluginClassInstance);
await LoadOptions(pluginClassInstance);
BindDefaultSettings(pluginClassInstance); BindDefaultSettings(pluginClassInstance);
valueTimer = new System.Timers.Timer updateTimer = new System.Timers.Timer
{ {
Interval = 1000 Interval = 1000
}; };
valueTimer.Elapsed += ValueTimer_Elapsed; updateTimer.Elapsed += UpdateTimer_Elapsed;
pluginClassInstance.Start(); pluginClassInstance.Start();
if (pluginClassInstance is AsyncPlugin asyncPluginStart)
{
pluginCancellationTokenSource?.Dispose();
pluginCancellationTokenSource = new CancellationTokenSource();
await asyncPluginStart.StartAsync(pluginCancellationTokenSource.Token);
}
UpdatePluginWindow(); UpdatePluginWindow();
await Dispatcher.InvokeAsync(ThemeChanged);
if (pluginClassInstance.UpdateInterval > 0) if (pluginClassInstance.UpdateInterval > 0)
{ {
valueTimer.Interval = pluginClassInstance.UpdateInterval; updateTimer.Interval = pluginClassInstance.UpdateInterval;
valueTimer.Start(); updateTimer.Start();
} }
} }
@@ -254,44 +557,92 @@ public partial class PluginWindow : Window
{ {
SetHorizontalAlignment(); SetHorizontalAlignment();
SetVerticalAlignment(); SetVerticalAlignment();
SetWindowLayer();
SetRotation();
SetThemeOverride();
SetThemeOverrideItems();
pluginClassInstance.horizontalAlignment.OnValueChanged += SetHorizontalAlignment; pluginClassInstance.horizontalAlignment.OnValueChanged += SetHorizontalAlignment;
pluginClassInstance.verticalAlignment.OnValueChanged += SetVerticalAlignment; pluginClassInstance.verticalAlignment.OnValueChanged += SetVerticalAlignment;
pluginClassInstance.windowLayer.OnValueChanged += SetWindowLayer;
pluginClassInstance.rotation.OnValueChanged += SetRotation;
pluginClassInstance.themeOverride.OnValueChanged += SetThemeOverride;
DesktopMagicSettings desktopMagicSettings = MainWindowDataContext.GetSettings();
desktopMagicSettings.Themes.CollectionChanged += (s, e) => SetThemeOverrideItems();
}); });
void SetVerticalAlignment() void SetVerticalAlignment()
{ {
viewBox.VerticalAlignment = pluginClassInstance.verticalAlignment.Value switch VerticalAlignment verticalAlignment = pluginClassInstance.verticalAlignment.Value switch
{ {
"Top" => VerticalAlignment.Top, "Top" => VerticalAlignment.Top,
"Center" => VerticalAlignment.Center, "Center" => VerticalAlignment.Center,
"Bottom" => VerticalAlignment.Bottom, "Bottom" => VerticalAlignment.Bottom,
_ => VerticalAlignment.Center _ => VerticalAlignment.Center
}; };
viewBox.VerticalAlignment = verticalAlignment;
border.VerticalAlignment = verticalAlignment;
} }
void SetHorizontalAlignment() void SetHorizontalAlignment()
{ {
viewBox.HorizontalAlignment = pluginClassInstance.horizontalAlignment.Value switch HorizontalAlignment horizontalAlignment = pluginClassInstance.horizontalAlignment.Value switch
{ {
"Left" => HorizontalAlignment.Left, "Left" => HorizontalAlignment.Left,
"Center" => HorizontalAlignment.Center, "Center" => HorizontalAlignment.Center,
"Right" => HorizontalAlignment.Right, "Right" => HorizontalAlignment.Right,
_ => HorizontalAlignment.Center _ => HorizontalAlignment.Center
}; };
viewBox.HorizontalAlignment = horizontalAlignment;
border.HorizontalAlignment = horizontalAlignment;
}
void SetWindowLayer()
{
WindowPos.SetWindowLayer(this, pluginClassInstance.windowLayer.Value);
}
void SetRotation()
{
image.LayoutTransform = new RotateTransform(pluginClassInstance.rotation.Value);
}
void SetThemeOverride()
{
if (pluginClassInstance.themeOverride.Value == "<None>")
{
settings.CurrentThemeName = null;
}
else
{
settings.CurrentThemeName = pluginClassInstance.themeOverride.Value;
}
}
void SetThemeOverrideItems()
{
DesktopMagicSettings desktopMagicSettings = MainWindowDataContext.GetSettings();
pluginClassInstance.themeOverride.Items.Clear();
pluginClassInstance.themeOverride.Items.Add("<None>");
foreach (Theme theme in desktopMagicSettings.Themes)
{
pluginClassInstance.themeOverride.Items.Add(theme.Name);
}
} }
} }
private void LoadOptions(object instance) private async Task LoadOptions(object instance)
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin options", source: "Plugin"); App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin options", source: "Plugin");
try try
{ {
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
FieldInfo[] props = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.GetField); FieldInfo[] props = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.GetField);
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
List<SettingElement> settingElements = []; List<SettingElement> settingElements = [];
foreach (FieldInfo prop in props) foreach (FieldInfo prop in props)
{ {
@@ -310,6 +661,16 @@ public partial class PluginWindow : Window
settingElement.JsonValue = settingsSettingElement.JsonValue; settingElement.JsonValue = settingsSettingElement.JsonValue;
} }
element.OnValueChanged += () =>
{
pluginClassInstance?.OnSettingsChanged();
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
{
pluginClassInstance.Application.UpdateWindow();
}
};
settingElements.Add(settingElement); settingElements.Add(settingElement);
break; break;
} }
@@ -323,26 +684,175 @@ public partial class PluginWindow : Window
{ {
IsRunning = false; IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
} }
} }
private void ValueTimer_Elapsed(object? sender, ElapsedEventArgs? e) private void LoadState(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin state", source: "Plugin");
try
{
if (settings.State.Count == 0)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - No state found, using defaults", source: "Plugin");
return;
}
FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
// Load fields
foreach (FieldInfo field in fields)
{
PersistStateAttribute? persistAttribute = field.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null)
{
continue;
}
if (settings.State.TryGetValue(field.Name, out JsonElement jsonElement))
{
try
{
object? value = jsonElement.Deserialize(field.FieldType);
field.SetValue(instance, value);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state field: {field.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore field {field.Name}: {ex.Message}", source: "Plugin");
}
}
}
// Load properties
foreach (PropertyInfo property in properties)
{
PersistStateAttribute? persistAttribute = property.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null || !property.CanWrite)
{
continue;
}
if (settings.State.TryGetValue(property.Name, out JsonElement jsonElement))
{
try
{
object? value = jsonElement.Deserialize(property.PropertyType);
property.SetValue(instance, value);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state property: {property.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore property {property.Name}: {ex.Message}", source: "Plugin");
}
}
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error loading state: {ex.Message}", source: "Plugin");
}
}
private void SaveState(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saving plugin state", source: "Plugin");
try
{
FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
// Save fields
foreach (FieldInfo field in fields)
{
PersistStateAttribute? persistAttribute = field.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null)
{
continue;
}
try
{
object? value = field.GetValue(instance);
JsonElement jsonElement = JsonSerializer.SerializeToElement(value, field.FieldType);
settings.State[field.Name] = jsonElement;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state field: {field.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save field {field.Name}: {ex.Message}", source: "Plugin");
}
}
// Save properties
foreach (PropertyInfo property in properties)
{
PersistStateAttribute? persistAttribute = property.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null || !property.CanRead)
{
continue;
}
try
{
object? value = property.GetValue(instance);
JsonElement jsonElement = JsonSerializer.SerializeToElement(value, property.PropertyType);
settings.State[property.Name] = jsonElement;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state property: {property.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save property {property.Name}: {ex.Message}", source: "Plugin");
}
}
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - State saved successfully ({settings.State.Count} items)", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error saving state: {ex.Message}", source: "Plugin");
}
}
private async void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e)
{ {
try try
{ {
if (IsRunning && pluginClassInstance is not null) if (IsRunning && pluginClassInstance is not null)
{ {
Bitmap? result = pluginClassInstance.Main(); Bitmap? result;
if (pluginClassInstance.UpdateInterval > 0) if (pluginClassInstance is AsyncPlugin asyncPlugin)
{ {
valueTimer!.Interval = pluginClassInstance.UpdateInterval; CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None;
result = await asyncPlugin.MainAsync(token);
} }
else else
{ {
valueTimer!.Stop(); result = pluginClassInstance.Main();
}
if (pluginClassInstance.UpdateInterval > 0)
{
updateTimer!.Interval = pluginClassInstance.UpdateInterval;
}
else
{
updateTimer!.Stop();
} }
if (result is not null) if (result is not null)
@@ -355,12 +865,7 @@ public partial class PluginWindow : Window
_ => BitmapScalingMode.Unspecified _ => BitmapScalingMode.Unspecified
}; };
//Update Image UpdateImageFromBitmap(result, renderOptions);
Dispatcher.Invoke(() =>
{
RenderOptions.SetBitmapScalingMode(image, renderOptions);
image.Source = BitmapToImageSource(result);
});
} }
} }
} }
@@ -368,22 +873,56 @@ public partial class PluginWindow : Window
{ {
IsRunning = false; IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
if (!IsRunning) if (!IsRunning)
{ {
valueTimer!.Stop(); updateTimer!.Stop();
} }
} }
private void Border_SizeChanged(object sender, SizeChangedEventArgs e)
{
rectangleGeometry.Rect = new Rect(-settings.Theme.Margin, -settings.Theme.Margin, e.NewSize.Width, e.NewSize.Height);
}
private void ViewBox_SizeChanged(object sender, SizeChangedEventArgs e)
{
border.Width = e.NewSize.Width + (settings.Theme.Margin * 2);
border.Height = e.NewSize.Height + (settings.Theme.Margin * 2);
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
IsRunning = false; IsRunning = false;
pluginClassInstance?.Stop();
// Cleanup hot reload resources
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = false;
pluginFileWatcher.Changed -= OnPluginFileChanged;
pluginFileWatcher.Dispose();
pluginFileWatcher = null;
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Dispose();
reloadDebounceTimer = null;
// Stop plugin using the shared method (no need to await in synchronous event handler)
_ = StopPlugin(unloadAssembly: true);
} }
#region Window Events #region Window Events
+12 -2
View File
@@ -11,17 +11,27 @@ using Color = System.Drawing.Color;
namespace DesktopMagic.Plugins; namespace DesktopMagic.Plugins;
public class Theme : ITheme, INotifyPropertyChanged public class Theme(string name) : ITheme, INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private Color primaryColor = Color.White; private Color primaryColor = Color.White;
private Color secondaryColor = Color.White; private Color secondaryColor = Color.LightGray;
private Color backgroundColor = Color.Transparent; private Color backgroundColor = Color.Transparent;
private string font = "Segoe UI"; private string font = "Segoe UI";
private int cornerRadius; private int cornerRadius;
private int margin; private int margin;
public string Name
{
get => name;
set
{
name = value;
OnPropertyChanged();
}
}
public Color PrimaryColor public Color PrimaryColor
{ {
get => primaryColor; get => primaryColor;
@@ -0,0 +1,40 @@
<Window x:Class="DesktopMagic.WebPluginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopMagic"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
Title="WebPluginWindow" Height="450" Width="800"
Background="Transparent"
WindowStyle="None"
ShowInTaskbar="False"
AllowsTransparency="True"
LocationChanged="Window_LocationChanged"
SizeChanged="Window_SizeChanged"
Closing="Window_Closing"
ContentRendered="Window_ContentRendered"
x:Name="window">
<busyIndicator:BusyMask x:Name="busyMask" IsBusy="True" Background="Transparent" BusyContent="" BusyContentMargin="-5" IndicatorType="Cogs">
<Grid>
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
<Border x:Name="border" Background="Transparent" CornerRadius="10">
<Grid>
<wv2:WebView2 x:Name="webView" DefaultBackgroundColor="Transparent" CoreWebView2InitializationCompleted="WebView_CoreWebView2InitializationCompleted" />
<!--
WORKAROUND: This rectangle exists to allow interaction with the WebView2 when the window is transparent
https://github.com/MicrosoftEdge/WebView2Feedback/issues/997
-->
<Rectangle Fill="#01000000" />
</Grid>
</Border>
</Grid>
</busyIndicator:BusyMask>
<WindowChrome.WindowChrome>
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
</Window>
@@ -0,0 +1,633 @@
using DesktopMagic.Api.Settings;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using Microsoft.Web.WebView2.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
namespace DesktopMagic;
public partial class WebPluginWindow : Window, IPluginWindow
{
public event Action? PluginLoaded;
public event Action? OnExit;
private readonly PluginSettings settings;
private bool isInitialized = false;
private FileSystemWatcher? fileWatcher;
private System.Timers.Timer? reloadDebounceTimer;
private bool isReloading = false;
public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; }
public string PluginFolderPath { get; private set; }
public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath)
{
InitializeComponent();
Window w = new()
{
Top = -100,
Left = -100,
Width = 0,
Height = 0,
WindowStyle = WindowStyle.ToolWindow,
ShowInTaskbar = false
};
WindowInteropHelper helper = new WindowInteropHelper(w);
_ = helper.EnsureHandle();
Owner = w;
settings.PropertyChanged += (e, s) =>
{
if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
{
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
ThemeChanged();
}
};
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
PluginMetadata = pluginMetadata;
this.settings = settings;
Left = settings.Position.X;
Top = settings.Position.Y;
Width = settings.Size.X;
Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath;
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
{
InitializeHotReload();
}
}
public void Exit()
{
IsRunning = false;
Dispatcher.Invoke(() =>
{
OnExit?.Invoke();
});
}
public void SetEditMode(bool enabled)
{
if (enabled)
{
Topmost = true;
panel.Visibility = Visibility.Visible;
_ = W32.EnableWindow(webView.Handle, false);
WindowPos.SetIsLocked(this, false);
tileBar.CaptionHeight = tileBar.CaptionHeight = ActualHeight - 10 < 0 ? 0 : ActualHeight - 10;
ResizeMode = ResizeMode.CanResize;
}
else
{
Topmost = false;
panel.Visibility = Visibility.Collapsed;
_ = W32.EnableWindow(webView.Handle, true);
WindowPos.SendWpfWindowBack(this);
WindowPos.SendWpfWindowBack(this);
WindowPos.SetIsLocked(this, true);
tileBar.CaptionHeight = 0;
ResizeMode = ResizeMode.NoResize;
}
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
WindowInteropHelper helper = new(this);
_ = WindowPos.SetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE,
WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE);
}
private async void Window_ContentRendered(object? sender, EventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting web plugin", source: "WebPlugin");
try
{
await InitializeWebView();
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "WebView2 initialization error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
Exit();
}
}
private void ThemeChanged()
{
webView.Margin = new Thickness(settings.Theme.Margin);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
string cssVariables = $@"
:root {{
--background-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.BackgroundColor)};
--primary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
--secondary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.SecondaryColor)};
--font-family: {settings.Theme.Font};
font-family: {settings.Theme.Font};
color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
}}
";
string script = $@"
(function() {{
let style = document.getElementById('desktop-magic-theme');
if (!style) {{
style = document.createElement('style');
style.id = 'desktop-magic-theme';
document.head.appendChild(style);
}}
style.textContent = `{cssVariables}`;
}})();
";
_ = webView.ExecuteScriptAsync(script);
}
private async System.Threading.Tasks.Task InitializeWebView()
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Initializing WebView2", source: "WebPlugin");
string htmlPath = Path.Combine(PluginFolderPath, "main.html");
if (!File.Exists(htmlPath))
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.html\" does not exist", source: "WebPlugin");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File \"main.html\" does not exist!",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
Exit();
return;
}
try
{
LoadWebPluginSettings();
string userDataFolder = Path.Combine(Path.GetTempPath(), "DesktopMagic", "WebView2", PluginMetadata.Id.ToString());
CoreWebView2Environment environment = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
await webView.EnsureCoreWebView2Async(environment);
webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
webView.CoreWebView2.Settings.AreDevToolsEnabled = false;
webView.CoreWebView2.Settings.IsStatusBarEnabled = false;
webView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
_ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.Source = new Uri(htmlUri);
isInitialized = true;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - WebView2 initialized successfully", source: "WebPlugin");
PluginLoaded?.Invoke();
busyMask.IsBusy = false;
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - WebView2 initialization failed: {ex}", source: "WebPlugin");
throw;
}
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping web plugin", source: "WebPlugin");
IsRunning = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
fileWatcher.Dispose();
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Dispose();
try
{
if (isInitialized && webView.CoreWebView2 != null)
{
webView.Dispose();
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
}
}
private void Window_LocationChanged(object sender, EventArgs e)
{
settings.Position = new System.Windows.Point(Left, Top);
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
settings.Size = new System.Windows.Point(Width, Height);
tileBar.CaptionHeight = ActualHeight - 10;
}
private bool firstLoad = true;
private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
webView.CoreWebView2.DOMContentLoaded += (_, _) =>
{
if (firstLoad)
{
firstLoad = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = true;
}
}
ThemeChanged();
};
}
private void LoadWebPluginSettings()
{
string settingsPath = Path.Combine(PluginFolderPath, "settings.json");
if (!File.Exists(settingsPath))
{
return;
}
try
{
string json = File.ReadAllText(settingsPath);
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
if (root.ValueKind != JsonValueKind.Array)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - settings.json must be a JSON array", source: "WebPlugin");
return;
}
List<SettingElement> settingElements = [];
int orderIndex = 0;
foreach (JsonElement element in root.EnumerateArray())
{
string id = element.GetProperty("id").GetString() ?? $"setting-{orderIndex}";
string name = element.TryGetProperty("name", out JsonElement nameElement) ? nameElement.GetString() ?? id : id;
string type = element.TryGetProperty("type", out JsonElement typeElement) ? typeElement.GetString() ?? "textbox" : "textbox";
Setting? setting = CreateSetting(type, element);
if (setting is null)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Unknown setting type \"{type}\" for \"{id}\"", source: "WebPlugin");
continue;
}
SettingElement settingElement = new(setting, id, name, orderIndex);
if (settings.Settings.Exists(e => e.Id == id))
{
SettingElement saved = settings.Settings.First(e => e.Id == id);
settingElement.JsonValue = saved.JsonValue;
}
string capturedId = id;
if (setting is Button button)
{
button.OnClick += () =>
{
_ = webView.Dispatcher.InvokeAsync(async () =>
{
try
{
_ = await webView.ExecuteScriptAsync($"window.desktopMagic?.onClick?.({JsonSerializer.Serialize(capturedId)})");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify button click for \"{capturedId}\": {ex}", source: "WebPlugin");
}
});
};
}
setting.OnValueChanged += () =>
{
_ = webView.Dispatcher.InvokeAsync(async () =>
{
try
{
await NotifySettingChange(capturedId, setting);
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify setting change for \"{capturedId}\": {ex}", source: "WebPlugin");
}
});
};
settingElements.Add(settingElement);
orderIndex++;
}
settings.Settings = [.. settingElements.OrderBy(x => x.OrderIndex)];
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to load settings from settings.json: {ex.Message}", source: "WebPlugin");
}
}
private string GetSettingsBridgeScript()
{
string settingsJson = SerializeSettingsToJson();
return $@"
(function() {{
window.desktopMagic = {{}};
window.desktopMagic._settings = {settingsJson};
window.desktopMagic.getSettings = function() {{
return JSON.parse(JSON.stringify(window.desktopMagic._settings));
}};
window.desktopMagic.getSetting = function(id) {{
return window.desktopMagic._settings ? window.desktopMagic._settings[id] : undefined;
}};
window.desktopMagic.onSettingChanged = null;
window.desktopMagic.onButtonClick = null;
window.desktopMagic.dispatchSettingChanged = function(id, value) {{
if (window.desktopMagic._settings) {{
window.desktopMagic._settings[id] = value;
}}
if (typeof window.desktopMagic.onSettingChanged === 'function') {{
window.desktopMagic.onSettingChanged(id, value);
}}
}};
window.desktopMagic.onClick = function(id) {{
if (typeof window.desktopMagic.onButtonClick === 'function') {{
window.desktopMagic.onButtonClick(id);
}}
}};
}})();
";
}
private void InitializeHotReload()
{
try
{
fileWatcher = new FileSystemWatcher(PluginFolderPath)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = false
};
fileWatcher.Changed += OnPluginFileChanged;
reloadDebounceTimer = new System.Timers.Timer(500)
{
AutoReset = false
};
reloadDebounceTimer.Elapsed += async (s, e) =>
{
await ReloadWebPlugin();
};
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "WebPlugin");
}
}
private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
{
if (isReloading)
{
return;
}
string? fileName = e.Name;
if (string.IsNullOrEmpty(fileName))
{
return;
}
string ext = Path.GetExtension(fileName).ToLowerInvariant();
if (ext is not ".html" and not ".json" and not ".js" and not ".css")
{
return;
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Start();
}
private async Task ReloadWebPlugin()
{
if (isReloading || !IsRunning)
{
return;
}
isReloading = true;
try
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading web plugin", source: "WebPlugin");
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
}
LoadWebPluginSettings();
await Dispatcher.Invoke(async () =>
{
if (isInitialized && webView.CoreWebView2 != null)
{
_ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
busyMask.IsBusy = true;
string htmlPath = Path.Combine(PluginFolderPath, "main.html");
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.CoreWebView2.Navigate(htmlUri);
busyMask.IsBusy = false;
}
});
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Web plugin reloaded", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload web plugin: {ex}", source: "WebPlugin");
}
finally
{
isReloading = false;
if (fileWatcher != null && IsRunning)
{
fileWatcher.EnableRaisingEvents = true;
}
}
}
private async System.Threading.Tasks.Task NotifySettingChange(string id, Setting setting)
{
object? value = GetSettingValue(setting);
string serializedValue = JsonSerializer.Serialize(value);
string serializedId = JsonSerializer.Serialize(id);
_ = await webView.ExecuteScriptAsync($"window.desktopMagic?.dispatchSettingChanged?.({serializedId}, {serializedValue})");
}
private string SerializeSettingsToJson()
{
Dictionary<string, object?> dict = [];
foreach (SettingElement element in settings.Settings)
{
dict[element.Id] = GetSettingValue(element.Input);
}
return JsonSerializer.Serialize(dict);
}
private static object? GetSettingValue(Setting setting)
{
return setting switch
{
TextBox tb => tb.Value,
CheckBox cb => cb.Value,
Slider sl => sl.Value,
IntegerUpDown iud => iud.Value,
ComboBox cb => cb.Value,
ColorPicker cp => MultiColorConverter.ConvertToHexRgba(cp.Value),
Button btn => btn.Value,
Label lbl => lbl.Value,
FileSelector fs => fs.Value,
_ => null
};
}
private static Setting? CreateSetting(string type, JsonElement element)
{
return type.ToLowerInvariant() switch
{
"textbox" => new TextBox(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : ""),
"checkbox" => new CheckBox(
element.TryGetProperty("default", out JsonElement defaultValue) && defaultValue.ValueKind == JsonValueKind.True),
"slider" => new Slider(
element.TryGetProperty("min", out JsonElement minValue) ? minValue.GetDouble() : 0,
element.TryGetProperty("max", out JsonElement maxValue) ? maxValue.GetDouble() : 100,
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetDouble() : 0),
"integer" => new IntegerUpDown(
element.TryGetProperty("min", out JsonElement minValue) ? minValue.GetInt32() : 0,
element.TryGetProperty("max", out JsonElement maxValue) ? maxValue.GetInt32() : 100,
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetInt32() : 0),
"combobox" => CreateComboBox(element),
"colorpicker" => new ColorPicker(
element.TryGetProperty("default", out JsonElement defaultValue)
? MultiColorConverter.ConvertToSystemColor(defaultValue.GetString() ?? "#FFFFFFFF", System.Drawing.Color.White)
: System.Drawing.Color.White),
"button" => new Button(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : ""),
"label" => new Label(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : "",
element.TryGetProperty("bold", out JsonElement boldValue) && boldValue.ValueKind == JsonValueKind.True),
"file" => new FileSelector(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : "",
element.TryGetProperty("filter", out JsonElement filterValue) ? filterValue.GetString() ?? "All Files|*.*" : "All Files|*.*",
element.TryGetProperty("title", out JsonElement titleValue) ? titleValue.GetString() ?? "Select File" : "Select File",
element.TryGetProperty("selectFolder", out JsonElement selectFolderValue) && selectFolderValue.ValueKind == JsonValueKind.True),
_ => null
};
}
private static ComboBox CreateComboBox(JsonElement element)
{
List<string> items = [];
if (element.TryGetProperty("items", out JsonElement itemsValue) && itemsValue.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement item in itemsValue.EnumerateArray())
{
items.Add(item.GetString() ?? "");
}
}
ComboBox comboBox = new([.. items]);
if (element.TryGetProperty("default", out JsonElement defaultValue))
{
string defaultText = defaultValue.GetString() ?? "";
if (!string.IsNullOrEmpty(defaultText) && items.Contains(defaultText))
{
comboBox.Value = defaultText;
}
}
return comboBox;
}
}
@@ -3,4 +3,5 @@
<BitmapImage x:Key="Icon" UriSource="/icon.ico" /> <BitmapImage x:Key="Icon" UriSource="/icon.ico" />
<BitmapImage x:Key="ModioCogBlue" UriSource="modio-cog-blue.png" /> <BitmapImage x:Key="ModioCogBlue" UriSource="modio-cog-blue.png" />
<BitmapImage x:Key="ModioLogoBlueDark" UriSource="modio-logo-bluedark.png" /> <BitmapImage x:Key="ModioLogoBlueDark" UriSource="modio-logo-bluedark.png" />
<BitmapImage x:Key="ModioLogoBlueLight" UriSource="modio-logo-bluelight.png" />
</ResourceDictionary> </ResourceDictionary>
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -3,34 +3,73 @@
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib" xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:system="clr-namespace:System;assembly=mscorlib"> xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="developedBy">Entwickelt von Stone__Red</system:String>
<system:String x:Key="reportBug">Bug Melden</system:String>
<system:String x:Key="requestFeature">Feature Anfragen</system:String>
<system:String x:Key="email">E-Mail</system:String>
<system:String x:Key="lineMode">Linie</system:String> <system:String x:Key="lineMode">Linie</system:String>
<system:String x:Key="mirrorMode">Spiegeln</system:String> <system:String x:Key="mirrorMode">Spiegeln</system:String>
<system:String x:Key="updatePlugins">Neu Laden</system:String> <system:String x:Key="folder">Ordner</system:String>
<system:String x:Key="openPluginsFolder">Plugins Ordner Öffnen</system:String> <system:String x:Key="autoStart">Starten beim Hochfahren</system:String>
<system:String x:Key="pluginManager">Plugin Manager</system:String> <system:String x:Key="unknown">Unbekannt</system:String>
<system:String x:Key="open">Öffnen</system:String>
<system:String x:Key="quit">Schließen</system:String>
<system:String x:Key="login">Einloggen</system:String>
<system:String x:Key="logout">Ausloggen</system:String>
<system:String x:Key="enterModIoEmail">Mod.io Account E-Mail eingeben</system:String>
<system:String x:Key="enterModIoAccessToken">Geben Sie Ihren mod.io Sicherheitscode ein</system:String>
<system:String x:Key="toggleEditMode">Bearbeitungsmodus umschalten</system:String>
<system:String x:Key="searchAllPlugins">Alle Plugins Suchen...</system:String>
<system:String x:Key="searchInstalledPlugins">Installierte Plugins Suchen...</system:String>
<system:String x:Key="reloadPlugins">Plugins Neu Laden</system:String>
<system:String x:Key="pluginsFolder">Plugins Ordner</system:String>
<system:String x:Key="pluginManager">Plugins Manager</system:String>
<system:String x:Key="createNewPlugin">Neues Plugin erstellen</system:String>
<system:String x:Key="enterPluginName">Pluginnamen eingeben</system:String>
<system:String x:Key="plugins">Plugins</system:String>
<system:String x:Key="enablePluginToConfigure">Aktivieren Sie das Plugin, um seine Einstellungen zu konfigurieren.</system:String>
<system:String x:Key="color">Farbe:</system:String> <system:String x:Key="color">Farbe:</system:String>
<system:String x:Key="default">Standard</system:String> <system:String x:Key="default">Standard</system:String>
<system:String x:Key="wantToCloseProgram">Wollen sie das Programm wirklich schließen?</system:String> <system:String x:Key="wantToCloseProgram">Wollen sie das Programm wirklich schließen?</system:String>
<system:String x:Key="noOptions">Keine Optionen!</system:String> <system:String x:Key="noOptions">Keine Optionen!</system:String>
<system:String x:Key="edit">Bearbeiten</system:String> <system:String x:Key="editLayout">Layout Bearbeiten</system:String>
<system:String x:Key="time">Uhrzeit</system:String> <system:String x:Key="time">Uhrzeit</system:String>
<system:String x:Key="date">Datum</system:String> <system:String x:Key="date">Datum</system:String>
<system:String x:Key="cpuUsage">CPU Auslastung</system:String> <system:String x:Key="cpuUsage">CPU Auslastung</system:String>
<system:String x:Key="googleCalendar">Google Kalender</system:String> <system:String x:Key="musicVisualizer">Musik Visualisierer</system:String>
<system:String x:Key="musicVisualizer">Musik Visualizer</system:String> <system:String x:Key="weather">Wetter</system:String>
<system:String x:Key="musicVisualizerOptions">Musik Visualizer Einstellungen:</system:String> <system:String x:Key="nextMeetingCountdown">Nächstes Meeting Countdown</system:String>
<system:String x:Key="agenda">Agenda</system:String>
<system:String x:Key="amplifier">Signalverstärkung:</system:String> <system:String x:Key="amplifier">Signalverstärkung:</system:String>
<system:String x:Key="newLayout">Neues Layout</system:String> <system:String x:Key="newLayout">Neues Layout</system:String>
<system:String x:Key="deleteLayout">Layout Löschen</system:String> <system:String x:Key="deleteLayout">Layout Löschen</system:String>
<system:String x:Key="newTheme">Neues Theme</system:String>
<system:String x:Key="deleteTheme">Theme Löschen</system:String>
<system:String x:Key="editTheme">Theme Bearbeiten</system:String>
<system:String x:Key="themes">Themes</system:String>
<system:String x:Key="ok">Ok</system:String> <system:String x:Key="ok">Ok</system:String>
<system:String x:Key="cancel">Abbrechen</system:String> <system:String x:Key="cancel">Abbrechen</system:String>
<system:String x:Key="confirmDeleteLayout">Möchten Sie dieses Layout wirklich löschen?</system:String>
<system:String x:Key="confirmDeleteTheme">Möchten Sie dieses Theme wirklich löschen?</system:String>
<system:String x:Key="enterLayoutName">Layoutnamen eingeben:</system:String> <system:String x:Key="enterLayoutName">Layoutnamen eingeben:</system:String>
<system:String x:Key="layoutAlreadyExists">Layout existiert bereits!</system:String> <system:String x:Key="layoutAlreadyExists">Layout existiert bereits!</system:String>
<system:String x:Key="enterThemeName">Themenamen eingeben:</system:String>
<system:String x:Key="themeAlreadyExists">Theme existiert bereits!</system:String>
<system:String x:Key="install">Installieren</system:String> <system:String x:Key="install">Installieren</system:String>
<system:String x:Key="uninstall">Deinstallieren</system:String>
<system:String x:Key="remove">Entfernen</system:String> <system:String x:Key="remove">Entfernen</system:String>
<system:String x:Key="authorFormat">Author: {0}</system:String>
<system:String x:Key="addedFormat">Erstellt: {0}</system:String>
<system:String x:Key="updatedFormat">Aktualisiert: {0}</system:String>
<system:String x:Key="versionFormat">Version: {0}</system:String>
<system:String x:Key="releaseInfoTitle">DesktopMagic wurde aktualisiert!</system:String>
<system:String x:Key="releaseInfoHeaderFormat">Neueste Version: {0}</system:String>
<system:String x:Key="releaseInfoPublishedFormat">Veröffentlicht: {0}</system:String>
<system:String x:Key="releaseInfoBodyLabel">Release Notes:</system:String>
<system:String x:Key="openReleasePage">Release-Seite öffnen</system:String>
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems"> <col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
<system:String>Unten</system:String> <system:String>Unten</system:String>
<system:String>Mitte</system:String> <system:String>Mitte</system:String>
<system:String>Oben</system:String> <system:String>Oben</system:String>
</col:ArrayList> </col:ArrayList>
</ResourceDictionary> </ResourceDictionary>
@@ -3,34 +3,75 @@
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib" xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:system="clr-namespace:System;assembly=mscorlib"> xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="developedBy">Developed by Stone__Red</system:String>
<system:String x:Key="reportBug">Report Bug</system:String>
<system:String x:Key="requestFeature">Request Feature</system:String>
<system:String x:Key="email">Email</system:String>
<system:String x:Key="lineMode">Line</system:String> <system:String x:Key="lineMode">Line</system:String>
<system:String x:Key="mirrorMode">Mirror</system:String> <system:String x:Key="mirrorMode">Mirror</system:String>
<system:String x:Key="updatePlugins">Reload</system:String> <system:String x:Key="folder">Folder</system:String>
<system:String x:Key="openPluginsFolder">Open Plugins Folder</system:String> <system:String x:Key="autoStart">Launch on startup</system:String>
<system:String x:Key="pluginManager">Plugin Manager</system:String> <system:String x:Key="unknown">Unknown</system:String>
<system:String x:Key="open">Open</system:String>
<system:String x:Key="quit">Quit</system:String>
<system:String x:Key="login">Log In</system:String>
<system:String x:Key="logout">Log Out</system:String>
<system:String x:Key="enterModIoEmail">Enter your mod.io account E-mail</system:String>
<system:String x:Key="enterModIoAccessToken">Enter your mod.io security code</system:String>
<system:String x:Key="toggleEditMode">Toggle Edit Mode</system:String>
<system:String x:Key="searchAllPlugins">Search All Plugins...</system:String>
<system:String x:Key="searchInstalledPlugins">Search Installed Plugins...</system:String>
<system:String x:Key="reloadPlugins">Reload Plugins</system:String>
<system:String x:Key="pluginsFolder">Plugins Folder</system:String>
<system:String x:Key="pluginManager">Plugins Manager</system:String>
<system:String x:Key="createNewPlugin">Create New Plugin</system:String>
<system:String x:Key="enterPluginName">Enter plugin name</system:String>
<system:String x:Key="plugins">Plugins</system:String>
<system:String x:Key="enablePluginToConfigure">Enable the plugin to configure its settings.</system:String>
<system:String x:Key="color">Color:</system:String> <system:String x:Key="color">Color:</system:String>
<system:String x:Key="default">Default</system:String> <system:String x:Key="default">Default</system:String>
<system:String x:Key="wantToCloseProgram">Do you really want to close the program?</system:String> <system:String x:Key="wantToCloseProgram">Do you really want to close the program?</system:String>
<system:String x:Key="noOptions">No Options!</system:String> <system:String x:Key="noOptions">No Options!</system:String>
<system:String x:Key="edit">Edit</system:String> <system:String x:Key="editLayout">Edit Layout</system:String>
<system:String x:Key="time">Time</system:String> <system:String x:Key="time">Time</system:String>
<system:String x:Key="date">Date</system:String> <system:String x:Key="date">Date</system:String>
<system:String x:Key="cpuUsage">CPU Usage</system:String> <system:String x:Key="cpuUsage">CPU Usage</system:String>
<system:String x:Key="googleCalendar">Google Calendar</system:String>
<system:String x:Key="musicVisualizer">Music Visualizer</system:String> <system:String x:Key="musicVisualizer">Music Visualizer</system:String>
<system:String x:Key="musicVisualizerOptions">Music Visualizer Options:</system:String> <system:String x:Key="weather">Weather</system:String>
<system:String x:Key="nextMeetingCountdown">Next Meeting Countdown</system:String>
<system:String x:Key="agenda">Agenda</system:String>
<system:String x:Key="amplifier">Signal Amplification:</system:String> <system:String x:Key="amplifier">Signal Amplification:</system:String>
<system:String x:Key="newLayout">New Layout</system:String> <system:String x:Key="newLayout">New Layout</system:String>
<system:String x:Key="deleteLayout">Delete Layout</system:String> <system:String x:Key="deleteLayout">Delete Layout</system:String>
<system:String x:Key="newTheme">New Theme</system:String>
<system:String x:Key="deleteTheme">Delete Theme</system:String>
<system:String x:Key="editTheme">Edit Theme</system:String>
<system:String x:Key="themes">Themes</system:String>
<system:String x:Key="ok">Ok</system:String> <system:String x:Key="ok">Ok</system:String>
<system:String x:Key="cancel">Cancel</system:String> <system:String x:Key="cancel">Cancel</system:String>
<system:String x:Key="confirmDeleteLayout">Do you really want to delete this layout?</system:String>
<system:String x:Key="confirmDeleteTheme">Do you really want to delete this theme?</system:String>
<system:String x:Key="enterLayoutName">Enter layout name</system:String> <system:String x:Key="enterLayoutName">Enter layout name</system:String>
<system:String x:Key="layoutAlreadyExists">Layout already exists!</system:String> <system:String x:Key="layoutAlreadyExists">Layout already exists!</system:String>
<system:String x:Key="cannotDeleteLastLayout">Can't delete last layout!</system:String>
<system:String x:Key="enterThemeName">Enter theme name</system:String>
<system:String x:Key="themeAlreadyExists">Theme already exists!</system:String>
<system:String x:Key="cannotDeleteLastTheme">Can't delete last theme!</system:String>
<system:String x:Key="install">Install</system:String> <system:String x:Key="install">Install</system:String>
<system:String x:Key="uninstall">Uninstall</system:String>
<system:String x:Key="remove">Remove</system:String> <system:String x:Key="remove">Remove</system:String>
<system:String x:Key="authorFormat">Author: {0}</system:String>
<system:String x:Key="addedFormat">Added: {0}</system:String>
<system:String x:Key="updatedFormat">Updated: {0}</system:String>
<system:String x:Key="versionFormat">Version: {0}</system:String>
<system:String x:Key="releaseInfoTitle">DesktopMagic has been updated!</system:String>
<system:String x:Key="releaseInfoHeaderFormat">Latest release: {0}</system:String>
<system:String x:Key="releaseInfoPublishedFormat">Published: {0}</system:String>
<system:String x:Key="releaseInfoBodyLabel">Release notes:</system:String>
<system:String x:Key="openReleasePage">Open release page</system:String>
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems"> <col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
<system:String>Bottom</system:String> <system:String>Bottom</system:String>
<system:String>Middle</system:String> <system:String>Middle</system:String>
<system:String>Top</system:String> <system:String>Top</system:String>
</col:ArrayList> </col:ArrayList>
</ResourceDictionary> </ResourceDictionary>
@@ -0,0 +1,131 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<system:Double x:Key="ToggleButtonWidth">40</system:Double>
<system:Double x:Key="ToggleButtonHeight">20</system:Double>
<Thickness x:Key="ToggleSwitchBorderThemeThickness">1</Thickness>
<Thickness x:Key="ToggleSwitchContentLeftMargin">0,0,8,0</Thickness>
<Style x:Key="ToggleSwitchContentLeftStyle" TargetType="{x:Type ui:ToggleSwitch}">
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOn}" />
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="{StaticResource ToggleSwitchBorderThemeThickness}" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:ToggleSwitch}">
<Grid Margin="{TemplateBinding Padding}" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="0"
Margin="{StaticResource ToggleSwitchContentLeftMargin}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True"
TextElement.Foreground="{TemplateBinding Foreground}" />
<Grid
Grid.Column="1"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}">
<Rectangle
x:Name="ToggleRectangle"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}"
Fill="{DynamicResource ToggleSwitchFillOff}"
RadiusX="10" RadiusY="10"
Stroke="{DynamicResource ToggleSwitchStrokeOff}"
StrokeThickness="1" />
<Rectangle
x:Name="ActiveToggleRectangle"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}"
Fill="{TemplateBinding Background}"
Opacity="0.0"
RadiusX="10" RadiusY="10" />
<Rectangle
x:Name="ToggleEllipse"
Width="12" Height="12"
Fill="{DynamicResource ToggleSwitchKnobFillOff}"
RadiusX="7" RadiusY="7">
<Rectangle.RenderTransform>
<TranslateTransform X="-9" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle
x:Name="ActiveToggleEllipse"
Width="12" Height="12"
Fill="{DynamicResource ToggleSwitchKnobFillOn}"
Opacity="0.0"
RadiusX="7" RadiusY="7">
<Rectangle.RenderTransform>
<TranslateTransform X="-9" />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Name="SlideRight">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="9" Duration="0:0:0.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="-9" Duration="0:0:0.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Content" Value="{x:Null}" />
<Condition Property="IsChecked" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OnContent, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Content" Value="{x:Null}" />
<Condition Property="IsChecked" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OffContent, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -1,4 +1,6 @@
using System.Collections.ObjectModel; using DesktopMagic.Plugins;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -6,19 +8,33 @@ using System.Text.Json.Serialization;
namespace DesktopMagic.Settings; namespace DesktopMagic.Settings;
internal class DesktopMagicSettings : INotifyPropertyChanged public class DesktopMagicSettings : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<Layout> layouts = []; private ObservableCollection<Layout> layouts = [];
private ObservableCollection<Theme> themes = [];
private string? currentLayoutName; private string? currentLayoutName;
public ObservableCollection<Theme> Themes
{
get => themes;
init
{
themes = value;
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
OnPropertyChanged();
}
}
public ObservableCollection<Layout> Layouts public ObservableCollection<Layout> Layouts
{ {
get => layouts; get => layouts;
set set
{ {
layouts = value; layouts = value;
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayout));
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
OnPropertyChanged(); OnPropertyChanged();
} }
} }
@@ -28,7 +44,16 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
public string? CurrentLayoutName public string? CurrentLayoutName
{ {
get => currentLayoutName ?? Layouts.FirstOrDefault()?.Name; get
{
if (!Layouts.Any(l => l.Name == currentLayoutName))
{
currentLayoutName = null;
}
return currentLayoutName ?? Layouts.FirstOrDefault()?.Name;
}
set set
{ {
currentLayoutName = value; currentLayoutName = value;
@@ -37,8 +62,20 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
} }
} }
public string? ModIoAccessToken { get; set; }
public string? ReleaseInfoLastAppVersion { get; set; }
public DesktopMagicSettings()
{
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayout));
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
}
protected void OnPropertyChanged([CallerMemberName] string? name = null) protected void OnPropertyChanged([CallerMemberName] string? name = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
} }
} }
+34 -7
View File
@@ -1,27 +1,42 @@
using DesktopMagic.Plugins; using DesktopMagic.DataContexts;
using DesktopMagic.Plugins;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace DesktopMagic.Settings; namespace DesktopMagic.Settings;
internal class Layout(string name) : INotifyPropertyChanged public class Layout(string name) : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private string name = name; private string name = name;
private Theme theme = new Theme(); private string? currentThemeName = null;
private Dictionary<uint, PluginSettings> plugins = []; private Dictionary<uint, PluginSettings> plugins = [];
[JsonIgnore]
public Theme Theme public Theme Theme
{ {
get => theme; get
{
DesktopMagicSettings settings = MainWindowDataContext.GetSettings();
return settings.Themes.FirstOrDefault(theme => theme.Name == currentThemeName) ?? settings.Themes.FirstOrDefault() ?? new Theme("ERROR");
}
}
public string? CurrentThemeName
{
get => Theme.Name;
set set
{ {
theme = value; if (currentThemeName != value)
OnPropertyChanged(); {
currentThemeName = value;
UpdateTheme();
}
} }
} }
@@ -47,7 +62,19 @@ internal class Layout(string name) : INotifyPropertyChanged
public void UpdatePlugins() public void UpdatePlugins()
{ {
Plugins = Plugins.ToDictionary(); plugins = plugins.ToDictionary();
OnPropertyChanged(nameof(Plugins));
}
public void UpdateTheme()
{
OnPropertyChanged(nameof(Theme));
OnPropertyChanged(nameof(CurrentThemeName));
foreach (PluginSettings pluginSettings in plugins.Values)
{
pluginSettings.UpdateTheme();
}
} }
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+52 -5
View File
@@ -3,7 +3,9 @@ using DesktopMagic.Plugins;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Windows; using System.Windows;
@@ -13,18 +15,40 @@ public class PluginSettings : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private readonly Theme? theme; private string? currentThemeName;
private List<SettingElement> settings = []; private List<SettingElement> settings = [];
private Dictionary<string, JsonElement> state = [];
private bool enabled = false; private bool enabled = false;
private Point position = new Point(100, 100); private Point position = new Point(100, 100);
private Point size = new Point(300, 300); private Point size = new Point(300, 300);
[JsonIgnore]
public Theme Theme => theme is null ? MainWindowDataContext.GetSettings().CurrentLayout.Theme : theme;
// Only for internal use to show the name of the plugin in the main window // Only for internal use to show the name of the plugin in the main window
[JsonIgnore] [JsonIgnore]
public string Name { get; set; } = string.Empty; public PluginMetadata Metadata { get; set; } = new();
[JsonIgnore]
public Theme Theme
{
get
{
DesktopMagicSettings settings = MainWindowDataContext.GetSettings();
return settings.Themes.FirstOrDefault(t => t.Name == currentThemeName) ?? settings.CurrentLayout.Theme;
}
}
public string? CurrentThemeName
{
get => currentThemeName;
set
{
if (currentThemeName != value)
{
currentThemeName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(Theme));
}
}
}
public List<SettingElement> Settings public List<SettingElement> Settings
{ {
@@ -39,6 +63,23 @@ public class PluginSettings : INotifyPropertyChanged
} }
} }
/// <summary>
/// Dictionary storing plugin state. Key is the field/property name, value is the JsonElement.
/// This is persisted per layout in settings.json.
/// </summary>
public Dictionary<string, JsonElement> State
{
get => state;
set
{
if (state != value)
{
state = value;
OnPropertyChanged();
}
}
}
public bool Enabled public bool Enabled
{ {
get => enabled; get => enabled;
@@ -78,6 +119,12 @@ public class PluginSettings : INotifyPropertyChanged
} }
} }
public void UpdateTheme()
{
OnPropertyChanged(nameof(Theme));
OnPropertyChanged(nameof(CurrentThemeName));
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{ {
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
@@ -3,6 +3,7 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<OutputType>Library</OutputType> <OutputType>Library</OutputType>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
+3 -3
View File
@@ -14,10 +14,10 @@ namespace DesktopMagic.PluginTest;
public class GifPlugin : Plugin public class GifPlugin : Plugin
{ {
[Setting("gif-path", "GIF path")] [Setting("gif-path", "GIF path")]
private readonly TextBox input = new TextBox(""); private readonly FileSelector input = new FileSelector();
[Setting("info")] [Setting("info")]
private readonly Label info = new Label(""); private readonly Label info = new Label("Select a GIF file to load.");
private readonly List<Bitmap> bitmaps = []; private readonly List<Bitmap> bitmaps = [];
@@ -92,7 +92,7 @@ public class GifPlugin : Plugin
info.Value = $"Loading frame {i + 1} of {gif.GetFrameCount(FrameDimension.Time)}"; info.Value = $"Loading frame {i + 1} of {gif.GetFrameCount(FrameDimension.Time)}";
} }
info.Value = string.Empty; info.Value = $"Loaded {bitmaps.Count} frames.";
} }
else else
{ {
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.Api;
/// <summary>
/// Provides an abstract base class for creating asynchronous plugins that can be integrated into the application, supporting periodic updates,
/// rendering, and user interaction.
/// </summary>
/// <remarks>Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
/// such as <see cref="Plugin.Start"/>, <see cref="Plugin.Stop"/>, and <see cref="Main"/> for activation, deactivation, and periodic
/// execution. It also provides event handlers for mouse and theme interactions, as well as access to application data
/// and rendering configuration. Implementations should override relevant methods to respond to user input, update
/// intervals, and configuration changes as needed.</remarks>
public abstract class AsyncPlugin : Plugin
{
/// <summary>
/// Occurs once when the plugin gets activated. Override for async initialization.
/// </summary>
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
public virtual Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs once when the plugin gets deactivated. Override for async cleanup.
/// </summary>
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs when the <see cref="Plugin.UpdateInterval"/> elapses.
/// </summary>
/// <returns></returns>
public abstract Task<Bitmap?> MainAsync(CancellationToken cancellationToken);
/// <summary>
/// This method should not be called directly! Override and use <see cref="MainAsync"/> for asynchronous plugin operations.
/// </summary>
/// <remarks>This method only exists to fulfill base class requirements.</remarks>
/// <returns>Calling it will always result in an <see cref="InvalidOperationException"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if this method is called directly. Use MainAsync for plugin execution instead.</exception>
public sealed override Bitmap? Main()
{
throw new InvalidOperationException($"AsyncPlugin.Main() should not be called directly. Override {nameof(MainAsync)} instead.");
}
}
@@ -1,36 +1,55 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Company>Stone_Red</Company> <Company>Stone_Red</Company>
<Product>Stone_Red</Product> <Product>Stone_Red</Product>
<Version>0.0.0.5</Version> <Version>1.0.0.0</Version>
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl> <RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>0.0.0.5</AssemblyVersion> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<PackageProjectUrl></PackageProjectUrl> <PackageProjectUrl></PackageProjectUrl>
<FileVersion>0.0.0.5</FileVersion> <FileVersion></FileVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Platforms>AnyCPU;x64</Platforms>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>Square44x44Logo.altform-lightunplated_targetsize-256.png</PackageIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DocumentationFile>C:\Users\David\Programmieren\DesktopMagic\src\DesktopMagicPluginAPI\DesktopMagicPluginAPI.xml</DocumentationFile> <DocumentationFile>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), "DesktopMagicPluginAPI.xml"))</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<DocumentationFile>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), "DesktopMagicPluginAPI.xml"))</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DocumentationFile>C:\Users\David\Programmieren\DesktopMagic\src\DesktopMagicPluginAPI\DesktopMagicPluginAPI.xml</DocumentationFile> <DocumentationFile>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), "DesktopMagicPluginAPI.xml"))</DocumentationFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
<DocumentationFile>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), "DesktopMagicPluginAPI.xml"))</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Include="..\..\LICENSE">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\MsixPackaging\Images\Square44x44Logo.altform-lightunplated_targetsize-256.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="System.Drawing.Common" Version="8.0.0" /> <PackageReference Include="System.Drawing.Common" Version="8.0.0" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Include="..\..\LICENSE">
<Pack>True</Pack>
<PackagePath></PackagePath>
</None>
</ItemGroup>
</Project> </Project>
@@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Drawing; using System.Drawing;
@@ -6,6 +7,8 @@ namespace DesktopMagic.Api.Drawing;
internal class FontComparer : IEqualityComparer<Font> internal class FontComparer : IEqualityComparer<Font>
{ {
private const float Tolerance = 0.01f;
public bool Equals(Font? font1, Font? font2) public bool Equals(Font? font1, Font? font2)
{ {
if (font1 is null || font2 is null) if (font1 is null || font2 is null)
@@ -18,7 +21,7 @@ internal class FontComparer : IEqualityComparer<Font>
return false; return false;
} }
if (font1.SizeInPoints != font2.SizeInPoints) if (Math.Abs(font1.SizeInPoints - font2.SizeInPoints) > Tolerance)
{ {
return false; return false;
} }
+23
View File
@@ -36,4 +36,27 @@ public interface IPluginData
/// Updates the plugin window. /// Updates the plugin window.
/// </summary> /// </summary>
void UpdateWindow(); void UpdateWindow();
/// <summary>
/// Logs a message to the application log.
/// </summary>
/// <param name="message">The message to log.</param>
/// <param name="level">The log level (Info, Warning, Error).</param>
void Log(string message, LogLevel level = LogLevel.Info);
/// <summary>
/// Shows a message box to the user.
/// </summary>
/// <param name="message">The message to display.</param>
/// <param name="title">The title of the message box.</param>
void ShowMessage(string message, string? title = null);
/// <summary>
/// Saves the current state of fields and properties marked with <see cref="PersistStateAttribute"/>
/// </summary>
/// <remarks>
/// State is automatically saved when the plugin stops, but this method can be called
/// to save state at any time, such as after important user interactions.
/// </remarks>
void SaveState();
} }
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Stone_Red
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+22
View File
@@ -0,0 +1,22 @@
namespace DesktopMagic.Api;
/// <summary>
/// Specifies the severity level of a log message.
/// </summary>
public enum LogLevel
{
/// <summary>
/// Informational message for general information.
/// </summary>
Info,
/// <summary>
/// Warning message indicating a potential issue.
/// </summary>
Warning,
/// <summary>
/// Error message indicating a failure or critical issue.
/// </summary>
Error
}
@@ -1,4 +1,4 @@
namespace DesktopMagic.Api.Settings; namespace DesktopMagic.Api;
/// <summary> /// <summary>
/// Mouse Buttons /// Mouse Buttons
@@ -0,0 +1,15 @@
using System;
namespace DesktopMagic.Api;
/// <summary>
/// Marks a field or property to be automatically persisted between plugin sessions.
/// </summary>
/// <remarks>
/// The field or property must be JSON serializable. Complex types should have parameterless constructors.
/// State is saved when the plugin stops and loaded when it starts.
/// </remarks>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class PersistStateAttribute : Attribute
{
}
+34 -5
View File
@@ -7,20 +7,35 @@ using System.Drawing;
namespace DesktopMagic.Api; namespace DesktopMagic.Api;
/// <summary> /// <summary>
/// The plugin class. /// Provides an abstract base class for creating plugins that can be integrated into the application, supporting periodic updates,
/// rendering, and user interaction.
/// </summary> /// </summary>
/// <remarks>Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
/// such as <see cref="Start"/>, <see cref="Stop"/>, and <see cref="Main"/> for activation, deactivation, and periodic
/// execution. It also provides event handlers for mouse and theme interactions, as well as access to application data
/// and rendering configuration. Implementations should override relevant methods to respond to user input, update
/// intervals, and configuration changes as needed.</remarks>
public abstract class Plugin public abstract class Plugin
{ {
[Setting("horizontalAlignment", "Horizontal Alignment", -999)] [Setting("desktopmagic-horizontal-alignment", "Horizontal Alignment", -999)]
internal ComboBox horizontalAlignment = new ComboBox("Center", "Left", "Right"); internal ComboBox horizontalAlignment = new ComboBox("Center", "Left", "Right");
[Setting("verticalAlignment", "Vertical Alignment", -998)] [Setting("desktopmagic-vertical-alignment", "Vertical Alignment", -998)]
internal ComboBox verticalAlignment = new ComboBox("Center", "Top", "Bottom"); internal ComboBox verticalAlignment = new ComboBox("Center", "Top", "Bottom");
[Setting("desktopmagic-window-layer", "Window Layer", -997)]
internal ComboBox windowLayer = new ComboBox("Always on Bottom", "Always on Top");
[Setting("desktopmagic-rotation", "Rotation", -996)]
internal IntegerUpDown rotation = new IntegerUpDown(-360, 360, 0);
[Setting("desktopmagic-theme-override", "Theme Override", -995)]
internal ComboBox themeOverride = new ComboBox("<None>");
private IPluginData application = null!; private IPluginData application = null!;
/// <summary> /// <summary>
/// Informations about the main application. /// Information about the main application.
/// </summary> /// </summary>
public IPluginData Application public IPluginData Application
{ {
@@ -83,4 +98,18 @@ public abstract class Plugin
public virtual void OnMouseWheel(Point position, int delta) public virtual void OnMouseWheel(Point position, int delta)
{ {
} }
}
/// <summary>
/// Occurs when the application's theme has changed.
/// </summary>
public virtual void OnThemeChanged()
{
}
/// <summary>
/// Invoked when the settings have changed to allow derived classes to respond to configuration updates.
/// </summary>
public virtual void OnSettingsChanged()
{
}
}
@@ -0,0 +1,62 @@
using System.Drawing;
using System.Globalization;
namespace DesktopMagic.Api.Settings;
/// <summary>
/// Represents a color picker control.
/// </summary>
public sealed class ColorPicker : Setting
{
private Color _value;
/// <summary>
/// Gets or sets the color value assigned to the <see cref="ColorPicker"/> element.
/// </summary>
public Color Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
ValueChanged();
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPicker"/> class with the provided <paramref name="defaultColor"/>.
/// </summary>
/// <param name="defaultColor">The default color value.</param>
public ColorPicker(Color defaultColor)
{
_value = defaultColor;
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPicker"/> class with a default color of White.
/// </summary>
public ColorPicker() : this(Color.White)
{
}
internal override string GetJsonValue()
{
return $"{Value.A},{Value.R},{Value.G},{Value.B}";
}
internal override void SetJsonValue(string value)
{
string[] parts = value.Split(',');
if (parts.Length == 4 &&
byte.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte a) &&
byte.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte r) &&
byte.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte g) &&
byte.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte b))
{
Value = Color.FromArgb(a, r, g, b);
}
}
}
@@ -0,0 +1,65 @@
namespace DesktopMagic.Api.Settings;
/// <summary>
/// Represents a file selector control that allows the user to browse for a file.
/// </summary>
public sealed class FileSelector : Setting
{
private string _value = string.Empty;
/// <summary>
/// Gets or sets the selected file path.
/// </summary>
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
ValueChanged();
}
}
}
/// <summary>
/// Gets or sets the file filter for the file dialog (e.g., "Image Files|*.png;*.jpg|All Files|*.*").
/// </summary>
public string Filter { get; set; }
/// <summary>
/// Gets or sets the title of the file dialog.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the file selector should select folders instead of files.
/// </summary>
public bool SelectFolder { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="FileSelector"/> class.
/// </summary>
/// <param name="defaultPath">The default file path.</param>
/// <param name="filter">The file filter for the file dialog.</param>
/// <param name="title">The title of the file dialog.</param>
/// <param name="selectFolder">If true, selects folders instead of files.</param>
public FileSelector(string defaultPath = "", string filter = "All Files|*.*", string title = "Select File", bool selectFolder = false)
{
_value = defaultPath;
Filter = filter;
Title = title;
SelectFolder = selectFolder;
}
internal override string GetJsonValue()
{
return Value;
}
internal override void SetJsonValue(string value)
{
Value = value;
}
}
@@ -1,4 +1,5 @@
using System; using System;
using System.Diagnostics.CodeAnalysis;
namespace DesktopMagic.Api.Settings; namespace DesktopMagic.Api.Settings;
@@ -22,6 +23,7 @@ public sealed class Slider : Setting
/// <summary> /// <summary>
/// Gets or sets the value assigned to the <see cref="Slider"/> element. /// Gets or sets the value assigned to the <see cref="Slider"/> element.
/// </summary> /// </summary>
[SuppressMessage("Major Bug", "S1244:Floating point numbers should not be tested for equality", Justification = "Not applicable here since we want to detect changes in the value.")]
public double Value public double Value
{ {
get => _value; get => _value;
-20
View File
@@ -1,20 +0,0 @@
{"message":"No project detected for extracting metadata.","source":"MetadataCommand.ExtractMetadata","date_time":"2022-03-23T17:27:09.2932822Z","message_severity":"warning","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.1.1.3"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\ukjrz4sg.zci'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.803282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.3"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\dwwldwwa.bzu'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.803282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.4"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\t3fz202q.zli'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.803282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.5"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\i2420sau.kem'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.803282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.6"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\cb1eimph.z3j'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.803282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.7"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\1055nvuh.cur'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.803282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.8"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\1dkzedxu.fnm'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.803282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.9"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\nkzrssns.lck'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.8042823Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.10"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\lwogcavo.5da'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.8042823Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.11"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\puc5uwdx.h5d'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.8042823Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.12"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\v12lrk4s.ngm'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.805282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.13"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\0hscybzu.ob2'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.805282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.14"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\rphd3j2r.zjr'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-23T17:27:10.805282Z","message_severity":"error","code":"FatalError","correlation_id":"05F9403E-48D7-4391-A021-24959EE7708C.15"}
{"message":"No project detected for extracting metadata.","source":"MetadataCommand.ExtractMetadata","date_time":"2022-03-24T12:36:07.32842Z","message_severity":"warning","correlation_id":"831032FA-9498-4B91-8F87-6478A11BD9B2.1.1.3"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\puc5uwdx.h5d'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-24T12:36:18.2594668Z","message_severity":"error","code":"FatalError","correlation_id":"831032FA-9498-4B91-8F87-6478A11BD9B2.3"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\lwogcavo.5da'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-24T12:36:18.2604591Z","message_severity":"error","code":"FatalError","correlation_id":"831032FA-9498-4B91-8F87-6478A11BD9B2.4"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\v12lrk4s.ngm'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-24T12:36:18.2604591Z","message_severity":"error","code":"FatalError","correlation_id":"831032FA-9498-4B91-8F87-6478A11BD9B2.5"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\t3fz202q.zli'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-24T12:36:18.2604591Z","message_severity":"error","code":"FatalError","correlation_id":"831032FA-9498-4B91-8F87-6478A11BD9B2.6"}
{"message":"System.IO.DirectoryNotFoundException: Could not find a part of the path 'C:\\Users\\David\\Programmieren\\DesktopMagic\\src\\DesktopMagicPluginAPI\\obj\\.cache\\build\\dwcg0axe.jl4\\0hscybzu.ob2'.\r\n at System.IO.__Error.WinIOError(Int32 errorCode, String maybeFullPath)\r\n at System.IO.File.InternalCopy(String sourceFileName, String destFileName, Boolean overwrite, Boolean checkHost)\r\n at Microsoft.DocAsCode.Common.RealFileWriter.Copy(PathMapping sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.FileAbstractLayer.Copy(RelativePath sourceFileName, RelativePath destFileName)\r\n at Microsoft.DocAsCode.Common.ManifestFileHelper.<>c__DisplayClass8_0.<Dereference>b__4(OutputFileInfo ofi)\r\n at System.Threading.Tasks.Parallel.<>c__DisplayClass42_0`2.<PartitionerForEachWorker>b__1()\r\n at System.Threading.Tasks.Task.InnerInvokeWithArg(Task childTask)\r\n at System.Threading.Tasks.Task.<>c__DisplayClass176_0.<ExecuteSelfReplicating>b__0(Object <p0>)","date_time":"2022-03-24T12:36:18.2604591Z","message_severity":"error","code":"FatalError","correlation_id":"831032FA-9498-4B91-8F87-6478A11BD9B2.7"}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Some files were not shown because too many files have changed in this diff Show More