87 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
70 changed files with 5400 additions and 1256 deletions
+3 -3
View File
@@ -23,12 +23,12 @@
## Preview ## Preview
![Screenshot 2023-12-31 164947](https://github.com/Stone-Red-Code/DesktopMagic/assets/56473591/191ac76f-264d-4e9f-8979-7a13b2a1cd12) ![Screenshot 2026-01-22 000348](https://github.com/user-attachments/assets/4617330d-4c4b-4509-80d7-9f6db413e84f)
![Screenshot 2023-12-31 164853](https://github.com/Stone-Red-Code/DesktopMagic/assets/56473591/22de55d2-dde3-43a5-b77b-c773a7697e8c) ![Screenshot 2026-01-22 000434](https://github.com/user-attachments/assets/aebeca87-b305-40a4-a942-d78fce0e9e97)
## Third party licenses ## Third party licenses
- [BusyIndicator](https://github.com/moh3ngolshani/BusyIndicator) - [MIT](https://github.com/Moh3nGolshani/BusyIndicator/blob/master/LICENSE) - [BusyIndicator](https://github.com/moh3ngolshani/BusyIndicator) - [MIT](https://github.com/Moh3nGolshani/BusyIndicator/blob/master/LICENSE)
- [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) - [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) - [NAudio](https://github.com/naudio/NAudio) - [MIT](https://github.com/naudio/NAudio/blob/master/license.txt)
Binary file not shown.
+1 -1
View File
@@ -1,7 +1,7 @@
; Script generated by the Inno Setup Script Wizard. ; Script generated by the Inno Setup Script Wizard.
; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! ; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
#expr Exec('cmd.exe /C ', 'dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\DesktopMagic\') #expr Exec('cmd.exe', '/C dotnet build -o "' + SourcePath + '\publish" -c Release ' + SourcePath + '..\src\DesktopMagic\')
#define MyAppName "DesktopMagic" #define MyAppName "DesktopMagic"
#define MyAppVersion GetStringFileInfo("/publish/DesktopMagic.exe","ProductVersion") #define MyAppVersion GetStringFileInfo("/publish/DesktopMagic.exe","ProductVersion")
+15 -7
View File
@@ -1,20 +1,28 @@
<Application x:Class="DesktopMagic.App" <Application x:Class="DesktopMagic.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:md="http://schemas.lepo.co/wpfui/2022/xaml/markdown"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:helpers="clr-namespace:DesktopMagic.Helpers"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml"> StartupUri="MainWindow.xaml">
<Application.Resources> <Application.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources\Strings\StringResources.en.xaml" /> <ResourceDictionary Source="Resources\Strings\StringResources.en.xaml" />
<materialDesign:BundledTheme BaseTheme="Light" PrimaryColor="Grey" SecondaryColor="LightBlue" /> <ui:ThemesDictionary Theme="Light" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesign3.Defaults.xaml" /> <ui:ControlsDictionary />
<md:ThemesDictionary Theme="Light" />
<md:ControlsDictionary />
<ResourceDictionary Source="pack://application:,,,/BusyIndicator;component/Theme/Default.xaml" /> <ResourceDictionary Source="pack://application:,,,/BusyIndicator;component/Theme/Default.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteBrushes.xaml" />
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.ObsoleteStyles.xaml" />
<ResourceDictionary Source="Resources\Images\ImageResources.xaml" /> <ResourceDictionary Source="Resources\Images\ImageResources.xaml" />
<ResourceDictionary Source="Resources\Styles\ToggleSwitchContentLeftStyle.xaml" />
</ResourceDictionary.MergedDictionaries> </ResourceDictionary.MergedDictionaries>
<Thickness x:Key="DefaultMargin" Top="6" Left="6" Right="6" Bottom ="6"></Thickness>
<!-- Converters -->
<helpers:UnderscoreEscapingConverter x:Key="UnderscoreEscapingConverter" />
<!-- Margins -->
<Thickness x:Key="DefaultMargin" Top="5" Left="5" Right="5" Bottom ="5" />
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>
+12 -2
View File
@@ -5,6 +5,8 @@ using System.IO;
using System.Threading; using System.Threading;
using System.Windows; using System.Windows;
using Wpf.Ui;
namespace DesktopMagic; namespace DesktopMagic;
/// <summary> /// <summary>
@@ -26,6 +28,8 @@ public partial class App : Application
public static string PluginsPath => Path.Combine(ApplicationDataPath, "Plugins"); public static string PluginsPath => Path.Combine(ApplicationDataPath, "Plugins");
public static IContentDialogService DialogService { get; } = new ContentDialogService();
public static Logger Logger { get; } = new Logger() public static Logger Logger { get; } = new Logger()
{ {
Config = new() Config = new()
@@ -130,7 +134,7 @@ public partial class App : Application
Logger.LogFatal(exception + (e.IsTerminating ? "\t Process terminating!" : ""), source: exception.Source ?? "Unknown"); 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; AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
@@ -156,7 +160,13 @@ public partial class App : Application
} }
catch (Exception ex) 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); 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 else
{ {
gr.DrawString(cpuUsageString, font, new SolidBrush(Application.Theme.PrimaryColor), 0, (size.Width / 2) - size.Height); gr.DrawString(cpuUsageString, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0);
} }
return bmp; return bmp;
+126 -13
View File
@@ -4,35 +4,114 @@ using DesktopMagic.Api.Settings;
using System; using System;
using System.Drawing; using System.Drawing;
using System.Drawing.Text; using System.Drawing.Text;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
namespace DesktopMagic.BuiltInPlugins; namespace DesktopMagic.BuiltInPlugins;
internal class DatePlugin : Plugin internal partial class DatePlugin : Plugin
{ {
[Setting("short-date", "Short date")] [Setting("week-day-style", "Week Day Style")]
private readonly CheckBox shortDateCheckBox = new CheckBox(true); 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 DateTime oldDateTime = DateTime.MinValue;
private Color oldColor = Color.White; private bool themeChanged = false;
private string oldFont = string.Empty;
private bool oldShortDateCheckBoxValue;
public override int UpdateInterval => 1000; public override int UpdateInterval => 1000;
public override Bitmap? Main() 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; return null;
} }
oldDateTime = DateTime.Now; oldDateTime = DateTime.Now;
oldColor = Application.Theme.PrimaryColor; themeChanged = false;
oldFont = Application.Theme.Font;
oldShortDateCheckBoxValue = shortDateCheckBox.Value;
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); Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100); bmp.SetResolution(100, 100);
@@ -45,10 +124,44 @@ internal class DatePlugin : Plugin
bmp.SetResolution(100, 100); bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp); using Graphics gr = Graphics.FromImage(bmp);
using SolidBrush brush = new SolidBrush(color);
gr.TextRenderingHint = TextRenderingHint.AntiAlias; gr.TextRenderingHint = TextRenderingHint.AntiAlias;
gr.DrawString(date, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0); gr.DrawString(date, font, brush, 0, 0);
return bmp; 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 Bitmap output = new Bitmap(880, 300);
private readonly System.Diagnostics.Stopwatch frameStopwatch = System.Diagnostics.Stopwatch.StartNew();
[Setting("fps-limit", "FPS Limit")]
private readonly IntegerUpDown fpsLimit = new IntegerUpDown(1, 144, 60);
[Setting("mirror", "Mirror")] [Setting("mirror", "Mirror")]
private readonly CheckBox mirrorMode = new CheckBox(false); private readonly CheckBox mirrorMode = new CheckBox(false);
@@ -51,8 +56,16 @@ internal class MusicVisualizerPlugin : Plugin
waveIn.StartRecording(); waveIn.StartRecording();
} }
public override Bitmap Main() public override Bitmap? Main()
{ {
double msPerFrame = 1000.0 / Math.Max(1, fpsLimit.Value);
if (frameStopwatch.Elapsed.TotalMilliseconds < msPerFrame)
{
return null; // Exit early if we haven't reached the time threshold
}
frameStopwatch.Restart(); // Reset timer for the next frame
return output; return output;
} }
@@ -0,0 +1,233 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using DesktopMagic.Helpers;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Text;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.BuiltInPlugins;
internal class NextMeetingCountdownPlugin : AsyncPlugin
{
[Setting("meeting-calendar-url", "iCal URL")]
private readonly TextBox calendarUrl = new TextBox(string.Empty);
[Setting("meeting-refresh-button", "Refresh Calendar")]
private readonly Button refreshButton = new Button("Refresh");
[Setting("meeting-refresh-interval", "Refresh Every (minutes)")]
private readonly IntegerUpDown refreshMinutes = new IntegerUpDown(1, 120, 10);
[Setting("meeting-lookahead-hours", "Look Ahead (hours)")]
private readonly IntegerUpDown lookAheadHours = new IntegerUpDown(1, 336, 48);
[Setting("meeting-include-all-day", "Include All-Day Events")]
private readonly CheckBox includeAllDayEvents = new CheckBox(false);
[Setting("meeting-time-display", "Start Time Format")]
private readonly ComboBox startTimeDisplay = new ComboBox("None", "Date and time", "Time only");
[Setting("meeting-show-location", "Show Location")]
private readonly CheckBox showLocation = new CheckBox(false);
[Setting("meeting-center-lines", "Center Lines")]
private readonly CheckBox centerLines = new CheckBox(false);
public override int UpdateInterval { get; set; } = 1000;
private List<CalendarEventEntry> upcomingEvents = [];
private DateTime nextRefreshTime = DateTime.MinValue;
private bool forceRefresh = true;
private bool themeChanged = true;
private string? errorText;
public override Task StartAsync(CancellationToken cancellationToken)
{
refreshButton.OnClick += () =>
{
forceRefresh = true;
Application.UpdateWindow();
};
calendarUrl.OnValueChanged += MarkForRefresh;
refreshMinutes.OnValueChanged += MarkForRefresh;
lookAheadHours.OnValueChanged += MarkForRefresh;
includeAllDayEvents.OnValueChanged += MarkForRefresh;
startTimeDisplay.OnValueChanged += Application.UpdateWindow;
showLocation.OnValueChanged += Application.UpdateWindow;
centerLines.OnValueChanged += Application.UpdateWindow;
return Task.CompletedTask;
}
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
{
if (themeChanged || forceRefresh || DateTime.Now >= nextRefreshTime)
{
await RefreshEventsAsync(cancellationToken);
themeChanged = false;
}
return RenderCountdownBitmap();
}
public override void OnThemeChanged()
{
themeChanged = true;
}
public override void OnSettingsChanged()
{
Application.UpdateWindow();
}
private void MarkForRefresh()
{
forceRefresh = true;
Application.UpdateWindow();
}
private async Task RefreshEventsAsync(CancellationToken cancellationToken)
{
forceRefresh = false;
nextRefreshTime = DateTime.Now.AddMinutes(Math.Max(1, refreshMinutes.Value));
if (string.IsNullOrWhiteSpace(calendarUrl.Value))
{
upcomingEvents = [];
errorText = "Set iCal URL in settings";
return;
}
try
{
DateTime now = DateTime.Now;
DateTime windowEnd = now.AddHours(Math.Max(1, lookAheadHours.Value));
upcomingEvents = await IcsCalendarHelper.GetUpcomingEventsAsync(
calendarUrl.Value.Trim(),
now,
windowEnd,
20,
includeAllDayEvents.Value,
cancellationToken);
errorText = null;
}
catch (HttpRequestException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
catch (TaskCanceledException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
catch (InvalidOperationException ex)
{
upcomingEvents = [];
errorText = $"Calendar unavailable: {ex.Message}";
}
}
private Bitmap RenderCountdownBitmap()
{
List<string> lines = [];
if (!string.IsNullOrWhiteSpace(errorText))
{
lines.Add(errorText);
}
else
{
DateTime now = DateTime.Now;
CalendarEventEntry? nextEvent = upcomingEvents.FirstOrDefault(eventEntry => eventEntry.EndLocal > now);
if (nextEvent is null)
{
lines.Add("No upcoming meetings");
}
else
{
lines.Add(nextEvent.Title);
if (startTimeDisplay.Value != "None")
{
string timeText = nextEvent.IsAllDay
? "All day"
: startTimeDisplay.Value == "Time only"
? nextEvent.StartLocal.ToString("t")
: $"{nextEvent.StartLocal:D} {nextEvent.StartLocal:t}";
lines.Add(timeText);
}
if (showLocation.Value && !string.IsNullOrWhiteSpace(nextEvent.Location))
{
lines.Add(nextEvent.Location!);
}
if (now < nextEvent.StartLocal)
{
lines.Add($"Starts in {FormatCountdown(nextEvent.StartLocal - now)}");
}
else
{
lines.Add("In progress");
}
}
}
using Font font = new Font(Application.Theme.Font, 90);
using Bitmap measureBitmap = new Bitmap(1, 1);
using Graphics measureGraphics = Graphics.FromImage(measureBitmap);
measureGraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
float maxWidth = 0;
float lineHeight = 0;
foreach (string line in lines)
{
SizeF size = measureGraphics.MeasureString(line, font);
maxWidth = Math.Max(maxWidth, size.Width);
lineHeight = Math.Max(lineHeight, size.Height);
}
int width = Math.Max(1, (int)Math.Ceiling(maxWidth));
int height = Math.Max(1, (int)Math.Ceiling(lineHeight * lines.Count));
Bitmap bitmap = new Bitmap(width, height);
bitmap.SetResolution(100, 100);
using Graphics graphics = Graphics.FromImage(bitmap);
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
graphics.Clear(Color.Transparent);
for (int index = 0; index < lines.Count; index++)
{
string line = lines[index];
float x = 0;
if (centerLines.Value)
{
SizeF lineSize = graphics.MeasureString(line, font);
x = Math.Max(0, (width - lineSize.Width) / 2f);
}
graphics.DrawString(line, font, brush, x, index * lineHeight);
}
return bitmap;
}
private static string FormatCountdown(TimeSpan timeSpan)
{
int totalHours = (int)timeSpan.TotalHours;
return $"{totalHours:00}:{timeSpan.Minutes:00}:{timeSpan.Seconds:00}";
}
}
+17 -9
View File
@@ -13,26 +13,23 @@ internal class TimePlugin : Plugin
private readonly CheckBox displaySecondsCheckBox = new CheckBox(true); private readonly CheckBox displaySecondsCheckBox = new CheckBox(true);
private string oldTime = string.Empty; private string oldTime = string.Empty;
private Color oldColor = Color.White; private bool themeChanged;
private string oldFont = string.Empty;
private bool oldDisplaySecondsCheckBoxValue;
public override int UpdateInterval => 1000; public override int UpdateInterval => 1000;
public override Bitmap? Main() public override Bitmap? Main()
{ {
string time = displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString(); string time = displaySecondsCheckBox.Value ? DateTime.Now.ToLongTimeString() : DateTime.Now.ToShortTimeString();
if (oldTime == time && oldColor == Application.Theme.PrimaryColor && oldFont == Application.Theme.Font && oldDisplaySecondsCheckBoxValue == displaySecondsCheckBox.Value) if (oldTime == time && !themeChanged)
{ {
return null; return null;
} }
oldTime = time; oldTime = time;
oldColor = Application.Theme.PrimaryColor; themeChanged = false;
oldFont = Application.Theme.Font;
oldDisplaySecondsCheckBoxValue = displaySecondsCheckBox.Value;
Font font = new Font(Application.Theme.Font, 200); using Font font = new Font(Application.Theme.Font, 200);
Bitmap bmp = new Bitmap(1, 1); Bitmap bmp = new Bitmap(1, 1);
bmp.SetResolution(100, 100); bmp.SetResolution(100, 100);
@@ -45,10 +42,21 @@ internal class TimePlugin : Plugin
bmp.SetResolution(100, 100); bmp.SetResolution(100, 100);
using Graphics gr = Graphics.FromImage(bmp); using Graphics gr = Graphics.FromImage(bmp);
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
gr.TextRenderingHint = TextRenderingHint.AntiAlias; gr.TextRenderingHint = TextRenderingHint.AntiAlias;
gr.DrawString(time, font, new SolidBrush(Application.Theme.PrimaryColor), 0, 0); gr.DrawString(time, font, brush, 0, 0);
return bmp; return bmp;
} }
public override void OnThemeChanged()
{
themeChanged = true;
}
public override void OnSettingsChanged()
{
themeChanged = true;
}
} }
@@ -0,0 +1,208 @@
using DesktopMagic.Api;
using DesktopMagic.Api.Settings;
using System;
using System.Diagnostics;
using System.Drawing;
using System.Globalization;
using System.Net.Http;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.BuiltInPlugins;
public class WeatherPlugin : AsyncPlugin
{
[Setting("city-name", "City Name")]
private readonly TextBox cityInput = new TextBox("Tokyo");
[Setting("search-btn", "Update Location")]
private readonly Button searchButton = new Button("Update Location");
[Setting("show-city", "Show City Name")]
private readonly CheckBox showCity = new CheckBox(true);
[Setting("show-temp", "Show Temperature")]
private readonly CheckBox showTemp = new CheckBox(true);
[Setting("show-status", "Show Weather Status")]
private readonly CheckBox showStatus = new CheckBox(true);
[Setting("show-time", "Show Last Updated")]
private readonly CheckBox showTime = new CheckBox(false);
[Setting("font-size", "Base Font Size")]
private readonly Slider fontSizeSlider = new Slider(10, 100, 30);
public override int UpdateInterval { get; set; } = 900000; // 15 Minutes
private readonly HttpClient httpClient = new HttpClient();
private string? cachedLat;
private string? cachedLon;
private string currentTemp = "--";
private string weatherStatus = "Enter city and search";
private string foundCityName = "No City Selected";
private string lastUpdated = "";
private bool isLoading = false;
public override async Task StartAsync(CancellationToken cancellationToken)
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (DesktopMagic)");
// Setup Event Handlers for instant UI updates when settings change
showCity.OnValueChanged += Application.UpdateWindow;
showTemp.OnValueChanged += Application.UpdateWindow;
showStatus.OnValueChanged += Application.UpdateWindow;
showTime.OnValueChanged += Application.UpdateWindow;
fontSizeSlider.OnValueChanged += Application.UpdateWindow;
await UpdateLocationAndWeather();
Application.UpdateWindow();
searchButton.OnClick += () =>
{
isLoading = true;
Application.UpdateWindow();
_ = Task.Run(async () =>
{
await UpdateLocationAndWeather();
isLoading = false;
Application.UpdateWindow();
});
};
}
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
{
// Periodic background update
if (!isLoading && cachedLat != null && cachedLon != null)
{
await UpdateWeatherOnly(cachedLat, cachedLon);
}
Bitmap bmp = new Bitmap(1200, 800);
bmp.SetResolution(300, 300);
using (Graphics g = Graphics.FromImage(bmp))
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
g.Clear(Color.Transparent);
int lineCount = 0;
int baseSize = (int)fontSizeSlider.Value;
if (isLoading)
{
DrawLine(bmp, g, "Fetching...", ref lineCount, baseSize, FontStyle.Italic);
}
else
{
if (showCity.Value)
{
DrawLine(bmp, g, foundCityName.ToUpper(), ref lineCount, baseSize, FontStyle.Bold);
}
if (showTemp.Value)
{
DrawLine(bmp, g, currentTemp, ref lineCount, baseSize, FontStyle.Bold);
}
if (showStatus.Value)
{
DrawLine(bmp, g, weatherStatus, ref lineCount, baseSize, FontStyle.Regular);
}
if (showTime.Value && !string.IsNullOrEmpty(lastUpdated))
{
DrawLine(bmp, g, $"Updated: {lastUpdated}", ref lineCount, baseSize, FontStyle.Italic);
}
}
}
return bmp;
}
private void DrawLine(Bitmap bitmap, Graphics graphics, string text, ref int lineCount, int fontSize, FontStyle style)
{
using Font font = new Font(Application.Theme.Font, fontSize, style);
SizeF textSize = graphics.MeasureString(text, font);
float x = (bitmap.Width - textSize.Width) / 2;
// Adjust vertical spacing based on font size
float y = (lineCount == 0) ? 60 : (lineCount * (textSize.Height + 10)) + 60;
using Brush brush = new SolidBrush(Application.Theme.PrimaryColor);
graphics.DrawString(text, font, brush, x, y);
lineCount++;
}
private async Task UpdateLocationAndWeather()
{
try
{
string geoUrl = $"https://geocoding-api.open-meteo.com/v1/search?name={Uri.EscapeDataString(cityInput.Value.Trim())}&count=1&language=en&format=json";
string geoJson = await httpClient.GetStringAsync(geoUrl);
using JsonDocument geoDoc = JsonDocument.Parse(geoJson);
if (!geoDoc.RootElement.TryGetProperty("results", out JsonElement results) || results.GetArrayLength() == 0)
{
foundCityName = cityInput.Value;
currentTemp = "--";
weatherStatus = "City not found";
return;
}
JsonElement location = results[0];
cachedLat = location.GetProperty("latitude").GetDouble().ToString(CultureInfo.InvariantCulture);
cachedLon = location.GetProperty("longitude").GetDouble().ToString(CultureInfo.InvariantCulture);
foundCityName = location.GetProperty("name").GetString() ?? cityInput.Value;
// We update the textbox to the "official" name found by the API
cityInput.Value = foundCityName;
await UpdateWeatherOnly(cachedLat, cachedLon);
}
catch (Exception ex)
{
Debug.WriteLine($"Error: {ex.Message}");
weatherStatus = "Connection Error";
}
}
private async Task UpdateWeatherOnly(string lat, string lon)
{
try
{
string weatherUrl = $"https://api.open-meteo.com/v1/forecast?latitude={lat}&longitude={lon}&current_weather=true";
string weatherJson = await httpClient.GetStringAsync(weatherUrl);
using JsonDocument weatherDoc = JsonDocument.Parse(weatherJson);
JsonElement current = weatherDoc.RootElement.GetProperty("current_weather");
currentTemp = $"{current.GetProperty("temperature").GetDouble()}°C";
weatherStatus = MapWeatherCode(current.GetProperty("weathercode").GetInt32());
lastUpdated = DateTime.Now.ToString("HH:mm");
}
catch { /* Ignore background update errors */ }
}
private string MapWeatherCode(int code)
{
return code switch
{
0 => "Clear Sky",
1 or 2 or 3 => "Partly Cloudy",
45 or 48 => "Foggy",
51 or 53 or 55 => "Drizzle",
61 or 63 or 65 => "Rainy",
71 or 73 or 75 => "Snowy",
95 or 96 or 99 => "Thunderstorm",
_ => "Cloudy"
};
}
}
@@ -11,7 +11,18 @@ internal class MainWindowDataContext : INotifyPropertyChanged
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
private static DesktopMagicSettings settings = new(); private static DesktopMagicSettings settings = new();
private bool isLoading = true; private bool isLoading = true;
private string? pluginsSearchText;
public string Title =>
#if DEBUG
$"{App.AppName} - Dev {System.Windows.Forms.Application.ProductVersion}";
#else
$"{App.AppName} - {System.Windows.Forms.Application.ProductVersion}";
#endif
public string AppName => App.AppName;
public DesktopMagicSettings Settings public DesktopMagicSettings Settings
{ {
@@ -33,6 +44,16 @@ internal class MainWindowDataContext : INotifyPropertyChanged
} }
} }
public string? PluginsSearchText
{
get => pluginsSearchText;
set
{
pluginsSearchText = value;
OnPropertyChanged();
}
}
public bool IsAutoStartEnabled public bool IsAutoStartEnabled
{ {
get => StartupManager.IsAutoStartEnabled(); get => StartupManager.IsAutoStartEnabled();
@@ -1,18 +1,19 @@
using DesktopMagic.Helpers; using DesktopMagic.Helpers;
using DesktopMagic.Plugins; using DesktopMagic.Plugins;
using MaterialDesignThemes.Wpf;
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows; using System.Windows;
using System.Windows.Input; using System.Windows.Input;
using Wpf.Ui.Controls;
namespace DesktopMagic.DataContexts; namespace DesktopMagic.DataContexts;
internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand command, 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; public event PropertyChangedEventHandler? PropertyChanged;
@@ -20,7 +21,7 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
public string Name => pluginMetadata.Name; public string Name => pluginMetadata.Name;
public string? Description => pluginMetadata.Description; public string? Description => pluginMetadata.Description?.Trim();
public string Author => pluginMetadata.Author ?? (string)App.LanguageDictionary["unknown"]; public string Author => pluginMetadata.Author ?? (string)App.LanguageDictionary["unknown"];
@@ -35,22 +36,40 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
public string? Path => path; public string? Path => path;
public bool IsLocalPlugin => string.IsNullOrWhiteSpace(pluginMetadata.ProfileUri?.ToString()); public bool IsLocalPlugin => pluginMetadata.IsLocalPlugin;
public ICommand Command => command; 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 public ButtonData OpenButtonData
{ {
get 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 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); _ = 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() private string GetInstallUninstallButtonText()
{ {
return mode switch return mode switch
@@ -94,7 +141,7 @@ internal class PluginEntryDataContext(PluginMetadata pluginMetadata, ICommand co
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); 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 public enum Mode
{ {
@@ -1,6 +1,10 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Media.Imaging;
using Wpf.Ui.Appearance;
namespace DesktopMagic.DataContexts; namespace DesktopMagic.DataContexts;
@@ -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"]; public string LoginButtonText => isAuthenticated ? (string)App.LanguageDictionary["logout"] : (string)App.LanguageDictionary["login"];
protected void OnPropertyChanged([CallerMemberName] string? name = null) protected void OnPropertyChanged([CallerMemberName] string? name = null)
+12 -5
View File
@@ -7,14 +7,15 @@
<ApplicationIcon>icon.ico</ApplicationIcon> <ApplicationIcon>icon.ico</ApplicationIcon>
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent> <RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
<Authors>Stone_Red</Authors> <Authors>Stone_Red</Authors>
<Version>1.2.0.0</Version> <Version>1.3.3.0</Version>
<PackageProjectUrl>https://github.com/Stone-Red-Code/DesktopMagic</PackageProjectUrl> <PackageProjectUrl>https://github.com/Stone-Red-Code/DesktopMagic</PackageProjectUrl>
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl> <RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
<AssemblyVersion>1.2.0.0</AssemblyVersion> <AssemblyVersion>1.3.3.0</AssemblyVersion>
<FileVersion>1.2.0.0</FileVersion> <FileVersion>1.3.3.0</FileVersion>
<TargetFramework>net8.0-windows7.0</TargetFramework> <TargetFramework>net8.0-windows7.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier> <RuntimeIdentifier>win-x64</RuntimeIdentifier>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion> <IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
</PropertyGroup> </PropertyGroup>
@@ -33,6 +34,7 @@
<None Remove="icon_Dark.ico" /> <None Remove="icon_Dark.ico" />
<None Remove="Resources\Images\modio-cog-blue.png" /> <None Remove="Resources\Images\modio-cog-blue.png" />
<None Remove="Resources\Images\modio-logo-bluedark.png" /> <None Remove="Resources\Images\modio-logo-bluedark.png" />
<None Remove="Resources\Images\modio-logo-bluelight.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -42,13 +44,17 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BusyIndicators" Version="2.1.2" /> <PackageReference Include="BusyIndicators" Version="2.1.3" />
<PackageReference Include="CuteUtils" Version="1.0.0" /> <PackageReference Include="CuteUtils" Version="1.0.0" />
<PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" /> <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="Modio" Version="1.0.0" />
<PackageReference Include="NAudio" Version="2.2.1" /> <PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="System.Management" Version="8.0.0" /> <PackageReference Include="System.Management" Version="8.0.0" />
<PackageReference Include="WPF-UI" Version="4.2.0" />
<PackageReference Include="WPF-UI.Markdown" Version="4.0.2" />
<PackageReference Include="WPF-UI.Tray" Version="4.2.0" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@@ -63,6 +69,7 @@
<CopyToOutputDirectory>Always</CopyToOutputDirectory> <CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Resource> </Resource>
<Resource Include="Resources\Images\modio-logo-bluedark.png" /> <Resource Include="Resources\Images\modio-logo-bluedark.png" />
<Resource Include="Resources\Images\modio-logo-bluelight.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+55 -31
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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
xmlns:local="clr-namespace:DesktopMagic.Dialogs" d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
mc:Ignorable="d" ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ResizeMode="NoResize" ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Title="ColorDialog" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="300"
Width="450"
MinHeight="300"
MinWidth="450"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"> ExtendsContentIntoTitleBar="True">
<Grid> <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" /> <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> <DockPanel Grid.Row="0">
<Label Content="A:" Width="20" /> <Label Content="A:" Width="20" />
<Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#00030000" /> <Slider x:Name="alphaSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</DockPanel> </DockPanel>
<DockPanel> <DockPanel Grid.Row="1">
<Label Content="R:" Width="20" /> <Label Content="R:" Width="20" />
<Slider x:Name="redSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#00030000" /> <Slider x:Name="redSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</DockPanel> </DockPanel>
<DockPanel> <DockPanel Grid.Row="2">
<Label Content="G:" Width="20" /> <Label Content="G:" Width="20" />
<Slider x:Name="greenSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#0000FF00" /> <Slider x:Name="greenSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" />
</DockPanel> </DockPanel>
<DockPanel> <DockPanel Grid.Row="3">
<Label Content="B:" Width="20" /> <Label Content="B:" Width="20" />
<Slider x:Name="blueSlider" VerticalAlignment="Center" Maximum="255" ValueChanged="ColorSliders_ValueChanged" TickFrequency="1" IsSnapToTickEnabled="True" Background="#000000FF" /> <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>
<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> </StackPanel>
</Grid> </Grid>
</Window> </ui:FluentWindow>
+6 -3
View File
@@ -4,12 +4,14 @@ using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
using Wpf.Ui.Controls;
namespace DesktopMagic.Dialogs; namespace DesktopMagic.Dialogs;
/// <summary> /// <summary>
/// Interaction logic for ColorDialog.xaml /// Interaction logic for ColorDialog.xaml
/// </summary> /// </summary>
public partial class ColorDialog : Window public partial class ColorDialog : FluentWindow
{ {
public System.Drawing.Color ResultColor { get; private set; } public System.Drawing.Color ResultColor { get; private set; }
@@ -22,6 +24,7 @@ public partial class ColorDialog : Window
Resources.MergedDictionaries.Add(App.LanguageDictionary); Resources.MergedDictionaries.Add(App.LanguageDictionary);
label.Content = content; label.Content = content;
titleBar.Title = title;
Title = title; Title = title;
alphaSlider.Value = defaultColor.A; alphaSlider.Value = defaultColor.A;
@@ -86,8 +89,8 @@ public partial class ColorDialog : Window
ResultBrush = brush; ResultBrush = brush;
ResultColor = systemColor; ResultColor = systemColor;
colorRechtangle.Fill = brush; colorRechtangle.Background = brush;
colorHexTextBox.Foreground = Brushes.Black; colorHexTextBox.Foreground = FindResource("TextFillColorPrimaryBrush") as Brush;
} }
else else
{ {
@@ -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;
}
}
+37 -12
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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopMagic.Dialogs" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
mc:Ignorable="d" d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
Title="InputDialog" ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ResizeMode="NoResize" ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="200"
Width="300"
MinHeight="160"
MinWidth="300"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"> ExtendsContentIntoTitleBar="True">
<Grid> <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" /> <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" /> <ui:TextBox x:Name="textBox" HorizontalAlignment="Stretch" Margin="5" TextWrapping="Wrap" VerticalAlignment="Top" 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> </StackPanel>
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="5">
<Button x:Name="okButton" Content="{DynamicResource ok }" HorizontalAlignment="Left" Margin="0 0 5 0" VerticalAlignment="Top" Width="100" Click="OkButton_Click" Cursor="Hand" />
<Button x:Name="cancelButton" Content="{DynamicResource cancel }" HorizontalAlignment="Left" Margin="0 0 0 0" VerticalAlignment="Top" Width="100" Click="CancelButton_Click" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</Window> </ui:FluentWindow>
+2 -1
View File
@@ -2,7 +2,7 @@
namespace DesktopMagic.Dialogs; namespace DesktopMagic.Dialogs;
public partial class InputDialog : Window public partial class InputDialog : Wpf.Ui.Controls.FluentWindow
{ {
public string ResponseText public string ResponseText
{ {
@@ -17,6 +17,7 @@ public partial class InputDialog : Window
Resources.MergedDictionaries.Add(App.LanguageDictionary); Resources.MergedDictionaries.Add(App.LanguageDictionary);
label.Content = content; label.Content = content;
titleBar.Title = title;
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;
}
}
+58 -42
View File
@@ -1,22 +1,45 @@
<Window x:Class="DesktopMagic.Dialogs.ThemeDialog" <ui:FluentWindow x:Class="DesktopMagic.Dialogs.ThemeDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
xmlns:local="clr-namespace:DesktopMagic.Dialogs" d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
mc:Ignorable="d" ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ResizeMode="NoResize" ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Title="ThemeDialog" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Height="290"
Width="550"
MinHeight="290"
MinWidth="550"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
SizeToContent="WidthAndHeight"> ExtendsContentIntoTitleBar="True">
<Grid> <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" /> <Label x:Name="label" Content="Label" HorizontalAlignment="Left" VerticalAlignment="Top" />
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/> <ColumnDefinition Width="auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch"> <Grid Grid.Column="0" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -33,44 +56,37 @@
<ComboBox x:Name="fontComboBox" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" SelectionChanged="FontComboBox_SelectionChanged" Margin="0,0,0,5" /> <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> <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"> <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" />
<materialDesign:TextFieldAssist.CharacterCounterStyle>
<Style TargetType="TextBlock" />
</materialDesign:TextFieldAssist.CharacterCounterStyle>
</TextBox>
<TextBlock Text="Margin: " Grid.Column="0" Grid.Row="3" VerticalAlignment="Center"></TextBlock> <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"> <ui:NumberBox x:Name="marginTextBox" Minimum="0" Grid.Column="1" Grid.Row="3" ValueChanged="MarginTextBox_ValueChanged" PlaceholderText="Enter a number"/>
<materialDesign:TextFieldAssist.CharacterCounterStyle>
<Style TargetType="TextBlock" />
</materialDesign:TextFieldAssist.CharacterCounterStyle>
</TextBox>
</Grid> </Grid>
<StackPanel Grid.Column="1" Margin="{StaticResource DefaultMargin}" HorizontalAlignment="Stretch"> <Grid 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"> <Grid.RowDefinitions>
<Rectangle x:Name="primaryColorRechtangle" Fill="White" /> <RowDefinition Height="*" />
</Border> <RowDefinition Height="*" />
<Button Content="Change Primary Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangePrimaryColorButton_Click" /> <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>
<DockPanel Margin="0,0,0,5"> <DockPanel Grid.Row="1">
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0"> <Border x:Name="secondaryColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Rectangle x:Name="secondaryColorRechtangle" Fill="White" /> <Button Content="Change Secondary Color" FontWeight="Regular" Click="ChangeSecondaryColorButton_Click" HorizontalAlignment="Stretch" />
</Border>
<Button Content="Change Secondary Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangeSecondaryColorButton_Click" />
</DockPanel> </DockPanel>
<DockPanel> <DockPanel Grid.Row="2">
<Border CornerRadius="1" BorderThickness="1.5" BorderBrush="Gray" Height="23" Width="23" Margin="0,0,5,0"> <Border x:Name="backgroundColorRechtangle" CornerRadius="4" BorderThickness="1" BorderBrush="{DynamicResource ControlElevationBorderBrush}" Background="White" Height="30" Width="30" Margin="0,0,10,0" />
<Rectangle x:Name="backgroundColorRechtangle" Fill="White" /> <Button Content="Change Background Color" FontWeight="Regular" Click="ChangeBackgroundColorButton_Click" HorizontalAlignment="Stretch" />
</Border>
<Button Content="Change Background Color" Height="23" FontSize="12" FontWeight="Regular" Click="ChangeBackgroundColorButton_Click" />
</DockPanel> </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> </StackPanel>
</Grid> </Grid>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,10,0,0"> </ui:FluentWindow>
<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>
</Grid>
</Window>
+16 -27
View File
@@ -10,7 +10,7 @@ namespace DesktopMagic.Dialogs;
/// <summary> /// <summary>
/// Interaction logic for ColorDialog.xaml /// Interaction logic for ColorDialog.xaml
/// </summary> /// </summary>
public partial class ThemeDialog : Window public partial class ThemeDialog : Wpf.Ui.Controls.FluentWindow
{ {
private readonly Theme theme; private readonly Theme theme;
@@ -22,13 +22,14 @@ public partial class ThemeDialog : Window
Resources.MergedDictionaries.Add(App.LanguageDictionary); Resources.MergedDictionaries.Add(App.LanguageDictionary);
cornerRadiusTextBox.Text = theme.CornerRadius.ToString(); cornerRadiusTextBox.Value = theme.CornerRadius;
marginTextBox.Text = theme.Margin.ToString(); marginTextBox.Value = theme.Margin;
primaryColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor)); primaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.PrimaryColor));
secondaryColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.SecondaryColor)); secondaryColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.SecondaryColor));
backgroundColorRechtangle.Fill = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor)); backgroundColorRechtangle.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(theme.BackgroundColor));
label.Content = content; label.Content = content;
titleBar.Title = title;
Title = title; Title = title;
} }
@@ -81,7 +82,7 @@ public partial class ThemeDialog : Window
if (colorDialog.ShowDialog() == true) if (colorDialog.ShowDialog() == true)
{ {
theme.PrimaryColor = colorDialog.ResultColor; theme.PrimaryColor = colorDialog.ResultColor;
primaryColorRechtangle.Fill = colorDialog.ResultBrush; primaryColorRechtangle.Background = colorDialog.ResultBrush;
} }
} }
@@ -95,7 +96,7 @@ public partial class ThemeDialog : Window
if (colorDialog.ShowDialog() == true) if (colorDialog.ShowDialog() == true)
{ {
theme.SecondaryColor = colorDialog.ResultColor; theme.SecondaryColor = colorDialog.ResultColor;
secondaryColorRechtangle.Fill = colorDialog.ResultBrush; secondaryColorRechtangle.Background = colorDialog.ResultBrush;
} }
} }
@@ -109,35 +110,23 @@ public partial class ThemeDialog : Window
if (colorDialog.ShowDialog() == true) if (colorDialog.ShowDialog() == true)
{ {
theme.BackgroundColor = colorDialog.ResultColor; theme.BackgroundColor = colorDialog.ResultColor;
backgroundColorRechtangle.Fill = colorDialog.ResultBrush; backgroundColorRechtangle.Background = colorDialog.ResultBrush;
} }
} }
private void CornerRadiusTextBox_TextChanged(object? sender, TextChangedEventArgs? e) private void MarginTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{ {
bool success = int.TryParse(cornerRadiusTextBox.Text, out int cornerRadius); if (args.NewValue is >= 0)
if (success)
{ {
cornerRadiusTextBox.Foreground = Brushes.Black; theme.Margin = (int)args.NewValue;
theme.CornerRadius = cornerRadius;
}
else
{
cornerRadiusTextBox.Foreground = Brushes.Red;
} }
} }
private void MarginTextBox_TextChanged(object? sender, TextChangedEventArgs? e) private void CornerRadiusTextBox_ValueChanged(object sender, Wpf.Ui.Controls.NumberBoxValueChangedEventArgs args)
{ {
bool success = int.TryParse(marginTextBox.Text, out int margin); if (args.NewValue is >= 0)
if (success)
{ {
marginTextBox.Foreground = Brushes.Black; theme.CornerRadius = (int)args.NewValue;
theme.Margin = margin;
}
else
{
marginTextBox.Foreground = Brushes.Red;
} }
} }
} }
+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("Performance", "CA1822:Mark members as static", Justification = "No need to")]
[assembly: SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "<Pending>")] [assembly: SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "<Pending>")]
[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.Settings")] [assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.Settings")]
[assembly: SuppressMessage("Critical Code Smell", "S2696:Instance members should not write to \"static\" fields", Justification = "<Pending>", Scope = "member", Target = "~P:DesktopMagic.DataContexts.MainWindowDataContext.DialogService")]
[assembly: SuppressMessage("Major Code Smell", "S3885:\"Assembly.Load\" should be used", Justification = "Assembly.Load does not load dependencies", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.ExecuteSource")] [assembly: SuppressMessage("Major Code Smell", "S3885:\"Assembly.Load\" should be used", Justification = "Assembly.Load does not load dependencies", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.ExecuteSource")]
[assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "member", Target = "~M:DesktopMagic.MainWindow.OpenPluginsFolderButton_Click(System.Object,System.Windows.RoutedEventArgs)")] [assembly: SuppressMessage("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "member", Target = "~M:DesktopMagic.MainWindow.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("Minor Code Smell", "S2325:Methods and properties that don't access instance data should be static", Justification = "<Pending>", Scope = "member", Target = "~M:DesktopMagic.MainWindow.ScrollViewer_PreviewMouseWheel(System.Object,System.Windows.Input.MouseWheelEventArgs)")]
[assembly: SuppressMessage("Major Code Smell", "S6966:Awaitable method should be used", Justification = "Breaks stuff", Scope = "member", Target = "~M:DesktopMagic.PluginWindow.UpdateTimer_Elapsed(System.Object,System.Timers.ElapsedEventArgs)")]
+36 -2
View File
@@ -6,16 +6,26 @@ namespace DesktopMagic.Helpers;
internal static partial class MultiColorConverter internal static partial class MultiColorConverter
{ {
public static string ConvertToHex(System.Drawing.Color color) public static string ConvertToHexArgb(System.Drawing.Color color)
{ {
return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}";
} }
public static string ConvertToHex(System.Windows.Media.Color color) public static string ConvertToHexArgb(System.Windows.Media.Color color)
{ {
return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}"; return $"#{color.A:X2}{color.R:X2}{color.G:X2}{color.B:X2}";
} }
public static string ConvertToHexRgba(System.Drawing.Color color)
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
}
public static string ConvertToHexRgba(System.Windows.Media.Color color)
{
return $"#{color.R:X2}{color.G:X2}{color.B:X2}{color.A:X2}";
}
public static bool TryConvertToSystemColor(string hex, out System.Drawing.Color color) public static bool TryConvertToSystemColor(string hex, out System.Drawing.Color color)
{ {
hex = hex.Replace("#", ""); hex = hex.Replace("#", "");
@@ -79,11 +89,35 @@ internal static partial class MultiColorConverter
return System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B); return System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B);
} }
public static System.Windows.Media.Color ConvertToMediaColor(string hex, System.Windows.Media.Color defaultColor)
{
if (TryConvertToMediaColor(hex, out System.Windows.Media.Color color))
{
return color;
}
else
{
return defaultColor;
}
}
public static System.Drawing.Color ConvertToSystemColor(System.Windows.Media.Color color) public static System.Drawing.Color ConvertToSystemColor(System.Windows.Media.Color color)
{ {
return System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B); return System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B);
} }
public static System.Drawing.Color ConvertToSystemColor(string hex, System.Drawing.Color defaultColor)
{
if (TryConvertToSystemColor(hex, out System.Drawing.Color color))
{
return color;
}
else
{
return defaultColor;
}
}
[GeneratedRegex("(?:[0-9a-fA-F]{8})")] [GeneratedRegex("(?:[0-9a-fA-F]{8})")]
private static partial Regex Hex8(); private static partial Regex Hex8();
@@ -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;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media;
namespace DesktopMagic.Helpers; namespace DesktopMagic.Helpers;
internal class SettingElementGenerator(ComboBox optionsComboBox) internal class SettingElementGenerator(uint pluginId)
{ {
private readonly ComboBox optionsComboBox = optionsComboBox; public FrameworkElement? Generate(SettingElement settingElement, System.Windows.Controls.TextBlock textBlock)
public void Generate(SettingElement settingElement, DockPanel dockPanel, TextBlock textBlock)
{ {
dockPanel.UpdateLayout();
textBlock.UpdateLayout();
if (settingElement.Input is DesktopMagic.Api.Settings.Label eLabel) if (settingElement.Input is DesktopMagic.Api.Settings.Label eLabel)
{ {
textBlock.Text = eLabel.Value; textBlock.Text = eLabel.Value;
@@ -32,16 +32,13 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBlock.Text = eLabel.Value; textBlock.Text = eLabel.Value;
}); });
}; };
return null;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.Button eButton) else if (settingElement.Input is DesktopMagic.Api.Settings.Button eButton)
{ {
Button button = new() Wpf.Ui.Controls.Button button = new()
{ {
Content = eButton.Value, Content = eButton.Value,
FontSize = 10,
Height = 20,
Margin = new Thickness(0, 10, 0, 10),
Padding = new Thickness(0),
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch
}; };
@@ -64,16 +61,18 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
}); });
}; };
_ = dockPanel.Children.Add(button); return button;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.CheckBox eCheckBox) else if (settingElement.Input is DesktopMagic.Api.Settings.CheckBox eCheckBox)
{ {
CheckBox checkBox = new() Wpf.Ui.Controls.ToggleSwitch checkBox = new()
{ {
IsChecked = eCheckBox.Value, IsChecked = eCheckBox.Value,
Style = (Style)dockPanel.FindResource("MaterialDesignDarkCheckBox"),
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch,
HorizontalContentAlignment = HorizontalAlignment.Right,
Height = 30,
Style = Application.Current.FindResource("ToggleSwitchContentLeftStyle") as Style
}; };
checkBox.Click += (_s, _e) => checkBox.Click += (_s, _e) =>
{ {
@@ -94,14 +93,14 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
}); });
}; };
_ = dockPanel.Children.Add(checkBox); return checkBox;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.TextBox eTextBox) else if (settingElement.Input is DesktopMagic.Api.Settings.TextBox eTextBox)
{ {
TextBox textBox = new() Wpf.Ui.Controls.TextBox textBox = new()
{ {
Text = eTextBox.Value, Text = eTextBox.Value,
TextWrapping = TextWrapping.Wrap, TextWrapping = TextWrapping.NoWrap,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch
}; };
@@ -123,26 +122,23 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
textBox.Text = eTextBox.Value; textBox.Text = eTextBox.Value;
}); });
}; };
_ = dockPanel.Children.Add(textBox); return textBox;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.IntegerUpDown eIntegerUpDown) else if (settingElement.Input is DesktopMagic.Api.Settings.IntegerUpDown eIntegerUpDown)
{ {
MaterialDesignThemes.Wpf.NumericUpDown numericUpDown = new() Wpf.Ui.Controls.NumberBox numberBox = new()
{ {
Value = eIntegerUpDown.Value, Value = eIntegerUpDown.Value,
Minimum = eIntegerUpDown.Minimum, Minimum = eIntegerUpDown.Minimum,
Maximum = eIntegerUpDown.Maximum, Maximum = eIntegerUpDown.Maximum,
IncreaseContent = new Label() { Content = "+" },
DecreaseContent = new Label() { Content = "" },
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch, HorizontalAlignment = HorizontalAlignment.Stretch
AllowChangeOnScroll = true
}; };
numericUpDown.ValueChanged += (_s, _e) => numberBox.ValueChanged += (_s, _e) =>
{ {
try try
{ {
eIntegerUpDown.Value = numericUpDown.Value; eIntegerUpDown.Value = (int)numberBox.Value;
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -151,12 +147,12 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
}; };
eIntegerUpDown.OnValueChanged += () => 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) else if (settingElement.Input is DesktopMagic.Api.Settings.Slider eSlider)
{ {
@@ -168,7 +164,8 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
TickFrequency = 1, TickFrequency = 1,
IsSnapToTickEnabled = true, IsSnapToTickEnabled = true,
VerticalAlignment = VerticalAlignment.Center, VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 5, 0, 5)
}; };
slider.ValueChanged += (_s, _e) => slider.ValueChanged += (_s, _e) =>
{ {
@@ -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) else if (settingElement.Input is DesktopMagic.Api.Settings.ComboBox eComboBox)
{ {
@@ -220,17 +217,170 @@ internal class SettingElementGenerator(ComboBox optionsComboBox)
comboBox.SelectedItem = eComboBox.Value; comboBox.SelectedItem = eComboBox.Value;
}; };
_ = dockPanel.Children.Add(comboBox); return comboBox;
} }
else if (settingElement.Input is DesktopMagic.Api.Settings.ColorPicker eColorPicker)
{
Grid panel = new()
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
panel.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
panel.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
Border colorPreview = new()
{
Width = 30,
Height = 30,
CornerRadius = new CornerRadius(4),
Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B)),
Margin = new Thickness(0, 0, 10, 0),
BorderBrush = Application.Current.FindResource("ControlElevationBorderBrush") as Brush,
BorderThickness = new Thickness(1)
};
Grid.SetColumn(colorPreview, 0);
Wpf.Ui.Controls.Button browseButton = new()
{
Content = "Select Color",
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
Grid.SetColumn(browseButton, 1);
browseButton.Click += (_s, _e) =>
{
try
{
ColorDialog colorDialog = new("Select a color", eColorPicker.Value)
{
Owner = Application.Current.MainWindow
};
if (colorDialog.ShowDialog() == true)
{
eColorPicker.Value = colorDialog.ResultColor;
colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
}
}
catch (Exception ex)
{
DisplayException(ex.Message);
}
};
eColorPicker.OnValueChanged += () =>
{
colorPreview.Dispatcher.Invoke(() =>
{
colorPreview.Background = new SolidColorBrush(Color.FromArgb(eColorPicker.Value.A, eColorPicker.Value.R, eColorPicker.Value.G, eColorPicker.Value.B));
});
};
_ = panel.Children.Add(colorPreview);
_ = panel.Children.Add(browseButton);
return panel;
}
else if (settingElement.Input is DesktopMagic.Api.Settings.FileSelector eFileSelector)
{
Grid grid = new()
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
Wpf.Ui.Controls.TextBox pathTextBox = new()
{
Text = eFileSelector.Value,
IsReadOnly = true,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 0, 5, 0)
};
Grid.SetColumn(pathTextBox, 0);
Wpf.Ui.Controls.Button browseButton = new()
{
Content = "Browse",
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetColumn(browseButton, 1);
browseButton.Click += (_s, _e) =>
{
try
{
if (eFileSelector.SelectFolder)
{
OpenFolderDialog folderDialog = new()
{
Title = eFileSelector.Title,
InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
};
if (folderDialog.ShowDialog() == true)
{
eFileSelector.Value = folderDialog.FolderName;
pathTextBox.Text = eFileSelector.Value;
}
}
else
{
OpenFileDialog fileDialog = new()
{
Title = eFileSelector.Title,
Filter = eFileSelector.Filter,
InitialDirectory = string.IsNullOrEmpty(eFileSelector.Value) ? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) : System.IO.Path.GetDirectoryName(eFileSelector.Value)
};
if (fileDialog.ShowDialog() == true)
{
eFileSelector.Value = fileDialog.FileName;
pathTextBox.Text = eFileSelector.Value;
}
}
}
catch (Exception ex)
{
DisplayException(ex.Message);
}
};
eFileSelector.OnValueChanged += () =>
{
pathTextBox.Dispatcher.Invoke(() =>
{
pathTextBox.Text = eFileSelector.Value;
});
};
_ = grid.Children.Add(pathTextBox);
_ = grid.Children.Add(browseButton);
return grid;
} }
private void DisplayException(string message) return null;
}
private async void DisplayException(string message)
{ {
App.Logger.LogInfo(message, source: "PluginInput"); App.Logger.LogInfo(message, source: "PluginInput");
_ = MessageBox.Show("File execution error:\n" + message, "Error", MessageBoxButton.OK, MessageBoxImage.Error); Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
int index = MainWindow.WindowNames.IndexOf(optionsComboBox.SelectedItem.ToString() ?? string.Empty); {
Title = "Error",
Content = "File execution error:\n" + message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
int index = Manager.Instance.PluginWindows.FindIndex(p => p.PluginMetadata.Id == pluginId);
PluginWindow window = MainWindow.Windows[index]; if (index >= 0 && index < Manager.Instance.PluginWindows.Count)
{
IPluginWindow window = Manager.Instance.PluginWindows[index];
window?.Exit(); window?.Exit();
} }
}
} }
+14 -7
View File
@@ -3,7 +3,6 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Windows;
namespace DesktopMagic.Helpers; 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(); AutoStartStatus status = IsAutoStartEnabled() ? DisableAutoStart() : EnableAutoStart();
_ = status switch string message = status switch
{ {
AutoStartStatus.Success => MessageBox.Show("Auto-start setting changed successfully."), AutoStartStatus.Success => "Auto-start setting changed successfully.",
AutoStartStatus.AlreadyEnabled => MessageBox.Show("Auto-start is already enabled."), AutoStartStatus.AlreadyEnabled => "Auto-start is already enabled.",
AutoStartStatus.AlreadyDisabled => MessageBox.Show("Auto-start is already disabled."), AutoStartStatus.AlreadyDisabled => "Auto-start is already disabled.",
_ => MessageBox.Show("Failed to change auto-start setting."), _ => "Failed to change auto-start setting.",
}; };
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = App.AppName,
Content = message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
} }
} }
@@ -0,0 +1,23 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace DesktopMagic.Helpers;
public class UnderscoreEscapingConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is string text)
{
// Replaces single underscores with double underscores to escape them in labels
return text.Replace("_", "__");
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
+15
View File
@@ -76,6 +76,21 @@ namespace DesktopMagic
DependencyProperty.RegisterAttached("IsHooked", typeof(WindowLockHook), typeof(WindowPos), DependencyProperty.RegisterAttached("IsHooked", typeof(WindowLockHook), typeof(WindowPos),
new PropertyMetadata(null)); new PropertyMetadata(null));
public static void SetWindowLayer(Window window, string layer)
{
switch (layer)
{
case "Always on Bottom":
window.Topmost = false;
SendWpfWindowBack(window);
SendWpfWindowBack(window);
break;
case "Always on Top":
window.Topmost = true;
break;
}
}
private class WindowLockHook private class WindowLockHook
{ {
private readonly Window Window; private readonly Window Window;
+108 -131
View File
@@ -1,156 +1,133 @@
<Window x:Class="DesktopMagic.MainWindow" <ui:FluentWindow x:Class="DesktopMagic.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator" xmlns:tray="http://schemas.lepo.co/wpfui/2022/xaml/tray"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:pages="clr-namespace:DesktopMagic.Pages"
xmlns:plugins="clr-namespace:DesktopMagic.Plugins"
mc:Ignorable="d" mc:Ignorable="d"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:DesktopMagic" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}" d:DataContext="{d:DesignInstance Type=dataContext:MainWindowDataContext}"
ui:Design.Background="{DynamicResource ApplicationBackgroundBrush}"
ui:Design.Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
WindowCornerPreference="Round"
WindowBackdropType="Tabbed"
Closing="Window_Closing" Closing="Window_Closing"
Closed="Window_Closed" Closed="Window_Closed"
ShowInTaskbar="True" Height="560"
Icon="{StaticResource Icon}" Width="900"
Background="{DynamicResource MaterialDesignPaper}" MinHeight="600"
FontFamily="{DynamicResource MaterialDesignFont}" MinWidth="940"
Height="520" ExtendsContentIntoTitleBar="True"
Width="570"
MinHeight="520"
MinWidth="570"
WindowState="Minimized" WindowState="Minimized"
Loaded="Window_Loaded"> Loaded="Window_Loaded">
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False">
<Grid>
<TextBlock TextWrapping="Wrap" Text="0" VerticalAlignment="Top" FontFamily="Comic Sans MS" Foreground="Transparent" />
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Margin="{StaticResource DefaultMargin}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</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 editLayout}" Click="EditCheckBox_Click" Foreground="Black" BorderBrush="#FF323232" Style="{StaticResource MaterialDesignDarkCheckBox}" IsChecked="True" />
</Grid>
<ScrollViewer Background="#FFBBBBBB" PreviewMouseWheel="ScrollViewer_PreviewMouseWheel">
<ItemsControl ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<CheckBox Click="PluginCheckBox_Click" Content="{Binding Value.Name}" Tag="{Binding Key}" IsChecked="{Binding Value.Enabled}" Style="{StaticResource MaterialDesignDarkCheckBox}" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<DockPanel Grid.Column="1" Margin="2.5,0,0,0"> <ui:TitleBar x:Name="TitleBar" Title="{Binding Title}">
<ComboBox x:Name="optionsComboBox" DisplayMemberPath="Name" DockPanel.Dock="Top" Height="24" SelectedIndex="0" SelectionChanged="OptionsComboBox_SelectionChanged" Background="#FFECECEC" Padding="4" VerticalAlignment="Center" /> <ui:TitleBar.Icon>
<ScrollViewer Background="#FFBBBBBB"> <ui:ImageIcon Source="{StaticResource Icon}" />
<Grid> </ui:TitleBar.Icon>
<StackPanel x:Name="optionsPanel" Margin="3,3,3,0" HorizontalAlignment="Stretch" Visibility="Collapsed"> </ui:TitleBar>
</StackPanel>
</Grid> <tray:NotifyIcon Grid.Row="0" FocusOnLeftClick="True" MenuOnRightClick="True" TooltipText="{Binding AppName}" Icon="{StaticResource Icon}" LeftClick="NotifyIcon_LeftClick">
</ScrollViewer> <tray:NotifyIcon.Menu>
</DockPanel> <ContextMenu>
</Grid> <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 Grid.Row="1">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="*" /> <RowDefinition Height="*"/>
<RowDefinition Height="110" /> <RowDefinition Height="Auto"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<ui:NavigationView x:Name="NavigationView" Grid.Row="0" IsBackButtonVisible="Collapsed" IsPaneToggleVisible="True" OpenPaneLength="200" Loaded="NavigationView_Loaded">
<ui:NavigationView.MenuItems>
<ui:NavigationViewItem Content="{DynamicResource plugins}" TargetPageType="{x:Type pages:MainPage}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Apps24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource themes}" TargetPageType="{x:Type pages:ThemePage}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Color24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource pluginManager}" TargetPageType="{x:Type plugins:PluginManager}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="AppsAddIn24" />
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
</ui:NavigationView.MenuItems>
<ui:NavigationView.FooterMenuItems>
<ui:NavigationViewItem Content="Documentation" Click="DocumentationNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="LightbulbFilament24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource requestFeature}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="ChatHelp24"/>
</ui:NavigationViewItem.Icon>
<ui:NavigationViewItem.MenuItems>
<ui:NavigationViewItem Content="GitHub" Click="RequestFeatureNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Globe24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource email}" Click="RequestFeatureEmailNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Mail24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
</ui:NavigationViewItem.MenuItems>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource reportBug}">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Bug24"/>
</ui:NavigationViewItem.Icon>
<ui:NavigationViewItem.MenuItems>
<ui:NavigationViewItem Content="GitHub" Click="ReportBugNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Globe24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
<ui:NavigationViewItem Content="{DynamicResource email}" Click="ReportBugEmailNavigationViewItem_Click">
<ui:NavigationViewItem.Icon>
<ui:SymbolIcon Symbol="Mail24"/>
</ui:NavigationViewItem.Icon>
</ui:NavigationViewItem>
</ui:NavigationViewItem.MenuItems>
</ui:NavigationViewItem>
</ui:NavigationView.FooterMenuItems>
</ui:NavigationView>
<Border Grid.Row="1" Background="{DynamicResource ControlFillColorDefaultBrush}" BorderBrush="{DynamicResource ControlStrokeColorDefaultBrush}" BorderThickness="0,1,0,0" Padding="5 2 5 5">
<Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition Width="Auto"/>
<ColumnDefinition /> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"/>
<ListBox x:Name="themesListBox" Grid.Row="0" Grid.ColumnSpan="1" Margin="{StaticResource DefaultMargin}" ItemsSource="{Binding Settings.Themes}" DisplayMemberPath="Name" SelectedValue="{Binding Settings.CurrentLayout.CurrentThemeName}" SelectedValuePath="Name" />
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" Margin="{StaticResource DefaultMargin}">
<Button x:Name="addThemeButton" Margin="0,0,0,5" HorizontalAlignment="Stretch" Click="AddThemeButton_Click" FontWeight="Regular">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Add" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource newTheme}" />
</StackPanel>
</Button>
<Button x:Name="deleteThemeButton" Margin="0,0,0,5" HorizontalAlignment="Stretch" Click="DeleteThemeButton_Click" FontWeight="Regular">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="DeleteOutline" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource deleteTheme}" />
</StackPanel>
</Button>
<Button x:Name="changeThemeButton" Margin="0,0,0,5" HorizontalAlignment="Stretch" Click="ChangeThemeButton_Click" FontWeight="Regular">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="PaletteOutline" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource changeTheme}" />
</StackPanel>
</Button>
</StackPanel>
<Grid Grid.Row="1" Grid.ColumnSpan="2" VerticalAlignment="Bottom">
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Rectangle Fill="#FFC5C5C5" Stroke="#FFC5C5C5" Grid.ColumnSpan="3" /> <ui:HyperlinkButton FontWeight="Bold" Content="{DynamicResource developedBy}" NavigateUri="https://me.stone-red.net"/>
<StackPanel Margin="{StaticResource DefaultMargin}" VerticalAlignment="Bottom"> <ui:ToggleSwitch x:Name="autoStartCheckBox" Style="{StaticResource ToggleSwitchContentLeftStyle}" Grid.Column="2" Margin="0 0 5 0" Content="{DynamicResource autoStart}" IsChecked="{Binding IsAutoStartEnabled}" VerticalAlignment="Center"/>
<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>
</Border>
</Grid> </Grid>
<ContentPresenter x:Name="rootContentDialog" Grid.Row="1" Grid.RowSpan="2" />
</Grid> </Grid>
</Grid> </ui:FluentWindow>
</busyIndicator:BusyMask>
</Window>
+178 -532
View File
@@ -1,283 +1,84 @@
using DesktopMagic.BuiltInPlugins;
using DesktopMagic.DataContexts; using DesktopMagic.DataContexts;
using DesktopMagic.Dialogs; using DesktopMagic.Dialogs;
using DesktopMagic.Helpers; using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using System; using System;
using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.Net.Http;
using System.Linq; using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls;
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();
private readonly System.Windows.Forms.NotifyIcon notifyIcon = new();
private readonly MainWindowDataContext mainWindowDataContext = new();
private readonly Dictionary<PluginMetadata, Type> builtInPlugins = new()
{
{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)}
};
private bool loaded = false;
private bool blockWindowsClosing = true;
public static List<PluginWindow> Windows { get; } = [];
public static List<string> WindowNames { get; } = [];
private DesktopMagicSettings Settings
{
get => mainWindowDataContext.Settings;
set => mainWindowDataContext.Settings = value;
}
[Obsolete]
public MainWindow() public MainWindow()
{ {
DataContext = mainWindowDataContext; SystemThemeWatcher.Watch(this);
try
{
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(); InitializeComponent();
DataContext = _mainWindowDataContext;
Resources.MergedDictionaries.Add(App.LanguageDictionary); Resources.MergedDictionaries.Add(App.LanguageDictionary);
App.DialogService.SetDialogHost(rootContentDialog);
#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 async void Window_Loaded(object sender, RoutedEventArgs e)
private readonly Dictionary<uint, InternalPluginData> plugins = [];
private void Window_Loaded(object sender, RoutedEventArgs e)
{ {
try try
{ {
//Write To Log File and Load Elements App.Logger.LogInfo("Loading application", source: "MainWindow");
App.Logger.LogInfo("Loading Plugin names", source: "Main"); _mainWindowDataContext.IsLoading = true;
LoadPlugins();
App.Logger.LogInfo("Loading Layout names", source: "Main");
LoadSettings();
App.Logger.LogInfo("Loading Layout", source: "Main");
LoadLayout();
loaded = true; // Load plugins and settings through manager
App.Logger.LogInfo("Window Loaded", source: "Main"); _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");
} }
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.LogError(ex.Message, source: "Main"); App.Logger.LogError(ex.Message, source: "MainWindow");
_ = MessageBox.Show(ex.ToString(), App.AppName, MessageBoxButton.OK, MessageBoxImage.Error); Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
}
}
private void LoadPlugins()
{ {
mainWindowDataContext.IsLoading = true; Title = App.AppName,
plugins.Clear(); Content = ex.ToString(),
CloseButtonText = "Ok"
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()
}; };
_ = await messageBox.ShowDialogAsync();
} }
else
{
window = new PluginWindow(internalPluginData.Metadata, pluginSettings, internalPluginData.DirectoryPath)
{
Title = internalPluginData.Metadata.Id.ToString()
};
} }
Action? onPluginLoaded = null; private async void NavigationView_Loaded(object sender, RoutedEventArgs e)
onPluginLoaded = () =>
{ {
Dispatcher.Invoke(() => _ = NavigationView.Navigate(typeof(Pages.MainPage));
{
if (!optionsComboBox.Items.Contains(internalPluginData.Metadata))
{
_ = optionsComboBox.Items.Add(internalPluginData.Metadata);
}
optionsComboBox.SelectedIndex = -1;
optionsComboBox.SelectedIndex = optionsComboBox.Items.IndexOf(internalPluginData.Metadata);
window.PluginLoaded -= onPluginLoaded;
});
};
window.OnExit += () =>
{
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);
}
private void DisplayWindow_Closing(object? sender, System.ComponentModel.CancelEventArgs e)
{
e.Cancel = blockWindowsClosing;
} }
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ {
SaveSettings(); _manager.SaveSettings();
if (blockWindowsClosing) if (_manager.BlockWindowsClosing)
{ {
e.Cancel = true; e.Cancel = true;
_manager.SetEditMode(false);
editCheckBox.IsChecked = false;
EditCheckBox_Click(null, null);
ShowInTaskbar = false; ShowInTaskbar = false;
WindowState = WindowState.Minimized; WindowState = WindowState.Minimized;
Visibility = Visibility.Collapsed; Visibility = Visibility.Collapsed;
@@ -288,106 +89,18 @@ namespace DesktopMagic
{ {
Visibility = Visibility.Collapsed; Visibility = Visibility.Collapsed;
UpdateLayout(); UpdateLayout();
foreach (Window window in Windows) _manager.CloseAllPluginWindows();
{
window.Hide();
}
Environment.Exit(0); Environment.Exit(0);
} }
#endregion Windows private void NotifyIcon_MouseClick(object? sender, System.Windows.Forms.MouseEventArgs e)
#region Options
private void AddThemeButton_Click(object sender, RoutedEventArgs e)
{ {
InputDialog inputDialog = new((string)FindResource("enterThemeName")) if (e.Button == System.Windows.Forms.MouseButtons.Left)
{ {
Owner = this RestoreWindow();
};
if (inputDialog.ShowDialog() == true)
{
if (Settings.Themes.Any(l => l.Name.Trim() == inputDialog.ResponseText.Trim()))
{
_ = MessageBox.Show((string)FindResource("themeAlreadyExists"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Settings.Themes.Add(new Theme(inputDialog.ResponseText.Trim()));
Settings.CurrentLayout.CurrentThemeName = inputDialog.ResponseText.Trim();
SaveSettings();
} }
} }
private void DeleteThemeButton_Click(object sender, RoutedEventArgs e)
{
if (Settings.Themes.Count <= 1)
{
_ = MessageBox.Show((string)FindResource("cannotDeleteLastTheme"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Settings.Themes.Remove((Theme)themesListBox.SelectedItem);
SaveSettings();
}
private void ChangeThemeButton_Click(object sender, RoutedEventArgs e)
{
Theme theme = themesListBox.SelectedItem as Theme ?? Settings.CurrentLayout.Theme;
ThemeDialog themeDialog = new(theme.Name, theme, App.AppName)
{
Owner = this
};
themeDialog.ShowDialog();
}
private void OptionsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
optionsPanel.Visibility = Visibility.Visible;
optionsPanel.Children.Clear();
optionsPanel.UpdateLayout();
if (optionsComboBox.SelectedItem is null)
{
return;
}
bool success = Settings.CurrentLayout.Plugins.TryGetValue(((PluginMetadata)optionsComboBox.SelectedItem).Id, out Settings.PluginSettings? pluginSettings);
if (!success || pluginSettings is null || pluginSettings.Settings.Count == 0)
{
_ = optionsPanel.Children.Add(new TextBlock() { Text = (string)FindResource("noOptions") });
return;
}
SettingElementGenerator settingElementGenerator = new SettingElementGenerator(optionsComboBox);
foreach (SettingElement settingElement in pluginSettings.Settings)
{
DockPanel dockPanel = new()
{
LastChildFill = true,
HorizontalAlignment = HorizontalAlignment.Stretch,
Margin = new Thickness(0, 0, 0, 5)
};
_ = optionsPanel.Children.Add(dockPanel);
TextBlock textBlock = new()
{
Text = $"{settingElement.Name}:",
Padding = new Thickness(0, 0, 3, 0),
VerticalAlignment = VerticalAlignment.Center
};
_ = dockPanel.Children.Add(textBlock);
settingElementGenerator.Generate(settingElement, dockPanel, textBlock);
}
}
#endregion Options
internal void RestoreWindow() internal void RestoreWindow()
{ {
for (int i = 0; i < 10; i++) for (int i = 0; i < 10; i++)
@@ -403,200 +116,14 @@ namespace DesktopMagic
private void Quit() private void Quit()
{ {
blockWindowsClosing = false; _manager.BlockWindowsClosing = false;
Close(); Close();
} }
private void OpenPluginsFolderButton_Click(object sender, RoutedEventArgs e) private void ReportBugNavigationViewItem_Click(object sender, RoutedEventArgs e)
{ {
_ = Process.Start("explorer.exe", App.PluginsPath); string uri = "https://github.com/Stone-Red-Code/DesktopMagic/issues/new?template=bug_report.md";
} ProcessStartInfo psi = new()
private void ScrollViewer_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
{
ScrollViewer scv = (ScrollViewer)sender;
scv.ScrollToVerticalOffset(scv.VerticalOffset - e.Delta);
e.Handled = true;
}
#region Layout
private readonly JsonSerializerOptions jsonSettingsOptions = new()
{
Converters =
{
new ColorJsonConverter()
}
};
private void LayoutsComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (loaded)
{
SaveSettings();
LoadLayout(false);
}
}
private void NewLayoutButton_Click(object sender, RoutedEventArgs e)
{
InputDialog inputDialog = new((string)FindResource("enterLayoutName"))
{
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();
}
}
private void RemoveLayoutButton_Click(object sender, RoutedEventArgs e)
{
if (Settings.Layouts.Count <= 1)
{
_ = MessageBox.Show((string)FindResource("cannotDeleteLastLayout"), App.AppName, MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
Settings.Layouts.Remove(Settings.CurrentLayout);
SaveSettings();
}
private void SaveSettings()
{
if (!loaded)
{
return;
}
string json = JsonSerializer.Serialize(Settings, jsonSettingsOptions);
File.WriteAllText(Path.Combine(App.ApplicationDataPath, "settings.json"), json);
}
private void LoadSettings()
{
if (!File.Exists(Path.Combine(App.ApplicationDataPath, "settings.json")))
{
Settings = new DesktopMagicSettings();
Settings.Layouts.Add(new Layout((string)FindResource("default")));
Settings.Themes.Add(new Theme((string)FindResource("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((string)FindResource("default")));
}
if (Settings.Themes.Count == 0)
{
Settings.Themes.Add(new Theme((string)FindResource("default")));
}
}
private void LoadLayout(bool minimize = true)
{
mainWindowDataContext.IsLoading = true;
blockWindowsClosing = false;
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
{ {
UseShellExecute = true, UseShellExecute = true,
FileName = uri FileName = uri
@@ -604,22 +131,141 @@ namespace DesktopMagic
_ = Process.Start(psi); _ = Process.Start(psi);
} }
private void PluginManagerButton_Click(object sender, RoutedEventArgs e) private void ReportBugEmailNavigationViewItem_Click(object sender, RoutedEventArgs e)
{ {
PluginManager pluginManager = new PluginManager string uri = "mailto:[email protected]";
ProcessStartInfo psi = new()
{ {
Owner = this UseShellExecute = true,
FileName = uri
}; };
pluginManager.ShowDialog(); _ = Process.Start(psi);
LoadPlugins();
LoadLayout(Visibility != Visibility.Visible);
}
} }
internal class InternalPluginData(PluginMetadata pluginMetadata, string directoryPath) private void RequestFeatureNavigationViewItem_Click(object sender, RoutedEventArgs e)
{ {
public PluginMetadata Metadata { get; set; } = pluginMetadata; string uri = "https://github.com/Stone-Red-Code/DesktopMagic/issues/new?template=feature_request.md";
public string DirectoryPath { get; set; } = directoryPath; ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void RequestFeatureEmailNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "mailto:[email protected]";
ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void DocumentationNavigationViewItem_Click(object sender, RoutedEventArgs e)
{
string uri = "https://github.com/Stone-Red-Code/DesktopMagic";
ProcessStartInfo psi = new()
{
UseShellExecute = true,
FileName = uri
};
_ = Process.Start(psi);
}
private void NotifyIcon_LeftClick(Wpf.Ui.Tray.Controls.NotifyIcon sender, RoutedEventArgs e)
{
RestoreWindow();
}
private void SettingsMenuItem_Click(object sender, RoutedEventArgs e)
{
RestoreWindow();
_ = NavigationView.Navigate(typeof(Pages.MainPage));
}
private void EditLayoutMenuItem_Click(object sender, RoutedEventArgs e)
{
_manager.SetEditMode(!_manager.IsEditMode);
}
private void QuitMenuItem_Click(object sender, RoutedEventArgs e)
{
Quit();
}
private async Task ShowLatestReleaseInfoAfterUpdateAsync()
{
string currentVersion = Assembly.GetExecutingAssembly().GetName().Version?.ToString() ?? "0.0.0.0";
string? lastShownVersion = _manager.Settings.ReleaseInfoLastAppVersion;
if (string.Equals(lastShownVersion, currentVersion, StringComparison.Ordinal))
{
return;
}
GitHubLatestRelease? latestRelease = await GetLatestReleaseInfoAsync();
if (latestRelease is null)
{
return;
}
string releaseName = !string.IsNullOrWhiteSpace(latestRelease.Name) ? latestRelease.Name : latestRelease.TagName;
string publishedAt = latestRelease.PublishedAt?.ToLocalTime().ToString("g") ?? (string)FindResource("unknown");
string publishedAtText = string.Format((string)FindResource("releaseInfoPublishedFormat"), publishedAt);
string releaseNotesMarkdown = string.IsNullOrWhiteSpace(latestRelease.Body) ? "-" : latestRelease.Body.Trim();
ReleaseInfoDialog releaseInfoDialog = new(
(string)FindResource("releaseInfoTitle"),
releaseName,
publishedAtText,
releaseNotesMarkdown,
(string)FindResource("openReleasePage"),
(string)FindResource("ok"))
{
Owner = this,
Topmost = true
};
_ = releaseInfoDialog.ShowDialog();
if (releaseInfoDialog.OpenReleaseRequested)
{
ProcessStartInfo processStartInfo = new()
{
UseShellExecute = true,
FileName = latestRelease.HtmlUrl
};
_ = Process.Start(processStartInfo);
}
_manager.Settings.ReleaseInfoLastAppVersion = currentVersion;
_manager.SaveSettings();
}
private static async Task<GitHubLatestRelease?> GetLatestReleaseInfoAsync()
{
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;
}
+53 -1
View File
@@ -1,8 +1,12 @@
using DesktopMagic.Api; using CuteUtils.Logging;
using DesktopMagic.Api;
using DesktopMagic.Settings; using DesktopMagic.Settings;
using System.Drawing; using System.Drawing;
using Wpf.Ui.Controls;
namespace DesktopMagic.Plugins; namespace DesktopMagic.Plugins;
internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : IPluginData internal class PluginData(PluginWindow window, PluginSettings pluginSettings) : IPluginData
@@ -19,8 +23,56 @@ internal class PluginData(PluginWindow window, PluginSettings pluginSettings) :
public string PluginPath => window.PluginFolderPath; public string PluginPath => window.PluginFolderPath;
public void Log(string message, LogLevel level = LogLevel.Info)
{
LogSeverity severity = level switch
{
LogLevel.Info => LogSeverity.Info,
LogLevel.Warning => LogSeverity.Warn,
LogLevel.Error => LogSeverity.Error,
_ => LogSeverity.Info,
};
App.Logger.Log($"\"{PluginName}\" - {message}", "Plugin", severity);
}
public void ShowMessage(string message, string? title = null)
{
title ??= PluginName;
_ = window.Dispatcher.Invoke(async () =>
{
System.Windows.Window temporaryOwner = new()
{
AllowsTransparency = true,
ShowInTaskbar = false,
WindowStyle = System.Windows.WindowStyle.None,
Background = System.Windows.Media.Brushes.Transparent,
Topmost = true,
};
temporaryOwner.Show();
MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Owner = temporaryOwner,
Title = $"{App.AppName} - {title}",
Content = message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
temporaryOwner.Close();
});
}
public void UpdateWindow() public void UpdateWindow()
{ {
window.UpdatePluginWindow(); window.UpdatePluginWindow();
} }
public void SaveState()
{
window.SavePluginState();
}
} }
+13 -23
View File
@@ -2,8 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 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:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:local="clr-namespace:DesktopMagic.Plugins" xmlns:local="clr-namespace:DesktopMagic.Plugins"
xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts" xmlns:dataContext="clr-namespace:DesktopMagic.DataContexts"
d:DataContext="{d:DesignInstance Type=dataContext:PluginEntryDataContext}" d:DataContext="{d:DesignInstance Type=dataContext:PluginEntryDataContext}"
@@ -12,11 +12,12 @@
Background="{DynamicResource MaterialDesignPaper}" Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}" FontFamily="{DynamicResource MaterialDesignFont}"
Visibility="{Binding Visibility}"> Visibility="{Binding Visibility}">
<Grid Background="White" Margin="10"> <ui:Card Padding="5">
<Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="2*" /> <RowDefinition Height="2*" />
<RowDefinition Height="auto" /> <RowDefinition Height="auto" />
<RowDefinition Height="3*" /> <RowDefinition Height="auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
@@ -24,40 +25,29 @@
<ColumnDefinition /> <ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border HorizontalAlignment="Center" Margin="10" Width="100" Height="100" BorderThickness="1" BorderBrush="LightGray"> <Image Width="100" Height="100" Margin="5" Source="{Binding Logo, FallbackValue={StaticResource ModioCogBlue}, TargetNullValue={StaticResource ModioCogBlue}}" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
<Image Source="{Binding Logo, FallbackValue={StaticResource ModioCogBlue}, TargetNullValue={StaticResource ModioCogBlue}}" Width="100" Height="100" />
</Border>
<StackPanel Grid.Column="1" VerticalAlignment="Center"> <StackPanel Grid.Column="1" VerticalAlignment="Center">
<Label Content="{Binding Author}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource authorFormat}" /> <Label Content="{Binding Author, Converter={StaticResource UnderscoreEscapingConverter}}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource authorFormat}" />
<Label Content="{Binding DateAdded}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource addedFormat}" /> <Label Content="{Binding DateAdded}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource addedFormat}" />
<Label Content="{Binding DateUpdated}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource updatedFormat}" /> <Label Content="{Binding DateUpdated}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource updatedFormat}" />
<Label Content="{Binding Version}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource versionFormat}" /> <Label Content="{Binding Version}" HorizontalAlignment="Center" ContentStringFormat="{DynamicResource versionFormat}" />
</StackPanel> </StackPanel>
<Grid Grid.Row="1" Grid.ColumnSpan="2"> <Grid Grid.Row="1" Grid.ColumnSpan="2" Margin="0 5">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition /> <ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Column="0" Command="{Binding InstallUninstallButtonData.Command}" IsEnabled="{Binding InstallUninstallButtonData.IsEnabled}" VerticalAlignment="Bottom" HorizontalAlignment="Stretch" Margin="10 5 5 0"> <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"/>
<StackPanel Orientation="Horizontal"> <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"/>
<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>
</Grid> </Grid>
<StackPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="10"> <StackPanel Grid.Row="2" Grid.ColumnSpan="2" Margin="5">
<Label Content="{Binding Name}" FontSize="20" Padding="0" /> <ui:TextBlock Text="{Binding Name, Converter={StaticResource UnderscoreEscapingConverter}}" FontTypography="Title" Padding="0" Margin="0 0 0 5" />
<TextBlock Text="{Binding Description}" TextWrapping="Wrap" /> <TextBlock Text="{Binding Description}" TextWrapping="Wrap" Margin="0" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</ui:Card>
</UserControl> </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="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator" xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:local="clr-namespace:DesktopMagic.Plugins" xmlns:local="clr-namespace:DesktopMagic.Plugins"
xmlns:dataContexts="clr-namespace:DesktopMagic.DataContexts" xmlns:dataContexts="clr-namespace:DesktopMagic.DataContexts"
mc:Ignorable="d" mc:Ignorable="d"
d:DataContext="{d:DesignInstance Type=dataContexts:PluginManagerDataContext}" d:DataContext="{d:DesignInstance Type=dataContexts:PluginManagerDataContext}"
Title="{DynamicResource pluginManager}" d:DesignHeight="450"
Height="450" d:DesignWidth="800"
Width="800" ScrollViewer.CanContentScroll="False"
MinHeight="520" Title="{DynamicResource pluginManager}">
MinWidth="700"
WindowStyle="ToolWindow"
Background="{DynamicResource MaterialDesignPaper}"
FontFamily="{DynamicResource MaterialDesignFont}">
<busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="Cogs" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" IsBusyAtStartup="False"> <busyIndicator:BusyMask x:Name="BusyIndicator" IsBusy="{Binding IsLoading}" IndicatorType="ThreeDots" BusyContent="Please wait..." BusyContentMargin="0,20,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
<Grid Margin="5"> <Grid Margin="{StaticResource DefaultMargin}">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="auto" /> <RowDefinition Height="auto" />
<RowDefinition Height="auto" /> <RowDefinition Height="auto" />
@@ -28,21 +24,29 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition />
<ColumnDefinition Width="5" />
<ColumnDefinition /> <ColumnDefinition />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Label Content="{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"> <busyIndicator:BusyMask Grid.Row="2" IsBusy="{Binding IsSearching}" IndicatorType="ThreeDots" BusyContent="Searching..." BusyContentMargin="0,50,0,0" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Background="{DynamicResource ControlStrokeColorDefaultBrush}">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <ScrollViewer VerticalScrollBarVisibility="Auto" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top" Margin="0 0 2.5 0">
<ItemsControl ItemsSource="{Binding AllPlugins}"> <ItemsControl ItemsSource="{Binding AllPlugins}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1"> <local:ModEntry Margin="0 0 0 5" />
<local:ModEntry />
</Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
<ItemsControl.Style> <ItemsControl.Style>
@@ -64,17 +68,11 @@
</ScrollViewer> </ScrollViewer>
</busyIndicator:BusyMask> </busyIndicator:BusyMask>
<Label Grid.Column="1" Content="{DynamicResource installedPlugins}" FontSize="20" /> <ScrollViewer VerticalScrollBarVisibility="Auto" Grid.Row="2" Grid.Column="2" VerticalAlignment="Stretch" Padding="0" VerticalContentAlignment="Top">
<ItemsControl ItemsSource="{Binding InstalledPlugins}" Margin="2.5 0 0 0">
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding InstalledPluginsSearchText, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" TextChanged="InstalledPluginsSearchTextBox_TextChanged" materialDesign:HintAssist.Hint="Search..." materialDesign:TextFieldAssist.HasLeadingIcon="True" materialDesign:TextFieldAssist.LeadingIcon="Search" />
<ScrollViewer Grid.Row="2" Grid.Column="1" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding InstalledPlugins}">
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border BorderBrush="LightGray" BorderThickness="1"> <local:ModEntry Margin="0 0 0 5"/>
<local:ModEntry />
</Border>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
<ItemsControl.Style> <ItemsControl.Style>
@@ -95,23 +93,24 @@
</ItemsControl> </ItemsControl>
</ScrollViewer> </ScrollViewer>
<Border Grid.Row="3" Grid.ColumnSpan="2" Padding="0 5 0 0" BorderThickness="0 2 0 0" BorderBrush="DarkGray"> <ui:Card Grid.Row="3" Grid.ColumnSpan="3" Padding="5" Margin="0 5 0 0">
<DockPanel> <Grid>
<Image Source="{StaticResource ModioLogoBlueDark}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" /> <Grid.ColumnDefinitions>
<Button DockPanel.Dock="Right" HorizontalAlignment="Right" Click="LogInButton_Click"> <ColumnDefinition Width="*" />
<StackPanel Orientation="Horizontal"> <ColumnDefinition Width="15" />
<materialDesign:PackIcon Kind="Login" Margin="0 0 10 0" /> <ColumnDefinition Width="1.05*" />
<TextBlock Text="{Binding LoginButtonText}" /> <ColumnDefinition Width="5.5" />
</StackPanel> <ColumnDefinition Width="1.05*" />
</Button> <ColumnDefinition Width="5" />
<Button DockPanel.Dock="Right" HorizontalAlignment="Right" Margin="0 0 10 0" Click="CreatePluginButton_Click"> <ColumnDefinition Width="1.05*" />
<StackPanel Orientation="Horizontal"> </Grid.ColumnDefinitions>
<materialDesign:PackIcon Kind="Add" Margin="0 0 10 0" />
<TextBlock Text="{DynamicResource createNewPlugin}" /> <Image Source="{Binding ModIoIcon}" Cursor="Hand" Height="30" MouseUp="Image_MouseUp" HorizontalAlignment="Left" />
</StackPanel> <ui:Button Content="{DynamicResource createNewPlugin}" Icon="{ui:SymbolIcon Add24}" Grid.Column="2" HorizontalAlignment="Stretch" Click="CreatePluginButton_Click"/>
</Button> <ui:Button Content="{DynamicResource reloadPlugins}" Icon="{ui:SymbolIcon ArrowClockwise24}" Grid.Column="4" HorizontalAlignment="Stretch" Click="ReloadPluginsButton_Click"/>
</DockPanel> <ui:Button Content="{Binding LoginButtonText}" Icon="{ui:SymbolIcon PersonKey20}" Grid.Column="6" HorizontalAlignment="Stretch" Click="LogInButton_Click"/>
</Border> </Grid>
</ui:Card>
</Grid> </Grid>
</busyIndicator:BusyMask> </busyIndicator:BusyMask>
</Window> </Page>
+340 -72
View File
@@ -8,7 +8,6 @@ using Modio.Models;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
@@ -19,6 +18,7 @@ using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading; using System.Windows.Threading;
using System.Xml.Linq; using System.Xml.Linq;
@@ -30,7 +30,7 @@ namespace DesktopMagic.Plugins;
/// <summary> /// <summary>
/// Interaction logic for PluginManager.xaml /// Interaction logic for PluginManager.xaml
/// </summary> /// </summary>
public partial class PluginManager : Window public partial class PluginManager : Page
{ {
private const int ModIoGameId = 5665; private const int ModIoGameId = 5665;
private const string ModIoApiKey = "88e6ea774c3a502b06114e7fee0829ac"; private const string ModIoApiKey = "88e6ea774c3a502b06114e7fee0829ac";
@@ -38,6 +38,9 @@ public partial class PluginManager : Window
private readonly PluginManagerDataContext pluginManagerDataContext = new(); private readonly PluginManagerDataContext pluginManagerDataContext = new();
private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins"); private readonly string pluginsPath = Path.Combine(App.ApplicationDataPath, "Plugins");
private readonly string pluginDevelopmentPath = Path.Combine(App.ApplicationDataPath, "PluginDevelopment"); private readonly string pluginDevelopmentPath = Path.Combine(App.ApplicationDataPath, "PluginDevelopment");
private readonly Manager _manager = Manager.Instance;
private bool changed = false;
private readonly DispatcherTimer searchTimer = new() private readonly DispatcherTimer searchTimer = new()
{ {
@@ -55,11 +58,13 @@ public partial class PluginManager : Window
if (modIoAccessToken is null) if (modIoAccessToken is null)
{ {
modIoClient = new Client(new Credentials(ModIoApiKey)); modIoClient = new Client(new Credentials(ModIoApiKey));
App.Logger.LogInfo("Initialized mod.io client without authentication", source: "PluginManager");
} }
else else
{ {
modIoClient = new Client(new Credentials(ModIoApiKey, modIoAccessToken)); modIoClient = new Client(new Credentials(ModIoApiKey, modIoAccessToken));
pluginManagerDataContext.IsAuthenticated = true; pluginManagerDataContext.IsAuthenticated = true;
App.Logger.LogInfo("Initialized mod.io client with authentication", source: "PluginManager");
} }
InitializeComponent(); InitializeComponent();
@@ -70,65 +75,87 @@ public partial class PluginManager : Window
searchTimer.Stop(); searchTimer.Stop();
await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText); await SearchAllPlugins(pluginManagerDataContext.AllPluginsSearchText);
}; };
Loaded += PluginManager_Loaded;
Unloaded += PluginManager_Unloaded;
} }
public async Task Remove(string pluginPath, uint id) private async void PluginManager_Loaded(object sender, RoutedEventArgs e)
{ {
pluginManagerDataContext.IsLoading = true; await InitializePluginManager();
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
if (Directory.Exists(pluginPath))
{
try
{
Directory.Delete(pluginPath, true);
} }
catch (Exception ex)
private void PluginManager_Unloaded(object sender, RoutedEventArgs e)
{ {
_ = MessageBox.Show(ex.Message, "Plugin Manager", MessageBoxButton.OK, MessageBoxImage.Error); searchTimer.Stop();
App.Logger.LogError(ex.Message, source: "PluginManager");
if (changed)
{
_manager.ReloadPlugins();
} }
} }
if (pluginEntryDataContext is not null) private async Task InitializePluginManager()
{ {
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext); App.Logger.LogInfo("Initializing Plugin Manager", source: "PluginManager");
}
if (pluginManagerDataContext.IsAuthenticated)
{
await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
}
pluginManagerDataContext.IsLoading = false;
}
protected override async void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
HashSet<uint> pluginIds = []; HashSet<uint> pluginIds = [];
pluginManagerDataContext.IsLoading = true;
pluginManagerDataContext.InstalledPlugins.Clear();
pluginManagerDataContext.AllPlugins.Clear();
foreach (string pluginPath in Directory.GetDirectories(pluginsPath)) foreach (string pluginPath in Directory.GetDirectories(pluginsPath))
{ {
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json"); string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
if (!File.Exists(pluginMetadataPath)) if (!File.Exists(pluginMetadataPath))
{ {
App.Logger.LogWarn($"Plugin metadata not found at: {pluginMetadataPath}", source: "PluginManager");
continue; continue;
} }
PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(await File.ReadAllTextAsync(pluginMetadataPath)); PluginMetadata? pluginMetadata = JsonSerializer.Deserialize<PluginMetadata>(await File.ReadAllTextAsync(pluginMetadataPath));
if (pluginMetadata is not null) if (pluginMetadata is null)
{ {
pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(pluginMetadata, new CommandHandler(async () => await Remove(pluginPath, pluginMetadata.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath)); 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); _ = 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); 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(); IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods) await foreach (Mod mod in mods)
{ {
@@ -139,15 +166,67 @@ public partial class PluginManager : Window
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($"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(); await SyncPlugins();
pluginManagerDataContext.IsLoading = false; pluginManagerDataContext.IsLoading = false;
App.Logger.LogInfo("Plugin Manager initialization complete", source: "PluginManager");
} }
protected override void OnClosing(CancelEventArgs e) public async Task Remove(string pluginPath, uint id)
{ {
e.Cancel = pluginManagerDataContext.IsLoading; App.Logger.LogInfo($"Removing plugin with ID {id} from path: {pluginPath}", source: "PluginManager");
pluginManagerDataContext.IsLoading = true;
changed = true;
PluginEntryDataContext? pluginEntryDataContext = pluginManagerDataContext.InstalledPlugins.FirstOrDefault(p => p.Id == id);
if (Directory.Exists(pluginPath))
{
try
{
Directory.Delete(pluginPath, true);
App.Logger.LogInfo($"Successfully deleted plugin directory: {pluginPath}", source: "PluginManager");
}
catch (Exception ex)
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = "Plugin Manager",
Content = ex.Message,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
App.Logger.LogError(ex.Message, source: "PluginManager");
}
}
if (pluginEntryDataContext is not null)
{
_ = pluginManagerDataContext.InstalledPlugins.Remove(pluginEntryDataContext);
App.Logger.LogInfo($"Removed plugin {id} from installed plugins list", source: "PluginManager");
}
if (pluginManagerDataContext.IsAuthenticated)
{
try
{
await modIoClient.Games[ModIoGameId].Mods.Unsubscribe(id);
App.Logger.LogInfo($"Unsubscribed from plugin {id} on mod.io", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Failed to unsubscribe from plugin {id}: {ex.Message}", source: "PluginManager");
}
}
pluginManagerDataContext.IsLoading = false;
} }
[GeneratedRegex(@"[^a-zA-Z0-9]")] [GeneratedRegex(@"[^a-zA-Z0-9]")]
@@ -155,10 +234,13 @@ public partial class PluginManager : Window
private async Task Install(Mod mod) private async Task Install(Mod mod)
{ {
App.Logger.LogInfo($"Installing plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
pluginManagerDataContext.IsLoading = true; pluginManagerDataContext.IsLoading = true;
changed = true;
if (mod.Modfile?.Download?.BinaryUrl is null) if (mod.Modfile?.Download?.BinaryUrl is null)
{ {
App.Logger.LogWarn($"Plugin {mod.Id} has no download URL", source: "PluginManager");
return; return;
} }
@@ -166,28 +248,39 @@ public partial class PluginManager : Window
string pluginPath = Path.Combine(pluginsPath, pluginGuid); string pluginPath = Path.Combine(pluginsPath, pluginGuid);
string zipFilePath = Path.Combine(pluginsPath, pluginGuid + ".zip"); string zipFilePath = Path.Combine(pluginsPath, pluginGuid + ".zip");
try
{
App.Logger.LogInfo($"Downloading plugin from: {mod.Modfile.Download.BinaryUrl}", source: "PluginManager");
using (Stream fileStream = await httpClient.GetStreamAsync(mod.Modfile.Download.BinaryUrl)) using (Stream fileStream = await httpClient.GetStreamAsync(mod.Modfile.Download.BinaryUrl))
{ {
using FileStream outputFileStream = new FileStream(zipFilePath, FileMode.Create); using FileStream outputFileStream = new FileStream(zipFilePath, FileMode.Create);
await fileStream.CopyToAsync(outputFileStream); await fileStream.CopyToAsync(outputFileStream);
} }
App.Logger.LogInfo($"Plugin downloaded to: {zipFilePath}", source: "PluginManager");
using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath)) using (ZipArchive zipArchive = ZipFile.OpenRead(zipFilePath))
{ {
zipArchive.ExtractToDirectory(pluginPath); zipArchive.ExtractToDirectory(pluginPath);
} }
App.Logger.LogInfo($"Plugin extracted to: {pluginPath}", source: "PluginManager");
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json"); string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(new PluginMetadata(mod))); await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(new PluginMetadata(mod)));
File.Delete(zipFilePath); File.Delete(zipFilePath);
if (!File.Exists(Path.Combine(pluginPath, "main.dll"))) if (!File.Exists(Path.Combine(pluginPath, "main.dll")) && !File.Exists(Path.Combine(pluginPath, "main.html")))
{ {
_ = Remove(pluginPath, mod.Id); App.Logger.LogError($"Plugin {mod.Id} does not contain main.dll or main.html", source: "PluginManager");
await Remove(pluginPath, mod.Id);
pluginManagerDataContext.IsLoading = false; 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); 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; return;
} }
@@ -199,11 +292,32 @@ public partial class PluginManager : Window
} }
if (pluginManagerDataContext.IsAuthenticated) if (pluginManagerDataContext.IsAuthenticated)
{
try
{ {
await modIoClient.Games[ModIoGameId].Mods.Subscribe(mod.Id); 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)); pluginManagerDataContext.InstalledPlugins.Add(new PluginEntryDataContext(new PluginMetadata(mod), new CommandHandler(async () => await Remove(pluginPath, mod.Id)), PluginEntryDataContext.Mode.Uninstall, pluginPath));
App.Logger.LogInfo($"Successfully installed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
}
catch (Exception ex)
{
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();
}
pluginManagerDataContext.IsLoading = false; pluginManagerDataContext.IsLoading = false;
} }
@@ -217,6 +331,7 @@ public partial class PluginManager : Window
FileName = uri FileName = uri
}; };
_ = Process.Start(psi); _ = Process.Start(psi);
App.Logger.LogInfo($"Opened mod.io page: {uri}", source: "PluginManager");
} }
private void AllPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) private void AllPluginsSearchTextBox_TextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e)
@@ -246,6 +361,7 @@ public partial class PluginManager : Window
private async Task SearchAllPlugins(string searchString) private async Task SearchAllPlugins(string searchString)
{ {
App.Logger.LogInfo($"Searching plugins with query: {searchString}", source: "PluginManager");
pluginManagerDataContext.AllPlugins.Clear(); pluginManagerDataContext.AllPlugins.Clear();
if (!searchString.Contains('*')) if (!searchString.Contains('*'))
@@ -255,6 +371,8 @@ public partial class PluginManager : Window
Filter filter = ModFilter.Name.Like($"{searchString}").And(ModFilter.Popular.Desc()).Limit(100); Filter filter = ModFilter.Name.Like($"{searchString}").And(ModFilter.Popular.Desc()).Limit(100);
try
{
IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable(); IAsyncEnumerable<Mod> mods = modIoClient.Games[ModIoGameId].Mods.Search(filter).ToEnumerable();
await foreach (Mod mod in mods) await foreach (Mod mod in mods)
{ {
@@ -265,28 +383,56 @@ public partial class PluginManager : Window
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; 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 try
{ {
CreateNewPlugin(); await CreateNewPlugin();
} }
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.LogError(ex.Message, source: "PluginManager"); 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")) 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; return;
} }
@@ -294,22 +440,21 @@ public partial class PluginManager : Window
string pluginPath = Path.Combine(pluginsPath, pluginGuid); string pluginPath = Path.Combine(pluginsPath, pluginGuid);
uint pluginId = (uint)Random.Shared.Next(1000, 9999); 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) if (inputDialog.ShowDialog() != true)
{ {
App.Logger.LogInfo("Plugin creation cancelled by user", source: "PluginManager");
return; return;
} }
string pluginName = inputDialog.ResponseText; string pluginName = inputDialog.ResponseText;
string pluginSafeName = pluginName.ToLower().Replace("_", " "); string pluginSafeName = GetPluginSafeName(pluginName);
TextInfo info = CultureInfo.CurrentCulture.TextInfo; App.Logger.LogInfo($"Creating plugin with name: {pluginName} (safe name: {pluginSafeName})", source: "PluginManager");
pluginSafeName = info.ToTitleCase(pluginSafeName);
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
if (!Directory.Exists(pluginDevelopmentPath)) if (!Directory.Exists(pluginDevelopmentPath))
{ {
@@ -320,25 +465,44 @@ public partial class PluginManager : Window
if (Directory.Exists(pluginProjectPath)) 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; return;
} }
pluginManagerDataContext.IsLoading = true;
PluginMetadata pluginMetadata = new(pluginName, pluginId); PluginMetadata pluginMetadata = new(pluginName, pluginId);
_ = Directory.CreateDirectory(pluginPath); _ = Directory.CreateDirectory(pluginPath);
_ = Directory.CreateDirectory(pluginProjectPath); _ = Directory.CreateDirectory(pluginProjectPath);
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json"); 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"; string cmd = $"new classlib -n {pluginSafeName} -o {pluginProjectPath} -f net8.0 --target-framework-override net8.0-windows7";
Process process = Process.Start("dotnet", cmd); 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 = 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")); 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() public override Bitmap Main()
{{ {{
Bitmap bmp = new Bitmap(2000, 1000); Bitmap bmp = new Bitmap(2000, 1000);
bmp.SetResolution(100, 100); // Set DPI to avoid scaling issues.
using (Graphics g = Graphics.FromImage(bmp)) 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), new SolidBrush(Application.Theme.PrimaryColor), new PointF(0, 0)); // Draw ""Hello World"" to the image.
g.DrawString(""Hello World"", new Font(Application.Theme.Font, 100), Brushes.Black, 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. return bmp; // Return the image.
}} }}
}} }}
"; ";
// Update the .csproj file
string csprojPath = Path.Combine(pluginProjectPath, $"{pluginSafeName}.csproj"); 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); XDocument doc = XDocument.Load(csprojPath);
XElement? propertyGroup = doc.Root?.Element("PropertyGroup"); XElement? propertyGroup = doc.Root?.Element("PropertyGroup");
@@ -412,23 +571,38 @@ public class {pluginSafeName}Plugin : Plugin
} }
doc.Save(csprojPath); doc.Save(csprojPath);
App.Logger.LogInfo("Updated .csproj file with custom build settings", source: "PluginManager");
} }
else 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; 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"); string? associatedProgram = FileUtilities.GetAssociatedProgram(".csproj");
if (associatedProgram is null) if (associatedProgram is null)
{ {
App.Logger.LogInfo($"Opening plugin project in Explorer: {pluginProjectPath}", source: "PluginManager");
_ = Process.Start("explorer.exe", pluginProjectPath); _ = Process.Start("explorer.exe", pluginProjectPath);
return;
} }
else
{
App.Logger.LogInfo($"Opening plugin project in IDE: {associatedProgram}", source: "PluginManager");
ProcessStartInfo psi = new ProcessStartInfo ProcessStartInfo psi = new ProcessStartInfo
{ {
FileName = associatedProgram, FileName = associatedProgram,
@@ -438,36 +612,90 @@ public class {pluginSafeName}Plugin : Plugin
_ = Process.Start(psi); _ = Process.Start(psi);
} }
App.Logger.LogInfo($"Successfully created plugin: {pluginName}", source: "PluginManager");
pluginManagerDataContext.IsLoading = false;
}
private async Task CreateNewWebPlugin(string pluginPath, string pluginName)
{
App.Logger.LogInfo($"Creating web plugin at: {pluginPath}", source: "PluginManager");
string mainHtml = $@"<!DOCTYPE html>
<html>
<head>
<meta charset=""utf-8"">
<style>
body {{ margin: 0; padding: 16px; font-family: var(--font-family); }}
h1 {{ color: var(--primary-color); }}
</style>
</head>
<body>
<h1 id=""greeting"">{pluginName}</h1>
<script>
function render() {{
const s = window.desktopMagic.getSettings();
document.getElementById('greeting').innerHTML = s.message;
document.getElementById('greeting').style.color = s.color;
document.getElementById('greeting').style.fontWeight = s.bold ? 'bold' : 'normal';
}}
window.desktopMagic.onSettingChanged = (id, value) => render();
render();
</script>
</body>
</html>";
var settings = new List<Dictionary<string, object?>>
{
new() { ["id"] = "message", ["name"] = "Greeting Message", ["type"] = "textbox", ["default"] = "Hello, World!" },
new() { ["id"] = "bold", ["name"] = "Bold Text", ["type"] = "checkbox", ["default"] = true },
new() { ["id"] = "color", ["name"] = "Text Color", ["type"] = "colorpicker", ["default"] = "#FF5722" }
};
string settingsJson = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(Path.Combine(pluginPath, "settings.json"), settingsJson);
await File.WriteAllTextAsync(Path.Combine(pluginPath, "main.html"), mainHtml);
App.Logger.LogInfo($"Successfully created web plugin: {pluginName}", source: "PluginManager");
}
private async void LogInButton_Click(object sender, RoutedEventArgs e) private async void LogInButton_Click(object sender, RoutedEventArgs e)
{ {
if (pluginManagerDataContext.IsAuthenticated) if (pluginManagerDataContext.IsAuthenticated)
{ {
App.Logger.LogInfo("Logging out from mod.io", source: "PluginManager");
MainWindowDataContext.GetSettings().ModIoAccessToken = null; MainWindowDataContext.GetSettings().ModIoAccessToken = null;
pluginManagerDataContext.IsAuthenticated = false; pluginManagerDataContext.IsAuthenticated = false;
return; return;
} }
App.Logger.LogInfo("Starting mod.io authentication", source: "PluginManager");
try try
{ {
InputDialog inputDialog = new((string)FindResource("enterModIoEmail"), "Plugin Manager") InputDialog inputDialog = new((string)FindResource("enterModIoEmail"), "Plugin Manager")
{ {
Owner = this, Owner = Window.GetWindow(this),
}; };
if (inputDialog.ShowDialog() != true) if (inputDialog.ShowDialog() != true)
{ {
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return; return;
} }
App.Logger.LogInfo($"Requesting authentication code for email: {inputDialog.ResponseText}", source: "PluginManager");
await modIoClient.Auth.RequestCode(ModIoApiKey, inputDialog.ResponseText); await modIoClient.Auth.RequestCode(ModIoApiKey, inputDialog.ResponseText);
inputDialog = new((string)FindResource("enterModIoAccessToken"), "Plugin Manager") inputDialog = new((string)FindResource("enterModIoAccessToken"), "Plugin Manager")
{ {
Owner = this, Owner = Window.GetWindow(this),
}; };
if (inputDialog.ShowDialog() != true) if (inputDialog.ShowDialog() != true)
{ {
App.Logger.LogInfo("Authentication cancelled by user", source: "PluginManager");
return; return;
} }
@@ -478,11 +706,13 @@ public class {pluginSafeName}Plugin : Plugin
if (accessToken.Value is not null) if (accessToken.Value is not null)
{ {
modIoClient = new Client(new Credentials(ModIoApiKey, accessToken.Value)); modIoClient = new Client(new Credentials(ModIoApiKey, accessToken.Value));
App.Logger.LogInfo("Successfully authenticated with mod.io", source: "PluginManager");
} }
MainWindowDataContext.GetSettings().ModIoAccessToken = accessToken.Value; MainWindowDataContext.GetSettings().ModIoAccessToken = accessToken.Value;
pluginManagerDataContext.IsAuthenticated = true; pluginManagerDataContext.IsAuthenticated = true;
App.Logger.LogInfo("Subscribing to installed plugins on mod.io", source: "PluginManager");
foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins) foreach (PluginEntryDataContext plugin in pluginManagerDataContext.InstalledPlugins)
{ {
if (!plugin.IsLocalPlugin) if (!plugin.IsLocalPlugin)
@@ -490,10 +720,11 @@ public class {pluginSafeName}Plugin : Plugin
try try
{ {
await modIoClient.Games[ModIoGameId].Mods.Subscribe(plugin.Id); await modIoClient.Games[ModIoGameId].Mods.Subscribe(plugin.Id);
App.Logger.LogInfo($"Subscribed to plugin {plugin.Id}", source: "PluginManager");
} }
catch (Exception ex) 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) catch (Exception ex)
{ {
App.Logger.LogError(ex.Message, source: "PluginManager"); App.Logger.LogError($"Authentication failed: {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();
} }
pluginManagerDataContext.IsLoading = false; pluginManagerDataContext.IsLoading = false;
@@ -516,7 +753,12 @@ public class {pluginSafeName}Plugin : Plugin
return; return;
} }
App.Logger.LogInfo("Syncing plugins with mod.io subscriptions", source: "PluginManager");
try
{
IReadOnlyList<Mod> mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList(); IReadOnlyList<Mod> mods = await modIoClient.User.GetSubscriptions(ModFilter.GameId.Eq(ModIoGameId)).ToList();
App.Logger.LogInfo($"Found {mods.Count} subscribed plugins on mod.io", source: "PluginManager");
List<PluginEntryDataContext> pluginsToRemove = []; List<PluginEntryDataContext> pluginsToRemove = [];
@@ -524,6 +766,7 @@ public class {pluginSafeName}Plugin : Plugin
{ {
if (!plugin.IsLocalPlugin && !mods.Any(m => m.Id == plugin.Id)) 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); pluginsToRemove.Add(plugin);
} }
} }
@@ -542,7 +785,32 @@ public class {pluginSafeName}Plugin : Plugin
foreach (Mod mod in notInstalledMods) foreach (Mod mod in notInstalledMods)
{ {
App.Logger.LogInfo($"Installing subscribed plugin: {mod.Name} (ID: {mod.Id})", source: "PluginManager");
await Install(mod); await Install(mod);
} }
App.Logger.LogInfo("Plugin sync completed", source: "PluginManager");
}
catch (Exception ex)
{
App.Logger.LogError($"Plugin sync failed: {ex.Message}", source: "PluginManager");
}
}
private static string GetPluginSafeName(string pluginName)
{
string pluginSafeName = pluginName.ToLower().Replace("_", " ");
TextInfo info = CultureInfo.CurrentCulture.TextInfo;
pluginSafeName = info.ToTitleCase(pluginSafeName);
pluginSafeName = IdentifierNameRegex().Replace(pluginSafeName, "");
return pluginSafeName;
}
private string GetCsprojPath(string pluginName)
{
string pluginSafeName = GetPluginSafeName(pluginName);
return Path.Combine(pluginDevelopmentPath, pluginSafeName, $"{pluginSafeName}.csproj");
} }
} }
@@ -1,6 +1,8 @@
using Modio.Models; using Modio.Models;
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
namespace DesktopMagic.Plugins; namespace DesktopMagic.Plugins;
@@ -27,6 +29,14 @@ public class PluginMetadata
public string? Version { get; set; } public string? Version { get; set; }
public List<string> Tags { get; set; } = [];
[JsonIgnore]
public bool SupportsUnloading => !Tags.Contains("Does Not Support Unloading");
[JsonIgnore]
public bool IsLocalPlugin => string.IsNullOrWhiteSpace(ProfileUri?.ToString());
public PluginMetadata(Mod mod) public PluginMetadata(Mod mod)
{ {
Name = mod.Name ?? mod.Id.ToString(); Name = mod.Name ?? mod.Id.ToString();
@@ -39,6 +49,7 @@ public class PluginMetadata
Description = mod.DescriptionPlaintext; Description = mod.DescriptionPlaintext;
Summary = mod.Summary; Summary = mod.Summary;
Version = mod.Modfile?.Version; Version = mod.Modfile?.Version;
Tags = mod.Tags.Select(t => t.Name).Where(t => t is not null).ToList()!;
} }
public PluginMetadata(string name, uint id) public PluginMetadata(string name, uint id)
@@ -1,6 +1,7 @@
<Window x:Class="DesktopMagic.PluginWindow" <Window x:Class="DesktopMagic.PluginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopMagic" xmlns:local="clr-namespace:DesktopMagic"
@@ -8,12 +9,14 @@
Title="PluginWindow" Height="450" Width="800" Title="PluginWindow" Height="450" Width="800"
Background="Transparent" Background="Transparent"
WindowStyle="None" WindowStyle="None"
ShowInTaskbar="False"
AllowsTransparency="True" AllowsTransparency="True"
LocationChanged="Window_LocationChanged" LocationChanged="Window_LocationChanged"
SizeChanged="Window_SizeChanged" SizeChanged="Window_SizeChanged"
Closing="Window_Closing" Closing="Window_Closing"
ContentRendered="Window_ContentRendered" ContentRendered="Window_ContentRendered"
x:Name="window"> x:Name="window">
<busyIndicator:BusyMask x:Name="busyMask" IsBusy="True" Background="Transparent" BusyContent="" BusyContentMargin="-5" IndicatorType="Cogs">
<Grid> <Grid>
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" /> <Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
@@ -27,6 +30,7 @@
</Border> </Border>
</Viewbox> </Viewbox>
</Grid> </Grid>
</busyIndicator:BusyMask>
<WindowChrome.WindowChrome> <WindowChrome.WindowChrome>
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" /> <WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome> </WindowChrome.WindowChrome>
+542 -48
View File
@@ -13,7 +13,10 @@ using System.Drawing.Imaging;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Loader;
using System.Text.Json;
using System.Threading; using System.Threading;
using System.Threading.Tasks;
using System.Timers; using System.Timers;
using System.Windows; using System.Windows;
using System.Windows.Interop; using System.Windows.Interop;
@@ -22,7 +25,7 @@ using System.Windows.Media.Imaging;
namespace DesktopMagic; namespace DesktopMagic;
public partial class PluginWindow : Window public partial class PluginWindow : Window, IPluginWindow
{ {
public event Action? PluginLoaded; public event Action? PluginLoaded;
@@ -32,6 +35,15 @@ public partial class PluginWindow : Window
private Thread? pluginThread; private Thread? pluginThread;
private System.Timers.Timer? updateTimer; private System.Timers.Timer? updateTimer;
private Plugin? pluginClassInstance; private Plugin? pluginClassInstance;
private AssemblyLoadContext assemblyLoadContext;
private CancellationTokenSource? pluginCancellationTokenSource;
private FileSystemWatcher? pluginFileWatcher;
private System.Timers.Timer? reloadDebounceTimer;
private bool isReloading = false;
private WriteableBitmap? writeableBitmap;
private BitmapScalingMode lastBitmapScalingMode = BitmapScalingMode.Unspecified;
public bool IsRunning { get; private set; } = true; public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; } public PluginMetadata PluginMetadata { get; private set; }
@@ -69,6 +81,11 @@ public partial class PluginWindow : Window
} }
}; };
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
PluginMetadata = pluginMetadata; PluginMetadata = pluginMetadata;
this.settings = settings; this.settings = settings;
@@ -78,6 +95,14 @@ public partial class PluginWindow : Window
Height = settings.Size.Y; Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath; PluginFolderPath = pluginFolderPath;
assemblyLoadContext = CreateAssemblyLoadContext();
// Initialize hot reload watcher if plugin supports unloading and is external
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
{
InitializeHotReload();
}
} }
public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty) public PluginWindow(Plugin pluginClassInstance, PluginMetadata pluginMetadata, PluginSettings settings) : this(pluginMetadata, settings, string.Empty)
@@ -85,12 +110,190 @@ public partial class PluginWindow : Window
this.pluginClassInstance = pluginClassInstance; this.pluginClassInstance = pluginClassInstance;
} }
private AssemblyLoadContext CreateAssemblyLoadContext()
{
AssemblyLoadContext context = new(PluginMetadata.Name, isCollectible: true);
context.Resolving += (ctx, assemblyName) =>
{
string assemblyPath = Path.Combine(PluginFolderPath, assemblyName.Name + ".dll");
if (File.Exists(assemblyPath))
{
return ctx.LoadFromAssemblyPath(assemblyPath);
}
else
{
_ = ctx.LoadFromAssemblyName(assemblyName);
}
return null;
};
return context;
}
private void InitializeHotReload()
{
try
{
pluginFileWatcher = new FileSystemWatcher(PluginFolderPath)
{
Filter = "main.dll",
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = false
};
pluginFileWatcher.Changed += OnPluginFileChanged;
reloadDebounceTimer = new System.Timers.Timer(500); // 500ms debounce
reloadDebounceTimer.Elapsed += async (s, e) =>
{
reloadDebounceTimer!.Stop();
await ReloadPlugin();
};
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "Plugin");
}
}
private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
{
if (isReloading)
{
return;
}
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin file changed, scheduling reload", source: "Plugin");
// Debounce multiple file change events
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Start();
}
private async Task StopPlugin(bool unloadAssembly = true)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
try
{
pluginCancellationTokenSource?.Cancel();
if (pluginClassInstance is AsyncPlugin asyncPluginStop)
{
try
{
await asyncPluginStop.StopAsync(pluginCancellationTokenSource?.Token ?? CancellationToken.None);
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping async plugin: {ex.Message}", source: "Plugin");
}
}
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
pluginClassInstance.Stop();
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Error stopping plugin: {ex}", source: "Plugin");
}
if (unloadAssembly)
{
assemblyLoadContext.Unload();
}
pluginCancellationTokenSource?.Dispose();
pluginCancellationTokenSource = null;
}
private async Task ReloadPlugin()
{
if (isReloading || !IsRunning)
{
return;
}
isReloading = true;
try
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading plugin", source: "Plugin");
// Stop the file watcher during reload
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = false;
}
updateTimer?.Stop();
await StopPlugin(unloadAssembly: true);
// Wait for file to be fully written and released
await Task.Delay(200);
// Clear the current instance
pluginClassInstance = null;
// Create a new AssemblyLoadContext since the old one was unloaded
assemblyLoadContext = CreateAssemblyLoadContext();
// Show busy indicator
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = true);
// Reload the plugin
await ExecuteSource();
// Hide busy indicator
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Plugin reloaded successfully", source: "Plugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload plugin: {ex}", source: "Plugin");
_ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Reload Error \"{PluginMetadata.Name}\"",
Content = $"Failed to reload plugin:\n{ex.Message}",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
}
finally
{
isReloading = false;
// Re-enable file watcher
if (pluginFileWatcher != null && IsRunning)
{
pluginFileWatcher.EnableRaisingEvents = true;
}
}
}
public void UpdatePluginWindow() public void UpdatePluginWindow()
{ {
Dispatcher.Invoke(ThemeChanged);
UpdateTimer_Elapsed(updateTimer, null); UpdateTimer_Elapsed(updateTimer, null);
} }
public void SavePluginState()
{
if (pluginClassInstance is not null)
{
SaveState(pluginClassInstance);
}
}
public void Exit() public void Exit()
{ {
IsRunning = false; IsRunning = false;
@@ -119,9 +322,8 @@ public partial class PluginWindow : Window
panel.Visibility = Visibility.Collapsed; panel.Visibility = Visibility.Collapsed;
imageBorder.BorderThickness = new Thickness(0); imageBorder.BorderThickness = new Thickness(0);
image.Margin = new Thickness(0); image.Margin = new Thickness(0);
WindowPos.SendWpfWindowBack(this); WindowPos.SetWindowLayer(this, pluginClassInstance?.windowLayer.Value ?? "Always on Bottom");
WindowPos.SendWpfWindowBack(this); WindowPos.SetIsLocked(window, true);
WindowPos.SetIsLocked(this, true);
tileBar.CaptionHeight = 0; tileBar.CaptionHeight = 0;
ResizeMode = ResizeMode.NoResize; ResizeMode = ResizeMode.NoResize;
} }
@@ -137,44 +339,78 @@ public partial class PluginWindow : Window
WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE); WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE);
} }
private static BitmapSource BitmapToImageSource(Bitmap bitmap) private void UpdateImageFromBitmap(Bitmap bitmap, BitmapScalingMode scalingMode)
{ {
BitmapData bitmapData = bitmap.LockBits( BitmapData bitmapData = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.ReadOnly, System.Drawing.Imaging.PixelFormat.Format32bppArgb);
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadOnly, bitmap.PixelFormat);
BitmapSource bitmapSource = BitmapSource.Create( try
{
Dispatcher.Invoke(() =>
{
if (lastBitmapScalingMode != scalingMode)
{
RenderOptions.SetBitmapScalingMode(image, scalingMode);
lastBitmapScalingMode = scalingMode;
}
if (writeableBitmap == null || writeableBitmap.PixelWidth != bitmapData.Width || writeableBitmap.PixelHeight != bitmapData.Height)
{
writeableBitmap = new WriteableBitmap(
bitmapData.Width, bitmapData.Height, bitmapData.Width, bitmapData.Height,
bitmap.HorizontalResolution, bitmap.VerticalResolution, bitmap.HorizontalResolution, bitmap.VerticalResolution,
PixelFormats.Bgra32, null, PixelFormats.Bgra32, null);
bitmapData.Scan0, bitmapData.Stride * bitmapData.Height, bitmapData.Stride); 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); bitmap.UnlockBits(bitmapData);
return bitmapSource; }
} }
private void Window_ContentRendered(object? sender, EventArgs e) private void Window_ContentRendered(object? sender, EventArgs e)
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting plugin thread", source: "Plugin"); App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting plugin thread", source: "Plugin");
pluginThread = new Thread(LoadPlugin); pluginThread = new Thread(async () => await LoadPlugin());
pluginThread.Start(); pluginThread.Start();
} }
private void ThemeChanged() private async Task LoadPlugin()
{
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()
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin", source: "Plugin"); App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin", source: "Plugin");
if (pluginClassInstance is null && !File.Exists($"{PluginFolderPath}\\main.dll")) if (pluginClassInstance is null && !File.Exists($"{PluginFolderPath}\\main.dll"))
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.dll\" does not exist", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.dll\" does not exist", source: "Plugin");
_ = MessageBox.Show("File \"main.dll\" does not exist!", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File \"main.dll\" does not exist!",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
@@ -182,31 +418,84 @@ public partial class PluginWindow : Window
try try
{ {
ExecuteSource(); await ExecuteSource();
} }
catch (Exception ex) catch (Exception ex)
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
PluginLoaded?.Invoke(); PluginLoaded?.Invoke();
_ = await Dispatcher.InvokeAsync(() => busyMask.IsBusy = false);
// Enable hot reload after initial load
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = true;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload monitoring started", source: "Plugin");
}
} }
private void ExecuteSource() private void ThemeChanged()
{
viewBox.Margin = new Thickness(settings.Theme.Margin);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
pluginClassInstance?.OnThemeChanged();
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
{
pluginClassInstance.Application.UpdateWindow();
}
}
private async Task ExecuteSource()
{ {
object? instance = pluginClassInstance; object? instance = pluginClassInstance;
if (instance is null) if (instance is null)
{ {
Assembly dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll"); Assembly dll;
if (PluginMetadata.SupportsUnloading)
{
byte[] assemblyData = await File.ReadAllBytesAsync($"{PluginFolderPath}\\main.dll");
using MemoryStream assemblyStream = new(assemblyData);
dll = assemblyLoadContext.LoadFromStream(assemblyStream);
}
else
{
dll = Assembly.LoadFrom($"{PluginFolderPath}\\main.dll");
}
Type? instanceType = Array.Find(dll.GetTypes(), type => type.GetTypeInfo().BaseType == typeof(Plugin)); Type? instanceType = Array.Find(dll.GetTypes(), type => type.GetTypeInfo().BaseType == typeof(Plugin));
if (instanceType is null) if (instanceType is null)
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
_ = MessageBox.Show($"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = $"The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
@@ -216,17 +505,27 @@ public partial class PluginWindow : Window
if (instance is Plugin plugin) if (instance is Plugin plugin)
{ {
pluginClassInstance = plugin; pluginClassInstance = plugin;
pluginClassInstance.Application = new Plugins.PluginData(this, settings); pluginClassInstance.Application = new PluginData(this, settings);
} }
else else
{ {
App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - The \"Plugin\" class could not be found! It has to inherit from \"{typeof(Plugin).FullName}\"", source: "Plugin");
_ = MessageBox.Show($"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"", $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = $"The \"Plugin\" class has to inherit from \"{typeof(Plugin).FullName}\"",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
LoadOptions(pluginClassInstance); LoadState(pluginClassInstance);
await LoadOptions(pluginClassInstance);
BindDefaultSettings(pluginClassInstance); BindDefaultSettings(pluginClassInstance);
updateTimer = new System.Timers.Timer updateTimer = new System.Timers.Timer
@@ -236,7 +535,14 @@ public partial class PluginWindow : Window
updateTimer.Elapsed += UpdateTimer_Elapsed; updateTimer.Elapsed += UpdateTimer_Elapsed;
pluginClassInstance.Start(); pluginClassInstance.Start();
if (pluginClassInstance is AsyncPlugin asyncPluginStart)
{
pluginCancellationTokenSource?.Dispose();
pluginCancellationTokenSource = new CancellationTokenSource();
await asyncPluginStart.StartAsync(pluginCancellationTokenSource.Token);
}
UpdatePluginWindow(); UpdatePluginWindow();
await Dispatcher.InvokeAsync(ThemeChanged);
if (pluginClassInstance.UpdateInterval > 0) if (pluginClassInstance.UpdateInterval > 0)
{ {
@@ -251,11 +557,15 @@ public partial class PluginWindow : Window
{ {
SetHorizontalAlignment(); SetHorizontalAlignment();
SetVerticalAlignment(); SetVerticalAlignment();
SetWindowLayer();
SetRotation();
SetThemeOverride(); SetThemeOverride();
SetThemeOverrideItems(); SetThemeOverrideItems();
pluginClassInstance.horizontalAlignment.OnValueChanged += SetHorizontalAlignment; pluginClassInstance.horizontalAlignment.OnValueChanged += SetHorizontalAlignment;
pluginClassInstance.verticalAlignment.OnValueChanged += SetVerticalAlignment; pluginClassInstance.verticalAlignment.OnValueChanged += SetVerticalAlignment;
pluginClassInstance.windowLayer.OnValueChanged += SetWindowLayer;
pluginClassInstance.rotation.OnValueChanged += SetRotation;
pluginClassInstance.themeOverride.OnValueChanged += SetThemeOverride; pluginClassInstance.themeOverride.OnValueChanged += SetThemeOverride;
DesktopMagicSettings desktopMagicSettings = MainWindowDataContext.GetSettings(); DesktopMagicSettings desktopMagicSettings = MainWindowDataContext.GetSettings();
@@ -290,6 +600,16 @@ public partial class PluginWindow : Window
border.HorizontalAlignment = horizontalAlignment; border.HorizontalAlignment = horizontalAlignment;
} }
void SetWindowLayer()
{
WindowPos.SetWindowLayer(this, pluginClassInstance.windowLayer.Value);
}
void SetRotation()
{
image.LayoutTransform = new RotateTransform(pluginClassInstance.rotation.Value);
}
void SetThemeOverride() void SetThemeOverride()
{ {
if (pluginClassInstance.themeOverride.Value == "<None>") if (pluginClassInstance.themeOverride.Value == "<None>")
@@ -316,16 +636,13 @@ public partial class PluginWindow : Window
} }
} }
private void LoadOptions(object instance) private async Task LoadOptions(object instance)
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin options", source: "Plugin"); App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Loading plugin options", source: "Plugin");
try try
{ {
#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
FieldInfo[] props = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.GetField); FieldInfo[] props = instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.GetField);
#pragma warning restore S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
List<SettingElement> settingElements = []; List<SettingElement> settingElements = [];
foreach (FieldInfo prop in props) foreach (FieldInfo prop in props)
{ {
@@ -344,6 +661,16 @@ public partial class PluginWindow : Window
settingElement.JsonValue = settingsSettingElement.JsonValue; settingElement.JsonValue = settingsSettingElement.JsonValue;
} }
element.OnValueChanged += () =>
{
pluginClassInstance?.OnSettingsChanged();
if (pluginClassInstance?.UpdateInterval is 0 or > 500)
{
pluginClassInstance.Application.UpdateWindow();
}
};
settingElements.Add(settingElement); settingElements.Add(settingElement);
break; break;
} }
@@ -357,18 +684,167 @@ public partial class PluginWindow : Window
{ {
IsRunning = false; IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
} }
} }
private void 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 try
{ {
if (IsRunning && pluginClassInstance is not null) 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) if (pluginClassInstance.UpdateInterval > 0)
{ {
@@ -389,12 +865,7 @@ public partial class PluginWindow : Window
_ => BitmapScalingMode.Unspecified _ => BitmapScalingMode.Unspecified
}; };
//Update Image UpdateImageFromBitmap(result, renderOptions);
Dispatcher.Invoke(() =>
{
RenderOptions.SetBitmapScalingMode(image, renderOptions);
image.Source = BitmapToImageSource(result);
});
} }
} }
} }
@@ -402,7 +873,16 @@ public partial class PluginWindow : Window
{ {
IsRunning = false; IsRunning = false;
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin"); App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "Plugin");
_ = MessageBox.Show("File execution error:\n" + ex, $"Error \"{PluginMetadata.Name}\"", MessageBoxButton.OK, MessageBoxImage.Error); _ = await Dispatcher.InvokeAsync(async () =>
{
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File execution error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
});
Exit(); Exit();
return; return;
} }
@@ -426,9 +906,23 @@ public partial class PluginWindow : Window
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e) private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{ {
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping plugin", source: "Plugin");
IsRunning = false; IsRunning = false;
pluginClassInstance?.Stop();
// Cleanup hot reload resources
if (pluginFileWatcher != null)
{
pluginFileWatcher.EnableRaisingEvents = false;
pluginFileWatcher.Changed -= OnPluginFileChanged;
pluginFileWatcher.Dispose();
pluginFileWatcher = null;
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Dispose();
reloadDebounceTimer = null;
// Stop plugin using the shared method (no need to await in synchronous event handler)
_ = StopPlugin(unloadAssembly: true);
} }
#region Window Events #region Window Events
@@ -0,0 +1,40 @@
<Window x:Class="DesktopMagic.WebPluginWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:busyIndicator="https://github.com/moh3ngolshani/BusyIndicator"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:DesktopMagic"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
mc:Ignorable="d"
Title="WebPluginWindow" Height="450" Width="800"
Background="Transparent"
WindowStyle="None"
ShowInTaskbar="False"
AllowsTransparency="True"
LocationChanged="Window_LocationChanged"
SizeChanged="Window_SizeChanged"
Closing="Window_Closing"
ContentRendered="Window_ContentRendered"
x:Name="window">
<busyIndicator:BusyMask x:Name="busyMask" IsBusy="True" Background="Transparent" BusyContent="" BusyContentMargin="-5" IndicatorType="Cogs">
<Grid>
<Rectangle x:Name="panel" Fill="#4CBAFFEF" Stroke="White" Margin="0,0,0,0" Visibility="Collapsed" />
<Border x:Name="border" Background="Transparent" CornerRadius="10">
<Grid>
<wv2:WebView2 x:Name="webView" DefaultBackgroundColor="Transparent" CoreWebView2InitializationCompleted="WebView_CoreWebView2InitializationCompleted" />
<!--
WORKAROUND: This rectangle exists to allow interaction with the WebView2 when the window is transparent
https://github.com/MicrosoftEdge/WebView2Feedback/issues/997
-->
<Rectangle Fill="#01000000" />
</Grid>
</Border>
</Grid>
</busyIndicator:BusyMask>
<WindowChrome.WindowChrome>
<WindowChrome x:Name="tileBar" CaptionHeight="30" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
</Window>
@@ -0,0 +1,633 @@
using DesktopMagic.Api.Settings;
using DesktopMagic.Helpers;
using DesktopMagic.Plugins;
using DesktopMagic.Settings;
using Microsoft.Web.WebView2.Core;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Media;
namespace DesktopMagic;
public partial class WebPluginWindow : Window, IPluginWindow
{
public event Action? PluginLoaded;
public event Action? OnExit;
private readonly PluginSettings settings;
private bool isInitialized = false;
private FileSystemWatcher? fileWatcher;
private System.Timers.Timer? reloadDebounceTimer;
private bool isReloading = false;
public bool IsRunning { get; private set; } = true;
public PluginMetadata PluginMetadata { get; private set; }
public string PluginFolderPath { get; private set; }
public WebPluginWindow(PluginMetadata pluginMetadata, PluginSettings settings, string pluginFolderPath)
{
InitializeComponent();
Window w = new()
{
Top = -100,
Left = -100,
Width = 0,
Height = 0,
WindowStyle = WindowStyle.ToolWindow,
ShowInTaskbar = false
};
WindowInteropHelper helper = new WindowInteropHelper(w);
_ = helper.EnsureHandle();
Owner = w;
settings.PropertyChanged += (e, s) =>
{
if (s.PropertyName == nameof(PluginSettings.CurrentThemeName))
{
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
ThemeChanged();
}
};
settings.Theme.PropertyChanged += (se, ev) =>
{
ThemeChanged();
};
PluginMetadata = pluginMetadata;
this.settings = settings;
Left = settings.Position.X;
Top = settings.Position.Y;
Width = settings.Size.X;
Height = settings.Size.Y;
PluginFolderPath = pluginFolderPath;
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
{
InitializeHotReload();
}
}
public void Exit()
{
IsRunning = false;
Dispatcher.Invoke(() =>
{
OnExit?.Invoke();
});
}
public void SetEditMode(bool enabled)
{
if (enabled)
{
Topmost = true;
panel.Visibility = Visibility.Visible;
_ = W32.EnableWindow(webView.Handle, false);
WindowPos.SetIsLocked(this, false);
tileBar.CaptionHeight = tileBar.CaptionHeight = ActualHeight - 10 < 0 ? 0 : ActualHeight - 10;
ResizeMode = ResizeMode.CanResize;
}
else
{
Topmost = false;
panel.Visibility = Visibility.Collapsed;
_ = W32.EnableWindow(webView.Handle, true);
WindowPos.SendWpfWindowBack(this);
WindowPos.SendWpfWindowBack(this);
WindowPos.SetIsLocked(this, true);
tileBar.CaptionHeight = 0;
ResizeMode = ResizeMode.NoResize;
}
}
protected override void OnSourceInitialized(EventArgs e)
{
base.OnSourceInitialized(e);
WindowInteropHelper helper = new(this);
_ = WindowPos.SetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE,
WindowPos.GetWindowLong(helper.Handle, WindowPos.GWL_EXSTYLE) | WindowPos.WS_EX_NOACTIVATE);
}
private async void Window_ContentRendered(object? sender, EventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Starting web plugin", source: "WebPlugin");
try
{
await InitializeWebView();
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "WebView2 initialization error:\n" + ex,
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
Exit();
}
}
private void ThemeChanged()
{
webView.Margin = new Thickness(settings.Theme.Margin);
border.Background = new SolidColorBrush(MultiColorConverter.ConvertToMediaColor(settings.Theme.BackgroundColor));
border.CornerRadius = new CornerRadius(settings.Theme.CornerRadius);
string cssVariables = $@"
:root {{
--background-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.BackgroundColor)};
--primary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
--secondary-color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.SecondaryColor)};
--font-family: {settings.Theme.Font};
font-family: {settings.Theme.Font};
color: {MultiColorConverter.ConvertToHexRgba(settings.Theme.PrimaryColor)};
}}
";
string script = $@"
(function() {{
let style = document.getElementById('desktop-magic-theme');
if (!style) {{
style = document.createElement('style');
style.id = 'desktop-magic-theme';
document.head.appendChild(style);
}}
style.textContent = `{cssVariables}`;
}})();
";
_ = webView.ExecuteScriptAsync(script);
}
private async System.Threading.Tasks.Task InitializeWebView()
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Initializing WebView2", source: "WebPlugin");
string htmlPath = Path.Combine(PluginFolderPath, "main.html");
if (!File.Exists(htmlPath))
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - File \"main.html\" does not exist", source: "WebPlugin");
Wpf.Ui.Controls.MessageBox messageBox = new Wpf.Ui.Controls.MessageBox
{
Title = $"Error \"{PluginMetadata.Name}\"",
Content = "File \"main.html\" does not exist!",
CloseButtonText = "Ok"
};
_ = await messageBox.ShowDialogAsync();
Exit();
return;
}
try
{
LoadWebPluginSettings();
string userDataFolder = Path.Combine(Path.GetTempPath(), "DesktopMagic", "WebView2", PluginMetadata.Id.ToString());
CoreWebView2Environment environment = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
await webView.EnsureCoreWebView2Async(environment);
webView.CoreWebView2.Settings.AreDefaultContextMenusEnabled = false;
webView.CoreWebView2.Settings.AreDevToolsEnabled = false;
webView.CoreWebView2.Settings.IsStatusBarEnabled = false;
webView.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
_ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.Source = new Uri(htmlUri);
isInitialized = true;
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - WebView2 initialized successfully", source: "WebPlugin");
PluginLoaded?.Invoke();
busyMask.IsBusy = false;
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - WebView2 initialization failed: {ex}", source: "WebPlugin");
throw;
}
}
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Stopping web plugin", source: "WebPlugin");
IsRunning = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
fileWatcher.Dispose();
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Dispose();
try
{
if (isInitialized && webView.CoreWebView2 != null)
{
webView.Dispose();
}
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - {ex}", source: "WebPlugin");
}
}
private void Window_LocationChanged(object sender, EventArgs e)
{
settings.Position = new System.Windows.Point(Left, Top);
}
private void Window_SizeChanged(object sender, SizeChangedEventArgs e)
{
settings.Size = new System.Windows.Point(Width, Height);
tileBar.CaptionHeight = ActualHeight - 10;
}
private bool firstLoad = true;
private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
{
webView.CoreWebView2.DOMContentLoaded += (_, _) =>
{
if (firstLoad)
{
firstLoad = false;
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = true;
}
}
ThemeChanged();
};
}
private void LoadWebPluginSettings()
{
string settingsPath = Path.Combine(PluginFolderPath, "settings.json");
if (!File.Exists(settingsPath))
{
return;
}
try
{
string json = File.ReadAllText(settingsPath);
using JsonDocument doc = JsonDocument.Parse(json);
JsonElement root = doc.RootElement;
if (root.ValueKind != JsonValueKind.Array)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - settings.json must be a JSON array", source: "WebPlugin");
return;
}
List<SettingElement> settingElements = [];
int orderIndex = 0;
foreach (JsonElement element in root.EnumerateArray())
{
string id = element.GetProperty("id").GetString() ?? $"setting-{orderIndex}";
string name = element.TryGetProperty("name", out JsonElement nameElement) ? nameElement.GetString() ?? id : id;
string type = element.TryGetProperty("type", out JsonElement typeElement) ? typeElement.GetString() ?? "textbox" : "textbox";
Setting? setting = CreateSetting(type, element);
if (setting is null)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Unknown setting type \"{type}\" for \"{id}\"", source: "WebPlugin");
continue;
}
SettingElement settingElement = new(setting, id, name, orderIndex);
if (settings.Settings.Exists(e => e.Id == id))
{
SettingElement saved = settings.Settings.First(e => e.Id == id);
settingElement.JsonValue = saved.JsonValue;
}
string capturedId = id;
if (setting is Button button)
{
button.OnClick += () =>
{
_ = webView.Dispatcher.InvokeAsync(async () =>
{
try
{
_ = await webView.ExecuteScriptAsync($"window.desktopMagic?.onClick?.({JsonSerializer.Serialize(capturedId)})");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify button click for \"{capturedId}\": {ex}", source: "WebPlugin");
}
});
};
}
setting.OnValueChanged += () =>
{
_ = webView.Dispatcher.InvokeAsync(async () =>
{
try
{
await NotifySettingChange(capturedId, setting);
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to notify setting change for \"{capturedId}\": {ex}", source: "WebPlugin");
}
});
};
settingElements.Add(settingElement);
orderIndex++;
}
settings.Settings = [.. settingElements.OrderBy(x => x.OrderIndex)];
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to load settings from settings.json: {ex.Message}", source: "WebPlugin");
}
}
private string GetSettingsBridgeScript()
{
string settingsJson = SerializeSettingsToJson();
return $@"
(function() {{
window.desktopMagic = {{}};
window.desktopMagic._settings = {settingsJson};
window.desktopMagic.getSettings = function() {{
return JSON.parse(JSON.stringify(window.desktopMagic._settings));
}};
window.desktopMagic.getSetting = function(id) {{
return window.desktopMagic._settings ? window.desktopMagic._settings[id] : undefined;
}};
window.desktopMagic.onSettingChanged = null;
window.desktopMagic.onButtonClick = null;
window.desktopMagic.dispatchSettingChanged = function(id, value) {{
if (window.desktopMagic._settings) {{
window.desktopMagic._settings[id] = value;
}}
if (typeof window.desktopMagic.onSettingChanged === 'function') {{
window.desktopMagic.onSettingChanged(id, value);
}}
}};
window.desktopMagic.onClick = function(id) {{
if (typeof window.desktopMagic.onButtonClick === 'function') {{
window.desktopMagic.onButtonClick(id);
}}
}};
}})();
";
}
private void InitializeHotReload()
{
try
{
fileWatcher = new FileSystemWatcher(PluginFolderPath)
{
NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size,
EnableRaisingEvents = false
};
fileWatcher.Changed += OnPluginFileChanged;
reloadDebounceTimer = new System.Timers.Timer(500)
{
AutoReset = false
};
reloadDebounceTimer.Elapsed += async (s, e) =>
{
await ReloadWebPlugin();
};
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Hot reload enabled", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogWarn($"\"{PluginMetadata.Name}\" - Failed to initialize hot reload: {ex.Message}", source: "WebPlugin");
}
}
private void OnPluginFileChanged(object sender, FileSystemEventArgs e)
{
if (isReloading)
{
return;
}
string? fileName = e.Name;
if (string.IsNullOrEmpty(fileName))
{
return;
}
string ext = Path.GetExtension(fileName).ToLowerInvariant();
if (ext is not ".html" and not ".json" and not ".js" and not ".css")
{
return;
}
reloadDebounceTimer?.Stop();
reloadDebounceTimer?.Start();
}
private async Task ReloadWebPlugin()
{
if (isReloading || !IsRunning)
{
return;
}
isReloading = true;
try
{
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Reloading web plugin", source: "WebPlugin");
if (fileWatcher != null)
{
fileWatcher.EnableRaisingEvents = false;
}
LoadWebPluginSettings();
await Dispatcher.Invoke(async () =>
{
if (isInitialized && webView.CoreWebView2 != null)
{
_ = await webView.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(GetSettingsBridgeScript());
busyMask.IsBusy = true;
string htmlPath = Path.Combine(PluginFolderPath, "main.html");
string htmlUri = new Uri(htmlPath).AbsoluteUri;
webView.CoreWebView2.Navigate(htmlUri);
busyMask.IsBusy = false;
}
});
App.Logger.LogInfo($"\"{PluginMetadata.Name}\" - Web plugin reloaded", source: "WebPlugin");
}
catch (Exception ex)
{
App.Logger.LogError($"\"{PluginMetadata.Name}\" - Failed to reload web plugin: {ex}", source: "WebPlugin");
}
finally
{
isReloading = false;
if (fileWatcher != null && IsRunning)
{
fileWatcher.EnableRaisingEvents = true;
}
}
}
private async System.Threading.Tasks.Task NotifySettingChange(string id, Setting setting)
{
object? value = GetSettingValue(setting);
string serializedValue = JsonSerializer.Serialize(value);
string serializedId = JsonSerializer.Serialize(id);
_ = await webView.ExecuteScriptAsync($"window.desktopMagic?.dispatchSettingChanged?.({serializedId}, {serializedValue})");
}
private string SerializeSettingsToJson()
{
Dictionary<string, object?> dict = [];
foreach (SettingElement element in settings.Settings)
{
dict[element.Id] = GetSettingValue(element.Input);
}
return JsonSerializer.Serialize(dict);
}
private static object? GetSettingValue(Setting setting)
{
return setting switch
{
TextBox tb => tb.Value,
CheckBox cb => cb.Value,
Slider sl => sl.Value,
IntegerUpDown iud => iud.Value,
ComboBox cb => cb.Value,
ColorPicker cp => MultiColorConverter.ConvertToHexRgba(cp.Value),
Button btn => btn.Value,
Label lbl => lbl.Value,
FileSelector fs => fs.Value,
_ => null
};
}
private static Setting? CreateSetting(string type, JsonElement element)
{
return type.ToLowerInvariant() switch
{
"textbox" => new TextBox(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : ""),
"checkbox" => new CheckBox(
element.TryGetProperty("default", out JsonElement defaultValue) && defaultValue.ValueKind == JsonValueKind.True),
"slider" => new Slider(
element.TryGetProperty("min", out JsonElement minValue) ? minValue.GetDouble() : 0,
element.TryGetProperty("max", out JsonElement maxValue) ? maxValue.GetDouble() : 100,
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetDouble() : 0),
"integer" => new IntegerUpDown(
element.TryGetProperty("min", out JsonElement minValue) ? minValue.GetInt32() : 0,
element.TryGetProperty("max", out JsonElement maxValue) ? maxValue.GetInt32() : 100,
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetInt32() : 0),
"combobox" => CreateComboBox(element),
"colorpicker" => new ColorPicker(
element.TryGetProperty("default", out JsonElement defaultValue)
? MultiColorConverter.ConvertToSystemColor(defaultValue.GetString() ?? "#FFFFFFFF", System.Drawing.Color.White)
: System.Drawing.Color.White),
"button" => new Button(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : ""),
"label" => new Label(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : "",
element.TryGetProperty("bold", out JsonElement boldValue) && boldValue.ValueKind == JsonValueKind.True),
"file" => new FileSelector(
element.TryGetProperty("default", out JsonElement defaultValue) ? defaultValue.GetString() ?? "" : "",
element.TryGetProperty("filter", out JsonElement filterValue) ? filterValue.GetString() ?? "All Files|*.*" : "All Files|*.*",
element.TryGetProperty("title", out JsonElement titleValue) ? titleValue.GetString() ?? "Select File" : "Select File",
element.TryGetProperty("selectFolder", out JsonElement selectFolderValue) && selectFolderValue.ValueKind == JsonValueKind.True),
_ => null
};
}
private static ComboBox CreateComboBox(JsonElement element)
{
List<string> items = [];
if (element.TryGetProperty("items", out JsonElement itemsValue) && itemsValue.ValueKind == JsonValueKind.Array)
{
foreach (JsonElement item in itemsValue.EnumerateArray())
{
items.Add(item.GetString() ?? "");
}
}
ComboBox comboBox = new([.. items]);
if (element.TryGetProperty("default", out JsonElement defaultValue))
{
string defaultText = defaultValue.GetString() ?? "";
if (!string.IsNullOrEmpty(defaultText) && items.Contains(defaultText))
{
comboBox.Value = defaultText;
}
}
return comboBox;
}
}
@@ -3,4 +3,5 @@
<BitmapImage x:Key="Icon" UriSource="/icon.ico" /> <BitmapImage x:Key="Icon" UriSource="/icon.ico" />
<BitmapImage x:Key="ModioCogBlue" UriSource="modio-cog-blue.png" /> <BitmapImage x:Key="ModioCogBlue" UriSource="modio-cog-blue.png" />
<BitmapImage x:Key="ModioLogoBlueDark" UriSource="modio-logo-bluedark.png" /> <BitmapImage x:Key="ModioLogoBlueDark" UriSource="modio-logo-bluedark.png" />
<BitmapImage x:Key="ModioLogoBlueLight" UriSource="modio-logo-bluelight.png" />
</ResourceDictionary> </ResourceDictionary>
Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

