This commit is contained in:
Niels Laute
2025-09-24 16:07:48 +02:00
parent 718efa7732
commit b96411983b
10 changed files with 650 additions and 2 deletions

View File

@@ -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;
}
}
}

View 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; }
}
}

View File

@@ -26,6 +26,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" />
@@ -156,7 +158,16 @@
</None>
<None Update="Assets\Settings\Scripts\DisableModule.ps1">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None> <Page Update="SettingsXAML\Controls\TitleBar\TitleBar.xaml">
</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">
</Page>
<Page Update="SettingsXAML\Controls\KeyVisual\KeyCharPresenter.xaml">
<Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,49 @@
<?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"
HeadingForeground="{StaticResource TextFillColorPrimaryBrush}" />
<tkcontrols:MarkdownConfig x:Key="ReleaseNotesMarkdownConfig" Themes="{StaticResource ReleaseNotesMarkdownThemeConfig}" />
</Page.Resources>
<ScrollViewer>
<StackPanel Padding="24" Spacing="12">
<!-- Header image -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" CornerRadius="12">
<Image
x:Name="HeaderImage"
Height="180"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
Stretch="UniformToFill"
Visibility="Collapsed" />
</Border>
<tkcontrols:MarkdownTextBlock
x:Name="MarkdownBlock"
Config="{StaticResource ReleaseNotesMarkdownConfig}"
UseAutoLinks="True"
UseEmphasisExtras="True"
UseListExtras="True"
UsePipeTables="True"
UseTaskLists="True" />
</StackPanel>
</ScrollViewer>
</Page>

View File

@@ -0,0 +1,53 @@
// 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.Runtime.InteropServices.WindowsRuntime;
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 Windows.Foundation;
using Windows.Foundation.Collections;
namespace Microsoft.PowerToys.Settings.UI.OOBE.Views
{
public sealed partial class ReleaseNotePage : Page
{
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;
}
}
base.OnNavigatedTo(e);
}
}
}

View File

@@ -0,0 +1,40 @@
<?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="286" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ListView
x:Name="ReleasesList"
IsItemClickEnabled="False"
ItemsSource="{x:Bind ReleaseItems, Mode=OneWay}"
SelectionChanged="ReleasesList_SelectionChanged"
SelectionMode="Single">
<ListView.ItemTemplate>
<DataTemplate x:DataType="helpers:ReleaseNotesItem">
<StackPanel Padding="12" Spacing="4">
<TextBlock FontWeight="SemiBold" Text="{x:Bind Title}" />
<TextBlock
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind PublishedDate}" />
</StackPanel>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<Frame x:Name="ContentFrame" Grid.Column="1" />
</Grid>
</Page>

View File

@@ -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: ![alt](url "title")
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);
}
}
}
}

View 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>

View File

@@ -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);
}
}
}

View File

@@ -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)