mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-09 22:06:51 +01:00
Compare commits
21 Commits
releaseChe
...
niels9001/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec467f6924 | ||
|
|
ee60423d5c | ||
|
|
16f2be0da0 | ||
|
|
12cb25eaf7 | ||
|
|
b96411983b | ||
|
|
718efa7732 | ||
|
|
437ca174d4 | ||
|
|
c9642c3c68 | ||
|
|
3eea52d062 | ||
|
|
5bc1644882 | ||
|
|
06ea71b57d | ||
|
|
04c73f6ff1 | ||
|
|
039e3d924c | ||
|
|
280c3642be | ||
|
|
4806b145f0 | ||
|
|
84d492a92e | ||
|
|
1e5fde68a8 | ||
|
|
85d8fbdabd | ||
|
|
eba0b9c619 | ||
|
|
8f029db21e | ||
|
|
9081f98de6 |
@@ -22,7 +22,7 @@
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251002-build.2316" />
|
||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.251101-build.2372" />
|
||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||
@@ -73,7 +73,7 @@
|
||||
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
|
||||
<PackageVersion Include="NLog.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.0.0" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.1.0" />
|
||||
<PackageVersion Include="ReverseMarkdown" Version="4.7.1" />
|
||||
<PackageVersion Include="RtfPipe" Version="2.0.7677.4303" />
|
||||
<PackageVersion Include="ScipBe.Common.Office.OneNote" Version="3.0.1" />
|
||||
<PackageVersion Include="SharpCompress" Version="0.37.2" />
|
||||
@@ -125,4 +125,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,82 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Converters
|
||||
{
|
||||
public sealed partial class DateTimeOffsetToStringConverter : IValueConverter
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets default .NET date format string. Can be overridden in XAML via ConverterParameter.
|
||||
/// </summary>
|
||||
public string Format { get; set; } = "MMMM yyyy";
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var culture = GetCulture(language);
|
||||
var format = parameter as string ?? Format;
|
||||
|
||||
if (value is DateTimeOffset dto)
|
||||
{
|
||||
return dto.ToString(format, culture);
|
||||
}
|
||||
|
||||
if (value is DateTime dt)
|
||||
{
|
||||
return dt.ToString(format, culture);
|
||||
}
|
||||
|
||||
if (value is string s)
|
||||
{
|
||||
// Try to parse strings robustly using the culture; assume unspecified is universal to avoid local offset surprises
|
||||
if (DateTimeOffset.TryParse(s, culture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var parsedDto))
|
||||
{
|
||||
return parsedDto.ToString(format, culture);
|
||||
}
|
||||
|
||||
if (DateTime.TryParse(s, culture, DateTimeStyles.AssumeLocal, out var parsedDt))
|
||||
{
|
||||
return parsedDt.ToString(format, culture);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, string language)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private static CultureInfo GetCulture(string language)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(language))
|
||||
{
|
||||
return new CultureInfo(language);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore and fall back
|
||||
}
|
||||
|
||||
// Prefer UI culture for display
|
||||
return CultureInfo.CurrentUICulture;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/settings-ui/Settings.UI/Helpers/ReleaseNotesItem.cs
Normal file
25
src/settings-ui/Settings.UI/Helpers/ReleaseNotesItem.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.Helpers
|
||||
{
|
||||
public sealed class ReleaseNotesItem
|
||||
{
|
||||
public string Title { get; set; }
|
||||
|
||||
public string Markdown { get; set; }
|
||||
|
||||
public DateTimeOffset PublishedDate { get; set; }
|
||||
|
||||
public string VersionGroup { get; set; }
|
||||
|
||||
public string HeaderImageUri { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,8 @@
|
||||
<None Remove="SettingsXAML\Controls\Dashboard\ShortcutConflictControl.xaml" />
|
||||
<None Remove="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml" />
|
||||
<None Remove="SettingsXAML\Controls\TitleBar\TitleBar.xaml" />
|
||||
<None Remove="SettingsXAML\OOBE\Views\ReleaseNotesPage.xaml" />
|
||||
<None Remove="SettingsXAML\ScoobeWindow.xaml" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Page Remove="SettingsXAML\App.xaml" />
|
||||
@@ -65,6 +67,7 @@
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Extensions" />
|
||||
<PackageReference Include="CommunityToolkit.WinUI.Converters" />
|
||||
<PackageReference Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" />
|
||||
<PackageReference Include="ReverseMarkdown" />
|
||||
<PackageReference Include="System.Net.Http" />
|
||||
<PackageReference Include="System.Private.Uri" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" />
|
||||
@@ -158,6 +161,15 @@
|
||||
<None Update="Assets\Settings\Scripts\DisableModule.ps1">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<Page Update="SettingsXAML\OOBE\Views\ReleaseNotesPage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="SettingsXAML\Views\ReleaseNotePage.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="SettingsXAML\ScoobeWindow.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="SettingsXAML\Controls\TitleBar\TitleBar.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
H2Margin="0, 16, 0, 4"
|
||||
H3FontSize="16"
|
||||
H3FontWeight="SemiBold"
|
||||
H3Margin="0, 16, 0, 4" />
|
||||
H3Margin="0, 16, 0, 4"/>
|
||||
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
||||
</Page.Resources>
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ReleaseNotePage"
|
||||
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="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:tkcontrols="using:CommunityToolkit.WinUI.Controls"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<tkcontrols:MarkdownThemes
|
||||
x:Key="ReleaseNotesMarkdownThemeConfig"
|
||||
H1FontSize="22"
|
||||
H1FontWeight="SemiBold"
|
||||
H1Margin="0, 36, 0, 8"
|
||||
H2FontSize="16"
|
||||
H2FontWeight="SemiBold"
|
||||
H2Margin="0, 16, 0, 4"
|
||||
H3FontSize="16"
|
||||
H3FontWeight="SemiBold"
|
||||
H3Margin="0, 16, 0, 4"
|
||||
ImageMaxHeight="200"
|
||||
ImageMaxWidth="600"
|
||||
ImageStretch="Uniform" />
|
||||
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
|
||||
</Page.Resources>
|
||||
<ScrollViewer>
|
||||
<Grid Padding="24" RowSpacing="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- Header image -->
|
||||
<!--<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="12">
|
||||
<Image
|
||||
x:Name="HeaderImage"
|
||||
Height="180"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Center"
|
||||
Stretch="UniformToFill"
|
||||
Visibility="Collapsed" />
|
||||
</Border>-->
|
||||
<Border Width="1000">
|
||||
<tkcontrols:MarkdownTextBlock
|
||||
x:Name="BlogTextBlock"
|
||||
MaxWidth="1000"
|
||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||
UseAutoLinks="True"
|
||||
UseEmphasisExtras="True"
|
||||
UseListExtras="True"
|
||||
UsePipeTables="True"
|
||||
UseTaskLists="True" />
|
||||
|
||||
|
||||
</Border>
|
||||
|
||||
<tkcontrols:MarkdownTextBlock
|
||||
x:Name="MarkdownBlock"
|
||||
Grid.Row="1"
|
||||
Config="{StaticResource ReleaseNotesMarkdownConfig}"
|
||||
UseAutoLinks="True"
|
||||
UseEmphasisExtras="True"
|
||||
UseListExtras="True"
|
||||
UsePipeTables="True"
|
||||
UseTaskLists="True" />
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Page>
|
||||
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Data;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Media.Imaging;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using ReverseMarkdown;
|
||||
using Windows.Foundation;
|
||||
using Windows.Foundation.Collections;
|
||||
using Windows.System;
|
||||
using static System.Net.WebRequestMethods;
|
||||
using static System.Resources.ResXFileRef;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class ReleaseNotePage : Page
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new();
|
||||
|
||||
public ReleaseNotePage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnNavigatedTo(NavigationEventArgs e)
|
||||
{
|
||||
if (e.Parameter is ReleaseNotesItem item)
|
||||
{
|
||||
MarkdownBlock.Text = item.Markdown ?? string.Empty;
|
||||
|
||||
/* if (!string.IsNullOrWhiteSpace(item.HeaderImageUri) &&
|
||||
Uri.TryCreate(item.HeaderImageUri, UriKind.Absolute, out var uri))
|
||||
{
|
||||
HeaderImage.Source = new BitmapImage(uri);
|
||||
HeaderImage.Visibility = Visibility.Visible;
|
||||
}
|
||||
else
|
||||
{
|
||||
HeaderImage.Source = null;
|
||||
HeaderImage.Visibility = Visibility.Collapsed;
|
||||
} */
|
||||
|
||||
GetBlogData();
|
||||
}
|
||||
|
||||
base.OnNavigatedTo(e);
|
||||
}
|
||||
|
||||
private static string NormalizePreCodeToFences(string html)
|
||||
{
|
||||
// ```lang\ncode\n```
|
||||
var rx = new Regex(
|
||||
@"<pre[^>]*>\s*<code(?:(?:\s+class=""[^""]*language-([a-z0-9+\-]+)[^""]*"")|[^>]*)>([\s\S]*?)</code>\s*</pre>",
|
||||
RegexOptions.IgnoreCase);
|
||||
|
||||
string Repl(Match m)
|
||||
{
|
||||
var lang = m.Groups[1].Success ? m.Groups[1].Value : string.Empty;
|
||||
var code = System.Net.WebUtility.HtmlDecode(m.Groups[2].Value);
|
||||
return $"```{lang}\n{code.TrimEnd()}\n```\n\n";
|
||||
}
|
||||
|
||||
// Also handle <pre>…</pre> without inner <code>
|
||||
var rxPre = new Regex(@"<pre[^>]*>([\s\S]*?)</pre>", RegexOptions.IgnoreCase);
|
||||
|
||||
html = rx.Replace(html, Repl);
|
||||
html = rxPre.Replace(html, m =>
|
||||
{
|
||||
var txt = System.Net.WebUtility.HtmlDecode(
|
||||
Regex.Replace(m.Groups[1].Value, "<.*?>", string.Empty, RegexOptions.Singleline));
|
||||
return $"```\n{txt.TrimEnd()}\n```\n\n";
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
private async void GetBlogData()
|
||||
{
|
||||
try
|
||||
{
|
||||
var url = "https://devblogs.microsoft.com/commandline/powertoys-0-94-is-here-settings-search-shortcut-conflict-detection-and-more/".Trim();
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 1) Figure out the site base + slug from the URL
|
||||
// Example: https://devblogs.microsoft.com/commandline/<slug>/
|
||||
var uri = new Uri(url);
|
||||
var basePath = uri.AbsolutePath.Trim('/'); // "commandline/powertoys-0-94-..."
|
||||
var firstSlash = basePath.IndexOf('/');
|
||||
if (firstSlash < 0)
|
||||
{
|
||||
throw new InvalidOperationException("Unexpected DevBlogs URL.");
|
||||
}
|
||||
|
||||
var site = basePath[..firstSlash]; // "commandline"
|
||||
var slug = basePath[(firstSlash + 1)..].Trim('/'); // "powertoys-0-94-..."
|
||||
|
||||
// 2) Call WordPress REST API for the sub-site
|
||||
var api = $"https://devblogs.microsoft.com/{site}/wp-json/wp/v2/posts?slug={Uri.EscapeDataString(slug)}&_fields=title,content,link,date,slug,id";
|
||||
var json = await HttpClient.GetStringAsync(api);
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
if (root.GetArrayLength() == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Post not found. Check the URL/slug.");
|
||||
}
|
||||
|
||||
var post = root[0];
|
||||
var html = post.GetProperty("content").GetProperty("rendered").GetString() ?? string.Empty;
|
||||
|
||||
// 3) Make image/anchor URLs absolute where needed
|
||||
html = RewriteRelativeUrls(html, $"https://devblogs.microsoft.com/{site}");
|
||||
html = EnforceImageMaxWidth(html);
|
||||
|
||||
// 3.1) Normalize <pre><code class="language-xxx">…</code></pre> into fenced blocks
|
||||
html = NormalizePreCodeToFences(html);
|
||||
|
||||
// 4) HTML → Markdown
|
||||
var config = new Config
|
||||
{
|
||||
GithubFlavored = true,
|
||||
RemoveComments = true,
|
||||
SmartHrefHandling = true,
|
||||
};
|
||||
var converter = new ReverseMarkdown.Converter(config);
|
||||
|
||||
var markdown = converter.Convert(html);
|
||||
BlogTextBlock.Text = markdown;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
BlogTextBlock.Text = $"**Error:** {ex.Message}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string RewriteRelativeUrls(string html, string siteBase)
|
||||
{
|
||||
// Convert src/href that start with "/" or are site-relative to absolute
|
||||
#pragma warning disable CA1310 // Specify StringComparison for correctness
|
||||
#pragma warning disable CA1866 // Use char overload
|
||||
string ToAbs(string url) =>
|
||||
url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
|
||||
url.StartsWith("https://", StringComparison.OrdinalIgnoreCase) ||
|
||||
url.StartsWith("data:", StringComparison.OrdinalIgnoreCase)
|
||||
? url
|
||||
: (url.StartsWith("/") ? $"https://devblogs.microsoft.com{url}" :
|
||||
url.StartsWith("./") || url.StartsWith("../") ? new Uri(new Uri(siteBase + "/"), url).ToString()
|
||||
: new Uri(new Uri(siteBase + "/"), url).ToString());
|
||||
#pragma warning restore CA1866 // Use char overload
|
||||
#pragma warning restore CA1310 // Specify StringComparison for correctness
|
||||
|
||||
#pragma warning disable SYSLIB1045 // Convert to 'GeneratedRegexAttribute'.
|
||||
#pragma warning disable SA1117 // Parameters should be on same line or separate lines
|
||||
html = Regex.Replace(html, "(?<attr>(?:src|href))=(\"|')(?<url>[^\"']+)(\"|')",
|
||||
m => $"{m.Groups["attr"].Value}=\"{ToAbs(m.Groups["url"].Value)}\"",
|
||||
RegexOptions.IgnoreCase);
|
||||
#pragma warning restore SA1117 // Parameters should be on same line or separate lines
|
||||
#pragma warning restore SYSLIB1045 // Convert to 'GeneratedRegexAttribute'.
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
private static string EnforceImageMaxWidth(string html, int maxWidth = 600)
|
||||
{
|
||||
return Regex.Replace(
|
||||
html,
|
||||
@"<img([^>]*?)>",
|
||||
m =>
|
||||
{
|
||||
var tag = m.Value;
|
||||
|
||||
// Skip if a style already contains max-width
|
||||
if (Regex.IsMatch(tag, @"max-width\s*:\s*\d+", RegexOptions.IgnoreCase))
|
||||
{
|
||||
return tag;
|
||||
}
|
||||
|
||||
// Inject style or append to existing one
|
||||
if (Regex.IsMatch(tag, @"style\s*=", RegexOptions.IgnoreCase))
|
||||
{
|
||||
return Regex.Replace(
|
||||
tag,
|
||||
@"style\s*=\s*(['""])(.*?)\1",
|
||||
m2 => $"style={m2.Groups[1].Value}{m2.Groups[2].Value}; max-width:{maxWidth}px;{m2.Groups[1].Value}",
|
||||
RegexOptions.IgnoreCase);
|
||||
}
|
||||
else
|
||||
{
|
||||
return tag.Insert(tag.Length - 1, $" style=\"max-width:{maxWidth}px; height:auto;\"");
|
||||
}
|
||||
},
|
||||
RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
||||
}
|
||||
|
||||
private async void MarkdownView_OnLinkClicked(object sender, CommunityToolkit.WinUI.Controls.LinkClickedEventArgs e)
|
||||
{
|
||||
// Open links externally
|
||||
await Launcher.LaunchUriAsync(e.Uri);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<Page
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.OOBE.Views.ReleaseNotesPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:Microsoft.PowerToys.Settings.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
<Page.Resources>
|
||||
<converters:DateTimeOffsetToStringConverter x:Key="DateTimeOffsetToStringConverter" />
|
||||
</Page.Resources>
|
||||
<Grid Loaded="Grid_Loaded">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="208" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<ListView
|
||||
x:Name="ReleasesList"
|
||||
IsItemClickEnabled="False"
|
||||
ItemContainerStyle="{StaticResource DefaultListViewItemStyle}"
|
||||
ItemsSource="{x:Bind ReleaseItems, Mode=OneWay}"
|
||||
SelectionChanged="ReleasesList_SelectionChanged"
|
||||
SelectionMode="Single">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="helpers:ReleaseNotesItem">
|
||||
<StackPanel Padding="12" Spacing="0">
|
||||
<TextBlock>
|
||||
<Run Text="Version" />
|
||||
<Run FontWeight="SemiBold" Text="{x:Bind Title}" />
|
||||
</TextBlock>
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind PublishedDate, Converter={StaticResource DateTimeOffsetToStringConverter}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
</ListView>
|
||||
<Grid
|
||||
Grid.Column="1"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8,0,0,0">
|
||||
<Frame x:Name="ContentFrame" />
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
</Page>
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
|
||||
{
|
||||
public sealed partial class ReleaseNotesPage : Page
|
||||
{
|
||||
public ReleaseNotesPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
// Your original regex constants
|
||||
private const string RemoveInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+## Highlights";
|
||||
private const string RemoveHotFixInstallerHashesRegex = @"(\r\n)+## Installer Hashes(\r\n.*)+$";
|
||||
private const RegexOptions RemoveInstallerHashesRegexOptions =
|
||||
RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.CultureInvariant;
|
||||
|
||||
// Image extraction regexes (Markdown image + HTML <img>)
|
||||
private static readonly Regex MdImageRegex =
|
||||
new(
|
||||
@"!\[(?:[^\]]*)\]\((?<url>[^)\s]+)(?:\s+""[^""]*"")?\)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
private static readonly Regex HtmlImageRegex =
|
||||
new(
|
||||
@"<img[^>]*\s+src\s*=\s*[""'](?<url>[^""']+)[""'][^>]*>",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
// PR URL normalization:
|
||||
// 1) Markdown links whose *text* is the full PR URL -> make text "#12345"
|
||||
private static readonly Regex MdLinkWithPrUrlTextRegex =
|
||||
new(
|
||||
@"\[(?<url>https?://github\.com/microsoft/PowerToys/pull/(?<id>\d+))\]\(\k<url>\)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
// 2) Bare PR URLs -> turn into "[#12345](url)"
|
||||
private static readonly Regex BarePrUrlRegex =
|
||||
new(
|
||||
@"(?<!\()(?<url>https?://github\.com/microsoft/PowerToys/pull/(?<id>\d+))(?!\))",
|
||||
RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
public ObservableCollection<ReleaseNotesItem> ReleaseItems { get; } = new();
|
||||
|
||||
// Fetch, group (by Major.Minor), clean, extract first image, and build items
|
||||
private async Task<IList<ReleaseNotesItem>> GetGroupedReleaseNotesAsync()
|
||||
{
|
||||
// Fetch GitHub releases using system proxy & user-agent
|
||||
using var proxyClientHandler = new HttpClientHandler
|
||||
{
|
||||
DefaultProxyCredentials = CredentialCache.DefaultCredentials,
|
||||
Proxy = WebRequest.GetSystemWebProxy(),
|
||||
PreAuthenticate = true,
|
||||
};
|
||||
|
||||
using var client = new HttpClient(proxyClientHandler);
|
||||
client.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "PowerToys");
|
||||
|
||||
string json = await client.GetStringAsync("https://api.github.com/repos/microsoft/PowerToys/releases");
|
||||
|
||||
// NOTE: PowerToysReleaseInfo + SourceGenerationContextContext are assumed to exist in your project
|
||||
IList<PowerToysReleaseInfo> releases =
|
||||
JsonSerializer.Deserialize<IList<PowerToysReleaseInfo>>(
|
||||
json, SourceGenerationContextContext.Default.IListPowerToysReleaseInfo)!;
|
||||
|
||||
// Prepare hash-removal regexes
|
||||
var removeHashRegex = new Regex(RemoveInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
var removeHotfixHashRegex = new Regex(RemoveHotFixInstallerHashesRegex, RemoveInstallerHashesRegexOptions);
|
||||
|
||||
// Parse versions; keep ones that contain x.y.z (handles "v0.93.2" etc.)
|
||||
var parsed = releases
|
||||
.Select(r => new
|
||||
{
|
||||
Release = r,
|
||||
Version = TryParseSemVer(r.TagName ?? r.Name, out var v) ? v : null,
|
||||
})
|
||||
.Where(x => x.Version is not null)
|
||||
.ToList();
|
||||
|
||||
// Group by Major.Minor (e.g., "0.93"), order groups by newest published date
|
||||
var groups = parsed
|
||||
.GroupBy(x => $"{x.Version!.Major}.{x.Version!.Minor}")
|
||||
.OrderByDescending(g => g.Max(x => x.Release.PublishedDate))
|
||||
.ToList();
|
||||
|
||||
var items = new List<ReleaseNotesItem>();
|
||||
|
||||
foreach (var g in groups)
|
||||
{
|
||||
// Order subreleases by version (patch desc), then date desc
|
||||
var ordered = g.OrderByDescending(x => x.Version)
|
||||
.ThenByDescending(x => x.Release.PublishedDate)
|
||||
.ToList();
|
||||
|
||||
// Title is the highest patch tag (e.g., "0.93.2"), trimmed of any leading 'v'
|
||||
var top = ordered.First();
|
||||
var title = TrimLeadingV(top.Release.TagName ?? top.Release.Name);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
int counter = 0;
|
||||
string headerImage = null;
|
||||
|
||||
for (int i = 0; i < ordered.Count; i++)
|
||||
{
|
||||
var r = ordered[i].Release;
|
||||
|
||||
// Clean installer hash sections
|
||||
var cleaned = removeHashRegex.Replace(r.ReleaseNotes ?? string.Empty, "\r\n### Highlights");
|
||||
cleaned = cleaned.Replace("[github-current-release-work]", $"[github-current-release-work{++counter}]");
|
||||
cleaned = removeHotfixHashRegex.Replace(cleaned, string.Empty);
|
||||
|
||||
// Capture & remove FIRST image across the whole group (only once)
|
||||
if (headerImage is null)
|
||||
{
|
||||
var (withoutFirstImage, foundUrl) = RemoveFirstImageAndGetUrl(cleaned);
|
||||
if (!string.IsNullOrWhiteSpace(foundUrl))
|
||||
{
|
||||
headerImage = foundUrl;
|
||||
cleaned = withoutFirstImage;
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize PR links to show "#12345" like GitHub
|
||||
cleaned = NormalizeGitHubPrLinks(cleaned);
|
||||
|
||||
if (i > 0)
|
||||
{
|
||||
// Horizontal rule between subreleases within the same group
|
||||
sb.AppendLine("\r\n---\r\n");
|
||||
}
|
||||
|
||||
// Keep a per-subrelease header for context (optional)
|
||||
var header = $"# {TrimLeadingV(r.TagName ?? r.Name)}";
|
||||
sb.AppendLine(header);
|
||||
sb.AppendLine(cleaned);
|
||||
}
|
||||
|
||||
items.Add(new ReleaseNotesItem
|
||||
{
|
||||
Title = title,
|
||||
VersionGroup = g.Key,
|
||||
PublishedDate = ordered.Max(x => x.Release.PublishedDate),
|
||||
Markdown = sb.ToString(),
|
||||
HeaderImageUri = headerImage,
|
||||
});
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
// Turn "https://github.com/microsoft/PowerToys/pull/41853" into "[#41853](...)".
|
||||
// Also, if the markdown link text equals the full URL, rewrite it to "#41853".
|
||||
private static string NormalizeGitHubPrLinks(string markdown)
|
||||
{
|
||||
if (string.IsNullOrEmpty(markdown))
|
||||
{
|
||||
return markdown;
|
||||
}
|
||||
|
||||
// Case 1: [https://github.com/.../pull/12345](https://github.com/.../pull/12345)
|
||||
markdown = MdLinkWithPrUrlTextRegex.Replace(
|
||||
markdown,
|
||||
m =>
|
||||
{
|
||||
var id = m.Groups["id"].Value;
|
||||
var url = m.Groups["url"].Value;
|
||||
return $"[#{id}]({url})";
|
||||
});
|
||||
|
||||
// Case 2: bare https://github.com/.../pull/12345 (not already inside link markup)
|
||||
markdown = BarePrUrlRegex.Replace(
|
||||
markdown,
|
||||
m =>
|
||||
{
|
||||
var id = m.Groups["id"].Value;
|
||||
var url = m.Groups["url"].Value;
|
||||
return $"[#{id}]({url})";
|
||||
});
|
||||
|
||||
return markdown;
|
||||
}
|
||||
|
||||
// Extract and remove the first image (Markdown or HTML) from a markdown string
|
||||
private static (string Cleaned, string Url) RemoveFirstImageAndGetUrl(string markdown)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(markdown))
|
||||
{
|
||||
return (markdown, null);
|
||||
}
|
||||
|
||||
// Markdown image first: 
|
||||
var m = MdImageRegex.Match(markdown);
|
||||
if (m.Success)
|
||||
{
|
||||
var url = m.Groups["url"].Value.Trim();
|
||||
var cleaned = MdImageRegex.Replace(markdown, string.Empty, 1);
|
||||
cleaned = CollapseExtraBlankLines(cleaned);
|
||||
return (cleaned, url);
|
||||
}
|
||||
|
||||
// Fallback: HTML <img src="...">
|
||||
m = HtmlImageRegex.Match(markdown);
|
||||
if (m.Success)
|
||||
{
|
||||
var url = m.Groups["url"].Value.Trim();
|
||||
var cleaned = HtmlImageRegex.Replace(markdown, string.Empty, 1);
|
||||
cleaned = CollapseExtraBlankLines(cleaned);
|
||||
return (cleaned, url);
|
||||
}
|
||||
|
||||
return (markdown, null);
|
||||
}
|
||||
|
||||
private static string CollapseExtraBlankLines(string s)
|
||||
{
|
||||
s = s.Trim();
|
||||
s = Regex.Replace(s, @"(\r?\n){3,}", "\r\n\r\n");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Try to parse the first x.y.z version found in a string (supports leading 'v')
|
||||
private static bool TryParseSemVer(string s, out Version v)
|
||||
{
|
||||
v = null;
|
||||
if (string.IsNullOrWhiteSpace(s))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var m = Regex.Match(s, @"(?<!\d)(\d+)\.(\d+)\.(\d+)");
|
||||
if (!m.Success)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (int.TryParse(m.Groups[1].Value, out var major) &&
|
||||
int.TryParse(m.Groups[2].Value, out var minor) &&
|
||||
int.TryParse(m.Groups[3].Value, out var patch))
|
||||
{
|
||||
v = new Version(major, minor, patch);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string TrimLeadingV(string s) =>
|
||||
string.IsNullOrEmpty(s) ? s : (s.StartsWith("v", StringComparison.OrdinalIgnoreCase) ? s[1..] : s);
|
||||
|
||||
private async void Grid_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (ReleaseItems.Count == 0)
|
||||
{
|
||||
var items = await GetGroupedReleaseNotesAsync();
|
||||
foreach (var item in items)
|
||||
{
|
||||
ReleaseItems.Add(item);
|
||||
}
|
||||
|
||||
if (ReleaseItems.Count > 0 && ReleasesList is not null)
|
||||
{
|
||||
ReleasesList.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleasesList_SelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (ReleasesList.SelectedItem is ReleaseNotesItem item)
|
||||
{
|
||||
ContentFrame.Navigate(typeof(ReleaseNotePage), item);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
38
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
Normal file
38
src/settings-ui/Settings.UI/SettingsXAML/ScoobeWindow.xaml
Normal file
@@ -0,0 +1,38 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<winuiex:WindowEx
|
||||
x:Class="Microsoft.PowerToys.Settings.UI.SettingsXAML.ScoobeWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:Microsoft.PowerToys.Settings.UI.Converters"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:Microsoft.PowerToys.Settings.UI.Helpers"
|
||||
xmlns:local="using:Microsoft.PowerToys.Settings.UI.SettingsXAML"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:views="using:Microsoft.PowerToys.Settings.UI.OOBE.Views"
|
||||
xmlns:winuiex="using:WinUIEx"
|
||||
Title="ScoobeWindow"
|
||||
Height="600"
|
||||
MaxWidth="1280"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Window.SystemBackdrop>
|
||||
<MicaBackdrop />
|
||||
</Window.SystemBackdrop>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TitleBar x:Name="titleBar">
|
||||
<!-- This is a workaround for https://github.com/microsoft/microsoft-ui-xaml/issues/10374, once fixed we should just be using IconSource -->
|
||||
<TitleBar.LeftHeader>
|
||||
<ImageIcon
|
||||
Height="16"
|
||||
Margin="16,0,0,0"
|
||||
Source="/Assets/Settings/icon.ico" />
|
||||
</TitleBar.LeftHeader>
|
||||
</TitleBar>
|
||||
<views:ReleaseNotesPage Grid.Row="1" />
|
||||
</Grid>
|
||||
</winuiex:WindowEx>
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.WinUI.Controls;
|
||||
using global::PowerToys.GPOWrapper;
|
||||
using ManagedCommon;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library.HotkeyConflicts;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.Enums;
|
||||
using Microsoft.PowerToys.Settings.UI.OOBE.ViewModel;
|
||||
using Microsoft.PowerToys.Settings.UI.SerializationContext;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.Views;
|
||||
using Microsoft.PowerToys.Telemetry;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using WinUIEx;
|
||||
|
||||
namespace Microsoft.PowerToys.Settings.UI.SettingsXAML
|
||||
{
|
||||
public sealed partial class ScoobeWindow : WindowEx
|
||||
{
|
||||
public ScoobeWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
this.ExtendsContentIntoTitleBar = true;
|
||||
SetTitleBar(titleBar);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ using Microsoft.PowerToys.Settings.UI.Controls;
|
||||
using Microsoft.PowerToys.Settings.UI.Helpers;
|
||||
using Microsoft.PowerToys.Settings.UI.Library;
|
||||
using Microsoft.PowerToys.Settings.UI.Services;
|
||||
using Microsoft.PowerToys.Settings.UI.SettingsXAML;
|
||||
using Microsoft.PowerToys.Settings.UI.ViewModels;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -373,7 +374,9 @@ namespace Microsoft.PowerToys.Settings.UI.Views
|
||||
|
||||
private void WhatIsNewItem_Tapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
OpenWhatIsNewWindowCallback();
|
||||
// OpenWhatIsNewWindowCallback();
|
||||
ScoobeWindow window = new ScoobeWindow();
|
||||
window.Activate();
|
||||
}
|
||||
|
||||
private void NavigationView_SelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
|
||||
|
||||
Reference in New Issue
Block a user