From 9f39176dc098988fabb31ed1b2bde668d4ac828c Mon Sep 17 00:00:00 2001 From: erri120 Date: Wed, 17 Apr 2024 14:25:47 +0200 Subject: [PATCH] Add markdown renderer component (#1230) * Add MarkdownRenderer component * FirstOrDefault -> FirstOrOptional * Update DiagnosticDetails --- .../Extensions/LoadoutExtensions.cs | 2 +- .../IMarkdownRendererViewModel.cs | 17 ++++ .../MarkdownRendererDesignViewModel.cs | 81 +++++++++++++++++++ .../MarkdownRendererView.axaml | 67 +++++++++++++++ .../MarkdownRendererView.axaml.cs | 23 ++++++ .../MarkdownRendererViewModel.cs | 27 +++++++ src/NexusMods.App.UI/NexusMods.App.UI.csproj | 6 ++ .../DiagnosticDetailsDesignViewModel.cs | 12 +-- .../Details/DiagnosticDetailsPage.cs | 12 +-- .../Details/DiagnosticDetailsView.axaml | 52 +----------- .../Details/DiagnosticDetailsView.axaml.cs | 8 +- .../Details/DiagnosticDetailsViewModel.cs | 23 ++---- .../Details/IDiagnosticDetailsViewModel.cs | 7 +- src/NexusMods.App.UI/Services.cs | 4 + 14 files changed, 252 insertions(+), 89 deletions(-) create mode 100644 src/NexusMods.App.UI/Controls/MarkdownRenderer/IMarkdownRendererViewModel.cs create mode 100644 src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererDesignViewModel.cs create mode 100644 src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml create mode 100644 src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml.cs create mode 100644 src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs diff --git a/src/Abstractions/NexusMods.Abstractions.Loadouts/Extensions/LoadoutExtensions.cs b/src/Abstractions/NexusMods.Abstractions.Loadouts/Extensions/LoadoutExtensions.cs index 3b1f513821..6819e7187a 100644 --- a/src/Abstractions/NexusMods.Abstractions.Loadouts/Extensions/LoadoutExtensions.cs +++ b/src/Abstractions/NexusMods.Abstractions.Loadouts/Extensions/LoadoutExtensions.cs @@ -57,6 +57,6 @@ public static Optional> GetFirstModWithMetadata( this Loadout loadout, bool onlyEnabledMods = true) where T : AModMetadata { - return loadout.GetModsWithMetadata(onlyEnabledMods).FirstOrDefault(); + return loadout.GetModsWithMetadata(onlyEnabledMods).FirstOrOptional(_ => true); } } diff --git a/src/NexusMods.App.UI/Controls/MarkdownRenderer/IMarkdownRendererViewModel.cs b/src/NexusMods.App.UI/Controls/MarkdownRenderer/IMarkdownRendererViewModel.cs new file mode 100644 index 0000000000..75fc11e31f --- /dev/null +++ b/src/NexusMods.App.UI/Controls/MarkdownRenderer/IMarkdownRendererViewModel.cs @@ -0,0 +1,17 @@ +using System.Reactive; +using ReactiveUI; + +namespace NexusMods.App.UI.Controls.MarkdownRenderer; + +public interface IMarkdownRendererViewModel : IViewModelInterface +{ + /// + /// Gets or sets the contents of the renderer. + /// + public string Contents { get; set; } + + /// + /// Gets the command used for opening links from Markdown. + /// + public ReactiveCommand OpenLinkCommand { get; } +} diff --git a/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererDesignViewModel.cs b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererDesignViewModel.cs new file mode 100644 index 0000000000..67568ee682 --- /dev/null +++ b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererDesignViewModel.cs @@ -0,0 +1,81 @@ +using System.Reactive; +using JetBrains.Annotations; +using ReactiveUI; + +namespace NexusMods.App.UI.Controls.MarkdownRenderer; + +public class MarkdownRendererDesignViewModel : AViewModel, IMarkdownRendererViewModel +{ + public string Contents { get; set; } + + public ReactiveCommand OpenLinkCommand { get; } = ReactiveCommand.Create(_ => { }); + + public MarkdownRendererDesignViewModel() : this(DefaultContents) { } + + public MarkdownRendererDesignViewModel(string contents) + { + Contents = contents; + } + + // From https://jaspervdj.be/lorem-markdownum/ + [LanguageInjection("markdown")] + private const string DefaultContents = +""" +# Quid nostro + +## Velox tot frugum accipe + +Lorem markdownum sacros si Iovis aquarum, oreris bene inmurmurat arborei +propulsa labori invidiosa, sic tibi sic: tumulis. Pectoris ait plectrum fregit +aegram. Cum *legit urit* nec solet corpora loquebatur cur, in vivaque quasque +corpus **sagittas Numam** Lucifer mentesque, **falsa**. Vult plagis sospite +veneni prodiga ratione, currus malus! Et sinat mersaeque fletque cycnus +auxiliaris, sum **quid frondentis** sensit. + +1. Moneo ora equos per monstro o foedera +2. Confusura veneni lacertis pisce inmedicabile quid tenuaverat +3. Ardere una quam paciscor alimenta liber +4. Captivarum Venus +5. Nunc iphis Orion aethera genitore doleas pro + +Insula illa optime in admonita exigit, clausit sua aut paelicis potiunda ipsius +canes falsisque esset donare. In mea ima loquor, superest vocas densa cognoscere +trepidum inque, restagnantis. Auras est accipiunt erant init turpi tenet isto +voce teretesque facta, quae dederat vultus milite primo. Adspicit ignare +saxumque, tenet fuga male tabuerint umbram *pariterque* nuda vinctumque pugna, +exercita. + +## Et tumida + +Ut abolere turba dignus, pone respondere comis credo moenia et Cragon nondum +pallenti. Urbem Thracum medii praeclusaque vocanti et senemque per? Deo +genuumque pater, in mihi ruborem: aut mutavit terris removi refert atque +indignave veros, in, promittet! Foeda tempora lux Pholus sit Ligurum quis +[cacumen tamen](http://et.io/cepit.html): hanc. + +- Fuisti aptas +- Furibunda arbore passus vulnera quinque Nox menti +- Et gerebat praedae ut duxerat memoranda per +- Nuper Vidi non crines non munusque accusasse +- Trahit opibus vellet rudis + +## Habendi ignibus + +Ille artus, alma deus est vetus, totidem deprensum arbor lacrimaeque? Illis +Canopo subit lucis tradit [ab](http://www.et-flore.com/pars-spirat.php) certis +mortemque seraque in, **o** vestris omine. *Validum* Lapithaeae, ita orbem dum +praesagaque, dictis, iam disci errore coercuit sit modo Hylactor! Rogat +*iacentes et quaeque* fulva, sertis unguibus quoque possis. Suo Rhodopeius +madefient, mitissima despectus diversa stratis. + +1. Diomede aquis +2. Regna mea mota sic usae tu maior +3. Nec hic adunci + +Sed esse, prima picum omnes nam patrii do resedit; petebat sed. Amores auras, +potentia subsidunt auras nec: dicar cum dimotis. Fuit Thestias: quam sed hunc +querella erat in inposita. Patuit nomen multi possum quosque erratica patefecit +laudemur: umbrae praedae locus caecis siquidem et cuncti. + +"""; +} diff --git a/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml new file mode 100644 index 0000000000..83ac2f005b --- /dev/null +++ b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml.cs b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml.cs new file mode 100644 index 0000000000..4f42367d28 --- /dev/null +++ b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererView.axaml.cs @@ -0,0 +1,23 @@ +using System.Reactive.Disposables; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace NexusMods.App.UI.Controls.MarkdownRenderer; + +public partial class MarkdownRendererView : ReactiveUserControl +{ + public MarkdownRendererView() + { + InitializeComponent(); + + this.WhenActivated(disposables => + { + this.OneWayBind(ViewModel, vm => vm.Contents, view => view.MarkdownScrollViewer.Markdown) + .DisposeWith(disposables); + + this.OneWayBind(ViewModel, vm => vm.OpenLinkCommand, view => view.MarkdownScrollViewer.Engine.HyperlinkCommand) + .DisposeWith(disposables); + }); + } +} + diff --git a/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs new file mode 100644 index 0000000000..7d86966ff1 --- /dev/null +++ b/src/NexusMods.App.UI/Controls/MarkdownRenderer/MarkdownRendererViewModel.cs @@ -0,0 +1,27 @@ +using System.Reactive; +using JetBrains.Annotations; +using NexusMods.CrossPlatform.Process; +using ReactiveUI; +using ReactiveUI.Fody.Helpers; + +namespace NexusMods.App.UI.Controls.MarkdownRenderer; + +[UsedImplicitly] +public class MarkdownRendererViewModel : AViewModel, IMarkdownRendererViewModel +{ + [Reactive] public string Contents { get; set; } = string.Empty; + + public ReactiveCommand OpenLinkCommand { get; } + + public MarkdownRendererViewModel(IOSInterop osInterop) + { + OpenLinkCommand = ReactiveCommand.CreateFromTask(async url => + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return; + await Task.Run(() => + { + osInterop.OpenUrl(uri); + }); + }); + } +} diff --git a/src/NexusMods.App.UI/NexusMods.App.UI.csproj b/src/NexusMods.App.UI/NexusMods.App.UI.csproj index ae0e0a2640..ece4dad72e 100644 --- a/src/NexusMods.App.UI/NexusMods.App.UI.csproj +++ b/src/NexusMods.App.UI/NexusMods.App.UI.csproj @@ -487,6 +487,12 @@ IApplyDiffViewModel.cs + + IMarkdownRendererViewModel.cs + + + IMarkdownRendererViewModel.cs + diff --git a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsDesignViewModel.cs b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsDesignViewModel.cs index d6ef77579b..92a68fe902 100644 --- a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsDesignViewModel.cs +++ b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsDesignViewModel.cs @@ -1,17 +1,17 @@ -using System.Reactive; using NexusMods.Abstractions.Diagnostics; +using NexusMods.App.UI.Controls.MarkdownRenderer; using NexusMods.App.UI.Windows; using NexusMods.App.UI.WorkspaceSystem; -using ReactiveUI; namespace NexusMods.App.UI.Pages.Diagnostics; public class DiagnosticDetailsDesignViewModel : APageViewModel, IDiagnosticDetailsViewModel { - public string Details { get; } = "This is an example diagnostic details, lots of stuff here."; - public DiagnosticSeverity Severity { get; } = DiagnosticSeverity.Critical; - + private const string Details = "This is an example diagnostic details, lots of stuff here."; + public DiagnosticSeverity Severity => DiagnosticSeverity.Critical; + + public IMarkdownRendererViewModel MarkdownRendererViewModel => new MarkdownRendererDesignViewModel(Details); + public DiagnosticDetailsDesignViewModel() : base(new DesignWindowManager()) { } - public ReactiveCommand MarkdownOpenLinkCommand { get; } = ReactiveCommand.Create(_ => { }); } diff --git a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsPage.cs b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsPage.cs index 7bca5bc2c5..6cc0a31de3 100644 --- a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsPage.cs +++ b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsPage.cs @@ -1,9 +1,8 @@ using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using NexusMods.Abstractions.Diagnostics; -using NexusMods.App.UI.Windows; +using NexusMods.App.UI.Controls.MarkdownRenderer; using NexusMods.App.UI.WorkspaceSystem; -using NexusMods.CrossPlatform.Process; namespace NexusMods.App.UI.Pages.Diagnostics; @@ -24,9 +23,10 @@ public DiagnosticDetailsPageFactory(IServiceProvider serviceProvider) : base(ser public override IDiagnosticDetailsViewModel CreateViewModel(DiagnosticDetailsPageContext context) { return new DiagnosticDetailsViewModel( - ServiceProvider.GetRequiredService(), - WindowManager, - ServiceProvider.GetRequiredService(), - context.Diagnostic); + WindowManager, + ServiceProvider.GetRequiredService(), + ServiceProvider.GetRequiredService(), + context.Diagnostic + ); } } diff --git a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml index a15fc1c0e5..2281f7f29f 100644 --- a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml +++ b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml @@ -7,8 +7,6 @@ xmlns:reactiveUi="http://reactiveui.net" xmlns:diagnostics="clr-namespace:NexusMods.App.UI.Pages.Diagnostics" xmlns:icons="clr-namespace:NexusMods.Icons;assembly=NexusMods.Icons" - xmlns:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia" - xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="NexusMods.App.UI.Pages.Diagnostics.DiagnosticDetailsView"> @@ -36,55 +34,7 @@ - - - - - - - - - - - - - - - - - - - - + diff --git a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml.cs b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml.cs index d6cc433629..4c050ed6c0 100644 --- a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml.cs +++ b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml.cs @@ -1,13 +1,14 @@ using System.Reactive.Disposables; using System.Reactive.Linq; using Avalonia.ReactiveUI; -using Markdown.Avalonia.Utils; +using JetBrains.Annotations; using NexusMods.Abstractions.Diagnostics; using NexusMods.App.UI.Resources; using ReactiveUI; namespace NexusMods.App.UI.Pages.Diagnostics; +[UsedImplicitly] public partial class DiagnosticDetailsView : ReactiveUserControl { public DiagnosticDetailsView() @@ -27,6 +28,8 @@ public DiagnosticDetailsView() private void InitializeData(IDiagnosticDetailsViewModel vm) { + MarkdownRendererViewModelViewHost.ViewModel = vm.MarkdownRendererViewModel; + switch (vm.Severity) { case DiagnosticSeverity.Suggestion: @@ -55,8 +58,5 @@ private void InitializeData(IDiagnosticDetailsViewModel vm) SeverityTitleTextBlock.Text = Language.DiagnosticDetailsView_SeverityTitle_HIDDEN.ToUpperInvariant(); break; } - - MarkdownScrollViewer.Engine.HyperlinkCommand = vm.MarkdownOpenLinkCommand; - MarkdownScrollViewer.Markdown = vm.Details; } } diff --git a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsViewModel.cs b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsViewModel.cs index 04a1bea8cc..700e72e63d 100644 --- a/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsViewModel.cs +++ b/src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsViewModel.cs @@ -1,9 +1,8 @@ -using System.Reactive; using System.Reactive.Disposables; using NexusMods.Abstractions.Diagnostics; +using NexusMods.App.UI.Controls.MarkdownRenderer; using NexusMods.App.UI.Windows; using NexusMods.App.UI.WorkspaceSystem; -using NexusMods.CrossPlatform.Process; using NexusMods.Icons; using ReactiveUI; @@ -11,32 +10,24 @@ namespace NexusMods.App.UI.Pages.Diagnostics; public class DiagnosticDetailsViewModel : APageViewModel, IDiagnosticDetailsViewModel { - public string Details { get; } public DiagnosticSeverity Severity { get; } - public ReactiveCommand MarkdownOpenLinkCommand { get; } + public IMarkdownRendererViewModel MarkdownRendererViewModel { get; } public DiagnosticDetailsViewModel( - IOSInterop osInterop, IWindowManager windowManager, - IDiagnosticWriter diagnosticWriter, + IDiagnosticWriter diagnosticWriter, + IMarkdownRendererViewModel markdownRendererViewModel, Diagnostic diagnostic) : base(windowManager) { Severity = diagnostic.Severity; var summary = diagnostic.FormatSummary(diagnosticWriter); - Details = $"## {summary}\n" + + var details = $"## {summary}\n" + $"{diagnostic.FormatDetails(diagnosticWriter)}"; - // TODO: once we have custom elements and goto-links, this should be factored out into a singleton handler - MarkdownOpenLinkCommand = ReactiveCommand.CreateFromTask(async url => - { - if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return; - await Task.Run(() => - { - osInterop.OpenUrl(uri); - }); - }); + MarkdownRendererViewModel = markdownRendererViewModel; + MarkdownRendererViewModel.Contents = details; this.WhenActivated(disposable => { diff --git a/src/NexusMods.App.UI/Pages/Diagnostics/Details/IDiagnosticDetailsViewModel.cs b/src/NexusMods.App.UI/Pages/Diagnostics/Details/IDiagnosticDetailsViewModel.cs index 31a26be081..088ea662fa 100644 --- a/src/NexusMods.App.UI/Pages/Diagnostics/Details/IDiagnosticDetailsViewModel.cs +++ b/src/NexusMods.App.UI/Pages/Diagnostics/Details/IDiagnosticDetailsViewModel.cs @@ -1,15 +1,12 @@ -using System.Reactive; using NexusMods.Abstractions.Diagnostics; +using NexusMods.App.UI.Controls.MarkdownRenderer; using NexusMods.App.UI.WorkspaceSystem; -using ReactiveUI; namespace NexusMods.App.UI.Pages.Diagnostics; public interface IDiagnosticDetailsViewModel : IPageViewModelInterface { - string Details { get; } - DiagnosticSeverity Severity { get; } - ReactiveCommand MarkdownOpenLinkCommand { get; } + IMarkdownRendererViewModel MarkdownRendererViewModel { get; } } diff --git a/src/NexusMods.App.UI/Services.cs b/src/NexusMods.App.UI/Services.cs index dfd95c409d..689ba4426a 100644 --- a/src/NexusMods.App.UI/Services.cs +++ b/src/NexusMods.App.UI/Services.cs @@ -14,6 +14,7 @@ using NexusMods.App.UI.Controls.DownloadGrid.Columns.DownloadStatus; using NexusMods.App.UI.Controls.DownloadGrid.Columns.DownloadVersion; using NexusMods.App.UI.Controls.GameWidget; +using NexusMods.App.UI.Controls.MarkdownRenderer; using NexusMods.App.UI.Controls.ModInfo.Error; using NexusMods.App.UI.Controls.ModInfo.Loading; using NexusMods.App.UI.Controls.ModInfo.ModFiles; @@ -182,6 +183,9 @@ public static IServiceCollection AddUI(this IServiceCollection c) .AddView() .AddViewModel() + .AddView() + .AddViewModel() + // workspace system .AddSingleton() .AddViewModel()