Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion StabilityMatrix.Avalonia/App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,11 @@ internal static void ConfigurePageViewModels(IServiceCollection services)
provider.GetRequiredService<OutputsPageViewModel>(),
provider.GetRequiredService<WorkflowsPageViewModel>(),
},
FooterPages = { provider.GetRequiredService<SettingsViewModel>() },
FooterPages =
{
provider.GetRequiredService<DocumentationViewModel>(),
provider.GetRequiredService<SettingsViewModel>(),
},
});
}

Expand Down Expand Up @@ -838,6 +842,15 @@ internal static IServiceCollection ConfigureServices(bool disableMessagePipeInte
})
.AddPolicyHandler(retryPolicy);

services
.AddRefitClient<IGitHubContentApi>(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://api.github.com");
c.Timeout = TimeSpan.FromMinutes(1);
})
.AddPolicyHandler(retryPolicy);

services
.AddRefitClient<IHuggingFaceApi>(defaultRefitSettings) // Assuming defaultRefitSettings is suitable
.ConfigureHttpClient(c =>
Expand Down
80 changes: 80 additions & 0 deletions StabilityMatrix.Avalonia/Controls/DocumentationMarkdownViewer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
using System;
using System.Windows.Input;
using Avalonia;
using Markdown.Avalonia;

namespace StabilityMatrix.Avalonia.Controls;

/// <summary>
/// A <see cref="BetterMarkdownScrollViewer"/> that routes hyperlink clicks through a
/// bindable <see cref="LinkCommand"/> (so relative <c>.md</c> links can navigate in-app
/// and external links can open in the browser) and resolves relative image paths against
/// <see cref="ImageBaseUrl"/> via the engine's asset path root.
/// </summary>
public class DocumentationMarkdownViewer : BetterMarkdownScrollViewer
{
/// <summary>
/// Command invoked when a hyperlink is clicked. The command parameter is the raw href string.
/// </summary>
public static readonly StyledProperty<ICommand?> LinkCommandProperty = AvaloniaProperty.Register<
DocumentationMarkdownViewer,
ICommand?
>(nameof(LinkCommand));

/// <summary>
/// Base URL used to resolve relative image paths in the rendered markdown
/// (e.g. the raw URL of the current page's folder).
/// </summary>
public static readonly StyledProperty<string?> ImageBaseUrlProperty = AvaloniaProperty.Register<
DocumentationMarkdownViewer,
string?
>(nameof(ImageBaseUrl));

public ICommand? LinkCommand
{
get => GetValue(LinkCommandProperty);
set => SetValue(LinkCommandProperty, value);
}

public string? ImageBaseUrl
{
get => GetValue(ImageBaseUrlProperty);
set => SetValue(ImageBaseUrlProperty, value);
}

public DocumentationMarkdownViewer()
{
ApplyLinkCommand();
ApplyImageBaseUrl();
}

protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);

if (change.Property == LinkCommandProperty)
{
ApplyLinkCommand();
}
else if (change.Property == ImageBaseUrlProperty)
{
ApplyImageBaseUrl();
}
Comment on lines +55 to +62

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The Engine property of MarkdownScrollViewer is often initialized or changed after the control's constructor runs (e.g., during template application or when custom styles are applied). Since ApplyLinkCommand is only called in the constructor and when LinkCommandProperty changes, the custom LinkCommand will never be applied to the engine if the engine is set after construction. Listening to changes on the Engine property ensures the command is always correctly bound.

        if (change.Property == LinkCommandProperty || change.Property.Name == "Engine")
        {
            ApplyLinkCommand();
        }
        else if (change.Property == ImageBaseUrlProperty)
        {
            ApplyImageBaseUrl();
        }

}

private void ApplyLinkCommand()
{
// The engine (IMarkdownEngine) owns the HyperlinkCommand used for all rendered links.
if (Engine is IMarkdownEngine engine)
{
engine.HyperlinkCommand = LinkCommand;
}
}

private void ApplyImageBaseUrl()
{
// AssetPathRoot flows through to the engine's bitmap loader so relative image
// paths resolve against the raw docs URL.
AssetPathRoot = ImageBaseUrl ?? string.Empty;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using CommunityToolkit.Mvvm.ComponentModel;

namespace StabilityMatrix.Avalonia.ViewModels.Documentation;

/// <summary>
/// A single navigable documentation page entry in the sidebar.
/// </summary>
public partial class DocumentationPageNavItem : ObservableObject
{
/// <summary>Display title, e.g. "Overview".</summary>
public required string Title { get; init; }

/// <summary>Path relative to the docs root, e.g. <c>getting-started/overview.md</c>.</summary>
public required string Path { get; init; }

[ObservableProperty]
private bool isSelected;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Collections.Generic;

namespace StabilityMatrix.Avalonia.ViewModels.Documentation;

/// <summary>
/// A section grouping in the documentation sidebar (e.g. "Getting Started").
/// </summary>
public class DocumentationSectionNavItem
{
/// <summary>Section title. Empty for the root section (renders without a header).</summary>
public required string Title { get; init; }

/// <summary>Whether this section has a visible header (i.e. is not the root section).</summary>
public bool HasHeader => !string.IsNullOrEmpty(Title);

/// <summary>Pages within this section.</summary>
public required IReadOnlyList<DocumentationPageNavItem> Pages { get; init; }
}
Loading
Loading