diff --git a/Directory.Packages.props b/Directory.Packages.props index 4d8a7c56..01c1b6fc 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -61,12 +61,10 @@ - - diff --git a/Marechai.App/Marechai.App.csproj b/Marechai.App/Marechai.App.csproj index 6bc168f5..44d585d3 100644 --- a/Marechai.App/Marechai.App.csproj +++ b/Marechai.App/Marechai.App.csproj @@ -64,6 +64,7 @@ + @@ -102,12 +103,10 @@ - - diff --git a/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs b/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs index 09532bbd..92d67f9d 100644 --- a/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs @@ -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 _people = []; @@ -194,7 +194,7 @@ public partial class CompanyDetailViewModel : ObservableObject, IRegionAware /// /// Gets whether a description is available /// - public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionHtml); + public bool HasDescription => !string.IsNullOrWhiteSpace(DescriptionMarkdown); /// /// 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) { diff --git a/Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs b/Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs index 49ed8f45..ed4a864a 100644 --- a/Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs @@ -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 /// /// Gets whether a description is available /// - 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) { diff --git a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs index 85b6842c..476b77c4 100644 --- a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs @@ -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 /// /// Gets whether a description is available /// - 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) { diff --git a/Marechai.App/Presentation/ViewModels/ProcessorDetailViewModel.cs b/Marechai.App/Presentation/ViewModels/ProcessorDetailViewModel.cs index 67a212e2..2c69ea50 100644 --- a/Marechai.App/Presentation/ViewModels/ProcessorDetailViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/ProcessorDetailViewModel.cs @@ -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 /// /// Gets whether a description is available /// - 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) { diff --git a/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs b/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs index 6e79284c..a4ce1f77 100644 --- a/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/SoftwareViewViewModel.cs @@ -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) { diff --git a/Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs b/Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs index f1233e42..7f5e3a2e 100644 --- a/Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs @@ -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 /// /// Gets whether a description is available /// - 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) { diff --git a/Marechai.App/Presentation/Views/CompanyDetailPage.xaml b/Marechai.App/Presentation/Views/CompanyDetailPage.xaml index d75ee668..39d30d0f 100644 --- a/Marechai.App/Presentation/Views/CompanyDetailPage.xaml +++ b/Marechai.App/Presentation/Views/CompanyDetailPage.xaml @@ -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"> - + diff --git a/Marechai.App/Presentation/Views/CompanyDetailPage.xaml.cs b/Marechai.App/Presentation/Views/CompanyDetailPage.xaml.cs index 7180546f..2be1a86b 100644 --- a/Marechai.App/Presentation/Views/CompanyDetailPage.xaml.cs +++ b/Marechai.App/Presentation/Views/CompanyDetailPage.xaml.cs @@ -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 $$""" - - - - - - - - {{bodyHtml}} - - """; } private async void OnTwitterClick(object sender, RoutedEventArgs e) diff --git a/Marechai.App/Presentation/Views/GpuDetailPage.xaml b/Marechai.App/Presentation/Views/GpuDetailPage.xaml index 4185a093..04d655ab 100644 --- a/Marechai.App/Presentation/Views/GpuDetailPage.xaml +++ b/Marechai.App/Presentation/Views/GpuDetailPage.xaml @@ -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 @@ - + diff --git a/Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs b/Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs index fa855a7d..77534383 100644 --- a/Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs +++ b/Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs @@ -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 $$""" - - - - - - - - {{bodyHtml}} - - """; } private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) diff --git a/Marechai.App/Presentation/Views/MachineViewPage.xaml b/Marechai.App/Presentation/Views/MachineViewPage.xaml index 49301a14..9c21c4a9 100644 --- a/Marechai.App/Presentation/Views/MachineViewPage.xaml +++ b/Marechai.App/Presentation/Views/MachineViewPage.xaml @@ -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 @@ - + diff --git a/Marechai.App/Presentation/Views/MachineViewPage.xaml.cs b/Marechai.App/Presentation/Views/MachineViewPage.xaml.cs index c666d977..e698877f 100644 --- a/Marechai.App/Presentation/Views/MachineViewPage.xaml.cs +++ b/Marechai.App/Presentation/Views/MachineViewPage.xaml.cs @@ -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 $$""" - - - - - - - - {{bodyHtml}} - - """; - } -} \ No newline at end of file +} diff --git a/Marechai.App/Presentation/Views/MarkdownRenderer.cs b/Marechai.App/Presentation/Views/MarkdownRenderer.cs new file mode 100644 index 00000000..6d32e83e --- /dev/null +++ b/Marechai.App/Presentation/Views/MarkdownRenderer.cs @@ -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; + +/// +/// 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. DisableHtml() means literal HTML in the source is never interpreted, +/// just shown as plain text, so there's no markup-injection surface to sanitize. +/// +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()) + { + grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); + int columnIndex = 0; + + foreach(TableCell cell in row.OfType()) + { + 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]; +} diff --git a/Marechai.App/Presentation/Views/MessageMarkdownView.xaml b/Marechai.App/Presentation/Views/MessageMarkdownView.xaml index a9cfe638..4064344e 100644 --- a/Marechai.App/Presentation/Views/MessageMarkdownView.xaml +++ b/Marechai.App/Presentation/Views/MessageMarkdownView.xaml @@ -2,13 +2,7 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> - + diff --git a/Marechai.App/Presentation/Views/MessageMarkdownView.xaml.cs b/Marechai.App/Presentation/Views/MessageMarkdownView.xaml.cs index 28830509..967886df 100644 --- a/Marechai.App/Presentation/Views/MessageMarkdownView.xaml.cs +++ b/Marechai.App/Presentation/Views/MessageMarkdownView.xaml.cs @@ -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)); } } diff --git a/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml b/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml index f6602256..15a55a03 100644 --- a/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml +++ b/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml @@ -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 @@ - + diff --git a/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml.cs b/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml.cs index 9184ae33..e361fe42 100644 --- a/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml.cs +++ b/Marechai.App/Presentation/Views/ProcessorDetailPage.xaml.cs @@ -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 $$""" - - - - - - - - {{bodyHtml}} - - """; } private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) diff --git a/Marechai.App/Presentation/Views/SoftwareViewPage.xaml b/Marechai.App/Presentation/Views/SoftwareViewPage.xaml index 3067ccba..17c5b93d 100644 --- a/Marechai.App/Presentation/Views/SoftwareViewPage.xaml +++ b/Marechai.App/Presentation/Views/SoftwareViewPage.xaml @@ -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 @@ - + diff --git a/Marechai.App/Presentation/Views/SoftwareViewPage.xaml.cs b/Marechai.App/Presentation/Views/SoftwareViewPage.xaml.cs index 92739a78..ab30b1be 100644 --- a/Marechai.App/Presentation/Views/SoftwareViewPage.xaml.cs +++ b/Marechai.App/Presentation/Views/SoftwareViewPage.xaml.cs @@ -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 $$""" - - - - - - - - {{bodyHtml}} - - """; - } } diff --git a/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml index f186260a..491640e9 100644 --- a/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml +++ b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml @@ -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 @@ - + diff --git a/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs index c7bef977..9794e8d0 100644 --- a/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs +++ b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs @@ -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 $$""" - - - - - - - - {{bodyHtml}} - - """; } private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)