mirror of
https://github.com/claunia/marechai.git
synced 2026-07-08 17:57:08 +00:00
Fix markdown rendering in Marechai.App via native XAML renderer
MarkdownTextBlock crashed with a NullReferenceException in ThemeListener under Uno, and the WebView2-based fallback used by description pages doesn't composite on Linux desktop. Render markdown directly to XAML elements (TextBlock/Span/Hyperlink/Image trees) via Markdig's AST instead, shared by MessageMarkdownView and all entity description pages, with no embedded browser dependency on any platform.
This commit is contained in:
@@ -61,12 +61,10 @@
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2"/>
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.2"/>
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls" Version="7.1.205"/>
|
||||
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls.Markdown" Version="7.1.205"/>
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
<PackageReference Include="Svg.Skia"/>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt"/>
|
||||
<PackageReference Include="QRCoder"/>
|
||||
<PackageReference Include="Markdig"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Presentation\Views\Shell.xaml.cs">
|
||||
@@ -102,12 +103,10 @@
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls"/>
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls.Markdown"/>
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls"/>
|
||||
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls.Markdown"/>
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ public partial class CompanyDetailViewModel : ObservableObject, IRegionAware
|
||||
private CompanyDto? _soldToCompany;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _descriptionHtml = string.Empty;
|
||||
private string _descriptionMarkdown = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<CompanyDetailItem> _people = [];
|
||||
@@ -194,7 +194,7 @@ public partial class CompanyDetailViewModel : ObservableObject, IRegionAware
|
||||
/// <summary>
|
||||
/// Gets whether a description is available
|
||||
/// </summary>
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionHtml);
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionMarkdown);
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether company has multiple logos
|
||||
@@ -235,7 +235,7 @@ public partial class CompanyDetailViewModel : ObservableObject, IRegionAware
|
||||
OnPropertyChanged(nameof(HasLogoContent));
|
||||
}
|
||||
|
||||
partial void OnDescriptionHtmlChanged(string value)
|
||||
partial void OnDescriptionMarkdownChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasDescription));
|
||||
}
|
||||
@@ -830,7 +830,7 @@ public partial class CompanyDetailViewModel : ObservableObject, IRegionAware
|
||||
string langCode = GetIso639CodeFromCulture();
|
||||
CompanyDescriptionDto? desc = await _companyDetailService.GetDescriptionAsync(CompanyId, langCode);
|
||||
|
||||
DescriptionHtml = desc?.Html ?? desc?.Markdown ?? string.Empty;
|
||||
DescriptionMarkdown = desc?.Markdown ?? string.Empty;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -77,7 +77,7 @@ public partial class GpuDetailViewModel : ObservableObject, IRegionAware
|
||||
private bool _hasSmartphones;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _descriptionHtml = string.Empty;
|
||||
private string _descriptionMarkdown = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private Visibility _showDescription = Visibility.Collapsed;
|
||||
@@ -97,9 +97,9 @@ public partial class GpuDetailViewModel : ObservableObject, IRegionAware
|
||||
/// <summary>
|
||||
/// Gets whether a description is available
|
||||
/// </summary>
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionHtml);
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionMarkdown);
|
||||
|
||||
partial void OnDescriptionHtmlChanged(string value)
|
||||
partial void OnDescriptionMarkdownChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasDescription));
|
||||
}
|
||||
@@ -330,7 +330,7 @@ public partial class GpuDetailViewModel : ObservableObject, IRegionAware
|
||||
string langCode = GetIso639CodeFromCulture();
|
||||
GpuDescriptionDto? desc = await _gpusService.GetDescriptionAsync(GpuId, langCode);
|
||||
|
||||
DescriptionHtml = desc?.Html ?? desc?.Markdown ?? string.Empty;
|
||||
DescriptionMarkdown = desc?.Markdown ?? string.Empty;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -62,7 +62,7 @@ public partial class MachineViewViewModel : ObservableObject, IRegionAware
|
||||
private string _companyName = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _descriptionHtml = string.Empty;
|
||||
private string _descriptionMarkdown = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _errorMessage = string.Empty;
|
||||
@@ -145,9 +145,9 @@ public partial class MachineViewViewModel : ObservableObject, IRegionAware
|
||||
/// <summary>
|
||||
/// Gets whether a description is available
|
||||
/// </summary>
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionHtml);
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionMarkdown);
|
||||
|
||||
partial void OnDescriptionHtmlChanged(string value)
|
||||
partial void OnDescriptionMarkdownChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasDescription));
|
||||
}
|
||||
@@ -662,7 +662,7 @@ public partial class MachineViewViewModel : ObservableObject, IRegionAware
|
||||
string langCode = GetIso639CodeFromCulture();
|
||||
MachineDescriptionDto? desc = await _computersService.GetDescriptionAsync(machineId, langCode);
|
||||
|
||||
DescriptionHtml = desc?.Html ?? desc?.Markdown ?? string.Empty;
|
||||
DescriptionMarkdown = desc?.Markdown ?? string.Empty;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -68,7 +68,7 @@ public partial class ProcessorDetailViewModel : ObservableObject, IRegionAware
|
||||
private bool _hasSmartphones;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _descriptionHtml = string.Empty;
|
||||
private string _descriptionMarkdown = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private Visibility _showDescription = Visibility.Collapsed;
|
||||
@@ -76,9 +76,9 @@ public partial class ProcessorDetailViewModel : ObservableObject, IRegionAware
|
||||
/// <summary>
|
||||
/// Gets whether a description is available
|
||||
/// </summary>
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionHtml);
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionMarkdown);
|
||||
|
||||
partial void OnDescriptionHtmlChanged(string value)
|
||||
partial void OnDescriptionMarkdownChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasDescription));
|
||||
}
|
||||
@@ -270,7 +270,7 @@ public partial class ProcessorDetailViewModel : ObservableObject, IRegionAware
|
||||
string langCode = GetIso639CodeFromCulture();
|
||||
ProcessorDescriptionDto? desc = await _processorsService.GetDescriptionAsync(ProcessorId, langCode);
|
||||
|
||||
DescriptionHtml = desc?.Html ?? desc?.Markdown ?? string.Empty;
|
||||
DescriptionMarkdown = desc?.Markdown ?? string.Empty;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -227,7 +227,7 @@ public partial class SoftwareViewViewModel : ObservableObject, IRegionAware
|
||||
private Visibility _showCredits = Visibility.Collapsed;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _descriptionHtml = string.Empty;
|
||||
private string _descriptionMarkdown = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private Visibility _showDescription = Visibility.Collapsed;
|
||||
@@ -235,12 +235,12 @@ public partial class SoftwareViewViewModel : ObservableObject, IRegionAware
|
||||
[ObservableProperty]
|
||||
private ImageSource? _heroImageSource;
|
||||
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionHtml);
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionMarkdown);
|
||||
public bool HasHeroImage => HeroImageSource is not null;
|
||||
public bool HasMarechaiScore => MarechaiScore.HasValue;
|
||||
public bool HasMarechaiScoreRankText => !string.IsNullOrWhiteSpace(MarechaiScoreRankText);
|
||||
|
||||
partial void OnDescriptionHtmlChanged(string value)
|
||||
partial void OnDescriptionMarkdownChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasDescription));
|
||||
}
|
||||
@@ -707,7 +707,7 @@ public partial class SoftwareViewViewModel : ObservableObject, IRegionAware
|
||||
{
|
||||
string langCode = GetIso639CodeFromCulture();
|
||||
SoftwareDescriptionDto? desc = await _browsingService.GetDescriptionAsync(softwareId, langCode);
|
||||
DescriptionHtml = desc?.Html ?? desc?.Markdown ?? string.Empty;
|
||||
DescriptionMarkdown = desc?.Markdown ?? string.Empty;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ public partial class SoundSynthDetailViewModel : ObservableObject, IRegionAware
|
||||
private bool _hasSmartphones;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _descriptionHtml = string.Empty;
|
||||
private string _descriptionMarkdown = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _displayName = string.Empty;
|
||||
@@ -93,9 +93,9 @@ public partial class SoundSynthDetailViewModel : ObservableObject, IRegionAware
|
||||
/// <summary>
|
||||
/// Gets whether a description is available
|
||||
/// </summary>
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionHtml);
|
||||
public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionMarkdown);
|
||||
|
||||
partial void OnDescriptionHtmlChanged(string value)
|
||||
partial void OnDescriptionMarkdownChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(HasDescription));
|
||||
}
|
||||
@@ -290,7 +290,7 @@ public partial class SoundSynthDetailViewModel : ObservableObject, IRegionAware
|
||||
string langCode = GetIso639CodeFromCulture();
|
||||
SoundSynthDescriptionDto? desc = await _soundSynthsService.GetDescriptionAsync(SoundSynthId, langCode);
|
||||
|
||||
DescriptionHtml = desc?.Html ?? desc?.Markdown ?? string.Empty;
|
||||
DescriptionMarkdown = desc?.Markdown ?? string.Empty;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="using:Marechai.App.Presentation.Converters"
|
||||
xmlns:local="using:Marechai.App.Presentation.Views"
|
||||
xmlns:utu="using:Uno.Toolkit.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
NavigationCacheMode="Required"
|
||||
@@ -856,8 +857,7 @@
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Padding="16"
|
||||
Spacing="8">
|
||||
<WebView2 x:Name="DescriptionWebView"
|
||||
MinHeight="400" />
|
||||
<local:MessageMarkdownView Markdown="{Binding DescriptionMarkdown}" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabViewItem>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Windows.System;
|
||||
using Marechai.App.Presentation.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
@@ -14,56 +13,6 @@ public sealed partial class CompanyDetailPage : Page
|
||||
public CompanyDetailPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
if(args.NewValue is CompanyDetailViewModel vm)
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if(e.PropertyName == nameof(CompanyDetailViewModel.DescriptionHtml) &&
|
||||
sender is CompanyDetailViewModel vm &&
|
||||
!string.IsNullOrWhiteSpace(vm.DescriptionHtml))
|
||||
{
|
||||
bool isDark = ActualTheme == ElementTheme.Dark;
|
||||
var html = WrapInHtmlDocument(vm.DescriptionHtml, isDark);
|
||||
DescriptionWebView.NavigateToString(html);
|
||||
}
|
||||
}
|
||||
|
||||
private static string WrapInHtmlDocument(string bodyHtml, bool isDark)
|
||||
{
|
||||
string textColor = isDark ? "#e0e0e0" : "#1a1a1a";
|
||||
string bgColor = isDark ? "#1e1e1e" : "#ffffff";
|
||||
string linkColor = isDark ? "#6cb4ee" : "#0063b1";
|
||||
|
||||
return $$"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
color: {{textColor}};
|
||||
background: {{bgColor}};
|
||||
}
|
||||
a { color: {{linkColor}}; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>{{bodyHtml}}</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
|
||||
private async void OnTwitterClick(object sender, RoutedEventArgs e)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="using:Marechai.App.Presentation.Converters"
|
||||
xmlns:local="using:Marechai.App.Presentation.Views"
|
||||
xmlns:utu="using:Uno.Toolkit.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
NavigationCacheMode="Required"
|
||||
@@ -256,8 +257,7 @@
|
||||
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=DescriptionSectionTitle}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold" />
|
||||
<WebView2 x:Name="DescriptionWebView"
|
||||
MinHeight="200" />
|
||||
<local:MessageMarkdownView Markdown="{Binding DescriptionMarkdown}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel;
|
||||
using Marechai.App.Presentation.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -12,56 +11,6 @@ public sealed partial class GpuDetailPage : Page
|
||||
public GpuDetailPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
if(args.NewValue is GpuDetailViewModel vm)
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if(e.PropertyName == nameof(GpuDetailViewModel.DescriptionHtml) &&
|
||||
sender is GpuDetailViewModel vm &&
|
||||
!string.IsNullOrWhiteSpace(vm.DescriptionHtml))
|
||||
{
|
||||
bool isDark = ActualTheme == ElementTheme.Dark;
|
||||
var html = WrapInHtmlDocument(vm.DescriptionHtml, isDark);
|
||||
DescriptionWebView.NavigateToString(html);
|
||||
}
|
||||
}
|
||||
|
||||
private static string WrapInHtmlDocument(string bodyHtml, bool isDark)
|
||||
{
|
||||
string textColor = isDark ? "#e0e0e0" : "#1a1a1a";
|
||||
string bgColor = isDark ? "#1e1e1e" : "#ffffff";
|
||||
string linkColor = isDark ? "#6cb4ee" : "#0063b1";
|
||||
|
||||
return $$"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
color: {{textColor}};
|
||||
background: {{bgColor}};
|
||||
}
|
||||
a { color: {{linkColor}}; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>{{bodyHtml}}</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
|
||||
private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="using:Marechai.App.Presentation.Converters"
|
||||
xmlns:wctui="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
xmlns:local="using:Marechai.App.Presentation.Views"
|
||||
xmlns:components="using:Marechai.App.Presentation.Components"
|
||||
NavigationCacheMode="Disabled"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
@@ -396,8 +397,7 @@
|
||||
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=DescriptionSectionTitle}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold" />
|
||||
<WebView2 x:Name="DescriptionWebView"
|
||||
MinHeight="200" />
|
||||
<local:MessageMarkdownView Markdown="{Binding DescriptionMarkdown}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel;
|
||||
using Marechai.App.Presentation.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Marechai.App.Presentation.Views;
|
||||
@@ -12,55 +9,5 @@ public sealed partial class MachineViewPage : Page
|
||||
public MachineViewPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
if(args.NewValue is MachineViewViewModel vm)
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if(e.PropertyName == nameof(MachineViewViewModel.DescriptionHtml) &&
|
||||
sender is MachineViewViewModel vm &&
|
||||
!string.IsNullOrWhiteSpace(vm.DescriptionHtml))
|
||||
{
|
||||
bool isDark = ActualTheme == ElementTheme.Dark;
|
||||
var html = WrapInHtmlDocument(vm.DescriptionHtml, isDark);
|
||||
DescriptionWebView.NavigateToString(html);
|
||||
}
|
||||
}
|
||||
|
||||
private static string WrapInHtmlDocument(string bodyHtml, bool isDark)
|
||||
{
|
||||
string textColor = isDark ? "#e0e0e0" : "#1a1a1a";
|
||||
string bgColor = isDark ? "#1e1e1e" : "#ffffff";
|
||||
string linkColor = isDark ? "#6cb4ee" : "#0063b1";
|
||||
|
||||
return $$"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
color: {{textColor}};
|
||||
background: {{bgColor}};
|
||||
}
|
||||
a { color: {{linkColor}}; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>{{bodyHtml}}</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
337
Marechai.App/Presentation/Views/MarkdownRenderer.cs
Normal file
337
Marechai.App/Presentation/Views/MarkdownRenderer.cs
Normal file
@@ -0,0 +1,337 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Markdig;
|
||||
using Markdig.Extensions.Footnotes;
|
||||
using Markdig.Extensions.Tables;
|
||||
using Markdig.Syntax;
|
||||
using Markdig.Syntax.Inlines;
|
||||
using Microsoft.UI.Text;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Documents;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using Windows.System;
|
||||
using Block = Markdig.Syntax.Block;
|
||||
using Markdown = Markdig.Markdown;
|
||||
|
||||
namespace Marechai.App.Presentation.Views;
|
||||
|
||||
/// <summary>
|
||||
/// Renders markdown directly into native XAML elements (TextBlock/Span/Hyperlink/Image trees) instead of going
|
||||
/// through HTML and an embedded WebView. Uno's WebView2 doesn't reliably composite on every desktop backend
|
||||
/// (it renders nothing on Linux/X11), so this avoids depending on a native embedded browser at all - it works
|
||||
/// identically on every Uno target. <c>DisableHtml()</c> means literal HTML in the source is never interpreted,
|
||||
/// just shown as plain text, so there's no markup-injection surface to sanitize.
|
||||
/// </summary>
|
||||
internal static class MarkdownRenderer
|
||||
{
|
||||
static readonly MarkdownPipeline _pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAdvancedExtensions()
|
||||
.DisableHtml()
|
||||
.Build();
|
||||
|
||||
public static UIElement Render(string? markdown)
|
||||
{
|
||||
var root = new StackPanel { Spacing = 8 };
|
||||
|
||||
if(string.IsNullOrWhiteSpace(markdown)) return root;
|
||||
|
||||
MarkdownDocument document = Markdown.Parse(markdown, _pipeline);
|
||||
|
||||
foreach(Block block in document) AppendBlock(root, block);
|
||||
|
||||
return root;
|
||||
}
|
||||
|
||||
static void AppendBlock(Panel container, Block block)
|
||||
{
|
||||
switch(block)
|
||||
{
|
||||
case HeadingBlock heading:
|
||||
container.Children.Add(BuildHeading(heading));
|
||||
break;
|
||||
|
||||
case QuoteBlock quote:
|
||||
container.Children.Add(BuildQuote(quote));
|
||||
break;
|
||||
|
||||
case ListBlock list:
|
||||
container.Children.Add(BuildList(list));
|
||||
break;
|
||||
|
||||
case FencedCodeBlock or CodeBlock:
|
||||
container.Children.Add(BuildCodeBlock((LeafBlock)block));
|
||||
break;
|
||||
|
||||
case ThematicBreakBlock:
|
||||
container.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Opacity = 0.4,
|
||||
Margin = new Thickness(0, 4, 0, 4),
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = ThemeBrush("TextFillColorSecondaryBrush")
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case Table table:
|
||||
container.Children.Add(BuildTable(table));
|
||||
break;
|
||||
|
||||
case FootnoteGroup footnotes:
|
||||
container.Children.Add(BuildFootnotes(footnotes));
|
||||
break;
|
||||
|
||||
case LeafBlock { Inline: not null } leaf:
|
||||
container.Children.Add(BuildParagraph(leaf.Inline));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static TextBlock BuildHeading(HeadingBlock heading)
|
||||
{
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
FontWeight = FontWeights.SemiBold,
|
||||
FontSize = heading.Level switch
|
||||
{
|
||||
1 => 24,
|
||||
2 => 20,
|
||||
3 => 18,
|
||||
4 => 16,
|
||||
5 => 15,
|
||||
_ => 14
|
||||
}
|
||||
};
|
||||
|
||||
if(heading.Inline is not null) AppendInlines(textBlock.Inlines, heading.Inline);
|
||||
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
static TextBlock BuildParagraph(ContainerInline? inline)
|
||||
{
|
||||
var textBlock = new TextBlock { TextWrapping = TextWrapping.Wrap, FontSize = 14 };
|
||||
|
||||
if(inline is not null) AppendInlines(textBlock.Inlines, inline);
|
||||
|
||||
return textBlock;
|
||||
}
|
||||
|
||||
static Border BuildQuote(QuoteBlock quote)
|
||||
{
|
||||
var inner = new StackPanel { Spacing = 4 };
|
||||
|
||||
foreach(Block child in quote) AppendBlock(inner, child);
|
||||
|
||||
return new Border
|
||||
{
|
||||
BorderBrush = ThemeBrush("TextFillColorSecondaryBrush"),
|
||||
BorderThickness = new Thickness(3, 0, 0, 0),
|
||||
Padding = new Thickness(8, 0, 0, 0),
|
||||
Child = inner
|
||||
};
|
||||
}
|
||||
|
||||
static StackPanel BuildList(ListBlock list)
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 4 };
|
||||
int index = list.IsOrdered && int.TryParse(list.OrderedStart, out int start) ? start : 1;
|
||||
|
||||
foreach(Block item in list)
|
||||
{
|
||||
if(item is not ListItemBlock listItem) continue;
|
||||
|
||||
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 4 };
|
||||
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = list.IsOrdered ? $"{index}." : "•", FontSize = 14, Margin = new Thickness(8, 0, 0, 0)
|
||||
});
|
||||
|
||||
var content = new StackPanel { Spacing = 4 };
|
||||
|
||||
foreach(Block child in listItem) AppendBlock(content, child);
|
||||
|
||||
row.Children.Add(content);
|
||||
panel.Children.Add(row);
|
||||
index++;
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
static Border BuildCodeBlock(LeafBlock code)
|
||||
{
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Text = code.Lines.ToString(),
|
||||
FontFamily = new FontFamily("Consolas"),
|
||||
FontSize = 13,
|
||||
TextWrapping = TextWrapping.NoWrap
|
||||
};
|
||||
|
||||
return new Border
|
||||
{
|
||||
Background = ThemeBrush("CardBackgroundFillColorDefaultBrush"),
|
||||
Padding = new Thickness(8),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Child = new ScrollViewer
|
||||
{
|
||||
Content = textBlock,
|
||||
HorizontalScrollBarVisibility = ScrollBarVisibility.Auto,
|
||||
VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
static Grid BuildTable(Table table)
|
||||
{
|
||||
var grid = new Grid();
|
||||
|
||||
for(int c = 0; c < table.ColumnDefinitions.Count; c++) grid.ColumnDefinitions.Add(new ColumnDefinition());
|
||||
|
||||
int rowIndex = 0;
|
||||
|
||||
foreach(TableRow row in table.OfType<TableRow>())
|
||||
{
|
||||
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
|
||||
int columnIndex = 0;
|
||||
|
||||
foreach(TableCell cell in row.OfType<TableCell>())
|
||||
{
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Margin = new Thickness(4),
|
||||
FontWeight = row.IsHeader ? FontWeights.SemiBold : FontWeights.Normal
|
||||
};
|
||||
|
||||
foreach(Block cellBlock in cell)
|
||||
if(cellBlock is ParagraphBlock { Inline: not null } paragraph)
|
||||
AppendInlines(textBlock.Inlines, paragraph.Inline);
|
||||
|
||||
Grid.SetRow(textBlock, rowIndex);
|
||||
Grid.SetColumn(textBlock, columnIndex);
|
||||
grid.Children.Add(textBlock);
|
||||
columnIndex++;
|
||||
}
|
||||
|
||||
rowIndex++;
|
||||
}
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
static StackPanel BuildFootnotes(FootnoteGroup group)
|
||||
{
|
||||
var panel = new StackPanel { Spacing = 4, Margin = new Thickness(0, 8, 0, 0) };
|
||||
|
||||
panel.Children.Add(new Border
|
||||
{
|
||||
Height = 1,
|
||||
Opacity = 0.4,
|
||||
Margin = new Thickness(0, 0, 0, 4),
|
||||
BorderThickness = new Thickness(1),
|
||||
BorderBrush = ThemeBrush("TextFillColorSecondaryBrush")
|
||||
});
|
||||
|
||||
foreach(Block block in group)
|
||||
{
|
||||
if(block is not Footnote footnote) continue;
|
||||
|
||||
var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 4 };
|
||||
|
||||
row.Children.Add(new TextBlock
|
||||
{
|
||||
Text = $"{footnote.Order}.", FontSize = 12, Opacity = 0.7, Margin = new Thickness(8, 0, 0, 0)
|
||||
});
|
||||
|
||||
var content = new StackPanel { Spacing = 4 };
|
||||
|
||||
foreach(Block child in footnote) AppendBlock(content, child);
|
||||
|
||||
row.Children.Add(content);
|
||||
panel.Children.Add(row);
|
||||
}
|
||||
|
||||
return panel;
|
||||
}
|
||||
|
||||
static void AppendInlines(InlineCollection target, ContainerInline container)
|
||||
{
|
||||
foreach(Markdig.Syntax.Inlines.Inline inline in container) AppendInline(target, inline);
|
||||
}
|
||||
|
||||
static void AppendInline(InlineCollection target, Markdig.Syntax.Inlines.Inline inline)
|
||||
{
|
||||
switch(inline)
|
||||
{
|
||||
case LiteralInline literal:
|
||||
target.Add(new Run { Text = literal.Content.ToString() });
|
||||
break;
|
||||
|
||||
case LineBreakInline:
|
||||
target.Add(new LineBreak());
|
||||
break;
|
||||
|
||||
case EmphasisInline emphasis:
|
||||
var span = new Span();
|
||||
|
||||
if(emphasis.DelimiterCount >= 2) span.FontWeight = FontWeights.Bold;
|
||||
else span.FontStyle = Windows.UI.Text.FontStyle.Italic;
|
||||
|
||||
AppendInlines(span.Inlines, emphasis);
|
||||
target.Add(span);
|
||||
break;
|
||||
|
||||
case CodeInline code:
|
||||
target.Add(new Run { Text = code.Content, FontFamily = new FontFamily("Consolas") });
|
||||
break;
|
||||
|
||||
case LinkInline { IsImage: true, Url: not null } image:
|
||||
target.Add(new InlineUIContainer
|
||||
{
|
||||
Child = new Image
|
||||
{
|
||||
Source = new Microsoft.UI.Xaml.Media.Imaging.BitmapImage(new Uri(image.Url)),
|
||||
MaxWidth = 480,
|
||||
MaxHeight = 360,
|
||||
Stretch = Stretch.Uniform
|
||||
}
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case LinkInline link:
|
||||
var hyperlink = new Hyperlink();
|
||||
AppendInlines(hyperlink.Inlines, link);
|
||||
|
||||
if(Uri.TryCreate(link.Url, UriKind.Absolute, out Uri? uri))
|
||||
hyperlink.Click += async (_, _) => await Launcher.LaunchUriAsync(uri);
|
||||
|
||||
target.Add(hyperlink);
|
||||
break;
|
||||
|
||||
case FootnoteLink footnoteLink:
|
||||
target.Add(new Span
|
||||
{
|
||||
FontSize = 11,
|
||||
Inlines = { new Run { Text = $"[{footnoteLink.Footnote?.Order}]" } }
|
||||
});
|
||||
|
||||
break;
|
||||
|
||||
case ContainerInline containerInline:
|
||||
AppendInlines(target, containerInline);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static Brush ThemeBrush(string resourceKey) =>
|
||||
(Brush)Application.Current.Resources[resourceKey];
|
||||
}
|
||||
@@ -2,13 +2,7 @@
|
||||
|
||||
<UserControl x:Class="Marechai.App.Presentation.Views.MessageMarkdownView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls">
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<controls:MarkdownTextBlock x:Name="MarkdownBlock"
|
||||
Margin="0"
|
||||
IsTextSelectionEnabled="True"
|
||||
ImageMaxWidth="720"
|
||||
ImageMaxHeight="480"
|
||||
LinkClicked="MarkdownBlock_OnLinkClicked" />
|
||||
<Grid x:Name="RootGrid" />
|
||||
</UserControl>
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using CommunityToolkit.WinUI.UI.Controls;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.System;
|
||||
|
||||
namespace Marechai.App.Presentation.Views;
|
||||
|
||||
@@ -27,16 +24,9 @@ public sealed partial class MessageMarkdownView : UserControl
|
||||
|
||||
static void OnMarkdownChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if(d is MessageMarkdownView view)
|
||||
view.MarkdownBlock.Text = e.NewValue as string ?? string.Empty;
|
||||
}
|
||||
if(d is not MessageMarkdownView view) return;
|
||||
|
||||
async void MarkdownBlock_OnLinkClicked(object sender, LinkClickedEventArgs e)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(e.Link)) return;
|
||||
|
||||
if(Uri.TryCreate(e.Link, UriKind.Absolute, out Uri? uri) &&
|
||||
(uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps || uri.Scheme == Uri.UriSchemeMailto))
|
||||
await Launcher.LaunchUriAsync(uri);
|
||||
view.RootGrid.Children.Clear();
|
||||
view.RootGrid.Children.Add(MarkdownRenderer.Render(e.NewValue as string));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="using:Marechai.App.Presentation.Converters"
|
||||
xmlns:local="using:Marechai.App.Presentation.Views"
|
||||
xmlns:utu="using:Uno.Toolkit.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
NavigationCacheMode="Required"
|
||||
@@ -422,8 +423,7 @@
|
||||
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=DescriptionSectionTitle}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold" />
|
||||
<WebView2 x:Name="DescriptionWebView"
|
||||
MinHeight="200" />
|
||||
<local:MessageMarkdownView Markdown="{Binding DescriptionMarkdown}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel;
|
||||
using Marechai.App.Presentation.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -12,56 +11,6 @@ public sealed partial class ProcessorDetailPage : Page
|
||||
public ProcessorDetailPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
if(args.NewValue is ProcessorDetailViewModel vm)
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if(e.PropertyName == nameof(ProcessorDetailViewModel.DescriptionHtml) &&
|
||||
sender is ProcessorDetailViewModel vm &&
|
||||
!string.IsNullOrWhiteSpace(vm.DescriptionHtml))
|
||||
{
|
||||
bool isDark = ActualTheme == ElementTheme.Dark;
|
||||
var html = WrapInHtmlDocument(vm.DescriptionHtml, isDark);
|
||||
DescriptionWebView.NavigateToString(html);
|
||||
}
|
||||
}
|
||||
|
||||
private static string WrapInHtmlDocument(string bodyHtml, bool isDark)
|
||||
{
|
||||
string textColor = isDark ? "#e0e0e0" : "#1a1a1a";
|
||||
string bgColor = isDark ? "#1e1e1e" : "#ffffff";
|
||||
string linkColor = isDark ? "#6cb4ee" : "#0063b1";
|
||||
|
||||
return $$"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
color: {{textColor}};
|
||||
background: {{bgColor}};
|
||||
}
|
||||
a { color: {{linkColor}}; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>{{bodyHtml}}</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
|
||||
private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="using:Marechai.App.Presentation.Converters"
|
||||
xmlns:components="using:Marechai.App.Presentation.Components"
|
||||
xmlns:local="using:Marechai.App.Presentation.Views"
|
||||
xmlns:wctui="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
NavigationCacheMode="Disabled"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
|
||||
@@ -234,8 +235,7 @@
|
||||
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=DescriptionSectionTitle}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold" />
|
||||
<WebView2 x:Name="DescriptionWebView"
|
||||
MinHeight="200" />
|
||||
<local:MessageMarkdownView Markdown="{Binding DescriptionMarkdown}" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Family -->
|
||||
|
||||
@@ -14,13 +14,6 @@ public sealed partial class SoftwareViewPage : Page
|
||||
public SoftwareViewPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
if(args.NewValue is SoftwareViewViewModel vm)
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
private async void OpenReviewDialog_Click(object sender, RoutedEventArgs e)
|
||||
@@ -143,46 +136,4 @@ public sealed partial class SoftwareViewPage : Page
|
||||
vm.ClearReviewFeedbackCommand.Execute(null);
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if(e.PropertyName == nameof(SoftwareViewViewModel.DescriptionHtml) &&
|
||||
sender is SoftwareViewViewModel vm &&
|
||||
!string.IsNullOrWhiteSpace(vm.DescriptionHtml))
|
||||
{
|
||||
bool isDark = ActualTheme == ElementTheme.Dark;
|
||||
var html = WrapInHtmlDocument(vm.DescriptionHtml, isDark);
|
||||
DescriptionWebView.NavigateToString(html);
|
||||
}
|
||||
}
|
||||
|
||||
private static string WrapInHtmlDocument(string bodyHtml, bool isDark)
|
||||
{
|
||||
string textColor = isDark ? "#e0e0e0" : "#1a1a1a";
|
||||
string bgColor = isDark ? "#1e1e1e" : "#ffffff";
|
||||
string linkColor = isDark ? "#6cb4ee" : "#0063b1";
|
||||
|
||||
return $$"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
color: {{textColor}};
|
||||
background: {{bgColor}};
|
||||
}
|
||||
a { color: {{linkColor}}; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>{{bodyHtml}}</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:loc="using:Marechai.App.Presentation.Converters"
|
||||
xmlns:local="using:Marechai.App.Presentation.Views"
|
||||
xmlns:utu="using:Uno.Toolkit.UI"
|
||||
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
|
||||
NavigationCacheMode="Required"
|
||||
@@ -194,8 +195,7 @@
|
||||
<TextBlock Text="{Binding Source={StaticResource Strings}, Path=DescriptionSectionTitle}"
|
||||
FontSize="20"
|
||||
FontWeight="SemiBold" />
|
||||
<WebView2 x:Name="DescriptionWebView"
|
||||
MinHeight="200" />
|
||||
<local:MessageMarkdownView Markdown="{Binding DescriptionMarkdown}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#nullable enable
|
||||
|
||||
using System.ComponentModel;
|
||||
using Marechai.App.Presentation.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
@@ -12,56 +11,6 @@ public sealed partial class SoundSynthDetailPage : Page
|
||||
public SoundSynthDetailPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
|
||||
{
|
||||
if(args.NewValue is SoundSynthDetailViewModel vm)
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
}
|
||||
|
||||
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if(e.PropertyName == nameof(SoundSynthDetailViewModel.DescriptionHtml) &&
|
||||
sender is SoundSynthDetailViewModel vm &&
|
||||
!string.IsNullOrWhiteSpace(vm.DescriptionHtml))
|
||||
{
|
||||
bool isDark = ActualTheme == ElementTheme.Dark;
|
||||
var html = WrapInHtmlDocument(vm.DescriptionHtml, isDark);
|
||||
DescriptionWebView.NavigateToString(html);
|
||||
}
|
||||
}
|
||||
|
||||
private static string WrapInHtmlDocument(string bodyHtml, bool isDark)
|
||||
{
|
||||
string textColor = isDark ? "#e0e0e0" : "#1a1a1a";
|
||||
string bgColor = isDark ? "#1e1e1e" : "#ffffff";
|
||||
string linkColor = isDark ? "#6cb4ee" : "#0063b1";
|
||||
|
||||
return $$"""
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
margin: 0;
|
||||
padding: 8px;
|
||||
color: {{textColor}};
|
||||
background: {{bgColor}};
|
||||
}
|
||||
a { color: {{linkColor}}; }
|
||||
img { max-width: 100%; height: auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>{{bodyHtml}}</body>
|
||||
</html>
|
||||
""";
|
||||
}
|
||||
|
||||
private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
|
||||
|
||||
Reference in New Issue
Block a user