diff --git a/.config/.csharpierrc.json b/.config/.csharpierrc.json
new file mode 100644
index 000000000..c821bbeb8
--- /dev/null
+++ b/.config/.csharpierrc.json
@@ -0,0 +1,4 @@
+{
+ "printWidth": 120,
+ "preprocessorSymbolSets": ["", "DEBUG", "DEBUG,CODE_STYLE"]
+}
diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json
new file mode 100644
index 000000000..5b4bb470d
--- /dev/null
+++ b/.config/dotnet-tools.json
@@ -0,0 +1,24 @@
+{
+ "version": 1,
+ "isRoot": true,
+ "tools": {
+ "husky": {
+ "version": "0.6.0",
+ "commands": [
+ "husky"
+ ]
+ },
+ "xamlstyler.console": {
+ "version": "3.2206.4",
+ "commands": [
+ "xstyler"
+ ]
+ },
+ "csharpier": {
+ "version": "0.25.0",
+ "commands": [
+ "dotnet-csharpier"
+ ]
+ }
+ }
+}
diff --git a/.husky/pre-commit b/.husky/pre-commit
new file mode 100644
index 000000000..92d1a6931
--- /dev/null
+++ b/.husky/pre-commit
@@ -0,0 +1,22 @@
+#!/bin/sh
+. "$(dirname "$0")/_/husky.sh"
+
+## husky task runner examples -------------------
+## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky'
+
+## run all tasks
+#husky run
+
+### run all tasks with group: 'group-name'
+dotnet husky run --group "pre-commit"
+
+## run task with name: 'task-name'
+#husky run --name task-name
+
+## pass hook arguments to task
+#husky run --args "$1" "$2"
+
+## or put your custom commands -------------------
+#echo 'Husky.Net is awesome!'
+
+#dotnet husky run
diff --git a/.husky/task-runner.json b/.husky/task-runner.json
new file mode 100644
index 000000000..8201d1d3f
--- /dev/null
+++ b/.husky/task-runner.json
@@ -0,0 +1,18 @@
+{
+ "tasks": [
+ {
+ "name": "Run csharpier",
+ "group": "pre-commit",
+ "command": "dotnet",
+ "args": [ "csharpier", "${staged}" ],
+ "include": [ "**/*.cs" ]
+ },
+ {
+ "name": "Run xamlstyler",
+ "group": "pre-commit",
+ "command": "dotnet",
+ "args": [ "xstyler", "${staged}" ],
+ "include": [ "**/*.axaml" ]
+ }
+ ]
+}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5102cdfad..8124cac04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,10 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
+## v2.3.4
+### Fixed
+- Fixed [#108](https://github.com/LykosAI/StabilityMatrix/issues/108) - (Linux) Fixed permission error on updates [#103](https://github.com/LykosAI/StabilityMatrix/pull/103)
+
## v2.3.3
### Fixed
- Fixed GPU recognition for Nvidia Tesla GPUs
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml
new file mode 100644
index 000000000..f8d3a9a68
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml
@@ -0,0 +1,74 @@
+
+
+
+
+
+
+ Black
+ Transparent
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs
new file mode 100644
index 000000000..4cd3e8b4b
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Controls/LogViewerControl.axaml.cs
@@ -0,0 +1,52 @@
+using System.Collections.Specialized;
+using Avalonia.Controls;
+using Avalonia.LogicalTree;
+using Avalonia.Threading;
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Controls;
+
+public partial class LogViewerControl : UserControl
+{
+ public LogViewerControl()
+ => InitializeComponent();
+
+ private ILogDataStoreImpl? vm;
+ private LogModel? item;
+
+ protected override void OnDataContextChanged(EventArgs e)
+ {
+ base.OnDataContextChanged(e);
+
+ if (DataContext is null)
+ return;
+
+ vm = (ILogDataStoreImpl)DataContext;
+ vm.DataStore.Entries.CollectionChanged += OnCollectionChanged;
+ }
+
+ private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ item = MyDataGrid.ItemsSource.Cast().LastOrDefault();
+ });
+ }
+
+ protected void OnLayoutUpdated(object? sender, EventArgs e)
+ {
+ if (CanAutoScroll.IsChecked != true || item is null)
+ return;
+
+ MyDataGrid.ScrollIntoView(item, null);
+ item = null;
+ }
+
+ protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
+ {
+ base.OnDetachedFromLogicalTree(e);
+
+ if (vm is null) return;
+ vm.DataStore.Entries.CollectionChanged -= OnCollectionChanged;
+ }
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs
new file mode 100644
index 000000000..9816aa25e
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/ChangeColorTypeConverter.cs
@@ -0,0 +1,25 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using SysDrawColor = System.Drawing.Color;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Converters;
+
+public class ChangeColorTypeConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is null)
+ return new SolidColorBrush((Color)(parameter ?? Colors.Black));
+
+ var sysDrawColor = (SysDrawColor)value!;
+ return new SolidColorBrush(Color.FromArgb(
+ sysDrawColor.A,
+ sysDrawColor.R,
+ sysDrawColor.G,
+ sysDrawColor.B));
+ }
+
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => throw new NotImplementedException();
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs
new file mode 100644
index 000000000..081ce0494
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Converters/EventIdConverter.cs
@@ -0,0 +1,22 @@
+using System.Globalization;
+using Avalonia.Data.Converters;
+using Microsoft.Extensions.Logging;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Converters;
+
+public class EventIdConverter : IValueConverter
+{
+ public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ if (value is null)
+ return "0";
+
+ var eventId = (EventId)value;
+
+ return eventId.ToString();
+ }
+
+ // If not implemented, an error is thrown
+ public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ => new EventId(0, value?.ToString() ?? string.Empty);
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs
new file mode 100644
index 000000000..7c84fa050
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Extensions/LoggerExtensions.cs
@@ -0,0 +1,55 @@
+using Microsoft.Extensions.Logging;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Extensions;
+
+public static class LoggerExtensions
+{
+ public static void Emit(this ILogger logger, EventId eventId,
+ LogLevel logLevel, string message, Exception? exception = null, params object?[] args)
+ {
+ if (logger is null)
+ return;
+
+ //if (!logger.IsEnabled(logLevel))
+ // return;
+
+ switch (logLevel)
+ {
+ case LogLevel.Trace:
+ logger.LogTrace(eventId, message, args);
+ break;
+
+ case LogLevel.Debug:
+ logger.LogDebug(eventId, message, args);
+ break;
+
+ case LogLevel.Information:
+ logger.LogInformation(eventId, message, args);
+ break;
+
+ case LogLevel.Warning:
+ logger.LogWarning(eventId, exception, message, args);
+ break;
+
+ case LogLevel.Error:
+ logger.LogError(eventId, exception, message, args);
+ break;
+
+ case LogLevel.Critical:
+ logger.LogCritical(eventId, exception, message, args);
+ break;
+ }
+ }
+
+ public static void TestPattern(this ILogger logger, EventId eventId)
+ {
+ var exception = new Exception("Test Error Message");
+
+ logger.Emit(eventId, LogLevel.Trace, "Trace Test Pattern");
+ logger.Emit(eventId, LogLevel.Debug, "Debug Test Pattern");
+ logger.Emit(eventId, LogLevel.Information, "Information Test Pattern");
+ logger.Emit(eventId, LogLevel.Warning, "Warning Test Pattern");
+ logger.Emit(eventId, LogLevel.Error, "Error Test Pattern", exception);
+ logger.Emit(eventId, LogLevel.Critical, "Critical Test Pattern", exception);
+ }
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs
new file mode 100644
index 000000000..35d93f0ce
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/DataStoreLoggerConfiguration.cs
@@ -0,0 +1,47 @@
+using System.Drawing;
+using Microsoft.Extensions.Logging;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+public class DataStoreLoggerConfiguration
+{
+ #region Properties
+
+ public EventId EventId { get; set; }
+
+ public Dictionary Colors { get; } = new()
+ {
+ [LogLevel.Trace] = new LogEntryColor
+ {
+ Foreground = Color.DarkGray
+ },
+ [LogLevel.Debug] = new LogEntryColor
+ {
+ Foreground = Color.Gray
+ },
+ [LogLevel.Information] = new LogEntryColor
+ {
+ Foreground = Color.WhiteSmoke,
+ },
+ [LogLevel.Warning] = new LogEntryColor
+ {
+ Foreground = Color.Orange
+ },
+ [LogLevel.Error] = new LogEntryColor
+ {
+ Foreground = Color.White,
+ Background = Color.OrangeRed
+ },
+ [LogLevel.Critical] = new LogEntryColor
+ {
+ Foreground = Color.White,
+ Background = Color.Red
+ },
+ [LogLevel.None] = new LogEntryColor
+ {
+ Foreground = Color.Magenta
+ }
+ };
+
+ #endregion
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs
new file mode 100644
index 000000000..caee277f2
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStore.cs
@@ -0,0 +1,9 @@
+using System.Collections.ObjectModel;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+public interface ILogDataStore
+{
+ ObservableCollection Entries { get; }
+ void AddEntry(LogModel logModel);
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs
new file mode 100644
index 000000000..1f3f38116
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/ILogDataStoreImpl.cs
@@ -0,0 +1,6 @@
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+public interface ILogDataStoreImpl
+{
+ public ILogDataStore DataStore { get; }
+}
\ No newline at end of file
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs
new file mode 100644
index 000000000..3f32ea163
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogDataStore.cs
@@ -0,0 +1,38 @@
+using System.Collections.ObjectModel;
+using Avalonia.Threading;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+public class LogDataStore : ILogDataStore
+{
+ public static LogDataStore Instance { get; } = new();
+
+ #region Fields
+
+ private static readonly SemaphoreSlim _semaphore = new(initialCount: 1);
+
+ #endregion
+
+ #region Properties
+
+ public ObservableCollection Entries { get; } = new();
+
+ #endregion
+
+ #region Methods
+
+ public virtual void AddEntry(LogModel logModel)
+ {
+ // ensure only one operation at time from multiple threads
+ _semaphore.Wait();
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ Entries.Add(logModel);
+ });
+
+ _semaphore.Release();
+ }
+
+ #endregion
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs
new file mode 100644
index 000000000..42c3b6e09
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogEntryColor.cs
@@ -0,0 +1,20 @@
+using System.Drawing;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+public class LogEntryColor
+{
+ public LogEntryColor()
+ {
+ }
+
+ public LogEntryColor(Color foreground, Color background)
+ {
+ Foreground = foreground;
+ Background = background;
+ }
+
+ public Color Foreground { get; set; } = Color.Black;
+ public Color Background { get; set; } = Color.Transparent;
+
+}
\ No newline at end of file
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs
new file mode 100644
index 000000000..af1ddca3d
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/Logging/LogModel.cs
@@ -0,0 +1,33 @@
+using Microsoft.Extensions.Logging;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+public class LogModel
+{
+ #region Properties
+
+ public DateTime Timestamp { get; set; }
+
+ public LogLevel LogLevel { get; set; }
+
+ public EventId EventId { get; set; }
+
+ public object? State { get; set; }
+
+ public string? LoggerName { get; set; }
+
+ public string? CallerClassName { get; set; }
+
+ public string? CallerMemberName { get; set; }
+
+ public string? Exception { get; set; }
+
+ public LogEntryColor? Color { get; set; }
+
+ #endregion
+
+ public string LoggerDisplayName =>
+ LoggerName?
+ .Split('.', StringSplitOptions.RemoveEmptyEntries)
+ .LastOrDefault() ?? "";
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs
new file mode 100644
index 000000000..1789a0e8a
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/LogViewerControlViewModel.cs
@@ -0,0 +1,21 @@
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
+
+public class LogViewerControlViewModel : ViewModel, ILogDataStoreImpl
+{
+ #region Constructor
+
+ public LogViewerControlViewModel(ILogDataStore dataStore)
+ {
+ DataStore = dataStore;
+ }
+
+ #endregion
+
+ #region Properties
+
+ public ILogDataStore DataStore { get; set; }
+
+ #endregion
+}
\ No newline at end of file
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs
new file mode 100644
index 000000000..e6792beaa
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ObservableObject.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel;
+using System.Runtime.CompilerServices;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
+
+public class ObservableObject : INotifyPropertyChanged
+{
+ protected bool Set(ref TValue field, TValue newValue, [CallerMemberName] string? propertyName = null)
+ {
+ if (EqualityComparer.Default.Equals(field, newValue)) return false;
+ field = newValue;
+ OnPropertyChanged(propertyName);
+
+ return true;
+ }
+
+ public event PropertyChangedEventHandler? PropertyChanged;
+
+ protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
+ => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs
new file mode 100644
index 000000000..b7b4aec0a
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Core/ViewModels/ViewModel.cs
@@ -0,0 +1,3 @@
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
+
+public class ViewModel : ObservableObject { /* skip */ }
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs
new file mode 100644
index 000000000..608b72455
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/DataStoreLoggerTarget.cs
@@ -0,0 +1,75 @@
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using NLog;
+using NLog.Targets;
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer;
+
+[Target("DataStoreLogger")]
+public class DataStoreLoggerTarget : TargetWithLayout
+{
+ #region Fields
+
+ private ILogDataStore? _dataStore;
+ private DataStoreLoggerConfiguration? _config;
+
+ #endregion
+
+ #region methods
+
+ protected override void InitializeTarget()
+ {
+ // we need to inject dependencies
+ // var serviceProvider = ResolveService();
+
+ // reference the shared instance
+ _dataStore = LogDataStore.Instance;
+ // _dataStore = serviceProvider.GetRequiredService();
+
+ // load the config options
+ /*var options
+ = serviceProvider.GetService>();*/
+
+ // _config = options?.CurrentValue ?? new DataStoreLoggerConfiguration();
+ _config = new DataStoreLoggerConfiguration();
+
+ base.InitializeTarget();
+ }
+
+ protected override void Write(LogEventInfo logEvent)
+ {
+ // cast NLog Loglevel to Microsoft LogLevel type
+ var logLevel = (MsLogLevel)Enum.ToObject(typeof(MsLogLevel), logEvent.Level.Ordinal);
+
+ // format the message
+ var message = RenderLogEvent(Layout, logEvent);
+
+ // retrieve the EventId
+ logEvent.Properties.TryGetValue("EventId", out var result);
+ if (result is not EventId eventId)
+ {
+ eventId = _config!.EventId;
+ }
+
+ // add log entry
+ _dataStore?.AddEntry(new LogModel
+ {
+ Timestamp = DateTime.UtcNow,
+ LogLevel = logLevel,
+ // do we override the default EventId if it exists?
+ EventId = eventId.Id == 0 && (_config?.EventId.Id ?? 0) != 0 ? _config!.EventId : eventId,
+ State = message,
+ LoggerName = logEvent.LoggerName,
+ CallerClassName = logEvent.CallerClassName,
+ CallerMemberName = logEvent.CallerMemberName,
+ Exception = logEvent.Exception?.Message ?? (logLevel == MsLogLevel.Error ? message : ""),
+ Color = _config!.Colors[logLevel],
+ });
+
+ Debug.WriteLine($"--- [{logLevel.ToString()[..3]}] {message} - {logEvent.Exception?.Message ?? "no error"}");
+ }
+
+ #endregion
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs
new file mode 100644
index 000000000..9ab616045
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Extensions/ServicesExtension.cs
@@ -0,0 +1,65 @@
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using NLog;
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.Logging;
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
+using LogDataStore = StabilityMatrix.Avalonia.Diagnostics.LogViewer.Logging.LogDataStore;
+using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions;
+
+[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
+public static class ServicesExtension
+{
+ public static IServiceCollection AddLogViewer(this IServiceCollection services)
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+
+ return services;
+ }
+
+ public static IServiceCollection AddLogViewer(
+ this IServiceCollection services,
+ Action configure)
+ {
+ services.AddSingleton(Core.Logging.LogDataStore.Instance);
+ services.AddSingleton();
+ services.Configure(configure);
+
+ return services;
+ }
+
+ public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config)
+ {
+ LogManager
+ .Setup()
+ // Register custom Target
+ .SetupExtensions(extensionBuilder =>
+ extensionBuilder.RegisterTarget("DataStoreLogger"));
+
+ /*builder
+ .ClearProviders()
+ .SetMinimumLevel(MsLogLevel.Trace)
+ // Load NLog settings from appsettings*.json
+ .AddNLog(config,
+ // custom options for capturing the EventId information
+ new NLogProviderOptions
+ {
+ // https://nlog-project.org/2021/08/25/nlog-5-0-preview1-ready.html#nlogextensionslogging-changes-capture-of-eventid
+ IgnoreEmptyEventId = false,
+ CaptureEventId = EventIdCaptureType.Legacy
+ });*/
+
+ return builder;
+ }
+
+ public static ILoggingBuilder AddNLogTargets(this ILoggingBuilder builder, IConfiguration config, Action configure)
+ {
+ builder.AddNLogTargets(config);
+ builder.Services.Configure(configure);
+ return builder;
+ }
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE
new file mode 100644
index 000000000..2b8b51633
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Graeme Grant
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs
new file mode 100644
index 000000000..4dbdbf509
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/Logging/LogDataStore.cs
@@ -0,0 +1,13 @@
+using Avalonia.Threading;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.LogViewer.Logging;
+
+public class LogDataStore : Core.Logging.LogDataStore
+{
+ #region Methods
+
+ public override async void AddEntry(Core.Logging.LogModel logModel)
+ => await Dispatcher.UIThread.InvokeAsync(() => base.AddEntry(logModel));
+
+ #endregion
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md
new file mode 100644
index 000000000..ff60c4261
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/LogViewer/README.md
@@ -0,0 +1,3 @@
+## LogViewer
+
+Source code in the `StabilityMatrix.Avalonia.Diagnostics.LogViewer `namespace is included from [CodeProject](https://www.codeproject.com/Articles/5357417/LogViewer-Control-for-WinForms-WPF-and-Avalonia-in) under the [MIT License](LICENSE).
\ No newline at end of file
diff --git a/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
new file mode 100644
index 000000000..cfbcd729e
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/StabilityMatrix.Avalonia.Diagnostics.csproj
@@ -0,0 +1,36 @@
+
+
+
+ net7.0
+ win-x64;linux-x64;osx-x64;osx-arm64
+ enable
+ enable
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ LogWindow.axaml
+
+
+
+
diff --git a/StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs b/StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs
new file mode 100644
index 000000000..860e2b687
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/ViewModels/LogWindowViewModel.cs
@@ -0,0 +1,20 @@
+using Microsoft.Extensions.DependencyInjection;
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Core.ViewModels;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.ViewModels;
+
+public class LogWindowViewModel
+{
+ public LogViewerControlViewModel LogViewer { get; }
+
+ public LogWindowViewModel(LogViewerControlViewModel logViewer)
+ {
+ LogViewer = logViewer;
+ }
+
+ public static LogWindowViewModel FromServiceProvider(IServiceProvider services)
+ {
+ return new LogWindowViewModel(
+ services.GetRequiredService());
+ }
+}
diff --git a/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml
new file mode 100644
index 000000000..b0dc3d650
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml
@@ -0,0 +1,22 @@
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs
new file mode 100644
index 000000000..d3ead4073
--- /dev/null
+++ b/StabilityMatrix.Avalonia.Diagnostics/Views/LogWindow.axaml.cs
@@ -0,0 +1,39 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using StabilityMatrix.Avalonia.Diagnostics.ViewModels;
+
+namespace StabilityMatrix.Avalonia.Diagnostics.Views;
+
+public partial class LogWindow : Window
+{
+ public LogWindow()
+ {
+ InitializeComponent();
+ }
+
+ public static IDisposable Attach(TopLevel root, IServiceProvider serviceProvider)
+ {
+ return Attach(root, serviceProvider, new KeyGesture(Key.F11));
+ }
+
+ public static IDisposable Attach(TopLevel root, IServiceProvider serviceProvider, KeyGesture gesture)
+ {
+ return (root ?? throw new ArgumentNullException(nameof(root))).AddDisposableHandler(
+ KeyDownEvent,
+ PreviewKeyDown,
+ RoutingStrategies.Tunnel);
+
+ void PreviewKeyDown(object? sender, KeyEventArgs e)
+ {
+ if (gesture.Matches(e))
+ {
+ var window = new LogWindow()
+ {
+ DataContext = LogWindowViewModel.FromServiceProvider(serviceProvider)
+ };
+ window.Show();
+ }
+ }
+ }
+}
diff --git a/StabilityMatrix.Avalonia/App.axaml.cs b/StabilityMatrix.Avalonia/App.axaml.cs
index 78baebbaf..9f40327a8 100644
--- a/StabilityMatrix.Avalonia/App.axaml.cs
+++ b/StabilityMatrix.Avalonia/App.axaml.cs
@@ -1,3 +1,7 @@
+#if DEBUG
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer;
+using StabilityMatrix.Avalonia.Diagnostics.LogViewer.Extensions;
+#endif
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
@@ -59,18 +63,26 @@
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using Application = Avalonia.Application;
+using DrawingColor = System.Drawing.Color;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace StabilityMatrix.Avalonia;
public sealed class App : Application
{
- [NotNull] public static IServiceProvider? Services { get; private set; }
- [NotNull] public static Visual? VisualRoot { get; private set; }
- [NotNull] public static IStorageProvider? StorageProvider { get; private set; }
+ [NotNull]
+ public static IServiceProvider? Services { get; private set; }
+
+ [NotNull]
+ public static Visual? VisualRoot { get; private set; }
+
+ [NotNull]
+ public static IStorageProvider? StorageProvider { get; private set; }
+
// ReSharper disable once MemberCanBePrivate.Global
- [NotNull] public static IConfiguration? Config { get; private set; }
-
+ [NotNull]
+ public static IConfiguration? Config { get; private set; }
+
// ReSharper disable once MemberCanBePrivate.Global
public IClassicDesktopStyleApplicationLifetime? DesktopLifetime =>
ApplicationLifetime as IClassicDesktopStyleApplicationLifetime;
@@ -85,11 +97,11 @@ public override void Initialize()
RequestedThemeVariant = ThemeVariant.Dark;
}
}
-
+
public override void OnFrameworkInitializationCompleted()
{
base.OnFrameworkInitializationCompleted();
-
+
if (Design.IsDesignMode)
{
DesignData.DesignData.Initialize();
@@ -114,9 +126,10 @@ public override void OnFrameworkInitializationCompleted()
setupWindow.ShowAsDialog = true;
setupWindow.ShowActivated = true;
setupWindow.ShowAsyncCts = new CancellationTokenSource();
-
- setupWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ?
- ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome;
+
+ setupWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects
+ ? ExtendClientAreaChromeHints.NoChrome
+ : ExtendClientAreaChromeHints.PreferSystemChrome;
DesktopLifetime.MainWindow = setupWindow;
@@ -143,47 +156,55 @@ public override void OnFrameworkInitializationCompleted()
private void ShowMainWindow()
{
- if (DesktopLifetime is null) return;
-
+ if (DesktopLifetime is null)
+ return;
+
var mainViewModel = Services.GetRequiredService();
-
+
var mainWindow = Services.GetRequiredService();
mainWindow.DataContext = mainViewModel;
-
- mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ?
- ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome;
-
+
+ mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects
+ ? ExtendClientAreaChromeHints.NoChrome
+ : ExtendClientAreaChromeHints.PreferSystemChrome;
+
var settingsManager = Services.GetRequiredService();
var windowSettings = settingsManager.Settings.WindowSettings;
if (windowSettings != null && !Program.Args.ResetWindowPosition)
{
mainWindow.Position = new PixelPoint(windowSettings.X, windowSettings.Y);
mainWindow.Width = windowSettings.Width;
- mainWindow.Height = windowSettings.Height;
+ mainWindow.Height = windowSettings.Height;
}
else
{
mainWindow.WindowStartupLocation = WindowStartupLocation.CenterScreen;
}
-
+
mainWindow.Closing += (_, _) =>
{
- var validWindowPosition =
- mainWindow.Screens.All.Any(screen => screen.Bounds.Contains(mainWindow.Position));
+ var validWindowPosition = mainWindow.Screens.All.Any(
+ screen => screen.Bounds.Contains(mainWindow.Position)
+ );
- settingsManager.Transaction(s =>
- {
- s.WindowSettings = new WindowSettings(
- mainWindow.Width, mainWindow.Height,
- validWindowPosition ? mainWindow.Position.X : 0,
- validWindowPosition ? mainWindow.Position.Y : 0);
- }, ignoreMissingLibraryDir: true);
+ settingsManager.Transaction(
+ s =>
+ {
+ s.WindowSettings = new WindowSettings(
+ mainWindow.Width,
+ mainWindow.Height,
+ validWindowPosition ? mainWindow.Position.X : 0,
+ validWindowPosition ? mainWindow.Position.Y : 0
+ );
+ },
+ ignoreMissingLibraryDir: true
+ );
};
mainWindow.Closed += (_, _) => Shutdown();
VisualRoot = mainWindow;
StorageProvider = mainWindow.StorageProvider;
-
+
DesktopLifetime.MainWindow = mainWindow;
DesktopLifetime.Exit += OnExit;
}
@@ -192,49 +213,50 @@ private static void ConfigureServiceProvider()
{
var services = ConfigureServices();
Services = services.BuildServiceProvider();
-
+
var settingsManager = Services.GetRequiredService();
-
+
if (settingsManager.TryFindLibrary())
{
Cultures.TrySetSupportedCulture(settingsManager.Settings.Language);
}
-
+
Services.GetRequiredService().StartEventListener();
}
internal static void ConfigurePageViewModels(IServiceCollection services)
{
- services.AddSingleton()
+ services
+ .AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton()
.AddSingleton();
-
- services.AddSingleton(provider =>
- new MainWindowViewModel(provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService>(),
- provider.GetRequiredService())
- {
- Pages =
- {
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- provider.GetRequiredService(),
- },
- FooterPages =
+
+ services.AddSingleton(
+ provider =>
+ new MainWindowViewModel(
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService>(),
+ provider.GetRequiredService()
+ )
{
- provider.GetRequiredService()
+ Pages =
+ {
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ },
+ FooterPages = { provider.GetRequiredService() }
}
- });
-
+ );
+
// Register disposable view models for shutdown cleanup
- services.AddSingleton(p
- => p.GetRequiredService());
+ services.AddSingleton(p => p.GetRequiredService());
}
internal static void ConfigureDialogViewModels(IServiceCollection services)
@@ -248,43 +270,44 @@ internal static void ConfigureDialogViewModels(IServiceCollection services)
services.AddTransient();
services.AddTransient();
services.AddTransient();
-
+
// Dialog view models (singleton)
services.AddSingleton();
services.AddSingleton();
-
+
// Other transients (usually sub view models)
services.AddTransient();
services.AddTransient();
services.AddTransient();
services.AddTransient();
-
+
// Global progress
services.AddSingleton();
-
+
// Controls
services.AddTransient();
-
+
// Dialog factory
- services.AddSingleton>(provider =>
- new ServiceManager()
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- .Register(provider.GetRequiredService)
- );
+ services.AddSingleton>(
+ provider =>
+ new ServiceManager()
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ .Register(provider.GetRequiredService)
+ );
}
internal static void ConfigureViews(IServiceCollection services)
@@ -297,7 +320,7 @@ internal static void ConfigureViews(IServiceCollection services)
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
-
+
// Dialogs
services.AddTransient();
services.AddTransient();
@@ -305,15 +328,15 @@ internal static void ConfigureViews(IServiceCollection services)
services.AddTransient();
services.AddTransient();
services.AddTransient();
-
+
// Controls
services.AddTransient();
-
+
// Windows
services.AddSingleton();
services.AddSingleton();
}
-
+
internal static void ConfigurePackages(IServiceCollection services)
{
services.AddSingleton();
@@ -333,7 +356,7 @@ private static IServiceCollection ConfigureServices()
ConfigurePageViewModels(services);
ConfigureDialogViewModels(services);
ConfigurePackages(services);
-
+
// Other services
services.AddSingleton();
services.AddSingleton();
@@ -346,23 +369,26 @@ private static IServiceCollection ConfigureServices()
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
-
+ services.AddSingleton();
+
services.AddSingleton();
- services.AddSingleton(provider =>
- (IDisposable) provider.GetRequiredService());
-
+ services.AddSingleton(
+ provider => (IDisposable)provider.GetRequiredService()
+ );
+
// Rich presence
services.AddSingleton();
- services.AddSingleton(provider =>
- provider.GetRequiredService());
+ services.AddSingleton(
+ provider => provider.GetRequiredService()
+ );
Config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
-
+
services.Configure(Config.GetSection(nameof(DebugOptions)));
-
+
if (Compat.IsWindows)
{
services.AddSingleton();
@@ -404,13 +430,13 @@ private static IServiceCollection ConfigureServices()
jsonSerializerOptions.Converters.Add(new ObjectToInferredTypesConverter());
jsonSerializerOptions.Converters.Add(new DefaultUnknownEnumConverter());
jsonSerializerOptions.Converters.Add(
- new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
+ new JsonStringEnumConverter(JsonNamingPolicy.CamelCase)
+ );
jsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
var defaultRefitSettings = new RefitSettings
{
- ContentSerializer =
- new SystemTextJsonContentSerializer(jsonSerializerOptions)
+ ContentSerializer = new SystemTextJsonContentSerializer(jsonSerializerOptions)
};
// HTTP Policies
@@ -422,9 +448,10 @@ private static IServiceCollection ConfigureServices()
HttpStatusCode.ServiceUnavailable, // 503
HttpStatusCode.GatewayTimeout // 504
};
- var delay = Backoff
- .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(80),
- retryCount: 5);
+ var delay = Backoff.DecorrelatedJitterBackoffV2(
+ medianFirstRetryDelay: TimeSpan.FromMilliseconds(80),
+ retryCount: 5
+ );
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.Or()
@@ -433,25 +460,29 @@ private static IServiceCollection ConfigureServices()
// Shorter timeout for local requests
var localTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(3));
- var localDelay = Backoff
- .DecorrelatedJitterBackoffV2(medianFirstRetryDelay: TimeSpan.FromMilliseconds(50),
- retryCount: 3);
+ var localDelay = Backoff.DecorrelatedJitterBackoffV2(
+ medianFirstRetryDelay: TimeSpan.FromMilliseconds(50),
+ retryCount: 3
+ );
var localRetryPolicy = HttpPolicyExtensions
.HandleTransientHttpError()
.Or()
.OrResult(r => retryStatusCodes.Contains(r.StatusCode))
- .WaitAndRetryAsync(localDelay, onRetryAsync: (_, _) =>
- {
- Debug.WriteLine("Retrying local request...");
- return Task.CompletedTask;
- });
+ .WaitAndRetryAsync(
+ localDelay,
+ onRetryAsync: (_, _) =>
+ {
+ Debug.WriteLine("Retrying local request...");
+ return Task.CompletedTask;
+ }
+ );
// named client for update
- services.AddHttpClient("UpdateClient")
- .AddPolicyHandler(retryPolicy);
+ services.AddHttpClient("UpdateClient").AddPolicyHandler(retryPolicy);
// Add Refit clients
- services.AddRefitClient(defaultRefitSettings)
+ services
+ .AddRefitClient(defaultRefitSettings)
.ConfigureHttpClient(c =>
{
c.BaseAddress = new Uri("https://civitai.com");
@@ -460,19 +491,34 @@ private static IServiceCollection ConfigureServices()
.AddPolicyHandler(retryPolicy);
// Add Refit client managers
- services.AddHttpClient("A3Client")
+ services
+ .AddHttpClient("A3Client")
.AddPolicyHandler(localTimeout.WrapAsync(localRetryPolicy));
+ ConditionalAddLogViewer(services);
+
// Add logging
services.AddLogging(builder =>
{
builder.ClearProviders();
- builder.AddFilter("Microsoft.Extensions.Http", LogLevel.Warning)
+ builder
+ .AddFilter("Microsoft.Extensions.Http", LogLevel.Warning)
.AddFilter("Microsoft.Extensions.Http.DefaultHttpClientFactory", LogLevel.Warning)
.AddFilter("Microsoft", LogLevel.Warning)
.AddFilter("System", LogLevel.Warning);
builder.SetMinimumLevel(LogLevel.Debug);
+#if DEBUG
+ builder.AddNLog(
+ ConfigureLogging(),
+ new NLogProviderOptions
+ {
+ IgnoreEmptyEventId = false,
+ CaptureEventId = EventIdCaptureType.Legacy
+ }
+ );
+#else
builder.AddNLog(ConfigureLogging());
+#endif
});
return services;
@@ -486,8 +532,8 @@ private static IServiceCollection ConfigureServices()
/// If Application.Current is null
public static void Shutdown(int exitCode = 0)
{
- if (Current is null) throw new NullReferenceException(
- "Current Application was null when Shutdown called");
+ if (Current is null)
+ throw new NullReferenceException("Current Application was null when Shutdown called");
if (Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
{
lifetime.Shutdown(exitCode);
@@ -501,11 +547,9 @@ private static void OnExit(object? sender, ControlledApplicationLifetimeExitEven
var settingsManager = Services.GetRequiredService();
// If RemoveFolderLinksOnShutdown is set, delete all package junctions
- if (settingsManager is
- {
- IsLibraryDirSet: true,
- Settings.RemoveFolderLinksOnShutdown: true
- })
+ if (
+ settingsManager is { IsLibraryDirSet: true, Settings.RemoveFolderLinksOnShutdown: true }
+ )
{
var sharedFolders = Services.GetRequiredService();
sharedFolders.RemoveLinksForAllPackages();
@@ -524,31 +568,51 @@ private static void OnExit(object? sender, ControlledApplicationLifetimeExitEven
private static LoggingConfiguration ConfigureLogging()
{
- LogManager.Setup().LoadConfiguration(builder => {
- var debugTarget = builder.ForTarget("console").WriteTo(new DebuggerTarget
- {
- Layout = "${message}"
- }).WithAsync();
-
- var fileTarget = builder.ForTarget("logfile").WriteTo(new FileTarget
- {
- Layout = "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
- ArchiveOldFileOnStartup = true,
- FileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
- ArchiveFileName = "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
- ArchiveNumbering = ArchiveNumberingMode.Rolling,
- MaxArchiveFiles = 2
- }).WithAsync();
-
+ var setupBuilder = LogManager.Setup();
+
+ ConditionalAddLogViewerNLog(setupBuilder);
+
+ setupBuilder.LoadConfiguration(builder =>
+ {
+ var debugTarget = builder
+ .ForTarget("console")
+ .WriteTo(new DebuggerTarget { Layout = "${message}" })
+ .WithAsync();
+
+ var fileTarget = builder
+ .ForTarget("logfile")
+ .WriteTo(
+ new FileTarget
+ {
+ Layout =
+ "${longdate}|${level:uppercase=true}|${logger}|${message:withexception=true}",
+ ArchiveOldFileOnStartup = true,
+ FileName =
+ "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.log",
+ ArchiveFileName =
+ "${specialfolder:folder=ApplicationData}/StabilityMatrix/app.{#}.log",
+ ArchiveNumbering = ArchiveNumberingMode.Rolling,
+ MaxArchiveFiles = 2
+ }
+ )
+ .WithAsync();
+
// Filter some sources to be warn levels or above only
builder.ForLogger("System.*").WriteToNil(NLog.LogLevel.Warn);
builder.ForLogger("Microsoft.*").WriteToNil(NLog.LogLevel.Warn);
builder.ForLogger("Microsoft.Extensions.Http.*").WriteToNil(NLog.LogLevel.Warn);
-
+
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(debugTarget);
builder.ForLogger().FilterMinLevel(NLog.LogLevel.Debug).WriteTo(fileTarget);
+
+#if DEBUG
+ var logViewerTarget = builder
+ .ForTarget("DataStoreLogger")
+ .WriteTo(new DataStoreLoggerTarget() { Layout = "${message}" });
+ builder.ForLogger().FilterMinLevel(NLog.LogLevel.Trace).WriteTo(logViewerTarget);
+#endif
});
-
+
// Sentry
if (SentrySdk.IsEnabled)
{
@@ -566,6 +630,27 @@ private static LoggingConfiguration ConfigureLogging()
});
}
+ LogManager.ReconfigExistingLoggers();
+
return LogManager.Configuration;
}
+
+ [Conditional("DEBUG")]
+ private static void ConditionalAddLogViewer(IServiceCollection services)
+ {
+#if DEBUG
+ services.AddLogViewer();
+#endif
+ }
+
+ [Conditional("DEBUG")]
+ private static void ConditionalAddLogViewerNLog(ISetupBuilder setupBuilder)
+ {
+#if DEBUG
+ setupBuilder.SetupExtensions(
+ extensionBuilder =>
+ extensionBuilder.RegisterTarget("DataStoreLogger")
+ );
+#endif
+ }
}
diff --git a/StabilityMatrix.Avalonia/DesignData/DesignData.cs b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
index 0f6405792..53d1688f5 100644
--- a/StabilityMatrix.Avalonia/DesignData/DesignData.cs
+++ b/StabilityMatrix.Avalonia/DesignData/DesignData.cs
@@ -89,6 +89,7 @@ public static void Initialize()
.AddSingleton()
.AddSingleton()
.AddSingleton()
+ .AddSingleton()
.AddSingleton();
// Placeholder services that nobody should need during design time
diff --git a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
index c102805ca..3b56d19a7 100644
--- a/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
+++ b/StabilityMatrix.Avalonia/DesignData/MockLiteDbContext.cs
@@ -14,6 +14,8 @@ public class MockLiteDbContext : ILiteDbContext
public ILiteCollectionAsync CivitModels => throw new NotImplementedException();
public ILiteCollectionAsync CivitModelVersions => throw new NotImplementedException();
public ILiteCollectionAsync CivitModelQueryCache => throw new NotImplementedException();
+ public ILiteCollectionAsync LocalModelFiles => throw new NotImplementedException();
+
public Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3)
{
return Task.FromResult<(CivitModel?, CivitModelVersion?)>((null, null));
diff --git a/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
new file mode 100644
index 000000000..ab52abf84
--- /dev/null
+++ b/StabilityMatrix.Avalonia/DesignData/MockModelIndexService.cs
@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using System.Threading.Tasks;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Database;
+using StabilityMatrix.Core.Services;
+
+namespace StabilityMatrix.Avalonia.DesignData;
+
+public class MockModelIndexService : IModelIndexService
+{
+ ///
+ public Task RefreshIndex()
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task> GetModelsOfType(SharedFolderType type)
+ {
+ return Task.FromResult>(new List());
+ }
+
+ ///
+ public void BackgroundRefreshIndex()
+ {
+ }
+}
diff --git a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
index 4ba1ba234..015312c62 100644
--- a/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
+++ b/StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
@@ -15,7 +15,7 @@
-
+
@@ -39,7 +39,7 @@
-
+
@@ -59,6 +59,10 @@
+
+
+
+
@@ -95,4 +99,10 @@
Resources.resx
+
+
+
+
+
+
diff --git a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
index ba33442d0..6def5d690 100644
--- a/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
+++ b/StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
@@ -50,6 +50,7 @@ public partial class SettingsViewModel : PageViewModelBase
private readonly IPyRunner pyRunner;
private readonly ServiceManager dialogFactory;
private readonly ITrackedDownloadService trackedDownloadService;
+ private readonly IModelIndexService modelIndexService;
public SharedState SharedState { get; }
@@ -114,7 +115,8 @@ public SettingsViewModel(
IPyRunner pyRunner,
ServiceManager dialogFactory,
SharedState sharedState,
- ITrackedDownloadService trackedDownloadService)
+ ITrackedDownloadService trackedDownloadService,
+ IModelIndexService modelIndexService)
{
this.notificationService = notificationService;
this.settingsManager = settingsManager;
@@ -122,7 +124,8 @@ public SettingsViewModel(
this.pyRunner = pyRunner;
this.dialogFactory = dialogFactory;
this.trackedDownloadService = trackedDownloadService;
-
+ this.modelIndexService = modelIndexService;
+
SharedState = sharedState;
SelectedTheme = settingsManager.Settings.Theme ?? AvailableThemes[1];
@@ -501,6 +504,12 @@ private void DebugThrowException()
throw new OperationCanceledException("Example Message");
}
+ [RelayCommand]
+ private async Task DebugRefreshModelsIndex()
+ {
+ await modelIndexService.RefreshIndex();
+ }
+
[RelayCommand]
private async Task DebugTrackedDownload()
{
diff --git a/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs b/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
index ea8ee3985..fca9f57fb 100644
--- a/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/LaunchPageView.axaml.cs
@@ -1,7 +1,6 @@
using System;
using Avalonia.Controls;
using Avalonia.Interactivity;
-using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Threading;
using AvaloniaEdit;
@@ -61,9 +60,4 @@ private void OnScrollToBottomRequested(object? sender, EventArgs e)
editor.ScrollToLine(line);
});
}
-
- private void InitializeComponent()
- {
- AvaloniaXamlLoader.Load(this);
- }
}
diff --git a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
index 603c432c7..149bd99ac 100644
--- a/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
+++ b/StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
@@ -26,6 +26,9 @@
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Processes;
+#if DEBUG
+using StabilityMatrix.Avalonia.Diagnostics.Views;
+#endif
namespace StabilityMatrix.Avalonia.Views;
@@ -42,18 +45,21 @@ public MainWindow()
notificationService = null!;
navigationService = null!;
}
-
- public MainWindow(INotificationService notificationService, INavigationService navigationService)
+
+ public MainWindow(
+ INotificationService notificationService,
+ INavigationService navigationService
+ )
{
this.notificationService = notificationService;
this.navigationService = navigationService;
-
+
InitializeComponent();
-
+
#if DEBUG
this.AttachDevTools();
+ LogWindow.Attach(this, App.Services);
#endif
-
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
}
@@ -62,15 +68,17 @@ public MainWindow(INotificationService notificationService, INavigationService n
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
-
- navigationService.SetFrame(FrameView ?? throw new NullReferenceException("Frame not found"));
-
+
+ navigationService.SetFrame(
+ FrameView ?? throw new NullReferenceException("Frame not found")
+ );
+
// Navigate to first page
if (DataContext is not MainWindowViewModel vm)
{
throw new NullReferenceException("DataContext is not MainWindowViewModel");
}
-
+
navigationService.NavigateTo(vm.Pages[0], new DrillInNavigationTransitionInfo());
}
@@ -79,7 +87,7 @@ protected override void OnOpened(EventArgs e)
base.OnOpened(e);
Application.Current!.ActualThemeVariantChanged += OnActualThemeVariantChanged;
-
+
var theme = ActualThemeVariant;
// Enable mica for Windows 11
if (IsWindows11 && theme != FluentAvaloniaTheme.HighContrastTheme)
@@ -91,11 +99,10 @@ protected override void OnOpened(EventArgs e)
protected override void OnClosing(WindowClosingEventArgs e)
{
// Show confirmation if package running
- var launchPageViewModel = App.Services
- .GetRequiredService();
+ var launchPageViewModel = App.Services.GetRequiredService();
launchPageViewModel.OnMainWindowClosing(e);
-
+
base.OnClosing(e);
}
@@ -104,7 +111,7 @@ protected override void OnLoaded(RoutedEventArgs e)
base.OnLoaded(e);
// Initialize notification service using this window as the visual root
notificationService.Initialize(this);
-
+
// Attach error notification handler for image loader
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
@@ -115,7 +122,7 @@ protected override void OnLoaded(RoutedEventArgs e)
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
-
+
// Detach error notification handler for image loader
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
{
@@ -132,20 +139,22 @@ private void NavigationView_OnItemInvoked(object sender, NavigationViewItemInvok
{
return;
}
-
+
if (nvi.Tag is null)
{
throw new InvalidOperationException("NavigationViewItem Tag is null");
}
-
+
if (nvi.Tag is not ViewModelBase vm)
{
- throw new InvalidOperationException($"NavigationViewItem Tag must be of type ViewModelBase, not {nvi.Tag?.GetType()}");
+ throw new InvalidOperationException(
+ $"NavigationViewItem Tag must be of type ViewModelBase, not {nvi.Tag?.GetType()}"
+ );
}
navigationService.NavigateTo(vm, new BetterEntranceNavigationTransition());
}
}
-
+
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{
if (IsWindows11)
@@ -161,7 +170,7 @@ private void OnActualThemeVariantChanged(object? sender, EventArgs e)
}
}
}
-
+
private void OnImageLoadFailed(object? sender, ImageLoadFailedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
@@ -171,24 +180,30 @@ private void OnImageLoadFailed(object? sender, ImageLoadFailedEventArgs e)
notificationService.ShowPersistent(
"Failed to load image",
$"Could not load '{displayName}'\n({e.Exception.Message})",
- NotificationType.Warning);
+ NotificationType.Warning
+ );
});
}
-
+
private void TryEnableMicaEffect()
{
TransparencyBackgroundFallback = Brushes.Transparent;
TransparencyLevelHint = new[]
{
- WindowTransparencyLevel.Mica,
+ WindowTransparencyLevel.Mica,
WindowTransparencyLevel.AcrylicBlur,
WindowTransparencyLevel.Blur
};
-
+
if (ActualThemeVariant == ThemeVariant.Dark)
{
- var color = this.TryFindResource("SolidBackgroundFillColorBase",
- ThemeVariant.Dark, out var value) ? (Color2)(Color)value! : new Color2(32, 32, 32);
+ var color = this.TryFindResource(
+ "SolidBackgroundFillColorBase",
+ ThemeVariant.Dark,
+ out var value
+ )
+ ? (Color2)(Color)value!
+ : new Color2(32, 32, 32);
color = color.LightenPercent(-0.8f);
@@ -197,8 +212,13 @@ private void TryEnableMicaEffect()
else if (ActualThemeVariant == ThemeVariant.Light)
{
// Similar effect here
- var color = this.TryFindResource("SolidBackgroundFillColorBase",
- ThemeVariant.Light, out var value) ? (Color2)(Color)value! : new Color2(243, 243, 243);
+ var color = this.TryFindResource(
+ "SolidBackgroundFillColorBase",
+ ThemeVariant.Light,
+ out var value
+ )
+ ? (Color2)(Color)value!
+ : new Color2(243, 243, 243);
color = color.LightenPercent(0.5f);
diff --git a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
index cb745ba9d..100d64a96 100644
--- a/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
+++ b/StabilityMatrix.Avalonia/Views/SettingsPage.axaml
@@ -307,6 +307,18 @@
Content="Add Tracked Download" />
+
+
+
+
+
+
diff --git a/StabilityMatrix.Core/Database/ILiteDbContext.cs b/StabilityMatrix.Core/Database/ILiteDbContext.cs
index 27d0c67f8..ac55191c0 100644
--- a/StabilityMatrix.Core/Database/ILiteDbContext.cs
+++ b/StabilityMatrix.Core/Database/ILiteDbContext.cs
@@ -11,6 +11,7 @@ public interface ILiteDbContext : IDisposable
ILiteCollectionAsync CivitModels { get; }
ILiteCollectionAsync CivitModelVersions { get; }
ILiteCollectionAsync CivitModelQueryCache { get; }
+ ILiteCollectionAsync LocalModelFiles { get; }
Task<(CivitModel?, CivitModelVersion?)> FindCivitModelFromFileHashAsync(string hashBlake3);
diff --git a/StabilityMatrix.Core/Database/LiteDbContext.cs b/StabilityMatrix.Core/Database/LiteDbContext.cs
index d0f7fde12..f568222e4 100644
--- a/StabilityMatrix.Core/Database/LiteDbContext.cs
+++ b/StabilityMatrix.Core/Database/LiteDbContext.cs
@@ -27,6 +27,7 @@ public class LiteDbContext : ILiteDbContext
public ILiteCollectionAsync CivitModelVersions => Database.GetCollection("CivitModelVersions");
public ILiteCollectionAsync CivitModelQueryCache => Database.GetCollection("CivitModelQueryCache");
public ILiteCollectionAsync GithubCache => Database.GetCollection("GithubCache");
+ public ILiteCollectionAsync LocalModelFiles => Database.GetCollection("LocalModelFiles");
public LiteDbContext(
ILogger logger,
diff --git a/StabilityMatrix.Core/Models/Database/LocalModelFile.cs b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
new file mode 100644
index 000000000..1ab984cd4
--- /dev/null
+++ b/StabilityMatrix.Core/Models/Database/LocalModelFile.cs
@@ -0,0 +1,47 @@
+using LiteDB;
+
+namespace StabilityMatrix.Core.Models.Database;
+
+///
+/// Represents a locally indexed model file.
+///
+public class LocalModelFile
+{
+ ///
+ /// Relative path to the file from the root model directory.
+ ///
+ [BsonId]
+ public required string RelativePath { get; set; }
+
+ ///
+ /// Type of the model file.
+ ///
+ public required SharedFolderType SharedFolderType { get; set; }
+
+ ///
+ /// Optional connected model information.
+ ///
+ public ConnectedModelInfo? ConnectedModelInfo { get; set; }
+
+ ///
+ /// Optional preview image relative path.
+ ///
+ public string? PreviewImageRelativePath { get; set; }
+
+ public string GetFullPath(string rootModelDirectory)
+ {
+ return Path.Combine(rootModelDirectory, RelativePath);
+ }
+
+ public string? GetPreviewImageFullPath(string rootModelDirectory)
+ {
+ return PreviewImageRelativePath == null ? null
+ : Path.Combine(rootModelDirectory, PreviewImageRelativePath);
+ }
+
+ public static readonly HashSet SupportedCheckpointExtensions =
+ new() { ".safetensors", ".pt", ".ckpt", ".pth", ".bin" };
+ public static readonly HashSet SupportedImageExtensions =
+ new() { ".png", ".jpg", ".jpeg" };
+ public static readonly HashSet SupportedMetadataExtensions = new() { ".json" };
+}
diff --git a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
index 05166c7a3..a682422da 100644
--- a/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
+++ b/StabilityMatrix.Core/Models/FileInterfaces/FilePath.cs
@@ -58,6 +58,11 @@ public FilePath(string path) : base(path)
{
}
+ public FilePath(FileInfo fileInfo) : base(fileInfo.FullName)
+ {
+ _info = fileInfo;
+ }
+
public FilePath(FileSystemPath path) : base(path)
{
}
diff --git a/StabilityMatrix.Core/Services/IModelIndexService.cs b/StabilityMatrix.Core/Services/IModelIndexService.cs
new file mode 100644
index 000000000..ceb6784f7
--- /dev/null
+++ b/StabilityMatrix.Core/Services/IModelIndexService.cs
@@ -0,0 +1,22 @@
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Database;
+
+namespace StabilityMatrix.Core.Services;
+
+public interface IModelIndexService
+{
+ ///
+ /// Refreshes the local model file index.
+ ///
+ Task RefreshIndex();
+
+ ///
+ /// Get all models of the specified type from the existing index.
+ ///
+ Task> GetModelsOfType(SharedFolderType type);
+
+ ///
+ /// Starts a background task to refresh the local model file index.
+ ///
+ void BackgroundRefreshIndex();
+}
diff --git a/StabilityMatrix.Core/Services/ModelIndexService.cs b/StabilityMatrix.Core/Services/ModelIndexService.cs
new file mode 100644
index 000000000..e36808122
--- /dev/null
+++ b/StabilityMatrix.Core/Services/ModelIndexService.cs
@@ -0,0 +1,135 @@
+using System.Diagnostics;
+using Microsoft.Extensions.Logging;
+using StabilityMatrix.Core.Database;
+using StabilityMatrix.Core.Models;
+using StabilityMatrix.Core.Models.Database;
+using StabilityMatrix.Core.Models.FileInterfaces;
+
+namespace StabilityMatrix.Core.Services;
+
+public class ModelIndexService : IModelIndexService
+{
+ private readonly ILogger logger;
+ private readonly ILiteDbContext liteDbContext;
+ private readonly ISettingsManager settingsManager;
+
+ public ModelIndexService(
+ ILogger logger,
+ ILiteDbContext liteDbContext,
+ ISettingsManager settingsManager
+ )
+ {
+ this.logger = logger;
+ this.liteDbContext = liteDbContext;
+ this.settingsManager = settingsManager;
+ }
+
+ ///
+ /// Deletes all entries in the local model file index.
+ ///
+ private async Task ClearIndex()
+ {
+ await liteDbContext.LocalModelFiles.DeleteAllAsync().ConfigureAwait(false);
+ }
+
+ ///
+ public async Task> GetModelsOfType(SharedFolderType type)
+ {
+ return await liteDbContext.LocalModelFiles
+ .Query()
+ .Where(m => m.SharedFolderType == type)
+ .ToArrayAsync().ConfigureAwait(false);
+ }
+
+ ///
+ public async Task RefreshIndex()
+ {
+ var modelsDir = new DirectoryPath(settingsManager.ModelsDirectory);
+
+ // Start
+ var stopwatch = Stopwatch.StartNew();
+ logger.LogInformation("Refreshing model index...");
+
+ using var db
+ = await liteDbContext.Database.BeginTransactionAsync().ConfigureAwait(false);
+
+ var localModelFiles = db.GetCollection("LocalModelFiles")!;
+
+ await localModelFiles.DeleteAllAsync().ConfigureAwait(false);
+
+ // Record start of actual indexing
+ var indexStart = stopwatch.Elapsed;
+
+ var added = 0;
+
+ foreach (
+ var file in modelsDir.Info
+ .EnumerateFiles("*.*", SearchOption.AllDirectories)
+ .Select(info => new FilePath(info))
+ )
+ {
+ // Skip if not supported extension
+ if (!LocalModelFile.SupportedCheckpointExtensions.Contains(file.Info.Extension))
+ {
+ continue;
+ }
+
+ var relativePath = Path.GetRelativePath(modelsDir, file);
+
+ // Get shared folder name
+ var sharedFolderName = relativePath.Split(Path.DirectorySeparatorChar,
+ StringSplitOptions.RemoveEmptyEntries)[0];
+ // Convert to enum
+ var sharedFolderType = Enum.Parse(sharedFolderName, true);
+
+ var localModel = new LocalModelFile
+ {
+ RelativePath = relativePath,
+ SharedFolderType = sharedFolderType,
+ };
+
+ // Try to find a connected model info
+ var jsonPath = file.Directory!.JoinFile(
+ new FilePath(file.NameWithoutExtension, ".cm-info.json"));
+
+ if (jsonPath.Exists)
+ {
+ var connectedModelInfo = ConnectedModelInfo.FromJson(
+ await jsonPath.ReadAllTextAsync().ConfigureAwait(false));
+
+ localModel.ConnectedModelInfo = connectedModelInfo;
+ }
+
+ // Try to find a preview image
+ var previewImagePath = LocalModelFile.SupportedImageExtensions
+ .Select(ext => file.Directory!.JoinFile($"{file.NameWithoutExtension}.preview{ext}")
+ ).FirstOrDefault(path => path.Exists);
+
+ if (previewImagePath != null)
+ {
+ localModel.PreviewImageRelativePath = Path.GetRelativePath(modelsDir, previewImagePath);
+ }
+
+ // Insert into database
+ await localModelFiles.InsertAsync(localModel).ConfigureAwait(false);
+
+ added++;
+ }
+
+ // Record end of actual indexing
+ var indexEnd = stopwatch.Elapsed;
+
+ await db.CommitAsync().ConfigureAwait(false);
+
+ // End
+ stopwatch.Stop();
+ var indexDuration = indexEnd - indexStart;
+ var dbDuration = stopwatch.Elapsed - indexDuration;
+
+ logger.LogInformation("Model index refreshed with {Entries} entries, took {IndexDuration:F1}ms ({DbDuration:F1}ms db)",
+ added, indexDuration.TotalMilliseconds, dbDuration.TotalMilliseconds);
+ }
+
+ ///
+ public void BackgroundRefreshIndex() { }
+}
diff --git a/StabilityMatrix.sln b/StabilityMatrix.sln
index 1f97cd45b..86751cd24 100644
--- a/StabilityMatrix.sln
+++ b/StabilityMatrix.sln
@@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Core", "Sta
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Avalonia", "StabilityMatrix.Avalonia\StabilityMatrix.Avalonia.csproj", "{3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Avalonia.Diagnostics", "StabilityMatrix.Avalonia.Diagnostics\StabilityMatrix.Avalonia.Diagnostics.csproj", "{6D088B89-12D4-4EA0-BA6B-305C7D10C084}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -31,6 +33,10 @@ Global
{3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3F9F96FA-12E7-4B82-BFE5-1FAD88F3ACB2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6D088B89-12D4-4EA0-BA6B-305C7D10C084}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE