Implement text/html processor

This commit is contained in:
Stone_Red
2024-04-04 14:46:02 +02:00
parent 942e58ac0f
commit bb78a4a067
14 changed files with 327 additions and 174 deletions
+1 -2
View File
@@ -7,5 +7,4 @@ using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Minor Code Smell", "S1075:URIs should not be hardcoded", Justification = "<Pending>")]
[assembly: SuppressMessage("Minor Code Smell", "CS0067:The event 'ChangeThemeCommand.CanExecuteChanged' is never used", Justification = "<Pending>", Scope = "member", Target = "~N:Markdowser.Commands")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "<Pending>")]
[assembly: SuppressMessage("Minor Code Smell", "S3267:Loops should be simplified with \"LINQ\" expressions", Justification = "<Pending>", Scope = "member", Target = "~M:Markdowser.Processing.ContentProcessorManager.ProcessContent(System.Net.HttpWebResponse,System.IProgress{Markdowser.Processing.ProcessingProgress})~Markdowser.ViewModels.ContentViewModelBase")]
[assembly: SuppressMessage("Performance", "CA1822:Mark members as static", Justification = "<Pending>")]
+3 -2
View File
@@ -39,7 +39,8 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Processing\Processors\" />
<Folder Include="Views\Content\" />
<Compile Update="Views\Content\MarkdownContentView.axaml.cs">
<DependentUpon>MarkdownContentView.axaml</DependentUpon>
</Compile>
</ItemGroup>
</Project>
@@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
namespace Markdowser.Processing;
@@ -16,9 +16,9 @@ internal class ContentProcessorManager
processors.Add(processor);
}
public Task<ContentViewModelBase> ProcessContent(HttpWebResponse response, IProgress<ProcessingProgress> progress)
public Task<ContentViewModelBase> ProcessContent(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress)
{
IContentProcessor? processor = processors.Find(processor => processor.CanProcess(response)) ?? throw new InvalidOperationException("No processor found for the given response.");
return processor.Process(response, progress);
IContentProcessor? processor = processors.Find(processor => processor.CanProcess(httpResponseMessage.Content.Headers)) ?? throw new InvalidOperationException($"No processor found for content type {httpResponseMessage.Content.Headers.ContentType}");
return processor.Process(httpResponseMessage, progress);
}
}
@@ -1,14 +1,15 @@
using Markdowser.ViewModels;
using System;
using System.Net;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Markdowser.Processing;
internal interface IContentProcessor
{
bool CanProcess(HttpWebResponse response);
bool CanProcess(HttpContentHeaders httpContentHeaders);
Task<ContentViewModelBase> Process(HttpWebResponse response, IProgress<ProcessingProgress> progress);
Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress);
}
@@ -1,9 +1,10 @@
namespace Markdowser.Processing;
public readonly struct ProcessingProgress(int current, int total)
public readonly struct ProcessingProgress(long current, long total, string? message = null)
{
public int Current { get; } = current;
public int Total { get; } = total;
public long Current { get; } = current;
public long Total { get; } = total;
public string? Message { get; } = message;
public double Percentage => (double)Current / Total * 100;
public int Percentage => (int)((double)Total / Current * 100);
}
@@ -0,0 +1,94 @@
using HtmlAgilityPack;
using Markdowser.ViewModels;
using Markdowser.ViewModels.Content;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace Markdowser.Processing.Processors;
internal partial class HtmlProcessor : IContentProcessor
{
private readonly ReverseMarkdown.Converter markdownConverter = new();
public HtmlProcessor()
{
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
markdownConverter.Config.SuppressDivNewlines = false;
markdownConverter.Config.SmartHrefHandling = false;
}
public bool CanProcess(HttpContentHeaders httpContentHeaders)
{
return httpContentHeaders.ContentType?.MediaType == "text/html";
}
public async Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress)
{
StringBuilder html = new();
StringBuilder finalMarkdown = new();
long length = httpResponseMessage.Content.Headers.ContentLength ?? 0;
using Stream contentStream = await httpResponseMessage.Content.ReadAsStreamAsync();
using StreamReader streamReader = new(contentStream);
while (!streamReader.EndOfStream)
{
_ = html.AppendLine(await streamReader.ReadLineAsync());
progress.Report(new ProcessingProgress(length, contentStream.Position));
}
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html.ToString());
string title = htmlDoc.DocumentNode.SelectSingleNode("html/head/title")?.InnerText?.Trim() ?? httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? "Untitled";
Debug.WriteLine("Converting HTML to markdown...");
string markdown = markdownConverter.Convert(html.ToString());
Debug.WriteLine("Processing markdown...");
int currentLine = 0;
string[] lines = markdown.Split('\n');
foreach (string line in lines)
{
progress.Report(new ProcessingProgress(lines.Length, currentLine));
currentLine++;
string trimmedLine = line.Trim();
if (trimmedLine.All(c => c == '#'))
{
_ = finalMarkdown.Append(trimmedLine);
continue;
}
else if (MarkdownImage().IsMatch(trimmedLine))
{
_ = finalMarkdown.AppendLine()
.Append(trimmedLine)
.AppendLine()
.AppendLine();
continue;
}
_ = finalMarkdown.AppendLine(trimmedLine);
}
return new MarkdownContentViewModel(title, finalMarkdown.ToString());
}
[GeneratedRegex("!\\[(.*?)\\]\\((.*?)\\)")]
private static partial Regex MarkdownImage();
}
+28 -2
View File
@@ -1,7 +1,13 @@
using Avalonia.Platform;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Notifications;
using Avalonia.Platform;
using Avalonia.Threading;
using Markdown.Avalonia.Utils;
using Markdowser.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
@@ -36,10 +42,22 @@ public class HttpPathResolver : IPathResolver
Debug.WriteLine($"Resolving image: {relativeOrAbsolutePath}");
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(relativeOrAbsolutePath)!;
HttpResponseMessage httpResponseMessage;
try
{
httpResponseMessage = await httpClient.GetAsync(relativeOrAbsolutePath)!;
}
catch (Exception ex)
{
// error with inluding url and message
ShowError($"Failed to fetch image: {ex.Message}\n{relativeOrAbsolutePath} - ");
return GetLogo();
}
if (!httpResponseMessage.IsSuccessStatusCode)
{
ShowError($"Failed to fetch image: {httpResponseMessage.ReasonPhrase}\n{relativeOrAbsolutePath}");
return GetLogo();
}
@@ -55,4 +73,12 @@ public class HttpPathResolver : IPathResolver
return AssetLoader.Open(new Uri("avares://Markdowser/Assets/Markdowser-Light-Transparent.png"))!;
}
private static void ShowError(string message)
{
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop && desktop.MainWindow?.DataContext is ViewModels.MainWindowViewModel vm)
{
Dispatcher.UIThread.Post(() => vm.WindowNotificationManager.Show(new Notification("Error", message, NotificationType.Error)));
}
}
}
+8 -6
View File
@@ -6,20 +6,22 @@ using Markdowser.ViewModels;
using System;
namespace Markdowser;
public class ViewLocator : IDataTemplate
{
public Control? Build(object? data)
{
if (data is null)
{
return null;
}
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
var type = Type.GetType(name);
string name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
Type? type = Type.GetType(name);
if (type != null)
{
var control = (Control)Activator.CreateInstance(type)!;
Control control = (Control)Activator.CreateInstance(type)!;
control.DataContext = data;
return control;
}
@@ -29,6 +31,6 @@ public class ViewLocator : IDataTemplate
public bool Match(object? data)
{
return data is ViewModelBase;
return data is ViewModelBase or ContentViewModelBase;
}
}
}
+62 -104
View File
@@ -1,10 +1,12 @@
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using HtmlAgilityPack;
using Markdowser.Models;
using Markdowser.Processing;
using Markdowser.Processing.Processors;
using Markdowser.Utilities;
using Markdowser.ViewModels.Content;
using ReactiveUI;
@@ -12,12 +14,9 @@ using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Input;
@@ -26,10 +25,9 @@ namespace Markdowser.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
private readonly HttpClient httpClient = new(new HttpClientHandler() { AllowAutoRedirect = true });
private readonly ReverseMarkdown.Converter markdownConverter = new();
private readonly ContentProcessorManager contentProcessorManager = new();
private readonly StringBuilder html = new();
private StringBuilder? content;
private ContentViewModelBase content;
private TabItem currentTab = null!;
private bool showSidePanel;
private bool isBusy;
@@ -54,9 +52,9 @@ public partial class MainWindowViewModel : ViewModelBase
}
}
public StringBuilder Content
public ContentViewModelBase Content
{
get => content ?? DefaultContent;
get => content;
set => this.RaiseAndSetIfChanged(ref content, value);
}
@@ -89,8 +87,8 @@ public partial class MainWindowViewModel : ViewModelBase
}
public SettingsViewModel SettingsViewModel => new SettingsViewModel();
public RawHtmlViewModel RawHtmlViewModel => new RawHtmlViewModel(html);
public RawMarkdownViewModel RawMarkdownViewModel => new RawMarkdownViewModel(() => Content);
public RawHtmlViewModel RawHtmlViewModel => new RawHtmlViewModel(new());
public RawMarkdownViewModel RawMarkdownViewModel => new RawMarkdownViewModel(() => new());
public bool CloseTabEnabled => Tabs.Count > 1;
public bool BackEnabled => GlobalState.BackHistory.Count > 0;
public bool ForwardEnabled => GlobalState.ForwardHistory.Count > 0;
@@ -145,21 +143,21 @@ public partial class MainWindowViewModel : ViewModelBase
this.RaisePropertyChanged(nameof(CloseTabEnabled));
});
private StringBuilder DefaultContent => new StringBuilder()
private MarkdownContentViewModel DefaultContent => new("New Tab", new StringBuilder()
.AppendLine($"![Logo](avares://Markdowser/Assets/Markdowser-{(Settings.Current.DarkMode ? "Dark" : "Light")}-Transparent.png)")
.AppendLine()
.AppendLine($"{nameof(Markdowser)} {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}\n")
.AppendLine("A markdown web browser.\n")
.AppendLine("[GitHub](https://github.me.stone-red.net/Markdowser)");
.AppendLine("[GitHub](https://github.me.stone-red.net/Markdowser)").ToString());
public MainWindowViewModel()
{
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"{nameof(Markdowser)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}");
httpClient.Timeout = TimeSpan.FromSeconds(10);
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
markdownConverter.Config.SuppressDivNewlines = false;
markdownConverter.Config.SmartHrefHandling = false;
content = DefaultContent;
contentProcessorManager.RegisterProcessor(new HtmlProcessor());
GlobalState.UrlChanged += (sender, url) =>
{
@@ -188,16 +186,11 @@ public partial class MainWindowViewModel : ViewModelBase
return Uri.TryCreate(uriString, UriKind.Absolute, out uri) && (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps);
}
[GeneratedRegex("!\\[(.*?)\\]\\((.*?)\\)")]
private static partial Regex MarkdownImage();
private void FetchUrl()
{
if (string.IsNullOrWhiteSpace(Url))
{
content = null;
_ = html.Clear();
ContentChanged();
Content = DefaultContent;
Debug.WriteLine("URL is empty.");
CurrentTab.Header = "New Tab";
return;
@@ -205,14 +198,17 @@ public partial class MainWindowViewModel : ViewModelBase
if (!IsValidHttpUri(Url, out _))
{
// Search with duckduckgo
Url = $"https://duckduckgo.com/html/?kd=-1&k1=-1&q={Uri.EscapeDataString(Url)}";
if (Url.StartsWith("//"))
{
Url = $"https:{Url}";
}
else
{
// Search with duckduckgo
Url = $"https://duckduckgo.com/html/?kd=-1&k1=-1&q={Uri.EscapeDataString(Url)}";
}
}
content ??= new StringBuilder();
_ = content.Clear();
_ = html.Clear();
IsBusy = true;
Progress = 0;
@@ -224,103 +220,65 @@ public partial class MainWindowViewModel : ViewModelBase
{
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(Url);
Url = httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? Url;
string? newUrl = httpResponseMessage.RequestMessage?.RequestUri?.ToString();
if (newUrl is not null && newUrl != Url)
{
string oldUrl = Url;
Dispatcher.UIThread.Post(() => WindowNotificationManager.Show(new Notification("Redirected", $"Redirected from\n{oldUrl}\nto\n{newUrl}", NotificationType.Warning)));
Url = newUrl;
}
if (!httpResponseMessage.IsSuccessStatusCode)
{
IsBusy = false;
_ = content.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
_ = content.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
ContentChanged();
StringBuilder errorMessage = new();
_ = errorMessage.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
_ = errorMessage.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
return;
}
long length = httpResponseMessage.Content.Headers.ContentLength ?? 0;
Content = await contentProcessorManager.ProcessContent(httpResponseMessage, new Progress<ProcessingProgress>(p => Progress = p.Percentage));
using Stream contentStream = await httpResponseMessage.Content.ReadAsStreamAsync();
using StreamReader streamReader = new(contentStream);
while (!streamReader.EndOfStream)
{
_ = html.AppendLine(await streamReader.ReadLineAsync());
Progress = (int)((double)contentStream.Position / length * 100);
}
HtmlDocument htmlDoc = new HtmlDocument();
htmlDoc.LoadHtml(html.ToString());
string title = htmlDoc.DocumentNode.SelectSingleNode("html/head/title")?.InnerText ?? Url;
Dispatcher.UIThread.Post(() =>
{
CurrentTab.Header = title?.Trim() ?? Url;
});
Dispatcher.UIThread.Post(() => CurrentTab.Header = Content.Title);
}
catch (HttpRequestException e)
{
IsBusy = false;
StringBuilder errorMessage = new();
if (e.StatusCode is not null)
{
_ = content.AppendLine($"# {(int)e.StatusCode} {e.StatusCode}");
_ = errorMessage.AppendLine($"# {(int)e.StatusCode} {e.StatusCode}");
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {(int)e.StatusCode} {e.StatusCode}");
}
else
{
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
}
_ = content.AppendLine($"# {e.HttpRequestError}");
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
ContentChanged();
_ = errorMessage.AppendLine($"# {e.HttpRequestError}");
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
}
catch (Exception e)
{
IsBusy = false;
_ = content.AppendLine($"# {e.GetType().Name}");
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
ContentChanged();
StringBuilder errorMessage = new();
_ = errorMessage.AppendLine($"# {e.GetType().Name}");
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
}
Progress = 0;
Debug.WriteLine("Converting HTML to markdown...");
string markdown = markdownConverter.Convert(html.ToString());
Debug.WriteLine("Processing markdown...");
int currentLine = 0;
string[] lines = markdown.Split('\n');
foreach (string line in lines)
finally
{
Progress = (int)((double)currentLine / lines.Length * 100);
currentLine++;
string trimmedLine = line.Trim();
if (trimmedLine.All(c => c == '#'))
{
_ = Content.Append(trimmedLine);
continue;
}
else if (MarkdownImage().IsMatch(trimmedLine))
{
_ = content.AppendLine()
.Append(trimmedLine)
.AppendLine()
.AppendLine();
continue;
}
_ = content.AppendLine(trimmedLine);
IsBusy = false;
}
ContentChanged();
IsBusy = false;
});
}
private void ContentChanged()
{
this.RaisePropertyChanged(nameof(Content));
this.RaisePropertyChanged(nameof(RawHtmlViewModel));
this.RaisePropertyChanged(nameof(RawMarkdownViewModel));
}
}
@@ -1,4 +1,5 @@
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Platform;
using Markdowser.Models;
@@ -11,7 +12,19 @@ namespace Markdowser.ViewModels;
public class ViewModelBase : ReactiveObject
{
private WindowNotificationManager? windowNotificationManager;
public string Title => $"{nameof(Markdowser)} - {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
public WindowIcon Icon => Settings.Current.DarkMode ? new WindowIcon(AssetLoader.Open(new("avares://Markdowser/Assets/Markdowser-Dark-Transparent.ico"))) : new WindowIcon(AssetLoader.Open(new("avares://Markdowser/Assets/Markdowser-Light-Transparent.ico")));
public WindowNotificationManager WindowNotificationManager => windowNotificationManager!;
protected internal void InitializeWindowNotificationManager(Window window)
{
TopLevel? topLevel = TopLevel.GetTopLevel(window);
windowNotificationManager = new WindowNotificationManager(topLevel)
{
Position = NotificationPosition.TopRight
};
}
}
@@ -0,0 +1,71 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Markdowser.ViewModels"
xmlns:cmd="using:Markdowser.Commands"
xmlns:utils="using:Markdowser.Utilities"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:md="https://github.com/whistyun/Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
x:DataType="vm:Content.MarkdownContentViewModel"
x:Class="Markdowser.Views.Content.MarkdownContentView">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<utils:HttpPathResolver x:Key="HttpPathResolver" />
<cmd:HyperlinkCommand x:Key="HyperlinkCommand" />
<cmd:ChangeThemeCommand x:Key="ChangeThemeCommand" />
<cmd:HomeCommand x:Key="HomeCommand" />
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<md:MarkdownScrollViewer Margin="10 0 10 10" Markdown="{Binding Markdown}">
<md:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CTextBlock">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading1">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading2">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading3">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading4">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading5">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading6">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CHyperlink">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLink}" />
</Style>
<Style Selector="ctxt|CHyperlink:pointerover">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkPointerover}" />
</Style>
<Style Selector="ctxt|CHyperlink:pressed">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkVisited}" />
</Style>
<Style Selector="ctxt|CImage">
<Setter Property="FittingWhenProtrude" Value="True" />
<Setter Property="LayoutWidth" Value="500" />
<Setter Property="SaveAspectRatio" Value="True" />
</Style>
</md:MarkdownScrollViewer.Styles>
<md:MarkdownScrollViewer.Plugins>
<md:MdAvPlugins PathResolver="{StaticResource HttpPathResolver}" HyperlinkCommand="{StaticResource HyperlinkCommand}" />
</md:MarkdownScrollViewer.Plugins>
</md:MarkdownScrollViewer>
</Grid>
</UserControl>
@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Markdowser.Views.Content;
public partial class MarkdownContentView : UserControl
{
public MarkdownContentView()
{
InitializeComponent();
}
}
+15 -47
View File
@@ -5,14 +5,13 @@
xmlns:utils="using:Markdowser.Utilities"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:md="https://github.com/whistyun/Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
xmlns:i="https://github.com/projektanker/icons.avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Markdowser.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Icon="{Binding Icon}"
Title="{Binding Title}">
Title="{Binding Title}"
Loaded="Window_Loaded">
<Design.DataContext>
<!-- This only sets the DataContext for the previewer in an IDE,
@@ -24,8 +23,6 @@
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<utils:HttpPathResolver x:Key="HttpPathResolver" />
<cmd:HyperlinkCommand x:Key="HyperlinkCommand" />
<cmd:ChangeThemeCommand x:Key="ChangeThemeCommand" />
<cmd:HomeCommand x:Key="HomeCommand" />
</ResourceDictionary>
@@ -33,6 +30,17 @@
</ResourceDictionary>
</Window.Resources>
<Window.KeyBindings>
<KeyBinding Gesture="Ctrl+R" Command="{Binding Browse}" />
<KeyBinding Gesture="Alt+Home" Command="{StaticResource HomeCommand}" />
<KeyBinding Gesture="Alt+H" Command="{StaticResource HomeCommand}" />
<KeyBinding Gesture="Alt+Left" Command="{Binding Back}" />
<KeyBinding Gesture="Alt+Right" Command="{Binding Forward}" />
<KeyBinding Gesture="Ctrl+T" Command="{Binding NewTab}" />
<KeyBinding Gesture="Ctrl+W" Command="{Binding CloseTab}" />
<KeyBinding Gesture="Alt+S" Command="{Binding ShowSidePanel}" />
</Window.KeyBindings>
<Grid RowDefinitions="Auto, *, Auto">
<Grid Grid.Row="0" ColumnDefinitions="*, Auto, Auto">
<TabControl Grid.Column="0" Theme="{DynamicResource ScrollTabControl}" SelectedItem="{Binding CurrentTab, Mode=TwoWay}" ItemsSource="{Binding Tabs}" />
@@ -47,48 +55,8 @@
<ColumnDefinition Width="*" utils:ColumnDefinition.IsVisible="{Binding ShowSidePanel}" />
</Grid.ColumnDefinitions>
<md:MarkdownScrollViewer Margin="10 0 10 10" IsEnabled="{Binding !IsBusy}" Markdown="{Binding Content}">
<md:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CTextBlock">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading1">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading2">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading3">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading4">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading5">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CTextBlock.Heading6">
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
</Style>
<Style Selector="ctxt|CHyperlink">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLink}" />
</Style>
<Style Selector="ctxt|CHyperlink:pointerover">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkPointerover}" />
</Style>
<Style Selector="ctxt|CHyperlink:pressed">
<Setter Property="Foreground" Value="{DynamicResource SemiColorLinkVisited}" />
</Style>
<Style Selector="ctxt|CImage">
<Setter Property="FittingWhenProtrude" Value="True" />
<Setter Property="LayoutWidth" Value="500" />
<Setter Property="SaveAspectRatio" Value="True" />
</Style>
</md:MarkdownScrollViewer.Styles>
<md:MarkdownScrollViewer.Plugins>
<md:MdAvPlugins PathResolver="{StaticResource HttpPathResolver}" HyperlinkCommand="{StaticResource HyperlinkCommand}" />
</md:MarkdownScrollViewer.Plugins>
</md:MarkdownScrollViewer>
<ContentControl IsEnabled="{Binding !IsBusy}" Margin="0 10 0 0" Content="{Binding Content}" />
<GridSplitter Grid.Column="1" IsVisible="{Binding ShowSidePanel}" Background="{DynamicResource SemiColorTertiary}" ResizeDirection="Columns" />
<TabControl Grid.Column="2" IsVisible="{Binding ShowSidePanel}" Margin="10">
<TabItem Header="Settings">
+8
View File
@@ -8,4 +8,12 @@ public partial class MainWindow : Window
{
InitializeComponent();
}
private void Window_Loaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (DataContext is ViewModels.MainWindowViewModel vm)
{
vm.InitializeWindowNotificationManager(this);
}
}
}