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" /> + + + +