diff --git a/installer/DesktopMagic-Installer.exe b/installer/DesktopMagic-Installer.exe
index 8d439d7..8e0e20c 100644
Binary files a/installer/DesktopMagic-Installer.exe and b/installer/DesktopMagic-Installer.exe differ
diff --git a/src/DesktopMagic/App.xaml b/src/DesktopMagic/App.xaml
index 83dca8a..85b7129 100644
--- a/src/DesktopMagic/App.xaml
+++ b/src/DesktopMagic/App.xaml
@@ -1,5 +1,6 @@
-
+
+
@@ -22,4 +25,4 @@
-
\ No newline at end of file
+
diff --git a/src/DesktopMagic/BuiltInPlugins/AgendaPlugin.cs b/src/DesktopMagic/BuiltInPlugins/AgendaPlugin.cs
new file mode 100644
index 0000000..d2eef39
--- /dev/null
+++ b/src/DesktopMagic/BuiltInPlugins/AgendaPlugin.cs
@@ -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 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 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;
+ }
+}
diff --git a/src/DesktopMagic/BuiltInPlugins/NextMeetingCountdownPlugin.cs b/src/DesktopMagic/BuiltInPlugins/NextMeetingCountdownPlugin.cs
new file mode 100644
index 0000000..da85aee
--- /dev/null
+++ b/src/DesktopMagic/BuiltInPlugins/NextMeetingCountdownPlugin.cs
@@ -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 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 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 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}";
+ }
+}
diff --git a/src/DesktopMagic/DesktopMagic.csproj b/src/DesktopMagic/DesktopMagic.csproj
index 60c67c3..c46f32c 100644
--- a/src/DesktopMagic/DesktopMagic.csproj
+++ b/src/DesktopMagic/DesktopMagic.csproj
@@ -7,10 +7,10 @@
icon.ico
OnBuildSuccess
Stone_Red
- 1.3.1.0
+ 1.3.2.0
https://github.com/Stone-Red-Code/DesktopMagic
https://github.com/Stone-Red-Code/DesktopMagic
- 1.3.1.0
+ 1.3.2.0
1.3.1.0
net8.0-windows7.0
win-x64
@@ -47,11 +47,13 @@
+
+
@@ -75,4 +77,4 @@
PreserveNewest
-
\ No newline at end of file
+
diff --git a/src/DesktopMagic/Dialogs/ReleaseInfoDialog.xaml b/src/DesktopMagic/Dialogs/ReleaseInfoDialog.xaml
new file mode 100644
index 0000000..0ac8655
--- /dev/null
+++ b/src/DesktopMagic/Dialogs/ReleaseInfoDialog.xaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/DesktopMagic/Dialogs/ReleaseInfoDialog.xaml.cs b/src/DesktopMagic/Dialogs/ReleaseInfoDialog.xaml.cs
new file mode 100644
index 0000000..7eb7bfa
--- /dev/null
+++ b/src/DesktopMagic/Dialogs/ReleaseInfoDialog.xaml.cs
@@ -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;
+ }
+}
diff --git a/src/DesktopMagic/Helpers/GitHubReleaseService.cs b/src/DesktopMagic/Helpers/GitHubReleaseService.cs
new file mode 100644
index 0000000..6e74863
--- /dev/null
+++ b/src/DesktopMagic/Helpers/GitHubReleaseService.cs
@@ -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 GetLatestReleaseInfoAsync()
+ {
+ using HttpResponseMessage response = await _httpClient.GetAsync(LatestReleaseApiUrl);
+ response.EnsureSuccessStatusCode();
+ await using var responseStream = await response.Content.ReadAsStreamAsync();
+ return await JsonSerializer.DeserializeAsync(responseStream);
+ }
+}
diff --git a/src/DesktopMagic/Helpers/IcsCalendarHelper.cs b/src/DesktopMagic/Helpers/IcsCalendarHelper.cs
new file mode 100644
index 0000000..4bad589
--- /dev/null
+++ b/src/DesktopMagic/Helpers/IcsCalendarHelper.cs
@@ -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> 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 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();
+ }
+}
diff --git a/src/DesktopMagic/Helpers/SettingElementGenerator.cs b/src/DesktopMagic/Helpers/SettingElementGenerator.cs
index dd236d6..d141ac8 100644
--- a/src/DesktopMagic/Helpers/SettingElementGenerator.cs
+++ b/src/DesktopMagic/Helpers/SettingElementGenerator.cs
@@ -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
};
diff --git a/src/DesktopMagic/MainWindow.xaml.cs b/src/DesktopMagic/MainWindow.xaml.cs
index ea07060..4672d65 100644
--- a/src/DesktopMagic/MainWindow.xaml.cs
+++ b/src/DesktopMagic/MainWindow.xaml.cs
@@ -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();
}
-}
\ No newline at end of file
+
+ 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 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;
+ }
+}
diff --git a/src/DesktopMagic/Manager.cs b/src/DesktopMagic/Manager.cs
index 7a9d009..c7bac72 100644
--- a/src/DesktopMagic/Manager.cs
+++ b/src/DesktopMagic/Manager.cs
@@ -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();
}
@@ -370,4 +375,4 @@ public enum PluginType
{
DotNet,
Web
-}
\ No newline at end of file
+}
diff --git a/src/DesktopMagic/Pages/MainPage.xaml.cs b/src/DesktopMagic/Pages/MainPage.xaml.cs
index 65cc4d3..6b6f56a 100644
--- a/src/DesktopMagic/Pages/MainPage.xaml.cs
+++ b/src/DesktopMagic/Pages/MainPage.xaml.cs
@@ -274,6 +274,7 @@ public partial class MainPage : Page
if (control is not null)
{
control.MinWidth = 200;
+ control.MaxWidth = 200;
card.Content = control;
}
}
diff --git a/src/DesktopMagic/Resources/Strings/StringResources.de.xaml b/src/DesktopMagic/Resources/Strings/StringResources.de.xaml
index 06be6cd..96d9736 100644
--- a/src/DesktopMagic/Resources/Strings/StringResources.de.xaml
+++ b/src/DesktopMagic/Resources/Strings/StringResources.de.xaml
@@ -38,6 +38,8 @@
CPU Auslastung
Musik Visualisierer
Wetter
+ Nächstes Meeting Countdown
+ Agenda
Signalverstärkung:
Neues Layout
Layout Löschen
@@ -60,9 +62,14 @@
Erstellt: {0}
Aktualisiert: {0}
Version: {0}
+ DesktopMagic wurde aktualisiert!
+ Neueste Version: {0}
+ Veröffentlicht: {0}
+ Release Notes:
+ Release-Seite öffnen
Unten
Mitte
Oben
-
\ No newline at end of file
+
diff --git a/src/DesktopMagic/Resources/Strings/StringResources.en.xaml b/src/DesktopMagic/Resources/Strings/StringResources.en.xaml
index f1e0508..a048d3d 100644
--- a/src/DesktopMagic/Resources/Strings/StringResources.en.xaml
+++ b/src/DesktopMagic/Resources/Strings/StringResources.en.xaml
@@ -38,6 +38,8 @@
CPU Usage
Music Visualizer
Weather
+ Next Meeting Countdown
+ Agenda
Signal Amplification:
New Layout
Delete Layout
@@ -62,9 +64,14 @@
Added: {0}
Updated: {0}
Version: {0}
+ DesktopMagic has been updated!
+ Latest release: {0}
+ Published: {0}
+ Release notes:
+ Open release page
Bottom
Middle
Top
-
\ No newline at end of file
+
diff --git a/src/DesktopMagic/Settings/DesktopMagicSettings.cs b/src/DesktopMagic/Settings/DesktopMagicSettings.cs
index 9cbb10d..1cb61f6 100644
--- a/src/DesktopMagic/Settings/DesktopMagicSettings.cs
+++ b/src/DesktopMagic/Settings/DesktopMagicSettings.cs
@@ -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();
@@ -76,4 +78,4 @@ public class DesktopMagicSettings : INotifyPropertyChanged
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
-}
\ No newline at end of file
+}
diff --git a/src/MsixPackaging/Package.appxmanifest b/src/MsixPackaging/Package.appxmanifest
index 6b84a76..c90a086 100644
--- a/src/MsixPackaging/Package.appxmanifest
+++ b/src/MsixPackaging/Package.appxmanifest
@@ -9,7 +9,7 @@
+ Version="1.3.2.0" />
Desktop Magic