mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 23:39:29 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4ba39c67ce | ||
|
|
507a0e9458 | ||
|
|
af2666f9a6 | ||
|
|
c48eb532f2 | ||
|
|
a6696a87d8 | ||
|
|
df650afd00 | ||
|
|
393ccb0ba6 | ||
|
|
bd29bcdb37 | ||
|
|
2e219e66c9 | ||
|
|
f4ac2c65f3 | ||
|
|
5fcbe2af79 | ||
|
|
33962cd68b | ||
|
|
c6b973b111 | ||
|
|
d43f1ed949 | ||
|
|
4999ae6e88 | ||
|
|
6061efba02 | ||
|
|
8e333df71b | ||
|
|
2c82c9373f | ||
|
|
51b4f15cf1 | ||
|
|
2916f0983a | ||
|
|
3683e75a17 | ||
|
|
3d98c8b196 | ||
|
|
8c97f6b56a | ||
|
|
01a1d109ff | ||
|
|
d2f123e0f5 | ||
|
|
f217574022 | ||
|
|
1e9c74af52 | ||
|
|
2853d49dd9 | ||
|
|
4133f61a5c | ||
|
|
e3056a398a | ||
|
|
cd0bcad181 | ||
|
|
fec26069be | ||
|
|
f84dd20eb9 | ||
|
|
9581cc185c | ||
|
|
a438ab5475 | ||
|
|
1b496db094 | ||
|
|
3dce9ab59a | ||
|
|
a85e57ad58 | ||
|
|
d319b32263 | ||
|
|
ca83e5585f | ||
|
|
0b278ed31a | ||
|
|
346a7ad5a1 | ||
|
|
ea07f0db3a | ||
|
|
667c913e57 | ||
|
|
4d0119e4b3 | ||
|
|
f957edd886 | ||
|
|
1a6cd5a744 | ||
|
|
deddea2888 | ||
|
|
e18ab1ec56 | ||
|
|
a8236bd8da | ||
|
|
cb8df79a5f | ||
|
|
907d743c0d | ||
|
|
835ef23719 | ||
|
|
d2da426baf | ||
|
|
82fe362559 | ||
|
|
439eca0ef0 | ||
|
|
fca25aeb59 | ||
|
|
dd9953cb7d | ||
|
|
990793fb01 | ||
|
|
c4517fb8ac | ||
|
|
92ecd2e93d | ||
|
|
056c42d4df | ||
|
|
c64b70eb92 | ||
|
|
0804738e45 | ||
|
|
727324ca9a | ||
|
|
389256a380 | ||
|
|
ec7917625c | ||
|
|
6aa107233c | ||
|
|
3edb2cff84 | ||
|
|
0bb5b10dba | ||
|
|
2b363bcd18 | ||
|
|
03da9189a5 | ||
|
|
0a57b8a3d6 | ||
|
|
16c5c98f1a | ||
|
|
9e9c49214a | ||
|
|
43c77e6a36 | ||
|
|
4778f5673f | ||
|
|
e3d6eb4c8c | ||
|
|
c90be28f38 | ||
|
|
a564b4db5e | ||
|
|
2cece7dbdc | ||
|
|
ba5bfc7df3 | ||
|
|
d316108868 | ||
|
|
9a96c60af0 | ||
|
|
a8fe2c80f1 | ||
|
|
ac7826898a | ||
|
|
780b506103 | ||
|
|
10733383d3 | ||
|
|
09ce072ec6 | ||
|
|
980ef6473e | ||
|
|
b380e35189 | ||
|
|
3fcda8e64e | ||
|
|
0bc7033196 | ||
|
|
10fe4c773b | ||
|
|
b7139a8821 | ||
|
|
d285bdc489 | ||
|
|
0a64184c32 | ||
|
|
2df00fbeeb | ||
|
|
088f51410a | ||
|
|
70d08aa4c3 | ||
|
|
af9aa60bd9 | ||
|
|
695cb867ee | ||
|
|
376176460b | ||
|
|
327b79d69c | ||
|
|
c74bc60209 | ||
|
|
930da5d679 | ||
|
|
ffd92d2259 | ||
|
|
6100f444a8 | ||
|
|
ead20d1279 | ||
|
|
b638eff451 | ||
|
|
793dcbad38 | ||
|
|
c2474e6425 | ||
|
|
cbeaaee88e | ||
|
|
92c13c5ed0 | ||
|
|
26e979af79 | ||
|
|
29619dbdda | ||
|
|
bc11832f9f | ||
|
|
f8f06a9e2c | ||
|
|
35d64ef2c3 | ||
|
|
3b9d49e8da | ||
|
|
aada1b8807 | ||
|
|
9abfb9cb66 | ||
|
|
ce1c181b14 | ||
|
|
e6be5c011b | ||
|
|
c6e858764f | ||
|
|
f684cdb051 | ||
|
|
3993e6d612 | ||
|
|
8e187f91fa | ||
|
|
566bcee426 | ||
|
|
038074cd87 | ||
|
|
7e1736696d | ||
|
|
e545f27461 | ||
|
|
d458feca1e | ||
|
|
f899fd3e9b | ||
|
|
c3b867ff67 | ||
|
|
1fe59e6338 | ||
|
|
242f6a8388 | ||
|
|
f7f75a4580 | ||
|
|
475623f333 | ||
|
|
aa1733e0c6 | ||
|
|
5804bade47 | ||
|
|
ca9f426bba | ||
|
|
4cd8c51592 | ||
|
|
d4cd384ce4 | ||
|
|
414f84a024 | ||
|
|
3c732117d3 | ||
|
|
9a75cbd8e7 | ||
|
|
536890a5e4 | ||
|
|
975831eb18 |
@@ -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
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
# DesktopMagic
|

