mirror of
https://github.com/Stone-Red-Code/DesktopMagic.git
synced 2026-09-04 00:46:12 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
930222f364 | ||
|
|
f4ac2c65f3 | ||
|
|
5fcbe2af79 | ||
|
|
33962cd68b | ||
|
|
c6b973b111 | ||
|
|
d43f1ed949 | ||
|
|
4999ae6e88 | ||
|
|
6061efba02 | ||
|
|
8e333df71b | ||
|
|
9b3b7aa4b6 | ||
|
|
2c82c9373f | ||
|
|
51b4f15cf1 | ||
|
|
2916f0983a | ||
|
|
3683e75a17 | ||
|
|
3d98c8b196 |
Binary file not shown.
@@ -1,5 +1,6 @@
|
||||
<Application x:Class="DesktopMagic.App"
|
||||
<Application x:Class="DesktopMagic.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
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"
|
||||
@@ -10,6 +11,8 @@
|
||||
<ResourceDictionary Source="Resources\Strings\StringResources.en.xaml" />
|
||||
<ui:ThemesDictionary Theme="Light" />
|
||||
<ui:ControlsDictionary />
|
||||
<md:ThemesDictionary Theme="Light" />
|
||||
<md:ControlsDictionary />
|
||||
<ResourceDictionary Source="pack://application:,,,/BusyIndicator;component/Theme/Default.xaml" />
|
||||
<ResourceDictionary Source="Resources\Images\ImageResources.xaml" />
|
||||
<ResourceDictionary Source="Resources\Styles\ToggleSwitchContentLeftStyle.xaml" />
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
using DesktopMagic.Api;
|
||||
using DesktopMagic.Api.Settings;
|
||||
using DesktopMagic.Helpers;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Drawing.Text;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DesktopMagic.BuiltInPlugins;
|
||||
|
||||
internal class AgendaPlugin : AsyncPlugin
|
||||
{
|
||||
[Setting("agenda-calendar-url", "iCal URL")]
|
||||
private readonly TextBox calendarUrl = new TextBox(string.Empty);
|
||||
|
||||
[Setting("agenda-refresh-button", "Refresh Calendar")]
|
||||
private readonly Button refreshButton = new Button("Refresh");
|
||||
|
||||
[Setting("agenda-refresh-interval", "Refresh Every (minutes)")]
|
||||
private readonly IntegerUpDown refreshMinutes = new IntegerUpDown(1, 120, 10);
|
||||
|
||||
[Setting("agenda-lookahead-hours", "Look Ahead (hours)")]
|
||||
private readonly IntegerUpDown lookAheadHours = new IntegerUpDown(1, 336, 24);
|
||||
|
||||
[Setting("agenda-max-events", "Maximum Events")]
|
||||
private readonly IntegerUpDown maxEvents = new IntegerUpDown(1, 15, 5);
|
||||
|
||||
[Setting("agenda-include-all-day", "Include All-Day Events")]
|
||||
private readonly CheckBox includeAllDayEvents = new CheckBox(true);
|
||||
|
||||
[Setting("agenda-show-date", "Show Date")]
|
||||
private readonly CheckBox showDate = new CheckBox(true);
|
||||
|
||||
public override int UpdateInterval { get; set; } = 30000;
|
||||
|
||||
private List<CalendarEventEntry> upcomingEvents = [];
|
||||
private DateTime nextRefreshTime = DateTime.MinValue;
|
||||
private bool forceRefresh = true;
|
||||
private bool themeChanged = true;
|
||||
private string? errorText;
|
||||
|
||||
public override Task StartAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
refreshButton.OnClick += () =>
|
||||
{
|
||||
forceRefresh = true;
|
||||
Application.UpdateWindow();
|
||||
};
|
||||
|
||||
calendarUrl.OnValueChanged += MarkForRefresh;
|
||||
refreshMinutes.OnValueChanged += MarkForRefresh;
|
||||
lookAheadHours.OnValueChanged += MarkForRefresh;
|
||||
maxEvents.OnValueChanged += MarkForRefresh;
|
||||
includeAllDayEvents.OnValueChanged += MarkForRefresh;
|
||||
showDate.OnValueChanged += Application.UpdateWindow;
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public override async Task<Bitmap?> MainAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (themeChanged || forceRefresh || DateTime.Now >= nextRefreshTime)
|
||||
{
|
||||
await RefreshEventsAsync(cancellationToken);
|
||||
themeChanged = false;
|
||||
}
|
||||
|
||||
return RenderAgendaBitmap();
|
||||
}
|
||||
|
||||
public override void OnThemeChanged()
|
||||
{
|
||||
themeChanged = true;
|
||||
}
|
||||
|
||||
public override void OnSettingsChanged()
|
||||
{
|
||||
Application.UpdateWindow();
|
||||
}
|
||||
|
||||
private void MarkForRefresh()
|
||||
{
|
||||
forceRefresh = true;
|
||||
Application.UpdateWindow();
|
||||
}
|
||||
|
||||
private async Task RefreshEventsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
forceRefresh = false;
|
||||
nextRefreshTime = DateTime.Now.AddMinutes(Math.Max(1, refreshMinutes.Value));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(calendarUrl.Value))
|
||||
{
|
||||
upcomingEvents = [];
|
||||
errorText = "Set iCal URL in settings";
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
DateTime now = DateTime.Now;
|
||||
DateTime windowEnd = now.AddHours(Math.Max(1, lookAheadHours.Value));
|
||||
|
||||
upcomingEvents = await IcsCalendarHelper.GetUpcomingEventsAsync(
|
||||
calendarUrl.Value.Trim(),
|
||||
now,
|
||||
windowEnd,
|
||||
maxEvents.Value,
|
||||
includeAllDayEvents.Value,
|
||||
cancellationToken);
|
||||
|
||||
errorText = null;
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
upcomingEvents = [];
|
||||
errorText = $"Calendar unavailable: {ex.Message}";
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
upcomingEvents = [];
|
||||
errorText = $"Calendar unavailable: {ex.Message}";
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
upcomingEvents = [];
|
||||
errorText = $"Calendar unavailable: {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private Bitmap RenderAgendaBitmap()
|
||||
{
|
||||
List<(string Day, string Date, string Time, string Title, bool IsMessage)> rows = [];
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(errorText))
|
||||
{
|
||||
rows.Add((string.Empty, string.Empty, string.Empty, errorText, true));
|
||||
}
|
||||
else if (upcomingEvents.Count == 0)
|
||||
{
|
||||
rows.Add((string.Empty, string.Empty, string.Empty, "No events in selected window", true));
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (CalendarEventEntry eventEntry in upcomingEvents)
|
||||
{
|
||||
string dayPart = showDate.Value ? eventEntry.StartLocal.ToString("ddd") : string.Empty;
|
||||
string datePart = showDate.Value ? eventEntry.StartLocal.ToString("dd MMM yyyy") : string.Empty;
|
||||
string timePart = eventEntry.IsAllDay ? "All day" : eventEntry.StartLocal.ToShortTimeString();
|
||||
|
||||
rows.Add((dayPart, datePart, timePart, eventEntry.Title, false));
|
||||
}
|
||||
}
|
||||
|
||||
using Font font = new Font(Application.Theme.Font, 70);
|
||||
using Bitmap measureBitmap = new Bitmap(1, 1);
|
||||
using Graphics measureGraphics = Graphics.FromImage(measureBitmap);
|
||||
measureGraphics.TextRenderingHint = TextRenderingHint.AntiAlias;
|
||||
|
||||
bool hasDayColumn = rows.Any(row => !row.IsMessage && !string.IsNullOrWhiteSpace(row.Day));
|
||||
bool hasDateColumn = rows.Any(row => !row.IsMessage && !string.IsNullOrWhiteSpace(row.Date));
|
||||
bool hasTimeColumn = rows.Any(row => !row.IsMessage && !string.IsNullOrWhiteSpace(row.Time));
|
||||
float maxDayWidth = 0;
|
||||
float maxDateWidth = 0;
|
||||
float maxTimeWidth = 0;
|
||||
float maxTitleWidth = 0;
|
||||
float lineHeight = 0;
|
||||
|
||||
foreach ((string day, string date, string time, string title, bool isMessage) in rows)
|
||||
{
|
||||
if (isMessage)
|
||||
{
|
||||
SizeF messageSize = measureGraphics.MeasureString(title, font);
|
||||
maxTitleWidth = Math.Max(maxTitleWidth, messageSize.Width);
|
||||
lineHeight = Math.Max(lineHeight, messageSize.Height);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasDayColumn)
|
||||
{
|
||||
SizeF daySize = measureGraphics.MeasureString(day, font);
|
||||
maxDayWidth = Math.Max(maxDayWidth, daySize.Width);
|
||||
lineHeight = Math.Max(lineHeight, daySize.Height);
|
||||
}
|
||||
|
||||
if (hasDateColumn)
|
||||
{
|
||||
SizeF dateSize = measureGraphics.MeasureString(date, font);
|
||||
maxDateWidth = Math.Max(maxDateWidth, dateSize.Width);
|
||||
lineHeight = Math.Max(lineHeight, dateSize.Height);
|
||||
}
|
||||
|
||||
if (hasTimeColumn)
|
||||
{
|
||||
SizeF timeSize = measureGraphics.MeasureString(time, font);
|
||||
maxTimeWidth = Math.Max(maxTimeWidth, timeSize.Width);
|
||||
lineHeight = Math.Max(lineHeight, timeSize.Height);
|
||||
}
|
||||
|
||||
SizeF titleSize = measureGraphics.MeasureString(title, font);
|
||||
maxTitleWidth = Math.Max(maxTitleWidth, titleSize.Width);
|
||||
lineHeight = Math.Max(lineHeight, titleSize.Height);
|
||||
}
|
||||
|
||||
float columnSpacing = measureGraphics.MeasureString(" ", font).Width;
|
||||
|
||||
float prefixWidth = 0;
|
||||
bool hasAnyPrefix = false;
|
||||
if (hasDayColumn)
|
||||
{
|
||||
prefixWidth += maxDayWidth;
|
||||
hasAnyPrefix = true;
|
||||
}
|
||||
if (hasDateColumn)
|
||||
{
|
||||
prefixWidth += (hasAnyPrefix ? columnSpacing : 0) + maxDateWidth;
|
||||
hasAnyPrefix = true;
|
||||
}
|
||||
if (hasTimeColumn)
|
||||
{
|
||||
prefixWidth += (hasAnyPrefix ? columnSpacing : 0) + maxTimeWidth;
|
||||
hasAnyPrefix = true;
|
||||
}
|
||||
|
||||
float totalWidth = (hasAnyPrefix ? prefixWidth + columnSpacing : 0) + maxTitleWidth;
|
||||
|
||||
int width = Math.Max(1, (int)Math.Ceiling(totalWidth));
|
||||
int height = Math.Max(1, (int)Math.Ceiling(lineHeight * rows.Count));
|
||||
|
||||
Bitmap bitmap = new Bitmap(width, height);
|
||||
bitmap.SetResolution(100, 100);
|
||||
|
||||
using Graphics graphics = Graphics.FromImage(bitmap);
|
||||
using SolidBrush brush = new SolidBrush(Application.Theme.PrimaryColor);
|
||||
graphics.TextRenderingHint = TextRenderingHint.AntiAlias;
|
||||
graphics.Clear(Color.Transparent);
|
||||
|
||||
float dayColumnX = 0;
|
||||
float dateColumnX = hasDayColumn ? dayColumnX + maxDayWidth + columnSpacing : dayColumnX;
|
||||
float timeColumnX = hasDateColumn ? dateColumnX + maxDateWidth + columnSpacing : dateColumnX;
|
||||
float titleColumnX = hasAnyPrefix ? timeColumnX + (hasTimeColumn ? maxTimeWidth : 0) + columnSpacing : 0;
|
||||
|
||||
for (int index = 0; index < rows.Count; index++)
|
||||
{
|
||||
(string day, string date, string time, string title, bool isMessage) = rows[index];
|
||||
float y = index * lineHeight;
|
||||
|
||||
if (isMessage)
|
||||
{
|
||||
graphics.DrawString(title, font, brush, 0, y);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hasDayColumn && !string.IsNullOrWhiteSpace(day))
|
||||
{
|
||||
graphics.DrawString(day, font, brush, dayColumnX, y);
|
||||
}
|
||||
|
||||
if (hasDateColumn && !string.IsNullOrWhiteSpace(date))
|
||||
{
|
||||
graphics.DrawString(date, font, brush, dateColumnX, y);
|
||||
}
|
||||
|
||||
if (hasTimeColumn && !string.IsNullOrWhiteSpace(time))
|
||||
{
|
||||
graphics.DrawString(time, font, brush, timeColumnX, y);
|
||||
}
|
||||
|
||||
graphics.DrawString(title, font, brush, titleColumnX, y);
|
||||
}
|
||||
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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}";
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
private static DesktopMagicSettings settings = new();
|
||||
|
||||
private bool isLoading = true;
|
||||
private string? pluginsSearchText;
|
||||
|
||||
public string Title =>
|
||||
#if DEBUG
|
||||
@@ -43,6 +44,16 @@ internal class MainWindowDataContext : INotifyPropertyChanged
|
||||
}
|
||||
}
|
||||
|
||||
public string? PluginsSearchText
|
||||
{
|
||||
get => pluginsSearchText;
|
||||
set
|
||||
{
|
||||
pluginsSearchText = value;
|
||||
OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAutoStartEnabled
|
||||
{
|
||||
get => StartupManager.IsAutoStartEnabled();
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
<RunPostBuildEvent>OnBuildSuccess</RunPostBuildEvent>
|
||||
<Authors>Stone_Red</Authors>
|
||||
<Version>1.3.1.0</Version>
|
||||
<Version>1.3.3.0</Version>
|
||||
<PackageProjectUrl>https://github.com/Stone-Red-Code/DesktopMagic</PackageProjectUrl>
|
||||
<RepositoryUrl>https://github.com/Stone-Red-Code/DesktopMagic</RepositoryUrl>
|
||||
<AssemblyVersion>1.3.1.0</AssemblyVersion>
|
||||
<FileVersion>1.3.1.0</FileVersion>
|
||||
<AssemblyVersion>1.3.3.0</AssemblyVersion>
|
||||
<FileVersion>1.3.3.0</FileVersion>
|
||||
<TargetFramework>net8.0-windows7.0</TargetFramework>
|
||||
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
|
||||
<Nullable>enable</Nullable>
|
||||
@@ -47,11 +47,13 @@
|
||||
<PackageReference Include="BusyIndicators" Version="2.1.3" />
|
||||
<PackageReference Include="CuteUtils" Version="1.0.0" />
|
||||
<PackageReference Include="Interop.IWshRuntimeLibrary" Version="1.0.1" />
|
||||
<PackageReference Include="Ical.Net" Version="4.3.1" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3650.58" />
|
||||
<PackageReference Include="Modio" Version="1.0.0" />
|
||||
<PackageReference Include="NAudio" Version="2.2.1" />
|
||||
<PackageReference Include="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>
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
WindowCornerPreference="Round"
|
||||
WindowBackdropType="Tabbed"
|
||||
Height="230"
|
||||
Width="300"
|
||||
Width="320"
|
||||
MinHeight="160"
|
||||
MinWidth="300"
|
||||
MinWidth="320"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
ExtendsContentIntoTitleBar="True">
|
||||
<Grid>
|
||||
@@ -48,7 +48,8 @@
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="3" 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="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>
|
||||
|
||||
@@ -10,6 +10,8 @@ public partial class CreatePluginDialog : Wpf.Ui.Controls.FluentWindow
|
||||
set => textBox.Text = value;
|
||||
}
|
||||
|
||||
public bool IsWebPlugin { get; private set; }
|
||||
|
||||
public CreatePluginDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -17,8 +19,15 @@ public partial class CreatePluginDialog : Wpf.Ui.Controls.FluentWindow
|
||||
Resources.MergedDictionaries.Add(App.LanguageDictionary);
|
||||
}
|
||||
|
||||
private void OkButton_Click(object sender, RoutedEventArgs e)
|
||||
private void DotNetButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
IsWebPlugin = false;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
private void WebPluginButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
IsWebPlugin = true;
|
||||
DialogResult = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -89,11 +89,35 @@ internal static partial class MultiColorConverter
|
||||
return System.Windows.Media.Color.FromArgb(color.A, color.R, color.G, color.B);
|
||||
}
|
||||
|
||||
public static System.Windows.Media.Color ConvertToMediaColor(string hex, System.Windows.Media.Color defaultColor)
|
||||
{
|
||||
if (TryConvertToMediaColor(hex, out System.Windows.Media.Color color))
|
||||
{
|
||||
return color;
|
||||
}
|
||||
else
|
||||
{
|
||||
return defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
public static System.Drawing.Color ConvertToSystemColor(System.Windows.Media.Color color)
|
||||
{
|
||||
return System.Drawing.Color.FromArgb(color.A, color.R, color.G, color.B);
|
||||
}
|
||||
|
||||
public static System.Drawing.Color ConvertToSystemColor(string hex, System.Drawing.Color defaultColor)
|
||||
{
|
||||
if (TryConvertToSystemColor(hex, out System.Drawing.Color color))
|
||||
{
|
||||
return color;
|
||||
}
|
||||
else
|
||||
{
|
||||
return defaultColor;
|
||||
}
|
||||
}
|
||||
|
||||
[GeneratedRegex("(?:[0-9a-fA-F]{8})")]
|
||||
private static partial Regex Hex8();
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Net.Http;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DesktopMagic.Helpers;
|
||||
|
||||
public sealed class GitHubLatestRelease
|
||||
{
|
||||
[JsonPropertyName("tag_name")]
|
||||
public string TagName { get; init; } = "Unknown";
|
||||
|
||||
[JsonPropertyName("name")]
|
||||
public string Name { get; init; } = "Unknown";
|
||||
|
||||
[JsonPropertyName("body")]
|
||||
public string Body { get; init; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("published_at")]
|
||||
public DateTimeOffset? PublishedAt { get; init; }
|
||||
|
||||
[JsonPropertyName("html_url")]
|
||||
public string HtmlUrl { get; init; } = "https://github.com/Stone-Red-Code/DesktopMagic/releases/latest";
|
||||
}
|
||||
|
||||
public static class GitHubReleaseService
|
||||
{
|
||||
private const string LatestReleaseApiUrl = "https://api.github.com/repos/Stone-Red-Code/DesktopMagic/releases/latest";
|
||||
private static readonly HttpClient _httpClient = CreateReleaseHttpClient();
|
||||
|
||||
private static HttpClient CreateReleaseHttpClient()
|
||||
{
|
||||
HttpClient client = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
client.DefaultRequestHeaders.Accept.ParseAdd("application/vnd.github+json");
|
||||
client.DefaultRequestHeaders.UserAgent.ParseAdd("DesktopMagic");
|
||||
return client;
|
||||
}
|
||||
|
||||
public static async Task<GitHubLatestRelease?> GetLatestReleaseInfoAsync()
|
||||
{
|
||||
using HttpResponseMessage response = await _httpClient.GetAsync(LatestReleaseApiUrl);
|
||||
response.EnsureSuccessStatusCode();
|
||||
await using var responseStream = await response.Content.ReadAsStreamAsync();
|
||||
return await JsonSerializer.DeserializeAsync<GitHubLatestRelease>(responseStream);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Ical.Net;
|
||||
using Ical.Net.CalendarComponents;
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DesktopMagic.Helpers;
|
||||
|
||||
public sealed class CalendarEventEntry
|
||||
{
|
||||
public string Title { get; init; } = "Untitled";
|
||||
public string? Location { get; init; }
|
||||
public DateTime StartLocal { get; init; }
|
||||
public DateTime EndLocal { get; init; }
|
||||
public bool IsAllDay { get; init; }
|
||||
}
|
||||
|
||||
public static class IcsCalendarHelper
|
||||
{
|
||||
private static readonly HttpClient httpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(12)
|
||||
};
|
||||
|
||||
static IcsCalendarHelper()
|
||||
{
|
||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd("DesktopMagic");
|
||||
httpClient.DefaultRequestHeaders.Accept.ParseAdd("text/calendar");
|
||||
}
|
||||
|
||||
public static async Task<List<CalendarEventEntry>> GetUpcomingEventsAsync(
|
||||
string calendarUrl,
|
||||
DateTime windowStartLocal,
|
||||
DateTime windowEndLocal,
|
||||
int maxEvents,
|
||||
bool includeAllDayEvents,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using HttpResponseMessage response = await httpClient.GetAsync(calendarUrl, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
string icsContent = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
|
||||
Calendar calendar = Calendar.Load(icsContent);
|
||||
DateTime windowStartUtc = windowStartLocal.ToUniversalTime();
|
||||
DateTime windowEndUtc = windowEndLocal.ToUniversalTime();
|
||||
|
||||
List<CalendarEventEntry> results = [];
|
||||
|
||||
foreach (var occurrence in calendar.GetOccurrences(windowStartUtc, windowEndUtc))
|
||||
{
|
||||
if (occurrence.Source is not CalendarEvent calendarEvent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
DateTime startLocal = occurrence.Period.StartTime.AsSystemLocal;
|
||||
DateTime endLocal = occurrence.Period.EndTime?.AsSystemLocal ?? startLocal.AddHours(1);
|
||||
bool isAllDay = !occurrence.Period.StartTime.HasTime;
|
||||
|
||||
if (!includeAllDayEvents && isAllDay)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (endLocal <= windowStartLocal)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
results.Add(new CalendarEventEntry
|
||||
{
|
||||
Title = string.IsNullOrWhiteSpace(calendarEvent.Summary) ? "Untitled" : calendarEvent.Summary,
|
||||
Location = string.IsNullOrWhiteSpace(calendarEvent.Location) ? null : calendarEvent.Location,
|
||||
StartLocal = startLocal,
|
||||
EndLocal = endLocal,
|
||||
IsAllDay = isAllDay
|
||||
});
|
||||
}
|
||||
|
||||
return results
|
||||
.OrderBy(eventEntry => eventEntry.StartLocal)
|
||||
.Take(Math.Max(1, maxEvents))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
@@ -100,7 +100,7 @@ internal class SettingElementGenerator(uint pluginId)
|
||||
Wpf.Ui.Controls.TextBox textBox = new()
|
||||
{
|
||||
Text = eTextBox.Value,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
TextWrapping = TextWrapping.NoWrap,
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
HorizontalAlignment = HorizontalAlignment.Stretch
|
||||
};
|
||||
|
||||
@@ -76,6 +76,21 @@ namespace DesktopMagic
|
||||
DependencyProperty.RegisterAttached("IsHooked", typeof(WindowLockHook), typeof(WindowPos),
|
||||
new PropertyMetadata(null));
|
||||
|
||||
public static void SetWindowLayer(Window window, string layer)
|
||||
{
|
||||
switch (layer)
|
||||
{
|
||||
case "Always on Bottom":
|
||||
window.Topmost = false;
|
||||
SendWpfWindowBack(window);
|
||||
SendWpfWindowBack(window);
|
||||
break;
|
||||
case "Always on Top":
|
||||
window.Topmost = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private class WindowLockHook
|
||||
{
|
||||
private readonly Window Window;
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
using DesktopMagic.DataContexts;
|
||||
using DesktopMagic.DataContexts;
|
||||
using DesktopMagic.Dialogs;
|
||||
using DesktopMagic.Helpers;
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Net.Http;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
using Wpf.Ui.Appearance;
|
||||
@@ -43,6 +49,7 @@ public partial class MainWindow : FluentWindow
|
||||
|
||||
_manager.IsLoaded = true;
|
||||
_mainWindowDataContext.IsLoading = false;
|
||||
await ShowLatestReleaseInfoAfterUpdateAsync();
|
||||
|
||||
App.Logger.LogInfo("Application loaded", source: "MainWindow");
|
||||
}
|
||||
@@ -190,4 +197,75 @@ public partial class MainWindow : FluentWindow
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,8 @@ public sealed class Manager
|
||||
{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
|
||||
@@ -232,6 +234,9 @@ public sealed class Manager
|
||||
{
|
||||
window.SetEditMode(editMode);
|
||||
}
|
||||
|
||||
Application.Current.MainWindow.Topmost = editMode;
|
||||
|
||||
EditModeChanged?.Invoke(editMode);
|
||||
SaveSettings();
|
||||
}
|
||||
|
||||
@@ -20,11 +20,19 @@
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<ui:Card Padding="5" Margin="0 0 0 5">
|
||||
<ui:ToggleSwitch x:Name="editCheckBox" Height="30" VerticalAlignment="Center" Margin="5 0 0 0" Content="{DynamicResource editLayout}" Click="EditCheckBox_Click" />
|
||||
<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 ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
|
||||
<ItemsControl x:Name="pluginsItemsControl" ItemsSource="{Binding Settings.CurrentLayout.Plugins}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ui:CardExpander Tag="{Binding}" Margin="0 0 0 5" Expanded="OptionsCardExpander_Expanded">
|
||||
|
||||
@@ -4,10 +4,12 @@ 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;
|
||||
|
||||
@@ -56,8 +58,8 @@ public partial class MainPage : Page
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
// Refresh the UI if needed
|
||||
_dataContext.Settings = _manager.Settings;
|
||||
ApplyPluginsFilter();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,6 +109,38 @@ public partial class MainPage : Page
|
||||
}
|
||||
}
|
||||
|
||||
#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)
|
||||
@@ -274,6 +308,7 @@ public partial class MainPage : Page
|
||||
if (control is not null)
|
||||
{
|
||||
control.MinWidth = 200;
|
||||
control.MaxWidth = 200;
|
||||
card.Content = control;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +486,15 @@ public partial class PluginManager : Page
|
||||
string pluginMetadataPath = Path.Combine(pluginPath, "metadata.json");
|
||||
await File.WriteAllTextAsync(pluginMetadataPath, JsonSerializer.Serialize(pluginMetadata));
|
||||
|
||||
if (inputDialog.IsWebPlugin)
|
||||
{
|
||||
await CreateNewWebPlugin(pluginPath, pluginName);
|
||||
changed = true;
|
||||
await InitializePluginManager();
|
||||
pluginManagerDataContext.IsLoading = false;
|
||||
return;
|
||||
}
|
||||
|
||||
App.Logger.LogInfo($"Creating .NET project at: {pluginProjectPath}", source: "PluginManager");
|
||||
string cmd = $"new classlib -n {pluginSafeName} -o {pluginProjectPath} -f net8.0 --target-framework-override net8.0-windows7";
|
||||
Process process = Process.Start("dotnet", cmd);
|
||||
@@ -608,6 +617,49 @@ public class {pluginSafeName}Plugin : Plugin
|
||||
pluginManagerDataContext.IsLoading = false;
|
||||
}
|
||||
|
||||
private async Task CreateNewWebPlugin(string pluginPath, string pluginName)
|
||||
{
|
||||
App.Logger.LogInfo($"Creating web plugin at: {pluginPath}", source: "PluginManager");
|
||||
|
||||
string mainHtml = $@"<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset=""utf-8"">
|
||||
<style>
|
||||
body {{ margin: 0; padding: 16px; font-family: var(--font-family); }}
|
||||
h1 {{ color: var(--primary-color); }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1 id=""greeting"">{pluginName}</h1>
|
||||
<script>
|
||||
function render() {{
|
||||
const s = window.desktopMagic.getSettings();
|
||||
document.getElementById('greeting').innerHTML = s.message;
|
||||
document.getElementById('greeting').style.color = s.color;
|
||||
document.getElementById('greeting').style.fontWeight = s.bold ? 'bold' : 'normal';
|
||||
}}
|
||||
|
||||
window.desktopMagic.onSettingChanged = (id, value) => render();
|
||||
render();
|
||||
</script>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
var settings = new List<Dictionary<string, object?>>
|
||||
{
|
||||
new() { ["id"] = "message", ["name"] = "Greeting Message", ["type"] = "textbox", ["default"] = "Hello, World!" },
|
||||
new() { ["id"] = "bold", ["name"] = "Bold Text", ["type"] = "checkbox", ["default"] = true },
|
||||
new() { ["id"] = "color", ["name"] = "Text Color", ["type"] = "colorpicker", ["default"] = "#FF5722" }
|
||||
};
|
||||
|
||||
string settingsJson = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(Path.Combine(pluginPath, "settings.json"), settingsJson);
|
||||
await File.WriteAllTextAsync(Path.Combine(pluginPath, "main.html"), mainHtml);
|
||||
|
||||
App.Logger.LogInfo($"Successfully created web plugin: {pluginName}", source: "PluginManager");
|
||||
}
|
||||
|
||||
private async void LogInButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (pluginManagerDataContext.IsAuthenticated)
|
||||
|
||||
@@ -322,9 +322,8 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
panel.Visibility = Visibility.Collapsed;
|
||||
imageBorder.BorderThickness = new Thickness(0);
|
||||
image.Margin = new Thickness(0);
|
||||
WindowPos.SendWpfWindowBack(this);
|
||||
WindowPos.SendWpfWindowBack(this);
|
||||
WindowPos.SetIsLocked(this, true);
|
||||
WindowPos.SetWindowLayer(this, pluginClassInstance?.windowLayer.Value ?? "Always on Bottom");
|
||||
WindowPos.SetIsLocked(window, true);
|
||||
tileBar.CaptionHeight = 0;
|
||||
ResizeMode = ResizeMode.NoResize;
|
||||
}
|
||||
@@ -558,12 +557,14 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
{
|
||||
SetHorizontalAlignment();
|
||||
SetVerticalAlignment();
|
||||
SetWindowLayer();
|
||||
SetRotation();
|
||||
SetThemeOverride();
|
||||
SetThemeOverrideItems();
|
||||
|
||||
pluginClassInstance.horizontalAlignment.OnValueChanged += SetHorizontalAlignment;
|
||||
pluginClassInstance.verticalAlignment.OnValueChanged += SetVerticalAlignment;
|
||||
pluginClassInstance.windowLayer.OnValueChanged += SetWindowLayer;
|
||||
pluginClassInstance.rotation.OnValueChanged += SetRotation;
|
||||
pluginClassInstance.themeOverride.OnValueChanged += SetThemeOverride;
|
||||
|
||||
@@ -599,6 +600,11 @@ public partial class PluginWindow : Window, IPluginWindow
|
||||
border.HorizontalAlignment = horizontalAlignment;
|
||||
}
|
||||
|
||||
void SetWindowLayer()
|
||||
{
|
||||
WindowPos.SetWindowLayer(this, pluginClassInstance.windowLayer.Value);
|
||||
}
|
||||
|
||||
void SetRotation()
|
||||
{
|
||||
image.LayoutTransform = new RotateTransform(pluginClassInstance.rotation.Value);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using DesktopMagic.Api.Settings;
|
||||
using DesktopMagic.Helpers;
|
||||
using DesktopMagic.Plugins;
|
||||
using DesktopMagic.Settings;
|
||||
@@ -5,7 +6,11 @@ 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;
|
||||
@@ -20,6 +25,9 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
|
||||
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; }
|
||||
@@ -71,6 +79,11 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
Height = settings.Size.Y;
|
||||
|
||||
PluginFolderPath = pluginFolderPath;
|
||||
|
||||
if (pluginMetadata.SupportsUnloading && !string.IsNullOrEmpty(pluginFolderPath))
|
||||
{
|
||||
InitializeHotReload();
|
||||
}
|
||||
}
|
||||
|
||||
public void Exit()
|
||||
@@ -193,6 +206,8 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
|
||||
try
|
||||
{
|
||||
LoadWebPluginSettings();
|
||||
|
||||
string userDataFolder = Path.Combine(Path.GetTempPath(), "DesktopMagic", "WebView2", PluginMetadata.Id.ToString());
|
||||
CoreWebView2Environment environment = await CoreWebView2Environment.CreateAsync(null, userDataFolder);
|
||||
await webView.EnsureCoreWebView2Async(environment);
|
||||
@@ -202,6 +217,8 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
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);
|
||||
|
||||
@@ -224,6 +241,15 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
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)
|
||||
@@ -249,8 +275,359 @@ public partial class WebPluginWindow : Window, IPluginWindow
|
||||
tileBar.CaptionHeight = ActualHeight - 10;
|
||||
}
|
||||
|
||||
private bool firstLoad = true;
|
||||
|
||||
private void WebView_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e)
|
||||
{
|
||||
webView.CoreWebView2.DOMContentLoaded += (_, _) => ThemeChanged();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
<system:String x:Key="cpuUsage">CPU Auslastung</system:String>
|
||||
<system:String x:Key="musicVisualizer">Musik Visualisierer</system:String>
|
||||
<system:String x:Key="weather">Wetter</system:String>
|
||||
<system:String x:Key="nextMeetingCountdown">Nächstes Meeting Countdown</system:String>
|
||||
<system:String x:Key="agenda">Agenda</system:String>
|
||||
<system:String x:Key="amplifier">Signalverstärkung:</system:String>
|
||||
<system:String x:Key="newLayout">Neues Layout</system:String>
|
||||
<system:String x:Key="deleteLayout">Layout Löschen</system:String>
|
||||
@@ -60,6 +62,11 @@
|
||||
<system:String x:Key="addedFormat">Erstellt: {0}</system:String>
|
||||
<system:String x:Key="updatedFormat">Aktualisiert: {0}</system:String>
|
||||
<system:String x:Key="versionFormat">Version: {0}</system:String>
|
||||
<system:String x:Key="releaseInfoTitle">DesktopMagic wurde aktualisiert!</system:String>
|
||||
<system:String x:Key="releaseInfoHeaderFormat">Neueste Version: {0}</system:String>
|
||||
<system:String x:Key="releaseInfoPublishedFormat">Veröffentlicht: {0}</system:String>
|
||||
<system:String x:Key="releaseInfoBodyLabel">Release Notes:</system:String>
|
||||
<system:String x:Key="openReleasePage">Release-Seite öffnen</system:String>
|
||||
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
|
||||
<system:String>Unten</system:String>
|
||||
<system:String>Mitte</system:String>
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
<system:String x:Key="cpuUsage">CPU Usage</system:String>
|
||||
<system:String x:Key="musicVisualizer">Music Visualizer</system:String>
|
||||
<system:String x:Key="weather">Weather</system:String>
|
||||
<system:String x:Key="nextMeetingCountdown">Next Meeting Countdown</system:String>
|
||||
<system:String x:Key="agenda">Agenda</system:String>
|
||||
<system:String x:Key="amplifier">Signal Amplification:</system:String>
|
||||
<system:String x:Key="newLayout">New Layout</system:String>
|
||||
<system:String x:Key="deleteLayout">Delete Layout</system:String>
|
||||
@@ -62,6 +64,11 @@
|
||||
<system:String x:Key="addedFormat">Added: {0}</system:String>
|
||||
<system:String x:Key="updatedFormat">Updated: {0}</system:String>
|
||||
<system:String x:Key="versionFormat">Version: {0}</system:String>
|
||||
<system:String x:Key="releaseInfoTitle">DesktopMagic has been updated!</system:String>
|
||||
<system:String x:Key="releaseInfoHeaderFormat">Latest release: {0}</system:String>
|
||||
<system:String x:Key="releaseInfoPublishedFormat">Published: {0}</system:String>
|
||||
<system:String x:Key="releaseInfoBodyLabel">Release notes:</system:String>
|
||||
<system:String x:Key="openReleasePage">Open release page</system:String>
|
||||
<col:ArrayList x:Key="musicVisualizerOptionsComboboxItems">
|
||||
<system:String>Bottom</system:String>
|
||||
<system:String>Middle</system:String>
|
||||
|
||||
@@ -64,6 +64,8 @@ public class DesktopMagicSettings : INotifyPropertyChanged
|
||||
|
||||
public string? ModIoAccessToken { get; set; }
|
||||
|
||||
public string? ReleaseInfoLastAppVersion { get; set; }
|
||||
|
||||
public DesktopMagicSettings()
|
||||
{
|
||||
themes.CollectionChanged += (s, e) => CurrentLayout.UpdateTheme();
|
||||
|
||||
@@ -23,10 +23,13 @@ public abstract class Plugin
|
||||
[Setting("desktopmagic-vertical-alignment", "Vertical Alignment", -998)]
|
||||
internal ComboBox verticalAlignment = new ComboBox("Center", "Top", "Bottom");
|
||||
|
||||
[Setting("rotation", "Rotation", -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("theme-override", "Theme Override", -996)]
|
||||
[Setting("desktopmagic-theme-override", "Theme Override", -995)]
|
||||
internal ComboBox themeOverride = new ComboBox("<None>");
|
||||
|
||||
private IPluginData application = null!;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<Identity
|
||||
Name="StoneRed.DesktopMagic"
|
||||
Publisher="CN=6D1FD759-1951-428A-9B03-964461C7DBF5"
|
||||
Version="1.3.1.0" />
|
||||
Version="1.3.3.0" />
|
||||
|
||||
<Properties>
|
||||
<DisplayName>Desktop Magic</DisplayName>
|
||||
|
||||
Reference in New Issue
Block a user