From 73787608c4e9dcb9aee3c13aa0cc1476309afb9a Mon Sep 17 00:00:00 2001 From: estrogen elf <87099578+BlossomiShymae@users.noreply.github.com> Date: Sun, 22 Jun 2025 16:14:04 -0500 Subject: [PATCH] feat: add settings page --- Needlework.Net/Constants/BlobCacheKeys.cs | 2 + Needlework.Net/DataModels/AppSettings.cs | 13 ++ Needlework.Net/Needlework.Net.csproj | 1 + Needlework.Net/Program.cs | 5 + .../MainWindow/MainWindowViewModel.cs | 94 +-------- .../Pages/Settings/SettingsViewModel.cs | 197 ++++++++++++++++++ .../Views/Pages/Settings/SettingsView.axaml | 75 +++++++ .../Pages/Settings/SettingsView.axaml.cs | 11 + 8 files changed, 305 insertions(+), 93 deletions(-) create mode 100644 Needlework.Net/DataModels/AppSettings.cs create mode 100644 Needlework.Net/ViewModels/Pages/Settings/SettingsViewModel.cs create mode 100644 Needlework.Net/Views/Pages/Settings/SettingsView.axaml create mode 100644 Needlework.Net/Views/Pages/Settings/SettingsView.axaml.cs diff --git a/Needlework.Net/Constants/BlobCacheKeys.cs b/Needlework.Net/Constants/BlobCacheKeys.cs index c229115..533902b 100644 --- a/Needlework.Net/Constants/BlobCacheKeys.cs +++ b/Needlework.Net/Constants/BlobCacheKeys.cs @@ -3,5 +3,7 @@ public static class BlobCacheKeys { public static readonly string GithubLatestRelease = nameof(GithubLatestRelease); + + public static readonly string AppSettings = nameof(AppSettings); } } diff --git a/Needlework.Net/DataModels/AppSettings.cs b/Needlework.Net/DataModels/AppSettings.cs new file mode 100644 index 0000000..4d7c782 --- /dev/null +++ b/Needlework.Net/DataModels/AppSettings.cs @@ -0,0 +1,13 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Needlework.Net.DataModels +{ + public partial class AppSettings : ObservableObject + { + [ObservableProperty] + private bool _isCheckForUpdates = true; + + [ObservableProperty] + private bool _isCheckForSchema = true; + } +} diff --git a/Needlework.Net/Needlework.Net.csproj b/Needlework.Net/Needlework.Net.csproj index c1c6e5b..6d06d67 100644 --- a/Needlework.Net/Needlework.Net.csproj +++ b/Needlework.Net/Needlework.Net.csproj @@ -30,6 +30,7 @@ + diff --git a/Needlework.Net/Program.cs b/Needlework.Net/Program.cs index 970acf9..5c3a514 100644 --- a/Needlework.Net/Program.cs +++ b/Needlework.Net/Program.cs @@ -14,6 +14,7 @@ using Needlework.Net.ViewModels.Pages.Console; using Needlework.Net.ViewModels.Pages.Endpoints; using Needlework.Net.ViewModels.Pages.Home; using Needlework.Net.ViewModels.Pages.Schemas; +using Needlework.Net.ViewModels.Pages.Settings; using Needlework.Net.ViewModels.Pages.WebSocket; using Needlework.Net.Views.MainWindow; using Needlework.Net.Views.Pages.About; @@ -21,6 +22,7 @@ using Needlework.Net.Views.Pages.Console; using Needlework.Net.Views.Pages.Endpoints; using Needlework.Net.Views.Pages.Home; using Needlework.Net.Views.Pages.Schemas; +using Needlework.Net.Views.Pages.Settings; using Needlework.Net.Views.Pages.WebSocket; using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia.FontAwesome; @@ -97,6 +99,8 @@ class Program // WEBSOCKET locator.Register(() => new WebSocketView()); locator.Register(() => new EventView()); + // SETTINGS + locator.Register(() => new SettingsView()); builder.AddSingleton(locator); } @@ -132,6 +136,7 @@ class Program builder.AddSingleton(); builder.AddSingleton(); builder.AddSingleton(); + builder.AddSingleton(); } private static void Program_UnhandledException(object sender, UnhandledExceptionEventArgs e) diff --git a/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs b/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs index b0f495e..d0cbdbb 100644 --- a/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs +++ b/Needlework.Net/ViewModels/MainWindow/MainWindowViewModel.cs @@ -1,8 +1,6 @@ using Avalonia; using Avalonia.Media; using Avalonia.Threading; -using BlossomiShymae.Briar; -using BlossomiShymae.Briar.Utils; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Messaging; @@ -11,7 +9,6 @@ using Needlework.Net.Constants; using Needlework.Net.Extensions; using Needlework.Net.Helpers; using Needlework.Net.Messages; -using Needlework.Net.Models; using Needlework.Net.Services; using Needlework.Net.ViewModels.Pages; using Needlework.Net.Views.MainWindow; @@ -20,8 +17,6 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; -using System.Net.Http.Json; -using System.Reactive; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; @@ -33,25 +28,18 @@ public partial class MainWindowViewModel { private readonly DocumentService _documentService; - private readonly GithubService _githubService; - private readonly NotificationService _notificationService; private readonly DialogService _dialogService; private readonly SchemaPaneService _schemaPaneService; - private readonly IDisposable _checkForUpdatesDisposable; - - private readonly IDisposable _checkForSchemaVersionDisposable; - - public MainWindowViewModel(IEnumerable pages, DialogService dialogService, DocumentService documentService, NotificationService notificationService, GithubService githubService, SchemaPaneService schemaPaneService) + public MainWindowViewModel(IEnumerable pages, DialogService dialogService, DocumentService documentService, NotificationService notificationService, SchemaPaneService schemaPaneService) { _dialogService = dialogService; _documentService = documentService; _notificationService = notificationService; _schemaPaneService = schemaPaneService; - _githubService = githubService; NavigationViewItems = pages .OrderBy(p => p.Index) @@ -89,42 +77,6 @@ public partial class MainWindowViewModel } }); - _checkForUpdatesDisposable = Observable.Timer(TimeSpan.Zero, Intervals.CheckForUpdates) - .Select(time => Unit.Default) - .Subscribe(async _ => - { - try - { - await CheckForUpdatesAsync(); - } - catch (Exception ex) - { - var message = "Failed to check for updates. Please check your internet connection or try again later."; - this.Log() - .Error(ex, message); - _notificationService.Notify(AppInfo.Name, message, InfoBarSeverity.Error); - _checkForUpdatesDisposable?.Dispose(); - } - }); - - _checkForSchemaVersionDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(5)) - .Select(time => Unit.Default) - .Subscribe(async _ => - { - try - { - await CheckForSchemaVersionAsync(); - } - catch (Exception ex) - { - var message = "Failed to check for schema version. Please check your internet connection or try again later."; - this.Log() - .Error(ex, message); - _notificationService.Notify(AppInfo.Name, message, InfoBarSeverity.Error); - _checkForSchemaVersionDisposable?.Dispose(); - } - }); - WeakReferenceMessenger.Default.RegisterAll(this); } @@ -151,8 +103,6 @@ public partial class MainWindowViewModel public List NavigationViewItems { get; private set; } = []; - public bool IsSchemaVersionChecked { get; private set; } - public string AppName => AppInfo.Name; public string Title => $"{AppInfo.Name} {AppInfo.Version}"; @@ -224,48 +174,6 @@ public partial class MainWindowViewModel } }; - private async Task CheckForUpdatesAsync() - { - var release = await _githubService.GetLatestReleaseAsync(); - if (release.IsLatest(AppInfo.Version)) - { - this.Log() - .Information("New version available: {TagName}", release.TagName); - _notificationService.Notify(AppInfo.Name, $"New version available: {release.TagName}", InfoBarSeverity.Informational, null, "https://github.com/BlossomiShymae/Needlework.Net/releases/latest"); - _checkForUpdatesDisposable?.Dispose(); - } - } - - - private async Task CheckForSchemaVersionAsync() - { - if (!ProcessFinder.IsPortOpen()) return; - - var lcuSchemaDocument = await _documentService.GetLcuSchemaDocumentAsync(); - var client = Connector.GetLcuHttpClientInstance(); - var currentSemVer = lcuSchemaDocument.Info.Version.Split('.'); - var systemBuild = await client.GetFromJsonAsync("/system/v1/builds") ?? throw new NullReferenceException(); - var latestSemVer = systemBuild.Version.Split('.'); - - if (!IsSchemaVersionChecked) - { - this.Log() - .Information("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version); - this.Log() - .Information("LCU Schema (latest): {Version}", systemBuild.Version); - IsSchemaVersionChecked = true; - } - - bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions - if (!isVersionMatching) - { - this.Log() - .Warning("LCU Schema version mismatch: Current {CurrentVersion}, Latest {LatestVersion}", lcuSchemaDocument.Info.Version, systemBuild.Version); - _notificationService.Notify(AppInfo.Name, $"LCU Schema is possibly outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, null, "https://github.com/dysolix/hasagi-types#updating-the-types"); - _checkForSchemaVersionDisposable?.Dispose(); - } - } - public async Task> PopulateAsync(string? searchText, CancellationToken cancellationToken) { if (searchText == null) return []; diff --git a/Needlework.Net/ViewModels/Pages/Settings/SettingsViewModel.cs b/Needlework.Net/ViewModels/Pages/Settings/SettingsViewModel.cs new file mode 100644 index 0000000..2a9dc34 --- /dev/null +++ b/Needlework.Net/ViewModels/Pages/Settings/SettingsViewModel.cs @@ -0,0 +1,197 @@ +using Akavache; +using Avalonia.Threading; +using BlossomiShymae.Briar; +using BlossomiShymae.Briar.Utils; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using FluentAvalonia.UI.Controls; +using Needlework.Net.Constants; +using Needlework.Net.DataModels; +using Needlework.Net.Extensions; +using Needlework.Net.Models; +using Needlework.Net.Services; +using System; +using System.Net.Http.Json; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; + +namespace Needlework.Net.ViewModels.Pages.Settings +{ + public partial class SettingsViewModel : PageBase, IEnableLogger + { + private readonly IBlobCache _blobCache; + + private readonly IDisposable _checkForUpdatesDisposable; + + private readonly IDisposable _checkForSchemaVersionDisposable; + + private readonly GithubService _githubService; + + private readonly DocumentService _documentService; + + private readonly NotificationService _notificationService; + + private readonly TaskCompletionSource _initializeTaskCompletionSource = new(); + + public SettingsViewModel(IBlobCache blobCache, GithubService githubService, DocumentService documentService, NotificationService notificationService) : base("Settings", "fa-solid fa-gear") + { + _blobCache = blobCache; + _githubService = githubService; + _documentService = documentService; + _notificationService = notificationService; + + _checkForUpdatesDisposable = Observable.Timer(TimeSpan.Zero, Intervals.CheckForUpdates) + .Select(time => Unit.Default) + .Subscribe(async _ => + { + try + { + await _initializeTaskCompletionSource.Task; + if (AppSettings!.IsCheckForUpdates) + { + await CheckForUpdatesAsync(); + } + } + catch (Exception ex) + { + var message = "Failed to check for updates. Please check your internet connection or try again later."; + this.Log() + .Error(ex, message); + _notificationService.Notify(AppInfo.Name, message, InfoBarSeverity.Error); + _checkForUpdatesDisposable?.Dispose(); + } + }); + + _checkForSchemaVersionDisposable = Observable.Timer(TimeSpan.Zero, TimeSpan.FromMinutes(5)) + .Select(time => Unit.Default) + .Subscribe(async _ => + { + try + { + await _initializeTaskCompletionSource.Task; + if (AppSettings!.IsCheckForSchema) + { + await CheckForSchemaVersionAsync(); + } + } + catch (Exception ex) + { + var message = "Failed to check for schema version. Please check your internet connection or try again later."; + this.Log() + .Error(ex, message); + _notificationService.Notify(AppInfo.Name, message, InfoBarSeverity.Error); + _checkForSchemaVersionDisposable?.Dispose(); + } + }); + } + + [ObservableProperty] + private bool _isBusy = true; + + [ObservableProperty] + private AppSettings? _appSettings; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(UpdateCheckTitle), nameof(UpdateCheckIconValue), nameof(UpdateCheckLastChecked))] + private Guid _upToDateGuid = Guid.Empty; + + public bool IsUpToDate { get; private set; } + + public bool IsSchemaVersionChecked { get; private set; } + + public string UpdateCheckTitle => IsUpToDate switch + { + true => "You're up to date", + false => "You're out of date" + }; + + public string UpdateCheckIconValue => IsUpToDate switch + { + true => "fa-heart-circle-check", + false => "fa-heart-circle-exclamation" + }; + + public string UpdateCheckLastChecked => $"Last checked: {DateTime.Now:dddd}, {DateTime.Now:T}"; + + partial void OnAppSettingsChanged(AppSettings? value) + { + if (AppSettings is AppSettings appSettings) + { + _blobCache.InsertObject(BlobCacheKeys.AppSettings, appSettings); + } + } + + public override async Task InitializeAsync() + { + await Dispatcher.UIThread.InvokeAsync(async () => + { + try + { + AppSettings = await _blobCache.GetObject(BlobCacheKeys.AppSettings); + } + catch (Exception ex) + { + this.Log() + .Warning(ex, "Failed to get application settings."); + AppSettings = new(); + } + finally + { + AppSettings!.PropertyChanged += (s, e) => OnAppSettingsChanged((AppSettings?)s); + IsBusy = false; + _initializeTaskCompletionSource.SetResult(true); + } + }); + } + + [RelayCommand] + private async Task CheckForUpdatesAsync() + { + var release = await _githubService.GetLatestReleaseAsync(); + if (release.IsLatest(AppInfo.Version)) + { + this.Log() + .Information("New version available: {TagName}", release.TagName); + _notificationService.Notify(AppInfo.Name, $"New version available: {release.TagName}", InfoBarSeverity.Informational, null, "https://github.com/BlossomiShymae/Needlework.Net/releases/latest"); + _checkForUpdatesDisposable?.Dispose(); + IsUpToDate = false; + } + else + { + IsUpToDate = true; + } + UpToDateGuid = Guid.NewGuid(); + } + + + private async Task CheckForSchemaVersionAsync() + { + if (!ProcessFinder.IsPortOpen()) return; + + var lcuSchemaDocument = await _documentService.GetLcuSchemaDocumentAsync(); + var client = Connector.GetLcuHttpClientInstance(); + var currentSemVer = lcuSchemaDocument.Info.Version.Split('.'); + var systemBuild = await client.GetFromJsonAsync("/system/v1/builds") ?? throw new NullReferenceException(); + var latestSemVer = systemBuild.Version.Split('.'); + + if (!IsSchemaVersionChecked) + { + this.Log() + .Information("LCU Schema (current): {Version}", lcuSchemaDocument.Info.Version); + this.Log() + .Information("LCU Schema (latest): {Version}", systemBuild.Version); + IsSchemaVersionChecked = true; + } + + bool isVersionMatching = currentSemVer[0] == latestSemVer[0] && currentSemVer[1] == latestSemVer[1]; // Compare major and minor versions + if (!isVersionMatching) + { + this.Log() + .Warning("LCU Schema outdated: Current {CurrentVersion}, Latest {LatestVersion}", lcuSchemaDocument.Info.Version, systemBuild.Version); + _notificationService.Notify(AppInfo.Name, $"LCU Schema is outdated compared to latest system build. Consider submitting a pull request on dysolix/hasagi-types.\nCurrent: {string.Join(".", currentSemVer)}\nLatest: {string.Join(".", latestSemVer)}", InfoBarSeverity.Warning, null, "https://github.com/dysolix/hasagi-types#updating-the-types"); + _checkForSchemaVersionDisposable?.Dispose(); + } + } + } +} diff --git a/Needlework.Net/Views/Pages/Settings/SettingsView.axaml b/Needlework.Net/Views/Pages/Settings/SettingsView.axaml new file mode 100644 index 0000000..427af8b --- /dev/null +++ b/Needlework.Net/Views/Pages/Settings/SettingsView.axaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Needlework.Net/Views/Pages/Settings/SettingsView.axaml.cs b/Needlework.Net/Views/Pages/Settings/SettingsView.axaml.cs new file mode 100644 index 0000000..dc91fb5 --- /dev/null +++ b/Needlework.Net/Views/Pages/Settings/SettingsView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.Controls; + +namespace Needlework.Net.Views.Pages.Settings; + +public partial class SettingsView : UserControl +{ + public SettingsView() + { + InitializeComponent(); + } +} \ No newline at end of file