@@ -3,6 +3,10 @@
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib" xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:system="clr-namespace:System;assembly=mscorlib"> xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="developedBy">Entwickelt von Stone__Red</system:String>
<system:String x:Key="reportBug">Bug Melden</system:String>
<system:String x:Key="requestFeature">Feature Anfragen</system:String>
<system:String x:Key="email">E-Mail</system:String>
<system:String x:Key="lineMode">Linie</system:String> <system:String x:Key="lineMode">Linie</system:String>
<system:String x:Key="mirrorMode">Spiegeln</system:String> <system:String x:Key="mirrorMode">Spiegeln</system:String>
<system:String x:Key="folder">Ordner</system:String> <system:String x:Key="folder">Ordner</system:String>
@@ -15,13 +19,15 @@
<system:String x:Key="enterModIoEmail">Mod.io Account E-Mail eingeben</system:String> <system:String x:Key="enterModIoEmail">Mod.io Account E-Mail eingeben</system:String>
<system:String x:Key="enterModIoAccessToken">Geben Sie Ihren mod.io Sicherheitscode ein</system:String> <system:String x:Key="enterModIoAccessToken">Geben Sie Ihren mod.io Sicherheitscode ein</system:String>
<system:String x:Key="toggleEditMode">Bearbeitungsmodus umschalten</system:String> <system:String x:Key="toggleEditMode">Bearbeitungsmodus umschalten</system:String>
<system:String x:Key="allPlugins">Alle Plugins</system:String> <system:String x:Key="searchAllPlugins">Alle Plugins Suchen...</system:String>
<system:String x:Key="installedPlugins">Installierte Plugins</system:String> <system:String x:Key="searchInstalledPlugins">Installierte Plugins Suchen...</system:String>
<system:String x:Key="updatePlugins">Neu Laden</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="pluginsFolder">Plugins Ordner</system:String>
<system:String x:Key="pluginManager">Plugins Manager</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="createNewPlugin">Neues Plugin erstellen</system:String>
<system:String x:Key="enterPluginName">Pluginnamen eingeben</system:String> <system:String x:Key="enterPluginName">Pluginnamen eingeben</system:String>
<system:String x:Key="plugins">Plugins</system:String>
<system:String x:Key="enablePluginToConfigure">Aktivieren Sie das Plugin, um seine Einstellungen zu konfigurieren.</system:String>
<system:String x:Key="color">Farbe:</system:String> <system:String x:Key="color">Farbe:</system:String>
<system:String x:Key="default">Standard</system:String> <system:String x:Key="default">Standard</system:String>
<system:String x:Key="wantToCloseProgram">Wollen sie das Programm wirklich schließen?</system:String> <system:String x:Key="wantToCloseProgram">Wollen sie das Programm wirklich schließen?</system:String>
@@ -31,14 +37,20 @@
<system:String x:Key="date">Datum</system:String> <system:String x:Key="date">Datum</system:String>
<system:String x:Key="cpuUsage">CPU Auslastung</system:String> <system:String x:Key="cpuUsage">CPU Auslastung</system:String>
<system:String x:Key="musicVisualizer">Musik Visualisierer</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="amplifier">Signalverstärkung:</system:String>
<system:String x:Key="newLayout">Neues Layout</system:String> <system:String x:Key="newLayout">Neues Layout</system:String>
<system:String x:Key="deleteLayout">Layout Löschen</system:String> <system:String x:Key="deleteLayout">Layout Löschen</system:String>
<system:String x:Key="newTheme">Neues Theme</system:String> <system:String x:Key="newTheme">Neues Theme</system:String>
<system:String x:Key="deleteTheme">Theme Löschen</system:String> <system:String x:Key="deleteTheme">Theme Löschen</system:String>
<system:String x:Key="changeTheme">Theme Ändern</system:String> <system:String x:Key="editTheme">Theme Bearbeiten</system:String>
<system:String x:Key="themes">Themes</system:String>
<system:String x:Key="ok">Ok</system:String> <system:String x:Key="ok">Ok</system:String>
<system:String x:Key="cancel">Abbrechen</system:String> <system:String x:Key="cancel">Abbrechen</system:String>
<system:String x:Key="confirmDeleteLayout">Möchten Sie dieses Layout wirklich löschen?</system:String>
<system:String x:Key="confirmDeleteTheme">Möchten Sie dieses Theme wirklich löschen?</system:String>
<system:String x:Key="enterLayoutName">Layoutnamen eingeben:</system:String> <system:String x:Key="enterLayoutName">Layoutnamen eingeben:</system:String>
<system:String x:Key="layoutAlreadyExists">Layout existiert bereits!</system:String> <system:String x:Key="layoutAlreadyExists">Layout existiert bereits!</system:String>
<system:String x:Key="enterThemeName">Themenamen eingeben:</system:String> <system:String x:Key="enterThemeName">Themenamen eingeben:</system:String>
@@ -50,6 +62,11 @@
<system:String x:Key="addedFormat">Erstellt: {0}</system:String> <system:String x:Key="addedFormat">Erstellt: {0}</system:String>
<system:String x:Key="updatedFormat">Aktualisiert: {0}</system:String> <system:String x:Key="updatedFormat">Aktualisiert: {0}</system:String>
<system:String x:Key="versionFormat">Version: {0}</system:String> <system:String x:Key="versionFormat">Version: {0}</system:String>
<system:String x:Key="releaseInfoTitle">DesktopMagic wurde aktualisiert!</system:String>
<system:String x:Key="releaseInfoHeaderFormat">Neueste Version: {0}</system:String>
<system:String x:Key="releaseInfoPublishedFormat">Veröffentlicht: {0}</system:String>
<system:String x:Key="releaseInfoBodyLabel">Release Notes:</system:String>
<system:String x:Key="openReleasePage">Release-Seite öffnen</system:String>
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems"> <col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
<system:String>Unten</system:String> <system:String>Unten</system:String>
<system:String>Mitte</system:String> <system:String>Mitte</system:String>
@@ -3,6 +3,10 @@
xmlns:col="clr-namespace:System.Collections;assembly=mscorlib" xmlns:col="clr-namespace:System.Collections;assembly=mscorlib"
xmlns:system="clr-namespace:System;assembly=mscorlib"> xmlns:system="clr-namespace:System;assembly=mscorlib">
<system:String x:Key="developedBy">Developed by Stone__Red</system:String>
<system:String x:Key="reportBug">Report Bug</system:String>
<system:String x:Key="requestFeature">Request Feature</system:String>
<system:String x:Key="email">Email</system:String>
<system:String x:Key="lineMode">Line</system:String> <system:String x:Key="lineMode">Line</system:String>
<system:String x:Key="mirrorMode">Mirror</system:String> <system:String x:Key="mirrorMode">Mirror</system:String>
<system:String x:Key="folder">Folder</system:String> <system:String x:Key="folder">Folder</system:String>
@@ -15,13 +19,15 @@
<system:String x:Key="enterModIoEmail">Enter your mod.io account E-mail</system:String> <system:String x:Key="enterModIoEmail">Enter your mod.io account E-mail</system:String>
<system:String x:Key="enterModIoAccessToken">Enter your mod.io security code</system:String> <system:String x:Key="enterModIoAccessToken">Enter your mod.io security code</system:String>
<system:String x:Key="toggleEditMode">Toggle Edit Mode</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="searchAllPlugins">Search All Plugins...</system:String>
<system:String x:Key="installedPlugins">Installed Plugins</system:String> <system:String x:Key="searchInstalledPlugins">Search Installed Plugins...</system:String>
<system:String x:Key="updatePlugins">Reload</system:String> <system:String x:Key="reloadPlugins">Reload Plugins</system:String>
<system:String x:Key="pluginsFolder">Plugins Folder</system:String> <system:String x:Key="pluginsFolder">Plugins Folder</system:String>
<system:String x:Key="pluginManager">Plugins Manager</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="createNewPlugin">Create New Plugin</system:String>
<system:String x:Key="enterPluginName">Enter plugin name</system:String> <system:String x:Key="enterPluginName">Enter plugin name</system:String>
<system:String x:Key="plugins">Plugins</system:String>
<system:String x:Key="enablePluginToConfigure">Enable the plugin to configure its settings.</system:String>
<system:String x:Key="color">Color:</system:String> <system:String x:Key="color">Color:</system:String>
<system:String x:Key="default">Default</system:String> <system:String x:Key="default">Default</system:String>
<system:String x:Key="wantToCloseProgram">Do you really want to close the program?</system:String> <system:String x:Key="wantToCloseProgram">Do you really want to close the program?</system:String>
@@ -31,14 +37,20 @@
<system:String x:Key="date">Date</system:String> <system:String x:Key="date">Date</system:String>
<system:String x:Key="cpuUsage">CPU Usage</system:String> <system:String x:Key="cpuUsage">CPU Usage</system:String>
<system:String x:Key="musicVisualizer">Music Visualizer</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="amplifier">Signal Amplification:</system:String>
<system:String x:Key="newLayout">New Layout</system:String> <system:String x:Key="newLayout">New Layout</system:String>
<system:String x:Key="deleteLayout">Delete Layout</system:String> <system:String x:Key="deleteLayout">Delete Layout</system:String>
<system:String x:Key="newTheme">New Theme</system:String> <system:String x:Key="newTheme">New Theme</system:String>
<system:String x:Key="deleteTheme">Delete Theme</system:String> <system:String x:Key="deleteTheme">Delete Theme</system:String>
<system:String x:Key="changeTheme">Change Theme</system:String> <system:String x:Key="editTheme">Edit Theme</system:String>
<system:String x:Key="themes">Themes</system:String>
<system:String x:Key="ok">Ok</system:String> <system:String x:Key="ok">Ok</system:String>
<system:String x:Key="cancel">Cancel</system:String> <system:String x:Key="cancel">Cancel</system:String>
<system:String x:Key="confirmDeleteLayout">Do you really want to delete this layout?</system:String>
<system:String x:Key="confirmDeleteTheme">Do you really want to delete this theme?</system:String>
<system:String x:Key="enterLayoutName">Enter layout name</system:String> <system:String x:Key="enterLayoutName">Enter layout name</system:String>
<system:String x:Key="layoutAlreadyExists">Layout already exists!</system:String> <system:String x:Key="layoutAlreadyExists">Layout already exists!</system:String>
<system:String x:Key="cannotDeleteLastLayout">Can't delete last layout!</system:String> <system:String x:Key="cannotDeleteLastLayout">Can't delete last layout!</system:String>
@@ -52,6 +64,11 @@
<system:String x:Key="addedFormat">Added: {0}</system:String> <system:String x:Key="addedFormat">Added: {0}</system:String>
<system:String x:Key="updatedFormat">Updated: {0}</system:String> <system:String x:Key="updatedFormat">Updated: {0}</system:String>
<system:String x:Key="versionFormat">Version: {0}</system:String> <system:String x:Key="versionFormat">Version: {0}</system:String>
<system:String x:Key="releaseInfoTitle">DesktopMagic has been updated!</system:String>
<system:String x:Key="releaseInfoHeaderFormat">Latest release: {0}</system:String>
<system:String x:Key="releaseInfoPublishedFormat">Published: {0}</system:String>
<system:String x:Key="releaseInfoBodyLabel">Release notes:</system:String>
<system:String x:Key="openReleasePage">Open release page</system:String>
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems"> <col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
<system:String>Bottom</system:String> <system:String>Bottom</system:String>
<system:String>Middle</system:String> <system:String>Middle</system:String>
@@ -0,0 +1,131 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="http://schemas.lepo.co/wpfui/2022/xaml"
xmlns:system="clr-namespace:System;assembly=System.Runtime">
<system:Double x:Key="ToggleButtonWidth">40</system:Double>
<system:Double x:Key="ToggleButtonHeight">20</system:Double>
<Thickness x:Key="ToggleSwitchBorderThemeThickness">1</Thickness>
<Thickness x:Key="ToggleSwitchContentLeftMargin">0,0,8,0</Thickness>
<Style x:Key="ToggleSwitchContentLeftStyle" TargetType="{x:Type ui:ToggleSwitch}">
<Setter Property="FocusVisualStyle" Value="{DynamicResource DefaultControlFocusVisualStyle}" />
<Setter Property="Background" Value="{DynamicResource ToggleSwitchFillOn}" />
<Setter Property="Foreground" Value="{DynamicResource ToggleSwitchContentForeground}" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="{StaticResource ToggleSwitchBorderThemeThickness}" />
<Setter Property="Padding" Value="0" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="HorizontalContentAlignment" Value="Left" />
<Setter Property="VerticalContentAlignment" Value="Center" />
<Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}" />
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ui:ToggleSwitch}">
<Grid Margin="{TemplateBinding Padding}" Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter
x:Name="ContentPresenter"
Grid.Column="0"
Margin="{StaticResource ToggleSwitchContentLeftMargin}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}"
RecognizesAccessKey="True"
TextElement.Foreground="{TemplateBinding Foreground}" />
<Grid
Grid.Column="1"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}">
<Rectangle
x:Name="ToggleRectangle"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}"
Fill="{DynamicResource ToggleSwitchFillOff}"
RadiusX="10" RadiusY="10"
Stroke="{DynamicResource ToggleSwitchStrokeOff}"
StrokeThickness="1" />
<Rectangle
x:Name="ActiveToggleRectangle"
Width="{StaticResource ToggleButtonWidth}"
Height="{StaticResource ToggleButtonHeight}"
Fill="{TemplateBinding Background}"
Opacity="0.0"
RadiusX="10" RadiusY="10" />
<Rectangle
x:Name="ToggleEllipse"
Width="12" Height="12"
Fill="{DynamicResource ToggleSwitchKnobFillOff}"
RadiusX="7" RadiusY="7">
<Rectangle.RenderTransform>
<TranslateTransform X="-9" />
</Rectangle.RenderTransform>
</Rectangle>
<Rectangle
x:Name="ActiveToggleEllipse"
Width="12" Height="12"
Fill="{DynamicResource ToggleSwitchKnobFillOn}"
Opacity="0.0"
RadiusX="7" RadiusY="7">
<Rectangle.RenderTransform>
<TranslateTransform X="-9" />
</Rectangle.RenderTransform>
</Rectangle>
</Grid>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsChecked" Value="True">
<Trigger.EnterActions>
<BeginStoryboard Name="SlideRight">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="9" Duration="0:0:0.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetName="ToggleRectangle" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleRectangle" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ToggleEllipse" Storyboard.TargetProperty="Opacity" To="1.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="Opacity" To="0.0" Duration="0:0:0.16" />
<DoubleAnimation Storyboard.TargetName="ActiveToggleEllipse" Storyboard.TargetProperty="(Rectangle.RenderTransform).(TranslateTransform.X)" To="-9" Duration="0:0:0.167" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Content" Value="{x:Null}" />
<Condition Property="IsChecked" Value="True" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OnContent, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="Content" Value="{x:Null}" />
<Condition Property="IsChecked" Value="False" />
</MultiTrigger.Conditions>
<Setter TargetName="ContentPresenter" Property="Content" Value="{Binding OffContent, RelativeSource={RelativeSource TemplatedParent}}" />
</MultiTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
@@ -8,7 +8,7 @@ using System.Text.Json.Serialization;
namespace DesktopMagic.Settings; namespace DesktopMagic.Settings;
internal class DesktopMagicSettings : INotifyPropertyChanged public class DesktopMagicSettings : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
@@ -44,7 +44,16 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
public string? CurrentLayoutName public string? CurrentLayoutName
{ {
get => currentLayoutName ?? Layouts.FirstOrDefault()?.Name; get
{
if (!Layouts.Any(l => l.Name == currentLayoutName))
{
currentLayoutName = null;
}
return currentLayoutName ?? Layouts.FirstOrDefault()?.Name;
}
set set
{ {
currentLayoutName = value; currentLayoutName = value;
@@ -55,6 +64,8 @@ internal class DesktopMagicSettings : INotifyPropertyChanged
public string? ModIoAccessToken { get; set; } public string? ModIoAccessToken { get; set; }
public string? ReleaseInfoLastAppVersion { get; set; }
public DesktopMagicSettings() public DesktopMagicSettings()
{ {
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme(); themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
+1 -1
View File
@@ -9,7 +9,7 @@ using System.Text.Json.Serialization;
namespace DesktopMagic.Settings; namespace DesktopMagic.Settings;
internal class Layout(string name) : INotifyPropertyChanged public class Layout(string name) : INotifyPropertyChanged
{ {
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
+20 -1
View File
@@ -5,6 +5,7 @@ using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using System.Windows; using System.Windows;
@@ -16,13 +17,14 @@ public class PluginSettings : INotifyPropertyChanged
private string? currentThemeName; private string? currentThemeName;
private List<SettingElement> settings = []; private List<SettingElement> settings = [];
private Dictionary<string, JsonElement> state = [];
private bool enabled = false; private bool enabled = false;
private Point position = new Point(100, 100); private Point position = new Point(100, 100);
private Point size = new Point(300, 300); private Point size = new Point(300, 300);
// Only for internal use to show the name of the plugin in the main window // Only for internal use to show the name of the plugin in the main window
[JsonIgnore] [JsonIgnore]
public string Name { get; set; } = string.Empty; public PluginMetadata Metadata { get; set; } = new();
[JsonIgnore] [JsonIgnore]
public Theme Theme public Theme Theme
@@ -61,6 +63,23 @@ public class PluginSettings : INotifyPropertyChanged
} }
} }
/// <summary>
/// Dictionary storing plugin state. Key is the field/property name, value is the JsonElement.
/// This is persisted per layout in settings.json.
/// </summary>
public Dictionary<string, JsonElement> State
{
get => state;
set
{
if (state != value)
{
state = value;
OnPropertyChanged();
}
}
}
public bool Enabled public bool Enabled
{ {
get => enabled; get => enabled;
+3 -3
View File
@@ -14,10 +14,10 @@ namespace DesktopMagic.PluginTest;
public class GifPlugin : Plugin public class GifPlugin : Plugin
{ {
[Setting("gif-path", "GIF path")] [Setting("gif-path", "GIF path")]
private readonly TextBox input = new TextBox(""); private readonly FileSelector input = new FileSelector();
[Setting("info")] [Setting("info")]
private readonly Label info = new Label(""); private readonly Label info = new Label("Select a GIF file to load.");
private readonly List<Bitmap> bitmaps = []; private readonly List<Bitmap> bitmaps = [];
@@ -92,7 +92,7 @@ public class GifPlugin : Plugin
info.Value = $"Loading frame {i + 1} of {gif.GetFrameCount(FrameDimension.Time)}"; info.Value = $"Loading frame {i + 1} of {gif.GetFrameCount(FrameDimension.Time)}";
} }
info.Value = string.Empty; info.Value = $"Loaded {bitmaps.Count} frames.";
} }
else else
{ {
+47
View File
@@ -0,0 +1,47 @@
using System;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
namespace DesktopMagic.Api;
/// <summary>
/// Provides an abstract base class for creating asynchronous plugins that can be integrated into the application, supporting periodic updates,
/// rendering, and user interaction.
/// </summary>
/// <remarks>Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
/// such as <see cref="Plugin.Start"/>, <see cref="Plugin.Stop"/>, and <see cref="Main"/> for activation, deactivation, and periodic
/// execution. It also provides event handlers for mouse and theme interactions, as well as access to application data
/// and rendering configuration. Implementations should override relevant methods to respond to user input, update
/// intervals, and configuration changes as needed.</remarks>
public abstract class AsyncPlugin : Plugin
{
/// <summary>
/// Occurs once when the plugin gets activated. Override for async initialization.
/// </summary>
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
public virtual Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs once when the plugin gets deactivated. Override for async cleanup.
/// </summary>
/// <param name="cancellationToken">Token signaled when the host requests cancellation.</param>
public virtual Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
/// <summary>
/// Occurs when the <see cref="Plugin.UpdateInterval"/> elapses.
/// </summary>
/// <returns></returns>
public abstract Task<Bitmap?> MainAsync(CancellationToken cancellationToken);
/// <summary>
/// This method should not be called directly! Override and use <see cref="MainAsync"/> for asynchronous plugin operations.
/// </summary>
/// <remarks>This method only exists to fulfill base class requirements.</remarks>
/// <returns>Calling it will always result in an <see cref="InvalidOperationException"/>.</returns>
/// <exception cref="InvalidOperationException">Thrown if this method is called directly. Use MainAsync for plugin execution instead.</exception>
public sealed override Bitmap? Main()
{
throw new InvalidOperationException($"AsyncPlugin.Main() should not be called directly. Override {nameof(MainAsync)} instead.");
}
}
@@ -4,15 +4,17 @@
<TargetFramework>net8.0-windows</TargetFramework> <TargetFramework>net8.0-windows</TargetFramework>
<Company>Stone_Red</Company> <Company>Stone_Red</Company>
<Product>Stone_Red</Product> <Product>Stone_Red</Product>
<Version>0.0.0.5</Version> <Version>1.0.0.0</Version>
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl> <RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<AssemblyVersion>0.0.0.5</AssemblyVersion> <AssemblyVersion>1.0.0.0</AssemblyVersion>
<PackageProjectUrl></PackageProjectUrl> <PackageProjectUrl></PackageProjectUrl>
<FileVersion>0.0.0.5</FileVersion> <FileVersion></FileVersion>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Platforms>AnyCPU;x64</Platforms> <Platforms>AnyCPU;x64</Platforms>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageIcon>Square44x44Logo.altform-lightunplated_targetsize-256.png</PackageIcon>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'"> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
@@ -36,6 +38,14 @@
<Pack>True</Pack> <Pack>True</Pack>
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
<None Include="..\..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Include="..\MsixPackaging\Images\Square44x44Logo.altform-lightunplated_targetsize-256.png">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+23
View File
@@ -36,4 +36,27 @@ public interface IPluginData
/// Updates the plugin window. /// Updates the plugin window.
/// </summary> /// </summary>
void UpdateWindow(); void UpdateWindow();
/// <summary>
/// Logs a message to the application log.
/// </summary>
/// <param name="message">The message to log.</param>
/// <param name="level">The log level (Info, Warning, Error).</param>
void Log(string message, LogLevel level = LogLevel.Info);
/// <summary>
/// Shows a message box to the user.
/// </summary>
/// <param name="message">The message to display.</param>
/// <param name="title">The title of the message box.</param>
void ShowMessage(string message, string? title = null);
/// <summary>
/// Saves the current state of fields and properties marked with <see cref="PersistStateAttribute"/>
/// </summary>
/// <remarks>
/// State is automatically saved when the plugin stops, but this method can be called
/// to save state at any time, such as after important user interactions.
/// </remarks>
void SaveState();
} }
+22
View File
@@ -0,0 +1,22 @@
namespace DesktopMagic.Api;
/// <summary>
/// Specifies the severity level of a log message.
/// </summary>
public enum LogLevel
{
/// <summary>
/// Informational message for general information.
/// </summary>
Info,
/// <summary>
/// Warning message indicating a potential issue.
/// </summary>
Warning,
/// <summary>
/// Error message indicating a failure or critical issue.
/// </summary>
Error
}
@@ -1,4 +1,4 @@
namespace DesktopMagic.Api.Settings; namespace DesktopMagic.Api;
/// <summary> /// <summary>
/// Mouse Buttons /// Mouse Buttons
@@ -0,0 +1,15 @@
using System;
namespace DesktopMagic.Api;
/// <summary>
/// Marks a field or property to be automatically persisted between plugin sessions.
/// </summary>
/// <remarks>
/// The field or property must be JSON serializable. Complex types should have parameterless constructors.
/// State is saved when the plugin stops and loaded when it starts.
/// </remarks>
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public sealed class PersistStateAttribute : Attribute
{
}
+28 -2
View File
@@ -7,8 +7,14 @@ using System.Drawing;
namespace DesktopMagic.Api; namespace DesktopMagic.Api;
/// <summary> /// <summary>
/// The plugin class. /// Provides an abstract base class for creating plugins that can be integrated into the application, supporting periodic updates,
/// rendering, and user interaction.
/// </summary> /// </summary>
/// <remarks>Derive from this class to implement custom plugin functionality. The class defines lifecycle methods
/// such as <see cref="Start"/>, <see cref="Stop"/>, and <see cref="Main"/> for activation, deactivation, and periodic
/// execution. It also provides event handlers for mouse and theme interactions, as well as access to application data
/// and rendering configuration. Implementations should override relevant methods to respond to user input, update
/// intervals, and configuration changes as needed.</remarks>
public abstract class Plugin public abstract class Plugin
{ {
[Setting("desktopmagic-horizontal-alignment", "Horizontal Alignment", -999)] [Setting("desktopmagic-horizontal-alignment", "Horizontal Alignment", -999)]
@@ -17,7 +23,13 @@ public abstract class Plugin
[Setting("desktopmagic-vertical-alignment", "Vertical Alignment", -998)] [Setting("desktopmagic-vertical-alignment", "Vertical Alignment", -998)]
internal ComboBox verticalAlignment = new ComboBox("Center", "Top", "Bottom"); internal ComboBox verticalAlignment = new ComboBox("Center", "Top", "Bottom");
[Setting("theme-override", "Theme Override", -997)] [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>"); internal ComboBox themeOverride = new ComboBox("<None>");
private IPluginData application = null!; private IPluginData application = null!;
@@ -86,4 +98,18 @@ public abstract class Plugin
public virtual void OnMouseWheel(Point position, int delta) public virtual void OnMouseWheel(Point position, int delta)
{ {
} }
/// <summary>
/// Occurs when the application's theme has changed.
/// </summary>
public virtual void OnThemeChanged()
{
}
/// <summary>
/// Invoked when the settings have changed to allow derived classes to respond to configuration updates.
/// </summary>
public virtual void OnSettingsChanged()
{
}
} }
@@ -0,0 +1,62 @@
using System.Drawing;
using System.Globalization;
namespace DesktopMagic.Api.Settings;
/// <summary>
/// Represents a color picker control.
/// </summary>
public sealed class ColorPicker : Setting
{
private Color _value;
/// <summary>
/// Gets or sets the color value assigned to the <see cref="ColorPicker"/> element.
/// </summary>
public Color Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
ValueChanged();
}
}
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPicker"/> class with the provided <paramref name="defaultColor"/>.
/// </summary>
/// <param name="defaultColor">The default color value.</param>
public ColorPicker(Color defaultColor)
{
_value = defaultColor;
}
/// <summary>
/// Initializes a new instance of the <see cref="ColorPicker"/> class with a default color of White.
/// </summary>
public ColorPicker() : this(Color.White)
{
}
internal override string GetJsonValue()
{
return $"{Value.A},{Value.R},{Value.G},{Value.B}";
}
internal override void SetJsonValue(string value)
{
string[] parts = value.Split(',');
if (parts.Length == 4 &&
byte.TryParse(parts[0], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte a) &&
byte.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte r) &&
byte.TryParse(parts[2], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte g) &&
byte.TryParse(parts[3], NumberStyles.Integer, CultureInfo.InvariantCulture, out byte b))
{
Value = Color.FromArgb(a, r, g, b);
}
}
}
@@ -0,0 +1,65 @@
namespace DesktopMagic.Api.Settings;
/// <summary>
/// Represents a file selector control that allows the user to browse for a file.
/// </summary>
public sealed class FileSelector : Setting
{
private string _value = string.Empty;
/// <summary>
/// Gets or sets the selected file path.
/// </summary>
public string Value
{
get => _value;
set
{
if (_value != value)
{
_value = value;
ValueChanged();
}
}
}
/// <summary>
/// Gets or sets the file filter for the file dialog (e.g., "Image Files|*.png;*.jpg|All Files|*.*").
/// </summary>
public string Filter { get; set; }
/// <summary>
/// Gets or sets the title of the file dialog.
/// </summary>
public string Title { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the file selector should select folders instead of files.
/// </summary>
public bool SelectFolder { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="FileSelector"/> class.
/// </summary>
/// <param name="defaultPath">The default file path.</param>
/// <param name="filter">The file filter for the file dialog.</param>
/// <param name="title">The title of the file dialog.</param>
/// <param name="selectFolder">If true, selects folders instead of files.</param>
public FileSelector(string defaultPath = "", string filter = "All Files|*.*", string title = "Select File", bool selectFolder = false)
{
_value = defaultPath;
Filter = filter;
Title = title;
SelectFolder = selectFolder;
}
internal override string GetJsonValue()
{
return Value;
}
internal override void SetJsonValue(string value)
{
Value = value;
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
<Identity <Identity
Name="StoneRed.DesktopMagic" Name="StoneRed.DesktopMagic"
Publisher="CN=6D1FD759-1951-428A-9B03-964461C7DBF5" Publisher="CN=6D1FD759-1951-428A-9B03-964461C7DBF5"
Version="1.2.0.0" /> Version="1.3.3.0" />
<Properties> <Properties>
<DisplayName>Desktop Magic</DisplayName> <DisplayName>Desktop Magic</DisplayName>