diff --git a/src/Markdowser/App.axaml.cs b/src/Markdowser/App.axaml.cs
index 1249b3b..9a29aca 100644
--- a/src/Markdowser/App.axaml.cs
+++ b/src/Markdowser/App.axaml.cs
@@ -3,10 +3,15 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Styling;
+using CuteUtils.Logging;
+
using Markdowser.Models;
+using Markdowser.Utilities;
using Markdowser.ViewModels;
using Markdowser.Views;
+using System.IO;
+
namespace Markdowser;
public partial class App : Application
@@ -18,6 +23,11 @@ public partial class App : Application
public override void OnFrameworkInitializationCompleted()
{
+ if (!Directory.Exists(Configuration.ApplicationDataPath))
+ {
+ _ = Directory.CreateDirectory(Configuration.ApplicationDataPath);
+ }
+
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
@@ -25,12 +35,23 @@ public partial class App : Application
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) =>
{
+ GlobalState.Logger.LogInfo("Saving settings...");
Settings.SaveSettings();
+ GlobalState.Logger.LogInfo("Settings saved.");
};
}
+ GlobalState.Logger.LogInfo("Application started.");
+
base.OnFrameworkInitializationCompleted();
if (Current is not null)
diff --git a/src/Markdowser/Commands/HyperlinkCommand.cs b/src/Markdowser/Commands/HyperlinkCommand.cs
index 6c97b5e..c2a10d8 100644
--- a/src/Markdowser/Commands/HyperlinkCommand.cs
+++ b/src/Markdowser/Commands/HyperlinkCommand.cs
@@ -1,7 +1,6 @@
using Markdowser.Utilities;
using System;
-using System.Diagnostics;
using System.Windows.Input;
namespace Markdowser.Commands;
@@ -23,7 +22,7 @@ public class HyperlinkCommand : ICommand
{
if (parameter is string url)
{
- Debug.WriteLine($"Hyperlink clicked: {url}");
+ GlobalState.Logger.LogDebug($"Hyperlink clicked: {url}");
if (Uri.IsWellFormedUriString(url, UriKind.Absolute))
{
@@ -35,7 +34,7 @@ public class HyperlinkCommand : ICommand
}
else
{
- Debug.WriteLine($"Invalid URL: {url}");
+ GlobalState.Logger.LogDebug($"Invalid URL: {url}");
}
}
}
diff --git a/src/Markdowser/Markdowser.csproj b/src/Markdowser/Markdowser.csproj
index 24b1d1b..e3b8058 100644
--- a/src/Markdowser/Markdowser.csproj
+++ b/src/Markdowser/Markdowser.csproj
@@ -7,6 +7,7 @@
app.manifest
true
0.1.0.0
+ false
@@ -30,12 +31,14 @@
+
+
diff --git a/src/Markdowser/Models/Settings.cs b/src/Markdowser/Models/Settings.cs
index 4179104..0dcb68e 100644
--- a/src/Markdowser/Models/Settings.cs
+++ b/src/Markdowser/Models/Settings.cs
@@ -1,6 +1,8 @@
using Markdowser.Utilities;
+using System;
using System.IO;
+using System.Reflection;
using System.Text.Json;
namespace Markdowser.Models;
@@ -15,6 +17,8 @@ public class Settings
public string? HomeUrl { get; set; }
+ public string UserAgent { get; set; } = $"{Assembly.GetExecutingAssembly().GetName().Name}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}";
+
public static void SaveSettings()
{
if (!Directory.Exists(Configuration.ApplicationDataPath))
@@ -29,7 +33,15 @@ public class Settings
{
if (File.Exists(Configuration.SettingsFilePath))
{
- return JsonSerializer.Deserialize(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings();
+ try
+ {
+ return JsonSerializer.Deserialize(File.ReadAllText(Configuration.SettingsFilePath)) ?? new Settings();
+ }
+ catch (Exception ex)
+ {
+ GlobalState.Logger.LogWarn(ex.Message);
+ return new Settings();
+ }
}
else
{
diff --git a/src/Markdowser/Processing/IContentProcessor.cs b/src/Markdowser/Processing/IContentProcessor.cs
index 09c6412..cd8e4b8 100644
--- a/src/Markdowser/Processing/IContentProcessor.cs
+++ b/src/Markdowser/Processing/IContentProcessor.cs
@@ -9,6 +9,10 @@ namespace Markdowser.Processing;
internal interface IContentProcessor
{
+ string Name { get; }
+
+ string Description { get; }
+
bool CanProcess(HttpContentHeaders httpContentHeaders);
Task Process(HttpResponseMessage httpResponseMessage, IProgress progress);
diff --git a/src/Markdowser/Processing/Processors/CommonImageProcessor.cs b/src/Markdowser/Processing/Processors/CommonImageProcessor.cs
index 5364948..50e2522 100644
--- a/src/Markdowser/Processing/Processors/CommonImageProcessor.cs
+++ b/src/Markdowser/Processing/Processors/CommonImageProcessor.cs
@@ -7,8 +7,13 @@ using System.Net.Http.Headers;
using System.Threading.Tasks;
namespace Markdowser.Processing.Processors;
+
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)
{
if (httpContentHeaders.ContentType?.MediaType == "image/svg+xml")
@@ -23,4 +28,4 @@ internal class CommonImageProcessor : IContentProcessor
{
return new CommonImageContentViewModel(httpResponseMessage.RequestMessage?.RequestUri?.Host?.ToString() ?? "Image", await httpResponseMessage.Content.ReadAsStreamAsync());
}
-}
+}
\ No newline at end of file
diff --git a/src/Markdowser/Processing/Processors/HtmlProcessor.cs b/src/Markdowser/Processing/Processors/HtmlProcessor.cs
index 66d6c7d..edaa421 100644
--- a/src/Markdowser/Processing/Processors/HtmlProcessor.cs
+++ b/src/Markdowser/Processing/Processors/HtmlProcessor.cs
@@ -1,10 +1,10 @@
using HtmlAgilityPack;
+using Markdowser.Utilities;
using Markdowser.ViewModels;
using Markdowser.ViewModels.Content;
using System;
-using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
@@ -19,6 +19,10 @@ internal partial class HtmlProcessor : IContentProcessor
{
private readonly ReverseMarkdown.Converter markdownConverter = new();
+ public string Name => "HTML Processor";
+
+ public string Description => "Processes HTML content";
+
public HtmlProcessor()
{
markdownConverter.Config.UnknownTags = ReverseMarkdown.Config.UnknownTagsOption.Bypass;
@@ -52,11 +56,11 @@ internal partial class HtmlProcessor : IContentProcessor
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...");
+ GlobalState.Logger.LogDebug("Converting HTML to markdown...");
string markdown = markdownConverter.Convert(html.ToString());
- Debug.WriteLine("Processing markdown...");
+ GlobalState.Logger.LogDebug("Processing markdown...");
int currentLine = 0;
diff --git a/src/Markdowser/Program.cs b/src/Markdowser/Program.cs
index 12b002f..9564c36 100644
--- a/src/Markdowser/Program.cs
+++ b/src/Markdowser/Program.cs
@@ -1,11 +1,17 @@
using Avalonia;
using Avalonia.ReactiveUI;
+using Markdowser.Utilities;
+
using Projektanker.Icons.Avalonia;
using Projektanker.Icons.Avalonia.FontAwesome;
using Projektanker.Icons.Avalonia.MaterialDesign;
+using ReactiveUI;
+
using System;
+using System.Reactive;
+using System.Threading.Tasks;
namespace Markdowser;
@@ -17,8 +23,30 @@ internal static class Program
[STAThread]
public static void Main(string[] args)
{
- _ = BuildAvaloniaApp()
- .StartWithClassicDesktopLifetime(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(ex =>
+ {
+ GlobalState.Logger.LogFatal(ex.ToString());
+ });
+
+ try
+ {
+ _ = BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+ }
+ catch (Exception ex)
+ {
+ GlobalState.Logger.LogFatal(ex.ToString());
+ }
}
// Avalonia configuration, don't remove; also used by visual designer.
diff --git a/src/Markdowser/Utilities/Configuration.cs b/src/Markdowser/Utilities/Configuration.cs
index f859e02..6975467 100644
--- a/src/Markdowser/Utilities/Configuration.cs
+++ b/src/Markdowser/Utilities/Configuration.cs
@@ -1,12 +1,39 @@
-using System;
+using Syroot.Windows.IO;
+
+using System;
using System.IO;
+using System.Runtime.InteropServices;
namespace Markdowser.Utilities;
internal static class Configuration
{
- public static string DownloadPath => Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
- public static string ApplicationDataPath => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), nameof(Markdowser));
+ public static string DownloadPath
+ {
+ 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 LogFilePath => Path.Combine(ApplicationDataPath, $"{nameof(Markdowser)}.log");
}
\ No newline at end of file
diff --git a/src/Markdowser/Utilities/GlobalState.cs b/src/Markdowser/Utilities/GlobalState.cs
index 856fb75..38cc0f6 100644
--- a/src/Markdowser/Utilities/GlobalState.cs
+++ b/src/Markdowser/Utilities/GlobalState.cs
@@ -1,5 +1,7 @@
using Avalonia.Controls;
+using CuteUtils.Logging;
+
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
@@ -16,6 +18,42 @@ internal static class GlobalState
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 BackHistory { get; } = new();
public static Stack ForwardHistory { get; } = new();
diff --git a/src/Markdowser/Utilities/HttpPathResolver.cs b/src/Markdowser/Utilities/HttpPathResolver.cs
index d959dff..a121f17 100644
--- a/src/Markdowser/Utilities/HttpPathResolver.cs
+++ b/src/Markdowser/Utilities/HttpPathResolver.cs
@@ -10,7 +10,6 @@ using Markdowser.Models;
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
@@ -40,7 +39,7 @@ public class HttpPathResolver : IPathResolver
relativeOrAbsolutePath = new Uri(new Uri(GlobalState.Url), relativeOrAbsolutePath).ToString();
}
- Debug.WriteLine($"Resolving image: {relativeOrAbsolutePath}");
+ GlobalState.Logger.LogDebug($"Resolving image: {relativeOrAbsolutePath}");
HttpResponseMessage httpResponseMessage;
@@ -80,5 +79,7 @@ public class HttpPathResolver : IPathResolver
{
Dispatcher.UIThread.Post(() => vm.WindowNotificationManager.Show(new Notification("Error", message, NotificationType.Error)));
}
+
+ GlobalState.Logger.LogError(message);
}
}
\ No newline at end of file
diff --git a/src/Markdowser/ViewModels/ContentViewModelBase.cs b/src/Markdowser/ViewModels/ContentViewModelBase.cs
index 95f0edf..13e720a 100644
--- a/src/Markdowser/ViewModels/ContentViewModelBase.cs
+++ b/src/Markdowser/ViewModels/ContentViewModelBase.cs
@@ -1,6 +1,12 @@
-namespace Markdowser.ViewModels;
+using CuteUtils.Logging;
+
+using Markdowser.Utilities;
+
+namespace Markdowser.ViewModels;
public abstract class ContentViewModelBase(string title)
{
public string Title { get; } = title;
+
+ public Logger Logger { get; } = GlobalState.Logger;
}
\ No newline at end of file
diff --git a/src/Markdowser/ViewModels/MainWindowViewModel.cs b/src/Markdowser/ViewModels/MainWindowViewModel.cs
index a26e6a2..00a53e6 100644
--- a/src/Markdowser/ViewModels/MainWindowViewModel.cs
+++ b/src/Markdowser/ViewModels/MainWindowViewModel.cs
@@ -12,7 +12,6 @@ using ReactiveUI;
using System;
using System.Collections.ObjectModel;
-using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Net.Http;
using System.Reflection;
@@ -48,7 +47,7 @@ public partial class MainWindowViewModel : ViewModelBase
_ = this.RaiseAndSetIfChanged(ref currentTab!, value);
- Url = currentTab?.Tag?.ToString() ?? string.Empty;
+ Url = value?.Tag?.ToString() ?? string.Empty;
FetchUrl(true);
}
@@ -134,18 +133,32 @@ public partial class MainWindowViewModel : ViewModelBase
public ICommand CloseTab => ReactiveCommand.Create(() =>
{
+ if (IsBusy)
+ {
+ WindowNotificationManager.Show(new Notification("Busy", "The browser is currently busy.", NotificationType.Warning));
+ return;
+ }
+
if (Tabs.Count > 1)
{
int currentIndex = Tabs.IndexOf(CurrentTab);
- _ = Tabs.Remove(CurrentTab);
CurrentTab = currentIndex > 0 ? Tabs[currentIndex - 1] : Tabs[0];
+
+ Tabs.RemoveAt(currentIndex);
+
this.RaisePropertyChanged(nameof(CloseTabEnabled));
}
});
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() };
Tabs.Add(tab);
CurrentTab = tab;
@@ -163,7 +176,6 @@ public partial class MainWindowViewModel : ViewModelBase
public MainWindowViewModel()
{
- httpClient.DefaultRequestHeaders.UserAgent.ParseAdd($"{nameof(Markdowser)}/{Assembly.GetExecutingAssembly().GetName().Version?.ToString()}");
httpClient.Timeout = TimeSpan.FromSeconds(10);
content = DefaultContent;
@@ -209,7 +221,7 @@ public partial class MainWindowViewModel : ViewModelBase
if (string.IsNullOrWhiteSpace(Url))
{
Content = DefaultContent;
- Debug.WriteLine("URL is empty.");
+ Logger.LogDebug("URL is empty.");
CurrentTab.Header = "New Tab";
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;
Progress = 0;
_ = Task.Run(async () =>
{
- Debug.WriteLine("Fetching URL...");
+ Logger.LogInfo($"Fetching URL: {Url}");
try
{
@@ -279,6 +298,8 @@ public partial class MainWindowViewModel : ViewModelBase
}
Dispatcher.UIThread.Post(() => CurrentTab.Header = Content.Title);
+
+ Logger.LogInfo($"Fetched URL: {Url}");
}
catch (HttpRequestException e)
{
@@ -298,6 +319,8 @@ public partial class MainWindowViewModel : ViewModelBase
_ = errorMessage.AppendLine($"Failed to fetch URL: {e.Message}");
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
+
+ Logger.LogError(e.Message);
}
catch (Exception e)
{
@@ -308,6 +331,8 @@ public partial class MainWindowViewModel : ViewModelBase
Content = new MarkdownContentViewModel("Error", errorMessage.ToString());
Dispatcher.UIThread.Post(() => CurrentTab.Header = $"Error: {e.GetType().Name}");
+
+ Logger.LogError(e.Message);
}
finally
{
diff --git a/src/Markdowser/ViewModels/SettingsViewModel.cs b/src/Markdowser/ViewModels/SettingsViewModel.cs
index 7d80466..b4d9d9c 100644
--- a/src/Markdowser/ViewModels/SettingsViewModel.cs
+++ b/src/Markdowser/ViewModels/SettingsViewModel.cs
@@ -16,6 +16,12 @@ public class SettingsViewModel : ViewModelBase
set => Settings.Current.SearchEngineUrl = value;
}
+ public string UserAgent
+ {
+ get => Settings.Current.UserAgent;
+ set => Settings.Current.UserAgent = value;
+ }
+
public bool DarkMode
{
get => Settings.Current.DarkMode;
diff --git a/src/Markdowser/ViewModels/ViewModelBase.cs b/src/Markdowser/ViewModels/ViewModelBase.cs
index 5c0baf0..41bcadd 100644
--- a/src/Markdowser/ViewModels/ViewModelBase.cs
+++ b/src/Markdowser/ViewModels/ViewModelBase.cs
@@ -2,10 +2,14 @@
using Avalonia.Controls.Notifications;
using Avalonia.Platform;
+using CuteUtils.Logging;
+
using Markdowser.Models;
+using Markdowser.Utilities;
using ReactiveUI;
+using System.Diagnostics;
using System.Reflection;
namespace Markdowser.ViewModels;
@@ -13,12 +17,30 @@ namespace Markdowser.ViewModels;
public class ViewModelBase : ReactiveObject
{
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 WindowNotificationManager WindowNotificationManager => windowNotificationManager!;
+ public Logger Logger => GlobalState.Logger;
+
protected internal void InitializeWindowNotificationManager(Window window)
{
TopLevel? topLevel = TopLevel.GetTopLevel(window);
diff --git a/src/Markdowser/Views/Content/MarkdownContentView.axaml b/src/Markdowser/Views/Content/MarkdownContentView.axaml
index 2627761..3dfdc1d 100644
--- a/src/Markdowser/Views/Content/MarkdownContentView.axaml
+++ b/src/Markdowser/Views/Content/MarkdownContentView.axaml
@@ -26,7 +26,7 @@
-
+