Skip to content

Commit 9f39176

Browse files
authored
Add markdown renderer component (#1230)
* Add MarkdownRenderer component * FirstOrDefault -> FirstOrOptional * Update DiagnosticDetails
1 parent 0f8d2e9 commit 9f39176

14 files changed

+252
-89
lines changed

src/Abstractions/NexusMods.Abstractions.Loadouts/Extensions/LoadoutExtensions.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ public static Optional<ValueTuple<Mod, T>> GetFirstModWithMetadata<T>(
5757
this Loadout loadout,
5858
bool onlyEnabledMods = true) where T : AModMetadata
5959
{
60-
return loadout.GetModsWithMetadata<T>(onlyEnabledMods).FirstOrDefault();
60+
return loadout.GetModsWithMetadata<T>(onlyEnabledMods).FirstOrOptional(_ => true);
6161
}
6262
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using System.Reactive;
2+
using ReactiveUI;
3+
4+
namespace NexusMods.App.UI.Controls.MarkdownRenderer;
5+
6+
public interface IMarkdownRendererViewModel : IViewModelInterface
7+
{
8+
/// <summary>
9+
/// Gets or sets the contents of the renderer.
10+
/// </summary>
11+
public string Contents { get; set; }
12+
13+
/// <summary>
14+
/// Gets the command used for opening links from Markdown.
15+
/// </summary>
16+
public ReactiveCommand<string, Unit> OpenLinkCommand { get; }
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Reactive;
2+
using JetBrains.Annotations;
3+
using ReactiveUI;
4+
5+
namespace NexusMods.App.UI.Controls.MarkdownRenderer;
6+
7+
public class MarkdownRendererDesignViewModel : AViewModel<IMarkdownRendererViewModel>, IMarkdownRendererViewModel
8+
{
9+
public string Contents { get; set; }
10+
11+
public ReactiveCommand<string, Unit> OpenLinkCommand { get; } = ReactiveCommand.Create<string>(_ => { });
12+
13+
public MarkdownRendererDesignViewModel() : this(DefaultContents) { }
14+
15+
public MarkdownRendererDesignViewModel(string contents)
16+
{
17+
Contents = contents;
18+
}
19+
20+
// From https://jaspervdj.be/lorem-markdownum/
21+
[LanguageInjection("markdown")]
22+
private const string DefaultContents =
23+
"""
24+
# Quid nostro
25+
26+
## Velox tot frugum accipe
27+
28+
Lorem markdownum sacros si Iovis aquarum, oreris bene inmurmurat arborei
29+
propulsa labori invidiosa, sic tibi sic: tumulis. Pectoris ait plectrum fregit
30+
aegram. Cum *legit urit* nec solet corpora loquebatur cur, in vivaque quasque
31+
corpus **sagittas Numam** Lucifer mentesque, **falsa**. Vult plagis sospite
32+
veneni prodiga ratione, currus malus! Et sinat mersaeque fletque cycnus
33+
auxiliaris, sum **quid frondentis** sensit.
34+
35+
1. Moneo ora equos per monstro o foedera
36+
2. Confusura veneni lacertis pisce inmedicabile quid tenuaverat
37+
3. Ardere una quam paciscor alimenta liber
38+
4. Captivarum Venus
39+
5. Nunc iphis Orion aethera genitore doleas pro
40+
41+
Insula illa optime in admonita exigit, clausit sua aut paelicis potiunda ipsius
42+
canes falsisque esset donare. In mea ima loquor, superest vocas densa cognoscere
43+
trepidum inque, restagnantis. Auras est accipiunt erant init turpi tenet isto
44+
voce teretesque facta, quae dederat vultus milite primo. Adspicit ignare
45+
saxumque, tenet fuga male tabuerint umbram *pariterque* nuda vinctumque pugna,
46+
exercita.
47+
48+
## Et tumida
49+
50+
Ut abolere turba dignus, pone respondere comis credo moenia et Cragon nondum
51+
pallenti. Urbem Thracum medii praeclusaque vocanti et senemque per? Deo
52+
genuumque pater, in mihi ruborem: aut mutavit terris removi refert atque
53+
indignave veros, in, promittet! Foeda tempora lux Pholus sit Ligurum quis
54+
[cacumen tamen](http://et.io/cepit.html): hanc.
55+
56+
- Fuisti aptas
57+
- Furibunda arbore passus vulnera quinque Nox menti
58+
- Et gerebat praedae ut duxerat memoranda per
59+
- Nuper Vidi non crines non munusque accusasse
60+
- Trahit opibus vellet rudis
61+
62+
## Habendi ignibus
63+
64+
Ille artus, alma deus est vetus, totidem deprensum arbor lacrimaeque? Illis
65+
Canopo subit lucis tradit [ab](http://www.et-flore.com/pars-spirat.php) certis
66+
mortemque seraque in, **o** vestris omine. *Validum* Lapithaeae, ita orbem dum
67+
praesagaque, dictis, iam disci errore coercuit sit modo Hylactor! Rogat
68+
*iacentes et quaeque* fulva, sertis unguibus quoque possis. Suo Rhodopeius
69+
madefient, mitissima despectus diversa stratis.
70+
71+
1. Diomede aquis
72+
2. Regna mea mota sic usae tu maior
73+
3. Nec hic adunci
74+
75+
Sed esse, prima picum omnes nam patrii do resedit; petebat sed. Amores auras,
76+
potentia subsidunt auras nec: dicar cum dimotis. Fuit Thestias: quam sed hunc
77+
querella erat in inposita. Patuit nomen multi possum quosque erratica patefecit
78+
laudemur: umbrae praedae locus caecis siquidem et cuncti.
79+
80+
""";
81+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
<reactive:ReactiveUserControl
2+
x:TypeArguments="local:IMarkdownRendererViewModel"
3+
xmlns="https://github.com/avaloniaui"
4+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
5+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
6+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
7+
xmlns:reactive="http://reactiveui.net"
8+
xmlns:local="clr-namespace:NexusMods.App.UI.Controls.MarkdownRenderer"
9+
xmlns:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
10+
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
11+
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
12+
x:Class="NexusMods.App.UI.Controls.MarkdownRenderer.MarkdownRendererView">
13+
14+
<Design.DataContext>
15+
<local:MarkdownRendererDesignViewModel />
16+
</Design.DataContext>
17+
18+
<!--
19+
NOTE(erri120): This control can only be styled directly, not through classes.
20+
https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-customise-style
21+
-->
22+
<md:MarkdownScrollViewer x:Name="MarkdownScrollViewer">
23+
<md:MarkdownScrollViewer.Styles>
24+
<Style Selector="ctxt|CTextBlock">
25+
<Setter Property="FontFamily" Value="{StaticResource FontBodyRegular}" />
26+
<Setter Property="Foreground" Value="{StaticResource NeutralSubduedBrush}" />
27+
</Style>
28+
29+
<Style Selector="ctxt|CHyperlink">
30+
<Setter Property="FontFamily" Value="{StaticResource FontBodyRegular}" />
31+
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
32+
</Style>
33+
34+
<Style Selector="ctxt|CTextBlock.Heading1">
35+
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
36+
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
37+
</Style>
38+
39+
<Style Selector="ctxt|CTextBlock.Heading2">
40+
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
41+
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
42+
</Style>
43+
44+
<Style Selector="ctxt|CTextBlock.Heading3">
45+
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
46+
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
47+
</Style>
48+
49+
<Style Selector="ctxt|CTextBlock.Heading4">
50+
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
51+
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
52+
</Style>
53+
54+
<Style Selector="ctxt|CTextBlock.Heading5">
55+
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
56+
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
57+
</Style>
58+
59+
<Style Selector="ctxt|CTextBlock.Heading6">
60+
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
61+
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
62+
</Style>
63+
</md:MarkdownScrollViewer.Styles>
64+
</md:MarkdownScrollViewer>
65+
66+
</reactive:ReactiveUserControl>
67+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
using System.Reactive.Disposables;
2+
using Avalonia.ReactiveUI;
3+
using ReactiveUI;
4+
5+
namespace NexusMods.App.UI.Controls.MarkdownRenderer;
6+
7+
public partial class MarkdownRendererView : ReactiveUserControl<IMarkdownRendererViewModel>
8+
{
9+
public MarkdownRendererView()
10+
{
11+
InitializeComponent();
12+
13+
this.WhenActivated(disposables =>
14+
{
15+
this.OneWayBind(ViewModel, vm => vm.Contents, view => view.MarkdownScrollViewer.Markdown)
16+
.DisposeWith(disposables);
17+
18+
this.OneWayBind(ViewModel, vm => vm.OpenLinkCommand, view => view.MarkdownScrollViewer.Engine.HyperlinkCommand)
19+
.DisposeWith(disposables);
20+
});
21+
}
22+
}
23+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.Reactive;
2+
using JetBrains.Annotations;
3+
using NexusMods.CrossPlatform.Process;
4+
using ReactiveUI;
5+
using ReactiveUI.Fody.Helpers;
6+
7+
namespace NexusMods.App.UI.Controls.MarkdownRenderer;
8+
9+
[UsedImplicitly]
10+
public class MarkdownRendererViewModel : AViewModel<IMarkdownRendererViewModel>, IMarkdownRendererViewModel
11+
{
12+
[Reactive] public string Contents { get; set; } = string.Empty;
13+
14+
public ReactiveCommand<string, Unit> OpenLinkCommand { get; }
15+
16+
public MarkdownRendererViewModel(IOSInterop osInterop)
17+
{
18+
OpenLinkCommand = ReactiveCommand.CreateFromTask<string>(async url =>
19+
{
20+
if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) return;
21+
await Task.Run(() =>
22+
{
23+
osInterop.OpenUrl(uri);
24+
});
25+
});
26+
}
27+
}

src/NexusMods.App.UI/NexusMods.App.UI.csproj

+6
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,12 @@
487487
<Compile Update="Pages\Diff\ApplyDiff\ApplyDiffDesignViewModel.cs">
488488
<DependentUpon>IApplyDiffViewModel.cs</DependentUpon>
489489
</Compile>
490+
<Compile Update="Controls\MarkdownRenderer\MarkdownRendererViewModel.cs">
491+
<DependentUpon>IMarkdownRendererViewModel.cs</DependentUpon>
492+
</Compile>
493+
<Compile Update="Controls\MarkdownRenderer\MarkdownRendererDesignViewModel.cs">
494+
<DependentUpon>IMarkdownRendererViewModel.cs</DependentUpon>
495+
</Compile>
490496
</ItemGroup>
491497

492498
<ItemGroup>
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
1-
using System.Reactive;
21
using NexusMods.Abstractions.Diagnostics;
2+
using NexusMods.App.UI.Controls.MarkdownRenderer;
33
using NexusMods.App.UI.Windows;
44
using NexusMods.App.UI.WorkspaceSystem;
5-
using ReactiveUI;
65

76
namespace NexusMods.App.UI.Pages.Diagnostics;
87

98
public class DiagnosticDetailsDesignViewModel : APageViewModel<IDiagnosticDetailsViewModel>, IDiagnosticDetailsViewModel
109
{
11-
public string Details { get; } = "This is an example diagnostic details, lots of stuff here.";
12-
public DiagnosticSeverity Severity { get; } = DiagnosticSeverity.Critical;
13-
10+
private const string Details = "This is an example diagnostic details, lots of stuff here.";
11+
public DiagnosticSeverity Severity => DiagnosticSeverity.Critical;
12+
13+
public IMarkdownRendererViewModel MarkdownRendererViewModel => new MarkdownRendererDesignViewModel(Details);
14+
1415
public DiagnosticDetailsDesignViewModel() : base(new DesignWindowManager()) { }
1516

16-
public ReactiveCommand<string, Unit> MarkdownOpenLinkCommand { get; } = ReactiveCommand.Create<string>(_ => { });
1717
}

src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsPage.cs

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
using JetBrains.Annotations;
22
using Microsoft.Extensions.DependencyInjection;
33
using NexusMods.Abstractions.Diagnostics;
4-
using NexusMods.App.UI.Windows;
4+
using NexusMods.App.UI.Controls.MarkdownRenderer;
55
using NexusMods.App.UI.WorkspaceSystem;
6-
using NexusMods.CrossPlatform.Process;
76

87
namespace NexusMods.App.UI.Pages.Diagnostics;
98

@@ -24,9 +23,10 @@ public DiagnosticDetailsPageFactory(IServiceProvider serviceProvider) : base(ser
2423
public override IDiagnosticDetailsViewModel CreateViewModel(DiagnosticDetailsPageContext context)
2524
{
2625
return new DiagnosticDetailsViewModel(
27-
ServiceProvider.GetRequiredService<IOSInterop>(),
28-
WindowManager,
29-
ServiceProvider.GetRequiredService<IDiagnosticWriter>(),
30-
context.Diagnostic);
26+
WindowManager,
27+
ServiceProvider.GetRequiredService<IDiagnosticWriter>(),
28+
ServiceProvider.GetRequiredService<IMarkdownRendererViewModel>(),
29+
context.Diagnostic
30+
);
3131
}
3232
}

src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml

+1-51
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@
77
xmlns:reactiveUi="http://reactiveui.net"
88
xmlns:diagnostics="clr-namespace:NexusMods.App.UI.Pages.Diagnostics"
99
xmlns:icons="clr-namespace:NexusMods.Icons;assembly=NexusMods.Icons"
10-
xmlns:md="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
11-
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
1210
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
1311
x:Class="NexusMods.App.UI.Pages.Diagnostics.DiagnosticDetailsView">
1412

@@ -36,55 +34,7 @@
3634

3735
<Border x:Name="HorizontalLine" Height="1" />
3836

39-
<!--
40-
NOTE(erri120): This control can only be styled directly, not through classes.
41-
If this needs to be re-used, make a wrapper control out of it.
42-
43-
https://github.com/whistyun/Markdown.Avalonia/wiki/How-to-customise-style
44-
-->
45-
<md:MarkdownScrollViewer x:Name="MarkdownScrollViewer">
46-
<md:MarkdownScrollViewer.Styles>
47-
<Style Selector="ctxt|CTextBlock">
48-
<Setter Property="FontFamily" Value="{StaticResource FontBodyRegular}" />
49-
<Setter Property="Foreground" Value="{StaticResource NeutralSubduedBrush}" />
50-
</Style>
51-
52-
<Style Selector="ctxt|CHyperlink">
53-
<Setter Property="FontFamily" Value="{StaticResource FontBodyRegular}" />
54-
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
55-
</Style>
56-
57-
<Style Selector="ctxt|CTextBlock.Heading1">
58-
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
59-
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
60-
</Style>
61-
62-
<Style Selector="ctxt|CTextBlock.Heading2">
63-
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
64-
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
65-
</Style>
66-
67-
<Style Selector="ctxt|CTextBlock.Heading3">
68-
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
69-
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
70-
</Style>
71-
72-
<Style Selector="ctxt|CTextBlock.Heading4">
73-
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
74-
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
75-
</Style>
76-
77-
<Style Selector="ctxt|CTextBlock.Heading5">
78-
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
79-
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
80-
</Style>
81-
82-
<Style Selector="ctxt|CTextBlock.Heading6">
83-
<Setter Property="FontFamily" Value="{StaticResource FontHeadlinesSemiBold}" />
84-
<Setter Property="Foreground" Value="{StaticResource NeutralModerateBrush}" />
85-
</Style>
86-
</md:MarkdownScrollViewer.Styles>
87-
</md:MarkdownScrollViewer>
37+
<reactiveUi:ViewModelViewHost x:Name="MarkdownRendererViewModelViewHost"/>
8838
</StackPanel>
8939
</Border>
9040

src/NexusMods.App.UI/Pages/Diagnostics/Details/DiagnosticDetailsView.axaml.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
using System.Reactive.Disposables;
22
using System.Reactive.Linq;
33
using Avalonia.ReactiveUI;
4-
using Markdown.Avalonia.Utils;
4+
using JetBrains.Annotations;
55
using NexusMods.Abstractions.Diagnostics;
66
using NexusMods.App.UI.Resources;
77
using ReactiveUI;
88

99
namespace NexusMods.App.UI.Pages.Diagnostics;
1010

11+
[UsedImplicitly]
1112
public partial class DiagnosticDetailsView : ReactiveUserControl<IDiagnosticDetailsViewModel>
1213
{
1314
public DiagnosticDetailsView()
@@ -27,6 +28,8 @@ public DiagnosticDetailsView()
2728

2829
private void InitializeData(IDiagnosticDetailsViewModel vm)
2930
{
31+
MarkdownRendererViewModelViewHost.ViewModel = vm.MarkdownRendererViewModel;
32+
3033
switch (vm.Severity)
3134
{
3235
case DiagnosticSeverity.Suggestion:
@@ -55,8 +58,5 @@ private void InitializeData(IDiagnosticDetailsViewModel vm)
5558
SeverityTitleTextBlock.Text = Language.DiagnosticDetailsView_SeverityTitle_HIDDEN.ToUpperInvariant();
5659
break;
5760
}
58-
59-
MarkdownScrollViewer.Engine.HyperlinkCommand = vm.MarkdownOpenLinkCommand;
60-
MarkdownScrollViewer.Markdown = vm.Details;
6161
}
6262
}

0 commit comments

Comments
 (0)