Add release notes window

This commit is contained in:
Jack251970 2025-06-06 23:08:32 +08:00
parent ba35f1269b
commit 1f4578c04c
3 changed files with 318 additions and 0 deletions

View file

@ -90,6 +90,7 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="InputSimulator" Version="1.0.4" />
<PackageReference Include="MdXaml" Version="1.27.0" />
<!-- Do not upgrade Microsoft.Extensions.DependencyInjection and Microsoft.Extensions.Hosting since we are .Net7.0 -->
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />

View file

@ -0,0 +1,168 @@
<Window
x:Class="Flow.Launcher.ReleaseNotesWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Flow.Launcher"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mdxam="clr-namespace:MdXaml;assembly=MdXaml"
xmlns:ui="http://schemas.modernwpf.com/2019"
xmlns:vm="clr-namespace:Flow.Launcher.ViewModel"
Title="{DynamicResource releaseNotes}"
Width="700"
Background="{DynamicResource PopuBGColor}"
Foreground="{DynamicResource PopupTextColor}"
Loaded="Window_Loaded"
ResizeMode="CanResize"
SizeToContent="Height"
StateChanged="Window_StateChanged"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<WindowChrome.WindowChrome>
<WindowChrome CaptionHeight="32" ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" />
</WindowChrome.WindowChrome>
<Window.InputBindings>
<KeyBinding Key="Escape" Command="Close" />
</Window.InputBindings>
<Window.CommandBindings>
<CommandBinding Command="Close" Executed="OnCloseExecuted" />
</Window.CommandBindings>
<Grid>
<Border Style="{StaticResource WindowMainPanelStyle}">
<Grid Background="{DynamicResource Color01B}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="32" />
<RowDefinition />
</Grid.RowDefinitions>
<!-- TitleBar and Control -->
<Image
Grid.Row="0"
Grid.Column="0"
Width="16"
Height="16"
Margin="10 4 4 4"
RenderOptions.BitmapScalingMode="HighQuality"
Source="/Images/app.png" />
<TextBlock
Grid.Row="0"
Grid.Column="1"
Margin="4 0 0 0"
VerticalAlignment="Center"
FontSize="12"
Foreground="{DynamicResource Color05B}"
Text="{DynamicResource releaseNotes}" />
<Button
Grid.Row="0"
Grid.Column="2"
Click="OnMinimizeButtonClick"
RenderOptions.EdgeMode="Aliased"
Style="{DynamicResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,15 H 28"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button
Name="MaximizeButton"
Grid.Row="0"
Grid.Column="3"
Click="OnMaximizeRestoreButtonClick"
Style="{StaticResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18.5,10.5 H 27.5 V 19.5 H 18.5 Z"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button
Name="RestoreButton"
Grid.Row="0"
Grid.Column="3"
Click="OnMaximizeRestoreButtonClick"
Style="{StaticResource TitleBarButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18.5,12.5 H 25.5 V 19.5 H 18.5 Z M 20.5,12.5 V 10.5 H 27.5 V 17.5 H 25.5"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<Button
Grid.Row="0"
Grid.Column="4"
Click="OnCloseButtonClick"
Style="{StaticResource TitleBarCloseButtonStyle}">
<Path
Width="46"
Height="32"
Data="M 18,11 27,20 M 18,20 27,11"
Stroke="{Binding Path=Foreground, RelativeSource={RelativeSource AncestorType={x:Type Button}}}"
StrokeThickness="1">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=IsActive, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type Window}}}" Value="False">
<Setter Property="Opacity" Value="0.5" />
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
</Button>
<mdxam:MarkdownScrollViewer
x:Name="MarkdownViewer"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="5"
MaxHeight="510"
Margin="15 0 20 0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ClickAction="SafetyDisplayWithRelativePath" />
</Grid>
</Border>
</Grid>
</Window>

View file

@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Infrastructure.Http;
namespace Flow.Launcher
{
public partial class ReleaseNotesWindow : Window
{
public ReleaseNotesWindow()
{
InitializeComponent();
}
#region Window Events
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "<Pending>")]
private async void Window_Loaded(object sender, RoutedEventArgs e)
{
RefreshMaximizeRestoreButton();
MarkdownViewer.Markdown = await GetReleaseNotesMarkdownAsync();
}
private void OnCloseExecuted(object sender, ExecutedRoutedEventArgs e)
{
Close();
}
#endregion
#region Window Custom TitleBar
private void OnMinimizeButtonClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
}
private void OnMaximizeRestoreButtonClick(object sender, RoutedEventArgs e)
{
WindowState = WindowState switch
{
WindowState.Maximized => WindowState.Normal,
_ => WindowState.Maximized
};
}
private void OnCloseButtonClick(object sender, RoutedEventArgs e)
{
Close();
}
private void RefreshMaximizeRestoreButton()
{
if (WindowState == WindowState.Maximized)
{
MaximizeButton.Visibility = Visibility.Hidden;
RestoreButton.Visibility = Visibility.Visible;
}
else
{
MaximizeButton.Visibility = Visibility.Visible;
RestoreButton.Visibility = Visibility.Hidden;
}
}
private void Window_StateChanged(object sender, EventArgs e)
{
RefreshMaximizeRestoreButton();
}
#endregion
#region Release Notes
private static async Task<string> GetReleaseNotesMarkdownAsync()
{
var releaseNotesJSON = await Http.GetStringAsync("https://api.github.com/repos/Flow-Launcher/Flow.Launcher/releases");
var releases = JsonSerializer.Deserialize<List<GitHubReleaseInfo>>(releaseNotesJSON);
// Get the latest releases
var latestReleases = releases.OrderByDescending(release => release.PublishedDate).Take(3);
// Build the release notes in Markdown format
var releaseNotesHtmlBuilder = new StringBuilder(string.Empty);
foreach (var release in latestReleases)
{
releaseNotesHtmlBuilder.AppendLine("# " + release.Name);
// Add unit for images: Replace <img src="..." width="500"> with <img src="..." width="500px">
var notes = ImageUnitRegex().Replace(release.ReleaseNotes, m =>
{
var prefix = m.Groups[1].Value;
var widthValue = m.Groups[2].Value;
var quote = m.Groups[3].Value;
var suffix = m.Groups[4].Value;
// Only replace if width is number like 500 without units like 500px
if (IsNumber(widthValue))
return $"{prefix}{widthValue}px{quote}{suffix}";
return m.Value;
});
releaseNotesHtmlBuilder.AppendLine(notes);
releaseNotesHtmlBuilder.AppendLine("&nbsp;");
}
return releaseNotesHtmlBuilder.ToString();
}
private static bool IsNumber(string input)
{
if (string.IsNullOrEmpty(input))
return false;
foreach (char c in input)
{
if (!char.IsDigit(c))
return false;
}
return true;
}
private sealed class GitHubReleaseInfo
{
[JsonPropertyName("published_at")]
public DateTimeOffset PublishedDate { get; set; }
[JsonPropertyName("name")]
public string Name { get; set; }
[JsonPropertyName("tag_name")]
public string TagName { get; set; }
[JsonPropertyName("body")]
public string ReleaseNotes { get; set; }
}
[GeneratedRegex("(<img\\s+[^>]*width\\s*=\\s*[\"']?)(\\d+)([\"']?)([^>]*>)", RegexOptions.IgnoreCase, "en-GB")]
private static partial Regex ImageUnitRegex();
#endregion
}
}