mirror of
https://github.com/Stone-Red-Code/Markdowser.git
synced 2026-09-04 00:56:13 +02:00
Implement text/html processor
This commit is contained in:
@@ -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>")]
|
||||
@@ -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>
|
||||
|
||||
+4
-4
@@ -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();
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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($"}-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();
|
||||
}
|
||||
}
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user