ChangelogHtml(AppCastItem item)
+ {
+ const string cssResourceName = "Coder.Desktop.App.Assets.changelog.css";
+ const string htmlTemplate = @"
+
+
+
+
+
+
+
+
+
+
+ {{CONTENT}}
+
+
+
+";
+
+ const string githubMarkdownCssToken = "{{GITHUB_MARKDOWN_CSS}}";
+ const string themeToken = "{{THEME}}";
+ const string contentToken = "{{CONTENT}}";
+
+ // We load the CSS from an embedded asset since it's large.
+ var css = "";
+ try
+ {
+ await using var stream = typeof(App).Assembly.GetManifestResourceStream(cssResourceName)
+ ?? throw new FileNotFoundException($"Embedded resource not found: {cssResourceName}");
+ using var reader = new StreamReader(stream);
+ css = await reader.ReadToEndAsync();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "failed to load changelog CSS theme from embedded asset, ignoring");
+ }
+
+ // We store the changelog in the description field, rather than using
+ // the release notes URL to avoid extra requests.
+ var innerHtml = item.Description;
+ if (string.IsNullOrWhiteSpace(innerHtml))
+ {
+ innerHtml = "No release notes available.
";
+ }
+
+ // The theme doesn't automatically update.
+ var currentTheme = Application.Current.RequestedTheme == ApplicationTheme.Dark ? "dark" : "light";
+ return htmlTemplate
+ .Replace(githubMarkdownCssToken, css)
+ .Replace(themeToken, currentTheme)
+ .Replace(contentToken, innerHtml);
+ }
+
+ public async Task Changelog_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is not WebView2 webView)
+ return;
+
+ // Start the engine.
+ await webView.EnsureCoreWebView2Async();
+
+ // Disable unwanted features.
+ var settings = webView.CoreWebView2.Settings;
+ settings.IsScriptEnabled = false; // disables JS
+ settings.AreHostObjectsAllowed = false; // disables interaction with app code
+#if !DEBUG
+ settings.AreDefaultContextMenusEnabled = false; // disables right-click
+ settings.AreDevToolsEnabled = false;
+#endif
+ settings.IsZoomControlEnabled = false;
+ settings.IsStatusBarEnabled = false;
+
+ // Hijack navigation to prevent links opening in the web view.
+ webView.CoreWebView2.NavigationStarting += (_, e) =>
+ {
+ // webView.NavigateToString uses data URIs, so allow those to work.
+ if (e.Uri.StartsWith("data:text/html", StringComparison.OrdinalIgnoreCase))
+ return;
+
+ // Prevent the web view from trying to navigate to it.
+ e.Cancel = true;
+
+ // Launch HTTP or HTTPS URLs in the default browser.
+ if (Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri) && uri is { Scheme: "http" or "https" })
+ Process.Start(new ProcessStartInfo(e.Uri) { UseShellExecute = true });
+ };
+ webView.CoreWebView2.NewWindowRequested += (_, e) =>
+ {
+ // Prevent new windows from being launched (e.g. target="_blank").
+ e.Handled = true;
+ // Launch HTTP or HTTPS URLs in the default browser.
+ if (Uri.TryCreate(e.Uri, UriKind.Absolute, out var uri) && uri is { Scheme: "http" or "https" })
+ Process.Start(new ProcessStartInfo(e.Uri) { UseShellExecute = true });
+ };
+
+ var html = await ChangelogHtml(CurrentItem);
+ webView.NavigateToString(html);
+ }
+
+ private void SendResponse(UpdateAvailableResult result)
+ {
+ Result = result;
+ UserResponded?.Invoke(this, new UpdateResponseEventArgs(result, CurrentItem));
+ }
+
+ public void SkipButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!SkipButtonVisible || MissingCriticalUpdate)
+ return;
+ SendResponse(UpdateAvailableResult.SkipUpdate);
+ }
+
+ public void RemindMeLaterButton_Click(object sender, RoutedEventArgs e)
+ {
+ if (!RemindMeLaterButtonVisible || MissingCriticalUpdate)
+ return;
+ SendResponse(UpdateAvailableResult.RemindMeLater);
+ }
+
+ public void InstallButton_Click(object sender, RoutedEventArgs e)
+ {
+ SendResponse(UpdateAvailableResult.InstallUpdate);
+ }
+}
diff --git a/App/Views/DirectoryPickerWindow.xaml b/App/Views/DirectoryPickerWindow.xaml
index 8a107cb..ce1623b 100644
--- a/App/Views/DirectoryPickerWindow.xaml
+++ b/App/Views/DirectoryPickerWindow.xaml
@@ -13,7 +13,7 @@
MinWidth="400" MinHeight="600">
-
+
diff --git a/App/Views/DirectoryPickerWindow.xaml.cs b/App/Views/DirectoryPickerWindow.xaml.cs
index 6ed5f43..d2eb320 100644
--- a/App/Views/DirectoryPickerWindow.xaml.cs
+++ b/App/Views/DirectoryPickerWindow.xaml.cs
@@ -1,6 +1,7 @@
using System;
using System.Runtime.InteropServices;
using Windows.Graphics;
+using Coder.Desktop.App.Utils;
using Coder.Desktop.App.ViewModels;
using Coder.Desktop.App.Views.Pages;
using Microsoft.UI.Windowing;
@@ -16,7 +17,7 @@ public sealed partial class DirectoryPickerWindow : WindowEx
public DirectoryPickerWindow(DirectoryPickerViewModel viewModel)
{
InitializeComponent();
- SystemBackdrop = new DesktopAcrylicBackdrop();
+ TitleBarIcon.SetTitlebarIcon(this);
viewModel.Initialize(this, DispatcherQueue);
RootFrame.Content = new DirectoryPickerMainPage(viewModel);
diff --git a/App/Views/FileSyncListWindow.xaml b/App/Views/FileSyncListWindow.xaml
index 070efd2..991d02a 100644
--- a/App/Views/FileSyncListWindow.xaml
+++ b/App/Views/FileSyncListWindow.xaml
@@ -13,7 +13,7 @@
MinWidth="1000" MinHeight="300">
-
+
diff --git a/App/Views/FileSyncListWindow.xaml.cs b/App/Views/FileSyncListWindow.xaml.cs
index 428363b..9d8510b 100644
--- a/App/Views/FileSyncListWindow.xaml.cs
+++ b/App/Views/FileSyncListWindow.xaml.cs
@@ -1,3 +1,4 @@
+using Coder.Desktop.App.Utils;
using Coder.Desktop.App.ViewModels;
using Coder.Desktop.App.Views.Pages;
using Microsoft.UI.Xaml.Media;
@@ -13,7 +14,7 @@ public FileSyncListWindow(FileSyncListViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
- SystemBackdrop = new DesktopAcrylicBackdrop();
+ TitleBarIcon.SetTitlebarIcon(this);
ViewModel.Initialize(this, DispatcherQueue);
RootFrame.Content = new FileSyncListMainPage(ViewModel);
diff --git a/App/Views/MessageWindow.xaml b/App/Views/MessageWindow.xaml
new file mode 100644
index 0000000..e38ee4f
--- /dev/null
+++ b/App/Views/MessageWindow.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/App/Views/MessageWindow.xaml.cs b/App/Views/MessageWindow.xaml.cs
new file mode 100644
index 0000000..00ed204
--- /dev/null
+++ b/App/Views/MessageWindow.xaml.cs
@@ -0,0 +1,32 @@
+using Coder.Desktop.App.Utils;
+using Microsoft.UI.Xaml;
+using WinUIEx;
+
+namespace Coder.Desktop.App.Views;
+
+public sealed partial class MessageWindow : WindowEx
+{
+ public string MessageTitle;
+ public string MessageContent;
+
+ public MessageWindow(string title, string content, string windowTitle = "Coder Desktop")
+ {
+ Title = windowTitle;
+ MessageTitle = title;
+ MessageContent = content;
+
+ InitializeComponent();
+ TitleBarIcon.SetTitlebarIcon(this);
+ this.CenterOnScreen();
+ AppWindow.Show();
+
+ // TODO: the window should resize to fit content and not be resizable
+ // by the user, probably possible with SizedFrame and a Page, but
+ // I didn't want to add a Page for this
+ }
+
+ public void CloseClicked(object? sender, RoutedEventArgs e)
+ {
+ Close();
+ }
+}
diff --git a/App/Views/Pages/DirectoryPickerMainPage.xaml b/App/Views/Pages/DirectoryPickerMainPage.xaml
index dd08c46..0fbbaea 100644
--- a/App/Views/Pages/DirectoryPickerMainPage.xaml
+++ b/App/Views/Pages/DirectoryPickerMainPage.xaml
@@ -9,8 +9,7 @@
xmlns:converters="using:Coder.Desktop.App.Converters"
xmlns:toolkit="using:CommunityToolkit.WinUI.Controls"
xmlns:viewmodels="using:Coder.Desktop.App.ViewModels"
- mc:Ignorable="d"
- Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
+ mc:Ignorable="d">
+ mc:Ignorable="d">