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", "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("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("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")]
|
|
||||||
@@ -39,7 +39,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Processing\Processors\" />
|
<Compile Update="Views\Content\MarkdownContentView.axaml.cs">
|
||||||
<Folder Include="Views\Content\" />
|
<DependentUpon>MarkdownContentView.axaml</DependentUpon>
|
||||||
|
</Compile>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
+4
-4
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Markdowser.Processing;
|
namespace Markdowser.Processing;
|
||||||
@@ -16,9 +16,9 @@ internal class ContentProcessorManager
|
|||||||
processors.Add(processor);
|
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.");
|
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(response, progress);
|
return processor.Process(httpResponseMessage, progress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
using Markdowser.ViewModels;
|
using Markdowser.ViewModels;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Net;
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Markdowser.Processing;
|
namespace Markdowser.Processing;
|
||||||
|
|
||||||
internal interface IContentProcessor
|
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;
|
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 long Current { get; } = current;
|
||||||
public int Total { get; } = total;
|
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 Markdown.Avalonia.Utils;
|
||||||
|
|
||||||
using Markdowser.Models;
|
using Markdowser.Models;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -36,10 +42,22 @@ public class HttpPathResolver : IPathResolver
|
|||||||
|
|
||||||
Debug.WriteLine($"Resolving image: {relativeOrAbsolutePath}");
|
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)
|
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
|
ShowError($"Failed to fetch image: {httpResponseMessage.ReasonPhrase}\n{relativeOrAbsolutePath}");
|
||||||
return GetLogo();
|
return GetLogo();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,4 +73,12 @@ public class HttpPathResolver : IPathResolver
|
|||||||
|
|
||||||
return AssetLoader.Open(new Uri("avares://Markdowser/Assets/Markdowser-Light-Transparent.png"))!;
|
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;
|
using System;
|
||||||
|
|
||||||
namespace Markdowser;
|
namespace Markdowser;
|
||||||
|
|
||||||
public class ViewLocator : IDataTemplate
|
public class ViewLocator : IDataTemplate
|
||||||
{
|
{
|
||||||
|
|
||||||
public Control? Build(object? data)
|
public Control? Build(object? data)
|
||||||
{
|
{
|
||||||
if (data is null)
|
if (data is null)
|
||||||
|
{
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
var name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
|
string name = data.GetType().FullName!.Replace("ViewModel", "View", StringComparison.Ordinal);
|
||||||
var type = Type.GetType(name);
|
Type? type = Type.GetType(name);
|
||||||
|
|
||||||
if (type != null)
|
if (type != null)
|
||||||
{
|
{
|
||||||
var control = (Control)Activator.CreateInstance(type)!;
|
Control control = (Control)Activator.CreateInstance(type)!;
|
||||||
control.DataContext = data;
|
control.DataContext = data;
|
||||||
return control;
|
return control;
|
||||||
}
|
}
|
||||||
@@ -29,6 +31,6 @@ public class ViewLocator : IDataTemplate
|
|||||||
|
|
||||||
public bool Match(object? data)
|
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;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
using HtmlAgilityPack;
|
|
||||||
|
|
||||||
using Markdowser.Models;
|
using Markdowser.Models;
|
||||||
|
using Markdowser.Processing;
|
||||||
|
using Markdowser.Processing.Processors;
|
||||||
using Markdowser.Utilities;
|
using Markdowser.Utilities;
|
||||||
|
using Markdowser.ViewModels.Content;
|
||||||
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
@@ -12,12 +14,9 @@ using System;
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
@@ -26,10 +25,9 @@ namespace Markdowser.ViewModels;
|
|||||||
public partial class MainWindowViewModel : ViewModelBase
|
public partial class MainWindowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly HttpClient httpClient = new(new HttpClientHandler() { AllowAutoRedirect = true });
|
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 ContentViewModelBase content;
|
||||||
private StringBuilder? content;
|
|
||||||
private TabItem currentTab = null!;
|
private TabItem currentTab = null!;
|
||||||
private bool showSidePanel;
|
private bool showSidePanel;
|
||||||
private bool isBusy;
|
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);
|
set => this.RaiseAndSetIfChanged(ref content, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +87,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
public SettingsViewModel SettingsViewModel => new SettingsViewModel();
|
public SettingsViewModel SettingsViewModel => new SettingsViewModel();
|
||||||
public RawHtmlViewModel RawHtmlViewModel => new RawHtmlViewModel(html);
|
public RawHtmlViewModel RawHtmlViewModel => new RawHtmlViewModel(new());
|
||||||
public RawMarkdownViewModel RawMarkdownViewModel => new RawMarkdownViewModel(() => Content);
|
public RawMarkdownViewModel RawMarkdownViewModel => new RawMarkdownViewModel(() => new());
|
||||||
public bool CloseTabEnabled => Tabs.Count > 1;
|
public bool CloseTabEnabled => Tabs.Count > 1;
|
||||||
public bool BackEnabled => GlobalState.BackHistory.Count > 0;
|
public bool BackEnabled => GlobalState.BackHistory.Count > 0;
|
||||||
public bool ForwardEnabled => GlobalState.ForwardHistory.Count > 0;
|
public bool ForwardEnabled => GlobalState.ForwardHistory.Count > 0;
|
||||||
@@ -145,21 +143,21 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
this.RaisePropertyChanged(nameof(CloseTabEnabled));
|
this.RaisePropertyChanged(nameof(CloseTabEnabled));
|
||||||
});
|
});
|
||||||
|
|
||||||
private StringBuilder DefaultContent => new StringBuilder()
|
private MarkdownContentViewModel DefaultContent => new("New Tab", new StringBuilder()
|
||||||
.AppendLine($"}-Transparent.png)")
|
.AppendLine($"}-Transparent.png)")
|
||||||
.AppendLine()
|
.AppendLine()
|
||||||
.AppendLine($"{nameof(Markdowser)} {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}\n")
|
.AppendLine($"{nameof(Markdowser)} {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}\n")
|
||||||
.AppendLine("A markdown web browser.\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()
|
public MainWindowViewModel()
|
||||||
{
|
{
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"{nameof(Markdowser)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}");
|
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"{nameof(Markdowser)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}");
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
|
content = DefaultContent;
|
||||||
markdownConverter.Config.SuppressDivNewlines = false;
|
|
||||||
markdownConverter.Config.SmartHrefHandling = false;
|
contentProcessorManager.RegisterProcessor(new HtmlProcessor());
|
||||||
|
|
||||||
GlobalState.UrlChanged += (sender, url) =>
|
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);
|
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()
|
private void FetchUrl()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(Url))
|
if (string.IsNullOrWhiteSpace(Url))
|
||||||
{
|
{
|
||||||
content = null;
|
Content = DefaultContent;
|
||||||
_ = html.Clear();
|
|
||||||
ContentChanged();
|
|
||||||
Debug.WriteLine("URL is empty.");
|
Debug.WriteLine("URL is empty.");
|
||||||
CurrentTab.Header = "New Tab";
|
CurrentTab.Header = "New Tab";
|
||||||
return;
|
return;
|
||||||
@@ -205,14 +198,17 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
if (!IsValidHttpUri(Url, out _))
|
if (!IsValidHttpUri(Url, out _))
|
||||||
{
|
{
|
||||||
// Search with duckduckgo
|
if (Url.StartsWith("//"))
|
||||||
Url = $"https://duckduckgo.com/html/?kd=-1&k1=-1&q={Uri.EscapeDataString(Url)}";
|
{
|
||||||
|
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;
|
IsBusy = true;
|
||||||
Progress = 0;
|
Progress = 0;
|
||||||
|
|
||||||
@@ -224,103 +220,65 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
HttpResponseMessage httpResponseMessage = await httpClient.GetAsync(Url);
|
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)
|
if (!httpResponseMessage.IsSuccessStatusCode)
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
_ = content.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
|
|
||||||
_ = content.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
|
StringBuilder errorMessage = new();
|
||||||
ContentChanged();
|
|
||||||
|
_ = errorMessage.AppendLine($"# {(int)httpResponseMessage.StatusCode} {httpResponseMessage.StatusCode}");
|
||||||
|
_ = errorMessage.AppendLine($"Failed to fetch URL: {httpResponseMessage.ReasonPhrase}");
|
||||||
|
|
||||||
|
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
||||||
return;
|
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();
|
Dispatcher.UIThread.Post(() => CurrentTab.Header = Content.Title);
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
StringBuilder errorMessage = new();
|
||||||
|
|
||||||
if (e.StatusCode is not null)
|
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}");
|
_ = errorMessage.AppendLine($"# {e.HttpRequestError}");
|
||||||
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
|
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
|
||||||
ContentChanged();
|
|
||||||
|
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
IsBusy = false;
|
StringBuilder errorMessage = new();
|
||||||
_ = content.AppendLine($"# {e.GetType().Name}");
|
|
||||||
_ = content.AppendLine($"Failed to fetch URL: {e.Message}");
|
_ = errorMessage.AppendLine($"# {e.GetType().Name}");
|
||||||
ContentChanged();
|
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
|
||||||
|
|
||||||
|
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
||||||
|
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
Progress = (int)((double)currentLine / lines.Length * 100);
|
IsBusy = false;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
using Avalonia.Controls.Notifications;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
|
|
||||||
using Markdowser.Models;
|
using Markdowser.Models;
|
||||||
@@ -11,7 +12,19 @@ namespace Markdowser.ViewModels;
|
|||||||
|
|
||||||
public class ViewModelBase : ReactiveObject
|
public class ViewModelBase : ReactiveObject
|
||||||
{
|
{
|
||||||
|
private WindowNotificationManager? windowNotificationManager;
|
||||||
public string Title => $"{nameof(Markdowser)} - {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
|
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 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:utils="using:Markdowser.Utilities"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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"
|
xmlns:i="https://github.com/projektanker/icons.avalonia"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Markdowser.Views.MainWindow"
|
x:Class="Markdowser.Views.MainWindow"
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
Icon="{Binding Icon}"
|
Icon="{Binding Icon}"
|
||||||
Title="{Binding Title}">
|
Title="{Binding Title}"
|
||||||
|
Loaded="Window_Loaded">
|
||||||
|
|
||||||
<Design.DataContext>
|
<Design.DataContext>
|
||||||
<!-- This only sets the DataContext for the previewer in an IDE,
|
<!-- This only sets the DataContext for the previewer in an IDE,
|
||||||
@@ -24,8 +23,6 @@
|
|||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<utils:HttpPathResolver x:Key="HttpPathResolver" />
|
|
||||||
<cmd:HyperlinkCommand x:Key="HyperlinkCommand" />
|
|
||||||
<cmd:ChangeThemeCommand x:Key="ChangeThemeCommand" />
|
<cmd:ChangeThemeCommand x:Key="ChangeThemeCommand" />
|
||||||
<cmd:HomeCommand x:Key="HomeCommand" />
|
<cmd:HomeCommand x:Key="HomeCommand" />
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
@@ -33,6 +30,17 @@
|
|||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Window.Resources>
|
</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 RowDefinitions="Auto, *, Auto">
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*, Auto, Auto">
|
<Grid Grid.Row="0" ColumnDefinitions="*, Auto, Auto">
|
||||||
<TabControl Grid.Column="0" Theme="{DynamicResource ScrollTabControl}" SelectedItem="{Binding CurrentTab, Mode=TwoWay}" ItemsSource="{Binding Tabs}" />
|
<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}" />
|
<ColumnDefinition Width="*" utils:ColumnDefinition.IsVisible="{Binding ShowSidePanel}" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<md:MarkdownScrollViewer Margin="10 0 10 10" IsEnabled="{Binding !IsBusy}" Markdown="{Binding Content}">
|
<ContentControl IsEnabled="{Binding !IsBusy}" Margin="0 10 0 0" Content="{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>
|
|
||||||
<GridSplitter Grid.Column="1" IsVisible="{Binding ShowSidePanel}" Background="{DynamicResource SemiColorTertiary}" ResizeDirection="Columns" />
|
<GridSplitter Grid.Column="1" IsVisible="{Binding ShowSidePanel}" Background="{DynamicResource SemiColorTertiary}" ResizeDirection="Columns" />
|
||||||
<TabControl Grid.Column="2" IsVisible="{Binding ShowSidePanel}" Margin="10">
|
<TabControl Grid.Column="2" IsVisible="{Binding ShowSidePanel}" Margin="10">
|
||||||
<TabItem Header="Settings">
|
<TabItem Header="Settings">
|
||||||
|
|||||||
@@ -8,4 +8,12 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
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