Wiki view (Closes #1118) (#1134)

* Setup wiki files deploy

* Create wiki view (Closes #1118)

---------

Co-authored-by: Alexandru Macocian <amacocian@microsoft.com>
This commit is contained in:
2025-12-12 15:13:35 +01:00
parent de94b93f2d
commit fc177a3007
14 changed files with 374 additions and 4 deletions
+1
View File
@@ -40,6 +40,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Get Latest Tag
id: getLatestTag
+1
View File
@@ -35,6 +35,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install .NET Core
uses: actions/setup-dotnet@v5
+2
View File
@@ -365,3 +365,5 @@ MigrationBackup/
FodyWeavers.xsd
.idea
Daybreak/wwwroot/wiki/*
@@ -329,6 +329,7 @@ public class ProjectConfiguration : PluginConfigurationBase
viewProducer.RegisterView<ScreenSelectorView, ScreenSelectorViewModel>();
viewProducer.RegisterView<UModManagementView, UModManagementViewModel>();
viewProducer.RegisterView<TradeNotificationView, TradeNotificationViewModel>();
viewProducer.RegisterView<WikiView, WikiViewModel>();
}
public override void RegisterStartupActions(IStartupActionProducer startupActionProducer)
@@ -425,8 +426,9 @@ public class ProjectConfiguration : PluginConfigurationBase
menuServiceProducer.CreateIfNotExistCategory("Daybreak")
//TODO: Implement Notifications view
//.RegisterButton("Notifications", "Open notifications view", sp => { })
.RegisterButton("Manage Plugins", "Open plugins view", sp => sp.GetRequiredService<ViewManager>().ShowView<PluginsView>())
.RegisterButton("Manage version", "Open version manager", sp => sp.GetRequiredService<ViewManager>().ShowView<VersionManagementView>());
.RegisterButton("Manage Plugins", "Open plugins view", sp => sp.GetRequiredService<IViewManager>().ShowView<PluginsView>())
.RegisterButton("Manage version", "Open version manager", sp => sp.GetRequiredService<IViewManager>().ShowView<VersionManagementView>())
.RegisterButton("Help", "Open Daybreak wiki", sp => sp.GetRequiredService<IViewManager>().ShowView<WikiView>((nameof(WikiView.Page), "Home")));
menuServiceProducer.CreateIfNotExistCategory("Trade")
.RegisterButton("Alerts", "Open trade alerts manager", sp => sp.GetRequiredService<IViewManager>().ShowView<TradeAlertsView>())
.RegisterButton("Kamadan", "Open kamadan trade chat", sp => sp.GetRequiredService<IViewManager>().ShowView<TradeChatView>((nameof(TradeChatView.Source), nameof(TraderSource.Kamadan))))
+16 -1
View File
@@ -84,10 +84,24 @@
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" />
<PackageReference Include="NAudio" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Daybreak.Shared\Daybreak.Shared.csproj" />
</ItemGroup>
<!-- Copy Daybreak.wiki to wwwroot/wiki for both Debug and Release builds -->
<PropertyGroup>
<WikiSourcePath>..\Daybreak.wiki</WikiSourcePath>
</PropertyGroup>
<ItemGroup>
<WikiFiles Include="$(WikiSourcePath)\**\*.*" Exclude="$(WikiSourcePath)\.git\**" />
</ItemGroup>
<Target Name="CopyWikiToWwwroot" BeforeTargets="BeforeBuild">
<Message Importance="high" Text="📚 Copying Daybreak.wiki to wwwroot/wiki" />
<Copy SourceFiles="@(WikiFiles)" DestinationFiles="@(WikiFiles->'wwwroot\wiki\%(RecursiveDir)%(Filename)%(Extension)')" SkipUnchangedFiles="true" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuildEvent" Condition="'$(Configuration)' == 'Release'">
<Exec Command="echo.&gt;$(Version).version" />
@@ -110,6 +124,7 @@
<UpToDateCheckInput Include="..\Daybreak.API\**\*.cs" />
<UpToDateCheckInput Include="..\Daybreak.7ZipExtractor\**\*.cs" />
<UpToDateCheckInput Include="..\Daybreak.Installer\**\*.cs" />
<UpToDateCheckInput Include="..\Daybreak.wiki\**\*.*" Exclude="..\Daybreak.wiki\.git\**" />
</ItemGroup>
<ItemGroup>
+23
View File
@@ -0,0 +1,23 @@
using System.IO;
namespace Daybreak.Utils;
public static class WebRootUtil
{
public static string GetWebRootPath()
{
var releasePath = Path.Combine(AppContext.BaseDirectory, "wwwroot");
if (Directory.Exists(releasePath))
{
return releasePath;
}
var debugPath = Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..", "wwwroot");
if (Directory.Exists(debugPath))
{
return debugPath;
}
throw new InvalidOperationException("Could not find web root folder");
}
}
+10
View File
@@ -2,6 +2,7 @@
using System.Diagnostics;
using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;
using Daybreak.Services.Logging;
using Daybreak.Services.Notifications.Handlers;
@@ -104,6 +105,15 @@ public sealed class AppViewModel
this.CloseNavigationMenu,
this.ToggleNavigationMenu
);
this.blazorHostWindow.PreviewKeyUp += this.BlazorHostWindow_PreviewKeyDown;
}
private void BlazorHostWindow_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key is Key.F1)
{
this.viewManager.ShowView<WikiView>((nameof(WikiView.Page), "Home"));
}
}
public async ValueTask InitializeApp(IJSRuntime jsRuntime)
+29
View File
@@ -0,0 +1,29 @@
<div class="wiki-container">
<nav class="wiki-toc backdrop-panel">
<h3>Contents</h3>
<ul>
@foreach (var p in this.ViewModel.Pages)
{
<li class="@(p == this.ViewModel.CurrentPage ? "active" : "")">
<a href="/wiki/@p">@p</a>
</li>
}
</ul>
</nav>
<div id="wiki-content" class="wiki-content backdrop-panel"></div>
</div>
@page "/wiki/{Page}"
@inherits ViewBase<WikiView, WikiViewModel>
@inject IJSRuntime JSRuntime
@code {
[Parameter]
public string? Page { get; set; }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
var filePath = $"wiki/{this.Page}.md";
await this.JSRuntime.InvokeVoidAsync("wikiRenderer.render", filePath, "wiki-content");
}
}
+28
View File
@@ -0,0 +1,28 @@
using Daybreak.Utils;
using System.Extensions;
using System.IO;
using TrailBlazr.ViewModels;
namespace Daybreak.Views;
public sealed class WikiViewModel : ViewModelBase<WikiViewModel, WikiView>
{
public string? CurrentPage { get; private set; }
public List<string> Pages { get; } = [];
public override ValueTask ParametersSet(WikiView view, CancellationToken cancellationToken)
{
this.CurrentPage = view.Page;
this.Pages.ClearAnd().AddRange(LoadPages());
return base.ParametersSet(view, cancellationToken);
}
private static IEnumerable<string> LoadPages()
{
var webRoot = WebRootUtil.GetWebRootPath();
var wikiPath = Path.Combine(webRoot, "wiki");
return Directory.EnumerateFiles(wikiPath, "*.md")
.Select(Path.GetFileNameWithoutExtension)
.OfType<string>();
}
}
+93
View File
@@ -0,0 +1,93 @@
.wiki-container {
display: flex;
height: 100%;
overflow: hidden;
}
.wiki-toc {
order: 1;
width: 220px;
min-width: 220px;
padding: 1rem;
overflow-y: auto;
border-left: 1px solid var(--neutral-stroke-rest);
}
.wiki-toc h3 {
margin: 0 0 1rem 0;
font-size: 1.1rem;
color: var(--neutral-foreground-rest);
}
.wiki-toc ul {
list-style: none;
margin: 0;
padding: 0;
}
.wiki-toc li {
margin: 0.25rem 0;
}
.wiki-toc li a {
display: block;
padding: 0.4rem 0.6rem;
color: var(--neutral-foreground-rest);
text-decoration: none;
border-radius: 4px;
transition: background-color 0.15s ease;
}
.wiki-toc li a:hover {
background: var(--neutral-fill-rest);
}
.wiki-toc li.active a {
background: var(--accent-fill-rest);
color: var(--accent-foreground-cut-rest);
}
.wiki-content {
flex: 1;
overflow-y: auto;
color: var(--neutral-foreground-rest);
line-height: 1.6;
padding: 0px 1rem 0px 1rem;
}
.wiki-content ::deep h1,
.wiki-content ::deep h2,
.wiki-content ::deep h3 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.wiki-content ::deep h1 {
font-size: 2rem;
border-bottom: 1px solid var(--neutral-stroke-rest);
padding-bottom: 0.5rem;
}
.wiki-content ::deep a {
color: var(--accent-fill-rest);
}
.wiki-content ::deep code {
background: var(--neutral-fill-rest);
padding: 0.2rem 0.4rem;
border-radius: 4px;
}
.wiki-content ::deep pre {
background: var(--neutral-fill-rest);
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
}
.wiki-content ::deep blockquote {
border-left: 4px solid var(--accent-fill-rest);
margin: 1rem 0;
padding: 0.5rem 1rem;
background: var(--neutral-fill-rest);
}
+2
View File
@@ -13,6 +13,8 @@
<body>
<div id="app"></div>
<script src="_framework/blazor.webview.js"></script>
<script src="deps/marked.js/15.0.12/marked.min.js"></script>
<script src="js/wiki-renderer.js"></script>
</body>
</html>
File diff suppressed because one or more lines are too long
+95
View File
@@ -0,0 +1,95 @@
window.wikiRenderer = {
render: async function (filePath, targetId) {
const element = document.getElementById(targetId);
if (!element) {
console.error('wiki-renderer: Element not found:', targetId);
return;
}
if (typeof marked === 'undefined') {
console.error('wiki-renderer: marked.js not loaded');
element.innerHTML = '<p>Markdown renderer not loaded.</p>';
return;
}
try {
const response = await fetch(filePath);
if (!response.ok) {
element.innerHTML = '<p>Page not found.</p>';
return;
}
const markdown = await response.text();
let html = marked.parse(markdown);
// Rewrite relative wiki links to local wiki paths
html = this.rewriteWikiLinks(html);
// Rewrite relative image paths to absolute wiki paths
html = this.rewriteImagePaths(html);
element.innerHTML = html;
// Add IDs to headings for anchor navigation
this.addHeadingIds(element);
// Add click handler for anchor links to prevent Blazor interception
this.setupAnchorLinks(element);
} catch (error) {
console.error('wiki-renderer: Failed to load markdown:', error);
element.innerHTML = '<p>Failed to load page.</p>';
}
},
addHeadingIds: function (container) {
const headings = container.querySelectorAll('h1, h2, h3, h4');
headings.forEach(heading => {
if (!heading.id) {
// Generate ID from heading text (same logic as typical markdown parsers)
const id = heading.textContent
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/\s+/g, '-');
heading.id = id;
}
});
},
setupAnchorLinks: function (container) {
const anchorLinks = container.querySelectorAll('a[href^="#"]');
anchorLinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const targetId = link.getAttribute('href').substring(1);
const targetElement = document.getElementById(targetId);
if (targetElement) {
targetElement.scrollIntoView({ behavior: 'smooth' });
}
});
});
},
rewriteWikiLinks: function (html) {
// Handle relative wiki links (e.g., href="PageName" or href="PageName.md")
// Convert to /wiki/PageName (stripping .md extension if present)
const relativeWikiPattern = /href="(?!http|\/|#)([^"]+)"/g;
html = html.replace(relativeWikiPattern, (match, pageName) => {
// Remove .md extension if present
const cleanName = pageName.replace(/\.md$/i, '');
return `href="/wiki/${cleanName}"`;
});
return html;
},
rewriteImagePaths: function (html) {
// Rewrite relative image src paths to absolute wiki paths
// src="assets/image.png" -> src="/wiki/assets/image.png"
const relativeImagePattern = /src="(?!http|\/|data:)([^"]+)"/g;
html = html.replace(relativeImagePattern, 'src="/wiki/$1"');
return html;
}
};