94 Commits
Author SHA1 Message Date
Stone_Red 930222f364 Merge pull request #21 from Stone-Red-Code/develop
Version 1.3.3.0
2026-06-27 20:02:14 +02:00
Stone_Red f4ac2c65f3 Update version and installer 2026-06-27 20:00:14 +02:00
Stone_Red 5fcbe2af79 Add plugins search box to main page 2026-06-27 19:52:02 +02:00
Stone_Red 33962cd68b Move SetIsLocked call from WindowPos to PluginWindow initialization 2026-06-27 19:17:49 +02:00
Stone_Red c6b973b111 Rename local variables for consistency and clarity 2026-06-27 19:11:44 +02:00
Stone_Red d43f1ed949 Add color conversion methods with defaults and refactor ColorPicker parsing for web plugins 2026-06-27 19:08:36 +02:00
Stone_Red 4999ae6e88 Add hot reload support for web plugins with file watcher and debounce 2026-06-27 19:07:53 +02:00
Stone_Red 6061efba02 Add web plugin creation flow and settings bridge to WebPluginWindow 2026-06-27 15:53:27 +02:00
Stone_Red 8e333df71b Add window layer setting with Always on Bottom and Always on Top options 2026-06-27 14:32:56 +02:00
Stone_Red 9b3b7aa4b6 Merge pull request #17 from Stone-Red-Code/develop
Version 1.3.2.0
2026-04-21 16:50:20 +02:00
Stone_Red 2c82c9373f Update version in manifest and update installer 2026-04-21 16:43:46 +02:00
Stone_Red 51b4f15cf1 Set TextWrapping to NoWrap and enforce MaxWidth of 200 on controls 2026-04-21 16:41:33 +02:00
Stone_Red 2916f0983a Add meeting countdown and agenda plugins 2026-04-21 16:07:47 +02:00
Stone_Red 3683e75a17 Add update dialog showing latest release info after app update 2026-04-21 01:49:47 +02:00
Stone_Red 3d98c8b196 Set MainWindow.Topmost based on editMode in SetEditMode method 2026-04-21 01:26:30 +02:00
Stone_Red 7604acf115 Merge pull request #16 from Stone-Red-Code/develop
Version 1.3.1.0
2026-02-07 00:09:58 +01:00
Stone_Red 8c97f6b56a Update version and installer 2026-02-07 00:09:04 +01:00
Stone_Red 01a1d109ff Add weekend color override settings to DatePlugin 2026-02-07 00:00:44 +01:00
Stone_Red d2f123e0f5 Make sure underscore is properly escaped 2026-02-06 23:45:34 +01:00
Stone_Red f217574022 Add FPS limit setting and throttle Main() calls in MusicVisualizerPlugin 2026-01-23 21:53:54 +01:00
Stone_Red 1e9c74af52 Optimize bitmap rendering in PluginWindow 2026-01-23 21:13:40 +01:00
Stone_Red 2853d49dd9 Add rotation setting to plugins 2026-01-23 20:35:15 +01:00
Stone_Red 4133f61a5c Add customizable date format options 2026-01-23 00:33:46 +01:00
Stone_Red e3056a398a Update default and post-load info messages for GIF plugin 2026-01-22 17:54:23 +01:00
Stone_Red cd0bcad181 Add submenu options for bug reporting and feature requests via GitHub or email 2026-01-22 17:53:20 +01:00
Stone_Red fec26069be Make sure date plugin updates when settings change 2026-01-22 15:29:35 +01:00
Stone_Red f84dd20eb9 Update NuGet packages 2026-01-22 15:29:35 +01:00
Stone_Red 9581cc185c Replace all Buttons with icons with ui:Button 2026-01-22 15:29:35 +01:00
Stone_Red a438ab5475 Add Documentation navigation item with click handler linking to GitHub 2026-01-22 15:29:35 +01:00
Stone_Red 1b496db094 Replace TextBox with FileSelector for gif-path plugin setting 2026-01-22 15:29:35 +01:00
Stone_Red 3dce9ab59a Add CreatePluginDialog for plugin creation and update search boxes with icons 2026-01-22 15:29:35 +01:00
Stone_Red a85e57ad58 Fix titles of dialogs 2026-01-22 15:29:34 +01:00
Stone_Red 1cafc01cd7 Merge pull request #13 from Stone-Red-Code/develop
Update README
2026-01-22 00:10:14 +01:00
Stone_Red d319b32263 Update third party licenses in README.md 2026-01-22 00:09:26 +01:00
Stone_Red ca83e5585f Replace old screenshots with new ones
Updated preview images in README with new screenshots.
2026-01-22 00:08:01 +01:00
Stone_Red 68ca15a81f Merge pull request #12 from Stone-Red-Code/develop
Version 1.3.0.0
2026-01-21 23:35:55 +01:00
Stone_Red 0b278ed31a Update installer and manifest to version 1.3.0.0 2026-01-21 23:35:36 +01:00
Stone_Red 346a7ad5a1 Remove unused methods from MainWindow.xaml.cs 2026-01-21 21:32:40 +01:00
Stone_Red ea07f0db3a Show loading indicator during plugin creation 2026-01-21 21:26:05 +01:00
Stone_Red 667c913e57 Make plugin creation and setup steps async 2026-01-21 21:25:44 +01:00
Stone_Red 4d0119e4b3 Cleanup plugin template 2026-01-21 21:22:40 +01:00
Stone_Red f957edd886 Add hot reload support for plugins using FileSystemWatcher 2026-01-21 21:20:27 +01:00
Stone_Red 1a6cd5a744 Add reload plugins button and open plugins in IDE support in plugin manager 2026-01-21 19:42:01 +01:00
Stone_Red deddea2888 Remove Card container for plugin logos 2026-01-20 21:11:33 +01:00
Stone_Red e18ab1ec56 Make UI structure more flat 2026-01-20 20:52:39 +01:00
Stone_Red a8236bd8da Make sure default layout gets loaded when deleting current layout 2026-01-20 19:22:09 +01:00
Stone_Red cb8df79a5f Reset CurrentLayoutName if it does not match any existing layout names 2026-01-20 15:51:48 +01:00
Stone_Red 907d743c0d Make sure newly created layout gets loaded 2026-01-20 15:51:32 +01:00
Stone_Red 835ef23719 Fix margin in MainPage 2026-01-19 22:47:44 +01:00
Stone_Red d2da426baf Update project version to 1.3.0.0 2026-01-19 22:35:06 +01:00
Stone_Red 82fe362559 Add theme-aware Modio icon and resource for PluginManager page 2026-01-17 00:39:53 +01:00
Stone_Red 439eca0ef0 Remove unused NotifyIcon field and its disposal on window close 2026-01-17 00:22:54 +01:00
Stone_Red fca25aeb59 Show error dialogs on UI thread using Dispatcher.InvokeAsync 2026-01-17 00:16:53 +01:00
Stone_Red dd9953cb7d Replace Rectangle fill with Border background for color selection UI 2026-01-17 00:16:34 +01:00
Stone_Red 990793fb01 Add ColorPicker and FileSelector settings controls and API classes 2026-01-17 00:15:57 +01:00
Stone_Red c4517fb8ac Display error message box on UI thread 2026-01-13 23:23:39 +01:00
Stone_Red 92ecd2e93d Add cancellation support to AsyncPlugin methods 2026-01-13 22:42:15 +01:00
Stone_Red 056c42d4df Update API version to 1.0.0.0 and add README and package icon to API project 2026-01-01 22:53:29 +01:00
Stone_Red c64b70eb92 Update installer script and installer 2026-01-01 22:53:07 +01:00
Stone_Red 0804738e45 Update message box style to new style 2025-12-29 00:35:23 +01:00
Stone_Red 727324ca9a Overhaul theme creation and editing UI 2025-12-28 23:20:50 +01:00
Stone_Red 389256a380 Improve performance of plugin window bitmap rendering 2025-12-28 01:47:38 +01:00
Stone_Red ec7917625c UI overhaul 2025-12-28 01:46:16 +01:00
Stone_Red 6aa107233c Add reference to WebView2 feedback issue in workaround comment 2025-12-25 20:03:26 +01:00
Stone_Red 3edb2cff84 Fix WebView2 interaction on transparent window with opaque rectangle 2025-12-25 19:57:39 +01:00
Stone_Red 0bb5b10dba Update AsyncPlugin XML docs for clarity and integration details 2025-12-25 18:20:53 +01:00
Stone_Red 2b363bcd18 Update WeatherPlugin default city 2025-12-25 18:16:27 +01:00
Stone_Red 03da9189a5 Move MouseButton to parent directory and update namespace 2025-12-25 18:16:07 +01:00
Stone_Red 0a57b8a3d6 Fix logging and dialogs in PluginData 2025-12-25 18:14:57 +01:00
Stone_Red 16c5c98f1a Add plugin state persistence via PersistStateAttribute serialization 2025-12-25 18:14:01 +01:00
Stone_Red 9e9c49214a Add support for async plugins 2025-12-25 01:23:03 +01:00
Stone_Red 43c77e6a36 Add plugin methods for changed themes, settings showing message boxes and logging 2025-12-25 01:22:39 +01:00
Stone_Red 4778f5673f Pass theme properties to web plugin 2025-12-24 23:59:08 +01:00
Stone_Red e3d6eb4c8c Add ThemeChanged event to IPluginData 2025-12-24 23:58:53 +01:00
Stone_Red c90be28f38 Add basic support for web based plugins 2025-12-24 17:49:04 +01:00
Stone_Red a564b4db5e Add better error handling and logs to plugin manager 2025-12-20 16:50:33 +01:00
Stone_Red 2cece7dbdc Update WeatherPlugin city name setting label text 2025-12-20 16:35:44 +01:00
Stone_Red ba5bfc7df3 Improve plugin metadata 2025-12-20 16:34:42 +01:00
Stone_Red d316108868 Fix Cpu Monitor plugin percentage alignment 2025-12-20 16:20:05 +01:00
Stone_Red 9a96c60af0 Add confirmation when deleting themes/layouts 2025-12-20 16:17:45 +01:00
Stone_Red a8fe2c80f1 Update plugins when opening plugin manager 2025-12-20 16:08:45 +01:00
Stone_Red ac7826898a Check "Does Not Support Unloading" tag to not use AssemblyLoadContext 2025-12-20 16:08:30 +01:00
Stone_Red 780b506103 Add collectible AssemblyLoadContext to support plugin unloading #5 2025-12-20 00:23:48 +01:00
Stone_Red 10733383d3 Add basic weather plugin 2025-12-20 00:20:31 +01:00
Stone_Red 09ce072ec6 Add try catch when stopping plugin 2025-12-09 17:55:05 +01:00
Stone_Red fcacf36187 Merge pull request #11 from Stone-Red-Code/develop
Update installer
2025-12-09 00:13:59 +01:00
Stone_Red 980ef6473e Update installer 2025-12-09 00:09:27 +01:00
Stone_Red 7755ece3cd Merge pull request #10 from Stone-Red-Code/develop
Develop
2025-12-09 00:05:39 +01:00
Stone_Red b380e35189 Update version 2025-12-09 00:05:27 +01:00
Stone_Red 3fcda8e64e Add per plugin theme override 2025-12-08 20:37:01 +01:00
Stone_Red 0bc7033196 Fix some issues with the new theme support 2025-12-08 20:11:12 +01:00
Stone_Red 10fe4c773b Add better theme support 2025-11-04 19:00:32 +01:00
Stone_Red 13ed2f6a22 Merge pull request #9 from Stone-Red-Code/develop
Update auto start step in README.md
2025-07-07 11:41:15 +02:00
Stone_Red b7139a8821 Update auto start step in README.md 2025-07-07 11:41:01 +02:00
71 changed files with 5713 additions and 1288 deletions
+4 -4
View File
@@ -12,8 +12,8 @@
1. Download & install the latest release
- Microsoft Store: [Store Page](https://www.microsoft.com/store/productId/9NPV8F9K1CWM?ocid=pdpshare)
- GitHub: [Releases](https://github.com/Stone-Red-Code/DesktopMagic/releases)
1. (Optional) Put a shortcut to the `DesktopMagic.exe` file in the installation directory into the `shell:startup` directory.
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.
@@ -23,12 +23,12 @@
## Preview
![Screenshot 2023-12-31 164947](https://github.com/Stone-Red-Code/DesktopMagic/assets/56473591/191ac76f-264d-4e9f-8979-7a13b2a1cd12)
![Screenshot 2023-12-31 164853](https://github.com/Stone-Red-Code/DesktopMagic/assets/56473591/22de55d2-dde3-43a5-b77b-c773a7697e8c)
![Screenshot 2026-01-22 000348](https://github.com/user-attachments/assets/4617330d-4c4b-4509-80d7-9f6db413e84f)
![Screenshot 2026-01-22 000434](https://github.com/user-attachments/assets/aebeca87-b305-40a4-a942-d78fce0e9e97)
## Third party licenses
- [BusyIndicator](https://github.com/moh3ngolshani/BusyIndicator) - [MIT](https://github.com/Moh3nGolshani/BusyIndicator/blob/master/LICENSE)
- [Material Design In XAML Toolkit](https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit) - [MIT](https://github.com/MaterialDesignInXAML/MaterialDesignInXamlToolkit/blob/master/LICENSE)
- [WPF UI](https://github.com/lepoco/wpfui) - [MIT](https://github.com/lepoco/wpfui/blob/main/LICENSE.md)
- [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)
- [NAudio](https://github.com/naudio/NAudio) - [MIT](https://github.com/naudio/NAudio/blob/master/license.txt)
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
; Script generated by the Inno Setup Script Wizard.
; 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 MyAppVersion GetStringFileInfo("/publish/DesktopMagic.exe","ProductVersion")
+16 -8
View File
@@ -1,20 +1,28 @@
<Application x:Class="DesktopMagic.App"
<Application x:Class="DesktopMagic.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns: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"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources\Strings\StringResources.en.xaml" />
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="Grey" SecondaryColor="LightBlue" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" />
<ui:ThemesDictionary Theme="Light" />
<ui:ControlsDictionary />
<md:ThemesDictionary Theme="Light" />
<md:ControlsDictionary />
<ResourceDictionary Source="pack://application:,,,/BusyIndicator;component/Theme/Default.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteBrushes.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteStyles.xaml" />
<ResourceDictionary Source="Resources\Images\ImageResources.xaml" />
<ResourceDictionary Source="Resources\Styles\ToggleSwitchContentLeftStyle.xaml" />
</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>
</Application.Resources>
</Application>
</Application>
+12 -2
View File
@@ -5,6 +5,8 @@ using System.IO;
using System.Threading;
using System.Windows;
using Wpf.Ui;
namespace DesktopMagic;
/// <summary>
@@ -26,6 +28,8 @@ public partial class App : Application
public static string PluginsPath => Path.Combine(ApplicationDataPath, "Plugins");
public static IContentDialogService DialogService { get; } = new ContentDialogService();
public static Logger Logger { get; } = new Logger()
{
Config = new()
@@ -130,7 +134,7 @@ public partial class App : Application
Logger.LogFatal(exception + (e.IsTerminating ? "\t Process terminating!" : ""), source: exception.Source ?? "Unknown");
}
private static void Setup(bool clearLogFile)
private static async void Setup(bool clearLogFile)
{
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
@@ -156,7 +160,13 @@ public partial class App : Application
}
catch (Exception ex)
{
_ = MessageBox.Show(ex.ToString(), AppName, MessageBoxButton.OK, MessageBoxImage.Error);
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);
}
@@ -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;
}
}
@@ -56,7 +56,7 @@ internal class CpuMonitorPlugin : Plugin
}
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;
+127 -14
View File
@@ -4,35 +4,114 @@ 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 class DatePlugin : Plugin
internal partial class DatePlugin : Plugin
{
[Setting("short-date", "Short date")]
private readonly CheckBox shortDateCheckBox = new CheckBox(true);
[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 Color oldColor = Color.White;
private string oldFont = string.Empty;
private bool oldShortDateCheckBoxValue;
private bool themeChanged = false;
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)
if (oldDateTime.Date == DateTime.Now.Date && !themeChanged)
{
return null;
}
oldDateTime = DateTime.Now;
oldColor = Application.Theme.PrimaryColor;
oldFont = Application.Theme.Font;
oldShortDateCheckBoxValue = shortDateCheckBox.Value;
themeChanged = false;
string date = shortDateCheckBox.Value ? DateTime.Now.ToShortDateString() : DateTime.Now.ToLongDateString();
CultureInfo culture = CultureInfo.CurrentCulture;
DateTime dateTime = DateTime.Now;
Font font = new Font(Application.Theme.Font, 200);
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);
@@ -45,10 +124,44 @@ internal class DatePlugin : Plugin
bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp);
using SolidBrush brush = new SolidBrush(color);
gr.TextRenderingHint = TextRenderingHint.AntiAlias;
gr.DrawString(date, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
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 }
}
@@ -20,6 +20,11 @@ internal class MusicVisualizerPlugin : Plugin
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")]
private readonly CheckBox mirrorMode = new CheckBox(false);
@@ -51,8 +56,16 @@ internal class MusicVisualizerPlugin : Plugin
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;
}
@@ -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}";
}
}
+17 -9
View File
@@ -13,26 +13,23 @@ internal class TimePlugin : Plugin
private readonly CheckBox displaySecondsCheckBox = new CheckBox(true);
private string oldTime = string.Empty;
private Color oldColor = Color.White;
private string oldFont = string.Empty;
private bool oldDisplaySecondsCheckBoxValue;
private bool themeChanged;
public override int UpdateInterval => 1000;
public override Bitmap? Main()
{
string time = displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString();
if (oldTime == time && oldColor == Application.Theme.PrimaryColor && oldFont == Application.Theme.Font && oldDisplaySecondsCheckBoxValue == displaySecondsCheckBox.Value)
if (oldTime == time && !themeChanged)
{
return null;
}
oldTime = time;
oldColor = Application.Theme.PrimaryColor;
oldFont = Application.Theme.Font;
oldDisplaySecondsCheckBoxValue = displaySecondsCheckBox.Value;
themeChanged = false;
Font font = new Font(Application.Theme.Font, 200);
using Font font = new Font(Application.Theme.Font, 200);
Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100);
@@ -45,10 +42,21 @@ internal class TimePlugin : Plugin
bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp);
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
gr.TextRenderingHint = TextRenderingHint.AntiAlias;
gr.DrawString(time, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
gr.DrawString(time, font, brush, 0, 0);
return bmp;
}
public override void OnThemeChanged()
{
themeChanged = true;
}
public override void OnSettingsChanged()
{
themeChanged = true;
}
}
@@ -0,0 +1,208 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.BuiltInPlugins;
public class WeatherPlugin : AsyncPlugin
{
[Setting("city-name", "City Name")]
private readonly TextBox cityInput = new TextBox("Tokyo");
[Setting("search-btn", "Update Location")]
private readonly Button searchButton = new Button("Update Location");
[Setting("show-city", "Show City Name")]
private readonly CheckBox showCity = new CheckBox(true);
[Setting("show-temp", "Show Temperature")]
private readonly CheckBox showTemp = new CheckBox(true);
[Setting("show-status", "Show Weather Status")]
private readonly CheckBox showStatus = new CheckBox(true);
[Setting("show-time", "Show Last Updated")]
private readonly CheckBox showTime = new CheckBox(false);
[Setting("font-size", "Base Font Size")]
private readonly Slider fontSizeSlider = new Slider(10, 100, 30);
public override int UpdateInterval { get; set; } = 900000; // 15 Minutes
private readonly HttpClient httpClient = new HttpClient();
private string? cachedLat;
private string? cachedLon;
private string currentTemp = "--";
private string weatherStatus = "Enter city and search";
private string foundCityName = "No City Selected";
private string lastUpdated = "";
private bool isLoading = false;
public override async Task StartAsync(CancellationToken cancellationToken)
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (DesktopMagic)");
// Setup Event Handlers for instant UI updates when settings change
showCity.OnValueChanged += Application.UpdateWindow;
showTemp.OnValueChanged += Application.UpdateWindow;
showStatus.OnValueChanged += Application.UpdateWindow;
showTime.OnValueChanged += Application.UpdateWindow;
fontSizeSlider.OnValueChanged += Application.UpdateWindow;
await UpdateLocationAndWeather();
Application.UpdateWindow();
searchButton.OnClick += () =>
{
isLoading = true;
Application.UpdateWindow();
_ = Task.Run(async () =>
{
await UpdateLocationAndWeather();
isLoading = false;
Application.UpdateWindow();
});
};
}
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
{
// Periodic background update
if (!isLoading && cachedLat != null && cachedLon != null)
{
await UpdateWeatherOnly(cachedLat, cachedLon);
}
Bitmap bmp = new Bitmap(1200, 800);
bmp.SetResolution(300, 300);
using (Graphics g = Graphics.FromImage(bmp))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
g.Clear(Color.Transparent);
int lineCount = 0;
int baseSize = (int)fontSizeSlider.Value;
if (isLoading)
{
DrawLine(bmp, g, "Fetching...", ref lineCount, baseSize, FontStyle.Italic);
}
else
{
if (showCity.Value)
{
DrawLine(bmp, g, foundCityName.ToUpper(), ref lineCount, baseSize, FontStyle.Bold);
}
if (showTemp.Value)
{
DrawLine(bmp, g, currentTemp, ref lineCount, baseSize, FontStyle.Bold);
}
if (showStatus.Value)
{
DrawLine(bmp, g, weatherStatus, ref lineCount, baseSize, FontStyle.Regular);
}
if (showTime.Value && !string.IsNullOrEmpty(lastUpdated))
{
DrawLine(bmp, g, $"Updated: {lastUpdated}", ref lineCount, baseSize, FontStyle.Italic);
}
}
}
return bmp;
}
private void DrawLine(Bitmap bitmap, Graphics graphics, string text, ref int lineCount, int fontSize, FontStyle style)
{
using Font font = new Font(Application.Theme.Font, fontSize, style);
SizeF textSize = graphics.MeasureString(text, font);
float x = (bitmap.Width - textSize.Width) / 2;
// Adjust vertical spacing based on font size
float y = (lineCount == 0) ? 60 : (lineCount * (textSize.Height + 10)) + 60;
using Brush brush = new SolidBrush(Application.Theme.PrimaryColor);
graphics.DrawString(text, font, brush, x, y);
lineCount++;
}
private async Task UpdateLocationAndWeather()
{
try
{
string geoUrl = $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(cityInput.Value.Trim())}&count=1&language=en&format=json";
string geoJson = await httpClient.GetStringAsync(geoUrl);
using JsonDocument geoDoc = JsonDocument.Parse(geoJson);
if (!geoDoc.RootElement.TryGetProperty("results", out JsonElement results) || results.GetArrayLength() == 0)
{
foundCityName = cityInput.Value;
currentTemp = "--";
weatherStatus = "City not found";
return;
}
JsonElement location = results[0];
cachedLat = location.GetProperty("latitude").GetDouble().ToString(CultureInfo.InvariantCulture);
cachedLon = location.GetProperty("longitude").GetDouble().ToString(CultureInfo.InvariantCulture);
foundCityName = location.GetProperty("name").GetString() ?? cityInput.Value;
// We update the textbox to the "official" name found by the API
cityInput.Value = foundCityName;
await UpdateWeatherOnly(cachedLat, cachedLon);
}
catch (Exception ex)
{
Debug.WriteLine($"Error: {ex.Message}");
weatherStatus = "Connection Error";
}
}
private async Task UpdateWeatherOnly(string lat, string lon)
{
try
{
string weatherUrl = $"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current_weather=true";
string weatherJson = await httpClient.GetStringAsync(weatherUrl);
using JsonDocument weatherDoc = JsonDocument.Parse(weatherJson);
JsonElement current = weatherDoc.RootElement.GetProperty("current_weather");
currentTemp = $"{current.GetProperty("temperature").GetDouble()}°C";
weatherStatus = MapWeatherCode(current.GetProperty("weathercode").GetInt32());
lastUpdated = DateTime.Now.ToString("HH:mm");
}
catch { /* Ignore background update errors */ }
}
private string MapWeatherCode(int code)
{
return code switch
{
0 => "Clear Sky",
1 or 2 or 3 => "Partly Cloudy",
45 or 48 => "Foggy",
51 or 53 or 55 => "Drizzle",
61 or 63 or 65 => "Rainy",
71 or 73 or 75 => "Snowy",
95 or 96 or 99 => "Thunderstorm",
_ => "Cloudy"
};
}
}
@@ -11,7 +11,18 @@ internal class MainWindowDataContext : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged;
private static DesktopMagicSettings settings = new();
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
{
@@ -33,6 +44,16 @@ internal class MainWindowDataContext : INotifyPropertyChanged
}
}
public string? PluginsSearchText
{
get => pluginsSearchText;
set
{
pluginsSearchText = value;
OnPropertyChanged();
}
}
public bool IsAutoStartEnabled
{
get => StartupManager.IsAutoStartEnabled();
@@ -1,18 +1,19 @@
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using MaterialDesignThemes.Wpf;
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Input;
using Wpf.Ui.Controls;
namespace DesktopMagic.DataContexts;
internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand command, PluginEntryDataContext.Mode mode, string? path = null) : INotifyPropertyChanged
internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand command, PluginEntryDataContext.Mode mode, string? path = null, string? csprojPath = null) : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
@@ -20,7 +21,7 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
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"];
@@ -35,22 +36,40 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
public string? Path => path;
public bool IsLocalPlugin => string.IsNullOrWhiteSpace(pluginMetadata.ProfileUri?.ToString());
public bool IsLocalPlugin => pluginMetadata.IsLocalPlugin;
public ICommand Command => command;
public ButtonData InstallUninstallButtonData => new(mode == Mode.Install ? PackIconKind.Download : PackIconKind.Remove, GetInstallUninstallButtonText(), true, Command);
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 ButtonData OpenButtonData
{
get
{
if (string.IsNullOrWhiteSpace(pluginMetadata.ProfileUri?.ToString()))
if (File.Exists(csprojPath))
{
return new(PackIconKind.FolderOutline, (string)App.LanguageDictionary["folder"], path is not null, new CommandHandler(() => Process.Start("explorer.exe", path!)));
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(PackIconKind.ExternalLink, "mod.io", true, new CommandHandler(OpenModIoPage));
return new(new SymbolIcon(SymbolRegular.Open24), "mod.io", true, new CommandHandler(OpenModIoPage));
}
}
}
@@ -79,6 +98,34 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
_ = 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
@@ -94,7 +141,7 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
public record ButtonData(PackIconKind IconKind, string Text, bool IsEnabled, ICommand Command);
public record ButtonData(SymbolIcon Icon, string Text, bool IsEnabled, ICommand Command);
public enum Mode
{
@@ -1,6 +1,10 @@
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media.Imaging;
using Wpf.Ui.Appearance;
namespace DesktopMagic.DataContexts;
@@ -70,6 +74,23 @@ internal class PluginManagerDataContext : INotifyPropertyChanged
}
}
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)
+13 -6
View File
@@ -7,14 +7,15 @@
<ApplicationIcon>icon.ico</ApplicationIcon>
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
<Authors>Stone_Red</Authors>
<Version>1.1.0.0</Version>
<Version>1.3.3.0</Version>
<PackageProjectUrl>https://github.com/Stone-Red-Code/DesktopMagic</PackageProjectUrl>
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
<AssemblyVersion>1.3.3.0</AssemblyVersion>
<FileVersion>1.3.3.0</FileVersion>
<TargetFramework>net8.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Platforms>AnyCPU;x64</Platforms>
</PropertyGroup>
@@ -33,6 +34,7 @@
<None Remove="icon_Dark.ico" />
<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>
@@ -42,13 +44,17 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="BusyIndicators" Version="2.1.2" />
<PackageReference Include="BusyIndicators" Version="2.1.3" />
<PackageReference Include="CuteUtils" Version="1.0.0" />
<PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" />
<PackageReference Include="MaterialDesignThemes" Version="5.1.0" />
<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="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>
@@ -63,6 +69,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource>
<Resource Include="Resources\Images\modio-logo-bluedark.png" />
<Resource Include="Resources\Images\modio-logo-bluelight.png" />
</ItemGroup>
<ItemGroup>
@@ -70,4 +77,4 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
</Project>
+63 -39
View File
@@ -1,50 +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: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:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:DesktopMagic.Dialogs"
mc:Ignorable="d"
ResizeMode="NoResize"
Title="ColorDialog"
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="300"
Width="450"
MinHeight="300"
MinWidth="450"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight">
ExtendsContentIntoTitleBar="True">
<Grid>
<StackPanel Margin="15">
<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 Margin="{StaticResource DefaultMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<DockPanel>
<Label Content="A:" Width="20" />
<Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#00030000" />
<DockPanel Grid.Row="0">
<Label Content="A:" Width="20" />
<Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</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>
<DockPanel>
<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">
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0">
<Rectangle x:Name="colorRechtangle" Fill="White" />
</Border>
<TextBox x:Name="colorHexTextBox" MaxLength="9" CharacterCasing="Upper" Height="23" TextWrapping="Wrap" Width="350" TextChanged="ColorHexTextBox_TextChanged" RenderTransformOrigin="0.5,0.5" VerticalContentAlignment="Center">
<materialDesign:TextFieldAssist.CharacterCounterStyle>
<Style TargetType="TextBlock" />
</materialDesign:TextFieldAssist.CharacterCounterStyle>
</TextBox>
</StackPanel>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0,0,10,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>
<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>
</Window>
</ui:FluentWindow>
+6 -3
View File
@@ -4,12 +4,14 @@ using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using Wpf.Ui.Controls;
namespace DesktopMagic.Dialogs;
/// <summary>
/// Interaction logic for ColorDialog.xaml
/// </summary>
public partial class ColorDialog : Window
public partial class ColorDialog : FluentWindow
{
public System.Drawing.Color ResultColor { get; private set; }
@@ -22,6 +24,7 @@ public partial class ColorDialog : Window
Resources.MergedDictionaries.Add(App.LanguageDictionary);
label.Content = content;
titleBar.Title = title;
Title = title;
alphaSlider.Value = defaultColor.A;
@@ -86,8 +89,8 @@ public partial class ColorDialog : Window
ResultBrush = brush;
ResultColor = systemColor;
colorRechtangle.Fill = brush;
colorHexTextBox.Foreground = Brushes.Black;
colorRechtangle.Background = brush;
colorHexTextBox.Foreground = FindResource("TextFillColorPrimaryBrush") as Brush;
}
else
{
@@ -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;
}
}
+38 -13
View File
@@ -1,22 +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: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:local="clr-namespace:DesktopMagic.Dialogs"
mc:Ignorable="d"
Title="InputDialog"
ResizeMode="NoResize"
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="200"
Width="300"
MinHeight="160"
MinWidth="300"
WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight">
ExtendsContentIntoTitleBar="True">
<Grid>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center" Margin="15">
<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" VerticalAlignment="Center" HorizontalAlignment="Stretch" Margin="15">
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
<TextBox x:Name="textBox" HorizontalAlignment="Left" Height="23" TextWrapping="Wrap" VerticalAlignment="Top" Width="280" VerticalContentAlignment="Center" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0,0,10,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>
<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>
</Grid>
</Window>
</ui:FluentWindow>
+2 -1
View File
@@ -2,7 +2,7 @@
namespace DesktopMagic.Dialogs;
public partial class InputDialog : Window
public partial class InputDialog : Wpf.Ui.Controls.FluentWindow
{
public string ResponseText
{
@@ -17,6 +17,7 @@ public partial class InputDialog : Window
Resources.MergedDictionaries.Add(App.LanguageDictionary);
label.Content = content;
titleBar.Title = title;
Title = title;
}
@@ -0,0 +1,48 @@
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ReleaseInfoDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:md="http://schemas.lepo.co/wpfui/2022/xaml/markdown"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="700"
Width="900"
MinHeight="500"
MinWidth="700"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="{StaticResource Icon}" />
</ui:TitleBar.Icon>
</ui:TitleBar>
<StackPanel Grid.Row="1" Margin="14,10,14,0">
<TextBlock x:Name="releaseNameTextBlock" FontSize="20" FontWeight="SemiBold" />
<TextBlock x:Name="publishedAtTextBlock" Foreground="{DynamicResource TextFillColorSecondaryBrush}" Margin="0,4,0,0" />
</StackPanel>
<Border Grid.Row="2" Margin="14,12,14,12" BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}" BorderThickness="1" CornerRadius="8">
<md:MarkdownViewer x:Name="releaseMarkdownViewer" Margin="8" />
</Border>
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="14,0,14,12">
<Button x:Name="openButton" Width="150" Margin="0,0,8,0" Click="OpenButton_Click" />
<Button x:Name="okButton" Width="100" Click="OkButton_Click" />
</StackPanel>
</Grid>
</ui:FluentWindow>
@@ -0,0 +1,40 @@
using System.Windows;
namespace DesktopMagic.Dialogs;
public partial class ReleaseInfoDialog : Wpf.Ui.Controls.FluentWindow
{
public bool OpenReleaseRequested { get; private set; }
public ReleaseInfoDialog(
string title,
string releaseName,
string publishedAt,
string markdown,
string openButtonText,
string okButtonText)
{
InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
titleBar.Title = title;
Title = title;
releaseNameTextBlock.Text = releaseName;
publishedAtTextBlock.Text = publishedAt;
releaseMarkdownViewer.Markdown = string.IsNullOrWhiteSpace(markdown) ? "-" : markdown;
openButton.Content = openButtonText;
okButton.Content = okButtonText;
}
private void OpenButton_Click(object sender, RoutedEventArgs e)
{
OpenReleaseRequested = true;
DialogResult = true;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
}
+92
View File
@@ -0,0 +1,92 @@
<ui:FluentWindow x:Class="DesktopMagic.Dialogs.ThemeDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="290"
Width="550"
MinHeight="290"
MinWidth="550"
WindowStartupLocation="CenterOwner"
ExtendsContentIntoTitleBar="True">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ui:TitleBar x:Name="titleBar" ShowMinimize="False" ShowMaximize="False">
<ui:TitleBar.Icon>
<ui:ImageIcon Source="{StaticResource Icon}" />
</ui:TitleBar.Icon>
</ui:TitleBar>
<StackPanel Grid.Row="1" Margin="15">
<Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Display Font: " Grid.Column="0" Grid.Row="0" VerticalAlignment="Center" Loaded="TextBlock_Loaded"/>
<ComboBox x:Name="fontComboBox" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" SelectionChanged="FontComboBox_SelectionChanged" Margin="0,0,0,5" />
<TextBlock Text="Corner Radius: " Grid.Column="0" Grid.Row="1" VerticalAlignment="Center"></TextBlock>
<ui:NumberBox x:Name="cornerRadiusTextBox" Minimum="0" Grid.Column="1" Grid.Row="1" ValueChanged="CornerRadiusTextBox_ValueChanged" PlaceholderText="Enter a number" Margin="0,0,0,5" />
<TextBlock Text="Margin: " Grid.Column="0" Grid.Row="3" VerticalAlignment="Center"></TextBlock>
<ui:NumberBox x:Name="marginTextBox" Minimum="0" Grid.Column="1" Grid.Row="3" ValueChanged="MarginTextBox_ValueChanged" PlaceholderText="Enter a number"/>
</Grid>
<Grid Grid.Column="1" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<DockPanel Grid.Row="0">
<Border x:Name="primaryColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Button Content="Change Primary Color" FontWeight="Regular" Click="ChangePrimaryColorButton_Click" HorizontalAlignment="Stretch" />
</DockPanel>
<DockPanel Grid.Row="1">
<Border x:Name="secondaryColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Button Content="Change Secondary Color" FontWeight="Regular" Click="ChangeSecondaryColorButton_Click" HorizontalAlignment="Stretch" />
</DockPanel>
<DockPanel Grid.Row="2">
<Border x:Name="backgroundColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Button Content="Change Background Color" FontWeight="Regular" Click="ChangeBackgroundColorButton_Click" HorizontalAlignment="Stretch" />
</DockPanel>
</Grid>
</Grid>
</StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</ui:FluentWindow>
@@ -0,0 +1,132 @@
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace DesktopMagic.Dialogs;
/// <summary>
/// Interaction logic for ColorDialog.xaml
/// </summary>
public partial class ThemeDialog : Wpf.Ui.Controls.FluentWindow
{
private readonly Theme theme;
public ThemeDialog(string content, Theme theme, string title = App.AppName)
{
this.theme = theme;
InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
cornerRadiusTextBox.Value = theme.CornerRadius;
marginTextBox.Value = theme.Margin;
primaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor));
secondaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.SecondaryColor));
backgroundColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor));
label.Content = content;
titleBar.Title = title;
Title = title;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = true;
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
}
private void TextBlock_Loaded(object sender, RoutedEventArgs e)
{
int index = 0;
foreach (FontFamily ff in Fonts.SystemFontFamilies)
{
ComboBoxItem comboBoxItem = new()
{
FontFamily = ff,
Content = ff.ToString()
};
_ = fontComboBox.Items.Add(comboBoxItem);
if (ff.ToString() == theme.Font)
{
fontComboBox.SelectedIndex = index;
}
index++;
}
if (fontComboBox.SelectedIndex == -1)
{
fontComboBox.SelectedIndex = 0;
}
}
private void FontComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
theme.Font = fontComboBox.SelectedValue.ToString()!.Replace("System.Windows.Controls.ComboBoxItem: ", "");
}
private void ChangePrimaryColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Primary Color", theme.PrimaryColor)
{
Owner = this
};
if (colorDialog.ShowDialog() == true)
{
theme.PrimaryColor = colorDialog.ResultColor;
primaryColorRechtangle.Background = colorDialog.ResultBrush;
}
}
private void ChangeSecondaryColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Secondary Color", theme.SecondaryColor)
{
Owner = this
};
if (colorDialog.ShowDialog() == true)
{
theme.SecondaryColor = colorDialog.ResultColor;
secondaryColorRechtangle.Background = colorDialog.ResultBrush;
}
}
private void ChangeBackgroundColorButton_Click(object sender, RoutedEventArgs e)
{
ColorDialog colorDialog = new ColorDialog("Set Background Color", theme.BackgroundColor)
{
Owner = this
};
if (colorDialog.ShowDialog() == true)
{
theme.BackgroundColor = colorDialog.ResultColor;
backgroundColorRechtangle.Background = colorDialog.ResultBrush;
}
}
private void MarginTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{
if (args.NewValue is >= 0)
{
theme.Margin = (int)args.NewValue;
}
}
private void CornerRadiusTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{
if (args.NewValue is >= 0)
{
theme.CornerRadius = (int)args.NewValue;
}
}
}
+2
View File
@@ -9,6 +9,8 @@ using System.Diagnostics.CodeAnalysis;
[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("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)")]
+36 -2
View File
@@ -6,16 +6,26 @@ namespace DesktopMagic.Helpers;
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}";
}
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}";
}
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)
{
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);
}
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)
{
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})")]
private static partial Regex Hex8();
@@ -0,0 +1,51 @@
using System;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace DesktopMagic.Helpers;
public sealed class GitHubLatestRelease
{
[JsonPropertyName("tag_name")]
public string TagName { get; init; } = "Unknown";
[JsonPropertyName("name")]
public string Name { get; init; } = "Unknown";
[JsonPropertyName("body")]
public string Body { get; init; } = string.Empty;
[JsonPropertyName("published_at")]
public DateTimeOffset? PublishedAt { get; init; }
[JsonPropertyName("html_url")]
public string HtmlUrl { get; init; } = "https://github.com/Stone-Red-Code/DesktopMagic/releases/latest";
}
public static class GitHubReleaseService
{
private const string LatestReleaseApiUrl = "https://api.github.com/repos/Stone-Red-Code/DesktopMagic/releases/latest";
private static readonly HttpClient _httpClient = CreateReleaseHttpClient();
private static HttpClient CreateReleaseHttpClient()
{
HttpClient client = new()
{
Timeout = TimeSpan.FromSeconds(10)
};
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
client.DefaultRequestHeaders.UserAgent.ParseAdd("DesktopMagic");
return client;
}
public static async Task<GitHubLatestRelease?> GetLatestReleaseInfoAsync()
{
using HttpResponseMessage response = await _httpClient.GetAsync(LatestReleaseApiUrl);
response.EnsureSuccessStatusCode();
await using var responseStream = await response.Content.ReadAsStreamAsync();
return await JsonSerializer.DeserializeAsync<GitHubLatestRelease>(responseStream);
}
}
@@ -0,0 +1,89 @@
using Ical.Net;
using Ical.Net.CalendarComponents;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.Helpers;
public sealed class CalendarEventEntry
{
public string Title { get; init; } = "Untitled";
public string? Location { get; init; }
public DateTime StartLocal { get; init; }
public DateTime EndLocal { get; init; }
public bool IsAllDay { get; init; }
}
public static class IcsCalendarHelper
{
private static readonly HttpClient httpClient = new()
{
Timeout = TimeSpan.FromSeconds(12)
};
static IcsCalendarHelper()
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("DesktopMagic");
httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/calendar");
}
public static async Task<List<CalendarEventEntry>> GetUpcomingEventsAsync(
string calendarUrl,
DateTime windowStartLocal,
DateTime windowEndLocal,
int maxEvents,
bool includeAllDayEvents,
CancellationToken cancellationToken)
{
using HttpResponseMessage response = await httpClient.GetAsync(calendarUrl, cancellationToken);
response.EnsureSuccessStatusCode();
string icsContent = await response.Content.ReadAsStringAsync(cancellationToken);
Calendar calendar = Calendar.Load(icsContent);
DateTime windowStartUtc = windowStartLocal.ToUniversalTime();
DateTime windowEndUtc = windowEndLocal.ToUniversalTime();
List<CalendarEventEntry> results = [];
foreach (var occurrence in calendar.GetOccurrences(windowStartUtc, windowEndUtc))
{
if (occurrence.Source is not CalendarEvent calendarEvent)
{
continue;
}
DateTime startLocal = occurrence.Period.StartTime.AsSystemLocal;
DateTime endLocal = occurrence.Period.EndTime?.AsSystemLocal ?? startLocal.AddHours(1);
bool isAllDay = !occurrence.Period.StartTime.HasTime;
if (!includeAllDayEvents && isAllDay)
{
continue;
}
if (endLocal <= windowStartLocal)
{
continue;
}
results.Add(new CalendarEventEntry
{
Title = string.IsNullOrWhiteSpace(calendarEvent.Summary) ? "Untitled" : calendarEvent.Summary,
Location = string.IsNullOrWhiteSpace(calendarEvent.Location) ? null : calendarEvent.Location,
StartLocal = startLocal,
EndLocal = endLocal,
IsAllDay = isAllDay
});
}
return results
.OrderBy(eventEntry => eventEntry.StartLocal)
.Take(Math.Max(1, maxEvents))
.ToList();
}
}
@@ -1,19 +1,19 @@
using DesktopMagic.Plugins;
using DesktopMagic.Dialogs;
using DesktopMagic.Plugins;
using Microsoft.Win32;
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace DesktopMagic.Helpers;
internal class SettingElementGenerator(ComboBox optionsComboBox)
internal class SettingElementGenerator(uint pluginId)
{
private readonly ComboBox optionsComboBox = optionsComboBox;
public void Generate(SettingElement settingElement, DockPanel dockPanel, TextBlock textBlock)
public FrameworkElement? Generate(SettingElement settingElement, System.Windows.Controls.TextBlock textBlock)
{
dockPanel.UpdateLayout();
textBlock.UpdateLayout();
if (settingElement.Input is DesktopMagic.Api.Settings.Label eLabel)
{
textBlock.Text = eLabel.Value;
@@ -32,16 +32,13 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBlock.Text = eLabel.Value;
});
};
return null;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.Button eButton)
{
Button button = new()
Wpf.Ui.Controls.Button button = new()
{
Content = eButton.Value,
FontSize = 10,
Height = 20,
Margin = new Thickness(0, 10, 0, 10),
Padding = new Thickness(0),
VerticalAlignment = VerticalAlignment.Center,
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)
{
CheckBox checkBox = new()
Wpf.Ui.Controls.ToggleSwitch checkBox = new()
{
IsChecked = eCheckBox.Value,
Style = (Style)dockPanel.FindResource("MaterialDesignDarkCheckBox"),
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) =>
{
@@ -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)
{
TextBox textBox = new()
Wpf.Ui.Controls.TextBox textBox = new()
{
Text = eTextBox.Value,
TextWrapping = TextWrapping.Wrap,
TextWrapping = TextWrapping.NoWrap,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
@@ -123,26 +122,23 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBox.Text = eTextBox.Value;
});
};
_ = dockPanel.Children.Add(textBox);
return textBox;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.IntegerUpDown eIntegerUpDown)
{
MaterialDesignThemes.Wpf.NumericUpDown numericUpDown = new()
Wpf.Ui.Controls.NumberBox numberBox = new()
{
Value = eIntegerUpDown.Value,
Minimum = eIntegerUpDown.Minimum,
Maximum = eIntegerUpDown.Maximum,
IncreaseContent = new Label() { Content = "+" },
DecreaseContent = new Label() { Content = "" },
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch,
AllowChangeOnScroll = true
HorizontalAlignment = HorizontalAlignment.Stretch
};
numericUpDown.ValueChanged += (_s, _e) =>
numberBox.ValueChanged += (_s, _e) =>
{
try
{
eIntegerUpDown.Value = numericUpDown.Value;
eIntegerUpDown.Value = (int)numberBox.Value;
}
catch (Exception ex)
{
@@ -151,12 +147,12 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
};
eIntegerUpDown.OnValueChanged += () =>
{
numericUpDown.Dispatcher.Invoke(() =>
numberBox.Dispatcher.Invoke(() =>
{
numericUpDown.Value = eIntegerUpDown.Value;
numberBox.Value = eIntegerUpDown.Value;
});
};
_ = dockPanel.Children.Add(numericUpDown);
return numberBox;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.Slider eSlider)
{
@@ -168,7 +164,8 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
TickFrequency = 1,
IsSnapToTickEnabled = true,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 5, 0, 5)
};
slider.ValueChanged += (_s, _e) =>
{
@@ -189,7 +186,7 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
});
};
_ = dockPanel.Children.Add(slider);
return slider;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.ComboBox eComboBox)
{
@@ -206,7 +203,8 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
{
try
{
eComboBox.Value = comboBox.SelectedItem.ToString() ?? string.Empty;
comboBox.SelectedItem ??= eComboBox.Value;
eComboBox.Value = comboBox.SelectedItem?.ToString() ?? eComboBox.Value;
}
catch (Exception ex)
{
@@ -219,17 +217,170 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
comboBox.SelectedItem = eComboBox.Value;
};
_ = dockPanel.Children.Add(comboBox);
return comboBox;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.ColorPicker eColorPicker)
{
Grid panel = new()
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
panel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
panel.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
Border colorPreview = new()
{
Width = 30,
Height = 30,
CornerRadius = new CornerRadius(4),
Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B)),
Margin = new Thickness(0, 0, 10, 0),
BorderBrush = Application.Current.FindResource("ControlElevationBorderBrush") as Brush,
BorderThickness = new Thickness(1)
};
Grid.SetColumn(colorPreview, 0);
Wpf.Ui.Controls.Button browseButton = new()
{
Content = "Select Color",
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
Grid.SetColumn(browseButton, 1);
browseButton.Click += (_s, _e) =>
{
try
{
ColorDialog colorDialog = new("Select a color", eColorPicker.Value)
{
Owner = Application.Current.MainWindow
};
if (colorDialog.ShowDialog() == true)
{
eColorPicker.Value = colorDialog.ResultColor;
colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
}
}
catch (Exception ex)
{
DisplayException(ex.Message);
}
};
eColorPicker.OnValueChanged += () =>
{
colorPreview.Dispatcher.Invoke(() =>
{
colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
});
};
_ = panel.Children.Add(colorPreview);
_ = panel.Children.Add(browseButton);
return panel;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.FileSelector eFileSelector)
{
Grid grid = new()
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Wpf.Ui.Controls.TextBox pathTextBox = new()
{
Text = eFileSelector.Value,
IsReadOnly = true,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 0, 5, 0)
};
Grid.SetColumn(pathTextBox, 0);
Wpf.Ui.Controls.Button browseButton = new()
{
Content = "Browse",
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetColumn(browseButton, 1);
browseButton.Click += (_s, _e) =>
{
try
{
if (eFileSelector.SelectFolder)
{
OpenFolderDialog folderDialog = new()
{
Title = eFileSelector.Title,
InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
};
if (folderDialog.ShowDialog() == true)
{
eFileSelector.Value = folderDialog.FolderName;
pathTextBox.Text = eFileSelector.Value;
}
}
else
{
OpenFileDialog fileDialog = new()
{
Title = eFileSelector.Title,
Filter = eFileSelector.Filter,
InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
};
if (fileDialog.ShowDialog() == true)
{
eFileSelector.Value = fileDialog.FileName;
pathTextBox.Text = eFileSelector.Value;
}
}
}
catch (Exception ex)
{
DisplayException(ex.Message);
}
};
eFileSelector.OnValueChanged += () =>
{
pathTextBox.Dispatcher.Invoke(() =>
{
pathTextBox.Text = eFileSelector.Value;
});
};
_ = grid.Children.Add(pathTextBox);
_ = grid.Children.Add(browseButton);
return grid;
}
return null;
}
private void DisplayException(string message)
private async void DisplayException(string message)
{
App.Logger.LogInfo(message, source: "PluginInput");
_ = MessageBox.Show("File execution error:\n" + message, "Error", MessageBoxButton.OK, MessageBoxImage.Error);
int index = MainWindow.WindowNames.IndexOf(optionsComboBox.SelectedItem.ToString() ?? string.Empty);
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);
PluginWindow window = MainWindow.Windows[index];
window?.Exit();
if (index >= 0 && index < Manager.Instance.PluginWindows.Count)
{
IPluginWindow window = Manager.Instance.PluginWindows[index];
window?.Exit();
}
}
}
+14 -7
View File
@@ -3,7 +3,6 @@
using System;
using System.IO;
using System.Reflection;
using System.Windows;
namespace DesktopMagic.Helpers;
@@ -67,16 +66,24 @@ public static class StartupManager
}
}
public static void ToggleAutoStart()
public static async void ToggleAutoStart()
{
AutoStartStatus status = IsAutoStartEnabled() ? DisableAutoStart() : EnableAutoStart();
_ = status switch
string message = status switch
{
AutoStartStatus.Success => MessageBox.Show("Auto-start setting changed successfully."),
AutoStartStatus.AlreadyEnabled => MessageBox.Show("Auto-start is already enabled."),
AutoStartStatus.AlreadyDisabled => MessageBox.Show("Auto-start is already disabled."),
_ => MessageBox.Show("Failed to change auto-start setting."),
AutoStartStatus.Success => "Auto-start setting changed successfully.",
AutoStartStatus.AlreadyEnabled => "Auto-start is already enabled.",
AutoStartStatus.AlreadyDisabled => "Auto-start is already disabled.",
_ => "Failed to change auto-start setting.",
};
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
}
@@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace DesktopMagic.Helpers;
public class UnderscoreEscapingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string text)
{
// Replaces single underscores with double underscores to escape them in labels
return text.Replace("_", "__");
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
+15
View File
@@ -76,6 +76,21 @@ namespace DesktopMagic
DependencyProperty.RegisterAttached("IsHooked", typeof(WindowLockHook), typeof(WindowPos),
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 readonly Window Window;
+117 -167
View File
@@ -1,183 +1,133 @@
<Window x:Class="DesktopMagic.MainWindow"
<ui:FluentWindow x:Class="DesktopMagic.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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:local="clr-namespace:DesktopMagic"
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"
Closing="Window_Closing"
Closed="Window_Closed"
ShowInTaskbar="True"
Icon="{StaticResource Icon}"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}"
Height="520"
Width="570"
MinHeight="520"
MinWidth="570"
Height="560"
Width="900"
MinHeight="600"
MinWidth="940"
ExtendsContentIntoTitleBar="True"
WindowState="Minimized"
Loaded="Window_Loaded">
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False">
<Grid>
<TextBlock TextWrapping="Wrap" Text="0" VerticalAlignment="Top" FontFamily="Comic Sans MS" Loaded="TextBlock_Loaded" Foreground="Transparent" />
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
<Grid Margin="{StaticResource DefaultMargin}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ui:TitleBar x:Name="TitleBar" Title="{Binding Title}">
<ui:TitleBar.Icon>
<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>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</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">
<ComboBox x:Name="optionsComboBox" DisplayMemberPath="Name" DockPanel.Dock="Top" Height="24" SelectedIndex="0" SelectionChanged="OptionsComboBox_SelectionChanged" Background="#FFECECEC" Padding="4" VerticalAlignment="Center" />
<ScrollViewer Background="#FFBBBBBB">
<Grid>
<StackPanel x:Name="optionsPanel" Margin="3,3,3,0" HorizontalAlignment="Stretch" Visibility="Collapsed">
</StackPanel>
</Grid>
</ScrollViewer>
</DockPanel>
<ui:HyperlinkButton FontWeight="Bold" Content="{DynamicResource developedBy}" NavigateUri="https://me.stone-red.net"/>
<ui:ToggleSwitch x:Name="autoStartCheckBox" Style="{StaticResource ToggleSwitchContentLeftStyle}" Grid.Column="2" Margin="0 0 5 0" Content="{DynamicResource autoStart}" IsChecked="{Binding IsAutoStartEnabled}" VerticalAlignment="Center"/>
</Grid>
<Grid Grid.Row="1">
<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" Margin="0,0,0,5" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="170" Click="NewLayoutButton_Click" FontWeight="Regular">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Add" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource newLayout}" />
</StackPanel>
</Button>
<Button x:Name="removeLayoutButton" HorizontalAlignment="Left" VerticalAlignment="Bottom" Width="170" Click="RemoveLayoutButton_Click" FontWeight="Regular">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="DeleteOutline" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource deleteLayout}" />
</StackPanel>
</Button>
</StackPanel>
<StackPanel Grid.Column="1" Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
<Button x:Name="updatePluginsButton" Margin="0,0,0,5" HorizontalAlignment="Center" Width="170" Click="UpdatePluginsButton_Click" FontWeight="Regular">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Reload" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource updatePlugins}" />
</StackPanel>
</Button>
<Button x:Name="githubButton" HorizontalAlignment="Center" Width="170" Click="GitHubButton_Click" FontWeight="Regular" Cursor="Hand">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Github" Margin="0 0 10 0" />
<TextBlock Text="GitHub" />
</StackPanel>
</Button>
</StackPanel>
<StackPanel Grid.Column="2" Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom">
<CheckBox x:Name="autoStartCheckBox" Content="{DynamicResource autoStart}" Foreground="Black" BorderBrush="#FF323232" Margin="0,0,0,5" Width="170" Height="20" Style="{StaticResource MaterialDesignDarkCheckBox}" IsChecked="{Binding IsAutoStartEnabled}" />
<Button x:Name="downloadPluginsButton" Margin="0,0,0,5" HorizontalAlignment="Right" Width="170" Click="PluginManagerButton_Click" FontWeight="Regular" Cursor="Hand">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="ExtensionOutline" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource pluginManager}" />
</StackPanel>
</Button>
<Button x:Name="openPluginsFolderButton" HorizontalAlignment="Right" Width="170" Click="OpenPluginsFolderButton_Click" FontWeight="Regular">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="FolderOutline" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource pluginsFolder}" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
</Grid>
</Grid>
</Border>
</Grid>
</busyIndicator:BusyMask>
</Window>
<ContentPresenter x:Name="rootContentDialog" Grid.Row="1" Grid.RowSpan="2" />
</Grid>
</ui:FluentWindow>
+212 -624
View File
@@ -1,683 +1,271 @@
using DesktopMagic.BuiltInPlugins;
using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
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((string)App.LanguageDictionary["musicVisualizer"], 1), typeof(MusicVisualizerPlugin)},
{new((string)App.LanguageDictionary["time"],2), typeof(TimePlugin)},
{new((string)App.LanguageDictionary["date"],3), typeof(DatePlugin)},
{new((string)App.LanguageDictionary["cpuUsage"], 4), typeof(CpuMonitorPlugin)}
};
App.Logger.LogInfo("Loading application", source: "MainWindow");
private bool loaded = false;
private bool blockWindowsClosing = true;
public static List<PluginWindow> Windows { get; } = [];
public static List<string> WindowNames { get; } = [];
_mainWindowDataContext.IsLoading = true;
private DesktopMagicSettings Settings
{
get => mainWindowDataContext.Settings;
set => mainWindowDataContext.Settings = value;
// Load plugins and settings through manager
_manager.LoadSettings();
_mainWindowDataContext.Settings = _manager.Settings;
_manager.LoadPlugins();
_manager.LoadLayout();
_manager.IsLoaded = true;
_mainWindowDataContext.IsLoading = false;
await ShowLatestReleaseInfoAfterUpdateAsync();
App.Logger.LogInfo("Application loaded", source: "MainWindow");
}
public MainWindow()
catch (Exception ex)
{
DataContext = mainWindowDataContext;
try
App.Logger.LogError(ex.Message, source: "MainWindow");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Stream iconStream = Application.GetResourceStream(new Uri("pack://application:,,,/DesktopMagic;component/icon.ico")).Stream;
notifyIcon.MouseClick += NotifyIcon_MouseClick;
notifyIcon.Visible = true;
notifyIcon.Text = App.AppName;
notifyIcon.Icon = new System.Drawing.Icon(iconStream);
notifyIcon.ContextMenuStrip = new System.Windows.Forms.ContextMenuStrip()
{
Items =
{
new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["open"], null, (s, e) => RestoreWindow()),
new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["toggleEditMode"], null, (s, e) => { editCheckBox.IsChecked = !editCheckBox.IsChecked; EditCheckBox_Click(null, null); }),
new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["pluginManager"], null, (s, e) => PluginManagerButton_Click(null!, null!)),
new System.Windows.Forms.ToolStripMenuItem("GitHub", null, (s, e) => GitHubButton_Click(null!, null!)),
new System.Windows.Forms.ToolStripMenuItem((string)App.LanguageDictionary["quit"], null, (s, e) => Quit()),
}
};
InitializeComponent();
Resources.MergedDictionaries.Add(App.LanguageDictionary);
#if DEBUG
Title = $"{App.AppName} - Dev {System.Windows.Forms.Application.ProductVersion}";
#else
Title = $"{App.AppName} - {System.Windows.Forms.Application.ProductVersion}";
#endif
}
catch (Exception ex)
{
_ = MessageBox.Show(ex.ToString(), App.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
#region Load
private readonly Dictionary<uint, InternalPluginData> plugins = [];
private void Window_Loaded(object sender, RoutedEventArgs e)
{
try
{
//Write To Log File and Load Elements
App.Logger.LogInfo("Loading Plugin names", source: "Main");
LoadPlugins();
App.Logger.LogInfo("Loading Layout names", source: "Main");
LoadSettings();
App.Logger.LogInfo("Loading Layout", source: "Main");
LoadLayout();
loaded = true;
App.Logger.LogInfo("Window Loaded", source: "Main");
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "Main");
_ = MessageBox.Show(ex.ToString(), App.AppName, MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void LoadPlugins()
{
mainWindowDataContext.IsLoading = true;
plugins.Clear();
foreach (var buildInPlugin in builtInPlugins.Keys)
{
plugins.Add(buildInPlugin.Id, new(buildInPlugin, string.Empty));
}
foreach (string directory in Directory.GetDirectories(App.PluginsPath))
{
string? pluginDllPath = Directory.GetFiles(directory, "main.dll").FirstOrDefault();
string? pluginMetadataPath = Directory.GetFiles(directory, "metadata.json").FirstOrDefault();
if (pluginDllPath is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no \"main.dll\"", source: "Main");
continue;
}
if (pluginMetadataPath is null)
{
App.Logger.LogWarn($"Plugin \"{directory}\" has no \"metadata.json\"", source: "Main");
continue;
}
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(pluginMetadataPath));
if (pluginMetadata is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no valid \"metadata.json\"", source: "Main");
continue;
}
if (plugins.ContainsKey(pluginMetadata.Id))
{
App.Logger.LogError($"Plugin \"{directory}\" has the same id as another plugin", source: "Main");
continue;
}
plugins.Add(pluginMetadata.Id, new(pluginMetadata, directory));
}
mainWindowDataContext.IsLoading = false;
}
#endregion Load
#region Windows
private void EditCheckBox_Click(object? sender, RoutedEventArgs? e)
{
foreach (PluginWindow window in Windows)
{
window.SetEditMode(editCheckBox.IsChecked == true);
}
SaveSettings();
}
private void PluginCheckBox_Click(object sender, RoutedEventArgs? e)
{
if (sender is not CheckBox checkBox)
{
return;
}
LoadPlugin(uint.Parse(checkBox.Tag.ToString()!));
}
private void LoadPlugin(uint pluginId)
{
if (!plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
{
return;
}
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
{
pluginSettings = new PluginSettings();
Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
}
if (WindowNames.Contains(internalPluginData.Metadata.Id.ToString()) || !pluginSettings.Enabled)
{
int index = WindowNames.IndexOf(internalPluginData.Metadata.Id.ToString());
if (index >= 0)
{
try
{
blockWindowsClosing = false;
Windows[index].Close();
blockWindowsClosing = true;
Windows.RemoveAt(index);
WindowNames.RemoveAt(index);
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "Main");
}
}
return;
}
PluginWindow window;
if (builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
{
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
else
{
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
Action? onPluginLoaded = null;
onPluginLoaded = () =>
{
Dispatcher.Invoke(() =>
{
if (!optionsComboBox.Items.Contains(internalPluginData.Metadata))
{
_ = optionsComboBox.Items.Add(internalPluginData.Metadata);
}
optionsComboBox.SelectedIndex = -1;
optionsComboBox.SelectedIndex = optionsComboBox.Items.IndexOf(internalPluginData.Metadata);
window.PluginLoaded -= onPluginLoaded;
});
Title = App.AppName,
Content = ex.ToString(),
CloseButtonText = "Ok"
};
window.OnExit += () =>
{
Windows.Remove(window);
WindowNames.Remove(window.Title);
blockWindowsClosing = false;
window.Close();
blockWindowsClosing = true;
pluginSettings.Enabled = false;
};
window.PluginLoaded += onPluginLoaded;
window.ShowInTaskbar = false;
window.Show();
window.SetEditMode(editCheckBox.IsChecked == true);
window.Closing += DisplayWindow_Closing;
Windows.Add(window);
WindowNames.Add(window.Title);
_ = await messageBox.ShowDialogAsync();
}
}
private void DisplayWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = blockWindowsClosing;
}
private async void NavigationView_Loaded(object sender, RoutedEventArgs e)
{
_ = NavigationView.Navigate(typeof(Pages.MainPage));
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
SaveSettings();
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_manager.SaveSettings();
if (blockWindowsClosing)
{
e.Cancel = true;
editCheckBox.IsChecked = false;
EditCheckBox_Click(null, null);
ShowInTaskbar = false;
WindowState = WindowState.Minimized;
Visibility = Visibility.Collapsed;
}
}
private void Window_Closed(object sender, EventArgs e)
if (_manager.BlockWindowsClosing)
{
e.Cancel = true;
_manager.SetEditMode(false);
ShowInTaskbar = false;
WindowState = WindowState.Minimized;
Visibility = Visibility.Collapsed;
UpdateLayout();
foreach (Window window in Windows)
{
window.Hide();
}
Environment.Exit(0);
}
}
#endregion Windows
private void Window_Closed(object sender, EventArgs e)
{
Visibility = Visibility.Collapsed;
UpdateLayout();
_manager.CloseAllPluginWindows();
Environment.Exit(0);
}
#region Options
private void FontComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
private void NotifyIcon_MouseClick(object? sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
Settings.CurrentLayout.Theme.Font = fontComboBox.SelectedValue.ToString()!.Replace("System.Windows.Controls.ComboBoxItem: ", "");
RestoreWindow();
}
}
private void OptionsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
internal void RestoreWindow()
{
for (int i = 0; i < 10; i++)
{
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);
}
ShowInTaskbar = true;
Visibility = Visibility.Visible;
SystemCommands.RestoreWindow(this);
Topmost = true;
_ = Activate();
Topmost = false;
}
}
#endregion Options
private void Quit()
{
_manager.BlockWindowsClosing = false;
Close();
}
internal void RestoreWindow()
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()
{
for (int i = 0; i < 10; i++)
{
ShowInTaskbar = true;
Visibility = Visibility.Visible;
SystemCommands.RestoreWindow(this);
Topmost = true;
_ = Activate();
Topmost = false;
}
}
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void Quit()
private void ReportBugEmailNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "mailto:[email protected]";
ProcessStartInfo psi = new()
{
blockWindowsClosing = false;
Close();
}
private void OpenPluginsFolderButton_Click(object sender, RoutedEventArgs e)
{
_ = Process.Start("explorer.exe", App.PluginsPath);
}
private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
private void 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)
{
Owner = this
};
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)
{
Owner = this
};
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)
{
Owner = this
};
if (colorDialog.ShowDialog() == true)
{
Settings.CurrentLayout.Theme.BackgroundColor = colorDialog.ResultColor;
backgroundColorRechtangle.Fill = colorDialog.ResultBrush;
}
}
private void CornerRadiusTextBox_TextChanged(object? sender, TextChangedEventArgs? e)
{
bool success = int.TryParse(cornerRadiusTextBox.Text, out int cornerRadius);
if (success)
{
cornerRadiusTextBox.Foreground = Brushes.Black;
Settings.CurrentLayout.Theme.CornerRadius = cornerRadius;
}
else
{
cornerRadiusTextBox.Foreground = Brushes.Red;
}
}
private void MarginTextBox_TextChanged(object? sender, TextChangedEventArgs? e)
{
bool success = int.TryParse(marginTextBox.Text, out int margin);
if (success)
{
marginTextBox.Foreground = Brushes.Black;
Settings.CurrentLayout.Theme.Margin = margin;
}
else
{
marginTextBox.Foreground = Brushes.Red;
}
}
#region Layout
private readonly JsonSerializerOptions jsonSettingsOptions = new()
{
Converters =
{
new ColorJsonConverter()
}
UseShellExecute = true,
FileName = uri
};
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
_ = 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()
{
if (loaded)
{
SaveSettings();
LoadLayout(false);
}
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;
}
private void NewLayoutButton_Click(object sender, RoutedEventArgs e)
GitHubLatestRelease? latestRelease = await GetLatestReleaseInfoAsync();
if (latestRelease is null)
{
InputDialog inputDialog = new((string)FindResource("enterLayoutName"))
{
Owner = this
};
if (inputDialog.ShowDialog() == true)
{
if (Settings.Layouts.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
{
_ = MessageBox.Show((string)FindResource("layoutAlreadyExists"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
SaveSettings();
}
return;
}
private void RemoveLayoutButton_Click(object sender, RoutedEventArgs e)
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"))
{
Settings.Layouts.Remove(Settings.CurrentLayout);
SaveSettings();
}
Owner = this,
Topmost = true
};
private void SaveSettings()
_ = releaseInfoDialog.ShowDialog();
if (releaseInfoDialog.OpenReleaseRequested)
{
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;
EditCheckBox_Click(null, null);
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)
{
Close();
}
else
{
RestoreWindow();
}
mainWindowDataContext.IsLoading = false;
}
#endregion Layout
private void UpdatePluginsButton_Click(object sender, RoutedEventArgs e)
{
LoadPlugins();
LoadLayout(false);
}
private void NotifyIcon_MouseClick(object? sender, System.Windows.Forms.MouseEventArgs e)
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
{
RestoreWindow();
}
}
private void GitHubButton_Click(object sender, RoutedEventArgs e)
{
string uri = "https://github.com/Stone-Red-Code/DesktopMagic";
ProcessStartInfo psi = new ProcessStartInfo
ProcessStartInfo processStartInfo = new()
{
UseShellExecute = true,
FileName = uri
FileName = latestRelease.HtmlUrl
};
_ = Process.Start(psi);
_ = Process.Start(processStartInfo);
}
private void PluginManagerButton_Click(object sender, RoutedEventArgs e)
{
PluginManager pluginManager = new PluginManager
{
Owner = this
};
pluginManager.ShowDialog();
LoadPlugins();
LoadLayout(Visibility != Visibility.Visible);
}
_manager.Settings.ReleaseInfoLastAppVersion = currentVersion;
_manager.SaveSettings();
}
internal class InternalPluginData(PluginMetadata pluginMetadata, string directoryPath)
private static async Task<GitHubLatestRelease?> GetLatestReleaseInfoAsync()
{
public PluginMetadata Metadata { get; set; } = pluginMetadata;
public string DirectoryPath { get; set; } = directoryPath;
try
{
return await GitHubReleaseService.GetLatestReleaseInfoAsync();
}
catch (HttpRequestException ex)
{
App.Logger.LogError($"Failed to retrieve latest release information: {ex.Message}", source: "MainWindow");
}
catch (TaskCanceledException ex)
{
App.Logger.LogWarn($"Timed out while retrieving latest release information: {ex.Message}", source: "MainWindow");
}
catch (JsonException ex)
{
App.Logger.LogError($"Failed to parse latest release information: {ex.Message}", source: "MainWindow");
}
return null;
}
}
}
+378
View File
@@ -0,0 +1,378 @@
using DesktopMagic.BuiltInPlugins;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Windows;
namespace DesktopMagic;
/// <summary>
/// Singleton Manager class that handles global state, plugin management, and shared operations
/// </summary>
public sealed class Manager
{
private static Manager? _instance;
private static readonly object _lock = new();
public static Manager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
_instance ??= new Manager();
}
}
return _instance;
}
}
// Plugin management
private readonly Dictionary<uint, InternalPluginData> _plugins = [];
private readonly Dictionary<PluginMetadata, Type> _builtInPlugins = new()
{
{new((string)App.LanguageDictionary["musicVisualizer"], 1) { Author = "Stone_Red" }, typeof(MusicVisualizerPlugin)},
{new((string)App.LanguageDictionary["time"],2) { Author = "Stone_Red" }, typeof(TimePlugin)},
{new((string)App.LanguageDictionary["date"],3) { Author = "Stone_Red" }, typeof(DatePlugin)},
{new((string)App.LanguageDictionary["cpuUsage"], 4) { Author = "Stone_Red" }, typeof(CpuMonitorPlugin)},
{new((string)App.LanguageDictionary["weather"], 5) { Author = "Stone_Red" }, typeof(WeatherPlugin)},
{new((string)App.LanguageDictionary["nextMeetingCountdown"], 6) { Author = "Stone_Red" }, typeof(NextMeetingCountdownPlugin)},
{new((string)App.LanguageDictionary["agenda"], 7) { Author = "Stone_Red" }, typeof(AgendaPlugin)},
};
// Window management
public List<IPluginWindow> PluginWindows { get; } = [];
public IReadOnlyDictionary<uint, InternalPluginData> Plugins => _plugins;
public bool BlockWindowsClosing { get; set; } = true;
// Edit mode tracking
private bool _isEditMode = false;
public bool IsEditMode => _isEditMode;
// Settings
public DesktopMagicSettings Settings { get; set; } = new();
public bool IsLoaded { get; set; } = false;
private readonly JsonSerializerOptions _jsonSettingsOptions = new()
{
Converters = { new ColorJsonConverter() }
};
// Events
public event Action? PluginsChanged;
public event Action? SettingsChanged;
public event Action<bool>? EditModeChanged;
private Manager()
{
// Private constructor for singleton
}
#region Plugin Management
public Dictionary<uint, InternalPluginData> GetPlugins() => new(_plugins);
public void LoadPlugins()
{
App.Logger.LogInfo("Loading plugins", source: "Manager");
_plugins.Clear();
// Load built-in plugins
foreach (PluginMetadata builtInPlugin in _builtInPlugins.Keys)
{
_plugins.Add(builtInPlugin.Id, new(builtInPlugin, PluginType.DotNet, string.Empty));
}
// Load external plugins
foreach (string directory in Directory.GetDirectories(App.PluginsPath))
{
string? pluginDllPath = Directory.GetFiles(directory, "main.dll").FirstOrDefault();
string? pluginHtmlPath = Directory.GetFiles(directory, "main.html").FirstOrDefault();
string? pluginMetadataPath = Directory.GetFiles(directory, "metadata.json").FirstOrDefault();
if (pluginDllPath is null && pluginHtmlPath is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no \"main.dll\" or \"main.html\"", source: "Manager");
continue;
}
if (pluginMetadataPath is null)
{
App.Logger.LogWarn($"Plugin \"{directory}\" has no \"metadata.json\"", source: "Manager");
continue;
}
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(File.ReadAllText(pluginMetadataPath));
if (pluginMetadata is null)
{
App.Logger.LogError($"Plugin \"{directory}\" has no valid \"metadata.json\"", source: "Manager");
continue;
}
if (_plugins.ContainsKey(pluginMetadata.Id))
{
App.Logger.LogError($"Plugin \"{directory}\" has the same id as another plugin", source: "Manager");
continue;
}
PluginType pluginType = pluginHtmlPath is not null ? PluginType.Web : PluginType.DotNet;
_plugins.Add(pluginMetadata.Id, new(pluginMetadata, pluginType, directory));
}
PluginsChanged?.Invoke();
App.Logger.LogInfo($"Loaded {_plugins.Count} plugins", source: "Manager");
}
public void LoadPlugin(uint pluginId, Action<InternalPluginData>? onPluginLoaded = null)
{
if (!_plugins.TryGetValue(pluginId, out InternalPluginData? internalPluginData))
{
return;
}
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
{
pluginSettings = new PluginSettings();
Settings.CurrentLayout.Plugins.Add(pluginId, pluginSettings);
}
IPluginWindow? existingWindow = PluginWindows.FirstOrDefault(w => w.PluginMetadata.Id == internalPluginData.Metadata.Id);
if (existingWindow is not null || !pluginSettings.Enabled)
{
// Close the window if it's already open or disabled
if (existingWindow is not null)
{
try
{
BlockWindowsClosing = false;
existingWindow.Close();
BlockWindowsClosing = true;
PluginWindows.Remove(existingWindow);
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "Manager");
}
}
return;
}
IPluginWindow window;
if (_builtInPlugins.TryGetValue(internalPluginData.Metadata, out Type? pluginType))
{
window = new PluginWindow((Api.Plugin)Activator.CreateInstance(pluginType)!, internalPluginData.Metadata, pluginSettings)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
else if (internalPluginData.Type == PluginType.Web)
{
window = new WebPluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
else
{
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
{
Title = internalPluginData.Metadata.Id.ToString()
};
}
Action? pluginLoadedHandler = null;
pluginLoadedHandler = () =>
{
onPluginLoaded?.Invoke(internalPluginData);
window.PluginLoaded -= pluginLoadedHandler;
};
Action exitHandler = () =>
{
PluginWindows.Remove(window);
BlockWindowsClosing = false;
window.Close();
BlockWindowsClosing = true;
pluginSettings.Enabled = false;
};
window.PluginLoaded += pluginLoadedHandler;
window.OnExit += exitHandler;
window.Show();
window.SetEditMode(_isEditMode);
PluginWindows.Add(window);
}
public void ReloadPlugins()
{
App.Logger.LogInfo("Reloading plugins", source: "PluginManager");
LoadPlugins();
LoadLayout(false);
}
public void SetEditMode(bool editMode)
{
_isEditMode = editMode;
foreach (IPluginWindow window in PluginWindows)
{
window.SetEditMode(editMode);
}
Application.Current.MainWindow.Topmost = editMode;
EditModeChanged?.Invoke(editMode);
SaveSettings();
}
#endregion
#region Settings Management
public void LoadSettings()
{
App.Logger.LogInfo("Loading settings", source: "Manager");
if (!File.Exists(Path.Combine(App.ApplicationDataPath, "settings.json")))
{
Settings = new DesktopMagicSettings();
Settings.Layouts.Add(new Layout("Default"));
Settings.Themes.Add(new Theme("Default"));
return;
}
string json = File.ReadAllText(Path.Combine(App.ApplicationDataPath, "settings.json"));
Settings = JsonSerializer.Deserialize<DesktopMagicSettings>(json, _jsonSettingsOptions) ?? new DesktopMagicSettings();
if (Settings.Layouts.Count == 0)
{
Settings.Layouts.Add(new Layout("Default"));
}
if (Settings.Themes.Count == 0)
{
Settings.Themes.Add(new Theme("Default"));
}
SettingsChanged?.Invoke();
}
public void SaveSettings()
{
if (!IsLoaded)
{
return;
}
string json = JsonSerializer.Serialize(Settings, _jsonSettingsOptions);
File.WriteAllText(Path.Combine(App.ApplicationDataPath, "settings.json"), json);
App.Logger.LogInfo("Settings saved", source: "Manager");
SettingsChanged?.Invoke();
}
#endregion
#region Layout Management
public void LoadLayout(bool minimize = true, Action? onComplete = null)
{
App.Logger.LogInfo("Loading layout", source: "Manager");
BlockWindowsClosing = false;
foreach (IPluginWindow window in PluginWindows)
{
window.Close();
}
BlockWindowsClosing = true;
PluginWindows.Clear();
bool showWindow = true;
// Load plugins
foreach (uint pluginId in _plugins.Keys)
{
InternalPluginData internalPluginData = _plugins[pluginId];
// Add plugin to layout if it doesn't exist
if (!Settings.CurrentLayout.Plugins.TryGetValue(pluginId, out PluginSettings? pluginSettings))
{
Settings.CurrentLayout.Plugins.Add(pluginId, new PluginSettings() { Metadata = internalPluginData.Metadata });
continue;
}
pluginSettings.Metadata = internalPluginData.Metadata;
if (pluginSettings.Enabled)
{
LoadPlugin(pluginId);
}
if (showWindow && pluginSettings.Enabled)
{
showWindow = false;
}
}
// Remove plugins that are not loaded anymore
List<uint> pluginIdsToRemove = Settings.CurrentLayout.Plugins.Keys.Where(id => !_plugins.ContainsKey(id)).ToList();
foreach (uint pluginId in pluginIdsToRemove)
{
Settings.CurrentLayout.Plugins.Remove(pluginId);
}
Settings.CurrentLayout.UpdatePlugins();
if (minimize && !showWindow)
{
Application.Current.MainWindow.WindowState = WindowState.Minimized;
Application.Current.MainWindow.ShowInTaskbar = false;
}
onComplete?.Invoke();
App.Logger.LogInfo("Layout loaded", source: "Manager");
}
#endregion
#region Cleanup
public void CloseAllPluginWindows()
{
foreach (IPluginWindow window in PluginWindows)
{
window.Hide();
}
}
#endregion
}
public class InternalPluginData(PluginMetadata pluginMetadata, PluginType pluginType, string directoryPath)
{
public PluginMetadata Metadata { get; set; } = pluginMetadata;
public PluginType Type { get; set; } = pluginType;
public string DirectoryPath { get; set; } = directoryPath;
}
public enum PluginType
{
DotNet,
Web
}
+87
View File
@@ -0,0 +1,87 @@
<Page x:Class="DesktopMagic.Pages.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DesktopMagic.Pages"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
d:DesignHeight="450" d:DesignWidth="800"
ScrollViewer.CanContentScroll="False"
Title="MainPage">
<Grid Margin="{StaticResource DefaultMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ui:Card Padding="5" Margin="0 0 0 5">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="10" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<ui:ToggleSwitch x:Name="editCheckBox" Height="30" VerticalAlignment="Center" Margin="5 0 0 0" Content="{DynamicResource editLayout}" Click="EditCheckBox_Click" />
<ui:TextBox Grid.Column="2" Text="{Binding PluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="AllPluginsSearchTextBox_TextChanged" PlaceholderText="{DynamicResource searchInstalledPlugins}" Icon="{ui:SymbolIcon Search24}" IconPlacement="Right" />
</Grid>
</ui:Card>
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel" Grid.Row="1" VerticalAlignment="Stretch" VerticalContentAlignment="Top">
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
<ui:CardExpander.Header>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ui:TextBlock Grid.Row="0" Grid.Column="0" FontTypography="Body" Text="{Binding Value.Metadata.Name}" />
<Label Grid.Row="1" Grid.Column="0" Foreground="{ui:ThemeResource TextFillColorTertiaryBrush}" FontSize="12" ContentStringFormat="{DynamicResource authorFormat}">
<Label.Style>
<Style TargetType="Label" BasedOn="{StaticResource {x:Type Label}}">
<Setter Property="Content" Value="{Binding Value.Metadata.Author, Converter={StaticResource UnderscoreEscapingConverter}}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Value.Metadata.Author}" Value="{x:Null}">
<Setter Property="Content" Value="{DynamicResource unknown}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Label.Style>
</Label>
<ui:ToggleSwitch Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Click="PluginCheckBox_Click" IsChecked="{Binding Value.Enabled}" Tag="{Binding Key}" Margin="0,0,16,0" VerticalAlignment="Center" />
</Grid>
</ui:CardExpander.Header>
</ui:CardExpander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<ui:Card Grid.Row="2" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<ComboBox x:Name="layoutsComboBox" ItemsSource="{Binding Settings.Layouts}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayoutName}" SelectedValuePath="Name" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Padding="4" SelectionChanged="LayoutsComboBox_SelectionChanged" />
<ui:Button x:Name="newLayoutButton" Content="{DynamicResource newLayout}" Grid.Column="2" Icon="{ui:SymbolIcon Add24}" HorizontalAlignment="Stretch" Click="NewLayoutButton_Click" FontWeight="Regular"/>
<ui:Button x:Name="removeLayoutButton" Content="{DynamicResource deleteLayout}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="RemoveLayoutButton_Click" FontWeight="Regular"/>
</Grid>
</ui:Card>
</Grid>
</Page>
+318
View File
@@ -0,0 +1,318 @@
using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace DesktopMagic.Pages;
/// <summary>
/// Interaction logic for MainPage.xaml
/// </summary>
public partial class MainPage : Page
{
private readonly Manager _manager = Manager.Instance;
private readonly MainWindowDataContext _dataContext;
private bool _isLoadingLayout = false;
public MainPage()
{
InitializeComponent();
_dataContext = new MainWindowDataContext
{
Settings = _manager.Settings
};
DataContext = _dataContext;
// Subscribe to manager events
_manager.PluginsChanged += OnPluginsChanged;
_manager.EditModeChanged += OnEditModeChanged;
Loaded += MainPage_Loaded;
Unloaded += MainPage_Unloaded;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
// Initialize edit checkbox state
editCheckBox.IsChecked = _manager.IsEditMode;
}
private void MainPage_Unloaded(object sender, RoutedEventArgs e)
{
// Unsubscribe from events
_manager.PluginsChanged -= OnPluginsChanged;
_manager.EditModeChanged -= OnEditModeChanged;
}
private void OnPluginsChanged()
{
Dispatcher.Invoke(() =>
{
_dataContext.Settings = _manager.Settings;
ApplyPluginsFilter();
});
}
private void OnEditModeChanged(bool editMode)
{
Dispatcher.Invoke(() =>
{
editCheckBox.IsChecked = editMode;
});
}
private void EditCheckBox_Click(object sender, RoutedEventArgs e)
{
_manager.SetEditMode(editCheckBox.IsChecked == true);
}
private void PluginCheckBox_Click(object sender, RoutedEventArgs e)
{
if (sender is not Control checkBox)
{
return;
}
uint pluginId = uint.Parse(checkBox.Tag.ToString()!);
_manager.LoadPlugin(pluginId, (internalPluginData) =>
{
Dispatcher.Invoke(() =>
{
// Update the card expander to show options when enabling a plugin
Wpf.Ui.Controls.CardExpander? cardExpander = ((checkBox.Parent as FrameworkElement)?.Parent) as Wpf.Ui.Controls.CardExpander;
if (cardExpander is not null)
{
OptionsCardExpander_Expanded(cardExpander, new RoutedEventArgs());
}
});
});
}
private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
if (sender is ScrollViewer scv)
{
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
}
#region Plugin Search
private void AllPluginsSearchTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
ApplyPluginsFilter();
}
private void ApplyPluginsFilter()
{
string? searchText = _dataContext.PluginsSearchText;
System.ComponentModel.ICollectionView view = CollectionViewSource.GetDefaultView(pluginsItemsControl.ItemsSource);
if (string.IsNullOrWhiteSpace(searchText))
{
view.Filter = null;
}
else
{
view.Filter = (item) =>
{
if (item is KeyValuePair<uint, PluginSettings> kvp)
{
return kvp.Value.Metadata.Name.Contains(searchText, StringComparison.OrdinalIgnoreCase);
}
return false;
};
}
}
#endregion
#region Layout Management
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
// Prevent recursive calls and only process if fully loaded
if (_isLoadingLayout || !_manager.IsLoaded || !IsLoaded)
{
return;
}
// Check if this is actually a user-initiated change
// by verifying that the removed and added items are different
if (e.RemovedItems.Count > 0 && e.AddedItems.Count > 0)
{
if (e.RemovedItems[0] == e.AddedItems[0])
{
return;
}
}
try
{
_isLoadingLayout = true;
_manager.SaveSettings();
_manager.LoadLayout(false);
}
finally
{
_isLoadingLayout = false;
}
}
private async void NewLayoutButton_Click(object sender, RoutedEventArgs e)
{
InputDialog inputDialog = new((string)FindResource("enterLayoutName"))
{
Owner = Window.GetWindow(this)
};
if (inputDialog.ShowDialog() == true)
{
if (_manager.Settings.Layouts.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("layoutAlreadyExists"),
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
try
{
_isLoadingLayout = true;
_manager.Settings.Layouts.Add(new Layout(inputDialog.ResponseText.Trim()));
_manager.Settings.CurrentLayoutName = inputDialog.ResponseText.Trim();
_manager.SaveSettings();
_manager.LoadLayout(false);
}
finally
{
_isLoadingLayout = false;
}
}
}
private async void RemoveLayoutButton_Click(object sender, RoutedEventArgs e)
{
if (_manager.Settings.Layouts.Count <= 1)
{
Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("cannotDeleteLastLayout"),
CloseButtonText = "Ok"
};
_ = await cannotDeleteMessageBox.ShowDialogAsync();
return;
}
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("confirmDeleteLayout"),
PrimaryButtonText = "Yes",
SecondaryButtonText = "No",
IsCloseButtonEnabled = false
};
Wpf.Ui.Controls.MessageBoxResult result = await messageBox.ShowDialogAsync();
if (result != Wpf.Ui.Controls.MessageBoxResult.Primary) // Primary is "Yes"
{
return;
}
try
{
_isLoadingLayout = true;
_ = _manager.Settings.Layouts.Remove(_manager.Settings.CurrentLayout);
_manager.SaveSettings();
_manager.LoadLayout(false);
}
finally
{
_isLoadingLayout = false;
}
}
#endregion
private void OptionsCardExpander_Expanded(object sender, RoutedEventArgs e)
{
if (sender is not Wpf.Ui.Controls.CardExpander expander || expander.Tag is not KeyValuePair<uint, PluginSettings> keyValuePair)
{
return;
}
PluginSettings? pluginSettings = keyValuePair.Value;
uint pluginId = keyValuePair.Key;
StackPanel optionsPanel = new StackPanel
{
Visibility = Visibility.Visible
};
expander.Content = optionsPanel;
optionsPanel.UpdateLayout();
// s.Input being null means the plugins has not been loaded yet but the settings are present in the saved configuration.
if (pluginSettings is null || pluginSettings.Settings.Count == 0 || pluginSettings.Settings.All(s => s.Input is null))
{
_ = optionsPanel.Children.Add(new System.Windows.Controls.TextBlock()
{
Text = (string)FindResource(pluginSettings?.Enabled == true ? "noOptions" : "enablePluginToConfigure")
});
return;
}
SettingElementGenerator settingElementGenerator = new SettingElementGenerator(pluginId);
foreach (SettingElement settingElement in pluginSettings.Settings)
{
Wpf.Ui.Controls.CardControl card = new()
{
Margin = new Thickness(0, 0, 0, 5),
Padding = new Thickness(5)
};
System.Windows.Controls.TextBlock textBlock = new()
{
Text = string.IsNullOrWhiteSpace(settingElement.Name) ? string.Empty : settingElement.Name,
Padding = new Thickness(0, 0, 3, 0),
VerticalAlignment = VerticalAlignment.Center,
FontWeight = FontWeights.SemiBold
};
card.Header = textBlock;
_ = optionsPanel.Children.Add(card);
FrameworkElement? control = settingElementGenerator.Generate(settingElement, textBlock);
if (control is not null)
{
control.MinWidth = 200;
control.MaxWidth = 200;
card.Content = control;
}
}
optionsPanel.UpdateLayout();
}
}
+52
View File
@@ -0,0 +1,52 @@
<Page x:Class="DesktopMagic.Pages.ThemePage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DesktopMagic.Pages"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
Title="ThemePage">
<Grid Margin="{StaticResource DefaultMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<ui:ListView x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" ItemsSource="{Binding Settings.Themes}" SelectedValue="{Binding Settings.CurrentLayout.CurrentThemeName}" SelectedValuePath="Name" SelectedIndex="2" SelectionMode="Single">
<ui:ListView.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Margin="8" VerticalAlignment="Center" FontWeight="SemiBold" Text="{Binding Name, Mode=OneWay}" />
<StackPanel Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal" Margin="{StaticResource DefaultMargin}">
<ui:Button x:Name="editThemeButton" Content="{DynamicResource editTheme}" Icon="{ui:SymbolIcon Color24}" HorizontalAlignment="Stretch" Click="EditThemeButton_Click" FontWeight="Regular" Tag="{Binding}">
</ui:Button>
</StackPanel>
</Grid>
</DataTemplate>
</ui:ListView.ItemTemplate>
</ui:ListView>
<ui:Card Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5 0 0" Padding="5">
<Grid >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="0.5*" />
</Grid.ColumnDefinitions>
<ui:Button x:Name="newThemeButton" Content="{DynamicResource newTheme}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="AddThemeButton_Click" FontWeight="Regular"/>
<ui:Button x:Name="deleteThemeButton" Content="{DynamicResource deleteTheme}" Icon="{ui:SymbolIcon Delete24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="DeleteThemeButton_Click" FontWeight="Regular"/>
</Grid>
</ui:Card>
</Grid>
</Page>
+126
View File
@@ -0,0 +1,126 @@
using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs;
using DesktopMagic.Plugins;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
namespace DesktopMagic.Pages;
/// <summary>
/// Interaction logic for ThemePage.xaml
/// </summary>
public partial class ThemePage : Page
{
private readonly Manager _manager = Manager.Instance;
private readonly MainWindowDataContext _dataContext;
public ThemePage()
{
InitializeComponent();
_dataContext = new MainWindowDataContext
{
Settings = _manager.Settings
};
DataContext = _dataContext;
// Subscribe to manager events
_manager.SettingsChanged += OnSettingsChanged;
Unloaded += ThemePage_Unloaded;
}
private void ThemePage_Unloaded(object sender, RoutedEventArgs e)
{
// Unsubscribe from events
_manager.SettingsChanged -= OnSettingsChanged;
}
private void OnSettingsChanged()
{
Dispatcher.Invoke(() =>
{
_dataContext.Settings = _manager.Settings;
});
}
private async void AddThemeButton_Click(object sender, RoutedEventArgs e)
{
InputDialog inputDialog = new((string)FindResource("enterThemeName"))
{
Owner = Window.GetWindow(this)
};
if (inputDialog.ShowDialog() == true)
{
if (_manager.Settings.Themes.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("themeAlreadyExists"),
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
return;
}
_manager.Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
_manager.Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
_manager.SaveSettings();
}
}
private async void DeleteThemeButton_Click(object sender, RoutedEventArgs e)
{
if (_manager.Settings.Themes.Count <= 1)
{
Wpf.Ui.Controls.MessageBox cannotDeleteMessageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("cannotDeleteLastTheme"),
CloseButtonText = "Ok"
};
_ = await cannotDeleteMessageBox.ShowDialogAsync();
return;
}
var messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = (string)FindResource("confirmDeleteTheme"),
PrimaryButtonText = "Yes",
SecondaryButtonText = "No",
IsCloseButtonEnabled = false
};
Wpf.Ui.Controls.MessageBoxResult result = await messageBox.ShowDialogAsync();
if (result != Wpf.Ui.Controls.MessageBoxResult.Primary) // Primary is "Yes"
{
return;
}
if (themesListBox.SelectedItem is Theme theme)
{
_ = _manager.Settings.Themes.Remove(theme);
_manager.SaveSettings();
}
}
private void EditThemeButton_Click(object sender, RoutedEventArgs e)
{
if (sender is not System.Windows.Controls.Button button || button.Tag is not Theme theme)
{
return;
}
ThemeDialog themeDialog = new(theme.Name, theme, App.AppName)
{
Owner = Window.GetWindow(this)
};
_ = themeDialog.ShowDialog();
}
}
+24
View File
@@ -0,0 +1,24 @@
using DesktopMagic.Plugins;
using System;
namespace DesktopMagic;
public interface IPluginWindow
{
event Action? PluginLoaded;
event Action? OnExit;
bool IsRunning { get; }
PluginMetadata PluginMetadata { get; }
string PluginFolderPath { get; }
string Title { get; set; }
void Exit();
void SetEditMode(bool enabled);
void Show();
void Hide();
void Close();
event System.ComponentModel.CancelEventHandler Closing;
}
+54 -2
View File
@@ -1,15 +1,19 @@
using DesktopMagic.Api;
using CuteUtils.Logging;
using DesktopMagic.Api;
using DesktopMagic.Settings;
using System.Drawing;
using Wpf.Ui.Controls;
namespace DesktopMagic.Plugins;
internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : IPluginData
{
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);
@@ -19,8 +23,56 @@ internal class PluginData(PluginWindow window, PluginSettings pluginSettings) :
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()
{
window.UpdatePluginWindow();
}
public void SaveState()
{
window.SavePluginState();
}
}
+33 -43
View File
@@ -2,8 +2,8 @@
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: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}"
@@ -12,52 +12,42 @@
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}"
Visibility="{Binding Visibility}">
<Grid Background="White" Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="auto" />
<RowDefinition Height="3*" />
</Grid.RowDefinitions>
<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>
<Border HorizontalAlignment="Center" Margin="10" 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" VerticalAlignment="Center">
<Label Content="{Binding Author}" 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">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<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" />
<Button Grid.Column="0" Command="{Binding InstallUninstallButtonData.Command}" IsEnabled="{Binding InstallUninstallButtonData.IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="10 5 5 0">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="{Binding InstallUninstallButtonData.IconKind}" Margin="0 0 10 0" />
<TextBlock Text="{Binding InstallUninstallButtonData.Text}" />
</StackPanel>
</Button>
<Button Grid.Column="1" Command="{Binding OpenButtonData.Command}" IsEnabled="{Binding OpenButtonData.IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="5 5 10 0">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="{Binding OpenButtonData.IconKind}" Margin="0 0 10 0" />
<TextBlock Text="{Binding OpenButtonData.Text}" />
</StackPanel>
</Button>
<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>
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="10">
<Label Content="{Binding Name}" FontSize="20" Padding="0" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</ui:Card>
</UserControl>
+45 -46
View File
@@ -1,25 +1,21 @@
<Window x:Class="DesktopMagic.Plugins.PluginManager"
<Page x:Class="DesktopMagic.Plugins.PluginManager"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns: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"
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:local="clr-namespace:DesktopMagic.Plugins"
xmlns:dataContexts="clr-namespace:DesktopMagic.DataContexts"
mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=dataContexts:PluginManagerDataContext}"
Title="{DynamicResource pluginManager}"
Height="450"
Width="800"
MinHeight="520"
MinWidth="700"
WindowStyle="ToolWindow"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}">
d:DesignHeight="450"
d:DesignWidth="800"
ScrollViewer.CanContentScroll="False"
Title="{DynamicResource pluginManager}">
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False">
<Grid Margin="5">
<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="{StaticResource DefaultMargin}">
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
@@ -28,21 +24,29 @@
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="5" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Label Content="{DynamicResource allPlugins}" 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">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<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" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top" Margin="0 0 2.5 0">
<ItemsControl ItemsSource="{Binding AllPlugins}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1">
<local:ModEntry />
</Border>
<local:ModEntry Margin="0 0 0 5" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Style>
@@ -64,17 +68,11 @@
</ScrollViewer>
</busyIndicator:BusyMask>
<Label Grid.Column="1" Content="{DynamicResource installedPlugins}" FontSize="20" />
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" materialDesign:HintAssist.Hint="Search..." materialDesign:TextFieldAssist.HasLeadingIcon="True" materialDesign:TextFieldAssist.LeadingIcon="Search" />
<ScrollViewer Grid.Row="2" Grid.Column="1" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding InstalledPlugins}">
<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>
<Border BorderBrush="LightGray" BorderThickness="1">
<local:ModEntry />
</Border>
<local:ModEntry Margin="0 0 0 5"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Style>
@@ -95,23 +93,24 @@
</ItemsControl>
</ScrollViewer>
<Border Grid.Row="3" Grid.ColumnSpan="2" Padding="0 5 0 0" BorderThickness="0 2 0 0" BorderBrush="DarkGray">
<DockPanel>
<Image Source="{StaticResource ModioLogoBlueDark}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
<Button DockPanel.Dock="Right" HorizontalAlignment="Right" Click="LogInButton_Click">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Login" Margin="0 0 10 0" />
<TextBlock Text="{Binding LoginButtonText}" />
</StackPanel>
</Button>
<Button DockPanel.Dock="Right" HorizontalAlignment="Right" Margin="0 0 10 0" Click="CreatePluginButton_Click">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Add" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource createNewPlugin}" />
</StackPanel>
</Button>
</DockPanel>
</Border>
<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>
<Image Source="{Binding ModIoIcon}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
<ui:Button Content="{DynamicResource createNewPlugin}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="CreatePluginButton_Click"/>
<ui:Button Content="{DynamicResource reloadPlugins}" Icon="{ui:SymbolIcon ArrowClockwise24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="ReloadPluginsButton_Click"/>
<ui:Button Content="{Binding LoginButtonText}" Icon="{ui:SymbolIcon PersonKey20}" Grid.Column="6" HorizontalAlignment="Stretch" Click="LogInButton_Click"/>
</Grid>
</ui:Card>
</Grid>
</busyIndicator:BusyMask>
</Window>
</Page>
+412 -144
View File
@@ -8,7 +8,6 @@ using Modio.Models;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
@@ -19,6 +18,7 @@ using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Xml.Linq;
@@ -30,7 +30,7 @@ namespace DesktopMagic.Plugins;
/// <summary>
/// Interaction logic for PluginManager.xaml
/// </summary>
public partial class PluginManager : Window
public partial class PluginManager : Page
{
private const int ModIoGameId = 5665;
private const string ModIoApiKey = "88e6ea774c3a502b06114e7fee0829ac";
@@ -38,6 +38,9 @@ public partial class PluginManager : Window
private readonly PluginManagerDataContext pluginManagerDataContext = new();
private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins");
private readonly string pluginDevelopmentPath = Path.Combine(App.ApplicationDataPath, "PluginDevelopment");
private readonly Manager _manager = Manager.Instance;
private bool changed = false;
private readonly DispatcherTimer searchTimer = new()
{
@@ -55,11 +58,13 @@ public partial class PluginManager : Window
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();
@@ -70,11 +75,115 @@ public partial class PluginManager : Window
searchTimer.Stop();
await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText);
};
Loaded += PluginManager_Loaded;
Unloaded += PluginManager_Unloaded;
}
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 = [];
pluginManagerDataContext.IsLoading = true;
pluginManagerDataContext.InstalledPlugins.Clear();
pluginManagerDataContext.AllPlugins.Clear();
foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
{
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
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;
}
try
{
Mod mod = await modIoClient.Games[ModIoGameId].Mods[pluginMetadata.Id].Get();
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);
try
{
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)
{
if (pluginIds.Contains(mod.Id))
{
continue;
}
pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
}
App.Logger.LogInfo($"Loaded {pluginManagerDataContext.AllPlugins.Count} available plugins", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to fetch plugins from mod.io: {ex.Message}", source: "PluginManager");
}
await SyncPlugins();
pluginManagerDataContext.IsLoading = false;
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
}
public async Task Remove(string pluginPath, uint id)
{
App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
changed = true;
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
@@ -83,10 +192,17 @@ public partial class PluginManager : Window
try
{
Directory.Delete(pluginPath, true);
App.Logger.LogInfo($"Successfully deleted plugin directory: {pluginPath}", source: "PluginManager");
}
catch (Exception ex)
{
_ = MessageBox.Show(ex.Message, "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
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");
}
}
@@ -94,71 +210,37 @@ public partial class PluginManager : Window
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
}
if (pluginManagerDataContext.IsAuthenticated)
{
await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
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;
}
protected override async void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
HashSet<uint> pluginIds = [];
foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
{
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
if (!File.Exists(pluginMetadataPath))
{
continue;
}
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(await File.ReadAllTextAsync(pluginMetadataPath));
if (pluginMetadata is not null)
{
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(pluginMetadata, new CommandHandler(async () => await Remove(pluginPath, pluginMetadata.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
_ = pluginIds.Add(pluginMetadata.Id);
}
}
Filter filter = ModFilter.Popular.Desc().Limit(100);
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods)
{
if (pluginIds.Contains(mod.Id))
{
continue;
}
pluginManagerDataContext.AllPlugins.Add(new PluginEntryDataContext(new(mod), new CommandHandler(async () => await Install(mod)), PluginEntryDataContext.Mode.Install));
}
await SyncPlugins();
pluginManagerDataContext.IsLoading = false;
}
protected override void OnClosing(CancelEventArgs e)
{
e.Cancel = pluginManagerDataContext.IsLoading;
}
[GeneratedRegex(@"[^a-zA-Z0-9]")]
private static partial Regex IdentifierNameRegex();
private async Task Install(Mod mod)
{
App.Logger.LogInfo($"Installing plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
changed = true;
if (mod.Modfile?.Download?.BinaryUrl is null)
{
App.Logger.LogWarn($"Plugin {mod.Id} has no download URL", source: "PluginManager");
return;
}
@@ -166,45 +248,77 @@ public partial class PluginManager : Window
string pluginPath = Path.Combine(pluginsPath, pluginGuid);
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");
}
using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
catch (Exception ex)
{
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");
await File.WriteAllTextAsync(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);
}
if (pluginManagerDataContext.IsAuthenticated)
{
await modIoClient.Games[ModIoGameId].Mods.Subscribe(mod.Id);
}
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(new PluginMetadata(mod), new CommandHandler(async () => await Remove(pluginPath, mod.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
pluginManagerDataContext.IsLoading = false;
}
@@ -217,6 +331,7 @@ public partial class PluginManager : Window
FileName = uri
};
_ = Process.Start(psi);
App.Logger.LogInfo($"Opened mod.io page: {uri}", source: "PluginManager");
}
private void AllPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
@@ -246,6 +361,7 @@ public partial class PluginManager : Window
private async Task SearchAllPlugins(string searchString)
{
App.Logger.LogInfo($"Searching plugins with query: {searchString}", source: "PluginManager");
pluginManagerDataContext.AllPlugins.Clear();
if (!searchString.Contains('*'))
@@ -255,38 +371,68 @@ public partial class PluginManager : Window
Filter filter = ModFilter.Name.Like($"{searchString}").And(ModFilter.Popular.Desc()).Limit(100);
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods)
try
{
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)), PluginEntryDataContext.Mode.Install));
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;
}
private void CreatePluginButton_Click(object sender, RoutedEventArgs e)
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
{
CreateNewPlugin();
await CreateNewPlugin();
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "PluginManager");
_ = MessageBox.Show(ex.Message, "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
}
}
private void CreateNewPlugin()
private async Task CreateNewPlugin()
{
App.Logger.LogInfo("Creating new plugin", source: "PluginManager");
if (!FileUtilities.ExistsOnPath("dotnet.exe"))
{
_ = MessageBox.Show("The .NET SDK is required to create a plugin. Please install it and try again.", "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
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;
}
@@ -294,22 +440,21 @@ public partial class PluginManager : Window
string pluginPath = Path.Combine(pluginsPath, pluginGuid);
uint pluginId = (uint)Random.Shared.Next(1000, 9999);
InputDialog inputDialog = new((string)FindResource("enterPluginName"), "Plugin Manager")
CreatePluginDialog inputDialog = new()
{
Owner = this,
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 = pluginName.ToLower().Replace("_", " ");
string pluginSafeName = GetPluginSafeName(pluginName);
TextInfo info = CultureInfo.CurrentCulture.TextInfo;
pluginSafeName = info.ToTitleCase(pluginSafeName);
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
App.Logger.LogInfo($"Creating plugin with name: {pluginName} (safe name: {pluginSafeName})", source: "PluginManager");
if (!Directory.Exists(pluginDevelopmentPath))
{
@@ -320,25 +465,44 @@ public partial class PluginManager : Window
if (Directory.Exists(pluginProjectPath))
{
_ = MessageBox.Show("A plugin with the same name already exists. Please choose a different name.", "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
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");
File.WriteAllText(pluginMetadataPath, JsonSerializer.Serialize(pluginMetadata));
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);
process.WaitForExit();
await process.WaitForExitAsync();
// Install the required NuGet packages
App.Logger.LogInfo("Installing NuGet package: DesktopMagic.Api", source: "PluginManager");
process = Process.Start("dotnet", $"add {pluginProjectPath} package DesktopMagic.Api");
process.WaitForExit();
await process.WaitForExitAsync();
File.Move(Path.Combine(pluginProjectPath, "Class1.cs"), Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"));
@@ -352,26 +516,21 @@ 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.Clear(Application.Theme.PrimaryColor); // Set the background color to the color specified in the DesktopMagic application.
g.DrawString(""Hello World"", new Font(Application.Theme.Font, 100), Brushes.Black, new PointF(0, 0)); // Draw ""Hello World"" to the image.
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.
}}
bmp.SetResolution(300, 300); // Set DPI to avoid scaling issues.
return bmp; // Return the image.
}}
}}
";
// Update the .csproj file
string csprojPath = Path.Combine(pluginProjectPath, $"{pluginSafeName}.csproj");
File.WriteAllText(Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"), code);
await File.WriteAllTextAsync(Path.Combine(pluginProjectPath, $"{pluginSafeName}.cs"), code);
XDocument doc = XDocument.Load(csprojPath);
XElement? propertyGroup = doc.Root?.Element("PropertyGroup");
@@ -412,62 +571,131 @@ public class {pluginSafeName}Plugin : Plugin
}
doc.Save(csprojPath);
App.Logger.LogInfo("Updated .csproj file with custom build settings", source: "PluginManager");
}
else
{
_ = MessageBox.Show("PropertyGroup element not found in .csproj", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
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;
}
// Open the project in the default IDE
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);
return;
}
else
{
App.Logger.LogInfo($"Opening plugin project in IDE: {associatedProgram}", source: "PluginManager");
ProcessStartInfo psi = new ProcessStartInfo
{
FileName = associatedProgram,
Arguments = csprojPath,
};
_ = Process.Start(psi);
}
ProcessStartInfo psi = new ProcessStartInfo
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?>>
{
FileName = associatedProgram,
Arguments = csprojPath,
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" }
};
_ = Process.Start(psi);
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 = this,
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 = this,
Owner = Window.GetWindow(this),
};
if (inputDialog.ShowDialog() != true)
{
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return;
}
@@ -478,11 +706,13 @@ public class {pluginSafeName}Plugin : Plugin
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)
@@ -490,10 +720,11 @@ public class {pluginSafeName}Plugin : Plugin
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(ex.Message, source: "PluginManager");
App.Logger.LogError($"Failed to subscribe to plugin {plugin.Id}: {ex.Message}", source: "PluginManager");
}
}
}
@@ -502,8 +733,14 @@ public class {pluginSafeName}Plugin : Plugin
}
catch (Exception ex)
{
App.Logger.LogError(ex.Message, source: "PluginManager");
_ = MessageBox.Show(ex.Message, "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error);
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;
@@ -516,33 +753,64 @@ public class {pluginSafeName}Plugin : Plugin
return;
}
IReadOnlyList<Mod> mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
App.Logger.LogInfo("Syncing plugins with mod.io subscriptions", source: "PluginManager");
List<PluginEntryDataContext> pluginsToRemove = [];
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
try
{
if (!plugin.IsLocalPlugin && !mods.Any(m => m.Id == plugin.Id))
{
pluginsToRemove.Add(plugin);
}
}
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");
foreach (PluginEntryDataContext plugin in pluginsToRemove)
{
if (plugin.Path is null)
List<PluginEntryDataContext> pluginsToRemove = [];
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{
continue;
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);
}
}
await Remove(plugin.Path, plugin.Id);
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");
}
IEnumerable<Mod> notInstalledMods = mods.Where(m => !pluginManagerDataContext.InstalledPlugins.Any(p => p.Id == m.Id));
foreach (Mod mod in notInstalledMods)
catch (Exception ex)
{
await Install(mod);
App.Logger.LogError($"Plugin sync failed: {ex.Message}", source: "PluginManager");
}
}
private static string GetPluginSafeName(string pluginName)
{
string pluginSafeName = pluginName.ToLower().Replace("_", " ");
TextInfo info = CultureInfo.CurrentCulture.TextInfo;
pluginSafeName = info.ToTitleCase(pluginSafeName);
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
return pluginSafeName;
}
private string GetCsprojPath(string pluginName)
{
string pluginSafeName = GetPluginSafeName(pluginName);
return Path.Combine(pluginDevelopmentPath, pluginSafeName, $"{pluginSafeName}.csproj");
}
}
@@ -1,6 +1,8 @@
using Modio.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace DesktopMagic.Plugins;
@@ -27,6 +29,14 @@ public class PluginMetadata
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)
{
Name = mod.Name ?? mod.Id.ToString();
@@ -39,6 +49,7 @@ public class PluginMetadata
Description = mod.DescriptionPlaintext;
Summary = mod.Summary;
Version = mod.Modfile?.Version;
Tags = mod.Tags.Select(t => t.Name).Where(t => t is not null).ToList()!;
}
public PluginMetadata(string name, uint id)
+16 -12
View File
@@ -1,6 +1,7 @@
<Window x:Class="DesktopMagic.PluginWindow"
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"
@@ -8,25 +9,28 @@
Title="PluginWindow" 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">
<Grid>
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
<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 SizeChanged="Border_SizeChanged" x:Name="border" />
<Viewbox SizeChanged="ViewBox_SizeChanged" x:Name="viewBox" StretchDirection="Both" Stretch="Uniform">
<Viewbox.Clip>
<RectangleGeometry x:Name="rectangleGeometry" RadiusX="{Binding ElementName=border, Path=CornerRadius.TopLeft}" RadiusY="{Binding ElementName=border, Path=CornerRadius.TopLeft}" />
</Viewbox.Clip>
<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" />
</Border>
</Viewbox>
</Grid>
<Border SizeChanged="Border_SizeChanged" x:Name="border" />
<Viewbox SizeChanged="ViewBox_SizeChanged" x:Name="viewBox" StretchDirection="Both" Stretch="Uniform">
<Viewbox.Clip>
<RectangleGeometry x:Name="rectangleGeometry" RadiusX="{Binding ElementName=border, Path=CornerRadius.TopLeft}" RadiusY="{Binding ElementName=border, Path=CornerRadius.TopLeft}" />
</Viewbox.Clip>
<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" />
</Border>
</Viewbox>
</Grid>
</busyIndicator:BusyMask>
<WindowChrome.WindowChrome>
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
+588 -52
View File
@@ -1,6 +1,7 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Drawing;
using DesktopMagic.Api.Settings;
using DesktopMagic.DataContexts;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
@@ -12,7 +13,10 @@ using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using System.Timers;
using System.Windows;
using System.Windows.Interop;
@@ -21,7 +25,7 @@ using System.Windows.Media.Imaging;
namespace DesktopMagic;
public partial class PluginWindow : Window
public partial class PluginWindow : Window, IPluginWindow
{
public event Action? PluginLoaded;
@@ -31,6 +35,15 @@ public partial class PluginWindow : Window
private Thread? pluginThread;
private System.Timers.Timer? updateTimer;
private Plugin? pluginClassInstance;
private AssemblyLoadContext assemblyLoadContext;
private CancellationTokenSource? pluginCancellationTokenSource;
private FileSystemWatcher? pluginFileWatcher;
private System.Timers.Timer? reloadDebounceTimer;
private bool isReloading = false;
private WriteableBitmap? writeableBitmap;
private BitmapScalingMode lastBitmapScalingMode = BitmapScalingMode.Unspecified;
public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; }
@@ -56,7 +69,22 @@ public partial class PluginWindow : Window
Owner = w;
settings.Theme.PropertyChanged += (e, s) => ThemeChanged();
settings.PropertyChanged += (e, s) =>
{
if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
{
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
ThemeChanged();
}
};
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
PluginMetadata = pluginMetadata;
this.settings = settings;
@@ -67,6 +95,14 @@ public partial class PluginWindow : Window
Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath;
assemblyLoadContext = CreateAssemblyLoadContext();
// Initialize hot reload watcher if plugin supports unloading and is external
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
{
InitializeHotReload();
}
}
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty)
@@ -74,12 +110,190 @@ public partial class PluginWindow : Window
this.pluginClassInstance = pluginClassInstance;
}
private AssemblyLoadContext CreateAssemblyLoadContext()
{
AssemblyLoadContext context = new(PluginMetadata.Name, isCollectible: true);
context.Resolving += (ctx, assemblyName) =>
{
string assemblyPath = Path.Combine(PluginFolderPath, assemblyName.Name + ".dll");
if (File.Exists(assemblyPath))
{
return ctx.LoadFromAssemblyPath(assemblyPath);
}
else
{
_ = ctx.LoadFromAssemblyName(assemblyName);
}
return null;
};
return context;
}
private void InitializeHotReload()
{
try
{
pluginFileWatcher = new FileSystemWatcher(PluginFolderPath)
{
Filter = "main.dll",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = false
};
pluginFileWatcher.Changed += OnPluginFileChanged;
reloadDebounceTimer = new System.Timers.Timer(500); // 500ms debounce
reloadDebounceTimer.Elapsed += async (s, e) =>
{
reloadDebounceTimer!.Stop();
await ReloadPlugin();
};
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "Plugin");
}
}
private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
{
if (isReloading)
{
return;
}
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin file changed, scheduling reload", source: "Plugin");
// Debounce multiple file change events
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Start();
}
private async Task StopPlugin(bool unloadAssembly = true)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
try
{
pluginCancellationTokenSource?.Cancel();
if (pluginClassInstance is AsyncPlugin asyncPluginStop)
{
try
{
await asyncPluginStop.StopAsync(pluginCancellationTokenSource?.Token ?? CancellationToken.None);
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping async plugin: {ex.Message}", source: "Plugin");
}
}
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
pluginClassInstance.Stop();
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping plugin: {ex}", source: "Plugin");
}
if (unloadAssembly)
{
assemblyLoadContext.Unload();
}
pluginCancellationTokenSource?.Dispose();
pluginCancellationTokenSource = null;
}
private async Task ReloadPlugin()
{
if (isReloading || !IsRunning)
{
return;
}
isReloading = true;
try
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading plugin", source: "Plugin");
// Stop the file watcher during reload
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = false;
}
updateTimer?.Stop();
await StopPlugin(unloadAssembly: true);
// Wait for file to be fully written and released
await Task.Delay(200);
// Clear the current instance
pluginClassInstance = null;
// Create a new AssemblyLoadContext since the old one was unloaded
assemblyLoadContext = CreateAssemblyLoadContext();
// Show busy indicator
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = true);
// Reload the plugin
await ExecuteSource();
// Hide busy indicator
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin reloaded successfully", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload plugin: {ex}", source: "Plugin");
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Reload Error \"{PluginMetadata.Name}\"",
Content = $"Failed to reload plugin:\n{ex.Message}",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
}
finally
{
isReloading = false;
// Re-enable file watcher
if (pluginFileWatcher != null && IsRunning)
{
pluginFileWatcher.EnableRaisingEvents = true;
}
}
}
public void UpdatePluginWindow()
{
Dispatcher.Invoke(ThemeChanged);
UpdateTimer_Elapsed(updateTimer, null);
}
public void SavePluginState()
{
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
}
}
public void Exit()
{
IsRunning = false;
@@ -108,9 +322,8 @@ public partial class PluginWindow : Window
panel.Visibility = Visibility.Collapsed;
imageBorder.BorderThickness = new Thickness(0);
image.Margin = new Thickness(0);
WindowPos.SendWpfWindowBack(this);
WindowPos.SendWpfWindowBack(this);
WindowPos.SetIsLocked(this, true);
WindowPos.SetWindowLayer(this, pluginClassInstance?.windowLayer.Value ?? "Always on Bottom");
WindowPos.SetIsLocked(window, true);
tileBar.CaptionHeight = 0;
ResizeMode = ResizeMode.NoResize;
}
@@ -126,44 +339,78 @@ public partial class PluginWindow : Window
WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE);
}
private static BitmapSource BitmapToImageSource(Bitmap bitmap)
private void UpdateImageFromBitmap(Bitmap bitmap, BitmapScalingMode scalingMode)
{
BitmapData bitmapData = bitmap.LockBits(
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly, bitmap.PixelFormat);
BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
BitmapSource bitmapSource = BitmapSource.Create(
bitmapData.Width, bitmapData.Height,
bitmap.HorizontalResolution, bitmap.VerticalResolution,
PixelFormats.Bgra32, null,
bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride);
try
{
Dispatcher.Invoke(() =>
{
if (lastBitmapScalingMode != scalingMode)
{
RenderOptions.SetBitmapScalingMode(image, scalingMode);
lastBitmapScalingMode = scalingMode;
}
bitmap.UnlockBits(bitmapData);
return bitmapSource;
if (writeableBitmap == null || writeableBitmap.PixelWidth != bitmapData.Width || writeableBitmap.PixelHeight != bitmapData.Height)
{
writeableBitmap = new WriteableBitmap(
bitmapData.Width, bitmapData.Height,
bitmap.HorizontalResolution, bitmap.VerticalResolution,
PixelFormats.Bgra32, null);
image.Source = writeableBitmap;
}
writeableBitmap.Lock();
try
{
unsafe
{
Buffer.MemoryCopy(
bitmapData.Scan0.ToPointer(),
writeableBitmap.BackBuffer.ToPointer(),
writeableBitmap.BackBufferStride * writeableBitmap.PixelHeight,
bitmapData.Stride * bitmapData.Height);
}
writeableBitmap.AddDirtyRect(new Int32Rect(0, 0, bitmapData.Width, bitmapData.Height));
}
finally
{
writeableBitmap.Unlock();
}
});
}
finally
{
bitmap.UnlockBits(bitmapData);
}
}
private void Window_ContentRendered(object? sender, EventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting plugin thread", source: "Plugin");
pluginThread = new Thread(LoadPlugin);
pluginThread = new Thread(async () => await LoadPlugin());
pluginThread.Start();
}
private void ThemeChanged()
{
viewBox.Margin = new Thickness(settings.Theme.Margin);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
}
private void LoadPlugin()
private async Task LoadPlugin()
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin", source: "Plugin");
if (pluginClassInstance is null && !File.Exists($"{PluginFolderPath}\\main.dll"))
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.dll\" does not exist", source: "Plugin");
_ = MessageBox.Show("File \"main.dll\" does not exist!", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File \"main.dll\" does not exist!",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit();
return;
@@ -171,31 +418,84 @@ public partial class PluginWindow : Window
try
{
ExecuteSource();
await ExecuteSource();
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit();
return;
}
PluginLoaded?.Invoke();
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
// Enable hot reload after initial load
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = true;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload monitoring started", source: "Plugin");
}
}
private void ExecuteSource()
private void ThemeChanged()
{
viewBox.Margin = new Thickness(settings.Theme.Margin);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
pluginClassInstance?.OnThemeChanged();
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
{
pluginClassInstance.Application.UpdateWindow();
}
}
private async Task ExecuteSource()
{
object? instance = pluginClassInstance;
if (instance is null)
{
Assembly dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll");
Assembly dll;
if (PluginMetadata.SupportsUnloading)
{
byte[] assemblyData = await File.ReadAllBytesAsync($"{PluginFolderPath}\\main.dll");
using MemoryStream assemblyStream = new(assemblyData);
dll = assemblyLoadContext.LoadFromStream(assemblyStream);
}
else
{
dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll");
}
Type? instanceType = Array.Find(dll.GetTypes(), type => type.GetTypeInfo().BaseType == typeof(Plugin));
if (instanceType is null)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
_ = MessageBox.Show($"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = $"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit();
return;
}
@@ -205,17 +505,27 @@ public partial class PluginWindow : Window
if (instance is Plugin plugin)
{
pluginClassInstance = plugin;
pluginClassInstance.Application = new Plugins.PluginData(this, settings);
pluginClassInstance.Application = new PluginData(this, settings);
}
else
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
_ = MessageBox.Show($"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = $"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit();
return;
}
LoadOptions(pluginClassInstance);
LoadState(pluginClassInstance);
await LoadOptions(pluginClassInstance);
BindDefaultSettings(pluginClassInstance);
updateTimer = new System.Timers.Timer
@@ -225,7 +535,14 @@ public partial class PluginWindow : Window
updateTimer.Elapsed += UpdateTimer_Elapsed;
pluginClassInstance.Start();
if (pluginClassInstance is AsyncPlugin asyncPluginStart)
{
pluginCancellationTokenSource?.Dispose();
pluginCancellationTokenSource = new CancellationTokenSource();
await asyncPluginStart.StartAsync(pluginCancellationTokenSource.Token);
}
UpdatePluginWindow();
await Dispatcher.InvokeAsync(ThemeChanged);
if (pluginClassInstance.UpdateInterval > 0)
{
@@ -240,9 +557,19 @@ public partial class PluginWindow : Window
{
SetHorizontalAlignment();
SetVerticalAlignment();
SetWindowLayer();
SetRotation();
SetThemeOverride();
SetThemeOverrideItems();
pluginClassInstance.horizontalAlignment.OnValueChanged += SetHorizontalAlignment;
pluginClassInstance.verticalAlignment.OnValueChanged += SetVerticalAlignment;
pluginClassInstance.windowLayer.OnValueChanged += SetWindowLayer;
pluginClassInstance.rotation.OnValueChanged += SetRotation;
pluginClassInstance.themeOverride.OnValueChanged += SetThemeOverride;
DesktopMagicSettings desktopMagicSettings = MainWindowDataContext.GetSettings();
desktopMagicSettings.Themes.CollectionChanged += (s, e) => SetThemeOverrideItems();
});
void SetVerticalAlignment()
@@ -272,18 +599,50 @@ public partial class PluginWindow : Window
viewBox.HorizontalAlignment = horizontalAlignment;
border.HorizontalAlignment = horizontalAlignment;
}
void SetWindowLayer()
{
WindowPos.SetWindowLayer(this, pluginClassInstance.windowLayer.Value);
}
void SetRotation()
{
image.LayoutTransform = new RotateTransform(pluginClassInstance.rotation.Value);
}
void SetThemeOverride()
{
if (pluginClassInstance.themeOverride.Value == "<None>")
{
settings.CurrentThemeName = null;
}
else
{
settings.CurrentThemeName = pluginClassInstance.themeOverride.Value;
}
}
void SetThemeOverrideItems()
{
DesktopMagicSettings desktopMagicSettings = MainWindowDataContext.GetSettings();
pluginClassInstance.themeOverride.Items.Clear();
pluginClassInstance.themeOverride.Items.Add("<None>");
foreach (Theme theme in desktopMagicSettings.Themes)
{
pluginClassInstance.themeOverride.Items.Add(theme.Name);
}
}
}
private void LoadOptions(object instance)
private async Task LoadOptions(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin options", source: "Plugin");
try
{
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
FieldInfo[] props = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.GetField);
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
List<SettingElement> settingElements = [];
foreach (FieldInfo prop in props)
{
@@ -302,6 +661,16 @@ public partial class PluginWindow : Window
settingElement.JsonValue = settingsSettingElement.JsonValue;
}
element.OnValueChanged += () =>
{
pluginClassInstance?.OnSettingsChanged();
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
{
pluginClassInstance.Application.UpdateWindow();
}
};
settingElements.Add(settingElement);
break;
}
@@ -315,18 +684,167 @@ public partial class PluginWindow : Window
{
IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit();
}
}
private void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e)
private void LoadState(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin state", source: "Plugin");
try
{
if (settings.State.Count == 0)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - No state found, using defaults", source: "Plugin");
return;
}
FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
// Load fields
foreach (FieldInfo field in fields)
{
PersistStateAttribute? persistAttribute = field.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null)
{
continue;
}
if (settings.State.TryGetValue(field.Name, out JsonElement jsonElement))
{
try
{
object? value = jsonElement.Deserialize(field.FieldType);
field.SetValue(instance, value);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state field: {field.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore field {field.Name}: {ex.Message}", source: "Plugin");
}
}
}
// Load properties
foreach (PropertyInfo property in properties)
{
PersistStateAttribute? persistAttribute = property.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null || !property.CanWrite)
{
continue;
}
if (settings.State.TryGetValue(property.Name, out JsonElement jsonElement))
{
try
{
object? value = jsonElement.Deserialize(property.PropertyType);
property.SetValue(instance, value);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Restored state property: {property.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to restore property {property.Name}: {ex.Message}", source: "Plugin");
}
}
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error loading state: {ex.Message}", source: "Plugin");
}
}
private void SaveState(object instance)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saving plugin state", source: "Plugin");
try
{
FieldInfo[] fields = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
PropertyInfo[] properties = instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
// Save fields
foreach (FieldInfo field in fields)
{
PersistStateAttribute? persistAttribute = field.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null)
{
continue;
}
try
{
object? value = field.GetValue(instance);
JsonElement jsonElement = JsonSerializer.SerializeToElement(value, field.FieldType);
settings.State[field.Name] = jsonElement;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state field: {field.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save field {field.Name}: {ex.Message}", source: "Plugin");
}
}
// Save properties
foreach (PropertyInfo property in properties)
{
PersistStateAttribute? persistAttribute = property.GetCustomAttribute<PersistStateAttribute>();
if (persistAttribute is null || !property.CanRead)
{
continue;
}
try
{
object? value = property.GetValue(instance);
JsonElement jsonElement = JsonSerializer.SerializeToElement(value, property.PropertyType);
settings.State[property.Name] = jsonElement;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Saved state property: {property.Name}", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to save property {property.Name}: {ex.Message}", source: "Plugin");
}
}
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - State saved successfully ({settings.State.Count} items)", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error saving state: {ex.Message}", source: "Plugin");
}
}
private async void UpdateTimer_Elapsed(object? sender, ElapsedEventArgs? e)
{
try
{
if (IsRunning && pluginClassInstance is not null)
{
Bitmap? result = pluginClassInstance.Main();
Bitmap? result;
if (pluginClassInstance is AsyncPlugin asyncPlugin)
{
CancellationToken token = pluginCancellationTokenSource?.Token ?? CancellationToken.None;
result = await asyncPlugin.MainAsync(token);
}
else
{
result = pluginClassInstance.Main();
}
if (pluginClassInstance.UpdateInterval > 0)
{
@@ -347,12 +865,7 @@ public partial class PluginWindow : Window
_ => BitmapScalingMode.Unspecified
};
//Update Image
Dispatcher.Invoke(() =>
{
RenderOptions.SetBitmapScalingMode(image, renderOptions);
image.Source = BitmapToImageSource(result);
});
UpdateImageFromBitmap(result, renderOptions);
}
}
}
@@ -360,7 +873,16 @@ public partial class PluginWindow : Window
{
IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error);
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit();
return;
}
@@ -384,9 +906,23 @@ public partial class PluginWindow : Window
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
IsRunning = false;
pluginClassInstance?.Stop();
// Cleanup hot reload resources
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = false;
pluginFileWatcher.Changed -= OnPluginFileChanged;
pluginFileWatcher.Dispose();
pluginFileWatcher = null;
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Dispose();
reloadDebounceTimer = null;
// Stop plugin using the shared method (no need to await in synchronous event handler)
_ = StopPlugin(unloadAssembly: true);
}
#region Window Events
+12 -2
View File
@@ -11,17 +11,27 @@ using Color = System.Drawing.Color;
namespace DesktopMagic.Plugins;
public class Theme : ITheme, INotifyPropertyChanged
public class Theme(string name) : ITheme, INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private Color primaryColor = Color.White;
private Color secondaryColor = Color.White;
private Color secondaryColor = Color.LightGray;
private Color backgroundColor = Color.Transparent;
private string font = "Segoe UI";
private int cornerRadius;
private int margin;
public string Name
{
get => name;
set
{
name = value;
OnPropertyChanged();
}
}
public Color PrimaryColor
{
get => primaryColor;
@@ -0,0 +1,40 @@
<Window x:Class="DesktopMagic.WebPluginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopMagic"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
Title="WebPluginWindow" Height="450" Width="800"
Background="Transparent"
WindowStyle="None"
ShowInTaskbar="False"
AllowsTransparency="True"
LocationChanged="Window_LocationChanged"
SizeChanged="Window_SizeChanged"
Closing="Window_Closing"
ContentRendered="Window_ContentRendered"
x:Name="window">
<busyIndicator:BusyMask x:Name="busyMask" IsBusy="True" Background="Transparent" BusyContent="" BusyContentMargin="-5" IndicatorType="Cogs">
<Grid>
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
<Border x:Name="border" Background="Transparent" CornerRadius="10">
<Grid>
<wv2:WebView2 x:Name="webView" DefaultBackgroundColor="Transparent" CoreWebView2InitializationCompleted="WebView_CoreWebView2InitializationCompleted" />
<!--
WORKAROUND: This rectangle exists to allow interaction with the WebView2 when the window is transparent
https://github.com/MicrosoftEdge/WebView2Feedback/issues/997
-->
<Rectangle Fill="#01000000" />
</Grid>
</Border>
</Grid>
</busyIndicator:BusyMask>
<WindowChrome.WindowChrome>
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
</Window>
@@ -0,0 +1,633 @@
using DesktopMagic.Api.Settings;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using Microsoft.Web.WebView2.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
namespace DesktopMagic;
public partial class WebPluginWindow : Window, IPluginWindow
{
public event Action? PluginLoaded;
public event Action? OnExit;
private readonly PluginSettings settings;
private bool isInitialized = false;
private FileSystemWatcher? fileWatcher;
private System.Timers.Timer? reloadDebounceTimer;
private bool isReloading = false;
public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; }
public string PluginFolderPath { get; private set; }
public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath)
{
InitializeComponent();
Window w = new()
{
Top = -100,
Left = -100,
Width = 0,
Height = 0,
WindowStyle = WindowStyle.ToolWindow,
ShowInTaskbar = false
};
WindowInteropHelper helper = new WindowInteropHelper(w);
_ = helper.EnsureHandle();
Owner = w;
settings.PropertyChanged += (e, s) =>
{
if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
{
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
ThemeChanged();
}
};
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
PluginMetadata = pluginMetadata;
this.settings = settings;
Left = settings.Position.X;
Top = settings.Position.Y;
Width = settings.Size.X;
Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath;
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
{
InitializeHotReload();
}
}
public void Exit()
{
IsRunning = false;
Dispatcher.Invoke(() =>
{
OnExit?.Invoke();
});
}
public void SetEditMode(bool enabled)
{
if (enabled)
{
Topmost = true;
panel.Visibility = Visibility.Visible;
_ = W32.EnableWindow(webView.Handle, false);
WindowPos.SetIsLocked(this, false);
tileBar.CaptionHeight = tileBar.CaptionHeight = ActualHeight - 10 < 0 ? 0 : ActualHeight - 10;
ResizeMode = ResizeMode.CanResize;
}
else
{
Topmost = false;
panel.Visibility = Visibility.Collapsed;
_ = W32.EnableWindow(webView.Handle, true);
WindowPos.SendWpfWindowBack(this);
WindowPos.SendWpfWindowBack(this);
WindowPos.SetIsLocked(this, true);
tileBar.CaptionHeight = 0;
ResizeMode = ResizeMode.NoResize;
}
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
WindowInteropHelper helper = new(this);
_ = WindowPos.SetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE,
WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE);
}
private async void Window_ContentRendered(object? sender, EventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting web plugin", source: "WebPlugin");
try
{
await InitializeWebView();
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "WebView2 initialization error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
Exit();
}
}
private void ThemeChanged()
{
webView.Margin = new Thickness(settings.Theme.Margin);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
string cssVariables = $@"
:root {{
--background-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.BackgroundColor)};
--primary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
--secondary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.SecondaryColor)};
--font-family: {settings.Theme.Font};
font-family: {settings.Theme.Font};
color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
}}
";
string script = $@"
(function() {{
let style = document.getElementById('desktop-magic-theme');
if (!style) {{
style = document.createElement('style');
style.id = 'desktop-magic-theme';
document.head.appendChild(style);
}}
style.textContent = `{cssVariables}`;
}})();
";
_ = webView.ExecuteScriptAsync(script);
}
private async System.Threading.Tasks.Task InitializeWebView()
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Initializing WebView2", source: "WebPlugin");
string htmlPath = Path.Combine(PluginFolderPath, "main.html");
if (!File.Exists(htmlPath))
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.html\" does not exist", source: "WebPlugin");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File \"main.html\" does not exist!",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
Exit();
return;
}
try
{
LoadWebPluginSettings();
string userDataFolder = Path.Combine(Path.GetTempPath(), "DesktopMagic", "WebView2", PluginMetadata.Id.ToString());
CoreWebView2Environment environment = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
await webView.EnsureCoreWebView2Async(environment);
webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
webView.CoreWebView2.Settings.AreDevToolsEnabled = false;
webView.CoreWebView2.Settings.IsStatusBarEnabled = false;
webView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
_ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.Source = new Uri(htmlUri);
isInitialized = true;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - WebView2 initialized successfully", source: "WebPlugin");
PluginLoaded?.Invoke();
busyMask.IsBusy = false;
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - WebView2 initialization failed: {ex}", source: "WebPlugin");
throw;
}
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping web plugin", source: "WebPlugin");
IsRunning = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
fileWatcher.Dispose();
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Dispose();
try
{
if (isInitialized && webView.CoreWebView2 != null)
{
webView.Dispose();
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
}
}
private void Window_LocationChanged(object sender, EventArgs e)
{
settings.Position = new System.Windows.Point(Left, Top);
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
settings.Size = new System.Windows.Point(Width, Height);
tileBar.CaptionHeight = ActualHeight - 10;
}
private bool firstLoad = true;
private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
webView.CoreWebView2.DOMContentLoaded += (_, _) =>
{
if (firstLoad)
{
firstLoad = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = true;
}
}
ThemeChanged();
};
}
private void LoadWebPluginSettings()
{
string settingsPath = Path.Combine(PluginFolderPath, "settings.json");
if (!File.Exists(settingsPath))
{
return;
}
try
{
string json = File.ReadAllText(settingsPath);
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
if (root.ValueKind != JsonValueKind.Array)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - settings.json must be a JSON array", source: "WebPlugin");
return;
}
List<SettingElement> settingElements = [];
int orderIndex = 0;
foreach (JsonElement element in root.EnumerateArray())
{
string id = element.GetProperty("id").GetString() ?? $"setting-{orderIndex}";
string name = element.TryGetProperty("name", out JsonElement nameElement) ? nameElement.GetString() ?? id : id;
string type = element.TryGetProperty("type", out JsonElement typeElement) ? typeElement.GetString() ?? "textbox" : "textbox";
Setting? setting = CreateSetting(type, element);
if (setting is null)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Unknown setting type \"{type}\" for \"{id}\"", source: "WebPlugin");
continue;
}
SettingElement settingElement = new(setting, id, name, orderIndex);
if (settings.Settings.Exists(e => e.Id == id))
{
SettingElement saved = settings.Settings.First(e => e.Id == id);
settingElement.JsonValue = saved.JsonValue;
}
string capturedId = id;
if (setting is Button button)
{
button.OnClick += () =>
{
_ = webView.Dispatcher.InvokeAsync(async () =>
{
try
{
_ = await webView.ExecuteScriptAsync($"window.desktopMagic?.onClick?.({JsonSerializer.Serialize(capturedId)})");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify button click for \"{capturedId}\": {ex}", source: "WebPlugin");
}
});
};
}
setting.OnValueChanged += () =>
{
_ = webView.Dispatcher.InvokeAsync(async () =>
{
try
{
await NotifySettingChange(capturedId, setting);
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify setting change for \"{capturedId}\": {ex}", source: "WebPlugin");
}
});
};
settingElements.Add(settingElement);
orderIndex++;
}
settings.Settings = [.. settingElements.OrderBy(x => x.OrderIndex)];
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to load settings from settings.json: {ex.Message}", source: "WebPlugin");
}
}
private string GetSettingsBridgeScript()
{
string settingsJson = SerializeSettingsToJson();
return $@"
(function() {{
window.desktopMagic = {{}};
window.desktopMagic._settings = {settingsJson};
window.desktopMagic.getSettings = function() {{
return JSON.parse(JSON.stringify(window.desktopMagic._settings));
}};
window.desktopMagic.getSetting = function(id) {{
return window.desktopMagic._settings ? window.desktopMagic._settings[id] : undefined;
}};
window.desktopMagic.onSettingChanged = null;
window.desktopMagic.onButtonClick = null;
window.desktopMagic.dispatchSettingChanged = function(id, value) {{
if (window.desktopMagic._settings) {{
window.desktopMagic._settings[id] = value;
}}
if (typeof window.desktopMagic.onSettingChanged === 'function') {{
window.desktopMagic.onSettingChanged(id, value);
}}
}};
window.desktopMagic.onClick = function(id) {{
if (typeof window.desktopMagic.onButtonClick === 'function') {{
window.desktopMagic.onButtonClick(id);
}}
}};
}})();
";
}
private void InitializeHotReload()
{
try
{
fileWatcher = new FileSystemWatcher(PluginFolderPath)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = false
};
fileWatcher.Changed += OnPluginFileChanged;
reloadDebounceTimer = new System.Timers.Timer(500)
{
AutoReset = false
};
reloadDebounceTimer.Elapsed += async (s, e) =>
{
await ReloadWebPlugin();
};
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "WebPlugin");
}
}
private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
{
if (isReloading)
{
return;
}
string? fileName = e.Name;
if (string.IsNullOrEmpty(fileName))
{
return;
}
string ext = Path.GetExtension(fileName).ToLowerInvariant();
if (ext is not ".html" and not ".json" and not ".js" and not ".css")
{
return;
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Start();
}
private async Task ReloadWebPlugin()
{
if (isReloading || !IsRunning)
{
return;
}
isReloading = true;
try
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading web plugin", source: "WebPlugin");
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
}
LoadWebPluginSettings();
await Dispatcher.Invoke(async () =>
{
if (isInitialized && webView.CoreWebView2 != null)
{
_ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
busyMask.IsBusy = true;
string htmlPath = Path.Combine(PluginFolderPath, "main.html");
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.CoreWebView2.Navigate(htmlUri);
busyMask.IsBusy = false;
}
});
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Web plugin reloaded", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload web plugin: {ex}", source: "WebPlugin");
}
finally
{
isReloading = false;
if (fileWatcher != null && IsRunning)
{
fileWatcher.EnableRaisingEvents = true;
}
}
}
private async System.Threading.Tasks.Task NotifySettingChange(string id, Setting setting)
{
object? value = GetSettingValue(setting);
string serializedValue = JsonSerializer.Serialize(value);
string serializedId = JsonSerializer.Serialize(id);
_ = await webView.ExecuteScriptAsync($"window.desktopMagic?.dispatchSettingChanged?.({serializedId}, {serializedValue})");
}
private string SerializeSettingsToJson()
{
Dictionary<string, object?> dict = [];
foreach (SettingElement element in settings.Settings)
{
dict[element.Id] = GetSettingValue(element.Input);
}
return JsonSerializer.Serialize(dict);
}
private static object? GetSettingValue(Setting setting)
{
return setting switch
{
TextBox tb => tb.Value,
CheckBox cb => cb.Value,
Slider sl => sl.Value,
IntegerUpDown iud => iud.Value,
ComboBox cb => cb.Value,
ColorPicker cp => MultiColorConverter.ConvertToHexRgba(cp.Value),
Button btn => btn.Value,
Label lbl => lbl.Value,
FileSelector fs => fs.Value,
_ => null
};
}
private static Setting? CreateSetting(string type, JsonElement element)
{
return type.ToLowerInvariant() switch
{
"textbox" => new TextBox(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : ""),
"checkbox" => new CheckBox(
element.TryGetProperty("default", out JsonElement defaultValue) && defaultValue.ValueKind == JsonValueKind.True),
"slider" => new Slider(
element.TryGetProperty("min", out JsonElement minValue) ? minValue.GetDouble() : 0,
element.TryGetProperty("max", out JsonElement maxValue) ? maxValue.GetDouble() : 100,
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetDouble() : 0),
"integer" => new IntegerUpDown(
element.TryGetProperty("min", out JsonElement minValue) ? minValue.GetInt32() : 0,
element.TryGetProperty("max", out JsonElement maxValue) ? maxValue.GetInt32() : 100,
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetInt32() : 0),
"combobox" => CreateComboBox(element),
"colorpicker" => new ColorPicker(
element.TryGetProperty("default", out JsonElement defaultValue)
? MultiColorConverter.ConvertToSystemColor(defaultValue.GetString() ?? "#FFFFFFFF", System.Drawing.Color.White)
: System.Drawing.Color.White),
"button" => new Button(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : ""),
"label" => new Label(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : "",
element.TryGetProperty("bold", out JsonElement boldValue) && boldValue.ValueKind == JsonValueKind.True),
"file" => new FileSelector(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : "",
element.TryGetProperty("filter", out JsonElement filterValue) ? filterValue.GetString() ?? "All Files|*.*" : "All Files|*.*",
element.TryGetProperty("title", out JsonElement titleValue) ? titleValue.GetString() ?? "Select File" : "Select File",
element.TryGetProperty("selectFolder", out JsonElement selectFolderValue) && selectFolderValue.ValueKind == JsonValueKind.True),
_ => null
};
}
private static ComboBox CreateComboBox(JsonElement element)
{
List<string> items = [];
if (element.TryGetProperty("items", out JsonElement itemsValue) && itemsValue.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement item in itemsValue.EnumerateArray())
{
items.Add(item.GetString() ?? "");
}
}
ComboBox comboBox = new([.. items]);
if (element.TryGetProperty("default", out JsonElement defaultValue))
{
string defaultText = defaultValue.GetString() ?? "";
if (!string.IsNullOrEmpty(defaultText) && items.Contains(defaultText))
{
comboBox.Value = defaultText;
}
}
return comboBox;
}
}
@@ -3,4 +3,5 @@
<BitmapImage x:Key="Icon" UriSource="/icon.ico" />
<BitmapImage x:Key="ModioCogBlue" UriSource="modio-cog-blue.png" />
<BitmapImage x:Key="ModioLogoBlueDark" UriSource="modio-logo-bluedark.png" />
<BitmapImage x:Key="ModioLogoBlueLight" UriSource="modio-logo-bluelight.png" />
</ResourceDictionary>
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -3,6 +3,10 @@
xmlns:col="clr-namespace:System.Collections;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="mirrorMode">Spiegeln</system:String>
<system:String x:Key="folder">Ordner</system:String>
@@ -15,29 +19,42 @@
<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="allPlugins">Alle Plugins</system:String>
<system:String x:Key="installedPlugins">Installierte Plugins</system:String>
<system:String x:Key="updatePlugins">Neu Laden</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="default">Standard</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="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="date">Datum</system:String>
<system:String x:Key="cpuUsage">CPU Auslastung</system:String>
<system:String x:Key="musicVisualizer">Musik Visualisierer</system:String>
<system:String x:Key="weather">Wetter</system:String>
<system:String x:Key="nextMeetingCountdown">Nächstes Meeting Countdown</system:String>
<system:String x:Key="agenda">Agenda</system:String>
<system:String x:Key="amplifier">Signalverstärkung:</system:String>
<system:String x:Key="newLayout">Neues Layout</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="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="layoutAlreadyExists">Layout existiert bereits!</system:String>
<system:String x:Key="enterThemeName">Themenamen eingeben:</system:String>
<system:String x:Key="themeAlreadyExists">Theme existiert bereits!</system:String>
<system:String x:Key="install">Installieren</system:String>
<system:String x:Key="uninstall">Deinstallieren</system:String>
<system:String x:Key="remove">Entfernen</system:String>
@@ -45,9 +62,14 @@
<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">
<system:String>Unten</system:String>
<system:String>Mitte</system:String>
<system:String>Oben</system:String>
</col:ArrayList>
</ResourceDictionary>
</ResourceDictionary>
@@ -3,6 +3,10 @@
xmlns:col="clr-namespace:System.Collections;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="mirrorMode">Mirror</system:String>
<system:String x:Key="folder">Folder</system:String>
@@ -15,29 +19,44 @@
<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="allPlugins">All Plugins</system:String>
<system:String x:Key="installedPlugins">Installed Plugins</system:String>
<system:String x:Key="updatePlugins">Reload</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="default">Default</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="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="date">Date</system:String>
<system:String x:Key="cpuUsage">CPU Usage</system:String>
<system:String x:Key="musicVisualizer">Music Visualizer</system:String>
<system:String x:Key="weather">Weather</system:String>
<system:String x:Key="nextMeetingCountdown">Next Meeting Countdown</system:String>
<system:String x:Key="agenda">Agenda</system:String>
<system:String x:Key="amplifier">Signal Amplification:</system:String>
<system:String x:Key="newLayout">New 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="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="layoutAlreadyExists">Layout already exists!</system:String>
<system:String x:Key="cannotDeleteLastLayout">Can't delete last layout!</system:String>
<system:String x:Key="enterThemeName">Enter theme name</system:String>
<system:String x:Key="themeAlreadyExists">Theme already exists!</system:String>
<system:String x:Key="cannotDeleteLastTheme">Can't delete last theme!</system:String>
<system:String x:Key="install">Install</system:String>
<system:String x:Key="uninstall">Uninstall</system:String>
<system:String x:Key="remove">Remove</system:String>
@@ -45,9 +64,14 @@
<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">
<system:String>Bottom</system:String>
<system:String>Middle</system:String>
<system:String>Top</system:String>
</col:ArrayList>
</ResourceDictionary>
</ResourceDictionary>
@@ -0,0 +1,131 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<system:Double x:Key="ToggleButtonWidth">40</system:Double>
<system:Double x:Key="ToggleButtonHeight">20</system:Double>
<Thickness x:Key="ToggleSwitchBorderThemeThickness">1</Thickness>
<Thickness x:Key="ToggleSwitchContentLeftMargin">0,0,8,0</Thickness>
<Style x:Key="ToggleSwitchContentLeftStyle" TargetType="{x:Type ui:ToggleSwitch}">
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOn}" />
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="{StaticResource ToggleSwitchBorderThemeThickness}" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:ToggleSwitch}">
<Grid Margin="{TemplateBinding Padding}" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="0"
Margin="{StaticResource ToggleSwitchContentLeftMargin}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True"
TextElement.Foreground="{TemplateBinding Foreground}" />
<Grid
Grid.Column="1"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}">
<Rectangle
x:Name="ToggleRectangle"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}"
Fill="{DynamicResource ToggleSwitchFillOff}"
RadiusX="10" RadiusY="10"
Stroke="{DynamicResource ToggleSwitchStrokeOff}"
StrokeThickness="1" />
<Rectangle
x:Name="ActiveToggleRectangle"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}"
Fill="{TemplateBinding Background}"
Opacity="0.0"
RadiusX="10" RadiusY="10" />
<Rectangle
x:Name="ToggleEllipse"
Width="12" Height="12"
Fill="{DynamicResource ToggleSwitchKnobFillOff}"
RadiusX="7" RadiusY="7">
<Rectangle.RenderTransform>
<TranslateTransform X="-9" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle
x:Name="ActiveToggleEllipse"
Width="12" Height="12"
Fill="{DynamicResource ToggleSwitchKnobFillOn}"
Opacity="0.0"
RadiusX="7" RadiusY="7">
<Rectangle.RenderTransform>
<TranslateTransform X="-9" />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Name="SlideRight">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="9" Duration="0:0:0.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="-9" Duration="0:0:0.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Content" Value="{x:Null}" />
<Condition Property="IsChecked" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OnContent, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Content" Value="{x:Null}" />
<Condition Property="IsChecked" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OffContent, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -1,4 +1,6 @@
using System.Collections.ObjectModel;
using DesktopMagic.Plugins;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
@@ -6,19 +8,33 @@ using System.Text.Json.Serialization;
namespace DesktopMagic.Settings;
internal class DesktopMagicSettings : INotifyPropertyChanged
public class DesktopMagicSettings : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private ObservableCollection<Layout> layouts = [];
private ObservableCollection<Theme> themes = [];
private string? currentLayoutName;
public ObservableCollection<Theme> Themes
{
get => themes;
init
{
themes = value;
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
OnPropertyChanged();
}
}
public ObservableCollection<Layout> Layouts
{
get => layouts;
set
{
layouts = value;
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayout));
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
OnPropertyChanged();
}
}
@@ -28,7 +44,16 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
public string? CurrentLayoutName
{
get => currentLayoutName ?? Layouts.FirstOrDefault()?.Name;
get
{
if (!Layouts.Any(l => l.Name == currentLayoutName))
{
currentLayoutName = null;
}
return currentLayoutName ?? Layouts.FirstOrDefault()?.Name;
}
set
{
currentLayoutName = value;
@@ -39,8 +64,18 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
public string? ModIoAccessToken { get; set; }
public string? ReleaseInfoLastAppVersion { get; set; }
public DesktopMagicSettings()
{
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayout));
layouts.CollectionChanged += (s, e) => OnPropertyChanged(nameof(CurrentLayoutName));
}
protected void OnPropertyChanged([CallerMemberName] string? name = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}
+34 -7
View File
@@ -1,27 +1,42 @@
using DesktopMagic.Plugins;
using DesktopMagic.DataContexts;
using DesktopMagic.Plugins;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json.Serialization;
namespace DesktopMagic.Settings;
internal class Layout(string name) : INotifyPropertyChanged
public class Layout(string name) : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private string name = name;
private Theme theme = new Theme();
private string? currentThemeName = null;
private Dictionary<uint, PluginSettings> plugins = [];
[JsonIgnore]
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
{
theme = value;
OnPropertyChanged();
if (currentThemeName != value)
{
currentThemeName = value;
UpdateTheme();
}
}
}
@@ -47,7 +62,19 @@ internal class Layout(string name) : INotifyPropertyChanged
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)
+52 -5
View File
@@ -3,7 +3,9 @@ using DesktopMagic.Plugins;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Windows;
@@ -13,18 +15,40 @@ public class PluginSettings : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
private readonly Theme? theme;
private string? currentThemeName;
private List<SettingElement> settings = [];
private Dictionary<string, JsonElement> state = [];
private bool enabled = false;
private Point position = new Point(100, 100);
private Point size = new Point(300, 300);
[JsonIgnore]
public Theme Theme => theme is null ? MainWindowDataContext.GetSettings().CurrentLayout.Theme : theme;
// Only for internal use to show the name of the plugin in the main window
[JsonIgnore]
public string Name { get; set; } = string.Empty;
public PluginMetadata Metadata { get; set; } = new();
[JsonIgnore]
public Theme Theme
{
get
{
DesktopMagicSettings settings = MainWindowDataContext.GetSettings();
return settings.Themes.FirstOrDefault(t => t.Name == currentThemeName) ?? settings.CurrentLayout.Theme;
}
}
public string? CurrentThemeName
{
get => currentThemeName;
set
{
if (currentThemeName != value)
{
currentThemeName = value;
OnPropertyChanged();
OnPropertyChanged(nameof(Theme));
}
}
}
public List<SettingElement> Settings
{
@@ -39,6 +63,23 @@ public class PluginSettings : INotifyPropertyChanged
}
}
/// <summary>
/// Dictionary storing plugin state. Key is the field/property name, value is the JsonElement.
/// This is persisted per layout in settings.json.
/// </summary>
public Dictionary<string, JsonElement> State
{
get => state;
set
{
if (state != value)
{
state = value;
OnPropertyChanged();
}
}
}
public bool Enabled
{
get => enabled;
@@ -78,6 +119,12 @@ public class PluginSettings : INotifyPropertyChanged
}
}
public void UpdateTheme()
{
OnPropertyChanged(nameof(Theme));
OnPropertyChanged(nameof(CurrentThemeName));
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+3 -3
View File
@@ -14,10 +14,10 @@ namespace DesktopMagic.PluginTest;
public class GifPlugin : Plugin
{
[Setting("gif-path", "GIF path")]
private readonly TextBox input = new TextBox("");
private readonly FileSelector input = new FileSelector();
[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 = [];
@@ -92,7 +92,7 @@ public class GifPlugin : Plugin
info.Value = $"Loading frame {i + 1} of {gif.GetFrameCount(FrameDimension.Time)}";
}
info.Value = string.Empty;
info.Value = $"Loaded {bitmaps.Count} frames.";
}
else
{
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.Api;
/// <summary>
/// Provides an abstract base class for creating asynchronous plugins that can be integrated into the application, supporting periodic updates,
/// rendering, and user interaction.
/// </summary>
/// <remarks>Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
/// such as <see cref="Plugin.Start"/>, <see cref="Plugin.Stop"/>, and <see cref="Main"/> for activation, deactivation, and periodic
/// execution. It also provides event handlers for mouse and theme interactions, as well as access to application data
/// and rendering configuration. Implementations should override relevant methods to respond to user input, update
/// intervals, and configuration changes as needed.</remarks>
public abstract class AsyncPlugin : Plugin
{
/// <summary>
/// Occurs once when the plugin gets activated. Override for async initialization.
/// </summary>
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
public virtual Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs once when the plugin gets deactivated. Override for async cleanup.
/// </summary>
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs when the <see cref="Plugin.UpdateInterval"/> elapses.
/// </summary>
/// <returns></returns>
public abstract Task<Bitmap?> MainAsync(CancellationToken cancellationToken);
/// <summary>
/// This method should not be called directly! Override and use <see cref="MainAsync"/> for asynchronous plugin operations.
/// </summary>
/// <remarks>This method only exists to fulfill base class requirements.</remarks>
/// <returns>Calling it will always result in an <see cref="InvalidOperationException"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if this method is called directly. Use MainAsync for plugin execution instead.</exception>
public sealed override Bitmap? Main()
{
throw new InvalidOperationException($"AsyncPlugin.Main() should not be called directly. Override {nameof(MainAsync)} instead.");
}
}
@@ -4,15 +4,17 @@
<TargetFramework>net8.0-windows</TargetFramework>
<Company>Stone_Red</Company>
<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>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>0.0.0.5</AssemblyVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<PackageProjectUrl></PackageProjectUrl>
<FileVersion>0.0.0.5</FileVersion>
<FileVersion></FileVersion>
<Nullable>enable</Nullable>
<Platforms>AnyCPU;x64</Platforms>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>Square44x44Logo.altform-lightunplated_targetsize-256.png</PackageIcon>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -36,6 +38,14 @@
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\MsixPackaging\Images\Square44x44Logo.altform-lightunplated_targetsize-256.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
+23
View File
@@ -36,4 +36,27 @@ public interface IPluginData
/// Updates the plugin window.
/// </summary>
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();
}
+22
View File
@@ -0,0 +1,22 @@
namespace DesktopMagic.Api;
/// <summary>
/// Specifies the severity level of a log message.
/// </summary>
public enum LogLevel
{
/// <summary>
/// Informational message for general information.
/// </summary>
Info,
/// <summary>
/// Warning message indicating a potential issue.
/// </summary>
Warning,
/// <summary>
/// Error message indicating a failure or critical issue.
/// </summary>
Error
}
@@ -1,4 +1,4 @@
namespace DesktopMagic.Api.Settings;
namespace DesktopMagic.Api;
/// <summary>
/// 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
{
}
+31 -2
View File
@@ -7,8 +7,14 @@ using System.Drawing;
namespace DesktopMagic.Api;
/// <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>
/// <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
{
[Setting("desktopmagic-horizontal-alignment", "Horizontal Alignment", -999)]
@@ -17,6 +23,15 @@ public abstract class Plugin
[Setting("desktopmagic-vertical-alignment", "Vertical Alignment", -998)]
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!;
/// <summary>
@@ -83,4 +98,18 @@ public abstract class Plugin
public virtual void OnMouseWheel(Point position, int delta)
{
}
}
/// <summary>
/// Occurs when the application's theme has changed.
/// </summary>
public virtual void OnThemeChanged()
{
}
/// <summary>
/// Invoked when the settings have changed to allow derived classes to respond to configuration updates.
/// </summary>
public virtual void OnSettingsChanged()
{
}
}
@@ -0,0 +1,62 @@
using System.Drawing;
using System.Globalization;
namespace DesktopMagic.Api.Settings;
/// <summary>
/// Represents a color picker control.
/// </summary>
public sealed class ColorPicker : Setting
{
private Color _value;
/// <summary>
/// Gets or sets the color value assigned to the <see cref="ColorPicker"/> element.
/// </summary>
public Color Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
ValueChanged();
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPicker"/> class with the provided <paramref name="defaultColor"/>.
/// </summary>
/// <param name="defaultColor">The default color value.</param>
public ColorPicker(Color defaultColor)
{
_value = defaultColor;
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPicker"/> class with a default color of White.
/// </summary>
public ColorPicker() : this(Color.White)
{
}
internal override string GetJsonValue()
{
return $"{Value.A},{Value.R},{Value.G},{Value.B}";
}
internal override void SetJsonValue(string value)
{
string[] parts = value.Split(',');
if (parts.Length == 4 &&
byte.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte a) &&
byte.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte r) &&
byte.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte g) &&
byte.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte b))
{
Value = Color.FromArgb(a, r, g, b);
}
}
}
@@ -0,0 +1,65 @@
namespace DesktopMagic.Api.Settings;
/// <summary>
/// Represents a file selector control that allows the user to browse for a file.
/// </summary>
public sealed class FileSelector : Setting
{
private string _value = string.Empty;
/// <summary>
/// Gets or sets the selected file path.
/// </summary>
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
ValueChanged();
}
}
}
/// <summary>
/// Gets or sets the file filter for the file dialog (e.g., "Image Files|*.png;*.jpg|All Files|*.*").
/// </summary>
public string Filter { get; set; }
/// <summary>
/// Gets or sets the title of the file dialog.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the file selector should select folders instead of files.
/// </summary>
public bool SelectFolder { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="FileSelector"/> class.
/// </summary>
/// <param name="defaultPath">The default file path.</param>
/// <param name="filter">The file filter for the file dialog.</param>
/// <param name="title">The title of the file dialog.</param>
/// <param name="selectFolder">If true, selects folders instead of files.</param>
public FileSelector(string defaultPath = "", string filter = "All Files|*.*", string title = "Select File", bool selectFolder = false)
{
_value = defaultPath;
Filter = filter;
Title = title;
SelectFolder = selectFolder;
}
internal override string GetJsonValue()
{
return Value;
}
internal override void SetJsonValue(string value)
{
Value = value;
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
<Identity
Name="StoneRed.DesktopMagic"
Publisher="CN=6D1FD759-1951-428A-9B03-964461C7DBF5"
Version="1.1.0.0" />
Version="1.3.3.0" />
<Properties>
<DisplayName>Desktop Magic</DisplayName>