mirror of
https://github.com/Stone-Red-Code/Markdowser.git
synced 2026-09-04 00:56:13 +02:00
Merge branch 'develop' into feature/cache
This commit is contained in:
@@ -3,10 +3,15 @@ using Avalonia.Controls.ApplicationLifetimes;
|
|||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
using Avalonia.Styling;
|
using Avalonia.Styling;
|
||||||
|
|
||||||
|
using CuteUtils.Logging;
|
||||||
|
|
||||||
using Markdowser.Models;
|
using Markdowser.Models;
|
||||||
|
using Markdowser.Utilities;
|
||||||
using Markdowser.ViewModels;
|
using Markdowser.ViewModels;
|
||||||
using Markdowser.Views;
|
using Markdowser.Views;
|
||||||
|
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace Markdowser;
|
namespace Markdowser;
|
||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
@@ -18,6 +23,11 @@ public partial class App : Application
|
|||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
|
if (!Directory.Exists(Configuration.ApplicationDataPath))
|
||||||
|
{
|
||||||
|
_ = Directory.CreateDirectory(Configuration.ApplicationDataPath);
|
||||||
|
}
|
||||||
|
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
desktop.MainWindow = new MainWindow
|
desktop.MainWindow = new MainWindow
|
||||||
@@ -25,12 +35,23 @@ public partial class App : Application
|
|||||||
DataContext = new MainWindowViewModel(),
|
DataContext = new MainWindowViewModel(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clear log files
|
||||||
|
GlobalState.Logger.ClearLogFile(LogSeverity.Debug);
|
||||||
|
GlobalState.Logger.ClearLogFile(LogSeverity.Info);
|
||||||
|
GlobalState.Logger.ClearLogFile(LogSeverity.Warn);
|
||||||
|
GlobalState.Logger.ClearLogFile(LogSeverity.Error);
|
||||||
|
GlobalState.Logger.ClearLogFile(LogSeverity.Fatal);
|
||||||
|
|
||||||
desktop.MainWindow.Closing += (sender, e) =>
|
desktop.MainWindow.Closing += (sender, e) =>
|
||||||
{
|
{
|
||||||
|
GlobalState.Logger.LogInfo("Saving settings...");
|
||||||
Settings.SaveSettings();
|
Settings.SaveSettings();
|
||||||
|
GlobalState.Logger.LogInfo("Settings saved.");
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalState.Logger.LogInfo("Application started.");
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
|
||||||
if (Current is not null)
|
if (Current is not null)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Markdowser.Utilities;
|
using Markdowser.Utilities;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
|
|
||||||
namespace Markdowser.Commands;
|
namespace Markdowser.Commands;
|
||||||
@@ -23,7 +22,7 @@ public class HyperlinkCommand : ICommand
|
|||||||
{
|
{
|
||||||
if (parameter is string url)
|
if (parameter is string url)
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"Hyperlink clicked: {url}");
|
GlobalState.Logger.LogDebug($"Hyperlink clicked: {url}");
|
||||||
|
|
||||||
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
||||||
{
|
{
|
||||||
@@ -35,7 +34,7 @@ public class HyperlinkCommand : ICommand
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Debug.WriteLine($"Invalid URL: {url}");
|
GlobalState.Logger.LogDebug($"Invalid URL: {url}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||||
<Version>0.1.0.0</Version>
|
<Version>0.1.0.0</Version>
|
||||||
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -30,12 +31,14 @@
|
|||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.10" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.10" />
|
||||||
|
<PackageReference Include="CuteUtils" Version="1.0.0" />
|
||||||
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" />
|
<PackageReference Include="Markdown.Avalonia" Version="11.0.2" />
|
||||||
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.1.2" />
|
<PackageReference Include="Projektanker.Icons.Avalonia" Version="9.1.2" />
|
||||||
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.1.2" />
|
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="9.1.2" />
|
||||||
<PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.1.2" />
|
<PackageReference Include="Projektanker.Icons.Avalonia.MaterialDesign" Version="9.1.2" />
|
||||||
<PackageReference Include="ReverseMarkdown" Version="4.3.0" />
|
<PackageReference Include="ReverseMarkdown" Version="4.3.0" />
|
||||||
<PackageReference Include="Semi.Avalonia" Version="11.0.7.1" />
|
<PackageReference Include="Semi.Avalonia" Version="11.0.7.1" />
|
||||||
|
<PackageReference Include="Syroot.Windows.IO.KnownFolders" Version="1.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
using Markdowser.Utilities;
|
using Markdowser.Utilities;
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace Markdowser.Models;
|
namespace Markdowser.Models;
|
||||||
@@ -15,6 +17,8 @@ public class Settings
|
|||||||
|
|
||||||
public string? HomeUrl { get; set; }
|
public string? HomeUrl { get; set; }
|
||||||
|
|
||||||
|
public string UserAgent { get; set; } = $"{Assembly.GetExecutingAssembly().GetName().Name}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
|
||||||
|
|
||||||
public static void SaveSettings()
|
public static void SaveSettings()
|
||||||
{
|
{
|
||||||
if (!Directory.Exists(Configuration.ApplicationDataPath))
|
if (!Directory.Exists(Configuration.ApplicationDataPath))
|
||||||
@@ -28,9 +32,17 @@ public class Settings
|
|||||||
private static Settings LoadSettings()
|
private static Settings LoadSettings()
|
||||||
{
|
{
|
||||||
if (File.Exists(Configuration.SettingsFilePath))
|
if (File.Exists(Configuration.SettingsFilePath))
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
return JsonSerializer.Deserialize<Settings>(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings();
|
return JsonSerializer.Deserialize<Settings>(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings();
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
GlobalState.Logger.LogWarn(ex.Message);
|
||||||
|
return new Settings();
|
||||||
|
}
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
return new Settings();
|
return new Settings();
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ namespace Markdowser.Processing;
|
|||||||
|
|
||||||
internal interface IContentProcessor
|
internal interface IContentProcessor
|
||||||
{
|
{
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
string Description { get; }
|
||||||
|
|
||||||
bool CanProcess(HttpContentHeaders httpContentHeaders);
|
bool CanProcess(HttpContentHeaders httpContentHeaders);
|
||||||
|
|
||||||
Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress);
|
Task<ContentViewModelBase> Process(HttpResponseMessage httpResponseMessage, IProgress<ProcessingProgress> progress);
|
||||||
|
|||||||
@@ -7,8 +7,13 @@ using System.Net.Http.Headers;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Markdowser.Processing.Processors;
|
namespace Markdowser.Processing.Processors;
|
||||||
|
|
||||||
internal class CommonImageProcessor : IContentProcessor
|
internal class CommonImageProcessor : IContentProcessor
|
||||||
{
|
{
|
||||||
|
public string Name => "Common Image Processor";
|
||||||
|
|
||||||
|
public string Description => "Processes common image types (e.g. PNG, JPEG, GIF)";
|
||||||
|
|
||||||
public bool CanProcess(HttpContentHeaders httpContentHeaders)
|
public bool CanProcess(HttpContentHeaders httpContentHeaders)
|
||||||
{
|
{
|
||||||
if (httpContentHeaders.ContentType?.MediaType == "image/svg+xml")
|
if (httpContentHeaders.ContentType?.MediaType == "image/svg+xml")
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
using HtmlAgilityPack;
|
using HtmlAgilityPack;
|
||||||
|
|
||||||
|
using Markdowser.Utilities;
|
||||||
using Markdowser.ViewModels;
|
using Markdowser.ViewModels;
|
||||||
using Markdowser.ViewModels.Content;
|
using Markdowser.ViewModels.Content;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
@@ -19,6 +19,10 @@ internal partial class HtmlProcessor : IContentProcessor
|
|||||||
{
|
{
|
||||||
private readonly ReverseMarkdown.Converter markdownConverter = new();
|
private readonly ReverseMarkdown.Converter markdownConverter = new();
|
||||||
|
|
||||||
|
public string Name => "HTML Processor";
|
||||||
|
|
||||||
|
public string Description => "Processes HTML content";
|
||||||
|
|
||||||
public HtmlProcessor()
|
public HtmlProcessor()
|
||||||
{
|
{
|
||||||
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
|
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
|
||||||
@@ -52,11 +56,11 @@ internal partial class HtmlProcessor : IContentProcessor
|
|||||||
htmlDoc.LoadHtml(html.ToString());
|
htmlDoc.LoadHtml(html.ToString());
|
||||||
string title = htmlDoc.DocumentNode.SelectSingleNode("html/head/title")?.InnerText?.Trim() ?? httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? "Untitled";
|
string title = htmlDoc.DocumentNode.SelectSingleNode("html/head/title")?.InnerText?.Trim() ?? httpResponseMessage.RequestMessage?.RequestUri?.ToString() ?? "Untitled";
|
||||||
|
|
||||||
Debug.WriteLine("Converting HTML to markdown...");
|
GlobalState.Logger.LogDebug("Converting HTML to markdown...");
|
||||||
|
|
||||||
string markdown = markdownConverter.Convert(html.ToString());
|
string markdown = markdownConverter.Convert(html.ToString());
|
||||||
|
|
||||||
Debug.WriteLine("Processing markdown...");
|
GlobalState.Logger.LogDebug("Processing markdown...");
|
||||||
|
|
||||||
int currentLine = 0;
|
int currentLine = 0;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
using Markdowser.Utilities;
|
||||||
|
|
||||||
using Projektanker.Icons.Avalonia;
|
using Projektanker.Icons.Avalonia;
|
||||||
using Projektanker.Icons.Avalonia.FontAwesome;
|
using Projektanker.Icons.Avalonia.FontAwesome;
|
||||||
using Projektanker.Icons.Avalonia.MaterialDesign;
|
using Projektanker.Icons.Avalonia.MaterialDesign;
|
||||||
|
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Markdowser;
|
namespace Markdowser;
|
||||||
|
|
||||||
@@ -16,10 +22,32 @@ internal static class Program
|
|||||||
// yet and stuff might break.
|
// yet and stuff might break.
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static void Main(string[] args)
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
AppDomain.CurrentDomain.UnhandledException += (sender, e) =>
|
||||||
|
{
|
||||||
|
GlobalState.Logger.LogFatal(e.ExceptionObject.ToString() ?? "Unknown error.");
|
||||||
|
};
|
||||||
|
|
||||||
|
TaskScheduler.UnobservedTaskException += (sender, e) =>
|
||||||
|
{
|
||||||
|
GlobalState.Logger.LogFatal(e.Exception.ToString());
|
||||||
|
};
|
||||||
|
|
||||||
|
RxApp.DefaultExceptionHandler = Observer.Create<Exception>(ex =>
|
||||||
|
{
|
||||||
|
GlobalState.Logger.LogFatal(ex.ToString());
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_ = BuildAvaloniaApp()
|
_ = BuildAvaloniaApp()
|
||||||
.StartWithClassicDesktopLifetime(args);
|
.StartWithClassicDesktopLifetime(args);
|
||||||
}
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
GlobalState.Logger.LogFatal(ex.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Avalonia configuration, don't remove; also used by visual designer.
|
// Avalonia configuration, don't remove; also used by visual designer.
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
|
|||||||
@@ -1,12 +1,39 @@
|
|||||||
using System;
|
using Syroot.Windows.IO;
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace Markdowser.Utilities;
|
namespace Markdowser.Utilities;
|
||||||
|
|
||||||
internal static class Configuration
|
internal static class Configuration
|
||||||
{
|
{
|
||||||
public static string DownloadPath => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
public static string DownloadPath
|
||||||
public static string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(Markdowser));
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return KnownFolders.Downloads.Path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments), "Downloads");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ApplicationDataPath
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||||
|
{
|
||||||
|
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "StoneRed", nameof(Markdowser));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StoneRed", nameof(Markdowser));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static string SettingsFilePath => Path.Combine(ApplicationDataPath, "settings.json");
|
public static string SettingsFilePath => Path.Combine(ApplicationDataPath, "settings.json");
|
||||||
|
public static string LogFilePath => Path.Combine(ApplicationDataPath, $"{nameof(Markdowser)}.log");
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
|
||||||
|
using CuteUtils.Logging;
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
@@ -16,6 +18,42 @@ internal static class GlobalState
|
|||||||
|
|
||||||
private static string url = string.Empty;
|
private static string url = string.Empty;
|
||||||
|
|
||||||
|
public static Logger Logger { get; } = new()
|
||||||
|
{
|
||||||
|
Config = new()
|
||||||
|
{
|
||||||
|
DebugConfig = new()
|
||||||
|
{
|
||||||
|
LogTarget = LogTarget.DebugConsole
|
||||||
|
},
|
||||||
|
InfoConfig = new()
|
||||||
|
{
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = Configuration.LogFilePath
|
||||||
|
},
|
||||||
|
WarnConfig = new()
|
||||||
|
{
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = Configuration.LogFilePath
|
||||||
|
},
|
||||||
|
ErrorConfig = new()
|
||||||
|
{
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = Configuration.LogFilePath
|
||||||
|
},
|
||||||
|
FatalConfig = new()
|
||||||
|
{
|
||||||
|
LogTarget = LogTarget.DebugConsole | LogTarget.File,
|
||||||
|
FilePath = Configuration.LogFilePath
|
||||||
|
},
|
||||||
|
FormatConfig = new()
|
||||||
|
{
|
||||||
|
DebugConsoleFormat = new LogFormatBuilder().DateTime().Text(" ").LogSeverity(padding: -6).FilePath().Text(":").MemberName().Text(":").LineNumber().Text(Environment.NewLine).Message(),
|
||||||
|
FileFormat = new LogFormatBuilder().DateTime().Text(" ").LogSeverity(padding: -6).FilePath().Text(":").MemberName().Text(":").LineNumber().Text(Environment.NewLine).Message(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
public static Stack<string> BackHistory { get; } = new();
|
public static Stack<string> BackHistory { get; } = new();
|
||||||
|
|
||||||
public static Stack<string> ForwardHistory { get; } = new();
|
public static Stack<string> ForwardHistory { get; } = new();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ using Markdowser.Models;
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
@@ -40,7 +39,7 @@ public class HttpPathResolver : IPathResolver
|
|||||||
relativeOrAbsolutePath = new Uri(new Uri(GlobalState.Url), relativeOrAbsolutePath).ToString();
|
relativeOrAbsolutePath = new Uri(new Uri(GlobalState.Url), relativeOrAbsolutePath).ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
Debug.WriteLine($"Resolving image: {relativeOrAbsolutePath}");
|
GlobalState.Logger.LogDebug($"Resolving image: {relativeOrAbsolutePath}");
|
||||||
|
|
||||||
HttpResponseMessage httpResponseMessage;
|
HttpResponseMessage httpResponseMessage;
|
||||||
|
|
||||||
@@ -80,5 +79,7 @@ public class HttpPathResolver : IPathResolver
|
|||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() => vm.WindowNotificationManager.Show(new Notification("Error", message, NotificationType.Error)));
|
Dispatcher.UIThread.Post(() => vm.WindowNotificationManager.Show(new Notification("Error", message, NotificationType.Error)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
GlobalState.Logger.LogError(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
namespace Markdowser.ViewModels;
|
using CuteUtils.Logging;
|
||||||
|
|
||||||
|
using Markdowser.Utilities;
|
||||||
|
|
||||||
|
namespace Markdowser.ViewModels;
|
||||||
|
|
||||||
public abstract class ContentViewModelBase(string title)
|
public abstract class ContentViewModelBase(string title)
|
||||||
{
|
{
|
||||||
public string Title { get; } = title;
|
public string Title { get; } = title;
|
||||||
|
|
||||||
|
public Logger Logger { get; } = GlobalState.Logger;
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,6 @@ using ReactiveUI;
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
@@ -48,7 +47,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
_ = this.RaiseAndSetIfChanged(ref currentTab!, value);
|
_ = this.RaiseAndSetIfChanged(ref currentTab!, value);
|
||||||
|
|
||||||
Url = currentTab?.Tag?.ToString() ?? string.Empty;
|
Url = value?.Tag?.ToString() ?? string.Empty;
|
||||||
|
|
||||||
FetchUrl(true);
|
FetchUrl(true);
|
||||||
}
|
}
|
||||||
@@ -134,18 +133,32 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public ICommand CloseTab => ReactiveCommand.Create(() =>
|
public ICommand CloseTab => ReactiveCommand.Create(() =>
|
||||||
{
|
{
|
||||||
|
if (IsBusy)
|
||||||
|
{
|
||||||
|
WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (Tabs.Count > 1)
|
if (Tabs.Count > 1)
|
||||||
{
|
{
|
||||||
int currentIndex = Tabs.IndexOf(CurrentTab);
|
int currentIndex = Tabs.IndexOf(CurrentTab);
|
||||||
_ = Tabs.Remove(CurrentTab);
|
|
||||||
|
|
||||||
CurrentTab = currentIndex > 0 ? Tabs[currentIndex - 1] : Tabs[0];
|
CurrentTab = currentIndex > 0 ? Tabs[currentIndex - 1] : Tabs[0];
|
||||||
|
|
||||||
|
Tabs.RemoveAt(currentIndex);
|
||||||
|
|
||||||
this.RaisePropertyChanged(nameof(CloseTabEnabled));
|
this.RaisePropertyChanged(nameof(CloseTabEnabled));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
public ICommand NewTab => ReactiveCommand.Create(() =>
|
public ICommand NewTab => ReactiveCommand.Create(() =>
|
||||||
{
|
{
|
||||||
|
if (IsBusy)
|
||||||
|
{
|
||||||
|
WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
TabItem tab = new() { Header = "New Tab", Name = Guid.NewGuid().ToString() };
|
TabItem tab = new() { Header = "New Tab", Name = Guid.NewGuid().ToString() };
|
||||||
Tabs.Add(tab);
|
Tabs.Add(tab);
|
||||||
CurrentTab = tab;
|
CurrentTab = tab;
|
||||||
@@ -163,7 +176,6 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
public MainWindowViewModel()
|
public MainWindowViewModel()
|
||||||
{
|
{
|
||||||
httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"{nameof(Markdowser)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}");
|
|
||||||
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
httpClient.Timeout = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
content = DefaultContent;
|
content = DefaultContent;
|
||||||
@@ -209,7 +221,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
if (string.IsNullOrWhiteSpace(Url))
|
if (string.IsNullOrWhiteSpace(Url))
|
||||||
{
|
{
|
||||||
Content = DefaultContent;
|
Content = DefaultContent;
|
||||||
Debug.WriteLine("URL is empty.");
|
Logger.LogDebug("URL is empty.");
|
||||||
CurrentTab.Header = "New Tab";
|
CurrentTab.Header = "New Tab";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -234,12 +246,19 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
httpClient.DefaultRequestHeaders.UserAgent.Clear();
|
||||||
|
if (!httpClient.DefaultRequestHeaders.UserAgent.TryParseAdd(Settings.Current.UserAgent))
|
||||||
|
{
|
||||||
|
WindowNotificationManager.Show(new Notification("Invalid User Agent", "Failed to set user agent.", NotificationType.Error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
Progress = 0;
|
Progress = 0;
|
||||||
|
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
Debug.WriteLine("Fetching URL...");
|
Logger.LogInfo($"Fetching URL: {Url}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -279,6 +298,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() => CurrentTab.Header = Content.Title);
|
Dispatcher.UIThread.Post(() => CurrentTab.Header = Content.Title);
|
||||||
|
|
||||||
|
Logger.LogInfo($"Fetched URL: {Url}");
|
||||||
}
|
}
|
||||||
catch (HttpRequestException e)
|
catch (HttpRequestException e)
|
||||||
{
|
{
|
||||||
@@ -298,6 +319,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
|
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
|
||||||
|
|
||||||
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
||||||
|
|
||||||
|
Logger.LogError(e.Message);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@@ -308,6 +331,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||||||
|
|
||||||
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
|
||||||
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
|
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
|
||||||
|
|
||||||
|
Logger.LogError(e.Message);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ public class SettingsViewModel : ViewModelBase
|
|||||||
set => Settings.Current.SearchEngineUrl = value;
|
set => Settings.Current.SearchEngineUrl = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public string UserAgent
|
||||||
|
{
|
||||||
|
get => Settings.Current.UserAgent;
|
||||||
|
set => Settings.Current.UserAgent = value;
|
||||||
|
}
|
||||||
|
|
||||||
public bool DarkMode
|
public bool DarkMode
|
||||||
{
|
{
|
||||||
get => Settings.Current.DarkMode;
|
get => Settings.Current.DarkMode;
|
||||||
|
|||||||
@@ -2,10 +2,14 @@
|
|||||||
using Avalonia.Controls.Notifications;
|
using Avalonia.Controls.Notifications;
|
||||||
using Avalonia.Platform;
|
using Avalonia.Platform;
|
||||||
|
|
||||||
|
using CuteUtils.Logging;
|
||||||
|
|
||||||
using Markdowser.Models;
|
using Markdowser.Models;
|
||||||
|
using Markdowser.Utilities;
|
||||||
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
|
||||||
namespace Markdowser.ViewModels;
|
namespace Markdowser.ViewModels;
|
||||||
@@ -13,12 +17,30 @@ namespace Markdowser.ViewModels;
|
|||||||
public class ViewModelBase : ReactiveObject
|
public class ViewModelBase : ReactiveObject
|
||||||
{
|
{
|
||||||
private WindowNotificationManager? windowNotificationManager;
|
private WindowNotificationManager? windowNotificationManager;
|
||||||
public string Title => $"{nameof(Markdowser)} - {Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
|
|
||||||
|
public string Title
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string title = nameof(Markdowser);
|
||||||
|
Assembly assembly = Assembly.GetExecutingAssembly();
|
||||||
|
FileVersionInfo fileVersionInfo = FileVersionInfo.GetVersionInfo(assembly.Location);
|
||||||
|
string? version = fileVersionInfo.ProductVersion;
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
return $"{title} - Dev {version}";
|
||||||
|
#else
|
||||||
|
return $"{title} - {version}";
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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!;
|
public WindowNotificationManager WindowNotificationManager => windowNotificationManager!;
|
||||||
|
|
||||||
|
public Logger Logger => GlobalState.Logger;
|
||||||
|
|
||||||
protected internal void InitializeWindowNotificationManager(Window window)
|
protected internal void InitializeWindowNotificationManager(Window window)
|
||||||
{
|
{
|
||||||
TopLevel? topLevel = TopLevel.GetTopLevel(window);
|
TopLevel? topLevel = TopLevel.GetTopLevel(window);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<md:MarkdownScrollViewer Margin="10 0 10 10" Markdown="{Binding Markdown}">
|
<md:MarkdownScrollViewer Margin="10 0 10 0" Markdown="{Binding Markdown}">
|
||||||
<md:MarkdownScrollViewer.Styles>
|
<md:MarkdownScrollViewer.Styles>
|
||||||
<Style Selector="ctxt|CTextBlock">
|
<Style Selector="ctxt|CTextBlock">
|
||||||
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
|
<Setter Property="Foreground" Value="{DynamicResource SemiColorText0}" />
|
||||||
|
|||||||
@@ -45,9 +45,16 @@
|
|||||||
|
|
||||||
<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" SelectedItem="{Binding CurrentTab, Mode=TwoWay}" ItemsSource="{Binding Tabs}" IsEnabled="{Binding !IsBusy}" />
|
||||||
<Button Grid.Column="1" Command="{Binding CloseTab}" IsEnabled="{Binding CloseTabEnabled}" HorizontalAlignment="Right" VerticalAlignment="Top" Height="32" i:Attached.Icon="fa-solid fa-xmark" />
|
<Button Grid.Column="1" Command="{Binding CloseTab}" HorizontalAlignment="Right" VerticalAlignment="Stretch" i:Attached.Icon="fa-solid fa-xmark">
|
||||||
<Button Grid.Column="2" Command="{Binding NewTab}" HorizontalAlignment="Right" VerticalAlignment="Top" Height="32" i:Attached.Icon="fa-solid fa-plus" />
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="CloseTabEnabled" />
|
||||||
|
<Binding Path="!IsBusy" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2" Command="{Binding NewTab}" HorizontalAlignment="Right" VerticalAlignment="Stretch" i:Attached.Icon="fa-solid fa-plus" IsEnabled="{Binding !IsBusy}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="1">
|
<Grid Grid.Row="1">
|
||||||
@@ -57,7 +64,7 @@
|
|||||||
<ColumnDefinition Width="*" utils:ColumnDefinition.IsVisible="{Binding ShowSidePanel}" />
|
<ColumnDefinition Width="*" utils:ColumnDefinition.IsVisible="{Binding ShowSidePanel}" />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
<ContentControl IsEnabled="{Binding !IsBusy}" Margin="0 10 0 0" Content="{Binding Content}" />
|
<ContentControl IsEnabled="{Binding !IsBusy}" Content="{Binding Content}" />
|
||||||
|
|
||||||
<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">
|
||||||
@@ -80,11 +87,25 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Border Grid.Row="2" BorderThickness="0 2 0 0" CornerRadius="0" BorderBrush="{DynamicResource SemiColorTertiary}">
|
<Border Grid.Row="2" BorderThickness="0 2 0 0" CornerRadius="0" BorderBrush="{DynamicResource SemiColorTertiary}">
|
||||||
<Grid IsEnabled="{Binding !IsBusy}" ColumnDefinitions="Auto, Auto, Auto, Auto, *, Auto">
|
<Grid ColumnDefinitions="Auto, Auto, Auto, Auto, *, Auto">
|
||||||
<Button Grid.Column="0" i:Attached.Icon="fa-solid fa-caret-left" IsEnabled="{Binding BackEnabled}" Command="{Binding Back}" />
|
<Button Grid.Column="0" i:Attached.Icon="fa-solid fa-caret-left" Command="{Binding Back}">
|
||||||
<Button Grid.Column="1" i:Attached.Icon="fa-solid fa-caret-right" IsEnabled="{Binding ForwardEnabled}" Command="{Binding Forward}" />
|
<Button.IsEnabled>
|
||||||
<Button Grid.Column="2" i:Attached.Icon="fa-solid fa-rotate-right" Command="{Binding Reload}" />
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
<Button Grid.Column="3" i:Attached.Icon="fa-solid fa-house" Command="{StaticResource HomeCommand}" />
|
<Binding Path="BackEnabled" />
|
||||||
|
<Binding Path="!IsBusy" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="1" i:Attached.Icon="fa-solid fa-caret-right" Command="{Binding Forward}">
|
||||||
|
<Button.IsEnabled>
|
||||||
|
<MultiBinding Converter="{x:Static BoolConverters.And}">
|
||||||
|
<Binding Path="ForwardEnabled" />
|
||||||
|
<Binding Path="!IsBusy" />
|
||||||
|
</MultiBinding>
|
||||||
|
</Button.IsEnabled>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="2" i:Attached.Icon="fa-solid fa-rotate-right" Command="{Binding Reload}" IsEnabled="{Binding !IsBusy}" />
|
||||||
|
<Button Grid.Column="3" i:Attached.Icon="fa-solid fa-house" Command="{StaticResource HomeCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||||
|
|
||||||
<Grid Grid.Column="4">
|
<Grid Grid.Column="4">
|
||||||
<TextBox IsVisible="{Binding !IsBusy}" Watermark="Search or type a URL" Text="{Binding Url}" BorderThickness="0">
|
<TextBox IsVisible="{Binding !IsBusy}" Watermark="Search or type a URL" Text="{Binding Url}" BorderThickness="0">
|
||||||
|
|||||||
@@ -28,5 +28,8 @@
|
|||||||
|
|
||||||
<TextBlock Text="Home URL" />
|
<TextBlock Text="Home URL" />
|
||||||
<TextBox Text="{Binding HomeUrl}" Watermark="URL, or leave empty to use default" />
|
<TextBox Text="{Binding HomeUrl}" Watermark="URL, or leave empty to use default" />
|
||||||
|
|
||||||
|
<TextBlock Text="User Agent" />
|
||||||
|
<TextBox Text="{Binding UserAgent}" Watermark="User agent string" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
Reference in New Issue
Block a user