|
||||||
|
|
||||||
> 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
|
||||||
|
|
||||||
|
[](https://x.com/search?q=from%3A%40StoneRedCode%20%23DesktopMagic)
|
||||||
|
|
||||||
## Preview
|
## Preview
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## 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,10 +1,10 @@
|
|||||||
; 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 GetVersionNumbersString("/publish/DesktopMagic.exe")
|
#define MyAppVersion GetStringFileInfo("/publish/DesktopMagic.exe","ProductVersion")
|
||||||
#define MyAppPublisher "Stone_Red"
|
#define MyAppPublisher "Stone_Red"
|
||||||
#define MyAppExeName "DesktopMagic.exe"
|
#define MyAppExeName "DesktopMagic.exe"
|
||||||
|
|
||||||
|
|||||||
+92
-4
@@ -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
|
||||||
|
|||||||
@@ -1,18 +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/MaterialDesignTheme.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="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>
|
||||||
+108
-98
@@ -1,14 +1,12 @@
|
|||||||
global using Stone_Red_Utilities.Logging;
|
using CuteUtils.Logging;
|
||||||
|
|
||||||
using AlwaysUpToDate;
|
|
||||||
|
|
||||||
using Stone_Red_C_Sharp_Utilities.Logging;
|
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
|
using Wpf.Ui;
|
||||||
|
|
||||||
namespace DesktopMagic;
|
namespace DesktopMagic;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -18,58 +16,109 @@ 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";
|
||||||
private readonly string logFilePath;
|
|
||||||
#if DEBUG
|
// This is the previous name of the application, used to migrate the application data folder
|
||||||
private readonly Updater updater = new Updater(TimeSpan.FromDays(1), "https://raw.githubusercontent.com/Stone-Red-Code/DesktopMagic/develop/update/updateInfo.json");
|
private const string PreviousAppName = "Desktop Magic";
|
||||||
#else
|
|
||||||
private readonly Updater updater = new Updater(TimeSpan.FromDays(1), "https://raw.githubusercontent.com/Stone-Red-Code/DesktopMagic/main/update/updateInfo.json");
|
private static readonly string logFilePath = Path.Combine(ApplicationDataPath, $"{AppName}.log");
|
||||||
#endif
|
|
||||||
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);
|
||||||
|
|
||||||
private readonly Logger logger = new Logger();
|
public static string PluginsPath => Path.Combine(ApplicationDataPath, "Plugins");
|
||||||
public static string ApplicationDataPath { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "StoneRed", AppName);
|
|
||||||
|
|
||||||
public static Logger Logger => ((App)Current).logger;
|
public static IContentDialogService DialogService { get; } = new ContentDialogService();
|
||||||
|
|
||||||
|
public static Logger Logger { get; } = new Logger()
|
||||||
|
{
|
||||||
|
Config = new()
|
||||||
|
{
|
||||||
|
FatalConfig = new OutputConfig()
|
||||||
|
{
|
||||||
|
ConsoleColor = ConsoleColor.DarkRed,
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = logFilePath
|
||||||
|
},
|
||||||
|
ErrorConfig = new OutputConfig()
|
||||||
|
{
|
||||||
|
ConsoleColor = ConsoleColor.Red,
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = logFilePath
|
||||||
|
},
|
||||||
|
WarnConfig = new OutputConfig()
|
||||||
|
{
|
||||||
|
ConsoleColor = ConsoleColor.Yellow,
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = logFilePath
|
||||||
|
},
|
||||||
|
InfoConfig = new OutputConfig()
|
||||||
|
{
|
||||||
|
ConsoleColor = ConsoleColor.White,
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = logFilePath
|
||||||
|
},
|
||||||
|
DebugConfig = new OutputConfig()
|
||||||
|
{
|
||||||
|
ConsoleColor = ConsoleColor.Gray,
|
||||||
|
LogTarget = LogTarget.DebugConsole,
|
||||||
|
},
|
||||||
|
FormatConfig = new FormatConfig()
|
||||||
|
{
|
||||||
|
DebugConsoleFormat = $"> {{{LogFormatType.DateTime}:HH:mm:ss}} | {{{LogFormatType.LogSeverity},-5}} | {{{LogFormatType.Message}}}\nat {{{LogFormatType.LineNumber}}} | {{{LogFormatType.FilePath}}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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()
|
||||||
{
|
{
|
||||||
logFilePath = ApplicationDataPath + "\\log.log";
|
|
||||||
|
|
||||||
// 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.Log("Shutting down because other instance already running.", "Setup", LogSeverity.Warn);
|
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);
|
||||||
Exit += CloseHandler;
|
Exit += CloseHandler;
|
||||||
|
|
||||||
updater.ProgressChanged += Updater_ProgressChanged;
|
|
||||||
updater.OnException += Updater_OnException;
|
|
||||||
updater.NoUpdateAvailible += Updater_NoUpdateAvailible;
|
|
||||||
updater.UpdateAvailible += Updater_UpdateAvailible;
|
|
||||||
updater.Start();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,78 +128,45 @@ public partial class App : Application
|
|||||||
eventThread?.Interrupt();
|
eventThread?.Interrupt();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Updater_UpdateAvailible(string version, string additionalInformation)
|
private static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||||
{
|
{
|
||||||
updater.Update();
|
Exception exception = (Exception)e.ExceptionObject;
|
||||||
|
Logger.LogFatal(exception + (e.IsTerminating ? "\t Process terminating!" : ""), source: exception.Source ?? "Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Updater_NoUpdateAvailible()
|
private static async void Setup(bool clearLogFile)
|
||||||
{
|
|
||||||
Logger.Log("No update available", "Updater");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Updater_OnException(Exception exception)
|
|
||||||
{
|
|
||||||
Logger.Log(exception.ToString(), "Updater");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Updater_ProgressChanged(long? totalFileSize, long totalBytesDownloaded, double? progressPercentage)
|
|
||||||
{
|
|
||||||
Logger.Log($"Downloading: {progressPercentage}% {totalBytesDownloaded}/{totalFileSize}", "Updater");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Setup(bool clearLogFile)
|
|
||||||
{
|
{
|
||||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||||
|
|
||||||
Logger.Config = new LogConfig()
|
|
||||||
{
|
|
||||||
FatalConfig = new OutputConfig()
|
|
||||||
{
|
|
||||||
Color = ConsoleColor.DarkRed,
|
|
||||||
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
|
||||||
FilePath = logFilePath
|
|
||||||
},
|
|
||||||
ErrorConfig = new OutputConfig()
|
|
||||||
{
|
|
||||||
Color = ConsoleColor.Red,
|
|
||||||
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
|
||||||
FilePath = logFilePath
|
|
||||||
},
|
|
||||||
WarnConfig = new OutputConfig()
|
|
||||||
{
|
|
||||||
Color = ConsoleColor.Yellow,
|
|
||||||
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
|
||||||
FilePath = logFilePath
|
|
||||||
},
|
|
||||||
InfoConfig = new OutputConfig()
|
|
||||||
{
|
|
||||||
Color = ConsoleColor.White,
|
|
||||||
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
|
||||||
FilePath = logFilePath
|
|
||||||
},
|
|
||||||
DebugConfig = new OutputConfig()
|
|
||||||
{
|
|
||||||
Color = ConsoleColor.Gray,
|
|
||||||
LogTarget = LogTarget.DebugConsole,
|
|
||||||
},
|
|
||||||
FormatConfig = new FormatConfig()
|
|
||||||
{
|
|
||||||
DebugConsoleFormat = $"> {{{LogFormatType.DateTime}:HH:mm:ss}} | {{{LogFormatType.LogSeverity},-5}} | {{{LogFormatType.Message}}}\nat {{{LogFormatType.LineNumber}}} | {{{LogFormatType.FilePath}}}"
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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.Log("Created ApplicationData Folder", "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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,12 +175,6 @@ public partial class App : Application
|
|||||||
Logger.ClearLogFile(LogSeverity.Info);
|
Logger.ClearLogFile(LogSeverity.Info);
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.Log("Log setup complete", "Setup");
|
Logger.LogInfo("Setup complete", source: "Setup");
|
||||||
}
|
|
||||||
|
|
||||||
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
|
||||||
{
|
|
||||||
Exception exception = (Exception)e.ExceptionObject;
|
|
||||||
Logger.Log(exception + (e.IsTerminating ? "\t Process terminating!" : ""), exception.Source, LogSeverity.Fatal);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
using DesktopMagic.Api;
|
||||||
|
using DesktopMagic.Api.Settings;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Drawing.Drawing2D;
|
||||||
|
|
||||||
|
namespace DesktopMagic.BuiltInPlugins;
|
||||||
|
|
||||||
|
internal class BenchmarkPlugin : Plugin
|
||||||
|
{
|
||||||
|
private const int BitmapWidth = 400;
|
||||||
|
private const int BitmapHeight = 300;
|
||||||
|
|
||||||
|
private readonly Random rng = new(42);
|
||||||
|
private readonly object rngLock = new();
|
||||||
|
|
||||||
|
private readonly System.Diagnostics.Stopwatch frameTimer = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
private double fps;
|
||||||
|
private long lastFrameMs;
|
||||||
|
|
||||||
|
[Setting("shape-count", "Shapes")]
|
||||||
|
private readonly IntegerUpDown shapeCount = new(10, 10000, 1000);
|
||||||
|
|
||||||
|
[Setting("shape-type", "Shape Type")]
|
||||||
|
private readonly ComboBox shapeType = new("Rectangles", "Ellipses", "Lines", "Text", "Mixed");
|
||||||
|
|
||||||
|
[Setting("fps-counter", "Fps")]
|
||||||
|
private readonly Label fpsCounter = new("");
|
||||||
|
|
||||||
|
public override int UpdateInterval => 16;
|
||||||
|
|
||||||
|
public override Bitmap? Main()
|
||||||
|
{
|
||||||
|
long now = frameTimer.ElapsedMilliseconds;
|
||||||
|
if (lastFrameMs > 0)
|
||||||
|
{
|
||||||
|
double elapsed = now - lastFrameMs;
|
||||||
|
fps = (fps * 0.9) + (1000.0 / Math.Max(elapsed, 1) * 0.1);
|
||||||
|
}
|
||||||
|
lastFrameMs = now;
|
||||||
|
|
||||||
|
int w = BitmapWidth;
|
||||||
|
int h = BitmapHeight;
|
||||||
|
|
||||||
|
Bitmap bmp = new Bitmap(w, h);
|
||||||
|
using Graphics g = Graphics.FromImage(bmp);
|
||||||
|
g.Clear(Color.Transparent);
|
||||||
|
g.SmoothingMode = SmoothingMode.HighSpeed;
|
||||||
|
g.CompositingQuality = CompositingQuality.HighSpeed;
|
||||||
|
g.InterpolationMode = InterpolationMode.Low;
|
||||||
|
|
||||||
|
Color color = Application.Theme.PrimaryColor;
|
||||||
|
int count = shapeCount.Value;
|
||||||
|
string type = shapeType.Value;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int x, y, rw, rh;
|
||||||
|
lock (rngLock)
|
||||||
|
{
|
||||||
|
x = rng.Next(0, w);
|
||||||
|
y = rng.Next(0, h);
|
||||||
|
rw = rng.Next(5, 60);
|
||||||
|
rh = rng.Next(5, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
using Brush brush = new SolidBrush(Color.FromArgb(rng.Next(100, 255), color));
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "Rectangles":
|
||||||
|
g.FillRectangle(brush, x, y, rw, rh);
|
||||||
|
break;
|
||||||
|
case "Ellipses":
|
||||||
|
g.FillEllipse(brush, x, y, rw, rh);
|
||||||
|
break;
|
||||||
|
case "Lines":
|
||||||
|
{
|
||||||
|
int thickness;
|
||||||
|
lock (rngLock) { thickness = rng.Next(1, 5); }
|
||||||
|
g.DrawLine(new Pen(brush, thickness), x, y, x + rw, y + rh);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "Text":
|
||||||
|
{
|
||||||
|
int fontSize;
|
||||||
|
lock (rngLock) { fontSize = rng.Next(8, 24); }
|
||||||
|
using Font font = new("Arial", fontSize);
|
||||||
|
g.DrawString("Mg", font, brush, x, y);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
{
|
||||||
|
int shape, thickness, fontSize;
|
||||||
|
lock (rngLock)
|
||||||
|
{
|
||||||
|
shape = rng.Next(0, 4);
|
||||||
|
thickness = rng.Next(1, 5);
|
||||||
|
fontSize = rng.Next(8, 24);
|
||||||
|
}
|
||||||
|
if (shape == 0)
|
||||||
|
{
|
||||||
|
g.FillRectangle(brush, x, y, rw, rh);
|
||||||
|
}
|
||||||
|
else if (shape == 1)
|
||||||
|
{
|
||||||
|
g.FillEllipse(brush, x, y, rw, rh);
|
||||||
|
}
|
||||||
|
else if (shape == 2)
|
||||||
|
{
|
||||||
|
g.DrawLine(new Pen(brush, thickness), x, y, x + rw, y + rh);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using Font font = new("Arial", fontSize);
|
||||||
|
g.DrawString("Mg", font, brush, x, y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fpsCounter.Value = fps.ToString();
|
||||||
|
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-2
@@ -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 }
|
||||||
|
}
|
||||||
+16
-3
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,7 +285,7 @@ internal class MusicVisualizerPlugin : Plugin
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
App.Logger.Log(ex.Message, "Music Visualizer", LogSeverity.Error);
|
App.Logger.LogError(ex.Message, source: "Music Visualizer");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
using DesktopMagic.Api;
|
||||||
|
using DesktopMagic.Api.Settings;
|
||||||
|
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DesktopMagic.BuiltInPlugins;
|
||||||
|
|
||||||
|
internal class SkiaBenchmarkPlugin : SkiaPlugin
|
||||||
|
{
|
||||||
|
private readonly Random rng = new(42);
|
||||||
|
private readonly object rngLock = new();
|
||||||
|
|
||||||
|
private readonly System.Diagnostics.Stopwatch frameTimer = System.Diagnostics.Stopwatch.StartNew();
|
||||||
|
private double fps;
|
||||||
|
private long lastFrameMs;
|
||||||
|
|
||||||
|
[Setting("skia-shape-count", "Shapes")]
|
||||||
|
private readonly IntegerUpDown shapeCount = new(10, 10000, 1000);
|
||||||
|
|
||||||
|
[Setting("skia-shape-type", "Shape Type")]
|
||||||
|
private readonly ComboBox shapeType = new("Rectangles", "Ellipses", "Lines", "Text", "Mixed");
|
||||||
|
|
||||||
|
public override int UpdateInterval => 16;
|
||||||
|
|
||||||
|
public override void Main(SKCanvas canvas)
|
||||||
|
{
|
||||||
|
long now = frameTimer.ElapsedMilliseconds;
|
||||||
|
if (lastFrameMs > 0)
|
||||||
|
{
|
||||||
|
double elapsed = now - lastFrameMs;
|
||||||
|
fps = fps * 0.9 + (1000.0 / Math.Max(elapsed, 1)) * 0.1;
|
||||||
|
}
|
||||||
|
lastFrameMs = now;
|
||||||
|
|
||||||
|
int w = (int)canvas.DeviceClipBounds.Width;
|
||||||
|
int h = (int)canvas.DeviceClipBounds.Height;
|
||||||
|
|
||||||
|
canvas.Clear(SKColors.Transparent);
|
||||||
|
|
||||||
|
SKColor color = new(
|
||||||
|
Application.Theme.PrimaryColor.R,
|
||||||
|
Application.Theme.PrimaryColor.G,
|
||||||
|
Application.Theme.PrimaryColor.B,
|
||||||
|
Application.Theme.PrimaryColor.A);
|
||||||
|
|
||||||
|
int count = shapeCount.Value;
|
||||||
|
string type = shapeType.Value;
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
int x, y, rw, rh;
|
||||||
|
lock (rngLock)
|
||||||
|
{
|
||||||
|
x = rng.Next(0, w);
|
||||||
|
y = rng.Next(0, h);
|
||||||
|
rw = rng.Next(5, 60);
|
||||||
|
rh = rng.Next(5, 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte alpha = (byte)rng.Next(100, 255);
|
||||||
|
using var paint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = color.WithAlpha(alpha),
|
||||||
|
IsAntialias = false,
|
||||||
|
Style = SKPaintStyle.Fill
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "Rectangles":
|
||||||
|
canvas.DrawRect(x, y, rw, rh, paint);
|
||||||
|
break;
|
||||||
|
case "Ellipses":
|
||||||
|
canvas.DrawOval(x + rw / 2f, y + rh / 2f, rw / 2f, rh / 2f, paint);
|
||||||
|
break;
|
||||||
|
case "Lines":
|
||||||
|
paint.Style = SKPaintStyle.Stroke;
|
||||||
|
paint.StrokeWidth = 2;
|
||||||
|
canvas.DrawLine(x, y, x + rw, y + rh, paint);
|
||||||
|
break;
|
||||||
|
case "Text":
|
||||||
|
using (var textFont = new SKFont(SKTypeface.Default, 16))
|
||||||
|
{
|
||||||
|
canvas.DrawText("Mg", x, y + 16, SKTextAlign.Left, textFont, paint);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
int shape;
|
||||||
|
lock (rngLock) { shape = rng.Next(0, 4); }
|
||||||
|
if (shape == 0)
|
||||||
|
{
|
||||||
|
canvas.DrawRect(x, y, rw, rh, paint);
|
||||||
|
}
|
||||||
|
else if (shape == 1)
|
||||||
|
{
|
||||||
|
canvas.DrawOval(x + rw / 2f, y + rh / 2f, rw / 2f, rh / 2f, paint);
|
||||||
|
}
|
||||||
|
else if (shape == 2)
|
||||||
|
{
|
||||||
|
paint.Style = SKPaintStyle.Stroke;
|
||||||
|
paint.StrokeWidth = 2;
|
||||||
|
canvas.DrawLine(x, y, x + rw, y + rh, paint);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using var textFont2 = new SKFont(SKTypeface.Default, 16);
|
||||||
|
canvas.DrawText("Mg", x, y + 16, SKTextAlign.Left, textFont2, paint);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var fpsFont = new SKFont(SKTypeface.Default, 14);
|
||||||
|
using var fpsPaint = new SKPaint
|
||||||
|
{
|
||||||
|
Color = color,
|
||||||
|
IsAntialias = false
|
||||||
|
};
|
||||||
|
canvas.DrawText($"FPS: {fps:F1} | Shapes: {count} | Type: {type}", 8, 18, SKTextAlign.Left, fpsFont, fpsPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
-5
@@ -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;
|
||||||
|
|
||||||
|
searchButton.OnClick += () =>
|
||||||
|
{
|
||||||
|
isLoading = true;
|
||||||
|
Application.UpdateWindow();
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await UpdateLocationAndWeather();
|
||||||
|
isLoading = false;
|
||||||
|
Application.UpdateWindow();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await UpdateLocationAndWeather();
|
||||||
|
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}¤t_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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<UserControl x:Class="DesktopMagic.Controls.ScreenSelector"
|
||||||
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
d:DesignHeight="60"
|
||||||
|
d:DesignWidth="600"
|
||||||
|
Loaded="UserControl_Loaded"
|
||||||
|
SizeChanged="UserControl_SizeChanged"
|
||||||
|
Unloaded="UserControl_Unloaded">
|
||||||
|
|
||||||
|
<UserControl.Resources>
|
||||||
|
<BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
|
||||||
|
</UserControl.Resources>
|
||||||
|
|
||||||
|
<Grid Background="Transparent">
|
||||||
|
<ItemsControl ItemsSource="{Binding ItemsSource, RelativeSource={RelativeSource AncestorType=UserControl}}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<Canvas />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<Border x:Name="ScreenBorder"
|
||||||
|
Width="{Binding Width}"
|
||||||
|
Height="{Binding Height}"
|
||||||
|
MouseLeftButtonDown="Screen_MouseLeftButtonDown"
|
||||||
|
Cursor="Hand"
|
||||||
|
ToolTipService.ToolTip="{Binding ToolTipText}">
|
||||||
|
<Border.RenderTransform>
|
||||||
|
<TranslateTransform X="{Binding X}" Y="{Binding Y}" />
|
||||||
|
</Border.RenderTransform>
|
||||||
|
<Border.Style>
|
||||||
|
<Style TargetType="Border">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ControlStrokeColorDefaultBrush}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="4" />
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="IsMouseOver" Value="True">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource ControlFillColorSecondaryBrush}" />
|
||||||
|
</Trigger>
|
||||||
|
<DataTrigger Binding="{Binding IsSelected}" Value="True">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource AccentFillColorDefaultBrush}" />
|
||||||
|
<Setter Property="BorderThickness" Value="2" />
|
||||||
|
</DataTrigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</Border.Style>
|
||||||
|
|
||||||
|
<Grid>
|
||||||
|
<TextBlock
|
||||||
|
Margin="4,1,0,0"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
FontSize="11"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||||
|
Text="{Binding Index}" />
|
||||||
|
|
||||||
|
<Ellipse
|
||||||
|
Width="7"
|
||||||
|
Height="7"
|
||||||
|
Margin="0,0,3,3"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
VerticalAlignment="Bottom"
|
||||||
|
Fill="{DynamicResource AccentFillColorDefaultBrush}"
|
||||||
|
Visibility="{Binding IsPrimary, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||||
|
ToolTipService.ToolTip="Primary screen" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
using DesktopMagic.DataContexts;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.Specialized;
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Controls;
|
||||||
|
using System.Windows.Input;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Controls;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Visual screen selector showing the connected monitors as a mini-map
|
||||||
|
/// scaled to the control's size while preserving their relative positions.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ScreenSelector : UserControl
|
||||||
|
{
|
||||||
|
public static readonly DependencyProperty ItemsSourceProperty =
|
||||||
|
DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(ScreenSelector),
|
||||||
|
new PropertyMetadata(null, OnItemsSourceChanged));
|
||||||
|
|
||||||
|
public IEnumerable? ItemsSource
|
||||||
|
{
|
||||||
|
get => (IEnumerable?)GetValue(ItemsSourceProperty);
|
||||||
|
set => SetValue(ItemsSourceProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static readonly DependencyProperty SelectedItemProperty =
|
||||||
|
DependencyProperty.Register(nameof(SelectedItem), typeof(ScreenDisplay), typeof(ScreenSelector),
|
||||||
|
new PropertyMetadata(null, OnSelectedItemChanged));
|
||||||
|
|
||||||
|
public ScreenDisplay? SelectedItem
|
||||||
|
{
|
||||||
|
get => (ScreenDisplay?)GetValue(SelectedItemProperty);
|
||||||
|
set => SetValue(SelectedItemProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private INotifyCollectionChanged? notifySource;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raised when the user picks a screen by clicking it.
|
||||||
|
/// </summary>
|
||||||
|
public event Action? SelectionChanged;
|
||||||
|
|
||||||
|
public ScreenSelector()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
ScreenSelector control = (ScreenSelector)d;
|
||||||
|
control.AttachSource();
|
||||||
|
control.UpdateCanvas();
|
||||||
|
control.UpdateSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
((ScreenSelector)d).UpdateSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AttachSource()
|
||||||
|
{
|
||||||
|
if (notifySource is INotifyCollectionChanged oldSource)
|
||||||
|
{
|
||||||
|
oldSource.CollectionChanged -= Source_CollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySource = ItemsSource as INotifyCollectionChanged;
|
||||||
|
if (notifySource is not null)
|
||||||
|
{
|
||||||
|
notifySource.CollectionChanged += Source_CollectionChanged;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Source_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateCanvas();
|
||||||
|
UpdateSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserControl_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
// Re-attach in case the control was unloaded and the source instance changed.
|
||||||
|
AttachSource();
|
||||||
|
UpdateCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserControl_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateCanvas();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UserControl_Unloaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
if (notifySource is INotifyCollectionChanged source)
|
||||||
|
{
|
||||||
|
source.CollectionChanged -= Source_CollectionChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifySource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Scales the real screen bounds so they fit the control while keeping their
|
||||||
|
/// relative positions (including negative coordinates), then centers them.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateCanvas()
|
||||||
|
{
|
||||||
|
if (ItemsSource is null || ActualWidth <= 0 || ActualHeight <= 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ScreenDisplay> screens = ItemsSource.Cast<ScreenDisplay>().ToList();
|
||||||
|
if (screens.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union of all monitor bounds (virtual screen space, negative coords kept).
|
||||||
|
Rectangle totalBounds = new Rectangle();
|
||||||
|
foreach (var item in screens)
|
||||||
|
{
|
||||||
|
totalBounds = Rectangle.Union(totalBounds, item.Bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Uniform scale factor + a little margin so the mini-map never touches the edge.
|
||||||
|
double factor = Math.Max(totalBounds.Height / ActualHeight, totalBounds.Width / ActualWidth) + 2;
|
||||||
|
|
||||||
|
foreach (var item in screens)
|
||||||
|
{
|
||||||
|
item.X = item.Bounds.Left / factor;
|
||||||
|
item.Y = item.Bounds.Top / factor;
|
||||||
|
item.Width = item.Bounds.Width / factor;
|
||||||
|
item.Height = item.Bounds.Height / factor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center the whole arrangement in the control.
|
||||||
|
double minLeft = screens.Min(item => item.X);
|
||||||
|
double maxRight = screens.Max(item => item.X + item.Width);
|
||||||
|
double minTop = screens.Min(item => item.Y);
|
||||||
|
double maxBottom = screens.Max(item => item.Y + item.Height);
|
||||||
|
|
||||||
|
double horizontalOffset = ((maxRight + minLeft) / 2) - (ActualWidth / 2);
|
||||||
|
double verticalOffset = ((maxBottom + minTop) / 2) - (ActualHeight / 2);
|
||||||
|
|
||||||
|
foreach (var item in screens)
|
||||||
|
{
|
||||||
|
item.X -= horizontalOffset;
|
||||||
|
item.Y -= verticalOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSelection()
|
||||||
|
{
|
||||||
|
if (ItemsSource is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (ScreenDisplay item in ItemsSource)
|
||||||
|
{
|
||||||
|
item.IsSelected = item == SelectedItem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Screen_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is FrameworkElement element && element.DataContext is ScreenDisplay screen)
|
||||||
|
{
|
||||||
|
SelectedItem = screen;
|
||||||
|
SelectionChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
e.Handled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
using DesktopMagic.Settings;
|
using DesktopMagic.Helpers;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace DesktopMagic.DataContexts;
|
namespace DesktopMagic.DataContexts;
|
||||||
@@ -10,7 +14,20 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
|||||||
public event PropertyChangedEventHandler? PropertyChanged;
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
private static DesktopMagicSettings settings = new();
|
private static DesktopMagicSettings settings = new();
|
||||||
|
private static string? selectedScreenDeviceName;
|
||||||
|
private string? selectedLayoutName;
|
||||||
|
|
||||||
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
|
||||||
{
|
{
|
||||||
@@ -22,6 +39,43 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ObservableCollection<ScreenDisplay> Screens { get; } = [];
|
||||||
|
|
||||||
|
public string? SelectedScreenId
|
||||||
|
{
|
||||||
|
get => selectedScreenDeviceName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
selectedScreenDeviceName = value;
|
||||||
|
UpdateSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public ScreenDisplay? SelectedScreen
|
||||||
|
{
|
||||||
|
get => Screens.FirstOrDefault(screen => screen.DeviceName == SelectedScreenId);
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value is not null)
|
||||||
|
{
|
||||||
|
SelectedScreenId = value.DeviceName;
|
||||||
|
}
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Layout SelectedLayout => Manager.Instance.SelectedLayout;
|
||||||
|
|
||||||
|
public string? SelectedLayoutName
|
||||||
|
{
|
||||||
|
get => selectedLayoutName;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
selectedLayoutName = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsLoading
|
public bool IsLoading
|
||||||
{
|
{
|
||||||
get => isLoading;
|
get => isLoading;
|
||||||
@@ -32,13 +86,195 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Refreshes the list of detected screens and ensures a screen is selected.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshScreens()
|
||||||
|
{
|
||||||
|
List<System.Windows.Forms.Screen> allScreens = ScreenUtilities.GetAllScreens();
|
||||||
|
Dictionary<string, System.Drawing.Rectangle> physicalBounds = ScreenUtilities.GetDisplayPhysicalBounds();
|
||||||
|
|
||||||
|
// Capture the hardware id of the currently selected screen so the selection can be
|
||||||
|
// preserved across display changes (e.g. unplug/replug renumbers device names).
|
||||||
|
ScreenDisplay? previousSelected = SelectedScreen;
|
||||||
|
|
||||||
|
Screens.Clear();
|
||||||
|
for (int i = 0; i < allScreens.Count; i++)
|
||||||
|
{
|
||||||
|
System.Windows.Forms.Screen screen = allScreens[i];
|
||||||
|
physicalBounds.TryGetValue(screen.DeviceName, out System.Drawing.Rectangle physicalBoundsRect);
|
||||||
|
Screens.Add(new ScreenDisplay(screen, i, physicalBoundsRect, ScreenUtilities.GetMonitorHardwareId(screen)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedScreenDeviceName is null || !Screens.Any(screen => screen.DeviceName == selectedScreenDeviceName))
|
||||||
|
{
|
||||||
|
ScreenDisplay? byHardwareId = previousSelected is null
|
||||||
|
? null
|
||||||
|
: Screens.FirstOrDefault(screen => screen.HardwareId == previousSelected.HardwareId);
|
||||||
|
SelectedScreenId = byHardwareId?.DeviceName ?? Screens.FirstOrDefault()?.DeviceName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UpdateSelection();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Raises change notifications for the currently selected screen's layout.
|
||||||
|
/// </summary>
|
||||||
|
public void RefreshSelection()
|
||||||
|
{
|
||||||
|
UpdateSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keeps the manager's screen selection in sync and raises notifications for the selected layout.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateSelection()
|
||||||
|
{
|
||||||
|
Manager.Instance.SelectedScreenDeviceName = selectedScreenDeviceName;
|
||||||
|
selectedLayoutName = Manager.Instance.SelectedLayout.Name;
|
||||||
|
OnPropertyChanged(nameof(SelectedScreenId));
|
||||||
|
OnPropertyChanged(nameof(SelectedScreen));
|
||||||
|
OnPropertyChanged(nameof(SelectedLayout));
|
||||||
|
OnPropertyChanged(nameof(SelectedLayoutName));
|
||||||
|
}
|
||||||
|
|
||||||
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A detected screen displayed in the UI.
|
||||||
|
/// </summary>
|
||||||
|
public class ScreenDisplay : INotifyPropertyChanged
|
||||||
|
{
|
||||||
|
public event PropertyChangedEventHandler? PropertyChanged;
|
||||||
|
|
||||||
|
private bool isSelected;
|
||||||
|
private double x;
|
||||||
|
private double y;
|
||||||
|
private double width;
|
||||||
|
private double height;
|
||||||
|
|
||||||
|
public ScreenDisplay(System.Windows.Forms.Screen screen, int index, System.Drawing.Rectangle physicalBounds, string hardwareId)
|
||||||
|
{
|
||||||
|
DeviceName = screen.DeviceName;
|
||||||
|
DisplayName = ScreenUtilities.GetScreenLabel(screen, index);
|
||||||
|
Bounds = physicalBounds.Width > 0 && physicalBounds.Height > 0 ? physicalBounds : screen.Bounds;
|
||||||
|
IsPrimary = screen.Primary;
|
||||||
|
Index = index + 1;
|
||||||
|
HardwareId = hardwareId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string DeviceName { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stable hardware identifier of the monitor (see <see cref="ScreenUtilities.GetMonitorHardwareId"/>),
|
||||||
|
/// used to persist screen bindings across display changes.
|
||||||
|
/// </summary>
|
||||||
|
public string HardwareId { get; }
|
||||||
|
|
||||||
|
public string DisplayName { get; }
|
||||||
|
|
||||||
|
public System.Drawing.Rectangle Bounds { get; }
|
||||||
|
|
||||||
|
public bool IsPrimary { get; }
|
||||||
|
|
||||||
|
public int Index { get; }
|
||||||
|
|
||||||
|
public string ToolTipText => IsPrimary ? $"{DisplayName} · Primary" : DisplayName;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this screen is currently selected in the screen selector.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsSelected
|
||||||
|
{
|
||||||
|
get => isSelected;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
isSelected = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalized (control-local) position/size set by the screen selector.
|
||||||
|
public double X
|
||||||
|
{
|
||||||
|
get => x;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
x = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Y
|
||||||
|
{
|
||||||
|
get => y;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
y = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Width
|
||||||
|
{
|
||||||
|
get => width;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
width = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double Height
|
||||||
|
{
|
||||||
|
get => height;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
height = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnPropertyChanged([CallerMemberName] string? name = null)
|
||||||
|
{
|
||||||
|
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -1,61 +1,81 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<UseWPF>true</UseWPF>
|
<UseWPF>true</UseWPF>
|
||||||
<RootNamespace>DesktopMagic</RootNamespace>
|
<RootNamespace>DesktopMagic</RootNamespace>
|
||||||
<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.0.0</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.0.0</AssemblyVersion>
|
<AssemblyVersion>1.3.3.0</AssemblyVersion>
|
||||||
<FileVersion>1.0.0.0</FileVersion>
|
<FileVersion>1.3.3.0</FileVersion>
|
||||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||||
<Nullable>enable</Nullable>
|
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||||
</PropertyGroup>
|
<Nullable>enable</Nullable>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
|
<Platforms>AnyCPU;x64</Platforms>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||||
<None Remove="icon.ico" />
|
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||||
<None Remove="icons8_volunteering_26_85L_icon.ico" />
|
</PropertyGroup>
|
||||||
<None Remove="icon_Dark.ico" />
|
|
||||||
<None Remove="Resources\Images\modio-cog-blue.png" />
|
|
||||||
<None Remove="Resources\Images\modio-logo-bluedark.png" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Resource Include="Resources\Images\modio-cog-blue.png">
|
<None Remove="icon.ico" />
|
||||||
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
<None Remove="icons8_volunteering_26_85L_icon.ico" />
|
||||||
</Resource>
|
<None Remove="icon_Dark.ico" />
|
||||||
</ItemGroup>
|
<None Remove="Resources\Images\modio-cog-blue.png" />
|
||||||
|
<None Remove="Resources\Images\modio-logo-bluedark.png" />
|
||||||
|
<None Remove="Resources\Images\modio-logo-bluelight.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AlwaysUpToDate" Version="1.0.0.4" />
|
<Resource Include="Resources\Images\modio-cog-blue.png">
|
||||||
<PackageReference Include="BusyIndicators" Version="2.1.2" />
|
<CopyToOutputDirectory>Never</CopyToOutputDirectory>
|
||||||
<PackageReference Include="Extended.Wpf.Toolkit" Version="4.5.1" />
|
</Resource>
|
||||||
<PackageReference Include="MaterialDesignThemes" Version="4.9.0" />
|
</ItemGroup>
|
||||||
<PackageReference Include="Modio.NET" Version="1.1.1" />
|
|
||||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
|
||||||
<PackageReference Include="Stone_Red-C-Sharp-Utilities" Version="1.0.3.2" />
|
|
||||||
<PackageReference Include="System.Management" Version="8.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\DesktopMagicPluginAPI\DesktopMagic.Api.csproj" />
|
<PackageReference Include="BusyIndicators" Version="2.1.3" />
|
||||||
</ItemGroup>
|
<PackageReference Include="CuteUtils" Version="1.0.0" />
|
||||||
|
<PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" />
|
||||||
|
<PackageReference Include="Ical.Net" Version="4.3.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="SkiaSharp" Version="4.148.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>
|
||||||
<Resource Include="icon_Dark.ico">
|
<ProjectReference Include="..\DesktopMagicPluginAPI\DesktopMagic.Api.csproj" />
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
</ItemGroup>
|
||||||
</Resource>
|
|
||||||
<Resource Include="icon.ico">
|
<ItemGroup>
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<Resource Include="icon_Dark.ico">
|
||||||
</Resource>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
<Resource Include="Resources\Images\modio-logo-bluedark.png" />
|
</Resource>
|
||||||
</ItemGroup>
|
<Resource Include="icon.ico">
|
||||||
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
|
</Resource>
|
||||||
|
<Resource Include="Resources\Images\modio-logo-bluedark.png" />
|
||||||
|
<Resource Include="Resources\Images\modio-logo-bluelight.png" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="AutoStart.ps1">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ScreenSelectorDialog"
|
||||||
|
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:controls="clr-namespace:DesktopMagic.Controls"
|
||||||
|
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
|
||||||
|
mc:Ignorable="d"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
|
||||||
|
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
|
||||||
|
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||||
|
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||||
|
WindowCornerPreference="Round"
|
||||||
|
WindowBackdropType="Tabbed"
|
||||||
|
SizeToContent="Height"
|
||||||
|
Width="380"
|
||||||
|
MinWidth="380"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
ExtendsContentIntoTitleBar="True">
|
||||||
|
<Grid>
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="*" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
|
||||||
|
<ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
|
||||||
|
<ui:TitleBar.Icon>
|
||||||
|
<ui:ImageIcon Source="{StaticResource Icon}" />
|
||||||
|
</ui:TitleBar.Icon>
|
||||||
|
</ui:TitleBar>
|
||||||
|
|
||||||
|
<controls:ScreenSelector x:Name="screenSelector" Grid.Row="1" Margin="15" Width="340" Height="200" ItemsSource="{Binding Screens}" SelectedItem="{Binding SelectedScreen, Mode=TwoWay}" />
|
||||||
|
</Grid>
|
||||||
|
</ui:FluentWindow>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using DesktopMagic.DataContexts;
|
||||||
|
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Dialogs;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Modal picker for selecting a screen from a visual monitor layout.
|
||||||
|
/// Closes itself once a screen has been picked.
|
||||||
|
/// </summary>
|
||||||
|
public partial class ScreenSelectorDialog : Wpf.Ui.Controls.FluentWindow
|
||||||
|
{
|
||||||
|
internal ScreenSelectorDialog(MainWindowDataContext dataContext, string title = App.AppName)
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
|
||||||
|
Resources.MergedDictionaries.Add(App.LanguageDictionary);
|
||||||
|
|
||||||
|
DataContext = dataContext;
|
||||||
|
titleBar.Title = title;
|
||||||
|
Title = title;
|
||||||
|
|
||||||
|
// Close the dialog once a screen has been picked.
|
||||||
|
screenSelector.SelectionChanged += () => DialogResult = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,3 +8,9 @@ using System.Diagnostics.CodeAnalysis;
|
|||||||
[assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Windows only application")]
|
[assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "Windows only application")]
|
||||||
[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.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("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)")]
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Security.Cryptography;
|
||||||
|
using System.Text;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Helpers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helpers for enumerating screens and converting between percentage based
|
||||||
|
/// positions/sizes (relative to a screen's bounds) and absolute positions.
|
||||||
|
/// All conversions work in WPF DIP space because WPF window <see cref="Window.Left"/>,
|
||||||
|
/// <see cref="Window.Top"/>, <see cref="Window.Width"/> and <see cref="Window.Height"/>
|
||||||
|
/// are expressed in device independent pixels, while <see cref="System.Windows.Forms.Screen.Bounds"/>
|
||||||
|
/// is expressed in physical pixels. Each screen is therefore converted to DIP space
|
||||||
|
/// using its own DPI scaling factor.
|
||||||
|
/// </summary>
|
||||||
|
public static class ScreenUtilities
|
||||||
|
{
|
||||||
|
private const uint MonitorDefaultToNearest = 0x00000002;
|
||||||
|
|
||||||
|
private const int MDT_EFFECTIVE_DPI = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of all screens currently connected, in the order reported by Windows.
|
||||||
|
/// </summary>
|
||||||
|
public static List<System.Windows.Forms.Screen> GetAllScreens()
|
||||||
|
{
|
||||||
|
return System.Windows.Forms.Screen.AllScreens.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the screen matching the given device name (e.g. "\\.\DISPLAY1"), or null.
|
||||||
|
/// </summary>
|
||||||
|
public static System.Windows.Forms.Screen? GetScreenByDeviceName(string? deviceName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(deviceName))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return System.Windows.Forms.Screen.AllScreens.FirstOrDefault(screen => string.Equals(screen.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the primary screen, or the first available screen as a fallback.
|
||||||
|
/// </summary>
|
||||||
|
public static System.Windows.Forms.Screen GetPrimaryScreen()
|
||||||
|
{
|
||||||
|
return System.Windows.Forms.Screen.PrimaryScreen ?? System.Windows.Forms.Screen.AllScreens.FirstOrDefault()
|
||||||
|
?? throw new InvalidOperationException("No screens detected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aspect ratio (Width / Height) of the screen's bounds.
|
||||||
|
/// </summary>
|
||||||
|
public static double GetAspectRatio(System.Windows.Forms.Screen screen)
|
||||||
|
{
|
||||||
|
if (screen.Bounds.Height == 0)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return screen.Bounds.Width / (double)screen.Bounds.Height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a percentage based position (0..1 relative to the screen bounds) to an absolute
|
||||||
|
/// WPF position (DIPs) on that screen.
|
||||||
|
/// </summary>
|
||||||
|
public static Point PercentToPosition(Point percent, System.Drawing.Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rect dips = GetScreenDips(bounds);
|
||||||
|
return new Point(
|
||||||
|
dips.Left + (percent.X * dips.Width),
|
||||||
|
dips.Top + (percent.Y * dips.Height));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an absolute WPF position (DIPs) to a percentage (0..1) of the screen bounds.
|
||||||
|
/// </summary>
|
||||||
|
public static Point PositionToPercent(Point position, System.Drawing.Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rect dips = GetScreenDips(bounds);
|
||||||
|
if (dips.Width == 0 || dips.Height == 0)
|
||||||
|
{
|
||||||
|
return new Point(0.05, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Point(
|
||||||
|
(position.X - dips.Left) / dips.Width,
|
||||||
|
(position.Y - dips.Top) / dips.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts a percentage based size (0..1 of the screen bounds) to an absolute WPF size (DIPs).
|
||||||
|
/// </summary>
|
||||||
|
public static Point PercentSizeToSize(Point percent, System.Drawing.Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rect dips = GetScreenDips(bounds);
|
||||||
|
return new Point(
|
||||||
|
percent.X * dips.Width,
|
||||||
|
percent.Y * dips.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an absolute WPF size (DIPs) to a percentage (0..1) of the screen bounds.
|
||||||
|
/// </summary>
|
||||||
|
public static Point SizeToPercent(Point size, System.Drawing.Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rect dips = GetScreenDips(bounds);
|
||||||
|
if (dips.Width == 0 || dips.Height == 0)
|
||||||
|
{
|
||||||
|
return new Point(0.3, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Point(
|
||||||
|
size.X / dips.Width,
|
||||||
|
size.Y / dips.Height);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clamps an absolute WPF position (DIPs) so that the window (of the given size)
|
||||||
|
/// stays within the screen bounds. This prevents widgets from being moved to another screen.
|
||||||
|
/// </summary>
|
||||||
|
public static Point ClampToScreenBounds(Point topLeft, Size size, System.Drawing.Rectangle bounds)
|
||||||
|
{
|
||||||
|
Rect dips = GetScreenDips(bounds);
|
||||||
|
double x = topLeft.X;
|
||||||
|
double y = topLeft.Y;
|
||||||
|
|
||||||
|
if (size.Width <= dips.Width)
|
||||||
|
{
|
||||||
|
x = Math.Clamp(x, dips.Left, dips.Right - size.Width);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
x = dips.Left;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (size.Height <= dips.Height)
|
||||||
|
{
|
||||||
|
y = Math.Clamp(y, dips.Top, dips.Bottom - size.Height);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
y = dips.Top;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Point(x, y);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds a human readable label for a screen, e.g. "Display 1 · DELL U2715H · 3840x2160".
|
||||||
|
/// </summary>
|
||||||
|
public static string GetScreenLabel(System.Windows.Forms.Screen screen, int index)
|
||||||
|
{
|
||||||
|
string name = GetFriendlyName(screen);
|
||||||
|
string resolution = $"{screen.Bounds.Width}x{screen.Bounds.Height}";
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(name) || string.Equals(name, "Generic PnP Monitor", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? $"Display {index + 1} · {resolution}"
|
||||||
|
: $"Display {index + 1} · {name} · {resolution}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the friendly monitor model name (e.g. "DELL U2715H") for the given screen,
|
||||||
|
/// or an empty string when it cannot be determined.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetFriendlyName(System.Windows.Forms.Screen screen)
|
||||||
|
{
|
||||||
|
return GetMonitorDisplayDevice(screen)?.DeviceString ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a stable hardware identifier for the screen: the PnP device instance ID of its
|
||||||
|
/// monitor (e.g. "MONITOR\DEL41F1\{...}\{0001}"), which is derived from the monitor's
|
||||||
|
/// EDID and survives disconnects and reconnects of the same monitor. When no hardware ID
|
||||||
|
/// is available (e.g. virtual or remote displays), a deterministic SHA-256 hash of the
|
||||||
|
/// bounds is used so the identifier is never empty.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetMonitorHardwareId(System.Windows.Forms.Screen screen)
|
||||||
|
{
|
||||||
|
string? deviceId = GetMonitorDisplayDevice(screen)?.DeviceID;
|
||||||
|
if (!string.IsNullOrWhiteSpace(deviceId))
|
||||||
|
{
|
||||||
|
return deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Drawing.Rectangle bounds = screen.Bounds;
|
||||||
|
string boundsString = $"{bounds.X}-{bounds.Y}-{bounds.Width}-{bounds.Height}";
|
||||||
|
string hashString = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(boundsString))).ToLowerInvariant();
|
||||||
|
return $"DISPLAY#{hashString}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the monitor device info (second-level <see cref="EnumDisplayDevices"/> entry) for
|
||||||
|
/// the given screen, or null when it cannot be determined.
|
||||||
|
/// </summary>
|
||||||
|
private static DISPLAY_DEVICE? GetMonitorDisplayDevice(System.Windows.Forms.Screen screen)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DISPLAY_DEVICE device = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||||
|
if (EnumDisplayDevices(screen.DeviceName, 0, ref device, 0))
|
||||||
|
{
|
||||||
|
DISPLAY_DEVICE monitor = new DISPLAY_DEVICE { cb = (uint)Marshal.SizeOf<DISPLAY_DEVICE>() };
|
||||||
|
if (EnumDisplayDevices(device.DeviceName, 0, ref monitor, 0))
|
||||||
|
{
|
||||||
|
return monitor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Fall through to null.
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enumerates monitors in a Per-Monitor V2 DPI awareness context and returns each
|
||||||
|
/// monitor's bounds in physical (device) pixels on the virtual screen, keyed by device
|
||||||
|
/// name (e.g. "\\.\DISPLAY1"). This uses the same coordinate space as DPI-aware apps
|
||||||
|
/// such as Lively Wallpaper, keeping multi-screen layouts with mixed DPI scaling
|
||||||
|
/// consistent regardless of this app's own DPI awareness mode.
|
||||||
|
/// </summary>
|
||||||
|
public static Dictionary<string, System.Drawing.Rectangle> GetDisplayPhysicalBounds()
|
||||||
|
{
|
||||||
|
Dictionary<string, System.Drawing.Rectangle> result = new Dictionary<string, System.Drawing.Rectangle>();
|
||||||
|
|
||||||
|
IntPtr prevContext = IntPtr.Zero;
|
||||||
|
bool contextChanged = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
prevContext = SetThreadDpiAwarenessContext((IntPtr)DpiAwarenessContextPerMonitorV2);
|
||||||
|
contextChanged = prevContext != IntPtr.Zero;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// SetThreadDpiAwarenessContext unavailable; fall back to default context.
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = EnumDisplayMonitors(IntPtr.Zero, IntPtr.Zero, (hMonitor, hdcMonitor, lprcMonitor, dwData) =>
|
||||||
|
{
|
||||||
|
MONITORINFOEX info = new MONITORINFOEX { cbSize = (uint)Marshal.SizeOf<MONITORINFOEX>() };
|
||||||
|
if (GetMonitorInfo(hMonitor, ref info))
|
||||||
|
{
|
||||||
|
string deviceName = info.szDevice.TrimEnd('\0');
|
||||||
|
result[deviceName] = new System.Drawing.Rectangle(
|
||||||
|
info.rcMonitor.Left, info.rcMonitor.Top,
|
||||||
|
info.rcMonitor.Right - info.rcMonitor.Left,
|
||||||
|
info.rcMonitor.Bottom - info.rcMonitor.Top);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}, IntPtr.Zero);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (contextChanged)
|
||||||
|
{
|
||||||
|
_ = SetThreadDpiAwarenessContext(prevContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the screen bounds converted to WPF DIP space using the screen's own DPI scaling factor.
|
||||||
|
/// </summary>
|
||||||
|
private static Rect GetScreenDips(System.Drawing.Rectangle bounds)
|
||||||
|
{
|
||||||
|
double scale = GetDpiScale(bounds);
|
||||||
|
return new Rect(bounds.Left / scale, bounds.Top / scale, bounds.Width / scale, bounds.Height / scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the DPI scaling factor (relative to 96 DPI) of the screen containing the given bounds.
|
||||||
|
/// </summary>
|
||||||
|
private static double GetDpiScale(System.Drawing.Rectangle bounds)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
POINT center = new POINT
|
||||||
|
{
|
||||||
|
X = bounds.Left + (bounds.Width / 2),
|
||||||
|
Y = bounds.Top + (bounds.Height / 2)
|
||||||
|
};
|
||||||
|
IntPtr monitor = MonitorFromPoint(center, MonitorDefaultToNearest);
|
||||||
|
if (GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, out uint dpiX, out _) == 0)
|
||||||
|
{
|
||||||
|
return dpiX / 96.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// Fall through to no scaling if DPI APIs are unavailable.
|
||||||
|
}
|
||||||
|
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr MonitorFromPoint(POINT point, uint dwFlags);
|
||||||
|
|
||||||
|
[DllImport("shcore.dll")]
|
||||||
|
private static extern int GetDpiForMonitor(IntPtr hMonitor, int dpiType, out uint dpiX, out uint dpiY);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern bool EnumDisplayDevices(string? lpDevice, uint iDevNum, ref DISPLAY_DEVICE lpDisplayDevice, uint dwFlags);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct DISPLAY_DEVICE
|
||||||
|
{
|
||||||
|
public uint cb;
|
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||||
|
public string DeviceName;
|
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||||
|
public string DeviceString;
|
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||||
|
public string DeviceID;
|
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
|
||||||
|
public string DeviceKey;
|
||||||
|
|
||||||
|
public uint StateFlags;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct POINT
|
||||||
|
{
|
||||||
|
public int X;
|
||||||
|
public int Y;
|
||||||
|
}
|
||||||
|
|
||||||
|
private const long DpiAwarenessContextPerMonitorV2 = -4;
|
||||||
|
|
||||||
|
private delegate bool EnumMonitorsProc(IntPtr hMonitor, IntPtr hdcMonitor, IntPtr lprcMonitor, IntPtr dwData);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern bool EnumDisplayMonitors(IntPtr hdc, IntPtr lprcClip, EnumMonitorsProc lpfnEnum, IntPtr dwData);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
|
||||||
|
private static extern bool GetMonitorInfo(IntPtr hMonitor, ref MONITORINFOEX lpmi);
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct MONITORRECT
|
||||||
|
{
|
||||||
|
public int Left;
|
||||||
|
public int Top;
|
||||||
|
public int Right;
|
||||||
|
public int Bottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||||
|
private struct MONITORINFOEX
|
||||||
|
{
|
||||||
|
public uint cbSize;
|
||||||
|
|
||||||
|
public MONITORRECT rcMonitor;
|
||||||
|
|
||||||
|
public MONITORRECT rcWork;
|
||||||
|
|
||||||
|
public uint dwFlags;
|
||||||
|
|
||||||
|
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
|
||||||
|
public string szDevice;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 async void DisplayException(string message)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo(message, source: "PluginInput");
|
||||||
|
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
|
||||||
|
{
|
||||||
|
Title = "Error",
|
||||||
|
Content = "File execution error:\n" + message,
|
||||||
|
CloseButtonText = "Ok"
|
||||||
|
};
|
||||||
|
_ = await messageBox.ShowDialogAsync();
|
||||||
|
int index = Manager.Instance.PluginWindows.FindIndex(p => p.PluginMetadata.Id == pluginId);
|
||||||
|
|
||||||
|
if (index >= 0 && index < Manager.Instance.PluginWindows.Count)
|
||||||
|
{
|
||||||
|
IPluginWindow window = Manager.Instance.PluginWindows[index];
|
||||||
|
window?.Exit();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DisplayException(string message)
|
|
||||||
{
|
|
||||||
App.Logger.Log(message, "PluginInput");
|
|
||||||
_ = MessageBox.Show("File execution error:\n" + message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
||||||
int index = MainWindow.WindowNames.IndexOf(optionsComboBox.SelectedItem.ToString() ?? string.Empty);
|
|
||||||
|
|
||||||
PluginWindow window = MainWindow.Windows[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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
#pragma warning disable
|
||||||
namespace DesktopMagic.Helpers;
|
namespace DesktopMagic.Helpers;
|
||||||
|
|
||||||
internal class W32
|
internal class W32
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ using System.Runtime.InteropServices;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Interop;
|
using System.Windows.Interop;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
|
#pragma warning disable
|
||||||
namespace DesktopMagic
|
namespace DesktopMagic
|
||||||
{
|
{
|
||||||
using static NativeMethods;
|
using static NativeMethods;
|
||||||
@@ -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;
|
||||||
|
|||||||
+118
-138
@@ -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"
|
WindowState="Normal"
|
||||||
MinHeight="520"
|
|
||||||
MinWidth="570"
|
|
||||||
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>
|
||||||
+246
-628
@@ -1,679 +1,297 @@
|
|||||||
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.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;
|
Microsoft.Win32.SystemEvents.DisplaySettingsChanged += SystemEvents_DisplaySettingsChanged;
|
||||||
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
|
_mainWindowDataContext.IsLoading = true;
|
||||||
{
|
|
||||||
get => mainWindowDataContext.Settings;
|
|
||||||
set => mainWindowDataContext.Settings = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MainWindow()
|
// Load plugins and settings through manager
|
||||||
{
|
_manager.LoadSettings();
|
||||||
DataContext = mainWindowDataContext;
|
_mainWindowDataContext.Settings = _manager.Settings;
|
||||||
|
_manager.LoadPlugins();
|
||||||
|
_manager.LoadLayout();
|
||||||
|
|
||||||
try
|
if (_manager.Settings.IsFirstRun)
|
||||||
{
|
{
|
||||||
Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/DesktopMagic;component/icon.ico")).Stream;
|
_manager.Settings.IsFirstRun = false;
|
||||||
notifyIcon.Click += TaskbarIcon_TrayLeftClick;
|
|
||||||
notifyIcon.Visible = true;
|
|
||||||
notifyIcon.Text = App.AppName;
|
|
||||||
notifyIcon.Icon = new System.Drawing.Icon(iconStream);
|
|
||||||
|
|
||||||
InitializeComponent();
|
|
||||||
|
|
||||||
SetLanguageDictionary();
|
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
Title = $"{App.AppName} - Dev {Assembly.GetExecutingAssembly().GetName().Version}";
|
|
||||||
#else
|
|
||||||
Title = $"{App.AppName} - {Assembly.GetExecutingAssembly().GetName().Version}";
|
|
||||||
#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.Log("Created Plugins Folder", "Main");
|
|
||||||
|
|
||||||
//Write To Log File and Load Elements
|
|
||||||
|
|
||||||
App.Logger.Log("Loading Plugin names", "Main");
|
|
||||||
LoadPlugins();
|
|
||||||
App.Logger.Log("Loading Layout names", "Main");
|
|
||||||
LoadSettings();
|
|
||||||
App.Logger.Log("Loading Layout", "Main");
|
|
||||||
LoadLayout();
|
|
||||||
|
|
||||||
loaded = true;
|
|
||||||
App.Logger.Log("Window Loaded", "Main");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
App.Logger.Log(ex.Message, "Main", LogSeverity.Error);
|
|
||||||
_ = 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.Log($"Plugin \"{directory}\" has no \"main.dll\"", "Main", LogSeverity.Error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pluginMetadataPath is null)
|
|
||||||
{
|
|
||||||
App.Logger.Log($"Plugin \"{directory}\" has no \"metadata.json\"", "Main", LogSeverity.Warn);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(pluginMetadataPath));
|
|
||||||
|
|
||||||
if (pluginMetadata is null)
|
|
||||||
{
|
|
||||||
App.Logger.Log($"Plugin \"{directory}\" has no valid \"metadata.json\"", "Main", LogSeverity.Error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugins.ContainsKey(pluginMetadata.Id))
|
|
||||||
{
|
|
||||||
App.Logger.Log($"Plugin \"{directory}\" has the same id as another plugin", "Main", LogSeverity.Error);
|
|
||||||
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.Log(ex.Message, "Main", LogSeverity.Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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 += () =>
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
if (sender is not Window window)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
WindowPos.SendWpfWindowBack(window);
|
|
||||||
WindowPos.SendWpfWindowBack(window);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DisplayWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
|
|
||||||
{
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
Visibility = Visibility.Collapsed;
|
|
||||||
UpdateLayout();
|
|
||||||
foreach (Window window in Windows)
|
|
||||||
{
|
|
||||||
window.Hide();
|
|
||||||
}
|
|
||||||
Environment.Exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void Window_StateChanged(object? sender, EventArgs? e)
|
|
||||||
{
|
|
||||||
if (WindowState == WindowState.Minimized)
|
|
||||||
{
|
|
||||||
EditCheckBox.IsChecked = false;
|
|
||||||
EditCheckBox_Click(null, null);
|
|
||||||
ShowInTaskbar = false;
|
|
||||||
Visibility = Visibility.Collapsed;
|
|
||||||
foreach (Window item in Windows)
|
|
||||||
{
|
|
||||||
WindowPos.SendWpfWindowBack(item);
|
|
||||||
WindowPos.SendWpfWindowBack(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion Windows
|
|
||||||
|
|
||||||
#region Options
|
|
||||||
|
|
||||||
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();
|
_ = 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
|
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;
|
WindowState = WindowState.Minimized;
|
||||||
Window_StateChanged(null, null);
|
Visibility = Visibility.Collapsed;
|
||||||
|
ShowInTaskbar = false;
|
||||||
}
|
}
|
||||||
else
|
|
||||||
|
_manager.IsLoaded = true;
|
||||||
|
_mainWindowDataContext.IsLoading = false;
|
||||||
|
await ShowLatestReleaseInfoAfterUpdateAsync();
|
||||||
|
|
||||||
|
App.Logger.LogInfo("Application loaded", source: "MainWindow");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError(ex.Message, source: "MainWindow");
|
||||||
|
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
|
||||||
{
|
{
|
||||||
WindowState = WindowState.Normal;
|
Title = App.AppName,
|
||||||
Window_StateChanged(null, null);
|
Content = ex.ToString(),
|
||||||
}
|
CloseButtonText = "Ok"
|
||||||
mainWindowDataContext.IsLoading = false;
|
};
|
||||||
|
_ = await messageBox.ShowDialogAsync();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion Layout
|
private async void NavigationView_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = NavigationView.Navigate(typeof(Pages.MainPage));
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdatePluginsButton_Click(object sender, RoutedEventArgs e)
|
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
|
||||||
|
{
|
||||||
|
_manager.SaveSettings();
|
||||||
|
|
||||||
|
if (_manager.BlockWindowsClosing)
|
||||||
{
|
{
|
||||||
LoadPlugins();
|
e.Cancel = true;
|
||||||
LoadLayout(false);
|
_manager.SetEditMode(false);
|
||||||
|
ShowInTaskbar = false;
|
||||||
|
WindowState = WindowState.Minimized;
|
||||||
|
Visibility = Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OpenPluginsFolderButton_Click(object sender, RoutedEventArgs e)
|
private void Window_Closed(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
_ = Process.Start("explorer.exe", App.ApplicationDataPath + "\\Plugins");
|
Microsoft.Win32.SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
|
||||||
}
|
|
||||||
|
|
||||||
private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
|
Visibility = Visibility.Collapsed;
|
||||||
{
|
UpdateLayout();
|
||||||
ScrollViewer scv = (ScrollViewer)sender;
|
_manager.CloseAllPluginWindows();
|
||||||
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
|
Environment.Exit(0);
|
||||||
e.Handled = true;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void TaskbarIcon_TrayLeftClick(object? sender, EventArgs e)
|
private void NotifyIcon_MouseClick(object? sender, System.Windows.Forms.MouseEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.Button == System.Windows.Forms.MouseButtons.Left)
|
||||||
{
|
{
|
||||||
RestoreWindow();
|
RestoreWindow();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void GitHubButton_Click(object sender, RoutedEventArgs e)
|
private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// Re-enumerate screens and reload all widget windows when monitors are added or removed.
|
||||||
|
_ = Application.Current.Dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
string uri = "https://github.com/Stone-Red-Code/DesktopMagic";
|
_mainWindowDataContext.RefreshScreens();
|
||||||
ProcessStartInfo psi = new ProcessStartInfo
|
_manager.LoadLayout();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,587 @@
|
|||||||
|
using DesktopMagic.BuiltInPlugins;
|
||||||
|
using DesktopMagic.Helpers;
|
||||||
|
using DesktopMagic.Plugins;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Drawing;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Name of the built-in layout that shows no widgets on a screen.
|
||||||
|
public const string EmptyLayoutName = "Empty";
|
||||||
|
|
||||||
|
// 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)},
|
||||||
|
{new((string)App.LanguageDictionary["benchmark"], 8) { Author = "Stone_Red" }, typeof(BenchmarkPlugin)},
|
||||||
|
{new((string)App.LanguageDictionary["skiaBenchmark"], 9) { Author = "Stone_Red" }, typeof(SkiaBenchmarkPlugin)},
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Screen selection (which screen is currently edited in the UI)
|
||||||
|
public string? SelectedScreenDeviceName { get; set; }
|
||||||
|
|
||||||
|
public System.Windows.Forms.Screen SelectedScreen => ScreenUtilities.GetScreenByDeviceName(SelectedScreenDeviceName) ?? ScreenUtilities.GetPrimaryScreen();
|
||||||
|
|
||||||
|
public Layout SelectedLayout => GetLayoutForScreen(SelectedScreen);
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads (or unloads) the given plugin on every screen that currently uses the given layout.
|
||||||
|
/// This keeps screens sharing a layout in sync when plugins are enabled or disabled.
|
||||||
|
/// </summary>
|
||||||
|
public void LoadPlugin(uint pluginId, Layout layout, Action<InternalPluginData>? onPluginLoaded = null)
|
||||||
|
{
|
||||||
|
if (!_plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!layout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||||
|
{
|
||||||
|
pluginSettings = new PluginSettings();
|
||||||
|
layout.Plugins.Add(pluginId, pluginSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginSettings.Metadata = internalPluginData.Metadata;
|
||||||
|
pluginSettings.Owner = layout;
|
||||||
|
|
||||||
|
foreach (System.Windows.Forms.Screen screen in ScreenUtilities.GetAllScreens())
|
||||||
|
{
|
||||||
|
if (GetLayoutForScreen(screen) == layout)
|
||||||
|
{
|
||||||
|
EnsurePluginWindow(screen, layout, internalPluginData, pluginSettings, onPluginLoaded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.UpdatePlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates or closes the plugin window for a single screen, based on the plugin settings.
|
||||||
|
/// </summary>
|
||||||
|
private void EnsurePluginWindow(System.Windows.Forms.Screen screen, Layout layout, InternalPluginData internalPluginData, PluginSettings pluginSettings, Action<InternalPluginData>? onPluginLoaded)
|
||||||
|
{
|
||||||
|
string screenDeviceName = screen.DeviceName;
|
||||||
|
Rectangle screenBounds = screen.Bounds;
|
||||||
|
|
||||||
|
IPluginWindow? existingWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id && w.ScreenDeviceName == screenDeviceName);
|
||||||
|
|
||||||
|
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, screenBounds, screenDeviceName)
|
||||||
|
{
|
||||||
|
Title = internalPluginData.Metadata.Id.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else if (internalPluginData.Type == PluginType.Web)
|
||||||
|
{
|
||||||
|
window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath, screenBounds, screenDeviceName)
|
||||||
|
{
|
||||||
|
Title = internalPluginData.Metadata.Id.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath, screenBounds, screenDeviceName)
|
||||||
|
{
|
||||||
|
Title = internalPluginData.Metadata.Id.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
Action? pluginLoadedHandler = null;
|
||||||
|
pluginLoadedHandler = () =>
|
||||||
|
{
|
||||||
|
onPluginLoaded?.Invoke(internalPluginData);
|
||||||
|
window.PluginLoaded -= pluginLoadedHandler;
|
||||||
|
};
|
||||||
|
|
||||||
|
Action exitHandler = () =>
|
||||||
|
{
|
||||||
|
// Close the widget on every screen using this layout
|
||||||
|
foreach (System.Windows.Forms.Screen sharedScreen in ScreenUtilities.GetAllScreens())
|
||||||
|
{
|
||||||
|
if (GetLayoutForScreen(sharedScreen) == layout)
|
||||||
|
{
|
||||||
|
IPluginWindow? sharedWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id && w.ScreenDeviceName == sharedScreen.DeviceName);
|
||||||
|
if (sharedWindow is not null)
|
||||||
|
{
|
||||||
|
PluginWindows.Remove(sharedWindow);
|
||||||
|
BlockWindowsClosing = false;
|
||||||
|
sharedWindow.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();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetEditMode(bool editMode)
|
||||||
|
{
|
||||||
|
_isEditMode = editMode;
|
||||||
|
foreach (IPluginWindow window in PluginWindows)
|
||||||
|
{
|
||||||
|
window.SetEditMode(editMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
Application.Current.MainWindow.Topmost = editMode;
|
||||||
|
|
||||||
|
EditModeChanged?.Invoke(editMode);
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Dictionary<(Layout, uint), SettingSynchronizer> settingSynchronizers = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets (or creates) the synchronizer that keeps the settings of all windows showing the
|
||||||
|
/// given plugin in the given layout in sync.
|
||||||
|
/// </summary>
|
||||||
|
internal SettingSynchronizer GetSettingSynchronizer(Layout layout, uint pluginId)
|
||||||
|
{
|
||||||
|
(Layout, uint) key = (layout, pluginId);
|
||||||
|
if (!settingSynchronizers.TryGetValue(key, out SettingSynchronizer? synchronizer))
|
||||||
|
{
|
||||||
|
synchronizer = new SettingSynchronizer();
|
||||||
|
settingSynchronizers.Add(key, synchronizer);
|
||||||
|
}
|
||||||
|
|
||||||
|
return synchronizer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Drops the synchronizer for the given plugin in the given layout once no windows use it anymore.
|
||||||
|
/// </summary>
|
||||||
|
internal void ReleaseSettingSynchronizer(Layout layout, uint pluginId)
|
||||||
|
{
|
||||||
|
_ = settingSynchronizers.Remove((layout, pluginId));
|
||||||
|
}
|
||||||
|
|
||||||
|
#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.Layouts.Add(new Layout(EmptyLayoutName));
|
||||||
|
Settings.Themes.Add(new Theme("Default"));
|
||||||
|
Settings.SchemaVersion = 1;
|
||||||
|
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"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.SchemaVersion < 1)
|
||||||
|
{
|
||||||
|
MigrateToScreenAwareSettings();
|
||||||
|
Settings.SchemaVersion = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Settings.Layouts.Any(layout => layout.Name == EmptyLayoutName))
|
||||||
|
{
|
||||||
|
Settings.Layouts.Add(new Layout(EmptyLayoutName));
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Migrates legacy settings to screen-aware layouts:
|
||||||
|
/// records the primary screen aspect ratio on each layout and converts
|
||||||
|
/// absolute pixel positions/sizes to percentages of the primary screen bounds.
|
||||||
|
/// </summary>
|
||||||
|
private void MigrateToScreenAwareSettings()
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo("Migrating settings to screen-aware layouts", source: "Manager");
|
||||||
|
|
||||||
|
System.Windows.Forms.Screen primaryScreen = ScreenUtilities.GetPrimaryScreen();
|
||||||
|
double aspectRatio = ScreenUtilities.GetAspectRatio(primaryScreen);
|
||||||
|
Rectangle bounds = primaryScreen.Bounds;
|
||||||
|
|
||||||
|
foreach (Layout layout in Settings.Layouts)
|
||||||
|
{
|
||||||
|
if (layout.ScreenAspectRatio <= 0)
|
||||||
|
{
|
||||||
|
layout.ScreenAspectRatio = aspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (PluginSettings plugin in layout.Plugins.Values)
|
||||||
|
{
|
||||||
|
if (plugin.Position.X > 1 || plugin.Position.Y > 1 || plugin.Position.X < 0 || plugin.Position.Y < 0)
|
||||||
|
{
|
||||||
|
plugin.Position = ScreenUtilities.PositionToPercent(new System.Windows.Point(plugin.Position.X, plugin.Position.Y), bounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (plugin.Size.X > 1 || plugin.Size.Y > 1)
|
||||||
|
{
|
||||||
|
plugin.Size = ScreenUtilities.SizeToPercent(new System.Windows.Point(plugin.Size.X, plugin.Size.Y), bounds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Settings.ScreenLayouts.Count == 0)
|
||||||
|
{
|
||||||
|
Settings.ScreenLayouts[ScreenUtilities.GetMonitorHardwareId(primaryScreen)] = Settings.CurrentLayoutName ?? "Default";
|
||||||
|
}
|
||||||
|
|
||||||
|
App.Logger.LogInfo("Settings migrated to screen-aware layouts", source: "Manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Resolves the layout that should be applied to the given screen:
|
||||||
|
/// 1. the layout explicitly bound to this screen's device name,
|
||||||
|
/// 2. the layout with the closest matching aspect ratio,
|
||||||
|
/// 3. the first layout.
|
||||||
|
/// </summary>
|
||||||
|
public Layout GetLayoutForScreen(System.Windows.Forms.Screen screen)
|
||||||
|
{
|
||||||
|
if (Settings.ScreenLayouts.TryGetValue(ScreenUtilities.GetMonitorHardwareId(screen), out string? layoutName))
|
||||||
|
{
|
||||||
|
Layout? bound = Settings.Layouts.FirstOrDefault(layout => layout.Name == layoutName);
|
||||||
|
if (bound is not null)
|
||||||
|
{
|
||||||
|
return bound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double targetRatio = ScreenUtilities.GetAspectRatio(screen);
|
||||||
|
Layout? byAspectRatio = Settings.Layouts
|
||||||
|
.Where(layout => layout.ScreenAspectRatio > 0)
|
||||||
|
.OrderBy(layout => Math.Abs(layout.ScreenAspectRatio - targetRatio))
|
||||||
|
.FirstOrDefault();
|
||||||
|
|
||||||
|
if (byAspectRatio is not null)
|
||||||
|
{
|
||||||
|
return byAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Settings.Layouts.FirstOrDefault() ?? new Layout("ERROR");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds the given layout to the given screen on this machine.
|
||||||
|
/// </summary>
|
||||||
|
public void BindLayoutToScreen(System.Windows.Forms.Screen screen, Layout layout)
|
||||||
|
{
|
||||||
|
Settings.ScreenLayouts[ScreenUtilities.GetMonitorHardwareId(screen)] = layout.Name;
|
||||||
|
SaveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all plugin windows currently shown on the given screen.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<IPluginWindow> GetWindowsForScreen(string screenDeviceName)
|
||||||
|
{
|
||||||
|
return PluginWindows.Where(window => window.ScreenDeviceName == screenDeviceName).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads all screens' layouts at once, opening the enabled widgets of every screen.
|
||||||
|
/// </summary>
|
||||||
|
public void LoadLayout(Action? onComplete = null)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo("Loading layouts", source: "Manager");
|
||||||
|
BlockWindowsClosing = false;
|
||||||
|
|
||||||
|
foreach (IPluginWindow window in PluginWindows)
|
||||||
|
{
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockWindowsClosing = true;
|
||||||
|
PluginWindows.Clear();
|
||||||
|
|
||||||
|
foreach (System.Windows.Forms.Screen screen in ScreenUtilities.GetAllScreens())
|
||||||
|
{
|
||||||
|
Layout layout = GetLayoutForScreen(screen);
|
||||||
|
LoadScreen(screen, layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
onComplete?.Invoke();
|
||||||
|
App.Logger.LogInfo("Layouts loaded", source: "Manager");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reloads the widgets of a single screen using the layout currently bound to it.
|
||||||
|
/// </summary>
|
||||||
|
public void ReloadScreen(System.Windows.Forms.Screen screen)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo($"Reloading screen {screen.DeviceName}", source: "Manager");
|
||||||
|
|
||||||
|
List<IPluginWindow> windows = PluginWindows.Where(window => window.ScreenDeviceName == screen.DeviceName).ToList();
|
||||||
|
|
||||||
|
BlockWindowsClosing = false;
|
||||||
|
foreach (IPluginWindow window in windows)
|
||||||
|
{
|
||||||
|
window.Close();
|
||||||
|
}
|
||||||
|
|
||||||
|
BlockWindowsClosing = true;
|
||||||
|
PluginWindows.RemoveAll(window => windows.Contains(window));
|
||||||
|
|
||||||
|
Layout layout = GetLayoutForScreen(screen);
|
||||||
|
LoadScreen(screen, layout);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadScreen(System.Windows.Forms.Screen screen, Layout layout)
|
||||||
|
{
|
||||||
|
App.Logger.LogInfo($"Loading layout \"{layout.Name}\" for screen {screen.DeviceName}", source: "Manager");
|
||||||
|
|
||||||
|
// The empty layout intentionally shows no widgets and is never populated.
|
||||||
|
if (layout.Name == EmptyLayoutName)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load plugins
|
||||||
|
foreach (uint pluginId in _plugins.Keys)
|
||||||
|
{
|
||||||
|
InternalPluginData internalPluginData = _plugins[pluginId];
|
||||||
|
|
||||||
|
// Add plugin to layout if it doesn't exist
|
||||||
|
if (!layout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
|
||||||
|
{
|
||||||
|
layout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata, Owner = layout });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginSettings.Metadata = internalPluginData.Metadata;
|
||||||
|
pluginSettings.Owner = layout;
|
||||||
|
|
||||||
|
if (pluginSettings.Enabled)
|
||||||
|
{
|
||||||
|
EnsurePluginWindow(screen, layout, internalPluginData, pluginSettings, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove plugins that are not loaded anymore
|
||||||
|
List<uint> pluginIdsToRemove = layout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
|
||||||
|
foreach (uint pluginId in pluginIdsToRemove)
|
||||||
|
{
|
||||||
|
layout.Plugins.Remove(pluginId);
|
||||||
|
}
|
||||||
|
|
||||||
|
layout.UpdatePlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
#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
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<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 SelectedLayout.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.RowDefinitions>
|
||||||
|
<RowDefinition Height="auto" />
|
||||||
|
<RowDefinition Height="5" />
|
||||||
|
<RowDefinition Height="auto" />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid.ColumnDefinitions>
|
||||||
|
<ColumnDefinition Width="auto" />
|
||||||
|
<ColumnDefinition Width="5" />
|
||||||
|
<ColumnDefinition Width="1*" />
|
||||||
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
|
<ui:Button x:Name="screenSelectorButton" VerticalAlignment="Stretch" Padding="10,4" Click="ScreenSelectorButton_Click">
|
||||||
|
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
|
||||||
|
<TextBlock x:Name="screenSelectorButtonText" VerticalAlignment="Center" Text="{Binding SelectedScreen.DisplayName}" />
|
||||||
|
<ui:SymbolIcon Symbol="ChevronDown24" Margin="8,0,0,0" FontSize="12" />
|
||||||
|
</StackPanel>
|
||||||
|
</ui:Button>
|
||||||
|
|
||||||
|
<ComboBox x:Name="layoutsComboBox" Grid.Column="2" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding SelectedLayoutName}" SelectedValuePath="Name" VerticalAlignment="Center" SelectionChanged="LayoutsComboBox_SelectionChanged" />
|
||||||
|
|
||||||
|
<Grid Grid.Row="2" Grid.ColumnSpan="3">
|
||||||
|
<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="newLayoutButton" Content="{DynamicResource newLayout}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" 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>
|
||||||
|
</Grid>
|
||||||
|
</ui:Card>
|
||||||
|
</Grid>
|
||||||
|
</Page>
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
_dataContext.RefreshScreens();
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ScreenSelectorButton_Click(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
ScreenSelectorDialog dialog = new(_dataContext)
|
||||||
|
{
|
||||||
|
Owner = Window.GetWindow(this)
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = dialog.ShowDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
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, _manager.SelectedLayout, (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)
|
||||||
|
{
|
||||||
|
ApplySelectedLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Binds the currently selected layout to the currently selected screen and reloads it.
|
||||||
|
/// No-op when the layout is already the one bound to the screen (e.g. programmatic resets).
|
||||||
|
/// </summary>
|
||||||
|
private void ApplySelectedLayout()
|
||||||
|
{
|
||||||
|
if (_dataContext.SelectedScreenId is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Windows.Forms.Screen? screen = ScreenUtilities.GetScreenByDeviceName(_dataContext.SelectedScreenId);
|
||||||
|
if (screen is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string? layoutName = _dataContext.SelectedLayoutName;
|
||||||
|
if (layoutName is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == layoutName);
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_manager.GetLayoutForScreen(screen) == layout)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_manager.BindLayoutToScreen(screen, layout);
|
||||||
|
_manager.ReloadScreen(screen);
|
||||||
|
_dataContext.RefreshSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
_manager.Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
|
||||||
|
_manager.SaveSettings();
|
||||||
|
|
||||||
|
// Select the new layout so the user can apply it to the current screen
|
||||||
|
_dataContext.SelectedLayoutName = inputDialog.ResponseText.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
Layout? layout = _manager.Settings.Layouts.FirstOrDefault(l => l.Name == _dataContext.SelectedLayoutName);
|
||||||
|
if (layout is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layout.Name == Manager.EmptyLayoutName)
|
||||||
|
{
|
||||||
|
Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
|
||||||
|
{
|
||||||
|
Title = App.AppName,
|
||||||
|
Content = (string)FindResource("cannotDeleteEmptyLayout"),
|
||||||
|
CloseButtonText = "Ok"
|
||||||
|
};
|
||||||
|
_ = await cannotDeleteMessageBox.ShowDialogAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = _manager.Settings.Layouts.Remove(layout);
|
||||||
|
|
||||||
|
// Remove any screen bindings pointing to the deleted layout
|
||||||
|
List<string> boundScreens = _manager.Settings.ScreenLayouts
|
||||||
|
.Where(kvp => kvp.Value == layout.Name)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (string screenId in boundScreens)
|
||||||
|
{
|
||||||
|
_ = _manager.Settings.ScreenLayouts.Remove(screenId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_manager.SaveSettings();
|
||||||
|
_manager.LoadLayout();
|
||||||
|
_dataContext.RefreshSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
#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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 SelectedLayout.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>
|
||||||
@@ -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.SelectedLayout.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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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; }
|
||||||
|
string ScreenDeviceName { get; }
|
||||||
|
|
||||||
|
void Exit();
|
||||||
|
void SetEditMode(bool enabled);
|
||||||
|
void Show();
|
||||||
|
void Hide();
|
||||||
|
void Close();
|
||||||
|
|
||||||
|
void ApplySettingValue(string id, string value);
|
||||||
|
void ApplyButtonClick(string id);
|
||||||
|
|
||||||
|
event System.ComponentModel.CancelEventHandler Closing;
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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>
|
||||||
@@ -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>
|
||||||
@@ -52,7 +56,7 @@
|
|||||||
<Setter Property="Template">
|
<Setter Property="Template">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<ControlTemplate>
|
<ControlTemplate>
|
||||||
<TextBlock Text="No plugins found!" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
<TextBlock Text="No plugins found!" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||||
</ControlTemplate>
|
</ControlTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
</Setter>
|
</Setter>
|
||||||
@@ -64,38 +68,49 @@
|
|||||||
</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">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<local:ModEntry Margin="0 0 0 5"/>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
<ItemsControl.Style>
|
||||||
|
<Style TargetType="ItemsControl">
|
||||||
|
<Style.Triggers>
|
||||||
|
<Trigger Property="HasItems" Value="false">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<Setter.Value>
|
||||||
|
<ControlTemplate>
|
||||||
|
<TextBlock Text="No plugins found!" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Trigger>
|
||||||
|
</Style.Triggers>
|
||||||
|
</Style>
|
||||||
|
</ItemsControl.Style>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
<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" />
|
<ui:Card Grid.Row="3" Grid.ColumnSpan="3" Padding="5" Margin="0 5 0 0">
|
||||||
|
<Grid>
|
||||||
|
<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>
|
||||||
|
|
||||||
<ItemsControl Grid.Row="2" Grid.Column="1" ItemsSource="{Binding InstalledPlugins}">
|
<Image Source="{Binding ModIoIcon}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
|
||||||
<ItemsControl.ItemTemplate>
|
<ui:Button Content="{DynamicResource createNewPlugin}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="CreatePluginButton_Click"/>
|
||||||
<DataTemplate>
|
<ui:Button Content="{DynamicResource reloadPlugins}" Icon="{ui:SymbolIcon ArrowClockwise24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="ReloadPluginsButton_Click"/>
|
||||||
<Border BorderBrush="LightGray" BorderThickness="1">
|
<ui:Button Content="{Binding LoginButtonText}" Icon="{ui:SymbolIcon PersonKey20}" Grid.Column="6" HorizontalAlignment="Stretch" Click="LogInButton_Click"/>
|
||||||
<local:ModEntry />
|
</Grid>
|
||||||
</Border>
|
</ui:Card>
|
||||||
</DataTemplate>
|
|
||||||
</ItemsControl.ItemTemplate>
|
|
||||||
<ItemsControl.Style>
|
|
||||||
<Style TargetType="ItemsControl">
|
|
||||||
<Style.Triggers>
|
|
||||||
<Trigger Property="HasItems" Value="false">
|
|
||||||
<Setter Property="Template">
|
|
||||||
<Setter.Value>
|
|
||||||
<ControlTemplate>
|
|
||||||
<TextBlock Text="No plugins found!" FontSize="20" VerticalAlignment="Center" HorizontalAlignment="Center"/>
|
|
||||||
</ControlTemplate>
|
|
||||||
</Setter.Value>
|
|
||||||
</Setter>
|
|
||||||
</Trigger>
|
|
||||||
</Style.Triggers>
|
|
||||||
</Style>
|
|
||||||
</ItemsControl.Style>
|
|
||||||
</ItemsControl>
|
|
||||||
|
|
||||||
<Border Grid.Row="3" Grid.ColumnSpan="2" Padding="0 5 0 0" BorderThickness="0 2 0 0" BorderBrush="DarkGray">
|
|
||||||
<Image Source="{StaticResource ModioLogoBlueDark}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
|
|
||||||
</Border>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</busyIndicator:BusyMask>
|
</busyIndicator:BusyMask>
|
||||||
</Window>
|
</Page>
|
||||||
@@ -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,42 @@ 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 readonly DispatcherTimer searchTimer = new()
|
private readonly DispatcherTimer searchTimer = new()
|
||||||
{
|
{
|
||||||
Interval = TimeSpan.FromMilliseconds(300),
|
Interval = TimeSpan.FromMilliseconds(300),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private bool changed = false;
|
||||||
|
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 +74,183 @@ 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)
|
public async Task Remove(string pluginPath, uint id)
|
||||||
{
|
{
|
||||||
|
App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
|
||||||
pluginManagerDataContext.IsLoading = true;
|
pluginManagerDataContext.IsLoading = true;
|
||||||
|
changed = true;
|
||||||
|
|
||||||
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
|
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)
|
if (pluginEntryDataContext is not null)
|
||||||
{
|
{
|
||||||
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
|
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
|
||||||
|
App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Directory.Exists(pluginPath))
|
if (pluginManagerDataContext.IsAuthenticated)
|
||||||
{
|
{
|
||||||
Directory.Delete(pluginPath, true);
|
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 async void OnInitialized(EventArgs e)
|
[GeneratedRegex(@"[^a-zA-Z0-9]")]
|
||||||
{
|
private static partial Regex IdentifierNameRegex();
|
||||||
base.OnInitialized(e);
|
|
||||||
|
|
||||||
|
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 async void PluginManager_Loaded(object sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
await InitializePluginManager();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PluginManager_Unloaded(object sender, RoutedEventArgs 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");
|
||||||
}
|
}
|
||||||
|
|
||||||
pluginManagerDataContext.IsLoading = false;
|
await SyncPlugins();
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnClosing(CancelEventArgs e)
|
pluginManagerDataContext.IsLoading = false;
|
||||||
{
|
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
|
||||||
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 +258,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 +341,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 +371,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 +381,435 @@ 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 string GetCsprojPath(string pluginName)
|
||||||
|
{
|
||||||
|
string pluginSafeName = GetPluginSafeName(pluginName);
|
||||||
|
return Path.Combine(pluginDevelopmentPath, pluginSafeName, $"{pluginSafeName}.csproj");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
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;
|
||||||
|
|
||||||
public class PluginMetadata
|
public class PluginMetadata
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
public uint Id { get; set; }
|
public uint Id { get; set; }
|
||||||
|
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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" Focusable="True" MouseDown="Image_MouseDown" MouseMove="Image_MouseMove" MouseWheel="Image_MouseWheel" KeyDown="Image_KeyDown" KeyUp="Image_KeyUp" />
|
||||||
</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>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -9,7 +9,7 @@ public class SettingElement
|
|||||||
private string jsonValue = string.Empty;
|
private string jsonValue = string.Empty;
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
public Setting Input { get; set; }
|
public Setting? Input { get; set; }
|
||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public int OrderIndex { get; set; }
|
public int OrderIndex { get; set; }
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Plugins;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Keeps the per-instance plugin settings of every window sharing the same layout in sync.
|
||||||
|
/// Each window owns its own <see cref="DesktopMagic.Api.Settings.Setting"/> objects (plugins
|
||||||
|
/// hold them in readonly fields and subscribe to them in Start), so a value change on one
|
||||||
|
/// window has to be mirrored onto the sibling windows.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class SettingSynchronizer
|
||||||
|
{
|
||||||
|
private readonly object _lock = new();
|
||||||
|
private readonly List<IPluginWindow> windows = [];
|
||||||
|
private readonly HashSet<(IPluginWindow Window, string Id)> inFlight = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a window so it receives mirrored settings from the other windows of this layout.
|
||||||
|
/// </summary>
|
||||||
|
public void Register(IPluginWindow window)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!windows.Contains(window))
|
||||||
|
{
|
||||||
|
windows.Add(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unregisters a window and returns whether no windows remain.
|
||||||
|
/// </summary>
|
||||||
|
public bool Unregister(IPluginWindow window)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
windows.Remove(window);
|
||||||
|
return windows.Count == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirrors a setting value change onto every other registered window.
|
||||||
|
/// </summary>
|
||||||
|
public void SettingChanged(IPluginWindow origin, string id, string value)
|
||||||
|
{
|
||||||
|
Mirror(origin, id, window => window.ApplySettingValue(id, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mirrors a button click onto every other registered window.
|
||||||
|
/// </summary>
|
||||||
|
public void ButtonClicked(IPluginWindow origin, string id)
|
||||||
|
{
|
||||||
|
Mirror(origin, id, window => window.ApplyButtonClick(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Mirror(IPluginWindow origin, string id, Action<IPluginWindow> apply)
|
||||||
|
{
|
||||||
|
IPluginWindow[] snapshot;
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
snapshot = windows.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (IPluginWindow window in snapshot)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(window, origin) || !window.IsRunning)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (!inFlight.Add((window, id)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window is not DispatcherObject dispatcherObject)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
inFlight.Remove((window, id));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = dispatcherObject.Dispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
apply(window);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
inFlight.Remove((window, id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,818 @@
|
|||||||
|
using DesktopMagic.Api.Settings;
|
||||||
|
using DesktopMagic.Helpers;
|
||||||
|
using DesktopMagic.Plugins;
|
||||||
|
using DesktopMagic.Settings;
|
||||||
|
|
||||||
|
using Microsoft.Web.WebView2.Core;
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.ComponentModel;
|
||||||
|
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;
|
||||||
|
|
||||||
|
private readonly System.Drawing.Rectangle screenBounds;
|
||||||
|
private readonly string screenDeviceName;
|
||||||
|
private bool isUpdatingPosition = false;
|
||||||
|
|
||||||
|
// Event handlers on long-lived settings objects, tracked so they can be unsubscribed on close.
|
||||||
|
private readonly PropertyChangedEventHandler settingsPropertyChangedHandler;
|
||||||
|
private readonly PropertyChangedEventHandler themePropertyChangedHandler;
|
||||||
|
private Theme? subscribedTheme;
|
||||||
|
private readonly List<(Setting Setting, Action Handler)> subscribedSettings = [];
|
||||||
|
private readonly List<(Button Button, Action Handler)> subscribedButtonClicks = [];
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, Setting> localSettings = [];
|
||||||
|
private SettingSynchronizer? synchronizer;
|
||||||
|
private bool suppressButtonSync = false;
|
||||||
|
|
||||||
|
public bool IsRunning { get; private set; } = true;
|
||||||
|
public PluginMetadata PluginMetadata { get; private set; }
|
||||||
|
public string PluginFolderPath { get; private set; }
|
||||||
|
public string ScreenDeviceName => screenDeviceName;
|
||||||
|
|
||||||
|
public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath, System.Drawing.Rectangle screenBounds, string screenDeviceName)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
|
||||||
|
settingsPropertyChangedHandler = (_, s) =>
|
||||||
|
{
|
||||||
|
if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
|
||||||
|
{
|
||||||
|
SubscribeToTheme(settings.Theme);
|
||||||
|
ThemeChanged();
|
||||||
|
}
|
||||||
|
else if (s.PropertyName == nameof(PluginSettings.Position))
|
||||||
|
{
|
||||||
|
UpdatePosition();
|
||||||
|
}
|
||||||
|
else if (s.PropertyName == nameof(PluginSettings.Size))
|
||||||
|
{
|
||||||
|
UpdateSize();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
settings.PropertyChanged += settingsPropertyChangedHandler;
|
||||||
|
|
||||||
|
themePropertyChangedHandler = (_, _) => ThemeChanged();
|
||||||
|
SubscribeToTheme(settings.Theme);
|
||||||
|
|
||||||
|
PluginMetadata = pluginMetadata;
|
||||||
|
this.settings = settings;
|
||||||
|
this.screenBounds = screenBounds;
|
||||||
|
this.screenDeviceName = screenDeviceName;
|
||||||
|
|
||||||
|
Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||||
|
Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||||
|
Left = position.X;
|
||||||
|
Top = position.Y;
|
||||||
|
Width = size.X;
|
||||||
|
Height = size.Y;
|
||||||
|
|
||||||
|
PluginFolderPath = pluginFolderPath;
|
||||||
|
|
||||||
|
if (settings.Owner is not null)
|
||||||
|
{
|
||||||
|
synchronizer = Manager.Instance.GetSettingSynchronizer(settings.Owner, PluginMetadata.Id);
|
||||||
|
synchronizer.Register(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
|
||||||
|
{
|
||||||
|
InitializeHotReload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeToTheme(Theme theme)
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(subscribedTheme, theme))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subscribedTheme is not null)
|
||||||
|
{
|
||||||
|
subscribedTheme.PropertyChanged -= themePropertyChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscribedTheme = theme;
|
||||||
|
subscribedTheme.PropertyChanged += themePropertyChangedHandler;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplySettingValue(string id, string value)
|
||||||
|
{
|
||||||
|
if (localSettings.TryGetValue(id, out Setting? setting) && setting.GetJsonValue() != value)
|
||||||
|
{
|
||||||
|
setting.SetJsonValue(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ApplyButtonClick(string id)
|
||||||
|
{
|
||||||
|
if (!localSettings.TryGetValue(id, out Setting? setting) || setting is not Button button)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
suppressButtonSync = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
button.Click();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
suppressButtonSync = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
reloadDebounceTimer = null;
|
||||||
|
|
||||||
|
UnsubscribeEvents();
|
||||||
|
|
||||||
|
if (synchronizer is not null && settings.Owner is not null)
|
||||||
|
{
|
||||||
|
if (synchronizer.Unregister(this))
|
||||||
|
{
|
||||||
|
Manager.Instance.ReleaseSettingSynchronizer(settings.Owner, PluginMetadata.Id);
|
||||||
|
}
|
||||||
|
synchronizer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (isInitialized && webView.CoreWebView2 != null)
|
||||||
|
{
|
||||||
|
webView.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UnsubscribeEvents()
|
||||||
|
{
|
||||||
|
settings.PropertyChanged -= settingsPropertyChangedHandler;
|
||||||
|
|
||||||
|
if (subscribedTheme is not null)
|
||||||
|
{
|
||||||
|
subscribedTheme.PropertyChanged -= themePropertyChangedHandler;
|
||||||
|
subscribedTheme = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ((Setting setting, Action handler) in subscribedSettings)
|
||||||
|
{
|
||||||
|
setting.OnValueChanged -= handler;
|
||||||
|
}
|
||||||
|
subscribedSettings.Clear();
|
||||||
|
|
||||||
|
foreach ((Button button, Action handler) in subscribedButtonClicks)
|
||||||
|
{
|
||||||
|
button.OnClick -= handler;
|
||||||
|
}
|
||||||
|
subscribedButtonClicks.Clear();
|
||||||
|
|
||||||
|
localSettings.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdatePosition()
|
||||||
|
{
|
||||||
|
if (isUpdatingPosition)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point position = ScreenUtilities.PercentToPosition(settings.Position, screenBounds);
|
||||||
|
if (position == new Point(Left, Top))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdatingPosition = true;
|
||||||
|
Left = position.X;
|
||||||
|
Top = position.Y;
|
||||||
|
isUpdatingPosition = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSize()
|
||||||
|
{
|
||||||
|
if (isUpdatingPosition)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point size = ScreenUtilities.PercentSizeToSize(settings.Size, screenBounds);
|
||||||
|
if (size == new Point(Width, Height))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isUpdatingPosition = true;
|
||||||
|
Width = size.X;
|
||||||
|
Height = size.Y;
|
||||||
|
isUpdatingPosition = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_LocationChanged(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (isUpdatingPosition)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Point topLeft = new(Left, Top);
|
||||||
|
Point clamped = ScreenUtilities.ClampToScreenBounds(topLeft, new Size(ActualWidth, ActualHeight), screenBounds);
|
||||||
|
|
||||||
|
if (clamped != topLeft)
|
||||||
|
{
|
||||||
|
isUpdatingPosition = true;
|
||||||
|
Left = clamped.X;
|
||||||
|
Top = clamped.Y;
|
||||||
|
isUpdatingPosition = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
settings.Position = ScreenUtilities.PositionToPercent(new Point(Left, Top), screenBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (!isUpdatingPosition)
|
||||||
|
{
|
||||||
|
settings.Size = ScreenUtilities.SizeToPercent(new Point(Width, Height), screenBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
localSettings[id] = setting;
|
||||||
|
|
||||||
|
SettingElement settingElement = new(setting, id, name, orderIndex);
|
||||||
|
|
||||||
|
if (settings.Settings.Exists(e => e.Id == id))
|
||||||
|
{
|
||||||
|
SettingElement saved = settings.Settings.First(e => e.Id == id);
|
||||||
|
string savedValue = saved.JsonValue;
|
||||||
|
if (!string.IsNullOrEmpty(savedValue) || setting is not Label and not Button)
|
||||||
|
{
|
||||||
|
settingElement.JsonValue = savedValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string capturedId = id;
|
||||||
|
if (setting is Button button)
|
||||||
|
{
|
||||||
|
Action clickHandler = () =>
|
||||||
|
{
|
||||||
|
if (!suppressButtonSync)
|
||||||
|
{
|
||||||
|
synchronizer?.ButtonClicked(this, capturedId);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
button.OnClick += clickHandler;
|
||||||
|
subscribedButtonClicks.Add((button, clickHandler));
|
||||||
|
}
|
||||||
|
|
||||||
|
Action valueChangedHandler = () =>
|
||||||
|
{
|
||||||
|
synchronizer?.SettingChanged(this, capturedId, setting.GetJsonValue());
|
||||||
|
|
||||||
|
_ = 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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
setting.OnValueChanged += valueChangedHandler;
|
||||||
|
subscribedSettings.Add((setting, valueChangedHandler));
|
||||||
|
|
||||||
|
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,31 +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="benchmark">Performance Benchmark</system:String>
|
||||||
|
<system:String x:Key="skiaBenchmark">SkiaSharp Benchmark</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="cannotDeleteEmptyLayout">Das leere Layout kann nicht gelöscht werden!</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>
|
||||||
|
|||||||
@@ -3,31 +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="benchmark">Performance Benchmark</system:String>
|
||||||
|
<system:String x:Key="skiaBenchmark">SkiaSharp Benchmark</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="cannotDeleteEmptyLayout">Can't delete the empty 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>
|
||||||
|
|||||||
@@ -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,7 @@
|
|||||||
using System.Collections.ObjectModel;
|
using DesktopMagic.Plugins;
|
||||||
|
|
||||||
|
using System.Collections.Generic;
|
||||||
|
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 +9,39 @@ 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) =>
|
||||||
|
{
|
||||||
|
foreach (Layout layout in layouts)
|
||||||
|
{
|
||||||
|
layout.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 +51,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,6 +69,38 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maps screen device names (e.g. "\\.\DISPLAY1") to the layout applied on that screen.
|
||||||
|
/// The layouts themselves are portable and store their target aspect ratio.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> ScreenLayouts { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Version of the settings schema, used to migrate older settings files.
|
||||||
|
/// 0 = legacy (layouts not screen aware, pixel based positions).
|
||||||
|
/// </summary>
|
||||||
|
public int SchemaVersion { get; set; }
|
||||||
|
|
||||||
|
public string? ModIoAccessToken { get; set; }
|
||||||
|
|
||||||
|
public string? ReleaseInfoLastAppVersion { get; set; }
|
||||||
|
|
||||||
|
public bool IsFirstRun { get; set; } = true;
|
||||||
|
|
||||||
|
public DesktopMagicSettings()
|
||||||
|
{
|
||||||
|
themes.CollectionChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
foreach (Layout layout in layouts)
|
||||||
|
{
|
||||||
|
layout.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));
|
||||||
|
|||||||
@@ -1,27 +1,43 @@
|
|||||||
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 = [];
|
||||||
|
private double screenAspectRatio = 0;
|
||||||
|
|
||||||
|
[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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,9 +61,38 @@ internal class Layout(string name) : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Aspect ratio (Width / Height) of the screen this layout was designed for.
|
||||||
|
/// Used to match layouts to screens and to make layouts portable across screens.
|
||||||
|
/// </summary>
|
||||||
|
public double ScreenAspectRatio
|
||||||
|
{
|
||||||
|
get => screenAspectRatio;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (screenAspectRatio != value)
|
||||||
|
{
|
||||||
|
screenAspectRatio = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -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,44 @@ 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(0.05, 0.05);
|
||||||
private Point size = new Point(300, 300);
|
private Point size = new Point(0.3, 0.3);
|
||||||
|
|
||||||
[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();
|
||||||
|
|
||||||
|
// The layout this plugin belongs to, used to resolve the fallback theme
|
||||||
|
[JsonIgnore]
|
||||||
|
public Layout? Owner { get; set; }
|
||||||
|
|
||||||
|
[JsonIgnore]
|
||||||
|
public Theme Theme
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
DesktopMagicSettings settings = MainWindowDataContext.GetSettings();
|
||||||
|
return settings.Themes.FirstOrDefault(t => t.Name == currentThemeName) ?? Owner?.Theme ?? 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 +67,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;
|
||||||
@@ -52,6 +97,9 @@ public class PluginSettings : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Position of the plugin window as percentages (0..1) of the owning screen's bounds.
|
||||||
|
/// </summary>
|
||||||
public Point Position
|
public Point Position
|
||||||
{
|
{
|
||||||
get => position;
|
get => position;
|
||||||
@@ -65,6 +113,9 @@ public class PluginSettings : INotifyPropertyChanged
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Size of the plugin window as percentages (0..1) of the owning screen's bounds.
|
||||||
|
/// </summary>
|
||||||
public Point Size
|
public Point Size
|
||||||
{
|
{
|
||||||
get => size;
|
get => size;
|
||||||
@@ -78,6 +129,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>
|
||||||
|
|||||||
@@ -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
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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,56 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<ItemGroup>
|
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||||
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
<DocumentationFile>$([System.IO.Path]::Combine($(MSBuildProjectDirectory), "DesktopMagicPluginAPI.xml"))</DocumentationFile>
|
||||||
</ItemGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\..\LICENSE">
|
<None Include="..\..\LICENSE">
|
||||||
<Pack>True</Pack>
|
<Pack>True</Pack>
|
||||||
<PackagePath></PackagePath>
|
<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>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="SkiaSharp" Version="4.148.0" />
|
||||||
|
<PackageReference Include="System.Drawing.Common" Version="8.0.0" />
|
||||||
|
</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,14 +7,21 @@ namespace DesktopMagic.Api.Drawing;
|
|||||||
|
|
||||||
internal class FontComparer : IEqualityComparer<Font>
|
internal class FontComparer : IEqualityComparer<Font>
|
||||||
{
|
{
|
||||||
public bool Equals(Font font1, Font font2)
|
private const float Tolerance = 0.01f;
|
||||||
|
|
||||||
|
public bool Equals(Font? font1, Font? font2)
|
||||||
{
|
{
|
||||||
|
if (font1 is null || font2 is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (font1.Name != font2.Name)
|
if (font1.Name != font2.Name)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (font1.SizeInPoints != font2.SizeInPoints)
|
if (Math.Abs(font1.SizeInPoints - font2.SizeInPoints) > Tolerance)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides data for keyboard events.
|
||||||
|
/// </summary>
|
||||||
|
public class KeyEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The key that was pressed or released.
|
||||||
|
/// </summary>
|
||||||
|
public Keys Key { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the Alt modifier was pressed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Alt { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the Control modifier was pressed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Control { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the Shift modifier was pressed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Shift { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the Windows logo key was pressed.
|
||||||
|
/// </summary>
|
||||||
|
public bool Windows { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="KeyEventArgs"/> class.
|
||||||
|
/// </summary>
|
||||||
|
public KeyEventArgs(Keys key, bool alt, bool control, bool shift, bool windows)
|
||||||
|
{
|
||||||
|
Key = key;
|
||||||
|
Alt = alt;
|
||||||
|
Control = control;
|
||||||
|
Shift = shift;
|
||||||
|
Windows = windows;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#pragma warning disable CS1591
|
||||||
|
|
||||||
|
namespace DesktopMagic.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines keyboard keys available for plugin input handling.
|
||||||
|
/// Values match <c>System.Windows.Input.Key</c> for direct casting.
|
||||||
|
/// </summary>
|
||||||
|
public enum Keys
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Cancel = 1,
|
||||||
|
Back = 2,
|
||||||
|
Tab = 3,
|
||||||
|
LineFeed = 4,
|
||||||
|
Clear = 5,
|
||||||
|
Return = 6,
|
||||||
|
Enter = 6,
|
||||||
|
ShiftKey = 16,
|
||||||
|
ControlKey = 17,
|
||||||
|
AltKey = 18,
|
||||||
|
Pause = 19,
|
||||||
|
CapsLock = 20,
|
||||||
|
Escape = 27,
|
||||||
|
Space = 32,
|
||||||
|
PageUp = 33,
|
||||||
|
PageDown = 34,
|
||||||
|
End = 35,
|
||||||
|
Home = 36,
|
||||||
|
Left = 37,
|
||||||
|
Up = 38,
|
||||||
|
Right = 39,
|
||||||
|
Down = 40,
|
||||||
|
Select = 41,
|
||||||
|
Print = 42,
|
||||||
|
Execute = 43,
|
||||||
|
PrintScreen = 44,
|
||||||
|
Insert = 45,
|
||||||
|
Delete = 46,
|
||||||
|
Help = 47,
|
||||||
|
D0 = 48,
|
||||||
|
D1 = 49,
|
||||||
|
D2 = 50,
|
||||||
|
D3 = 51,
|
||||||
|
D4 = 52,
|
||||||
|
D5 = 53,
|
||||||
|
D6 = 54,
|
||||||
|
D7 = 55,
|
||||||
|
D8 = 56,
|
||||||
|
D9 = 57,
|
||||||
|
A = 65,
|
||||||
|
B = 66,
|
||||||
|
C = 67,
|
||||||
|
D = 68,
|
||||||
|
E = 69,
|
||||||
|
F = 70,
|
||||||
|
G = 71,
|
||||||
|
H = 72,
|
||||||
|
I = 73,
|
||||||
|
J = 74,
|
||||||
|
K = 75,
|
||||||
|
L = 76,
|
||||||
|
M = 77,
|
||||||
|
N = 78,
|
||||||
|
O = 79,
|
||||||
|
P = 80,
|
||||||
|
Q = 81,
|
||||||
|
R = 82,
|
||||||
|
S = 83,
|
||||||
|
T = 84,
|
||||||
|
U = 85,
|
||||||
|
V = 86,
|
||||||
|
W = 87,
|
||||||
|
X = 88,
|
||||||
|
Y = 89,
|
||||||
|
Z = 90,
|
||||||
|
LWin = 91,
|
||||||
|
RWin = 92,
|
||||||
|
Apps = 93,
|
||||||
|
Sleep = 95,
|
||||||
|
NumPad0 = 96,
|
||||||
|
NumPad1 = 97,
|
||||||
|
NumPad2 = 98,
|
||||||
|
NumPad3 = 99,
|
||||||
|
NumPad4 = 100,
|
||||||
|
NumPad5 = 101,
|
||||||
|
NumPad6 = 102,
|
||||||
|
NumPad7 = 103,
|
||||||
|
NumPad8 = 104,
|
||||||
|
NumPad9 = 105,
|
||||||
|
Multiply = 106,
|
||||||
|
Add = 107,
|
||||||
|
Separator = 108,
|
||||||
|
Subtract = 109,
|
||||||
|
Decimal = 110,
|
||||||
|
Divide = 111,
|
||||||
|
F1 = 112,
|
||||||
|
F2 = 113,
|
||||||
|
F3 = 114,
|
||||||
|
F4 = 115,
|
||||||
|
F5 = 116,
|
||||||
|
F6 = 117,
|
||||||
|
F7 = 118,
|
||||||
|
F8 = 119,
|
||||||
|
F9 = 120,
|
||||||
|
F10 = 121,
|
||||||
|
F11 = 122,
|
||||||
|
F12 = 123,
|
||||||
|
F13 = 124,
|
||||||
|
F14 = 125,
|
||||||
|
F15 = 126,
|
||||||
|
F16 = 127,
|
||||||
|
F17 = 128,
|
||||||
|
F18 = 129,
|
||||||
|
F19 = 130,
|
||||||
|
F20 = 131,
|
||||||
|
F21 = 132,
|
||||||
|
F22 = 133,
|
||||||
|
F23 = 134,
|
||||||
|
F24 = 135,
|
||||||
|
NumLock = 144,
|
||||||
|
Scroll = 145,
|
||||||
|
LeftShift = 160,
|
||||||
|
RightShift = 161,
|
||||||
|
LeftCtrl = 162,
|
||||||
|
RightCtrl = 163,
|
||||||
|
LeftAlt = 164,
|
||||||
|
RightAlt = 165,
|
||||||
|
BrowserBack = 166,
|
||||||
|
BrowserForward = 167,
|
||||||
|
BrowserRefresh = 168,
|
||||||
|
BrowserStop = 169,
|
||||||
|
BrowserSearch = 170,
|
||||||
|
BrowserFavorites = 171,
|
||||||
|
BrowserHome = 172,
|
||||||
|
VolumeMute = 173,
|
||||||
|
VolumeDown = 174,
|
||||||
|
VolumeUp = 175,
|
||||||
|
MediaNextTrack = 176,
|
||||||
|
MediaPreviousTrack = 177,
|
||||||
|
MediaStop = 178,
|
||||||
|
MediaPlayPause = 179,
|
||||||
|
LaunchMail = 180,
|
||||||
|
SelectMedia = 181,
|
||||||
|
LaunchApplication1 = 182,
|
||||||
|
LaunchApplication2 = 183,
|
||||||
|
OemSemicolon = 186,
|
||||||
|
OemPlus = 187,
|
||||||
|
OemComma = 188,
|
||||||
|
OemMinus = 189,
|
||||||
|
OemPeriod = 190,
|
||||||
|
OemQuestion = 191,
|
||||||
|
OemTilde = 192,
|
||||||
|
OemOpenBrackets = 219,
|
||||||
|
OemPipe = 220,
|
||||||
|
OemCloseBrackets = 221,
|
||||||
|
OemQuotes = 222,
|
||||||
|
Oem8 = 223,
|
||||||
|
OemBackslash = 226,
|
||||||
|
}
|
||||||
@@ -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.
|
|
||||||
@@ -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
-1
@@ -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
|
||||||
|
{
|
||||||
|
}
|
||||||
@@ -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,34 @@ public abstract class Plugin
|
|||||||
public virtual void OnMouseWheel(Point position, int delta)
|
public virtual void OnMouseWheel(Point position, int delta)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when a key is pressed while the plugin window has keyboard focus.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="e">The key event data.</param>
|
||||||
|
public virtual void OnKeyDown(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Occurs when a key is released while the plugin window has keyboard focus.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="e">The key event data.</param>
|
||||||
|
public virtual void OnKeyUp(KeyEventArgs e)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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()
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,9 +10,9 @@ public class Button : Setting
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the button gets clicked.
|
/// Occurs when the button gets clicked.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action OnClick;
|
public event Action? OnClick;
|
||||||
|
|
||||||
private string _value;
|
private string _value = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the text caption displayed in the <see cref="Button"/> element.
|
/// Gets or sets the text caption displayed in the <see cref="Button"/> element.
|
||||||
@@ -46,4 +46,14 @@ public class Button : Setting
|
|||||||
{
|
{
|
||||||
OnClick?.Invoke();
|
OnClick?.Invoke();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal override string GetJsonValue()
|
||||||
|
{
|
||||||
|
return Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal override void SetJsonValue(string value)
|
||||||
|
{
|
||||||
|
Value = value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class Label : Setting
|
public class Label : Setting
|
||||||
{
|
{
|
||||||
private string _value;
|
private string _value = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the text associated with this <see cref="Label"/>.
|
/// Gets or sets the text associated with this <see cref="Label"/>.
|
||||||
@@ -38,4 +38,14 @@ public class Label : Setting
|
|||||||
Value = value;
|
Value = value;
|
||||||
Bold = bold;
|
Bold = bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TextBox : Setting
|
public class TextBox : Setting
|
||||||
{
|
{
|
||||||
private string _value;
|
private string _value = string.Empty;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the text associated with this <see cref="TextBox"/>.
|
/// Gets or sets the text associated with this <see cref="TextBox"/>.
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
using System.Drawing;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an abstract base class for creating asynchronous plugins using SkiaSharp rendering.
|
||||||
|
/// Plugins override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead of <see cref="Plugin.Main()"/>.
|
||||||
|
/// All existing <see cref="Plugin"/>, <see cref="AsyncPlugin"/>, and <see cref="SkiaPlugin"/> code is unaffected.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SkiaAsyncPlugin : AsyncPlugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This method is sealed and returns null for async Skia plugins.
|
||||||
|
/// Override <see cref="MainAsync(SKCanvas, CancellationToken)"/> instead.
|
||||||
|
/// </summary>
|
||||||
|
public sealed override Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.FromResult<Bitmap?>(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin asynchronously.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
|
||||||
|
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
|
||||||
|
public abstract Task MainAsync(SKCanvas canvas, CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
using System.Drawing;
|
||||||
|
|
||||||
|
namespace DesktopMagic.Api;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides an abstract base class for creating plugins using SkiaSharp rendering.
|
||||||
|
/// Plugins override <see cref="Main(SKCanvas)"/> instead of <see cref="Plugin.Main()"/>.
|
||||||
|
/// All existing <see cref="Plugin"/> and <see cref="AsyncPlugin"/> code is unaffected.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class SkiaPlugin : Plugin
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// This method is sealed and returns null for Skia plugins.
|
||||||
|
/// Override <see cref="Main(SKCanvas)"/> instead.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>Always null.</returns>
|
||||||
|
public sealed override Bitmap? Main()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called every <see cref="Plugin.UpdateInterval"/> milliseconds to render the plugin.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="canvas">The SkiaSharp canvas to draw on.</param>
|
||||||
|
public abstract void Main(SKCanvas canvas);
|
||||||
|
}
|
||||||
@@ -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 |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user