From 21ccd84c80cc9527e2177292473fa0898998bd68 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Wed, 25 Mar 2026 23:37:13 -0400 Subject: [PATCH 01/16] feat: DatabaseService + ConfigurationService --- .../Controls/ShortcutsDialog.cs | 2 +- .../Helpers/ComboRowExtensions.cs | 2 +- .../Helpers/GtkBuilderFactory.cs | 2 +- .../HostApplicationBuilderExtensions.cs | 2 +- .../Helpers/IGtkBuilderFactory.cs | 2 +- .../Helpers/LinuxImports.cs | 2 +- .../Helpers/MacOSImports.cs | 2 +- .../Helpers/WindowExtensions.cs | 2 +- .../Helpers/WindowsImports.cs | 2 +- .../Hosting/AdwUserInterfaceContext.cs | 2 +- .../Hosting/AdwUserInterfaceThread.cs | 2 +- Nickvision.Desktop.Tests/EnvironmentTests.cs | 2 +- Nickvision.Desktop.Tests/HostingTests.cs | 2 +- .../JsonFileServiceTests.cs | 2 +- .../KeyringServiceTests.cs | 40 +- Nickvision.Desktop.Tests/MSTestSettings.cs | 2 +- .../Mocks/MockHttpClientFactory.cs | 2 +- Nickvision.Desktop.Tests/Mocks/MockLogger.cs | 2 +- .../NotificationServiceTests.cs | 2 +- .../PasswordGeneratorTests.cs | 2 +- Nickvision.Desktop.Tests/PowerServiceTests.cs | 2 +- .../SecretServiceTests.cs | 2 +- .../UpdaterServiceTests.cs | 2 +- .../UserDirectoriesTests.cs | 2 +- .../Controls/StatusPage.xaml.cs | 2 +- .../Controls/ViewStack.xaml.cs | 2 +- .../Converters/BoolToDoubleConverter.cs | 2 +- .../Converters/BoolToStyleConverter.cs | 2 +- .../Converters/BoolToVisibilityConverter.cs | 2 +- .../Converters/IntToBoolConverter.cs | 2 +- .../NullableToVisibilityConverter.cs | 2 +- .../Converters/StringToVisibilityConverter.cs | 2 +- .../Converters/VisibilityToBoolConverter.cs | 2 +- .../Helpers/BindableSelectionItem.cs | 2 +- .../Helpers/ComboBoxExtensions.cs | 2 +- .../HostApplicationBuilderExtensions.cs | 2 +- .../Helpers/ListViewExtensions.cs | 2 +- .../Helpers/SelectionItemListExtensions.cs | 2 +- .../Hosting/WinUIUserInterfaceContext.cs | 2 +- .../Hosting/WinUIUserInterfaceThread.cs | 2 +- Nickvision.Desktop/Application/AppInfo.cs | 63 --- Nickvision.Desktop/Application/AppVersion.cs | 2 +- .../Application/ArgumentsService.cs | 23 +- .../Application/ConfigurationService.cs | 371 ++++++++++++++ .../Application/DatabaseService.cs | 454 ++++++++++++++++++ .../Application/IArgumentsService.cs | 32 +- .../Application/IConfigurationService.cs | 28 ++ .../Application/IDatabaseService.cs | 30 ++ .../Application/IUpdaterService.cs | 26 - .../Application/PasswordRequiredEventArgs.cs | 13 + .../Application/SelectionItem.cs | 2 +- .../Application/UpdaterService.cs | 47 -- .../Application/WindowGeometry.cs | 35 -- .../NullToDefaultObjectConverter.cs | 2 +- .../Converters/NullToDefaultValueConverter.cs | 2 +- .../Converters/NullToEmptyStringConverter.cs | 2 +- .../Converters/NullToFalseBoolConverter.cs | 2 +- .../Converters/NullToTrueBoolConverter.cs | 2 +- .../Converters/NullToZeroIntConverter.cs | 2 +- .../Filesystem/IJsonFileService.cs | 36 -- .../Filesystem/JsonFileSavedEventArgs.cs | 12 - .../Filesystem/JsonFileService.cs | 47 -- .../Filesystem/UserDirectories.cs | 41 -- .../FreeDesktop/ScreenSaverProxy.cs | 15 - .../FreeDesktop/SecretServiceProxy.cs | 63 +-- .../Globalization/ITranslationService.cs | 125 ----- .../Globalization/TranslationService.cs | 78 --- .../Helpers/GitHubJsonContext.cs | 2 +- Nickvision.Desktop/Helpers/GitHubRelease.cs | 2 +- .../Helpers/GitHubReleaseAsset.cs | 2 +- .../HostApplicationBuilderExtensions.cs | 3 +- .../Helpers/LinuxProcessHelpers.cs | 2 +- .../Helpers/MacOSProcessHelpers.cs | 2 +- .../Helpers/ObjectExtensions.cs | 2 +- .../Helpers/ProcessExtensions.cs | 2 +- Nickvision.Desktop/Helpers/TaskExtensions.cs | 6 - Nickvision.Desktop/Helpers/UriExtensions.cs | 14 - .../Hosting/IUserInterfaceContext.cs | 12 +- .../Hosting/IUserInterfaceThread.cs | 11 +- .../Hosting/UserInterfaceHostedService.cs | 20 +- Nickvision.Desktop/Keyring/Credential.cs | 22 - Nickvision.Desktop/Keyring/IKeyringService.cs | 38 +- Nickvision.Desktop/Keyring/KeyringService.cs | 248 ++-------- Nickvision.Desktop/Keyring/PasswordContent.cs | 3 - .../Keyring/PasswordGenerator.cs | 19 - .../Network/DownloadProgress.cs | 22 - .../Notifications/AppNotification.cs | 20 - .../AppNotificationSentEventArgs.cs | 13 - .../Notifications/INotificationService.cs | 10 - .../Notifications/NotificationService.cs | 10 - .../Notifications/NotificationSeverity.cs | 3 - .../Notifications/ShellNotification.cs | 12 - .../System/DependencySearchOption.cs | 3 - Nickvision.Desktop/System/DeploymentMode.cs | 3 - Nickvision.Desktop/System/Environment.cs | 31 -- Nickvision.Desktop/System/IPowerService.cs | 11 - Nickvision.Desktop/System/ISecretService.cs | 28 -- Nickvision.Desktop/System/PowerService.cs | 25 - Nickvision.Desktop/System/Secret.cs | 3 - Nickvision.Desktop/System/SecretService.cs | 32 -- 100 files changed, 1028 insertions(+), 1281 deletions(-) create mode 100644 Nickvision.Desktop/Application/ConfigurationService.cs create mode 100644 Nickvision.Desktop/Application/DatabaseService.cs create mode 100644 Nickvision.Desktop/Application/IConfigurationService.cs create mode 100644 Nickvision.Desktop/Application/IDatabaseService.cs create mode 100644 Nickvision.Desktop/Application/PasswordRequiredEventArgs.cs diff --git a/Nickvision.Desktop.GNOME/Controls/ShortcutsDialog.cs b/Nickvision.Desktop.GNOME/Controls/ShortcutsDialog.cs index 76e0b0a..b2b77f8 100644 --- a/Nickvision.Desktop.GNOME/Controls/ShortcutsDialog.cs +++ b/Nickvision.Desktop.GNOME/Controls/ShortcutsDialog.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.GNOME.Helpers; +using Nickvision.Desktop.GNOME.Helpers; namespace Nickvision.Desktop.GNOME.Controls; diff --git a/Nickvision.Desktop.GNOME/Helpers/ComboRowExtensions.cs b/Nickvision.Desktop.GNOME/Helpers/ComboRowExtensions.cs index 2a05888..16dba41 100644 --- a/Nickvision.Desktop.GNOME/Helpers/ComboRowExtensions.cs +++ b/Nickvision.Desktop.GNOME/Helpers/ComboRowExtensions.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; using System.Collections.Generic; namespace Nickvision.Desktop.GNOME.Helpers; diff --git a/Nickvision.Desktop.GNOME/Helpers/GtkBuilderFactory.cs b/Nickvision.Desktop.GNOME/Helpers/GtkBuilderFactory.cs index d0a4d43..95f6914 100644 --- a/Nickvision.Desktop.GNOME/Helpers/GtkBuilderFactory.cs +++ b/Nickvision.Desktop.GNOME/Helpers/GtkBuilderFactory.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Globalization; +using Nickvision.Desktop.Globalization; using System; using System.IO; using System.Linq; diff --git a/Nickvision.Desktop.GNOME/Helpers/HostApplicationBuilderExtensions.cs b/Nickvision.Desktop.GNOME/Helpers/HostApplicationBuilderExtensions.cs index 33b16ff..cc67c03 100644 --- a/Nickvision.Desktop.GNOME/Helpers/HostApplicationBuilderExtensions.cs +++ b/Nickvision.Desktop.GNOME/Helpers/HostApplicationBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Nickvision.Desktop.Application; using Nickvision.Desktop.GNOME.Controls; diff --git a/Nickvision.Desktop.GNOME/Helpers/IGtkBuilderFactory.cs b/Nickvision.Desktop.GNOME/Helpers/IGtkBuilderFactory.cs index e7cb389..48c58c4 100644 --- a/Nickvision.Desktop.GNOME/Helpers/IGtkBuilderFactory.cs +++ b/Nickvision.Desktop.GNOME/Helpers/IGtkBuilderFactory.cs @@ -1,4 +1,4 @@ -namespace Nickvision.Desktop.GNOME.Helpers; +namespace Nickvision.Desktop.GNOME.Helpers; public interface IGtkBuilderFactory { diff --git a/Nickvision.Desktop.GNOME/Helpers/LinuxImports.cs b/Nickvision.Desktop.GNOME/Helpers/LinuxImports.cs index 7dd85f3..5163d85 100644 --- a/Nickvision.Desktop.GNOME/Helpers/LinuxImports.cs +++ b/Nickvision.Desktop.GNOME/Helpers/LinuxImports.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace Nickvision.Desktop.GNOME.Helpers; diff --git a/Nickvision.Desktop.GNOME/Helpers/MacOSImports.cs b/Nickvision.Desktop.GNOME/Helpers/MacOSImports.cs index 625b846..826fa00 100644 --- a/Nickvision.Desktop.GNOME/Helpers/MacOSImports.cs +++ b/Nickvision.Desktop.GNOME/Helpers/MacOSImports.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace Nickvision.Desktop.GNOME.Helpers; diff --git a/Nickvision.Desktop.GNOME/Helpers/WindowExtensions.cs b/Nickvision.Desktop.GNOME/Helpers/WindowExtensions.cs index f03dfc4..82f4fd9 100644 --- a/Nickvision.Desktop.GNOME/Helpers/WindowExtensions.cs +++ b/Nickvision.Desktop.GNOME/Helpers/WindowExtensions.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; namespace Nickvision.Desktop.GNOME.Helpers; diff --git a/Nickvision.Desktop.GNOME/Helpers/WindowsImports.cs b/Nickvision.Desktop.GNOME/Helpers/WindowsImports.cs index 6bd01fe..56c3b45 100644 --- a/Nickvision.Desktop.GNOME/Helpers/WindowsImports.cs +++ b/Nickvision.Desktop.GNOME/Helpers/WindowsImports.cs @@ -1,4 +1,4 @@ -using System.Runtime.InteropServices; +using System.Runtime.InteropServices; namespace Nickvision.Desktop.GNOME.Helpers; diff --git a/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceContext.cs b/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceContext.cs index a89c1f7..977c05e 100644 --- a/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceContext.cs +++ b/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceContext.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Hosting; +using Nickvision.Desktop.Hosting; namespace Nickvision.Desktop.GNOME.Hosting; diff --git a/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceThread.cs b/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceThread.cs index 34eb472..335a7d0 100644 --- a/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceThread.cs +++ b/Nickvision.Desktop.GNOME/Hosting/AdwUserInterfaceThread.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Nickvision.Desktop.Application; diff --git a/Nickvision.Desktop.Tests/EnvironmentTests.cs b/Nickvision.Desktop.Tests/EnvironmentTests.cs index a7227c5..c20c7cb 100644 --- a/Nickvision.Desktop.Tests/EnvironmentTests.cs +++ b/Nickvision.Desktop.Tests/EnvironmentTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.System; +using Nickvision.Desktop.System; using System.IO; using System.Linq; using Environment = Nickvision.Desktop.System.Environment; diff --git a/Nickvision.Desktop.Tests/HostingTests.cs b/Nickvision.Desktop.Tests/HostingTests.cs index c4aa02c..7115fd1 100644 --- a/Nickvision.Desktop.Tests/HostingTests.cs +++ b/Nickvision.Desktop.Tests/HostingTests.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Nickvision.Desktop.Application; using Nickvision.Desktop.Filesystem; diff --git a/Nickvision.Desktop.Tests/JsonFileServiceTests.cs b/Nickvision.Desktop.Tests/JsonFileServiceTests.cs index ee71dd8..6f3358d 100644 --- a/Nickvision.Desktop.Tests/JsonFileServiceTests.cs +++ b/Nickvision.Desktop.Tests/JsonFileServiceTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.Tests.Mocks; using System.IO; diff --git a/Nickvision.Desktop.Tests/KeyringServiceTests.cs b/Nickvision.Desktop.Tests/KeyringServiceTests.cs index 9660a1a..2be3037 100644 --- a/Nickvision.Desktop.Tests/KeyringServiceTests.cs +++ b/Nickvision.Desktop.Tests/KeyringServiceTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.Keyring; using Nickvision.Desktop.System; @@ -13,29 +13,24 @@ namespace Nickvision.Desktop.Tests; [TestClass] public sealed class KeyringServiceTests { + private static DatabaseService? _databaseService; private static KeyringService? _keyringService; [TestMethod] public void Case001_Init() { - _keyringService = new KeyringService(new MockLogger(), new AppInfo("org.nickvision.desktop.test", "Nickvision.Desktop.Test", "Test"), new SecretService(new MockLogger())); + _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test", "Nickvision.Desktop.Test", "Test"), new SecretService(new MockLogger())); + _keyringService = new KeyringService(new MockLogger(), _databaseService); + Assert.IsNotNull(_databaseService); Assert.IsNotNull(_keyringService); - Assert.IsTrue(_keyringService.IsSavingToDisk); } [TestMethod] - public void Case002_Check() - { - Assert.IsNotNull(_keyringService); - Assert.IsTrue(_keyringService.IsSavingToDisk); - } - - [TestMethod] - public async Task Case003_Add() + public async Task Case002_Add() { Assert.IsNotNull(_keyringService); Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("YouTube", "abc", "123", new Uri("https://www.youtube.com")))); - Assert.IsNotNull(_keyringService.Credentials.FirstOrDefault(c => c.Name == "YouTube")); + Assert.IsNotNull((await _keyringService.GetAllCredentialAsync()).FirstOrDefault(c => c.Name == "YouTube")); Assert.IsFalse(await _keyringService.AddCredentialAsync(new Credential("YouTube", "abc", "123", new Uri("https://www.youtube.com")))); } @@ -44,11 +39,11 @@ public async Task Case004_Update() { Assert.IsNotNull(_keyringService); Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("Google", "x@gmail.com", "asdfgh123!", new Uri("https://www.google.com")))); - var cred = _keyringService.Credentials.FirstOrDefault(c => c.Name == "Google"); + var cred = (await _keyringService.GetAllCredentialAsync()).FirstOrDefault(c => c.Name == "Google"); Assert.IsNotNull(cred); cred.Password = "newpassword456!"; Assert.IsTrue(await _keyringService.UpdateCredentialAsync(cred)); - var updatedCred = _keyringService.Credentials.FirstOrDefault(c => c.Name == "Google"); + var updatedCred = (await _keyringService.GetAllCredentialAsync()).FirstOrDefault(c => c.Name == "Google"); Assert.IsNotNull(updatedCred); Assert.AreEqual("newpassword456!", updatedCred.Password); } @@ -58,20 +53,23 @@ public async Task Case005_Remove() { Assert.IsNotNull(_keyringService); Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("Example", "user1", "pass1", new Uri("https://www.example.com")))); - var cred = _keyringService.Credentials.FirstOrDefault(c => c.Name == "Example"); + var cred = (await _keyringService.GetAllCredentialAsync()).FirstOrDefault(c => c.Name == "Example"); Assert.IsNotNull(cred); - Assert.IsTrue(await _keyringService.RemoveCredentialAsync(cred)); - Assert.IsNull(_keyringService.Credentials.FirstOrDefault(c => c.Name == "Example")); + Assert.IsTrue(await _keyringService.DeleteCredentialAsync(cred)); + Assert.IsNull((await _keyringService.GetAllCredentialAsync()).FirstOrDefault(c => c.Name == "Example")); } [TestMethod] public async Task Case006_Cleanup() { + var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test", "app.db"); + Assert.IsNotNull(_databaseService); Assert.IsNotNull(_keyringService); - Assert.IsTrue(await _keyringService.DestroyAsync()); - Assert.IsFalse(_keyringService.Credentials.Any()); - Assert.IsFalse(_keyringService.IsSavingToDisk); - Assert.IsFalse(File.Exists(Path.Combine(UserDirectories.Config, "Nickvision", "Keyring", "Nickvision.Desktop.Keyring.Test.ring2"))); + await _databaseService.DisposeAsync(); + File.Delete(path); + Directory.Delete(Path.GetDirectoryName(path)!); + Assert.IsFalse(File.Exists(path)); + _databaseService = null; _keyringService = null; } } diff --git a/Nickvision.Desktop.Tests/MSTestSettings.cs b/Nickvision.Desktop.Tests/MSTestSettings.cs index 7bb65c4..6079489 100644 --- a/Nickvision.Desktop.Tests/MSTestSettings.cs +++ b/Nickvision.Desktop.Tests/MSTestSettings.cs @@ -1 +1 @@ -[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)] +[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.ClassLevel)] diff --git a/Nickvision.Desktop.Tests/Mocks/MockHttpClientFactory.cs b/Nickvision.Desktop.Tests/Mocks/MockHttpClientFactory.cs index a1130e1..84606b9 100644 --- a/Nickvision.Desktop.Tests/Mocks/MockHttpClientFactory.cs +++ b/Nickvision.Desktop.Tests/Mocks/MockHttpClientFactory.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Net.Http; namespace Nickvision.Desktop.Tests.Mocks; diff --git a/Nickvision.Desktop.Tests/Mocks/MockLogger.cs b/Nickvision.Desktop.Tests/Mocks/MockLogger.cs index eb5bb12..b046d50 100644 --- a/Nickvision.Desktop.Tests/Mocks/MockLogger.cs +++ b/Nickvision.Desktop.Tests/Mocks/MockLogger.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging; using System; namespace Nickvision.Desktop.Tests.Mocks; diff --git a/Nickvision.Desktop.Tests/NotificationServiceTests.cs b/Nickvision.Desktop.Tests/NotificationServiceTests.cs index 11ba121..fae8c3b 100644 --- a/Nickvision.Desktop.Tests/NotificationServiceTests.cs +++ b/Nickvision.Desktop.Tests/NotificationServiceTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Filesystem; +using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.Notifications; using System.Threading.Tasks; diff --git a/Nickvision.Desktop.Tests/PasswordGeneratorTests.cs b/Nickvision.Desktop.Tests/PasswordGeneratorTests.cs index 21a78fd..3c317b6 100644 --- a/Nickvision.Desktop.Tests/PasswordGeneratorTests.cs +++ b/Nickvision.Desktop.Tests/PasswordGeneratorTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Keyring; +using Nickvision.Desktop.Keyring; using System; namespace Nickvision.Desktop.Tests; diff --git a/Nickvision.Desktop.Tests/PowerServiceTests.cs b/Nickvision.Desktop.Tests/PowerServiceTests.cs index a5ec6cd..1be3716 100644 --- a/Nickvision.Desktop.Tests/PowerServiceTests.cs +++ b/Nickvision.Desktop.Tests/PowerServiceTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.System; +using Nickvision.Desktop.System; using Nickvision.Desktop.Tests.Mocks; using System.Threading.Tasks; diff --git a/Nickvision.Desktop.Tests/SecretServiceTests.cs b/Nickvision.Desktop.Tests/SecretServiceTests.cs index 180a76b..fefb465 100644 --- a/Nickvision.Desktop.Tests/SecretServiceTests.cs +++ b/Nickvision.Desktop.Tests/SecretServiceTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.System; +using Nickvision.Desktop.System; using Nickvision.Desktop.Tests.Mocks; using System.Threading.Tasks; diff --git a/Nickvision.Desktop.Tests/UpdaterServiceTests.cs b/Nickvision.Desktop.Tests/UpdaterServiceTests.cs index 42e0534..a62bbca 100644 --- a/Nickvision.Desktop.Tests/UpdaterServiceTests.cs +++ b/Nickvision.Desktop.Tests/UpdaterServiceTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.Tests.Mocks; using System; diff --git a/Nickvision.Desktop.Tests/UserDirectoriesTests.cs b/Nickvision.Desktop.Tests/UserDirectoriesTests.cs index 404b372..ce95308 100644 --- a/Nickvision.Desktop.Tests/UserDirectoriesTests.cs +++ b/Nickvision.Desktop.Tests/UserDirectoriesTests.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Filesystem; +using Nickvision.Desktop.Filesystem; using System.IO; namespace Nickvision.Desktop.Tests; diff --git a/Nickvision.Desktop.WinUI/Controls/StatusPage.xaml.cs b/Nickvision.Desktop.WinUI/Controls/StatusPage.xaml.cs index d6962f2..1e4988d 100644 --- a/Nickvision.Desktop.WinUI/Controls/StatusPage.xaml.cs +++ b/Nickvision.Desktop.WinUI/Controls/StatusPage.xaml.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Media; using System.ComponentModel; diff --git a/Nickvision.Desktop.WinUI/Controls/ViewStack.xaml.cs b/Nickvision.Desktop.WinUI/Controls/ViewStack.xaml.cs index 092f0a1..c9757e6 100644 --- a/Nickvision.Desktop.WinUI/Controls/ViewStack.xaml.cs +++ b/Nickvision.Desktop.WinUI/Controls/ViewStack.xaml.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using System.Collections.ObjectModel; diff --git a/Nickvision.Desktop.WinUI/Converters/BoolToDoubleConverter.cs b/Nickvision.Desktop.WinUI/Converters/BoolToDoubleConverter.cs index 8f4fb9a..8fbcbf3 100644 --- a/Nickvision.Desktop.WinUI/Converters/BoolToDoubleConverter.cs +++ b/Nickvision.Desktop.WinUI/Converters/BoolToDoubleConverter.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Data; using System; namespace Nickvision.Desktop.WinUI.Converters; diff --git a/Nickvision.Desktop.WinUI/Converters/BoolToStyleConverter.cs b/Nickvision.Desktop.WinUI/Converters/BoolToStyleConverter.cs index 91f000a..3dc045a 100644 --- a/Nickvision.Desktop.WinUI/Converters/BoolToStyleConverter.cs +++ b/Nickvision.Desktop.WinUI/Converters/BoolToStyleConverter.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; using System; diff --git a/Nickvision.Desktop.WinUI/Converters/BoolToVisibilityConverter.cs b/Nickvision.Desktop.WinUI/Converters/BoolToVisibilityConverter.cs index 194d9c1..35bbd68 100644 --- a/Nickvision.Desktop.WinUI/Converters/BoolToVisibilityConverter.cs +++ b/Nickvision.Desktop.WinUI/Converters/BoolToVisibilityConverter.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; using System; diff --git a/Nickvision.Desktop.WinUI/Converters/IntToBoolConverter.cs b/Nickvision.Desktop.WinUI/Converters/IntToBoolConverter.cs index f151bcf..180fdb1 100644 --- a/Nickvision.Desktop.WinUI/Converters/IntToBoolConverter.cs +++ b/Nickvision.Desktop.WinUI/Converters/IntToBoolConverter.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Data; using System; namespace Nickvision.Desktop.WinUI.Converters; diff --git a/Nickvision.Desktop.WinUI/Converters/NullableToVisibilityConverter.cs b/Nickvision.Desktop.WinUI/Converters/NullableToVisibilityConverter.cs index 0d0f512..20e5a37 100644 --- a/Nickvision.Desktop.WinUI/Converters/NullableToVisibilityConverter.cs +++ b/Nickvision.Desktop.WinUI/Converters/NullableToVisibilityConverter.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; using System; diff --git a/Nickvision.Desktop.WinUI/Converters/StringToVisibilityConverter.cs b/Nickvision.Desktop.WinUI/Converters/StringToVisibilityConverter.cs index 944dcfa..a46a75d 100644 --- a/Nickvision.Desktop.WinUI/Converters/StringToVisibilityConverter.cs +++ b/Nickvision.Desktop.WinUI/Converters/StringToVisibilityConverter.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; using System; diff --git a/Nickvision.Desktop.WinUI/Converters/VisibilityToBoolConverter.cs b/Nickvision.Desktop.WinUI/Converters/VisibilityToBoolConverter.cs index 0fd8f32..d691ad1 100644 --- a/Nickvision.Desktop.WinUI/Converters/VisibilityToBoolConverter.cs +++ b/Nickvision.Desktop.WinUI/Converters/VisibilityToBoolConverter.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Data; using System; diff --git a/Nickvision.Desktop.WinUI/Helpers/BindableSelectionItem.cs b/Nickvision.Desktop.WinUI/Helpers/BindableSelectionItem.cs index 56a8286..c0936c4 100644 --- a/Nickvision.Desktop.WinUI/Helpers/BindableSelectionItem.cs +++ b/Nickvision.Desktop.WinUI/Helpers/BindableSelectionItem.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; using System.ComponentModel; using WinRT; diff --git a/Nickvision.Desktop.WinUI/Helpers/ComboBoxExtensions.cs b/Nickvision.Desktop.WinUI/Helpers/ComboBoxExtensions.cs index 730f691..51b6eba 100644 --- a/Nickvision.Desktop.WinUI/Helpers/ComboBoxExtensions.cs +++ b/Nickvision.Desktop.WinUI/Helpers/ComboBoxExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls; using Nickvision.Desktop.Application; using System.Collections.Generic; using System.Linq; diff --git a/Nickvision.Desktop.WinUI/Helpers/HostApplicationBuilderExtensions.cs b/Nickvision.Desktop.WinUI/Helpers/HostApplicationBuilderExtensions.cs index 51b8029..33cf977 100644 --- a/Nickvision.Desktop.WinUI/Helpers/HostApplicationBuilderExtensions.cs +++ b/Nickvision.Desktop.WinUI/Helpers/HostApplicationBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Nickvision.Desktop.Application; using Nickvision.Desktop.Hosting; diff --git a/Nickvision.Desktop.WinUI/Helpers/ListViewExtensions.cs b/Nickvision.Desktop.WinUI/Helpers/ListViewExtensions.cs index a5a5bc3..1e8288c 100644 --- a/Nickvision.Desktop.WinUI/Helpers/ListViewExtensions.cs +++ b/Nickvision.Desktop.WinUI/Helpers/ListViewExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls; using Nickvision.Desktop.Application; using System.Collections.Generic; using System.Linq; diff --git a/Nickvision.Desktop.WinUI/Helpers/SelectionItemListExtensions.cs b/Nickvision.Desktop.WinUI/Helpers/SelectionItemListExtensions.cs index 153f996..bfd2260 100644 --- a/Nickvision.Desktop.WinUI/Helpers/SelectionItemListExtensions.cs +++ b/Nickvision.Desktop.WinUI/Helpers/SelectionItemListExtensions.cs @@ -1,4 +1,4 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; using System.Collections.Generic; using System.Linq; diff --git a/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceContext.cs b/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceContext.cs index 4da2ba8..d186030 100644 --- a/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceContext.cs +++ b/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceContext.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Dispatching; +using Microsoft.UI.Dispatching; using Nickvision.Desktop.Hosting; namespace Nickvision.Desktop.WinUI.Hosting; diff --git a/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceThread.cs b/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceThread.cs index e798e39..0130d74 100644 --- a/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceThread.cs +++ b/Nickvision.Desktop.WinUI/Hosting/WinUIUserInterfaceThread.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.UI.Dispatching; using Nickvision.Desktop.Hosting; diff --git a/Nickvision.Desktop/Application/AppInfo.cs b/Nickvision.Desktop/Application/AppInfo.cs index 1faca5b..86ac0ac 100644 --- a/Nickvision.Desktop/Application/AppInfo.cs +++ b/Nickvision.Desktop/Application/AppInfo.cs @@ -4,101 +4,41 @@ namespace Nickvision.Desktop.Application; -/// -/// A class containing information about an application. -/// public class AppInfo { - /// - /// A map of artists' names and their emails related to the app. - /// public Dictionary Artists { get; } - /// - /// The changelog of the app in Markdown format. - /// public string? Changelog { get; set; } - /// - /// The description of the app. - /// public string? Description { get; set; } - /// - /// A map of designers' names and their emails related to the app. - /// public Dictionary Designers { get; } - /// - /// A map of developers' names and their emails related to the app. - /// public Dictionary Developers { get; } - /// - /// The url to the discussions forum of the app. - /// public Uri? DiscussionsForum { get; set; } - /// - /// The url to the documentation store of the app. - /// public Uri? DocumentationStore { get; set; } - /// - /// The short name of the app in English (untranslated). - /// public string EnglishShortName { get; init; } - /// - /// A map of extra links' names and their urls related to the app. - /// public Dictionary ExtraLinks { get; } - /// - /// The id of the app. - /// public string Id { get; init; } - /// - /// The url to the issue tracker of the app. - /// public Uri? IssueTracker { get; set; } - /// - /// The name of the app. - /// public string Name { get; init; } - /// - /// The short name of the app (translated). - /// public string? ShortName { get; set; } - /// - /// The url to the source repository of the app. - /// public Uri? SourceRepository { get; set; } - /// - /// The translation credits for the app. - /// public string? TranslationCredits { get; set; } - /// - /// The current running version of the app. - /// public AppVersion? Version { get; set; } - /// - /// Whether or not the app is running in portable mode. - /// public bool IsPortable { get; set; } - /// - /// Constructs an AppInfo. - /// - /// The id of the app - /// The name of the app - /// The short name of the app in English (untranslated) public AppInfo(string id, string name, string englishShortName) { Id = id; @@ -111,8 +51,5 @@ public AppInfo(string id, string name, string englishShortName) IsPortable = false; } - /// - /// The changelog of the app in HTML format. - /// public string? HtmlChangelog => string.IsNullOrEmpty(Changelog) ? null : Markdown.ToHtml(Changelog.Trim()); } diff --git a/Nickvision.Desktop/Application/AppVersion.cs b/Nickvision.Desktop/Application/AppVersion.cs index c9f8eda..1f6a103 100644 --- a/Nickvision.Desktop/Application/AppVersion.cs +++ b/Nickvision.Desktop/Application/AppVersion.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.Json.Serialization; namespace Nickvision.Desktop.Application; diff --git a/Nickvision.Desktop/Application/ArgumentsService.cs b/Nickvision.Desktop/Application/ArgumentsService.cs index 6cb6519..8949dc4 100644 --- a/Nickvision.Desktop/Application/ArgumentsService.cs +++ b/Nickvision.Desktop/Application/ArgumentsService.cs @@ -1,24 +1,15 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; namespace Nickvision.Desktop.Application; -/// -/// A service for accessing application arguments. -/// public class ArgumentsService : IArgumentsService { private readonly List _args; - /// - /// The raw arguments. - /// public IReadOnlyList Data => _args; + public int Count => _args.Count; - /// - /// Constructs an ArgumentService. - /// - /// The raw arguments public ArgumentsService(string[] args) { _args = args.ToList(); @@ -34,18 +25,8 @@ public bool Add(string arg) return true; } - /// - /// Checks if the arguments contains a specific argument - /// - /// The argument to check for - /// True if the arguments contain the specified argument, else false public bool Contains(string arg) => _args.Contains(arg); - /// - /// Gets the next argument after a specific argument. - /// - /// The argument to start from - /// The argument after the specified argument if exists, else null public string? GetNext(string arg) { for (int i = 0; i < _args.Count - 1; i++) diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs new file mode 100644 index 0000000..66e3a83 --- /dev/null +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -0,0 +1,371 @@ +using Microsoft.Extensions.Logging; +using Nickvision.Desktop.Keyring; +using System.Collections.Generic; +using System.Text.Json; +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace Nickvision.Desktop.Application; + +public class ConfigurationService : IConfigurationService +{ + private static readonly string TableName; + + private readonly ILogger _logger; + private readonly IDatabaseService _databaseService; + private readonly Dictionary _cache; + private bool _tableEnsured; + + static ConfigurationService() + { + TableName = "configuration"; + } + + public ConfigurationService(ILogger logger, IDatabaseService databaseService) + { + _logger = logger; + _databaseService = databaseService; + _cache = new Dictionary(); + _tableEnsured = false; + } + + public bool GetBool(string name, bool defaultValue = false) + { + if (_cache.TryGetValue(name, out var value) && value is bool t) + { + return t; + } + _cache[name] = defaultValue; + EnsureTable(); + using var command = _databaseService.SelectFromTable(TableName, "name", name); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + try + { + _cache[name] = bool.Parse(reader.GetString(0)); + } + catch { } + } + return (bool)_cache[name]; + } + + public async Task GetBoolAsync(string name, bool defaultValue = false) + { + if (_cache.TryGetValue(name, out var value) && value is bool t) + { + return t; + } + _cache[name] = defaultValue; + await EnsureTableAsync(); + await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + try + { + _cache[name] = bool.Parse(reader.GetString(0)); + } + catch { } + } + return (bool)_cache[name]; + } + + public double GetDouble(string name, double defaultValue = 0.0) + { + if (_cache.TryGetValue(name, out var value) && value is double t) + { + return t; + } + _cache[name] = defaultValue; + EnsureTable(); + using var command = _databaseService.SelectFromTable(TableName, "name", name); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + try + { + _cache[name] = double.Parse(reader.GetString(0)); + } + catch { } + } + return (double)_cache[name]; + } + + public async Task GetDoubleAsync(string name, double defaultValue = 0.0) + { + if (_cache.TryGetValue(name, out var value) && value is double t) + { + return t; + } + _cache[name] = defaultValue; + await EnsureTableAsync(); + await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + try + { + _cache[name] = double.Parse(reader.GetString(0)); + } + catch { } + } + return (double)_cache[name]; + } + + public int GetInt(string name, int defaultValue = 0) + { + if (_cache.TryGetValue(name, out var value) && value is int t) + { + return t; + } + _cache[name] = defaultValue; + EnsureTable(); + using var command = _databaseService.SelectFromTable(TableName, "name", name); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + try + { + _cache[name] = int.Parse(reader.GetString(0)); + } + catch { } + } + return (int)_cache[name]; + } + + public async Task GetIntAsync(string name, int defaultValue = 0) + { + if (_cache.TryGetValue(name, out var value) && value is int t) + { + return t; + } + _cache[name] = defaultValue; + await EnsureTableAsync(); + await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + try + { + _cache[name] = int.Parse(reader.GetString(0)); + } + catch { } + } + return (int)_cache[name]; + } + + public T GetObject(string name, T defaultValue, JsonTypeInfo info) where T : notnull + { + if (_cache.TryGetValue(name, out var value) && value is T t) + { + return t; + } + _cache[name] = defaultValue; + EnsureTable(); + using var command = _databaseService.SelectFromTable(TableName, "name", name); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + try + { + _cache[name] = JsonSerializer.Deserialize(reader.GetString(0), info)!; + } + catch { } + } + return (T)_cache[name]; + } + + public async Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull + { + if (_cache.TryGetValue(name, out var value) && value is T t) + { + return t; + } + _cache[name] = defaultValue; + await EnsureTableAsync(); + await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + try + { + _cache[name] = JsonSerializer.Deserialize(reader.GetString(0), info)!; + } + catch { } + } + return (T)_cache[name]; + } + + public string GetString(string name, string defaultValue = "") + { + if (_cache.TryGetValue(name, out var value) && value is string t) + { + return t; + } + _cache[name] = defaultValue; + EnsureTable(); + using var command = _databaseService.SelectFromTable(TableName, "name", name); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + try + { + _cache[name] = reader.GetString(0); + } + catch { } + } + return (string)_cache[name]; + } + + public async Task GetStringAsync(string name, string defaultValue = "") + { + if (_cache.TryGetValue(name, out var value) && value is string t) + { + return t; + } + _cache[name] = defaultValue; + await EnsureTableAsync(); + await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + try + { + _cache[name] = reader.GetString(0); + } + catch { } + } + return (string)_cache[name]; + } + + public void Set(string name, bool value) + { + _cache[name] = value; + EnsureTable(); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", value.ToString() } + }); + } + + public void Set(string name, double value) + { + _cache[name] = value; + EnsureTable(); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", value.ToString() } + }); + } + + public void Set(string name, int value) + { + _cache[name] = value; + EnsureTable(); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", value.ToString() } + }); + } + + public void Set(string name, string value) + { + _cache[name] = value; + EnsureTable(); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", value } + }); + } + + public void Set(string name, T value, JsonTypeInfo info) where T : notnull + { + _cache[name] = value; + EnsureTable(); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", JsonSerializer.Serialize(value, info) } + }); + } + + public async Task SetAsync(string name, bool value) + { + _cache[name] = value; + await EnsureTableAsync(); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", value.ToString() } + }); + } + + public async Task SetAsync(string name, double value) + { + _cache[name] = value; + await EnsureTableAsync(); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", value.ToString() } + }); + } + + public async Task SetAsync(string name, int value) + { + _cache[name] = value; + await EnsureTableAsync(); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", value.ToString() } + }); + } + + public async Task SetAsync(string name, string value) + { + _cache[name] = value; + await EnsureTableAsync(); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", value } + }); + } + + public async void SetAsync(string name, T value, JsonTypeInfo info) where T : notnull + { + _cache[name] = value; + await EnsureTableAsync(); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", JsonSerializer.Serialize(value, info) } + }); + } + + private void EnsureTable() + { + if (_tableEnsured) + { + return; + } + _databaseService.EnsureTableExists(TableName, "name TEXT PRIMARY KEY, value TEXT"); + _tableEnsured = true; + } + + private async Task EnsureTableAsync() + { + if (_tableEnsured) + { + return; + } + await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT, value TEXT"); + _tableEnsured = true; + } +} diff --git a/Nickvision.Desktop/Application/DatabaseService.cs b/Nickvision.Desktop/Application/DatabaseService.cs new file mode 100644 index 0000000..e04ccd1 --- /dev/null +++ b/Nickvision.Desktop/Application/DatabaseService.cs @@ -0,0 +1,454 @@ +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; +using Nickvision.Desktop.Filesystem; +using Nickvision.Desktop.System; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +namespace Nickvision.Desktop.Application; + +public class DatabaseService : IAsyncDisposable, IDisposable, IDatabaseService +{ + private readonly ILogger _logger; + private readonly ISecretService _secretService; + private readonly AppInfo _appInfo; + private SqliteConnection? _connection; + + public event EventHandler? PasswordRequired; + + public DatabaseService(ILogger logger, AppInfo appInfo, ISecretService secretService) + { + _logger = logger; + _secretService = secretService; + _appInfo = appInfo; + _connection = null; + } + + ~DatabaseService() + { + Dispose(false); + } + + public bool ContainsInTable(string tableName, string columnName, string matchingValue) + { + EnsureDatabase(); + _logger.LogInformation($"Checking if {tableName} contains value in column ({columnName})..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $param"; + command.Parameters.AddWithValue("$param", matchingValue); + using var reader = command.ExecuteReader(); + var result = false; + while (reader.Read()) + { + if (reader.GetInt32(0) >= 1) + { + result = true; + break; + } + } + if (result) + { + _logger.LogInformation($"Found matching column ({columnName}) value in {tableName}."); + } + else + { + _logger.LogInformation($"Failed to find matching column ({columnName}) value in {tableName}."); + } + return result; + } + + public async Task ContainsInTableAsync(string tableName, string columnName, string matchingValue) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Checking if {tableName} contains value in column ({columnName})..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $param"; + command.Parameters.AddWithValue("$param", matchingValue); + await using var reader = await command.ExecuteReaderAsync(); + var result = false; + while (await reader.ReadAsync()) + { + if (reader.GetInt32(0) >= 1) + { + result = true; + break; + } + } + if (result) + { + _logger.LogInformation($"Found matching column ({columnName}) value in {tableName}."); + } + else + { + _logger.LogInformation($"Failed to find matching column ({columnName}) value in {tableName}."); + } + return result; + } + + public SqliteTransaction CreateTransation() + { + EnsureDatabase(); + _logger.LogInformation("Created database transaction."); + return _connection!.BeginTransaction(); + } + + public async Task CreateTransationAsync() + { + await EnsureDatabaseAsync(); + _logger.LogInformation("Created database transaction."); + return _connection!.BeginTransaction(); + } + + public bool DeleteFromTable(string tableName, string columnName, string matchingValue) + { + EnsureDatabase(); + _logger.LogInformation($"Deleting row from {tableName}..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $param"; + command.Parameters.AddWithValue("$param", matchingValue); + var result = command.ExecuteNonQuery() > 0; + if (result) + { + _logger.LogInformation($"Deleted row from {tableName} successfully."); + + } + else + { + _logger.LogError($"Failed to delete row from {tableName}."); + } + return result; + } + + public async Task DeleteFromTableAsync(string tableName, string columnName, string matchingValue) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Deleting row from {tableName}..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $param"; + command.Parameters.AddWithValue("$param", matchingValue); + var result = await command.ExecuteNonQueryAsync() > 0; + if (result) + { + _logger.LogInformation($"Deleted row from {tableName} successfully."); + + } + else + { + _logger.LogError($"Failed to delete row from {tableName}."); + } + return result; + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + Dispose(false); + GC.SuppressFinalize(this); + } + + public bool EnsureTableExists(string tableName, string layout) + { + EnsureDatabase(); + _logger.LogInformation($"Ensuring table ({tableName}) exists..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({layout})"; + command.ExecuteNonQuery(); + _logger.LogInformation($"Table ({tableName}) exists."); + return true; + } + + public async Task EnsureTableExistsAsync(string tableName, string layout) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Ensuring table ({tableName}) exists..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"CREATE TABLE IF NOT EXISTS {tableName} ({layout})"; + await command.ExecuteNonQueryAsync(); + _logger.LogInformation($"Table ({tableName}) exists."); + return true; + } + + public bool InsertIntoTable(string tableName, Dictionary data) + { + EnsureDatabase(); + _logger.LogInformation($"Insering data into {tableName}..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"INSERT INTO {tableName} ({string.Join(", ", data.Keys)}) VALUES ({string.Join(", ", data.Keys.Select(k => $"${k}"))})"; + foreach (var pair in data) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + var result = command.ExecuteNonQuery() > 0; + if (result) + { + _logger.LogInformation($"Inserted data into {tableName} successfully."); + } + else + { + _logger.LogError($"Failed to insert data into {tableName}."); + } + return result; + } + + public async Task InsertIntoTableAsync(string tableName, Dictionary data) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Insering data into {tableName}..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"INSERT INTO {tableName} ({string.Join(", ", data.Keys)}) VALUES ({string.Join(", ", data.Keys.Select(k => $"${k}"))})"; + foreach (var pair in data) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + var result = await command.ExecuteNonQueryAsync() > 0; + if (result) + { + _logger.LogInformation($"Inserted data into {tableName} successfully."); + } + else + { + _logger.LogError($"Failed to insert data into {tableName}."); + } + return result; + } + + public bool ReplaceIntoTable(string tableName, Dictionary data) + { + EnsureDatabase(); + _logger.LogInformation($"Replacing data into {tableName}..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"INSERT OR REPLACE INTO {tableName} ({string.Join(", ", data.Keys)}) VALUES ({string.Join(", ", data.Keys.Select(k => $"${k}"))})"; + foreach (var pair in data) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + var result = command.ExecuteNonQuery() > 0; + if (result) + { + _logger.LogInformation($"Inserted data into {tableName} successfully."); + } + else + { + _logger.LogError($"Failed to insert data into {tableName}."); + } + return result; + } + + public async Task ReplaceIntoTableAsync(string tableName, Dictionary data) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Insering data into {tableName}..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"INSERT OR REPLACE INTO {tableName} ({string.Join(", ", data.Keys)}) VALUES ({string.Join(", ", data.Keys.Select(k => $"${k}"))})"; + foreach (var pair in data) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + var result = await command.ExecuteNonQueryAsync() > 0; + if (result) + { + _logger.LogInformation($"Inserted data into {tableName} successfully."); + } + else + { + _logger.LogError($"Failed to insert data into {tableName}."); + } + return result; + } + + public SqliteCommand SelectFromTable(string tableName, string columnName, string matchingValue) + { + EnsureDatabase(); + _logger.LogInformation($"Selecting data from table {tableName} with matching column ({columnName})..."); + var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT * FROM {tableName} WHERE {columnName} = $param"; + command.Parameters.AddWithValue("$param", matchingValue); + _logger.LogInformation($"Selected data from table {tableName} with matching column ({columnName})."); + return command; + } + + public async Task SelectFromTableAsync(string tableName, string columnName, string matchingValue) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Selecting data from table {tableName} with matching column ({columnName})..."); + var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT * FROM {tableName} WHERE {columnName} = $param"; + command.Parameters.AddWithValue("$param", matchingValue); + _logger.LogInformation($"Selected data from table {tableName} with matching column ({columnName})."); + return command; + } + + public SqliteCommand SelectAllFromTable(string tableName) + { + EnsureDatabase(); + _logger.LogInformation($"Selecting all data from table {tableName}..."); + var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT * FROM {tableName}"; + _logger.LogInformation($"Selected all data from table {tableName}."); + return command; + } + + public async Task SelectAllFromTableAsync(string tableName) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Selecting all data from table {tableName}..."); + var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT * FROM {tableName}"; + _logger.LogInformation($"Selected all data from table {tableName}."); + return command; + } + + public bool UpdateInTable(string tableName, string columnName, string matchingValue, Dictionary newData) + { + EnsureDatabase(); + _logger.LogInformation($"Updating data in {tableName}..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"UPDATE {tableName} SET {string.Join(", ", newData.Keys.Select(k => $"{k} = ${k}"))}"; + foreach (var pair in newData) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + var result = command.ExecuteNonQuery() > 0; + if (result) + { + _logger.LogInformation($"Updated data in {tableName} successfully."); + } + else + { + _logger.LogError($"Failed to update data in {tableName}."); + } + return result; + } + + public async Task UpdateInTableAsync(string tableName, string columnName, string matchingValue, Dictionary newData) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Updating data in {tableName}..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"UPDATE {tableName} SET {string.Join(", ", newData.Keys.Select(k => $"{k} = ${k}"))}"; + foreach (var pair in newData) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + var result = await command.ExecuteNonQueryAsync() > 0; + if (result) + { + _logger.LogInformation($"Updated data in {tableName} successfully."); + } + else + { + _logger.LogError($"Failed to update data in {tableName}."); + } + return result; + } + + private void EnsureDatabase() + { + if (_connection is not null) + { + return; + } + var path = Path.Combine(_appInfo.IsPortable ? System.Environment.ExecutingDirectory : Path.Combine(UserDirectories.Config, _appInfo.Name), "app.db"); + _logger.LogInformation($"Opening application database ({path})..."); + var secret = string.Empty; + if (!_appInfo.IsPortable && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())) + { + secret = ((Task.Run(() => _secretService.GetAsync(_appInfo.Id)).GetAwaiter().GetResult()) ?? (Task.Run(() => _secretService.CreateAsync(_appInfo.Id)).GetAwaiter().GetResult()))?.Value; + } + while (string.IsNullOrEmpty(secret)) + { + _logger.LogInformation("Empty secret value. Sending password required event..."); + var args = new PasswordRequiredEventArgs(); + PasswordRequired?.Invoke(this, args); + secret = args.Password; + } + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _connection = new SqliteConnection(new SqliteConnectionStringBuilder($"Data Source='{path}'") + { + Mode = SqliteOpenMode.ReadWriteCreate, + Password = secret, + Pooling = false + }.ToString()); + try + { + _connection.Open(); + _logger.LogInformation($"Opened application database ({path})."); + } + catch (SqliteException e) + { + _logger.LogError($"Failed to open application database: {e}"); + _connection.Dispose(); + _connection = null; + } + } + + private async Task EnsureDatabaseAsync() + { + if (_connection is not null) + { + return; + } + var path = Path.Combine(_appInfo.IsPortable ? System.Environment.ExecutingDirectory : Path.Combine(UserDirectories.Config, _appInfo.Name), "app.db"); + _logger.LogInformation($"Opening application database ({path})..."); + var secret = string.Empty; + if (!_appInfo.IsPortable && (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsLinux())) + { + secret = ((await _secretService.GetAsync(_appInfo.Id)) ?? (await _secretService.CreateAsync(_appInfo.Id)))?.Value; + } + while (string.IsNullOrEmpty(secret)) + { + _logger.LogInformation("Empty secret value. Sending password required event..."); + var args = new PasswordRequiredEventArgs(); + PasswordRequired?.Invoke(this, args); + secret = args.Password; + } + Directory.CreateDirectory(Path.GetDirectoryName(path)!); + _connection = new SqliteConnection(new SqliteConnectionStringBuilder($"Data Source='{path}'") + { + Mode = SqliteOpenMode.ReadWriteCreate, + Password = secret, + Pooling = false + }.ToString()); + try + { + await _connection.OpenAsync(); + _logger.LogInformation($"Opened application database ({path})."); + } + catch (SqliteException e) + { + _logger.LogError($"Failed to open application database: {e}"); + await _connection.DisposeAsync(); + _connection = null; + } + } + + protected virtual async ValueTask DisposeAsyncCore() + { + if (_connection is not null) + { + await _connection.CloseAsync(); + await _connection.DisposeAsync().ConfigureAwait(false); + } + _connection = null; + } + + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + _connection?.Close(); + _connection?.Dispose(); + _connection = null; + } +} diff --git a/Nickvision.Desktop/Application/IArgumentsService.cs b/Nickvision.Desktop/Application/IArgumentsService.cs index 47a1c38..5ca4db1 100644 --- a/Nickvision.Desktop/Application/IArgumentsService.cs +++ b/Nickvision.Desktop/Application/IArgumentsService.cs @@ -1,33 +1,13 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Nickvision.Desktop.Application; -/// -/// An interface for a service that access application arguments. -/// public interface IArgumentsService { - /// - /// The raw arguments. - /// - public IReadOnlyList Data { get; } + IReadOnlyList Data { get; } + int Count { get; } - /// - /// Adds the unique argument. - /// - /// The argument to add - /// True if the argument was added, else false - public bool Add(string arg); - /// - /// Checks if the arguments contains a specific argument - /// - /// The argument to check for - /// True if the arguments contain the specified argument, else false - public bool Contains(string arg); - /// - /// Gets the next argument after a specific argument. - /// - /// The argument to start from - /// The argument after the specified argument if exists, else null - public string? GetNext(string arg); + bool Add(string arg); + bool Contains(string arg); + string? GetNext(string arg); } diff --git a/Nickvision.Desktop/Application/IConfigurationService.cs b/Nickvision.Desktop/Application/IConfigurationService.cs new file mode 100644 index 0000000..377bb52 --- /dev/null +++ b/Nickvision.Desktop/Application/IConfigurationService.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization.Metadata; +using System.Threading.Tasks; + +namespace Nickvision.Desktop.Application; + +public interface IConfigurationService +{ + bool GetBool(string name, bool defaultValue = false); + Task GetBoolAsync(string name, bool defaultValue = false); + double GetDouble(string name, double defaultValue = 0.0); + Task GetDoubleAsync(string name, double defaultValue = 0.0); + int GetInt(string name, int defaultValue = 0); + Task GetIntAsync(string name, int defaultValue = 0); + T GetObject(string name, T defaultValue, JsonTypeInfo info) where T : notnull; + Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull; + string GetString(string name, string defaultValue = ""); + Task GetStringAsync(string name, string defaultValue = ""); + void Set(string name, bool value); + void Set(string name, double value); + void Set(string name, int value); + void Set(string name, string value); + void Set(string name, T value, JsonTypeInfo info) where T : notnull; + Task SetAsync(string name, bool value); + Task SetAsync(string name, double value); + Task SetAsync(string name, int value); + Task SetAsync(string name, string value); + void SetAsync(string name, T value, JsonTypeInfo info) where T : notnull; +} diff --git a/Nickvision.Desktop/Application/IDatabaseService.cs b/Nickvision.Desktop/Application/IDatabaseService.cs new file mode 100644 index 0000000..e950d1c --- /dev/null +++ b/Nickvision.Desktop/Application/IDatabaseService.cs @@ -0,0 +1,30 @@ +using Microsoft.Data.Sqlite; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Nickvision.Desktop.Application; + +public interface IDatabaseService +{ + event EventHandler? PasswordRequired; + + bool ContainsInTable(string tableName, string columnName, string matchingValue); + Task ContainsInTableAsync(string tableName, string columnName, string matchingValue); + SqliteTransaction CreateTransation(); + Task CreateTransationAsync(); + bool DeleteFromTable(string tableName, string columnName, string matchingValue); + Task DeleteFromTableAsync(string tableName, string columnName, string matchingValue); + bool EnsureTableExists(string tableName, string layout); + Task EnsureTableExistsAsync(string tableName, string layout); + bool InsertIntoTable(string tableName, Dictionary data); + Task InsertIntoTableAsync(string tableName, Dictionary data); + bool ReplaceIntoTable(string tableName, Dictionary data); + Task ReplaceIntoTableAsync(string tableName, Dictionary data); + SqliteCommand SelectFromTable(string tableName, string columnName, string matchingValue); + Task SelectFromTableAsync(string tableName, string columnName, string matchingValue); + SqliteCommand SelectAllFromTable(string tableName); + Task SelectAllFromTableAsync(string tableName); + bool UpdateInTable(string tableName, string columnName, string matchingValue, Dictionary newData); + Task UpdateInTableAsync(string tableName, string columnName, string matchingValue, Dictionary newData); +} diff --git a/Nickvision.Desktop/Application/IUpdaterService.cs b/Nickvision.Desktop/Application/IUpdaterService.cs index 30fc4ea..0d6618f 100644 --- a/Nickvision.Desktop/Application/IUpdaterService.cs +++ b/Nickvision.Desktop/Application/IUpdaterService.cs @@ -4,39 +4,13 @@ namespace Nickvision.Desktop.Application; -/// -/// An interface of a service for updating an application. -/// public interface IUpdaterService { - /// - /// Downloads an asset from a released version. - /// - /// The released version - /// The path of where to download the asset to - /// The name of the asset to download - /// Whether the asset name should match exactly to the asset to download - /// An optional progress reporter - /// Task DownloadReleaseAssetAsync(AppVersion version, string path, string assertName, bool exactMatch = true, IProgress? progress = null); - /// - /// Gets the latest preview version available. - /// - /// The latest preview version or null if unavailable Task GetLatestPreviewVersionAsync(); - /// - /// Gets the latest stable version available. - /// - /// The latest stable version or null if unavailable Task GetLatestStableVersionAsync(); - /// - /// Downloads and runs the updated Windows installer of the given released version. - /// - /// The released version - /// An optional progress reporter - /// True if the update was downloaded and ran successfully, else false Task WindowsApplicationUpdateAsync(AppVersion version, IProgress? progress = null); } diff --git a/Nickvision.Desktop/Application/PasswordRequiredEventArgs.cs b/Nickvision.Desktop/Application/PasswordRequiredEventArgs.cs new file mode 100644 index 0000000..314cbc8 --- /dev/null +++ b/Nickvision.Desktop/Application/PasswordRequiredEventArgs.cs @@ -0,0 +1,13 @@ +using System; + +namespace Nickvision.Desktop.Application; + +public class PasswordRequiredEventArgs : EventArgs +{ + public string Password { get; set; } + + public PasswordRequiredEventArgs() + { + Password = string.Empty; + } +} diff --git a/Nickvision.Desktop/Application/SelectionItem.cs b/Nickvision.Desktop/Application/SelectionItem.cs index f638ad4..b3b5bab 100644 --- a/Nickvision.Desktop/Application/SelectionItem.cs +++ b/Nickvision.Desktop/Application/SelectionItem.cs @@ -1,4 +1,4 @@ -using System.ComponentModel; +using System.ComponentModel; using System.Runtime.CompilerServices; namespace Nickvision.Desktop.Application; diff --git a/Nickvision.Desktop/Application/UpdaterService.cs b/Nickvision.Desktop/Application/UpdaterService.cs index d94bde4..83d2aeb 100644 --- a/Nickvision.Desktop/Application/UpdaterService.cs +++ b/Nickvision.Desktop/Application/UpdaterService.cs @@ -18,9 +18,6 @@ namespace Nickvision.Desktop.Application; -/// -/// A service for updating an application via GitHub releases. -/// public class UpdaterService : IDisposable, IUpdaterService { private readonly ILogger _logger; @@ -30,13 +27,6 @@ public class UpdaterService : IDisposable, IUpdaterService private readonly string _name; private readonly string _cacheReleasesPath; - /// - /// Constructs an UpdaterService. - /// - /// Logger for the service - /// The AppInfo object for the app - /// The HttpClient for the app - /// Thrown if the AppInfo.SourceRepository is missing or ill-formated [ActivatorUtilitiesConstructor] public UpdaterService(ILogger logger, AppInfo appInfo, IHttpClientFactory httpClientFactory) { @@ -64,14 +54,6 @@ public UpdaterService(ILogger logger, AppInfo appInfo, IHttpClie Directory.CreateDirectory(Path.GetDirectoryName(_cacheReleasesPath)!); } - /// - /// Constructs an UpdaterService. - /// - /// Logger for the service - /// The repository owner - /// The repository name - /// The HttpClient for the app - /// Thrown if the Owner and/or Name are empty public UpdaterService(ILogger logger, string owner, string name, HttpClient httpClient) { if (string.IsNullOrEmpty(owner) || string.IsNullOrEmpty(name)) @@ -87,32 +69,17 @@ public UpdaterService(ILogger logger, string owner, string name, _cacheReleasesPath = Path.Combine(UserDirectories.Cache, $"{_owner}-{_name}-releases.json"); } - /// - /// Destructs an UpdaterService. - /// ~UpdaterService() { Dispose(false); } - /// - /// Frees resources used by the UpdaterService. - /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } - /// - /// Downloads an asset from a released version. - /// - /// The released version - /// The path of where to download the asset to - /// The name of the asset to download - /// Whether the asset name should match exactly to the asset to download - /// An optional progress reporter - /// public async Task DownloadReleaseAssetAsync(AppVersion version, string path, string assertName, bool exactMatch = true, IProgress? progress = null) { _logger.LogInformation($"Starting download of asset ({assertName}{(exactMatch ? string.Empty : "*")}) for {_owner}/{_name} version {version}..."); @@ -190,10 +157,6 @@ public async Task DownloadReleaseAssetAsync(AppVersion version, string pat return false; } - /// - /// Gets the latest preview version available. - /// - /// The latest preview version or null if unavailable public async Task GetLatestPreviewVersionAsync() { var releases = await GetReleasesAsync(); @@ -208,10 +171,6 @@ public async Task DownloadReleaseAssetAsync(AppVersion version, string pat return null; } - /// - /// Gets the latest stable version available. - /// - /// The latest stable version or null if unavailable public async Task GetLatestStableVersionAsync() { var releases = await GetReleasesAsync(); @@ -226,12 +185,6 @@ public async Task DownloadReleaseAssetAsync(AppVersion version, string pat return null; } - /// - /// Downloads and runs the updated Windows installer of the given released version. - /// - /// The released version - /// An optional progress reporter - /// True if the update was downloaded and ran successfully, else false public async Task WindowsApplicationUpdateAsync(AppVersion version, IProgress? progress = null) { _logger.LogInformation($"Starting Windows application update for {_owner}/{_name} version {version}..."); diff --git a/Nickvision.Desktop/Application/WindowGeometry.cs b/Nickvision.Desktop/Application/WindowGeometry.cs index 69f9bb2..e5b7af9 100644 --- a/Nickvision.Desktop/Application/WindowGeometry.cs +++ b/Nickvision.Desktop/Application/WindowGeometry.cs @@ -1,62 +1,27 @@ namespace Nickvision.Desktop.Application; -/// -/// A class containing window geometry information. -/// public class WindowGeometry { - /// - /// The height of the window. - /// public int Height { get; set; } - /// - /// Whether the window is maximized. - /// public bool IsMaximized { get; set; } - /// - /// The width of the window. - /// public int Width { get; set; } - /// - /// The x position of the window. - /// public int X { get; set; } - /// - /// The y position of the window. - /// public int Y { get; set; } - /// - /// Constructs a WindowGeometry. - /// public WindowGeometry() : this(900, 700, false, 10, 10) { } - /// - /// Constructs a WindowGeometry. - /// - /// The width of the window - /// The height of the window - /// Whether the window is maximized public WindowGeometry(int width, int height, bool isMaximized) : this(width, height, isMaximized, 10, 10) { } - /// - /// Constructs a WindowGeometry. - /// - /// The width of the window - /// The height of the window - /// Whether the window is maximized - /// The x position of the window - /// The y position of the window public WindowGeometry(int width, int height, bool isMaximized, int x, int y) { Width = width; diff --git a/Nickvision.Desktop/Converters/NullToDefaultObjectConverter.cs b/Nickvision.Desktop/Converters/NullToDefaultObjectConverter.cs index 2f0b797..e4655de 100644 --- a/Nickvision.Desktop/Converters/NullToDefaultObjectConverter.cs +++ b/Nickvision.Desktop/Converters/NullToDefaultObjectConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; diff --git a/Nickvision.Desktop/Converters/NullToDefaultValueConverter.cs b/Nickvision.Desktop/Converters/NullToDefaultValueConverter.cs index 2630b28..85cc0e9 100644 --- a/Nickvision.Desktop/Converters/NullToDefaultValueConverter.cs +++ b/Nickvision.Desktop/Converters/NullToDefaultValueConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.Json; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; diff --git a/Nickvision.Desktop/Converters/NullToEmptyStringConverter.cs b/Nickvision.Desktop/Converters/NullToEmptyStringConverter.cs index 9d0cc7f..0a90375 100644 --- a/Nickvision.Desktop/Converters/NullToEmptyStringConverter.cs +++ b/Nickvision.Desktop/Converters/NullToEmptyStringConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Nickvision.Desktop/Converters/NullToFalseBoolConverter.cs b/Nickvision.Desktop/Converters/NullToFalseBoolConverter.cs index 20a0baa..df9c08a 100644 --- a/Nickvision.Desktop/Converters/NullToFalseBoolConverter.cs +++ b/Nickvision.Desktop/Converters/NullToFalseBoolConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Nickvision.Desktop/Converters/NullToTrueBoolConverter.cs b/Nickvision.Desktop/Converters/NullToTrueBoolConverter.cs index 343e8af..574ad37 100644 --- a/Nickvision.Desktop/Converters/NullToTrueBoolConverter.cs +++ b/Nickvision.Desktop/Converters/NullToTrueBoolConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Nickvision.Desktop/Converters/NullToZeroIntConverter.cs b/Nickvision.Desktop/Converters/NullToZeroIntConverter.cs index 50a6c1a..7c21bdb 100644 --- a/Nickvision.Desktop/Converters/NullToZeroIntConverter.cs +++ b/Nickvision.Desktop/Converters/NullToZeroIntConverter.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/Nickvision.Desktop/Filesystem/IJsonFileService.cs b/Nickvision.Desktop/Filesystem/IJsonFileService.cs index 0c6dc7f..1e22c15 100644 --- a/Nickvision.Desktop/Filesystem/IJsonFileService.cs +++ b/Nickvision.Desktop/Filesystem/IJsonFileService.cs @@ -4,48 +4,12 @@ namespace Nickvision.Desktop.Filesystem; -/// -/// An interface of a service for working with json files. -/// public interface IJsonFileService { - /// - /// The event for when json files are saved. - /// event EventHandler? Saved; - /// - /// Loads a json file and deserializes it into an object. - /// - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to deserialize to - /// A deserialized object from the json file if successful, else a default constructed object T Load(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new(); - /// - /// Loads a json file and deserializes it into an object asynchronously. - /// - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to deserialize to - /// A deserialized object from the json file if successful, else a default constructed object Task LoadAsync(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new(); - /// - /// Saves an object by serializing it into a json file. - /// - /// The object to serialize - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to serialize - /// True if the file was saved successfully, else false bool Save(T obj, JsonTypeInfo jsonTypeInfo, string? name = null); - /// - /// Saves an object by serializing it into a json file asynchronously. - /// - /// The object to serialize - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to serialize - /// True if the file was saved successfully, else false Task SaveAsync(T obj, JsonTypeInfo jsonTypeInfo, string? name = null); } diff --git a/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs b/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs index cba3ad2..4bb712e 100644 --- a/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs +++ b/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs @@ -2,24 +2,12 @@ namespace Nickvision.Desktop.Filesystem; -/// -/// A class of event arguments for when a json file is saved. -/// public class JsonFileSavedEventArgs : EventArgs { - /// - /// The object that was saved to a json file. - /// public object Data { get; init; } - /// - /// The type of the saved object. - /// public Type DataType { get; init; } - /// - /// The name of the json file (without the .json extension). - /// public string Name { get; init; } public JsonFileSavedEventArgs(object data, Type dataType, string name) diff --git a/Nickvision.Desktop/Filesystem/JsonFileService.cs b/Nickvision.Desktop/Filesystem/JsonFileService.cs index e926c84..12b33c5 100644 --- a/Nickvision.Desktop/Filesystem/JsonFileService.cs +++ b/Nickvision.Desktop/Filesystem/JsonFileService.cs @@ -8,35 +8,18 @@ namespace Nickvision.Desktop.Filesystem; -/// -/// A service for working with json files on disk. -/// public class JsonFileService : IJsonFileService { private readonly ILogger _logger; private readonly string _directory; - /// - /// The event for when json files are saved. - /// public event EventHandler? Saved; - /// - /// Constructs a JsonFileService. - /// - /// Logger for the service - /// The AppInfo object for the app public JsonFileService(ILogger logger, AppInfo appInfo) : this(logger, appInfo.IsPortable ? System.Environment.ExecutingDirectory : Path.Combine(UserDirectories.Config, appInfo.Name)) { } - /// - /// Constructs a JsonFileService. - /// - /// Logger for the service - /// The directory of where to load and save json files from - /// The directory will be created if it doesn't exist private JsonFileService(ILogger logger, string directory) { _logger = logger; @@ -47,13 +30,6 @@ private JsonFileService(ILogger logger, string directory) _directory = directory; } - /// - /// Loads a json file and deserializes it into an object. - /// - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to deserialize to - /// A deserialized object from the json file if successful, else a default constructed object public T Load(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new() { var path = Path.Combine(_directory, $"{(string.IsNullOrEmpty(name) ? typeof(T).Name.ToLower() : name)}.json"); @@ -77,13 +53,6 @@ private JsonFileService(ILogger logger, string directory) } } - /// - /// Loads a json file and deserializes it into an object asynchronously. - /// - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to deserialize to - /// A deserialized object from the json file if successful, else a default constructed object public async Task LoadAsync(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new() { var path = Path.Combine(_directory, $"{(string.IsNullOrEmpty(name) ? typeof(T).Name.ToLower() : name)}.json"); @@ -107,14 +76,6 @@ private JsonFileService(ILogger logger, string directory) } } - /// - /// Saves an object by serializing it into a json file. - /// - /// The object to serialize - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to serialize - /// True if the file was saved successfully, else false public bool Save(T obj, JsonTypeInfo jsonTypeInfo, string? name = null) { if (obj is null) @@ -139,14 +100,6 @@ public bool Save(T obj, JsonTypeInfo jsonTypeInfo, string? name = null) } } - /// - /// Saves an object by serializing it into a json file asynchronously. - /// - /// The object to serialize - /// The JsonTypeInfo for T, obtained from a source-generated JsonSerializerContext - /// The name of the json file (without the .json extension) - /// The type of the object to serialize - /// True if the file was saved successfully, else false public async Task SaveAsync(T obj, JsonTypeInfo jsonTypeInfo, string? name = null) { if (obj is null) diff --git a/Nickvision.Desktop/Filesystem/UserDirectories.cs b/Nickvision.Desktop/Filesystem/UserDirectories.cs index 9b2a29a..6375682 100644 --- a/Nickvision.Desktop/Filesystem/UserDirectories.cs +++ b/Nickvision.Desktop/Filesystem/UserDirectories.cs @@ -6,19 +6,10 @@ namespace Nickvision.Desktop.Filesystem; -/// -/// A helper class for getting user directories cross-platform. -/// public static class UserDirectories { - /// - /// The user's home directory. - /// public static string Home => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - /// - /// The user's config directory. - /// public static string Config { get @@ -55,9 +46,6 @@ public static string Config } } - /// - /// The user's cache directory. - /// public static string Cache { get @@ -94,9 +82,6 @@ public static string Cache } } - /// - /// The user's local data directory. - /// public static string LocalData { get @@ -133,9 +118,6 @@ public static string LocalData } } - /// - /// The user's desktop directory. - /// public static string Desktop { get @@ -172,9 +154,6 @@ public static string Desktop } } - /// - /// The user's documents directory. - /// public static string Documents { get @@ -210,9 +189,6 @@ public static string Documents } } - /// - /// The user's downloads directory. - /// public static string Downloads { get @@ -261,9 +237,6 @@ public static string Downloads } } - /// - /// The user's music directory. - /// public static string Music { get @@ -300,9 +273,6 @@ public static string Music } } - /// - /// The user's pictures directory. - /// public static string Pictures { get @@ -339,9 +309,6 @@ public static string Pictures } } - /// - /// The user's templates directory. - /// public static string Templates { get @@ -378,9 +345,6 @@ public static string Templates } } - /// - /// The user's videos directory. - /// public static string Videos { get @@ -417,11 +381,6 @@ public static string Videos } } - /// - /// Gets an XDG user directory from the user-dirs.dirs file or environment variable. - /// - /// The name of the XDG user directory to get - /// The path of the XDG user directory if found, else null private static string? GetXdgUserDir(string name) { if (string.IsNullOrEmpty(name)) diff --git a/Nickvision.Desktop/FreeDesktop/ScreenSaverProxy.cs b/Nickvision.Desktop/FreeDesktop/ScreenSaverProxy.cs index 30aab58..e264017 100644 --- a/Nickvision.Desktop/FreeDesktop/ScreenSaverProxy.cs +++ b/Nickvision.Desktop/FreeDesktop/ScreenSaverProxy.cs @@ -3,22 +3,12 @@ namespace Nickvision.Desktop.FreeDesktop; -/// -/// Internal proxy for the org.freedesktop.ScreenSaver D-Bus interface. -/// internal static class ScreenSaverProxy { private const string Service = "org.freedesktop.ScreenSaver"; private const string Path = "/org/freedesktop/ScreenSaver"; private const string Interface = "org.freedesktop.ScreenSaver"; - /// - /// Inhibits the screen saver, preventing system suspend. - /// - /// The D-Bus connection - /// The name of the inhibiting application - /// The reason for inhibiting - /// A cookie that can be used to uninhibit internal static async Task InhibitAsync(DBusConnection connection, string applicationName, string reasonForInhibit) { MessageBuffer buffer; @@ -36,11 +26,6 @@ internal static async Task InhibitAsync(DBusConnection connection, string }, null); } - /// - /// Removes the screen saver inhibit, re-allowing system suspend. - /// - /// The D-Bus connection - /// The cookie returned by InhibitAsync internal static async Task UnInhibitAsync(DBusConnection connection, uint cookie) { MessageBuffer buffer; diff --git a/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs b/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs index 31ac4c7..a649684 100644 --- a/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs +++ b/Nickvision.Desktop/FreeDesktop/SecretServiceProxy.cs @@ -6,10 +6,6 @@ namespace Nickvision.Desktop.FreeDesktop; -/// -/// Internal proxy for the org.freedesktop.secrets D-Bus interface (freedesktop Secret Service). -/// Uses "plain" (unencrypted) session transport, which is safe for local D-Bus sockets. -/// internal sealed class SecretServiceProxy : IDisposable { private const string SecretsBus = "org.freedesktop.secrets"; @@ -30,10 +26,6 @@ private SecretServiceProxy(DBusConnection connection, string sessionPath) _disposed = false; } - /// - /// Connects to the D-Bus session bus and opens a plain encryption session with the secrets service. - /// - /// A connected SecretServiceProxy, or null if the secrets service is unavailable internal static async Task ConnectAsync() { var sessionAddress = DBusAddress.Session; @@ -52,9 +44,6 @@ private SecretServiceProxy(DBusConnection connection, string sessionPath) return new SecretServiceProxy(connection, sessionPath); } - /// - /// Gets the object path of the default collection, or null/slash if it does not exist. - /// internal async Task GetDefaultCollectionPathAsync() { MessageBuffer buffer; @@ -71,12 +60,6 @@ private SecretServiceProxy(DBusConnection connection, string sessionPath) }, null); } - /// - /// Creates a collection with the given label and alias. - /// - /// The human-readable label of the collection - /// The alias (e.g. "default") - /// The object path of the created collection, or null on failure internal async Task CreateCollectionAsync(string label, string alias) { MessageBuffer buffer; @@ -100,11 +83,6 @@ private SecretServiceProxy(DBusConnection connection, string sessionPath) }, null); } - /// - /// Unlocks the given object (collection or item path), prompting the user if required. - /// - /// The D-Bus object path to unlock - /// True if unlocked successfully, false if the user dismissed the prompt internal async Task UnlockAsync(string objectPath) { MessageBuffer buffer; @@ -129,11 +107,6 @@ internal async Task UnlockAsync(string objectPath) return await PromptAsync(promptPath); } - /// - /// Invokes a Secret Service prompt and waits for the user to complete or dismiss it. - /// - /// The D-Bus object path of the prompt - /// True if the user completed the prompt, false if dismissed private async Task PromptAsync(string promptPath) { var tcs = new TaskCompletionSource(); @@ -167,15 +140,6 @@ private async Task PromptAsync(string promptPath) return await tcs.Task; } - /// - /// Creates an item in the specified collection. - /// - /// The collection object path - /// The label for the new item - /// Lookup attributes for the item - /// The secret value - /// Whether to replace an existing item with the same attributes - /// The object path of the created item, or null on failure internal async Task CreateItemAsync(string collectionPath, string label, Dictionary attributes, string value, bool replace = false) { var attrDict = new Dict(); @@ -210,11 +174,6 @@ private async Task PromptAsync(string promptPath) }, null); } - /// - /// Searches for items across all collections that match the given attributes. - /// - /// Attributes to match - /// A tuple of unlocked and locked item object paths internal async Task<(string[] Unlocked, string[] Locked)> SearchItemsAsync(Dictionary attributes) { MessageBuffer buffer; @@ -251,11 +210,6 @@ private async Task PromptAsync(string promptPath) }, null); } - /// - /// Gets the secret value of the specified item. - /// - /// The item object path - /// The secret value as a string, or null on failure internal async Task GetSecretAsync(string itemPath) { MessageBuffer buffer; @@ -280,7 +234,7 @@ private async Task PromptAsync(string promptPath) } catch (DBusErrorReplyException e) { - if(e.ErrorName == "org.freedesktop.Secret.Error.IsLocked") + if (e.ErrorName == "org.freedesktop.Secret.Error.IsLocked") { return null; } @@ -288,11 +242,6 @@ private async Task PromptAsync(string promptPath) return null; } - /// - /// Sets the secret value of the specified item. - /// - /// The item object path - /// The new secret value internal async Task SetSecretAsync(string itemPath, string value) { MessageBuffer buffer; @@ -309,10 +258,6 @@ internal async Task SetSecretAsync(string itemPath, string value) await _connection.CallMethodAsync(buffer); } - /// - /// Deletes the specified item. - /// - /// The item object path internal async Task DeleteItemAsync(string itemPath) { MessageBuffer buffer; @@ -329,9 +274,6 @@ await _connection.CallMethodAsync(buffer, static (Message m, object? _) => }, null); } - /// - /// Opens a "plain" encryption session with the secrets service. - /// private static async Task OpenSessionAsync(DBusConnection connection) { MessageBuffer buffer; @@ -351,9 +293,6 @@ private static async Task OpenSessionAsync(DBusConnection connection) }, null); } - /// - /// Disposes the SecretServiceProxy and its underlying D-Bus connection. - /// public void Dispose() { if (!_disposed) diff --git a/Nickvision.Desktop/Globalization/ITranslationService.cs b/Nickvision.Desktop/Globalization/ITranslationService.cs index 7188d1d..85ecd0d 100644 --- a/Nickvision.Desktop/Globalization/ITranslationService.cs +++ b/Nickvision.Desktop/Globalization/ITranslationService.cs @@ -2,165 +2,40 @@ namespace Nickvision.Desktop.Globalization; -/// -/// An interface for a service for translations. -/// public interface ITranslationService { - /// - /// The list of available language codes for translations. - /// IEnumerable AvailableLanguages { get; } - /// - /// The language code for translations. - /// - /// - /// An empty string will use the system's language code for translations. The language code "C" will cause strings - /// to remain untranslated - /// string Language { get; set; } - /// - /// Translates a string. - /// - /// The string to translate. - /// The translated string string _(string text); - /// - /// Translates a format string. - /// - /// The format string to translate. - /// The arguments for the format string - /// The translated format string string _(string text, params object[] args); - /// - /// Translates a possible plural string. - /// - /// The non-plural string to translate - /// The plural string to translate - /// The number of objects - /// The translated plural string if n != 1, else the translated non-plural string string _n(string text, string pluralText, long n); - /// - /// Translates a possible plural format string. - /// - /// The non-plural format string to translate - /// The plural format string to translate - /// The number of objects - /// The arguments for the format string - /// The translated format plural string if n != 1, else the translated non-plural format string string _n(string text, string pluralText, long n, params object[] args); - /// - /// Translates a string for a particular context. - /// - /// The context of the string - /// The string to translate - /// The translated string for the context string _p(string context, string text); - /// - /// Translates a format string for a particular context. - /// - /// The context of the string - /// The format string to translate - /// The arguments for the format string - /// The translated format string for the context string _p(string context, string text, params object[] args); - /// - /// Translates a possible plural string for a particular context. - /// - /// The context of the string - /// The non-plural string to translate - /// The plural string to translate - /// The number of objects - /// The translated plural string if n != 1, else the translated non-plural string for the context string _pn(string context, string text, string pluralText, long n); - /// - /// Translates a possible plural format string for a particular context. - /// - /// The context of the string - /// The non-plural format string to translate - /// The plural format string to translate - /// The number of objects - /// The arguments for the format string - /// The translated plural format string if n != 1, else the translated non-plural format string for the context string _pn(string context, string text, string pluralText, long n, params object[] args); - /// - /// Translates a string. - /// - /// The string to translate. - /// The translated string string Get(string text) => _(text); - /// - /// Translates a format string. - /// - /// The format string to translate. - /// The arguments for the format string - /// The translated format string string Get(string text, params object[] args) => _(text, args); - /// - /// Translates a possible plural string. - /// - /// The non-plural string to translate - /// The plural string to translate - /// The number of objects - /// The translated plural string if n != 1, else the translated non-plural string string GetPlural(string text, string pluralText, long n) => _n(text, pluralText, n); - /// - /// Translates a possible plural format string. - /// - /// The non-plural format string to translate - /// The plural format string to translate - /// The number of objects - /// The arguments for the format string - /// The translated format plural string if n != 1, else the translated non-plural format string string GetPlural(string text, string pluralText, long n, params object[] args) => _n(text, pluralText, n, args); - /// - /// Translates a string for a particular context. - /// - /// The context of the string - /// The string to translate - /// The translated string for the context string GetParticular(string context, string text) => _p(context, text); - /// - /// Translates a format string for a particular context. - /// - /// The context of the string - /// The format string to translate - /// The arguments for the format string - /// The translated format string for the context string GetParticular(string context, string text, params object[] args) => _p(context, text, args); - /// - /// Translates a possible plural string for a particular context. - /// - /// The context of the string - /// The non-plural string to translate - /// The plural string to translate - /// The number of objects - /// The translated plural string if n != 1, else the translated non-plural string for the context string GetParticularPlural(string context, string text, string pluralText, long n) => _pn(context, text, pluralText, n); - /// - /// Translates a possible plural format string for a particular context. - /// - /// The context of the string - /// The non-plural format string to translate - /// The plural format string to translate - /// The number of objects - /// The arguments for the format string - /// The translated plural format string if n != 1, else the translated non-plural format string for the context string GetParticularPlural(string context, string text, string pluralText, long n, params object[] args) => _pn(context, text, pluralText, n, args); } diff --git a/Nickvision.Desktop/Globalization/TranslationService.cs b/Nickvision.Desktop/Globalization/TranslationService.cs index d59cf82..468233c 100644 --- a/Nickvision.Desktop/Globalization/TranslationService.cs +++ b/Nickvision.Desktop/Globalization/TranslationService.cs @@ -8,36 +8,17 @@ namespace Nickvision.Desktop.Globalization; -/// -/// A service for translations using Gettext. -/// public class TranslationService : ITranslationService { private readonly string _domainName; private Catalog? _catalog; - /// - /// Constructs a TranslationService. - /// - /// The AppInfo object for the app - /// The language code to use for translations - /// - /// An empty string language code will use the system's language code for translations. The language code "C" will - /// cause strings to remain untranslated - /// public TranslationService(AppInfo appInfo) { _domainName = appInfo.EnglishShortName.Replace(" ", "").ToLower(); Language = "C"; } - /// - /// The language code for translations. - /// - /// - /// An empty string will use the system's language code for translations. The language code "C" will cause strings - /// to remain untranslated - /// public string Language { get => field; @@ -60,9 +41,6 @@ public string Language } } - /// - /// The list of available language codes for translations. - /// public IEnumerable AvailableLanguages { get @@ -79,75 +57,19 @@ public IEnumerable AvailableLanguages } } - /// - /// Translates a string. - /// - /// The string to translate. - /// The translated string public string _(string text) => _catalog?.GetString(text) ?? text; - /// - /// Translates a format string. - /// - /// The format string to translate. - /// The arguments for the format string - /// The translated format string public string _(string text, params object[] args) => _catalog?.GetString(text, args) ?? text; - /// - /// Translates a possible plural string. - /// - /// The non-plural string to translate - /// The plural string to translate - /// The number of objects - /// The translated plural string if n != 1, else the translated non-plural string public string _n(string text, string pluralText, long n) => _catalog?.GetPluralString(text, pluralText, n) ?? (n == 1 ? text : pluralText); - /// - /// Translates a possible plural format string. - /// - /// The non-plural format string to translate - /// The plural format string to translate - /// The number of objects - /// The arguments for the format string - /// The translated format plural string if n != 1, else the translated non-plural format string public string _n(string text, string pluralText, long n, params object[] args) => _catalog?.GetPluralString(text, pluralText, n, args) ?? (n == 1 ? text : pluralText); - /// - /// Translates a string for a particular context. - /// - /// The context of the string - /// The string to translate - /// The translated string for the context public string _p(string context, string text) => _catalog?.GetParticularString(context, text) ?? text; - /// - /// Translates a format string for a particular context. - /// - /// The context of the string - /// The format string to translate - /// The arguments for the format string - /// The translated format string for the context public string _p(string context, string text, params object[] args) => _catalog?.GetParticularString(context, text, args) ?? text; - /// - /// Translates a possible plural string for a particular context. - /// - /// The context of the string - /// The non-plural string to translate - /// The plural string to translate - /// The number of objects - /// The translated plural string if n != 1, else the translated non-plural string for the context public string _pn(string context, string text, string pluralText, long n) => _catalog?.GetParticularPluralString(context, text, pluralText, n) ?? (n == 1 ? text : pluralText); - /// - /// Translates a possible plural format string for a particular context. - /// - /// The context of the string - /// The non-plural format string to translate - /// The plural format string to translate - /// The number of objects - /// The arguments for the format string - /// The translated plural format string if n != 1, else the translated non-plural format string for the context public string _pn(string context, string text, string pluralText, long n, params object[] args) => _catalog?.GetParticularPluralString(context, text, pluralText, n, args) ?? (n == 1 ? text : pluralText); } diff --git a/Nickvision.Desktop/Helpers/GitHubJsonContext.cs b/Nickvision.Desktop/Helpers/GitHubJsonContext.cs index 57aad93..dc6af36 100644 --- a/Nickvision.Desktop/Helpers/GitHubJsonContext.cs +++ b/Nickvision.Desktop/Helpers/GitHubJsonContext.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using System.Text.Json.Serialization; namespace Nickvision.Desktop.Helpers; diff --git a/Nickvision.Desktop/Helpers/GitHubRelease.cs b/Nickvision.Desktop/Helpers/GitHubRelease.cs index 2f158ca..a3cf544 100644 --- a/Nickvision.Desktop/Helpers/GitHubRelease.cs +++ b/Nickvision.Desktop/Helpers/GitHubRelease.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace Nickvision.Desktop.Helpers; diff --git a/Nickvision.Desktop/Helpers/GitHubReleaseAsset.cs b/Nickvision.Desktop/Helpers/GitHubReleaseAsset.cs index 7497304..d5128b4 100644 --- a/Nickvision.Desktop/Helpers/GitHubReleaseAsset.cs +++ b/Nickvision.Desktop/Helpers/GitHubReleaseAsset.cs @@ -1,4 +1,4 @@ -namespace Nickvision.Desktop.Helpers; +namespace Nickvision.Desktop.Helpers; internal class GitHubReleaseAsset { diff --git a/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs b/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs index 09a3112..9b0b9e5 100644 --- a/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs +++ b/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Nickvision.Desktop.Application; @@ -26,6 +26,7 @@ public IHostApplicationBuilder ConfigureNickvision(string[] args, string logging } builder.Services.AddHttpClient(); builder.Services.AddSingleton(new ArgumentsService(args)); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/Nickvision.Desktop/Helpers/LinuxProcessHelpers.cs b/Nickvision.Desktop/Helpers/LinuxProcessHelpers.cs index b000a76..306d347 100644 --- a/Nickvision.Desktop/Helpers/LinuxProcessHelpers.cs +++ b/Nickvision.Desktop/Helpers/LinuxProcessHelpers.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Runtime.InteropServices; namespace Nickvision.Desktop.Helpers; diff --git a/Nickvision.Desktop/Helpers/MacOSProcessHelpers.cs b/Nickvision.Desktop/Helpers/MacOSProcessHelpers.cs index e13b8bd..952022f 100644 --- a/Nickvision.Desktop/Helpers/MacOSProcessHelpers.cs +++ b/Nickvision.Desktop/Helpers/MacOSProcessHelpers.cs @@ -1,4 +1,4 @@ -using System.Diagnostics; +using System.Diagnostics; using System.Runtime.InteropServices; namespace Nickvision.Desktop.Helpers; diff --git a/Nickvision.Desktop/Helpers/ObjectExtensions.cs b/Nickvision.Desktop/Helpers/ObjectExtensions.cs index d32263b..a524016 100644 --- a/Nickvision.Desktop/Helpers/ObjectExtensions.cs +++ b/Nickvision.Desktop/Helpers/ObjectExtensions.cs @@ -1,4 +1,4 @@ -using System.Text.Json; +using System.Text.Json; using System.Text.Json.Serialization.Metadata; namespace Nickvision.Desktop.Helpers; diff --git a/Nickvision.Desktop/Helpers/ProcessExtensions.cs b/Nickvision.Desktop/Helpers/ProcessExtensions.cs index 6810135..b9c2686 100644 --- a/Nickvision.Desktop/Helpers/ProcessExtensions.cs +++ b/Nickvision.Desktop/Helpers/ProcessExtensions.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; namespace Nickvision.Desktop.Helpers; diff --git a/Nickvision.Desktop/Helpers/TaskExtensions.cs b/Nickvision.Desktop/Helpers/TaskExtensions.cs index c506698..bc6fd96 100644 --- a/Nickvision.Desktop/Helpers/TaskExtensions.cs +++ b/Nickvision.Desktop/Helpers/TaskExtensions.cs @@ -2,16 +2,10 @@ namespace Nickvision.Desktop.Helpers; -/// -/// Helpers for Task. -/// public static class TaskExtensions { extension(Task task) { - /// - /// Starts a Task without awaiting it and ignores any exceptions thrown. - /// public async void FireAndForget() { try diff --git a/Nickvision.Desktop/Helpers/UriExtensions.cs b/Nickvision.Desktop/Helpers/UriExtensions.cs index 043bf6f..0f194d2 100644 --- a/Nickvision.Desktop/Helpers/UriExtensions.cs +++ b/Nickvision.Desktop/Helpers/UriExtensions.cs @@ -2,16 +2,10 @@ namespace Nickvision.Desktop.Helpers; -/// -/// Helpers for Uri. -/// public static class UriExtensions { private static readonly Uri EmptyUri; - /// - /// Constructs a static UriExtensions. - /// static UriExtensions() { EmptyUri = new Uri("about:blank"); @@ -19,19 +13,11 @@ static UriExtensions() extension(Uri) { - /// - /// An empty Uri (about:blank). - /// - /// The empty Uri public static Uri Empty => EmptyUri; } extension(Uri uri) { - /// - /// Whether the Uri is empty (about:blank). - /// - /// True if the Uri is empty, else false public bool IsEmpty => uri == EmptyUri || uri.ToString() == "about:blank"; } } diff --git a/Nickvision.Desktop/Hosting/IUserInterfaceContext.cs b/Nickvision.Desktop/Hosting/IUserInterfaceContext.cs index dea20cf..05d0a92 100644 --- a/Nickvision.Desktop/Hosting/IUserInterfaceContext.cs +++ b/Nickvision.Desktop/Hosting/IUserInterfaceContext.cs @@ -1,17 +1,7 @@ -namespace Nickvision.Desktop.Hosting; +namespace Nickvision.Desktop.Hosting; -/// -/// An interface for context for a user interface application. -/// -/// The application class public interface IUserInterfaceContext where T : class { - /// - /// The application instance. - /// public T? Application { get; set; } - /// - /// Whether or not the application is running. - /// public bool IsRunning { get; set; } } diff --git a/Nickvision.Desktop/Hosting/IUserInterfaceThread.cs b/Nickvision.Desktop/Hosting/IUserInterfaceThread.cs index f5c5007..5d25303 100644 --- a/Nickvision.Desktop/Hosting/IUserInterfaceThread.cs +++ b/Nickvision.Desktop/Hosting/IUserInterfaceThread.cs @@ -1,18 +1,9 @@ -using System.Threading.Tasks; +using System.Threading.Tasks; namespace Nickvision.Desktop.Hosting; -/// -/// An interface for a thread of a user interface application. -/// public interface IUserInterfaceThread { - /// - /// Asynchronously starts the user interface. - /// Task StartAsync(); - /// - /// Asynchronously stops the user interface. - /// Task StopAsync(); } diff --git a/Nickvision.Desktop/Hosting/UserInterfaceHostedService.cs b/Nickvision.Desktop/Hosting/UserInterfaceHostedService.cs index 5e10967..d2adff1 100644 --- a/Nickvision.Desktop/Hosting/UserInterfaceHostedService.cs +++ b/Nickvision.Desktop/Hosting/UserInterfaceHostedService.cs @@ -1,26 +1,16 @@ -using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using System.Threading; using System.Threading.Tasks; namespace Nickvision.Desktop.Hosting; -/// -/// A hosted service for a user interface application. -/// -/// The application class public class UserInterfaceHostedService : IHostedService where T : class { private ILogger> _logger; private IUserInterfaceThread _userInterfaceThread; private IUserInterfaceContext _userInterfaceContext; - /// - /// Constructs a UserInterfaceHostedService. - /// - /// The logger - /// The thread for the user interface application - /// The context for the user interface application public UserInterfaceHostedService(ILogger> logger, IUserInterfaceThread userInterfaceThread, IUserInterfaceContext userInterfaceContext) { _logger = logger; @@ -28,10 +18,6 @@ public UserInterfaceHostedService(ILogger> logger, _userInterfaceContext = userInterfaceContext; } - /// - /// Asynchronously starts the user interface. - /// - /// The cancellation token public Task StartAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested) @@ -42,10 +28,6 @@ public Task StartAsync(CancellationToken cancellationToken) return _userInterfaceThread.StartAsync(); } - /// - /// Asynchronously stops the user interface. - /// - /// The cancellation token public Task StopAsync(CancellationToken cancellationToken) { if (cancellationToken.IsCancellationRequested || !_userInterfaceContext.IsRunning) diff --git a/Nickvision.Desktop/Keyring/Credential.cs b/Nickvision.Desktop/Keyring/Credential.cs index c23835c..8b80a7f 100644 --- a/Nickvision.Desktop/Keyring/Credential.cs +++ b/Nickvision.Desktop/Keyring/Credential.cs @@ -3,38 +3,16 @@ namespace Nickvision.Desktop.Keyring; -/// -/// A class representing a credential. -/// public class Credential { - /// - /// The friendly name of the credential. - /// public string Name { get; set; } - /// - /// The password of the credential. - /// public string Password { get; set; } - /// - /// The url of the credential. - /// public Uri Url { get; set; } - /// - /// The username of the credential. - /// public string Username { get; set; } - /// - /// Constructs a credential. - /// - /// The friendly name of the credential - /// The username of the credential - /// The password of the credential - /// The url of the credential public Credential(string name, string username, string password, Uri? url = null) { Name = name; diff --git a/Nickvision.Desktop/Keyring/IKeyringService.cs b/Nickvision.Desktop/Keyring/IKeyringService.cs index a2b9642..c25d50f 100644 --- a/Nickvision.Desktop/Keyring/IKeyringService.cs +++ b/Nickvision.Desktop/Keyring/IKeyringService.cs @@ -3,44 +3,10 @@ namespace Nickvision.Desktop.Keyring; -/// -/// An interface of a service for managing credentials in a keyring. -/// public interface IKeyringService { - /// - /// The list of credentials in the keyring. - /// - IEnumerable Credentials { get; } - /// - /// Whether the keyring is currently saving to disk. - /// - bool IsSavingToDisk { get; } - - /// - /// Adds a credential to the keyring. - /// - /// The credential to add - /// True if the keyring was successfully added, else false Task AddCredentialAsync(Credential credential); - - /// - /// Destroys the keyring and all its credentials. - /// - /// True if the keyring was successfully added, else false - Task DestroyAsync(); - - /// - /// Removes a credential from the keyring. - /// - /// The credential to remove - /// True if the keyring was successfully removed, else false - Task RemoveCredentialAsync(Credential credential); - - /// - /// Updates a credential in the keyring. - /// - /// The credential to update - /// True if the keyring was successfully updated, else false + Task DeleteCredentialAsync(Credential credential); + Task> GetAllCredentialAsync(); Task UpdateCredentialAsync(Credential credential); } diff --git a/Nickvision.Desktop/Keyring/KeyringService.cs b/Nickvision.Desktop/Keyring/KeyringService.cs index bd966ed..fe01351 100644 --- a/Nickvision.Desktop/Keyring/KeyringService.cs +++ b/Nickvision.Desktop/Keyring/KeyringService.cs @@ -1,152 +1,47 @@ -using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Nickvision.Desktop.Application; -using Nickvision.Desktop.Filesystem; -using Nickvision.Desktop.System; using System; using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Threading.Tasks; namespace Nickvision.Desktop.Keyring; -/// -/// A service for managing credentials in a database keyring. -/// -public class KeyringService : IAsyncDisposable, IDisposable, IKeyringService +public class KeyringService : IKeyringService { - private readonly ILogger _logger; - private readonly List _credentials; - private readonly string _path; - private SqliteConnection? _connection; + private static readonly string TableName; - /// - /// Constructs a KeyringService. - /// - /// Logger for the service - /// The AppInfo object for the app - /// The service for managing secrets - /// This will create a new encrypted database store if it doesn't already exist. - /// If the database is unable to be created or unlocked, changes will not be saved to disk. - public KeyringService(ILogger logger, AppInfo info, ISecretService secretService) - { - _logger = logger; - var keyringDir = Path.Combine(UserDirectories.Config, "Nickvision", "Keyring"); - Directory.CreateDirectory(keyringDir); - _credentials = []; - _path = Path.Combine(keyringDir, $"{info.Id}.ring2"); - _connection = null; - _logger.LogInformation($"Opening keyring database ({_path})."); - if (OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsLinux()) - { - var secret = Task.Run(() => secretService.GetAsync(info.Id)).GetAwaiter().GetResult() ?? Task.Run(() => secretService.CreateAsync(info.Id)).GetAwaiter().GetResult(); - if (secret is not null) - { - _connection = new SqliteConnection(new SqliteConnectionStringBuilder($"Data Source='{_path}'") - { - Mode = SqliteOpenMode.ReadWriteCreate, - Password = secret.Value, - Pooling = false - }.ToString()); - try - { - _connection.Open(); - _logger.LogInformation($"Opened keyring database ({_path}) successfully."); - } - catch (SqliteException e) - { - _logger.LogError($"Failed to open keyring database ({_path}): {e}"); - _connection.Dispose(); - _connection = null; - } - } - else - { - _logger.LogError($"Unable to open keyring database ({_path}). The system secret ({info.Id}) could not be retrieved or created."); - } - } - if (_connection is null) - { - _logger.LogError($"Keyring database ({_path}) connection is unavailable. Changes will not be saved to disk."); - return; - } - using var createTableCommand = _connection.CreateCommand(); - createTableCommand.CommandText = "CREATE TABLE IF NOT EXISTS credentials (name TEXT, uri TEXT, username TEXT, password TEXT)"; - createTableCommand.ExecuteNonQuery(); - using var selectAllCommand = _connection.CreateCommand(); - selectAllCommand.CommandText = "SELECT * FROM credentials"; - using var reader = selectAllCommand.ExecuteReader(); - while (reader.Read()) - { - _credentials.Add(new Credential(reader.GetString(0), reader.GetString(2), reader.GetString(3), new Uri(reader.GetString(1)))); - } - _logger.LogInformation($"Loaded {_credentials.Count} credentials from keyring."); - } - - /// - /// Finalizes a KeyringService. - /// - ~KeyringService() - { - Dispose(false); - } + private readonly ILogger _logger; + private readonly IDatabaseService _databaseService; + private bool _tableEnsured; - /// - /// Disposes a KeyringService asynchronously. - /// - public async ValueTask DisposeAsync() + static KeyringService() { - await DisposeAsyncCore().ConfigureAwait(false); - Dispose(false); - GC.SuppressFinalize(this); + TableName = "credentials"; } - /// - /// Disposes a KeyringService. - /// - public void Dispose() + public KeyringService(ILogger logger, IDatabaseService databaseService) { - Dispose(true); - GC.SuppressFinalize(this); + _logger = logger; + _databaseService = databaseService; + _tableEnsured = false; } - /// - /// Whether the keyring is currently saving to disk. - /// - public bool IsSavingToDisk => _connection is not null; - - /// - /// The list of credentials in the keyring. - /// - public IEnumerable Credentials => _credentials; - - /// - /// Adds a credential to the keyring. - /// - /// The credential to add - /// True if the keyring was successfully added, else false public async Task AddCredentialAsync(Credential credential) { + await EnsureTableAsync(); _logger.LogInformation($"Adding keyring credential ({credential.Name})."); - if (_credentials.Any(c => c.Name == credential.Name)) + if (await _databaseService.ContainsInTableAsync(TableName, "name", credential.Name)) { _logger.LogError($"Unable to add keyring credential ({credential.Name}) as it already exists."); return false; } - _credentials.Add(credential); - if (_connection is null) + var result = _databaseService.InsertIntoTable(TableName, new Dictionary() { - _logger.LogError($"Unable to persist keyring credential ({credential.Name}) to disk as the database connection is unavailable."); - return false; - } - await using var insertCommand = _connection.CreateCommand(); - insertCommand.CommandText = "INSERT INTO credentials (name, uri, username, password) VALUES ($name, $uri, $username, $password)"; - insertCommand.Parameters.AddWithValue("$name", credential.Name); - insertCommand.Parameters.AddWithValue("$uri", credential.Url.ToString()); - insertCommand.Parameters.AddWithValue("$username", credential.Username); - insertCommand.Parameters.AddWithValue("$password", credential.Password); - var result = await insertCommand.ExecuteNonQueryAsync() > 0; + { "name", credential.Name }, + { "uri", credential.Url.ToString() }, + { "username", credential.Username }, + { "password", credential.Password }, + }); if (result) { _logger.LogInformation($"Added keyring credential ({credential.Name}) successfully."); @@ -158,90 +53,49 @@ public async Task AddCredentialAsync(Credential credential) return result; } - /// - /// Destroys the keyring and all its credentials. - /// - /// True if the keyring was successfully added, else false - public async Task DestroyAsync() + public async Task DeleteCredentialAsync(Credential credential) { - _logger.LogInformation($"Destroying keyring database ({_path})."); - await DisposeAsync(); - _credentials.Clear(); - File.Delete(_path); - var result = !File.Exists(_path); + await EnsureTableAsync(); + _logger.LogInformation($"Deleting keyring credential ({credential.Name})."); + var result = await _databaseService.DeleteFromTableAsync(TableName, "name", credential.Name); if (result) { - _logger.LogInformation($"Destroyed keyring database ({_path}) successfully."); + _logger.LogInformation($"Removed keyring credential ({credential.Name}) successfully."); } else { - _logger.LogError($"Failed to destroy keyring database ({_path})."); + _logger.LogError($"Failed to remove keyring credential ({credential.Name}) from database."); } return result; } - /// - /// Removes a credential from the keyring. - /// - /// The credential to remove - /// True if the keyring was successfully removed, else false - public async Task RemoveCredentialAsync(Credential credential) + public async Task> GetAllCredentialAsync() { - _logger.LogInformation($"Removing keyring credential ({credential.Name})."); - var credentialIndex = _credentials.FindIndex(c => c.Name == credential.Name); - if (credentialIndex == -1) - { - _logger.LogError($"Unable to remove keyring credential ({credential.Name}) as it does not exist."); - return false; - } - _credentials.RemoveAt(credentialIndex); - if (_connection is null) + await EnsureTableAsync(); + var credentials = new List(); + await using var command = await _databaseService.SelectAllFromTableAsync(TableName); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) { - _logger.LogError($"Unable to remove keyring credential ({credential.Name}) from disk as the database connection is unavailable."); - return false; + credentials.Add(new Credential(reader.GetString(0), reader.GetString(2), reader.GetString(3), new Uri(reader.GetString(1)))); } - await using var deleteCommand = _connection.CreateCommand(); - deleteCommand.CommandText = "DELETE FROM credentials WHERE name = $name"; - deleteCommand.Parameters.AddWithValue("$name", credential.Name); - var result = await deleteCommand.ExecuteNonQueryAsync() > 0; - if (result) - { - _logger.LogInformation($"Removed keyring credential ({credential.Name}) successfully."); - } - else - { - _logger.LogError($"Failed to remove keyring credential ({credential.Name}) from database."); - } - return result; + return credentials; } - /// - /// Updates a credential in the keyring. - /// - /// The credential to update - /// True if the keyring was successfully updated, else false public async Task UpdateCredentialAsync(Credential credential) { _logger.LogInformation($"Updating keyring credential ({credential.Name})."); - var credentialIndex = _credentials.FindIndex(c => c.Name == credential.Name); - if (credentialIndex == -1) + if (!await _databaseService.ContainsInTableAsync(TableName, "name", credential.Name)) { _logger.LogError($"Unable to update keyring credential ({credential.Name}) as it does not exist."); return false; } - _credentials[credentialIndex] = credential; - if (_connection is null) + var result = await _databaseService.UpdateInTableAsync(TableName, "name", credential.Name, new Dictionary() { - _logger.LogError($"Unable to update keyring credential ({credential.Name}) on disk as the database connection is unavailable."); - return false; - } - await using var updateCommand = _connection.CreateCommand(); - updateCommand.CommandText = "UPDATE credentials SET uri = $uri, username = $username, password = $password WHERE name = $name"; - updateCommand.Parameters.AddWithValue("$name", credential.Name); - updateCommand.Parameters.AddWithValue("$uri", credential.Url.ToString()); - updateCommand.Parameters.AddWithValue("$username", credential.Username); - updateCommand.Parameters.AddWithValue("$password", credential.Password); - var result = await updateCommand.ExecuteNonQueryAsync() > 0; + { "uri", credential.Url.ToString() }, + { "username", credential.Username }, + { "password", credential.Password }, + }); if (result) { _logger.LogInformation($"Updated keyring credential ({credential.Name}) successfully."); @@ -253,29 +107,13 @@ public async Task UpdateCredentialAsync(Credential credential) return result; } - /// - /// Disposes a KeyringService asynchronously. - /// - protected virtual async ValueTask DisposeAsyncCore() - { - if (_connection is not null) - { - await _connection.DisposeAsync().ConfigureAwait(false); - } - _connection = null; - } - - /// - /// Disposes a KeyringService. - /// - /// Whether to dispose managed resources - private void Dispose(bool disposing) + private async Task EnsureTableAsync() { - if (!disposing) + if (_tableEnsured) { return; } - _connection?.Dispose(); - _connection = null; + await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT PRIMARY KEY, uri TEXT, username TEXT, password TEXT"); + _tableEnsured = true; } } diff --git a/Nickvision.Desktop/Keyring/PasswordContent.cs b/Nickvision.Desktop/Keyring/PasswordContent.cs index d9a4534..39fdedd 100644 --- a/Nickvision.Desktop/Keyring/PasswordContent.cs +++ b/Nickvision.Desktop/Keyring/PasswordContent.cs @@ -2,9 +2,6 @@ namespace Nickvision.Desktop.Keyring; -/// -/// Flags for password content types. -/// [Flags] public enum PasswordContent { diff --git a/Nickvision.Desktop/Keyring/PasswordGenerator.cs b/Nickvision.Desktop/Keyring/PasswordGenerator.cs index 249988f..41b15d2 100644 --- a/Nickvision.Desktop/Keyring/PasswordGenerator.cs +++ b/Nickvision.Desktop/Keyring/PasswordGenerator.cs @@ -4,9 +4,6 @@ namespace Nickvision.Desktop.Keyring; -/// -/// A class for generating random passwords. -/// public class PasswordGenerator { private static readonly List NumericChars; @@ -14,14 +11,8 @@ public class PasswordGenerator private static readonly List LowerChars; private static readonly List SpecialChars; - /// - /// The content type flags to include when generating a password. - /// public PasswordContent ContentFlags { get; set; } - /// - /// Constructs a static PasswordGenerator. - /// static PasswordGenerator() { NumericChars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']; @@ -30,21 +21,11 @@ static PasswordGenerator() SpecialChars = ['!', '#', '$', '%', '&', '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~']; } - /// - /// Constructs a PasswordGenerator. - /// - /// The content type flags to include when generating a password public PasswordGenerator(PasswordContent contentFlags = PasswordContent.All) { ContentFlags = contentFlags; } - /// - /// Generates a random password. - /// - /// The length of the password to generate - /// The new generated password - /// Thrown if random generation fails public string Next(int length = 16) { var password = string.Empty; diff --git a/Nickvision.Desktop/Network/DownloadProgress.cs b/Nickvision.Desktop/Network/DownloadProgress.cs index 584bd04..48cc518 100644 --- a/Nickvision.Desktop/Network/DownloadProgress.cs +++ b/Nickvision.Desktop/Network/DownloadProgress.cs @@ -1,31 +1,13 @@ namespace Nickvision.Desktop.Network; -/// -/// A class containing information about a download's progress. -/// public class DownloadProgress { - /// - /// The number of bytes received already. - /// public long BytesReceived { get; init; } - /// - /// Whether the download is marked as completed. - /// public bool Completed { get; init; } - /// - /// The total number of bytes to be received. - /// public long TotalBytesToReceive { get; init; } - /// - /// Constructs a DownloadProgress. - /// - /// The total number of bytes to be received - /// The number of bytes received already - /// Whether the download is marked as completed public DownloadProgress(long totalBytesToReceive, long bytesReceived, bool completed) { TotalBytesToReceive = totalBytesToReceive; @@ -33,9 +15,5 @@ public DownloadProgress(long totalBytesToReceive, long bytesReceived, bool compl Completed = completed; } - /// - /// The percentage of the download completed. - /// - /// Ranges from 0.0 to 1.0 public double Percentage => TotalBytesToReceive <= 0 ? 0.0 : (double)BytesReceived / TotalBytesToReceive; } diff --git a/Nickvision.Desktop/Notifications/AppNotification.cs b/Nickvision.Desktop/Notifications/AppNotification.cs index d9203e2..6d7bbc0 100644 --- a/Nickvision.Desktop/Notifications/AppNotification.cs +++ b/Nickvision.Desktop/Notifications/AppNotification.cs @@ -1,32 +1,12 @@ namespace Nickvision.Desktop.Notifications; -/// -/// A class containing information about an application notification. -/// public class AppNotification { - /// - /// The action name of the notification. - /// public string? Action { get; set; } - /// - /// The action parameter of the notification. - /// public string? ActionParam { get; set; } - /// - /// The message of the notification. - /// public string Message { get; init; } - /// - /// The severity of the notification. - /// public NotificationSeverity Severity { get; init; } - /// - /// Constructs an AppNotification. - /// - /// The message of the notification - /// The severity of the notification public AppNotification(string message, NotificationSeverity severity) { Message = message; diff --git a/Nickvision.Desktop/Notifications/AppNotificationSentEventArgs.cs b/Nickvision.Desktop/Notifications/AppNotificationSentEventArgs.cs index b9ce2ec..d53ca7a 100644 --- a/Nickvision.Desktop/Notifications/AppNotificationSentEventArgs.cs +++ b/Nickvision.Desktop/Notifications/AppNotificationSentEventArgs.cs @@ -2,25 +2,12 @@ namespace Nickvision.Desktop.Notifications; -/// -/// A class of event arguments for when an app notification is sent. -/// public class AppNotificationSentEventArgs : EventArgs { - /// - /// The AppNotification sent. - /// public AppNotification Notification { get; init; } - /// - /// The timestamp of when the notification was sent. - /// public DateTime Timestamp { get; init; } - /// - /// Constructs an AppNotificationSentEventArgs. - /// - /// The AppNotification sent public AppNotificationSentEventArgs(AppNotification notification) { Notification = notification; diff --git a/Nickvision.Desktop/Notifications/INotificationService.cs b/Nickvision.Desktop/Notifications/INotificationService.cs index c898a1f..114e591 100644 --- a/Nickvision.Desktop/Notifications/INotificationService.cs +++ b/Nickvision.Desktop/Notifications/INotificationService.cs @@ -2,19 +2,9 @@ namespace Nickvision.Desktop.Notifications; -/// -/// An interface for a service for managing notifications. -/// public interface INotificationService { - /// - /// The event for when app notifications are sent. - /// event EventHandler? AppNotificationSent; - /// - /// Sends an app notification. - /// - /// The AppNotification to send void Send(AppNotification notification); } diff --git a/Nickvision.Desktop/Notifications/NotificationService.cs b/Nickvision.Desktop/Notifications/NotificationService.cs index f8ed5ed..9566311 100644 --- a/Nickvision.Desktop/Notifications/NotificationService.cs +++ b/Nickvision.Desktop/Notifications/NotificationService.cs @@ -2,19 +2,9 @@ namespace Nickvision.Desktop.Notifications; -/// -/// A service for managing notifications. -/// public class NotificationService : INotificationService { - /// - /// The event for when app notifications are sent. - /// public event EventHandler? AppNotificationSent; - /// - /// Sends an app notification. - /// - /// The AppNotification to send public void Send(AppNotification notification) => AppNotificationSent?.Invoke(this, new AppNotificationSentEventArgs(notification)); } \ No newline at end of file diff --git a/Nickvision.Desktop/Notifications/NotificationSeverity.cs b/Nickvision.Desktop/Notifications/NotificationSeverity.cs index 4d4d7ed..122b17b 100644 --- a/Nickvision.Desktop/Notifications/NotificationSeverity.cs +++ b/Nickvision.Desktop/Notifications/NotificationSeverity.cs @@ -1,8 +1,5 @@ namespace Nickvision.Desktop.Notifications; -/// -/// Levels of severity for notifications. -/// public enum NotificationSeverity { Information, diff --git a/Nickvision.Desktop/Notifications/ShellNotification.cs b/Nickvision.Desktop/Notifications/ShellNotification.cs index 3ad8107..88e4d71 100644 --- a/Nickvision.Desktop/Notifications/ShellNotification.cs +++ b/Nickvision.Desktop/Notifications/ShellNotification.cs @@ -1,21 +1,9 @@ namespace Nickvision.Desktop.Notifications; -/// -/// A class containing information about a shell notification. -/// public class ShellNotification : AppNotification { - /// - /// The title of the notification. - /// public string Title { get; init; } - /// - /// Constructs a ShellNotification. - /// - /// The title of the notification - /// The message of the notification - /// The severity of the notification public ShellNotification(string title, string message, NotificationSeverity severity) : base(message, severity) { Title = title; diff --git a/Nickvision.Desktop/System/DependencySearchOption.cs b/Nickvision.Desktop/System/DependencySearchOption.cs index 4dcb75b..9c8ab2f 100644 --- a/Nickvision.Desktop/System/DependencySearchOption.cs +++ b/Nickvision.Desktop/System/DependencySearchOption.cs @@ -1,8 +1,5 @@ namespace Nickvision.Desktop.System; -/// -/// Options for searching for dependencies. -/// public enum DependencySearchOption { Global, diff --git a/Nickvision.Desktop/System/DeploymentMode.cs b/Nickvision.Desktop/System/DeploymentMode.cs index 5cdc2ad..4f8fbcb 100644 --- a/Nickvision.Desktop/System/DeploymentMode.cs +++ b/Nickvision.Desktop/System/DeploymentMode.cs @@ -1,8 +1,5 @@ namespace Nickvision.Desktop.System; -/// -/// Modes of deployment for the application. -/// public enum DeploymentMode { Local, diff --git a/Nickvision.Desktop/System/Environment.cs b/Nickvision.Desktop/System/Environment.cs index c1d57f3..df211ab 100644 --- a/Nickvision.Desktop/System/Environment.cs +++ b/Nickvision.Desktop/System/Environment.cs @@ -10,24 +10,15 @@ namespace Nickvision.Desktop.System; -/// -/// Helpers for working with the system environment. -/// public static class Environment { private static readonly Dictionary<(string Dependency, DependencySearchOption Search), string?> Dependencies; - /// - /// Constructs a static Environment. - /// static Environment() { Dependencies = []; } - /// - /// The deployment mode of the application. - /// public static DeploymentMode DeploymentMode { get @@ -44,19 +35,10 @@ public static DeploymentMode DeploymentMode } } - /// - /// The application executable's directory. - /// public static string ExecutingDirectory => Path.GetDirectoryName(ExecutingPath) ?? global::System.Environment.CurrentDirectory; - /// - /// The application executable's path. - /// public static string ExecutingPath => Path.GetFullPath(global::System.Environment.ProcessPath!); - /// - /// The list of directories in the PATH variable. - /// public static IEnumerable PathVariable { get @@ -66,13 +48,6 @@ public static IEnumerable PathVariable } } - /// - /// Finds a dependency on the system. - /// - /// The dependency to find - /// The search options - /// The path of the dependency if found, else null - /// Thrown if the search option is invalid public static string? FindDependency(string dependency, DependencySearchOption search = DependencySearchOption.Global) { if (OperatingSystem.IsWindows()) @@ -145,12 +120,6 @@ public static IEnumerable PathVariable return Dependencies[(dependency, search)]; } - /// - /// Gets the debug information for the application. - /// - /// The AppInfo object for the app - /// Any extra information to include in the debug information string - /// The debug information string public static string GetDebugInformation(AppInfo info, string extra = "") => $""" App: {info.Name} Version: {info.Version} diff --git a/Nickvision.Desktop/System/IPowerService.cs b/Nickvision.Desktop/System/IPowerService.cs index 7897ffb..383c51c 100644 --- a/Nickvision.Desktop/System/IPowerService.cs +++ b/Nickvision.Desktop/System/IPowerService.cs @@ -2,20 +2,9 @@ namespace Nickvision.Desktop.System; -/// -/// An interface for a service for managing power options. -/// public interface IPowerService { - /// - /// Allows the system to suspend. - /// - /// True if the action was applied successfully, else false Task AllowSuspendAsync(); - /// - /// Prevents the system from suspending. - /// - /// True if the action was applied successfully, else false Task PreventSuspendAsync(); } diff --git a/Nickvision.Desktop/System/ISecretService.cs b/Nickvision.Desktop/System/ISecretService.cs index 3b04078..c76f046 100644 --- a/Nickvision.Desktop/System/ISecretService.cs +++ b/Nickvision.Desktop/System/ISecretService.cs @@ -2,39 +2,11 @@ namespace Nickvision.Desktop.System; -/// -/// An interface of a service for managing secrets. -/// public interface ISecretService { - /// - /// Adds a secret asynchronously. - /// - /// The secret to add - /// True if the secret was added successfully, else false Task AddAsync(Secret secret); - /// - /// Creates a secret asynchronously with a random but secure value. - /// - /// The name of the secret to create - /// The created secret if successful, else null Task CreateAsync(string name); - /// - /// Deletes a secret asynchronously. - /// - /// The name of the secret to delete - /// True if the secret was deleted successfully, else false Task DeleteAsync(string name); - /// - /// Gets a secret asynchronously. - /// - /// The name of the secret to find - /// The secret if found, else null Task GetAsync(string name); - /// - /// Updates a secret asynchronously. - /// - /// The secret to update - /// True if the secret was updated successfully, else false Task UpdateAsync(Secret secret); } diff --git a/Nickvision.Desktop/System/PowerService.cs b/Nickvision.Desktop/System/PowerService.cs index a45a5ed..74fb248 100644 --- a/Nickvision.Desktop/System/PowerService.cs +++ b/Nickvision.Desktop/System/PowerService.cs @@ -11,9 +11,6 @@ namespace Nickvision.Desktop.System; -/// -/// A server for managing power options. -/// public class PowerService : IDisposable, IPowerService { private readonly ILogger _logger; @@ -22,10 +19,6 @@ public class PowerService : IDisposable, IPowerService private uint _inhibitCookie; private Process? _preventSuspendProcess; - /// - /// Constructs a PowerService. - /// - /// Logger for the service public PowerService(ILogger logger) { _logger = logger; @@ -33,27 +26,17 @@ public PowerService(ILogger logger) _inhibitCookie = 0; } - /// - /// Finalizes a PowerService. - /// ~PowerService() { Dispose(false); } - /// - /// Disposes a PowerService. - /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } - /// - /// Allows the system to suspend. - /// - /// True if the action was applied successfully, else false public async Task AllowSuspendAsync() { _logger.LogInformation("Allowing system suspend..."); @@ -104,10 +87,6 @@ public async Task AllowSuspendAsync() } } - /// - /// Prevents the system from suspending. - /// - /// True if the action was applied successfully, else false public async Task PreventSuspendAsync() { _logger.LogInformation("Preventing system suspend..."); @@ -189,10 +168,6 @@ public async Task PreventSuspendAsync() } } - /// - /// Disposes a PowerService. - /// - /// Whether to dispose managed resources private void Dispose(bool disposing) { if (_disposed) diff --git a/Nickvision.Desktop/System/Secret.cs b/Nickvision.Desktop/System/Secret.cs index 36abf30..5058827 100644 --- a/Nickvision.Desktop/System/Secret.cs +++ b/Nickvision.Desktop/System/Secret.cs @@ -1,8 +1,5 @@ namespace Nickvision.Desktop.System; -/// -/// A class representing a secret. -/// public class Secret { public string Name { get; init; } diff --git a/Nickvision.Desktop/System/SecretService.cs b/Nickvision.Desktop/System/SecretService.cs index 3c15b05..31e9ce4 100644 --- a/Nickvision.Desktop/System/SecretService.cs +++ b/Nickvision.Desktop/System/SecretService.cs @@ -13,27 +13,15 @@ namespace Nickvision.Desktop.System; -/// -/// A service for managing secrets using the system's secret storage. -/// public class SecretService : ISecretService { private readonly ILogger _logger; - /// - /// Constructs a SecretService. - /// - /// Logger for the service public SecretService(ILogger logger) { _logger = logger; } - /// - /// Adds a secret asynchronously. - /// - /// The secret to add - /// True if the secret was added successfully, else false public async Task AddAsync(Secret secret) { _logger.LogInformation($"Adding system secret ({secret.Name})."); @@ -151,11 +139,6 @@ public async Task AddAsync(Secret secret) } } - /// - /// Creates a secret asynchronously with a random but secure value. - /// - /// The name of the secret to create - /// The created secret if successful, else null public async Task CreateAsync(string name) { _logger.LogInformation($"Creating system secret ({name})."); @@ -177,11 +160,6 @@ public async Task AddAsync(Secret secret) return result; } - /// - /// Deletes a secret asynchronously. - /// - /// The name of the secret to delete - /// True if the secret was deleted successfully, else false public async Task DeleteAsync(string name) { _logger.LogInformation($"Deleting system secret ({name})."); @@ -266,11 +244,6 @@ public async Task DeleteAsync(string name) } } - /// - /// Gets a secret asynchronously. - /// - /// The name of the secret to find - /// The secret if found, else null public async Task GetAsync(string name) { _logger.LogInformation($"Getting system secret ({name})."); @@ -376,11 +349,6 @@ public async Task DeleteAsync(string name) } } - /// - /// Updates a secret asynchronously. - /// - /// The secret to update - /// True if the secret was updated successfully, else false public async Task UpdateAsync(Secret secret) { _logger.LogInformation($"Updating system secret ({secret.Name})."); From bed5cff55ade38d3640736a38aeda36021fdc251 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Thu, 26 Mar 2026 22:07:59 -0400 Subject: [PATCH 02/16] feat: Add Saved event to ConfigurationService --- .../ConfigurationSavedEventArgs.cs | 17 +++ .../Application/ConfigurationService.cs | 63 +++++++++ .../Application/IConfigurationService.cs | 5 +- .../Filesystem/IJsonFileService.cs | 15 --- .../Filesystem/JsonFileSavedEventArgs.cs | 19 --- .../Filesystem/JsonFileService.cs | 126 ------------------ 6 files changed, 84 insertions(+), 161 deletions(-) create mode 100644 Nickvision.Desktop/Application/ConfigurationSavedEventArgs.cs delete mode 100644 Nickvision.Desktop/Filesystem/IJsonFileService.cs delete mode 100644 Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs delete mode 100644 Nickvision.Desktop/Filesystem/JsonFileService.cs diff --git a/Nickvision.Desktop/Application/ConfigurationSavedEventArgs.cs b/Nickvision.Desktop/Application/ConfigurationSavedEventArgs.cs new file mode 100644 index 0000000..32851ef --- /dev/null +++ b/Nickvision.Desktop/Application/ConfigurationSavedEventArgs.cs @@ -0,0 +1,17 @@ +using System; + +namespace Nickvision.Desktop.Application; + +public class ConfigurationSavedEventArgs : EventArgs +{ + public string ChangedPropertyName { get; } + public object ChangedPropertyNewValue { get; } + public Type ChangedPropertyType { get; } + + public ConfigurationSavedEventArgs(string changedPropertyName, object changedPropertyNewValue, Type changedPropertyType) + { + ChangedPropertyName = changedPropertyName; + ChangedPropertyNewValue = changedPropertyNewValue; + ChangedPropertyType = changedPropertyType; + } +} diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index 66e3a83..96b278e 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using Nickvision.Desktop.Keyring; +using System; using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Serialization.Metadata; @@ -16,6 +17,8 @@ public class ConfigurationService : IConfigurationService private readonly Dictionary _cache; private bool _tableEnsured; + public event EventHandler? Saved; + static ConfigurationService() { TableName = "configuration"; @@ -31,8 +34,10 @@ public ConfigurationService(ILogger logger, IDatabaseService dat public bool GetBool(string name, bool defaultValue = false) { + _logger.LogInformation($"Getting boolean configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is bool t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -47,13 +52,16 @@ public bool GetBool(string name, bool defaultValue = false) } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (bool)_cache[name]; } public async Task GetBoolAsync(string name, bool defaultValue = false) { + _logger.LogInformation($"Getting boolean configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is bool t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -68,13 +76,16 @@ public async Task GetBoolAsync(string name, bool defaultValue = false) } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (bool)_cache[name]; } public double GetDouble(string name, double defaultValue = 0.0) { + _logger.LogInformation($"Getting double configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is double t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -89,13 +100,16 @@ public double GetDouble(string name, double defaultValue = 0.0) } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (double)_cache[name]; } public async Task GetDoubleAsync(string name, double defaultValue = 0.0) { + _logger.LogInformation($"Getting double configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is double t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -110,13 +124,16 @@ public async Task GetDoubleAsync(string name, double defaultValue = 0.0) } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (double)_cache[name]; } public int GetInt(string name, int defaultValue = 0) { + _logger.LogInformation($"Getting integer configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is int t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -131,13 +148,16 @@ public int GetInt(string name, int defaultValue = 0) } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (int)_cache[name]; } public async Task GetIntAsync(string name, int defaultValue = 0) { + _logger.LogInformation($"Getting integer configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is int t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -152,13 +172,16 @@ public async Task GetIntAsync(string name, int defaultValue = 0) } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (int)_cache[name]; } public T GetObject(string name, T defaultValue, JsonTypeInfo info) where T : notnull { + _logger.LogInformation($"Getting object configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is T t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -173,13 +196,16 @@ public T GetObject(string name, T defaultValue, JsonTypeInfo info) where T } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (T)_cache[name]; } public async Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull { + _logger.LogInformation($"Getting object configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is T t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -194,13 +220,16 @@ public async Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (T)_cache[name]; } public string GetString(string name, string defaultValue = "") { + _logger.LogInformation($"Getting string configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is string t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -215,13 +244,16 @@ public string GetString(string name, string defaultValue = "") } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (string)_cache[name]; } public async Task GetStringAsync(string name, string defaultValue = "") { + _logger.LogInformation($"Getting string configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is string t) { + _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; } _cache[name] = defaultValue; @@ -236,11 +268,13 @@ public async Task GetStringAsync(string name, string defaultValue = "") } catch { } } + _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); return (string)_cache[name]; } public void Set(string name, bool value) { + _logger.LogInformation($"Setting boolean configuration property ({name}) to value ({value})..."); _cache[name] = value; EnsureTable(); _databaseService.ReplaceIntoTable(TableName, new Dictionary() @@ -248,10 +282,13 @@ public void Set(string name, bool value) { "name", name }, { "value", value.ToString() } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public void Set(string name, double value) { + _logger.LogInformation($"Setting double configuration property ({name}) to value ({value})..."); _cache[name] = value; EnsureTable(); _databaseService.ReplaceIntoTable(TableName, new Dictionary() @@ -259,10 +296,13 @@ public void Set(string name, double value) { "name", name }, { "value", value.ToString() } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public void Set(string name, int value) { + _logger.LogInformation($"Setting integer configuration property ({name}) to value ({value})..."); _cache[name] = value; EnsureTable(); _databaseService.ReplaceIntoTable(TableName, new Dictionary() @@ -270,10 +310,13 @@ public void Set(string name, int value) { "name", name }, { "value", value.ToString() } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public void Set(string name, string value) { + _logger.LogInformation($"Setting string configuration property ({name}) to value ({value})..."); _cache[name] = value; EnsureTable(); _databaseService.ReplaceIntoTable(TableName, new Dictionary() @@ -281,10 +324,13 @@ public void Set(string name, string value) { "name", name }, { "value", value } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public void Set(string name, T value, JsonTypeInfo info) where T : notnull { + _logger.LogInformation($"Setting object configuration property ({name}) to value ({value})..."); _cache[name] = value; EnsureTable(); _databaseService.ReplaceIntoTable(TableName, new Dictionary() @@ -292,10 +338,13 @@ public void Set(string name, T value, JsonTypeInfo info) where T : notnull { "name", name }, { "value", JsonSerializer.Serialize(value, info) } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public async Task SetAsync(string name, bool value) { + _logger.LogInformation($"Setting boolean configuration property ({name}) to value ({value})..."); _cache[name] = value; await EnsureTableAsync(); await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() @@ -303,10 +352,13 @@ public async Task SetAsync(string name, bool value) { "name", name }, { "value", value.ToString() } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public async Task SetAsync(string name, double value) { + _logger.LogInformation($"Setting double configuration property ({name}) to value ({value})..."); _cache[name] = value; await EnsureTableAsync(); await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() @@ -314,10 +366,13 @@ public async Task SetAsync(string name, double value) { "name", name }, { "value", value.ToString() } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public async Task SetAsync(string name, int value) { + _logger.LogInformation($"Setting integer configuration property ({name}) to value ({value})..."); _cache[name] = value; await EnsureTableAsync(); await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() @@ -325,10 +380,13 @@ public async Task SetAsync(string name, int value) { "name", name }, { "value", value.ToString() } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public async Task SetAsync(string name, string value) { + _logger.LogInformation($"Setting string configuration property ({name}) to value ({value})..."); _cache[name] = value; await EnsureTableAsync(); await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() @@ -336,10 +394,13 @@ public async Task SetAsync(string name, string value) { "name", name }, { "value", value } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } public async void SetAsync(string name, T value, JsonTypeInfo info) where T : notnull { + _logger.LogInformation($"Setting object configuration property ({name}) to value ({value})..."); _cache[name] = value; await EnsureTableAsync(); await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() @@ -347,6 +408,8 @@ public async void SetAsync(string name, T value, JsonTypeInfo info) where { "name", name }, { "value", JsonSerializer.Serialize(value, info) } }); + _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } private void EnsureTable() diff --git a/Nickvision.Desktop/Application/IConfigurationService.cs b/Nickvision.Desktop/Application/IConfigurationService.cs index 377bb52..4becf66 100644 --- a/Nickvision.Desktop/Application/IConfigurationService.cs +++ b/Nickvision.Desktop/Application/IConfigurationService.cs @@ -1,10 +1,13 @@ -using System.Text.Json.Serialization.Metadata; +using System; +using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; namespace Nickvision.Desktop.Application; public interface IConfigurationService { + event EventHandler? Saved; + bool GetBool(string name, bool defaultValue = false); Task GetBoolAsync(string name, bool defaultValue = false); double GetDouble(string name, double defaultValue = 0.0); diff --git a/Nickvision.Desktop/Filesystem/IJsonFileService.cs b/Nickvision.Desktop/Filesystem/IJsonFileService.cs deleted file mode 100644 index 1e22c15..0000000 --- a/Nickvision.Desktop/Filesystem/IJsonFileService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Text.Json.Serialization.Metadata; -using System.Threading.Tasks; - -namespace Nickvision.Desktop.Filesystem; - -public interface IJsonFileService -{ - event EventHandler? Saved; - - T Load(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new(); - Task LoadAsync(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new(); - bool Save(T obj, JsonTypeInfo jsonTypeInfo, string? name = null); - Task SaveAsync(T obj, JsonTypeInfo jsonTypeInfo, string? name = null); -} diff --git a/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs b/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs deleted file mode 100644 index 4bb712e..0000000 --- a/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; - -namespace Nickvision.Desktop.Filesystem; - -public class JsonFileSavedEventArgs : EventArgs -{ - public object Data { get; init; } - - public Type DataType { get; init; } - - public string Name { get; init; } - - public JsonFileSavedEventArgs(object data, Type dataType, string name) - { - Data = data; - DataType = dataType; - Name = name; - } -} diff --git a/Nickvision.Desktop/Filesystem/JsonFileService.cs b/Nickvision.Desktop/Filesystem/JsonFileService.cs deleted file mode 100644 index 12b33c5..0000000 --- a/Nickvision.Desktop/Filesystem/JsonFileService.cs +++ /dev/null @@ -1,126 +0,0 @@ -using Microsoft.Extensions.Logging; -using Nickvision.Desktop.Application; -using System; -using System.IO; -using System.Text.Json; -using System.Text.Json.Serialization.Metadata; -using System.Threading.Tasks; - -namespace Nickvision.Desktop.Filesystem; - -public class JsonFileService : IJsonFileService -{ - private readonly ILogger _logger; - private readonly string _directory; - - public event EventHandler? Saved; - - public JsonFileService(ILogger logger, AppInfo appInfo) : this(logger, appInfo.IsPortable ? System.Environment.ExecutingDirectory : Path.Combine(UserDirectories.Config, appInfo.Name)) - { - - } - - private JsonFileService(ILogger logger, string directory) - { - _logger = logger; - if (!Directory.Exists(directory)) - { - Directory.CreateDirectory(directory); - } - _directory = directory; - } - - public T Load(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new() - { - var path = Path.Combine(_directory, $"{(string.IsNullOrEmpty(name) ? typeof(T).Name.ToLower() : name)}.json"); - _logger.LogInformation($"Loading {typeof(T).Name} from {path}..."); - if (!File.Exists(path)) - { - _logger.LogWarning($"{path} not found, returning default {typeof(T).Name}."); - return new T(); - } - try - { - var text = File.ReadAllText(path); - var obj = JsonSerializer.Deserialize(text, jsonTypeInfo); - _logger.LogInformation($"Loaded {typeof(T).Name} successfully."); - return obj ?? new T(); - } - catch (Exception e) - { - _logger.LogWarning(e, $"Failed to load {path}, returning default {typeof(T).Name}."); - return new T(); - } - } - - public async Task LoadAsync(JsonTypeInfo jsonTypeInfo, string? name = null) where T : new() - { - var path = Path.Combine(_directory, $"{(string.IsNullOrEmpty(name) ? typeof(T).Name.ToLower() : name)}.json"); - _logger.LogInformation($"Loading {typeof(T).Name} from {path}..."); - if (!File.Exists(path)) - { - _logger.LogWarning($"{path} not found, returning default {typeof(T).Name}."); - return new T(); - } - try - { - var text = await File.ReadAllTextAsync(path); - var obj = JsonSerializer.Deserialize(text, jsonTypeInfo); - _logger.LogInformation($"Loaded {typeof(T).Name} successfully."); - return obj ?? new T(); - } - catch (Exception e) - { - _logger.LogWarning(e, $"Failed to load {path}, returning default {typeof(T).Name}."); - return new T(); - } - } - - public bool Save(T obj, JsonTypeInfo jsonTypeInfo, string? name = null) - { - if (obj is null) - { - return false; - } - name = string.IsNullOrEmpty(name) ? typeof(T).Name.ToLower() : name; - var path = Path.Combine(_directory, $"{name}.json"); - _logger.LogInformation($"Saving {typeof(T).Name} to {path}..."); - try - { - var text = JsonSerializer.Serialize(obj, jsonTypeInfo); - File.WriteAllText(path, text); - Saved?.Invoke(this, new JsonFileSavedEventArgs(obj, typeof(T), name)); - _logger.LogInformation($"Saved {path} successfully."); - return true; - } - catch (Exception e) - { - _logger.LogError($"Failed to save {path}: {e}"); - return false; - } - } - - public async Task SaveAsync(T obj, JsonTypeInfo jsonTypeInfo, string? name = null) - { - if (obj is null) - { - return false; - } - name = string.IsNullOrEmpty(name) ? typeof(T).Name.ToLower() : name; - var path = Path.Combine(_directory, $"{name}.json"); - _logger.LogInformation($"Saving {typeof(T).Name} to {path}..."); - try - { - var text = JsonSerializer.Serialize(obj, jsonTypeInfo); - await File.WriteAllTextAsync(path, text); - Saved?.Invoke(this, new JsonFileSavedEventArgs(obj, typeof(T), name)); - _logger.LogInformation($"Saved {path} successfully."); - return true; - } - catch (Exception e) - { - _logger.LogError($"Failed to save {path}: {e}"); - return false; - } - } -} \ No newline at end of file From 35f9d8d882746fad08680f73f5e9fff124ffe6e1 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Thu, 26 Mar 2026 22:31:59 -0400 Subject: [PATCH 03/16] feat: More efficient database operations --- .../Application/ConfigurationService.cs | 93 ++++++++++++++++--- .../Application/DatabaseService.cs | 22 +++++ .../Application/IConfigurationService.cs | 2 + .../Application/IDatabaseService.cs | 2 + .../HostApplicationBuilderExtensions.cs | 3 +- 5 files changed, 108 insertions(+), 14 deletions(-) diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index 96b278e..9cf93b0 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -1,4 +1,5 @@ -using Microsoft.Extensions.Logging; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Logging; using Nickvision.Desktop.Keyring; using System; using System.Collections.Generic; @@ -8,7 +9,7 @@ namespace Nickvision.Desktop.Application; -public class ConfigurationService : IConfigurationService +public class ConfigurationService : IAsyncDisposable, IConfigurationService, IDisposable { private static readonly string TableName; @@ -16,6 +17,7 @@ public class ConfigurationService : IConfigurationService private readonly IDatabaseService _databaseService; private readonly Dictionary _cache; private bool _tableEnsured; + private SqliteTransaction? _transaction; public event EventHandler? Saved; @@ -30,6 +32,25 @@ public ConfigurationService(ILogger logger, IDatabaseService dat _databaseService = databaseService; _cache = new Dictionary(); _tableEnsured = false; + _transaction = null; + } + + ~ConfigurationService() + { + Dispose(false); + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore().ConfigureAwait(false); + Dispose(false); + GC.SuppressFinalize(this); } public bool GetBool(string name, bool defaultValue = false) @@ -48,7 +69,7 @@ public bool GetBool(string name, bool defaultValue = false) { try { - _cache[name] = bool.Parse(reader.GetString(0)); + _cache[name] = bool.Parse(reader.GetString(1)); } catch { } } @@ -72,7 +93,7 @@ public async Task GetBoolAsync(string name, bool defaultValue = false) { try { - _cache[name] = bool.Parse(reader.GetString(0)); + _cache[name] = bool.Parse(reader.GetString(1)); } catch { } } @@ -96,7 +117,7 @@ public double GetDouble(string name, double defaultValue = 0.0) { try { - _cache[name] = double.Parse(reader.GetString(0)); + _cache[name] = double.Parse(reader.GetString(1)); } catch { } } @@ -120,7 +141,7 @@ public async Task GetDoubleAsync(string name, double defaultValue = 0.0) { try { - _cache[name] = double.Parse(reader.GetString(0)); + _cache[name] = double.Parse(reader.GetString(1)); } catch { } } @@ -144,7 +165,7 @@ public int GetInt(string name, int defaultValue = 0) { try { - _cache[name] = int.Parse(reader.GetString(0)); + _cache[name] = int.Parse(reader.GetString(1)); } catch { } } @@ -168,7 +189,7 @@ public async Task GetIntAsync(string name, int defaultValue = 0) { try { - _cache[name] = int.Parse(reader.GetString(0)); + _cache[name] = int.Parse(reader.GetString(1)); } catch { } } @@ -192,7 +213,7 @@ public T GetObject(string name, T defaultValue, JsonTypeInfo info) where T { try { - _cache[name] = JsonSerializer.Deserialize(reader.GetString(0), info)!; + _cache[name] = JsonSerializer.Deserialize(reader.GetString(1), info)!; } catch { } } @@ -216,7 +237,7 @@ public async Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo { try { - _cache[name] = JsonSerializer.Deserialize(reader.GetString(0), info)!; + _cache[name] = JsonSerializer.Deserialize(reader.GetString(1), info)!; } catch { } } @@ -240,7 +261,7 @@ public string GetString(string name, string defaultValue = "") { try { - _cache[name] = reader.GetString(0); + _cache[name] = reader.GetString(1); } catch { } } @@ -264,7 +285,7 @@ public async Task GetStringAsync(string name, string defaultValue = "") { try { - _cache[name] = reader.GetString(0); + _cache[name] = reader.GetString(1); } catch { } } @@ -272,6 +293,23 @@ public async Task GetStringAsync(string name, string defaultValue = "") return (string)_cache[name]; } + public void Save() + { + _transaction?.Commit(); + _transaction?.Dispose(); + EnsureTable(); + } + + public async Task SaveAsync() + { + if(_transaction is not null) + { + await _transaction.CommitAsync(); + await _transaction.DisposeAsync().ConfigureAwait(false); + } + await EnsureTableAsync(); + } + public void Set(string name, bool value) { _logger.LogInformation($"Setting boolean configuration property ({name}) to value ({value})..."); @@ -412,13 +450,39 @@ public async void SetAsync(string name, T value, JsonTypeInfo info) where Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } + protected virtual async ValueTask DisposeAsyncCore() + { + if (_transaction is not null) + { + await _transaction.CommitAsync(); + await _transaction.DisposeAsync().ConfigureAwait(false); + } + _transaction = null; + } + + private void Dispose(bool disposing) + { + if (!disposing) + { + return; + } + _transaction?.Commit(); + _transaction?.Dispose(); + _transaction = null; + } + private void EnsureTable() { if (_tableEnsured) { + if(_transaction is null) + { + _transaction = _databaseService.CreateTransation(); + } return; } _databaseService.EnsureTableExists(TableName, "name TEXT PRIMARY KEY, value TEXT"); + _transaction = _databaseService.CreateTransation(); _tableEnsured = true; } @@ -426,9 +490,14 @@ private async Task EnsureTableAsync() { if (_tableEnsured) { + if(_transaction is null) + { + _transaction = await _databaseService.CreateTransationAsync(); + } return; } await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT, value TEXT"); + _transaction = await _databaseService.CreateTransationAsync(); _tableEnsured = true; } } diff --git a/Nickvision.Desktop/Application/DatabaseService.cs b/Nickvision.Desktop/Application/DatabaseService.cs index e04ccd1..fa1a1ee 100644 --- a/Nickvision.Desktop/Application/DatabaseService.cs +++ b/Nickvision.Desktop/Application/DatabaseService.cs @@ -155,6 +155,28 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + public bool DropTable(string tableName) + { + EnsureDatabase(); + _logger.LogInformation($"Dropping table ({tableName})..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"DROP TABLE IF EXISTS {tableName}"; + command.ExecuteNonQuery(); + _logger.LogInformation($"Dropped table ({tableName})."); + return true; + } + + public async Task DropTableAsync(string tableName) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Dropping table ({tableName})..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"DROP TABLE IF EXISTS {tableName}"; + await command.ExecuteNonQueryAsync(); + _logger.LogInformation($"Dropped table ({tableName})."); + return true; + } + public bool EnsureTableExists(string tableName, string layout) { EnsureDatabase(); diff --git a/Nickvision.Desktop/Application/IConfigurationService.cs b/Nickvision.Desktop/Application/IConfigurationService.cs index 4becf66..a8240bf 100644 --- a/Nickvision.Desktop/Application/IConfigurationService.cs +++ b/Nickvision.Desktop/Application/IConfigurationService.cs @@ -18,6 +18,8 @@ public interface IConfigurationService Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull; string GetString(string name, string defaultValue = ""); Task GetStringAsync(string name, string defaultValue = ""); + void Save(); + Task SaveAsync(); void Set(string name, bool value); void Set(string name, double value); void Set(string name, int value); diff --git a/Nickvision.Desktop/Application/IDatabaseService.cs b/Nickvision.Desktop/Application/IDatabaseService.cs index e950d1c..5de02ad 100644 --- a/Nickvision.Desktop/Application/IDatabaseService.cs +++ b/Nickvision.Desktop/Application/IDatabaseService.cs @@ -15,6 +15,8 @@ public interface IDatabaseService Task CreateTransationAsync(); bool DeleteFromTable(string tableName, string columnName, string matchingValue); Task DeleteFromTableAsync(string tableName, string columnName, string matchingValue); + bool DropTable(string tableName); + Task DropTableAsync(string tableName); bool EnsureTableExists(string tableName, string layout); Task EnsureTableExistsAsync(string tableName, string layout); bool InsertIntoTable(string tableName, Dictionary data); diff --git a/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs b/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs index 9b0b9e5..62d7f43 100644 --- a/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs +++ b/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Nickvision.Desktop.Application; -using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.Globalization; using Nickvision.Desktop.Keyring; using Nickvision.Desktop.Notifications; @@ -26,8 +25,8 @@ public IHostApplicationBuilder ConfigureNickvision(string[] args, string logging } builder.Services.AddHttpClient(); builder.Services.AddSingleton(new ArgumentsService(args)); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); - builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); From ed1854f14eac513946ea52745508ca4df59930e8 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Thu, 26 Mar 2026 22:49:21 -0400 Subject: [PATCH 04/16] fix: Missing WHERE in DatabaseService --- .../ConfigurationServiceTests.cs | 21 +++ .../DatabaseServiceTests.cs | 18 +++ Nickvision.Desktop.Tests/HostingTests.cs | 4 +- .../JsonFileServiceTests.cs | 135 ------------------ .../Application/ConfigurationService.cs | 11 +- .../Application/DatabaseService.cs | 30 ++-- 6 files changed, 62 insertions(+), 157 deletions(-) create mode 100644 Nickvision.Desktop.Tests/ConfigurationServiceTests.cs create mode 100644 Nickvision.Desktop.Tests/DatabaseServiceTests.cs delete mode 100644 Nickvision.Desktop.Tests/JsonFileServiceTests.cs diff --git a/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs new file mode 100644 index 0000000..7c0c189 --- /dev/null +++ b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs @@ -0,0 +1,21 @@ +using Nickvision.Desktop.Application; +using Nickvision.Desktop.System; +using Nickvision.Desktop.Tests.Mocks; + +namespace Nickvision.Desktop.Tests; + +[TestClass] +public class ConfigurationServiceTests +{ + private static DatabaseService? _databaseService; + private static ConfigurationService? _configurationService; + + [TestMethod] + public void Case001_Init() + { + _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test", "Nickvision.Desktop.Test", "Test"), new SecretService(new MockLogger())); + _configurationService = new ConfigurationService(new MockLogger(), _databaseService); + Assert.IsNotNull(_databaseService); + Assert.IsNotNull(_configurationService); + } +} diff --git a/Nickvision.Desktop.Tests/DatabaseServiceTests.cs b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs new file mode 100644 index 0000000..b031460 --- /dev/null +++ b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs @@ -0,0 +1,18 @@ +using Nickvision.Desktop.Application; +using Nickvision.Desktop.System; +using Nickvision.Desktop.Tests.Mocks; + +namespace Nickvision.Desktop.Tests; + +[TestClass] +public class DatabaseServiceTests +{ + private static DatabaseService? _databaseService; + + [TestMethod] + public void Case001_Init() + { + _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test", "Nickvision.Desktop.Test", "Test"), new SecretService(new MockLogger())); + Assert.IsNotNull(_databaseService); + } +} diff --git a/Nickvision.Desktop.Tests/HostingTests.cs b/Nickvision.Desktop.Tests/HostingTests.cs index 7115fd1..774275a 100644 --- a/Nickvision.Desktop.Tests/HostingTests.cs +++ b/Nickvision.Desktop.Tests/HostingTests.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Nickvision.Desktop.Application; -using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.Globalization; using Nickvision.Desktop.Helpers; using Nickvision.Desktop.Keyring; @@ -44,7 +43,8 @@ public void Case003_Build() Assert.IsNotNull(host); Assert.IsNotNull(host.Services.GetRequiredService()); Assert.IsNotNull(host.Services.GetRequiredService()); - Assert.IsNotNull(host.Services.GetRequiredService()); + Assert.IsNotNull(host.Services.GetRequiredService()); + Assert.IsNotNull(host.Services.GetRequiredService()); Assert.IsNotNull(host.Services.GetRequiredService()); Assert.IsNotNull(host.Services.GetRequiredService()); Assert.IsNotNull(host.Services.GetRequiredService()); diff --git a/Nickvision.Desktop.Tests/JsonFileServiceTests.cs b/Nickvision.Desktop.Tests/JsonFileServiceTests.cs deleted file mode 100644 index 6f3358d..0000000 --- a/Nickvision.Desktop.Tests/JsonFileServiceTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -using Nickvision.Desktop.Application; -using Nickvision.Desktop.Filesystem; -using Nickvision.Desktop.Tests.Mocks; -using System.IO; -using System.Text.Json.Serialization; -using System.Threading.Tasks; - -namespace Nickvision.Desktop.Tests; - -public class Config -{ - public bool DarkModeEnabled { get; set; } - public WindowGeometry WindowGeometry { get; set; } - - public Config() - { - DarkModeEnabled = false; - WindowGeometry = new WindowGeometry(); - } -} - -[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true)] -[JsonSerializable(typeof(Config))] -internal partial class TestJsonContext : JsonSerializerContext { } - -[TestClass] -public sealed class JsonFileServiceTests -{ - private static JsonFileService? _jsonFileService; - - [ClassInitialize] - public static void ClassInitialize(TestContext context) - { - var configPath = Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config.json"); - var configAsyncPath = Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config-async.json"); - Directory.CreateDirectory(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests")); - foreach (var path in new[] { configPath, configAsyncPath }) - { - if (File.Exists(path)) - { - File.Delete(path); - } - } - } - - [TestMethod] - public void Case001_Initialize() - { - var appInfo = new AppInfo("org.nickvision.desktop.tests", "Nickvision.Desktop Tests", "Tests"); - _jsonFileService = new JsonFileService(new MockLogger(), appInfo); - Assert.IsNotNull(_jsonFileService); - } - - [TestMethod] - public void Case002_Load() - { - Assert.IsNotNull(_jsonFileService); - var config = _jsonFileService.Load(TestJsonContext.Default.Config); - Assert.IsNotNull(config); - Assert.IsFalse(config.DarkModeEnabled); - Assert.AreEqual(900, config.WindowGeometry.Width); - Assert.AreEqual(700, config.WindowGeometry.Height); - Assert.IsFalse(File.Exists(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config.json"))); - } - - [TestMethod] - public async Task Case003_LoadAsync() - { - Assert.IsNotNull(_jsonFileService); - var configAsync = await _jsonFileService.LoadAsync(TestJsonContext.Default.Config, "config-async"); - Assert.IsNotNull(configAsync); - Assert.IsFalse(configAsync.DarkModeEnabled); - Assert.AreEqual(900, configAsync.WindowGeometry.Width); - Assert.AreEqual(700, configAsync.WindowGeometry.Height); - Assert.IsFalse(File.Exists(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config-async.json"))); - } - - [TestMethod] - public void Case004_Change() - { - Assert.IsNotNull(_jsonFileService); - var config = _jsonFileService.Load(TestJsonContext.Default.Config); - config.DarkModeEnabled = true; - Assert.IsTrue(config.DarkModeEnabled); - Assert.IsTrue(_jsonFileService.Save(config, TestJsonContext.Default.Config)); - Assert.IsTrue(File.Exists(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config.json"))); - } - - [TestMethod] - public async Task Case005_ChangeAsync() - { - Assert.IsNotNull(_jsonFileService); - var configAsync = await _jsonFileService.LoadAsync(TestJsonContext.Default.Config, "config-async"); - configAsync.DarkModeEnabled = true; - Assert.IsTrue(configAsync.DarkModeEnabled); - Assert.IsTrue(await _jsonFileService.SaveAsync(configAsync, TestJsonContext.Default.Config, "config-async")); - Assert.IsTrue(File.Exists(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config-async.json"))); - } - - [TestMethod] - public void Case006_Verify() - { - Assert.IsNotNull(_jsonFileService); - var config = _jsonFileService.Load(TestJsonContext.Default.Config); - Assert.IsNotNull(config); - Assert.IsTrue(config.DarkModeEnabled); - } - - [TestMethod] - public async Task Case007_VerifyAsync() - { - Assert.IsNotNull(_jsonFileService); - var configAsync = await _jsonFileService.LoadAsync(TestJsonContext.Default.Config, "config-async"); - Assert.IsNotNull(configAsync); - Assert.IsTrue(configAsync.DarkModeEnabled); - } - - [TestMethod] - public void Case008_Cleanup() - { - var configPath = Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config.json"); - var configAsyncPath = Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests", "config-async.json"); - if (File.Exists(configPath)) - { - File.Delete(configPath); - } - if (File.Exists(configAsyncPath)) - { - File.Delete(configAsyncPath); - } - Assert.IsFalse(File.Exists(configPath)); - Assert.IsFalse(File.Exists(configAsyncPath)); - Directory.Delete(Path.Combine(UserDirectories.Config, "Nickvision.Desktop Tests")); - } -} diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index 9cf93b0..504fb7e 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -1,6 +1,5 @@ using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; -using Nickvision.Desktop.Keyring; using System; using System.Collections.Generic; using System.Text.Json; @@ -13,7 +12,7 @@ public class ConfigurationService : IAsyncDisposable, IConfigurationService, IDi { private static readonly string TableName; - private readonly ILogger _logger; + private readonly ILogger _logger; private readonly IDatabaseService _databaseService; private readonly Dictionary _cache; private bool _tableEnsured; @@ -26,7 +25,7 @@ static ConfigurationService() TableName = "configuration"; } - public ConfigurationService(ILogger logger, IDatabaseService databaseService) + public ConfigurationService(ILogger logger, IDatabaseService databaseService) { _logger = logger; _databaseService = databaseService; @@ -302,7 +301,7 @@ public void Save() public async Task SaveAsync() { - if(_transaction is not null) + if (_transaction is not null) { await _transaction.CommitAsync(); await _transaction.DisposeAsync().ConfigureAwait(false); @@ -475,7 +474,7 @@ private void EnsureTable() { if (_tableEnsured) { - if(_transaction is null) + if (_transaction is null) { _transaction = _databaseService.CreateTransation(); } @@ -490,7 +489,7 @@ private async Task EnsureTableAsync() { if (_tableEnsured) { - if(_transaction is null) + if (_transaction is null) { _transaction = await _databaseService.CreateTransationAsync(); } diff --git a/Nickvision.Desktop/Application/DatabaseService.cs b/Nickvision.Desktop/Application/DatabaseService.cs index fa1a1ee..240bcb3 100644 --- a/Nickvision.Desktop/Application/DatabaseService.cs +++ b/Nickvision.Desktop/Application/DatabaseService.cs @@ -37,8 +37,8 @@ public bool ContainsInTable(string tableName, string columnName, string matching EnsureDatabase(); _logger.LogInformation($"Checking if {tableName} contains value in column ({columnName})..."); using var command = _connection!.CreateCommand(); - command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $param"; - command.Parameters.AddWithValue("$param", matchingValue); + command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); using var reader = command.ExecuteReader(); var result = false; while (reader.Read()) @@ -65,8 +65,8 @@ public async Task ContainsInTableAsync(string tableName, string columnName await EnsureDatabaseAsync(); _logger.LogInformation($"Checking if {tableName} contains value in column ({columnName})..."); await using var command = _connection!.CreateCommand(); - command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $param"; - command.Parameters.AddWithValue("$param", matchingValue); + command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); await using var reader = await command.ExecuteReaderAsync(); var result = false; while (await reader.ReadAsync()) @@ -107,8 +107,8 @@ public bool DeleteFromTable(string tableName, string columnName, string matching EnsureDatabase(); _logger.LogInformation($"Deleting row from {tableName}..."); using var command = _connection!.CreateCommand(); - command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $param"; - command.Parameters.AddWithValue("$param", matchingValue); + command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); var result = command.ExecuteNonQuery() > 0; if (result) { @@ -127,8 +127,8 @@ public async Task DeleteFromTableAsync(string tableName, string columnName await EnsureDatabaseAsync(); _logger.LogInformation($"Deleting row from {tableName}..."); await using var command = _connection!.CreateCommand(); - command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $param"; - command.Parameters.AddWithValue("$param", matchingValue); + command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); var result = await command.ExecuteNonQueryAsync() > 0; if (result) { @@ -292,8 +292,8 @@ public SqliteCommand SelectFromTable(string tableName, string columnName, string EnsureDatabase(); _logger.LogInformation($"Selecting data from table {tableName} with matching column ({columnName})..."); var command = _connection!.CreateCommand(); - command.CommandText = $"SELECT * FROM {tableName} WHERE {columnName} = $param"; - command.Parameters.AddWithValue("$param", matchingValue); + command.CommandText = $"SELECT * FROM {tableName} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); _logger.LogInformation($"Selected data from table {tableName} with matching column ({columnName})."); return command; } @@ -303,8 +303,8 @@ public async Task SelectFromTableAsync(string tableName, string c await EnsureDatabaseAsync(); _logger.LogInformation($"Selecting data from table {tableName} with matching column ({columnName})..."); var command = _connection!.CreateCommand(); - command.CommandText = $"SELECT * FROM {tableName} WHERE {columnName} = $param"; - command.Parameters.AddWithValue("$param", matchingValue); + command.CommandText = $"SELECT * FROM {tableName} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); _logger.LogInformation($"Selected data from table {tableName} with matching column ({columnName})."); return command; } @@ -334,7 +334,8 @@ public bool UpdateInTable(string tableName, string columnName, string matchingVa EnsureDatabase(); _logger.LogInformation($"Updating data in {tableName}..."); using var command = _connection!.CreateCommand(); - command.CommandText = $"UPDATE {tableName} SET {string.Join(", ", newData.Keys.Select(k => $"{k} = ${k}"))}"; + command.CommandText = $"UPDATE {tableName} SET {string.Join(", ", newData.Keys.Select(k => $"{k} = ${k}"))} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); foreach (var pair in newData) { command.Parameters.AddWithValue($"${pair.Key}", pair.Value); @@ -356,7 +357,8 @@ public async Task UpdateInTableAsync(string tableName, string columnName, await EnsureDatabaseAsync(); _logger.LogInformation($"Updating data in {tableName}..."); await using var command = _connection!.CreateCommand(); - command.CommandText = $"UPDATE {tableName} SET {string.Join(", ", newData.Keys.Select(k => $"{k} = ${k}"))}"; + command.CommandText = $"UPDATE {tableName} SET {string.Join(", ", newData.Keys.Select(k => $"{k} = ${k}"))} WHERE {columnName} = $matchingValueParam"; + command.Parameters.AddWithValue("$matchingValueParam", matchingValue); foreach (var pair in newData) { command.Parameters.AddWithValue($"${pair.Key}", pair.Value); From 235e0b6bf7fcd59736878f58e8d4ba20f7ae53be Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Thu, 26 Mar 2026 22:57:11 -0400 Subject: [PATCH 05/16] fix: Config service --- Nickvision.Desktop/Application/ConfigurationService.cs | 6 ++++-- Nickvision.Desktop/Application/IConfigurationService.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index 504fb7e..4edc0b6 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -296,6 +296,7 @@ public void Save() { _transaction?.Commit(); _transaction?.Dispose(); + _transaction = null; EnsureTable(); } @@ -305,6 +306,7 @@ public async Task SaveAsync() { await _transaction.CommitAsync(); await _transaction.DisposeAsync().ConfigureAwait(false); + _transaction = null; } await EnsureTableAsync(); } @@ -435,7 +437,7 @@ public async Task SetAsync(string name, string value) Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } - public async void SetAsync(string name, T value, JsonTypeInfo info) where T : notnull + public async Task SetAsync(string name, T value, JsonTypeInfo info) where T : notnull { _logger.LogInformation($"Setting object configuration property ({name}) to value ({value})..."); _cache[name] = value; @@ -495,7 +497,7 @@ private async Task EnsureTableAsync() } return; } - await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT, value TEXT"); + await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT PRIMARY KEY, value TEXT"); _transaction = await _databaseService.CreateTransationAsync(); _tableEnsured = true; } diff --git a/Nickvision.Desktop/Application/IConfigurationService.cs b/Nickvision.Desktop/Application/IConfigurationService.cs index a8240bf..8d05cb9 100644 --- a/Nickvision.Desktop/Application/IConfigurationService.cs +++ b/Nickvision.Desktop/Application/IConfigurationService.cs @@ -29,5 +29,5 @@ public interface IConfigurationService Task SetAsync(string name, double value); Task SetAsync(string name, int value); Task SetAsync(string name, string value); - void SetAsync(string name, T value, JsonTypeInfo info) where T : notnull; + Task SetAsync(string name, T value, JsonTypeInfo info) where T : notnull; } From cb9f8cb2cfcd6db1f569984a5a5a4a1e28b7e221 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Thu, 26 Mar 2026 23:44:42 -0400 Subject: [PATCH 06/16] feat: Add get raw methods to ConfigurationService --- .../ConfigurationServiceTests.cs | 170 +++++++++++++++++- .../Application/ConfigurationService.cs | 41 ++++- .../Application/IConfigurationService.cs | 3 + 3 files changed, 211 insertions(+), 3 deletions(-) diff --git a/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs index 7c0c189..41e9390 100644 --- a/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs +++ b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs @@ -1,9 +1,27 @@ using Nickvision.Desktop.Application; +using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.System; using Nickvision.Desktop.Tests.Mocks; +using System.IO; +using System.Text.Json.Serialization; +using System.Threading.Tasks; namespace Nickvision.Desktop.Tests; +public class TestObj +{ + public string Test { get; set; } + + public TestObj(string test) + { + Test = test; + } +} + +[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true)] +[JsonSerializable(typeof(TestObj))] +internal partial class TestJsonContext : JsonSerializerContext { } + [TestClass] public class ConfigurationServiceTests { @@ -13,9 +31,159 @@ public class ConfigurationServiceTests [TestMethod] public void Case001_Init() { - _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test", "Nickvision.Desktop.Test", "Test"), new SecretService(new MockLogger())); + _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test.config", "Nickvision.Desktop.Test.Config", "Config Test"), new SecretService(new MockLogger())); _configurationService = new ConfigurationService(new MockLogger(), _databaseService); Assert.IsNotNull(_databaseService); Assert.IsNotNull(_configurationService); } + + [TestMethod] + public async Task Case002_Get() + { + Assert.IsNotNull(_configurationService); + var val1 = _configurationService.GetBool("nonExistentBool", false); + Assert.AreEqual(false, val1); + var val2 = await _configurationService.GetBoolAsync("nonExistentBoolAsync", true); + Assert.AreEqual(true, val2); + var val3 = _configurationService.GetDouble("nonExistentDouble", 1.5); + Assert.AreEqual(1.5, val3); + var val4 = await _configurationService.GetDoubleAsync("nonExistentDoubleAsync", 2.5); + Assert.AreEqual(2.5, val4); + var val5 = _configurationService.GetInt("nonExistentInt", 42); + Assert.AreEqual(42, val5); + var val6 = await _configurationService.GetIntAsync("nonExistentIntAsync", 84); + Assert.AreEqual(84, val6); + var val7 = _configurationService.GetString("nonExistentString", "default"); + Assert.AreEqual("default", val7); + var val8 = await _configurationService.GetStringAsync("nonExistentStringAsync", "asyncDefault"); + Assert.AreEqual("asyncDefault", val8); + var val9 = _configurationService.GetObject("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); + Assert.AreEqual("value", val9.Test); + var val10 = await _configurationService.GetObjectAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); + Assert.AreEqual("asyncValue", val10.Test); + } + + [TestMethod] + public void Case003_Save() + { + Assert.IsNotNull(_configurationService); + _configurationService.Save(); + } + + [TestMethod] + public async Task Case004_Set() + { + Assert.IsNotNull(_configurationService); + _configurationService.Set("nonExistentBool", true); + await _configurationService.SetAsync("nonExistentBoolAsync", false); + _configurationService.Set("nonExistentDouble", 2.5); + await _configurationService.SetAsync("nonExistentDoubleAsync", 1.5); + _configurationService.Set("nonExistentInt", 84); + await _configurationService.SetAsync("nonExistentIntAsync", 42); + _configurationService.Set("nonExistentString", "default2"); + await _configurationService.SetAsync("nonExistentStringAsync", "asyncDefault2"); + _configurationService.Set("nonExistentObject", new TestObj("value2"), TestJsonContext.Default.TestObj); + await _configurationService.SetAsync("nonExistentObjectAsync", new TestObj("asyncValue2"), TestJsonContext.Default.TestObj); + } + + [TestMethod] + public async Task Case005_Save() + { + Assert.IsNotNull(_configurationService); + await _configurationService.SaveAsync(); + } + + [TestMethod] + public async Task Case006_Get() + { + Assert.IsNotNull(_configurationService); + var val1 = _configurationService.GetBool("nonExistentBool", false); + Assert.AreEqual(true, val1); + var val2 = await _configurationService.GetBoolAsync("nonExistentBoolAsync", true); + Assert.AreEqual(false, val2); + var val3 = _configurationService.GetDouble("nonExistentDouble", 1.5); + Assert.AreEqual(2.5, val3); + var val4 = await _configurationService.GetDoubleAsync("nonExistentDoubleAsync", 2.5); + Assert.AreEqual(1.5, val4); + var val5 = _configurationService.GetInt("nonExistentInt", 42); + Assert.AreEqual(84, val5); + var val6 = await _configurationService.GetIntAsync("nonExistentIntAsync", 84); + Assert.AreEqual(42, val6); + var val7 = _configurationService.GetString("nonExistentString", "default"); + Assert.AreEqual("default2", val7); + var val8 = await _configurationService.GetStringAsync("nonExistentStringAsync", "asyncDefault"); + Assert.AreEqual("asyncDefault2", val8); + var val9 = _configurationService.GetObject("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); + Assert.AreEqual("value2", val9.Test); + var val10 = await _configurationService.GetObjectAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); + Assert.AreEqual("asyncValue2", val10.Test); + } + + [TestMethod] + public async Task Case007_Dispose() + { + Assert.IsNotNull(_databaseService); + Assert.IsNotNull(_configurationService); + await _configurationService.DisposeAsync(); + await _databaseService.DisposeAsync(); + _databaseService = null; + _configurationService = null; + } + + [TestMethod] + public void Case008_Init() + { + _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test.config", "Nickvision.Desktop.Test.Config", "Config Test"), new SecretService(new MockLogger())); + _configurationService = new ConfigurationService(new MockLogger(), _databaseService); + Assert.IsNotNull(_databaseService); + Assert.IsNotNull(_configurationService); + } + + [TestMethod] + public async Task Case009_Get() + { + Assert.IsNotNull(_configurationService); + var val1 = _configurationService.GetBool("nonExistentBool", false); + Assert.AreEqual(true, val1); + var val2 = await _configurationService.GetBoolAsync("nonExistentBoolAsync", true); + Assert.AreEqual(false, val2); + var val3 = _configurationService.GetDouble("nonExistentDouble", 1.5); + Assert.AreEqual(2.5, val3); + var val4 = await _configurationService.GetDoubleAsync("nonExistentDoubleAsync", 2.5); + Assert.AreEqual(1.5, val4); + var val5 = _configurationService.GetInt("nonExistentInt", 42); + Assert.AreEqual(84, val5); + var val6 = await _configurationService.GetIntAsync("nonExistentIntAsync", 84); + Assert.AreEqual(42, val6); + var val7 = _configurationService.GetString("nonExistentString", "default"); + Assert.AreEqual("default2", val7); + var val8 = await _configurationService.GetStringAsync("nonExistentStringAsync", "asyncDefault"); + Assert.AreEqual("asyncDefault2", val8); + var val9 = _configurationService.GetObject("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); + Assert.AreEqual("value2", val9.Test); + var val10 = await _configurationService.GetObjectAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); + Assert.AreEqual("asyncValue2", val10.Test); + } + + [TestMethod] + public async Task Case010_GetAll() + { + Assert.IsNotNull(_configurationService); + Assert.AreEqual(10, (await _configurationService.GetAllRawAsync()).Count); + } + + [TestMethod] + public async Task Case011_Cleanup() + { + var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test.Config", "app.db"); + Assert.IsNotNull(_databaseService); + Assert.IsNotNull(_configurationService); + await _configurationService.DisposeAsync(); + await _databaseService.DisposeAsync(); + File.Delete(path); + Directory.Delete(Path.GetDirectoryName(path)!); + Assert.IsFalse(File.Exists(path)); + _databaseService = null; + _configurationService = null; + } } diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index 4edc0b6..51e84c9 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -52,6 +52,45 @@ public async ValueTask DisposeAsync() GC.SuppressFinalize(this); } + public Dictionary GetAllRaw() + { + _logger.LogInformation("Getting all raw configuration properties..."); + var dict = new Dictionary(); + EnsureTable(); + using var command = _databaseService.SelectAllFromTable(TableName); + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + try + { + dict[reader.GetString(0)] = reader.GetString(1); + } + catch { } + } + _logger.LogInformation($"Found ({dict.Count}) raw configuration properties in database."); + return dict; + } + + public async Task> GetAllRawAsync() + { + _logger.LogInformation("Getting all raw configuration properties..."); + await EnsureTableAsync(); + var dict = new Dictionary(); + await using var command = await _databaseService.SelectAllFromTableAsync(TableName); + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + try + { + dict[reader.GetString(0)] = reader.GetString(1); + _logger.LogInformation($"Found raw configuration property ({reader.GetString(0)}) in database with value ({reader.GetString(1)})."); + } + catch { } + } + _logger.LogInformation($"Found ({dict.Count}) raw configuration properties in database."); + return dict; + } + public bool GetBool(string name, bool defaultValue = false) { _logger.LogInformation($"Getting boolean configuration property ({name})..."); @@ -297,7 +336,6 @@ public void Save() _transaction?.Commit(); _transaction?.Dispose(); _transaction = null; - EnsureTable(); } public async Task SaveAsync() @@ -308,7 +346,6 @@ public async Task SaveAsync() await _transaction.DisposeAsync().ConfigureAwait(false); _transaction = null; } - await EnsureTableAsync(); } public void Set(string name, bool value) diff --git a/Nickvision.Desktop/Application/IConfigurationService.cs b/Nickvision.Desktop/Application/IConfigurationService.cs index 8d05cb9..217e612 100644 --- a/Nickvision.Desktop/Application/IConfigurationService.cs +++ b/Nickvision.Desktop/Application/IConfigurationService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; @@ -8,6 +9,8 @@ public interface IConfigurationService { event EventHandler? Saved; + Dictionary GetAllRaw(); + Task> GetAllRawAsync(); bool GetBool(string name, bool defaultValue = false); Task GetBoolAsync(string name, bool defaultValue = false); double GetDouble(string name, double defaultValue = 0.0); From 478e162f1d7d6cd2f5adc5825c0a0551f9e9cb8e Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Thu, 26 Mar 2026 23:44:56 -0400 Subject: [PATCH 07/16] feat: Add migration to KeyringService from old ring2 --- .../KeyringServiceTests.cs | 8 ++- Nickvision.Desktop/Keyring/KeyringService.cs | 56 +++++++++++++++++-- 2 files changed, 57 insertions(+), 7 deletions(-) diff --git a/Nickvision.Desktop.Tests/KeyringServiceTests.cs b/Nickvision.Desktop.Tests/KeyringServiceTests.cs index 2be3037..628afb6 100644 --- a/Nickvision.Desktop.Tests/KeyringServiceTests.cs +++ b/Nickvision.Desktop.Tests/KeyringServiceTests.cs @@ -19,8 +19,10 @@ public sealed class KeyringServiceTests [TestMethod] public void Case001_Init() { - _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test", "Nickvision.Desktop.Test", "Test"), new SecretService(new MockLogger())); - _keyringService = new KeyringService(new MockLogger(), _databaseService); + var appInfo = new AppInfo("org.nickvision.desktop.test.keyring", "Nickvision.Desktop.Test.Keyring", "Keyring Test"); + var secretService = new SecretService(new MockLogger()); + _databaseService = new DatabaseService(new MockLogger(), appInfo, secretService); + _keyringService = new KeyringService(new MockLogger(), appInfo, _databaseService, secretService); Assert.IsNotNull(_databaseService); Assert.IsNotNull(_keyringService); } @@ -62,7 +64,7 @@ public async Task Case005_Remove() [TestMethod] public async Task Case006_Cleanup() { - var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test", "app.db"); + var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test.Keyring", "app.db"); Assert.IsNotNull(_databaseService); Assert.IsNotNull(_keyringService); await _databaseService.DisposeAsync(); diff --git a/Nickvision.Desktop/Keyring/KeyringService.cs b/Nickvision.Desktop/Keyring/KeyringService.cs index fe01351..105f9f6 100644 --- a/Nickvision.Desktop/Keyring/KeyringService.cs +++ b/Nickvision.Desktop/Keyring/KeyringService.cs @@ -1,7 +1,11 @@ +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using Nickvision.Desktop.Application; +using Nickvision.Desktop.Filesystem; +using Nickvision.Desktop.System; using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; namespace Nickvision.Desktop.Keyring; @@ -11,7 +15,9 @@ public class KeyringService : IKeyringService private static readonly string TableName; private readonly ILogger _logger; + private readonly AppInfo _appInfo; private readonly IDatabaseService _databaseService; + private readonly ISecretService _secretService; private bool _tableEnsured; static KeyringService() @@ -19,17 +25,19 @@ static KeyringService() TableName = "credentials"; } - public KeyringService(ILogger logger, IDatabaseService databaseService) + public KeyringService(ILogger logger, AppInfo appInfo, IDatabaseService databaseService, ISecretService secretService) { _logger = logger; + _appInfo = appInfo; _databaseService = databaseService; + _secretService = secretService; _tableEnsured = false; } public async Task AddCredentialAsync(Credential credential) { await EnsureTableAsync(); - _logger.LogInformation($"Adding keyring credential ({credential.Name})."); + _logger.LogInformation($"Adding keyring credential ({credential.Name})..."); if (await _databaseService.ContainsInTableAsync(TableName, "name", credential.Name)) { _logger.LogError($"Unable to add keyring credential ({credential.Name}) as it already exists."); @@ -56,7 +64,7 @@ public async Task AddCredentialAsync(Credential credential) public async Task DeleteCredentialAsync(Credential credential) { await EnsureTableAsync(); - _logger.LogInformation($"Deleting keyring credential ({credential.Name})."); + _logger.LogInformation($"Deleting keyring credential ({credential.Name})..."); var result = await _databaseService.DeleteFromTableAsync(TableName, "name", credential.Name); if (result) { @@ -84,7 +92,7 @@ public async Task> GetAllCredentialAsync() public async Task UpdateCredentialAsync(Credential credential) { - _logger.LogInformation($"Updating keyring credential ({credential.Name})."); + _logger.LogInformation($"Updating keyring credential ({credential.Name})..."); if (!await _databaseService.ContainsInTableAsync(TableName, "name", credential.Name)) { _logger.LogError($"Unable to update keyring credential ({credential.Name}) as it does not exist."); @@ -115,5 +123,45 @@ private async Task EnsureTableAsync() } await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT PRIMARY KEY, uri TEXT, username TEXT, password TEXT"); _tableEnsured = true; + var ring2Path = Path.Combine(UserDirectories.Config, "Nickvision", "Keyring", $"{_appInfo.Id}.ring2"); + if(File.Exists(ring2Path)) + { + _logger.LogInformation($"Found old keyring ring2 file ({ring2Path}), migrating credentials..."); + var secret = await _secretService.GetAsync(_appInfo.Id); + if(secret is null) + { + _logger.LogError($"Unable to migrate old keyring ring2 file ({ring2Path}) as no secret were found for the app."); + File.Delete(ring2Path); + _logger.LogInformation($"Deleted old keyring ring2 file ({ring2Path})."); + return; + } + var oldCredentialDb = new SqliteConnection(new SqliteConnectionStringBuilder($"Data Source='{ring2Path}'") + { + Mode = SqliteOpenMode.ReadWriteCreate, + Password = secret.Value, + Pooling = false + }.ToString()); + try + { + await oldCredentialDb.OpenAsync(); + await using var command = oldCredentialDb.CreateCommand(); + command.CommandText = $"SELECT name, uri, username, password FROM {TableName}"; + await using var reader = await command.ExecuteReaderAsync(); + while (await reader.ReadAsync()) + { + var credential = new Credential(reader.GetString(0), reader.GetString(2), reader.GetString(3), new Uri(reader.GetString(1))); + await AddCredentialAsync(credential); + } + await oldCredentialDb.CloseAsync(); + _logger.LogInformation($"Migrated keyring credentials from ring2 file ({ring2Path}) successfully."); + } + catch (Exception ex) + { + _logger.LogError($"Failed to migrate keyring credentials from ring2 file ({ring2Path}): {ex}"); + } + await oldCredentialDb.DisposeAsync(); + File.Delete(ring2Path); + _logger.LogInformation($"Deleted old keyring ring2 file ({ring2Path})."); + } } } From 5866b4bee70bf11a29ce57bf0c413f2206768a7e Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Thu, 26 Mar 2026 23:54:54 -0400 Subject: [PATCH 08/16] feat: Improve keyring performance --- .../KeyringServiceTests.cs | 24 +++++++++++++--- Nickvision.Desktop/Keyring/Credential.cs | 8 +++++- Nickvision.Desktop/Keyring/IKeyringService.cs | 2 +- Nickvision.Desktop/Keyring/KeyringService.cs | 28 +++++++++++-------- 4 files changed, 45 insertions(+), 17 deletions(-) diff --git a/Nickvision.Desktop.Tests/KeyringServiceTests.cs b/Nickvision.Desktop.Tests/KeyringServiceTests.cs index 628afb6..01cb471 100644 --- a/Nickvision.Desktop.Tests/KeyringServiceTests.cs +++ b/Nickvision.Desktop.Tests/KeyringServiceTests.cs @@ -28,16 +28,32 @@ public void Case001_Init() } [TestMethod] - public async Task Case002_Add() + public async Task Case002_Get() + { + Assert.IsNotNull(_keyringService); + Assert.AreEqual(0, (await _keyringService.GetAllCredentialAsync()).Count); + } + + [TestMethod] + public async Task Case003_Add() { Assert.IsNotNull(_keyringService); Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("YouTube", "abc", "123", new Uri("https://www.youtube.com")))); Assert.IsNotNull((await _keyringService.GetAllCredentialAsync()).FirstOrDefault(c => c.Name == "YouTube")); Assert.IsFalse(await _keyringService.AddCredentialAsync(new Credential("YouTube", "abc", "123", new Uri("https://www.youtube.com")))); + Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("YouTube 2", "def", "456", new Uri("https://www.youtube.com")))); + Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("YouTube 3", "ghi", "789", new Uri("https://www.youtube.com")))); + } + + [TestMethod] + public async Task Case004_Get() + { + Assert.IsNotNull(_keyringService); + Assert.AreEqual(3, (await _keyringService.GetAllCredentialAsync()).Count); } [TestMethod] - public async Task Case004_Update() + public async Task Case005_Update() { Assert.IsNotNull(_keyringService); Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("Google", "x@gmail.com", "asdfgh123!", new Uri("https://www.google.com")))); @@ -51,7 +67,7 @@ public async Task Case004_Update() } [TestMethod] - public async Task Case005_Remove() + public async Task Case006_Remove() { Assert.IsNotNull(_keyringService); Assert.IsTrue(await _keyringService.AddCredentialAsync(new Credential("Example", "user1", "pass1", new Uri("https://www.example.com")))); @@ -62,7 +78,7 @@ public async Task Case005_Remove() } [TestMethod] - public async Task Case006_Cleanup() + public async Task Case007_Cleanup() { var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test.Keyring", "app.db"); Assert.IsNotNull(_databaseService); diff --git a/Nickvision.Desktop/Keyring/Credential.cs b/Nickvision.Desktop/Keyring/Credential.cs index 8b80a7f..f091d61 100644 --- a/Nickvision.Desktop/Keyring/Credential.cs +++ b/Nickvision.Desktop/Keyring/Credential.cs @@ -3,7 +3,7 @@ namespace Nickvision.Desktop.Keyring; -public class Credential +public class Credential : IEquatable { public string Name { get; set; } @@ -20,4 +20,10 @@ public Credential(string name, string username, string password, Uri? url = null Password = password; Url = url ?? Uri.Empty; } + + public override bool Equals(object? obj) => obj is Credential credential && Equals(credential); + + public bool Equals(Credential? other) => other is not null && Name == other.Name; + + public override int GetHashCode() => Name.GetHashCode(); } diff --git a/Nickvision.Desktop/Keyring/IKeyringService.cs b/Nickvision.Desktop/Keyring/IKeyringService.cs index c25d50f..83046f2 100644 --- a/Nickvision.Desktop/Keyring/IKeyringService.cs +++ b/Nickvision.Desktop/Keyring/IKeyringService.cs @@ -7,6 +7,6 @@ public interface IKeyringService { Task AddCredentialAsync(Credential credential); Task DeleteCredentialAsync(Credential credential); - Task> GetAllCredentialAsync(); + Task> GetAllCredentialAsync(); Task UpdateCredentialAsync(Credential credential); } diff --git a/Nickvision.Desktop/Keyring/KeyringService.cs b/Nickvision.Desktop/Keyring/KeyringService.cs index 105f9f6..7c226ca 100644 --- a/Nickvision.Desktop/Keyring/KeyringService.cs +++ b/Nickvision.Desktop/Keyring/KeyringService.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; namespace Nickvision.Desktop.Keyring; @@ -18,6 +19,7 @@ public class KeyringService : IKeyringService private readonly AppInfo _appInfo; private readonly IDatabaseService _databaseService; private readonly ISecretService _secretService; + private readonly List _credentials; private bool _tableEnsured; static KeyringService() @@ -31,6 +33,7 @@ public KeyringService(ILogger logger, AppInfo appInfo, IDatabase _appInfo = appInfo; _databaseService = databaseService; _secretService = secretService; + _credentials = []; _tableEnsured = false; } @@ -38,7 +41,7 @@ public async Task AddCredentialAsync(Credential credential) { await EnsureTableAsync(); _logger.LogInformation($"Adding keyring credential ({credential.Name})..."); - if (await _databaseService.ContainsInTableAsync(TableName, "name", credential.Name)) + if (_credentials.Any(c => c.Name == credential.Name)) { _logger.LogError($"Unable to add keyring credential ({credential.Name}) as it already exists."); return false; @@ -52,6 +55,7 @@ public async Task AddCredentialAsync(Credential credential) }); if (result) { + _credentials.Add(credential); _logger.LogInformation($"Added keyring credential ({credential.Name}) successfully."); } else @@ -68,6 +72,7 @@ public async Task DeleteCredentialAsync(Credential credential) var result = await _databaseService.DeleteFromTableAsync(TableName, "name", credential.Name); if (result) { + _credentials.Remove(credential); _logger.LogInformation($"Removed keyring credential ({credential.Name}) successfully."); } else @@ -77,23 +82,17 @@ public async Task DeleteCredentialAsync(Credential credential) return result; } - public async Task> GetAllCredentialAsync() + public async Task> GetAllCredentialAsync() { await EnsureTableAsync(); - var credentials = new List(); - await using var command = await _databaseService.SelectAllFromTableAsync(TableName); - await using var reader = await command.ExecuteReaderAsync(); - while (await reader.ReadAsync()) - { - credentials.Add(new Credential(reader.GetString(0), reader.GetString(2), reader.GetString(3), new Uri(reader.GetString(1)))); - } - return credentials; + return _credentials; } public async Task UpdateCredentialAsync(Credential credential) { _logger.LogInformation($"Updating keyring credential ({credential.Name})..."); - if (!await _databaseService.ContainsInTableAsync(TableName, "name", credential.Name)) + var index = _credentials.IndexOf(credential); + if (index == -1) { _logger.LogError($"Unable to update keyring credential ({credential.Name}) as it does not exist."); return false; @@ -106,6 +105,7 @@ public async Task UpdateCredentialAsync(Credential credential) }); if (result) { + _credentials[index] = credential; _logger.LogInformation($"Updated keyring credential ({credential.Name}) successfully."); } else @@ -123,6 +123,12 @@ private async Task EnsureTableAsync() } await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT PRIMARY KEY, uri TEXT, username TEXT, password TEXT"); _tableEnsured = true; + await using var commandAll = await _databaseService.SelectAllFromTableAsync(TableName); + await using var readerAll = await commandAll.ExecuteReaderAsync(); + while (await readerAll.ReadAsync()) + { + _credentials.Add(new Credential(readerAll.GetString(0), readerAll.GetString(2), readerAll.GetString(3), new Uri(readerAll.GetString(1)))); + } var ring2Path = Path.Combine(UserDirectories.Config, "Nickvision", "Keyring", $"{_appInfo.Id}.ring2"); if(File.Exists(ring2Path)) { From b113d50b32ec133b2138839211a64b106359b0d4 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 19:24:30 -0400 Subject: [PATCH 09/16] feat: ImportFromJsonFile ConfigurationService --- .../ConfigurationServiceTests.cs | 33 ++++++++++++++++++- .../Application/ConfigurationService.cs | 21 ++++++++++++ .../Application/IConfigurationService.cs | 1 + 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs index 41e9390..8e97b13 100644 --- a/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs +++ b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs @@ -173,7 +173,38 @@ public async Task Case010_GetAll() } [TestMethod] - public async Task Case011_Cleanup() + public async Task Case011_ImportFromJsonFileAsync() + { + Assert.IsNotNull(_configurationService); + var path = Path.GetTempFileName(); + await File.WriteAllTextAsync(path, """ + { + "importBool": false, + "importInt": 123, + "importString": "hello", + "importObject": { + "Test": "fromImport" + } + } + """); + try + { + var imported = await _configurationService.ImportFromJsonFileAsync(path); + Assert.AreEqual(4, imported); + Assert.AreEqual(false, await _configurationService.GetBoolAsync("importBool", true)); + Assert.AreEqual(123, await _configurationService.GetIntAsync("importInt", 0)); + Assert.AreEqual("hello", await _configurationService.GetStringAsync("importString", "")); + var importedObject = await _configurationService.GetObjectAsync("importObject", new TestObj("default"), TestJsonContext.Default.TestObj); + Assert.AreEqual("fromImport", importedObject.Test); + } + finally + { + File.Delete(path); + } + } + + [TestMethod] + public async Task Case012_Cleanup() { var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test.Config", "app.db"); Assert.IsNotNull(_databaseService); diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index 51e84c9..ea66e46 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.IO; using System.Text.Json; using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; @@ -331,6 +332,26 @@ public async Task GetStringAsync(string name, string defaultValue = "") return (string)_cache[name]; } + public async Task ImportFromJsonFileAsync(string path) + { + if (!File.Exists(path)) + { + return -1; + } + _logger.LogInformation($"Importing configuration properties from JSON file ({path})..."); + using var json = JsonDocument.Parse(await File.ReadAllTextAsync(path)); + var imported = 0; + foreach (var property in json.RootElement.EnumerateObject()) + { + await SetAsync(property.Name, property.Value.GetRawText().Trim('"')); + _logger.LogInformation($"Found and imported configuration property ({property.Name}) in JSON file ({path})."); + imported++; + } + await SaveAsync(); + _logger.LogInformation($"Imported {imported} configuration properties from JSON file ({path})."); + return imported; + } + public void Save() { _transaction?.Commit(); diff --git a/Nickvision.Desktop/Application/IConfigurationService.cs b/Nickvision.Desktop/Application/IConfigurationService.cs index 217e612..55fd8ed 100644 --- a/Nickvision.Desktop/Application/IConfigurationService.cs +++ b/Nickvision.Desktop/Application/IConfigurationService.cs @@ -21,6 +21,7 @@ public interface IConfigurationService Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull; string GetString(string name, string defaultValue = ""); Task GetStringAsync(string name, string defaultValue = ""); + Task ImportFromJsonFileAsync(string path); void Save(); Task SaveAsync(); void Set(string name, bool value); From 1a2efe1352c12ba69abcb59337ab6c4bd640eed2 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 19:30:45 -0400 Subject: [PATCH 10/16] feat: Add database tests --- .../DatabaseServiceTests.cs | 146 +++++++++++++++++- Nickvision.Desktop/Keyring/KeyringService.cs | 4 +- 2 files changed, 146 insertions(+), 4 deletions(-) diff --git a/Nickvision.Desktop.Tests/DatabaseServiceTests.cs b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs index b031460..dc54b49 100644 --- a/Nickvision.Desktop.Tests/DatabaseServiceTests.cs +++ b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs @@ -1,6 +1,10 @@ -using Nickvision.Desktop.Application; +using Nickvision.Desktop.Application; +using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.System; using Nickvision.Desktop.Tests.Mocks; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; namespace Nickvision.Desktop.Tests; @@ -12,7 +16,145 @@ public class DatabaseServiceTests [TestMethod] public void Case001_Init() { - _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test", "Nickvision.Desktop.Test", "Test"), new SecretService(new MockLogger())); + _databaseService = new DatabaseService(new MockLogger(), new AppInfo("org.nickvision.desktop.test.database", "Nickvision.Desktop.Test.Database", "Test Database"), new SecretService(new MockLogger())); Assert.IsNotNull(_databaseService); } + + [TestMethod] + public void Case002_EnsureTableAndTransaction() + { + Assert.IsNotNull(_databaseService); + Assert.IsTrue(_databaseService.EnsureTableExists("test_table", "id TEXT PRIMARY KEY, name TEXT, age INTEGER")); + using var transaction = _databaseService.CreateTransation(); + Assert.IsNotNull(transaction); + transaction.Commit(); + } + + [TestMethod] + public void Case003_InsertContainsAndSelect() + { + Assert.IsNotNull(_databaseService); + Assert.IsTrue(_databaseService.InsertIntoTable("test_table", new Dictionary() + { + { "id", "row1" }, + { "name", "Alice" }, + { "age", 30 } + })); + Assert.IsTrue(_databaseService.ContainsInTable("test_table", "id", "row1")); + Assert.IsFalse(_databaseService.ContainsInTable("test_table", "id", "row_does_not_exist")); + using var command = _databaseService.SelectFromTable("test_table", "id", "row1"); + using var reader = command.ExecuteReader(); + Assert.IsTrue(reader.Read()); + Assert.AreEqual("row1", reader.GetString(0)); + Assert.AreEqual("Alice", reader.GetString(1)); + Assert.AreEqual("30", reader.GetString(2)); + Assert.IsFalse(reader.Read()); + } + + [TestMethod] + public void Case004_UpdateReplaceDeleteAndDrop() + { + Assert.IsNotNull(_databaseService); + Assert.IsTrue(_databaseService.UpdateInTable("test_table", "id", "row1", new Dictionary() + { + { "name", "Alice Updated" }, + { "age", 31 } + })); + using (var command = _databaseService.SelectFromTable("test_table", "id", "row1")) + using (var reader = command.ExecuteReader()) + { + Assert.IsTrue(reader.Read()); + Assert.AreEqual("Alice Updated", reader.GetString(1)); + Assert.AreEqual("31", reader.GetString(2)); + } + Assert.IsTrue(_databaseService.ReplaceIntoTable("test_table", new Dictionary() + { + { "id", "row1" }, + { "name", "Alice Replaced" }, + { "age", 40 } + })); + using (var command = _databaseService.SelectFromTable("test_table", "id", "row1")) + using (var reader = command.ExecuteReader()) + { + Assert.IsTrue(reader.Read()); + Assert.AreEqual("Alice Replaced", reader.GetString(1)); + Assert.AreEqual("40", reader.GetString(2)); + } + Assert.IsTrue(_databaseService.DeleteFromTable("test_table", "id", "row1")); + Assert.IsFalse(_databaseService.DeleteFromTable("test_table", "id", "row1")); + Assert.IsFalse(_databaseService.ContainsInTable("test_table", "id", "row1")); + Assert.IsTrue(_databaseService.DropTable("test_table")); + } + + [TestMethod] + public async Task Case005_AsyncCrud() + { + Assert.IsNotNull(_databaseService); + const string asyncTable = "test_table_async"; + Assert.IsTrue(await _databaseService.EnsureTableExistsAsync(asyncTable, "id TEXT PRIMARY KEY, name TEXT, age INTEGER")); + await using (var transaction = await _databaseService.CreateTransationAsync()) + { + Assert.IsNotNull(transaction); + await transaction.CommitAsync(); + } + Assert.IsTrue(await _databaseService.InsertIntoTableAsync(asyncTable, new Dictionary() + { + { "id", "rowA" }, + { "name", "Bob" }, + { "age", 20 } + })); + Assert.IsTrue(await _databaseService.ContainsInTableAsync(asyncTable, "id", "rowA")); + Assert.IsFalse(await _databaseService.ContainsInTableAsync(asyncTable, "id", "row_missing")); + await using (var command = await _databaseService.SelectAllFromTableAsync(asyncTable)) + await using (var reader = await command.ExecuteReaderAsync()) + { + Assert.IsTrue(await reader.ReadAsync()); + Assert.AreEqual("rowA", reader.GetString(0)); + Assert.AreEqual("Bob", reader.GetString(1)); + Assert.AreEqual("20", reader.GetString(2)); + Assert.IsFalse(await reader.ReadAsync()); + } + Assert.IsTrue(await _databaseService.UpdateInTableAsync(asyncTable, "id", "rowA", new Dictionary() + { + { "name", "Bob Updated" }, + { "age", 21 } + })); + await using (var command = await _databaseService.SelectFromTableAsync(asyncTable, "id", "rowA")) + await using (var reader = await command.ExecuteReaderAsync()) + { + Assert.IsTrue(await reader.ReadAsync()); + Assert.AreEqual("Bob Updated", reader.GetString(1)); + Assert.AreEqual("21", reader.GetString(2)); + Assert.IsFalse(await reader.ReadAsync()); + } + Assert.IsTrue(await _databaseService.ReplaceIntoTableAsync(asyncTable, new Dictionary() + { + { "id", "rowA" }, + { "name", "Bob Replaced" }, + { "age", 22 } + })); + await using (var command = await _databaseService.SelectFromTableAsync(asyncTable, "id", "rowA")) + await using (var reader = await command.ExecuteReaderAsync()) + { + Assert.IsTrue(await reader.ReadAsync()); + Assert.AreEqual("Bob Replaced", reader.GetString(1)); + Assert.AreEqual("22", reader.GetString(2)); + Assert.IsFalse(await reader.ReadAsync()); + } + Assert.IsTrue(await _databaseService.DeleteFromTableAsync(asyncTable, "id", "rowA")); + Assert.IsFalse(await _databaseService.DeleteFromTableAsync(asyncTable, "id", "rowA")); + Assert.IsTrue(await _databaseService.DropTableAsync(asyncTable)); + } + + [TestMethod] + public async Task Case006_Cleanup() + { + var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test.Database", "app.db"); + Assert.IsNotNull(_databaseService); + await _databaseService.DisposeAsync(); + File.Delete(path); + Directory.Delete(Path.GetDirectoryName(path)!); + Assert.IsFalse(File.Exists(path)); + _databaseService = null; + } } diff --git a/Nickvision.Desktop/Keyring/KeyringService.cs b/Nickvision.Desktop/Keyring/KeyringService.cs index 7c226ca..d212b3a 100644 --- a/Nickvision.Desktop/Keyring/KeyringService.cs +++ b/Nickvision.Desktop/Keyring/KeyringService.cs @@ -130,11 +130,11 @@ private async Task EnsureTableAsync() _credentials.Add(new Credential(readerAll.GetString(0), readerAll.GetString(2), readerAll.GetString(3), new Uri(readerAll.GetString(1)))); } var ring2Path = Path.Combine(UserDirectories.Config, "Nickvision", "Keyring", $"{_appInfo.Id}.ring2"); - if(File.Exists(ring2Path)) + if (File.Exists(ring2Path)) { _logger.LogInformation($"Found old keyring ring2 file ({ring2Path}), migrating credentials..."); var secret = await _secretService.GetAsync(_appInfo.Id); - if(secret is null) + if (secret is null) { _logger.LogError($"Unable to migrate old keyring ring2 file ({ring2Path}) as no secret were found for the app."); File.Delete(ring2Path); From ddf8d89af6ed703bb7e440dbdf60cc29bc7d1897 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 20:10:08 -0400 Subject: [PATCH 11/16] fix: Service implementations --- .../Application/ConfigurationService.cs | 12 ++++++++++-- Nickvision.Desktop/Keyring/KeyringService.cs | 3 ++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index ea66e46..68c9543 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -336,14 +336,22 @@ public async Task ImportFromJsonFileAsync(string path) { if (!File.Exists(path)) { - return -1; + _logger.LogError($"Failed to import configuration properties from JSON file ({path}) because it does not exist."); + return 0; } _logger.LogInformation($"Importing configuration properties from JSON file ({path})..."); using var json = JsonDocument.Parse(await File.ReadAllTextAsync(path)); var imported = 0; foreach (var property in json.RootElement.EnumerateObject()) { - await SetAsync(property.Name, property.Value.GetRawText().Trim('"')); + if (property.Value.ValueKind == JsonValueKind.String) + { + await SetAsync(property.Name, property.Value.GetString()!); + } + else + { + await SetAsync(property.Name, property.Value.GetRawText()); + } _logger.LogInformation($"Found and imported configuration property ({property.Name}) in JSON file ({path})."); imported++; } diff --git a/Nickvision.Desktop/Keyring/KeyringService.cs b/Nickvision.Desktop/Keyring/KeyringService.cs index d212b3a..271b9fa 100644 --- a/Nickvision.Desktop/Keyring/KeyringService.cs +++ b/Nickvision.Desktop/Keyring/KeyringService.cs @@ -46,7 +46,7 @@ public async Task AddCredentialAsync(Credential credential) _logger.LogError($"Unable to add keyring credential ({credential.Name}) as it already exists."); return false; } - var result = _databaseService.InsertIntoTable(TableName, new Dictionary() + var result = await _databaseService.InsertIntoTableAsync(TableName, new Dictionary() { { "name", credential.Name }, { "uri", credential.Url.ToString() }, @@ -90,6 +90,7 @@ public async Task> GetAllCredentialAsync() public async Task UpdateCredentialAsync(Credential credential) { + await EnsureTableAsync(); _logger.LogInformation($"Updating keyring credential ({credential.Name})..."); var index = _credentials.IndexOf(credential); if (index == -1) From fe3c1815bf721c0f411952aa923f6166812d134c Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 20:16:02 -0400 Subject: [PATCH 12/16] feat: DatabaseServiceTests --- .../DatabaseServiceTests.cs | 76 +++++++++- .../Application/DatabaseService.cs | 135 +++++++++++++----- .../Application/IDatabaseService.cs | 22 +-- 3 files changed, 190 insertions(+), 43 deletions(-) diff --git a/Nickvision.Desktop.Tests/DatabaseServiceTests.cs b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs index dc54b49..69df1f7 100644 --- a/Nickvision.Desktop.Tests/DatabaseServiceTests.cs +++ b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs @@ -25,6 +25,8 @@ public void Case002_EnsureTableAndTransaction() { Assert.IsNotNull(_databaseService); Assert.IsTrue(_databaseService.EnsureTableExists("test_table", "id TEXT PRIMARY KEY, name TEXT, age INTEGER")); + Assert.IsTrue(_databaseService.TableExists("test_table")); + Assert.IsFalse(_databaseService.TableExists("missing_table")); using var transaction = _databaseService.CreateTransation(); Assert.IsNotNull(transaction); transaction.Commit(); @@ -92,6 +94,8 @@ public async Task Case005_AsyncCrud() Assert.IsNotNull(_databaseService); const string asyncTable = "test_table_async"; Assert.IsTrue(await _databaseService.EnsureTableExistsAsync(asyncTable, "id TEXT PRIMARY KEY, name TEXT, age INTEGER")); + Assert.IsTrue(await _databaseService.TableExistsAsync(asyncTable)); + Assert.IsFalse(await _databaseService.TableExistsAsync("missing_table_async")); await using (var transaction = await _databaseService.CreateTransationAsync()) { Assert.IsNotNull(transaction); @@ -147,7 +151,77 @@ public async Task Case005_AsyncCrud() } [TestMethod] - public async Task Case006_Cleanup() + public void Case006_CountAndExecuteNonQuery() + { + Assert.IsNotNull(_databaseService); + Assert.IsTrue(_databaseService.EnsureTableExists("test_count", "id TEXT PRIMARY KEY, val TEXT")); + Assert.AreEqual(1, _databaseService.ExecuteNonQuery("INSERT INTO test_count (id, val) VALUES ($id, $val)", new Dictionary() + { + { "id", "c1" }, + { "val", "v1" } + })); + Assert.AreEqual(1, _databaseService.CountInTable("test_count")); + Assert.IsTrue(_databaseService.DropTable("test_count")); + } + + [TestMethod] + public void Case007_TypedContainsInTable() + { + Assert.IsNotNull(_databaseService); + Assert.IsTrue(_databaseService.EnsureTableExists("test_typed", "id TEXT PRIMARY KEY, age INTEGER, enabled INTEGER, name TEXT")); + Assert.IsTrue(_databaseService.InsertIntoTable("test_typed", new Dictionary() + { + { "id", "typed1" }, + { "age", 55 }, + { "enabled", true }, + { "name", "typed-name" } + })); + Assert.IsTrue(_databaseService.ContainsInTable("test_typed", "age", 55)); + Assert.IsFalse(_databaseService.ContainsInTable("test_typed", "age", 56)); + Assert.IsTrue(_databaseService.ContainsInTable("test_typed", "enabled", true)); + Assert.IsFalse(_databaseService.ContainsInTable("test_typed", "enabled", false)); + Assert.IsTrue(_databaseService.ContainsInTable("test_typed", "name", "typed-name")); + Assert.IsFalse(_databaseService.ContainsInTable("test_typed", "name", "typed-missing")); + Assert.IsTrue(_databaseService.DropTable("test_typed")); + } + + [TestMethod] + public async Task Case008_CountAndExecuteNonQueryAsync() + { + Assert.IsNotNull(_databaseService); + Assert.IsTrue(await _databaseService.EnsureTableExistsAsync("test_count_async", "id TEXT PRIMARY KEY, val TEXT")); + Assert.AreEqual(1, await _databaseService.ExecuteNonQueryAsync("INSERT INTO test_count_async (id, val) VALUES ($id, $val)", new Dictionary() + { + { "id", "ac1" }, + { "val", "av1" } + })); + Assert.AreEqual(1, await _databaseService.CountInTableAsync("test_count_async")); + Assert.IsTrue(await _databaseService.DropTableAsync("test_count_async")); + } + + [TestMethod] + public async Task Case009_TypedContainsInTableAsync() + { + Assert.IsNotNull(_databaseService); + Assert.IsTrue(await _databaseService.EnsureTableExistsAsync("test_typed_async", "id TEXT PRIMARY KEY, age INTEGER, enabled INTEGER, name TEXT")); + Assert.IsTrue(await _databaseService.InsertIntoTableAsync("test_typed_async", new Dictionary() + { + { "id", "typedA" }, + { "age", 77 }, + { "enabled", false }, + { "name", "typed-async-name" } + })); + Assert.IsTrue(await _databaseService.ContainsInTableAsync("test_typed_async", "age", 77)); + Assert.IsFalse(await _databaseService.ContainsInTableAsync("test_typed_async", "age", 78)); + Assert.IsTrue(await _databaseService.ContainsInTableAsync("test_typed_async", "enabled", false)); + Assert.IsFalse(await _databaseService.ContainsInTableAsync("test_typed_async", "enabled", true)); + Assert.IsTrue(await _databaseService.ContainsInTableAsync("test_typed_async", "name", "typed-async-name")); + Assert.IsFalse(await _databaseService.ContainsInTableAsync("test_typed_async", "name", "typed-async-missing")); + Assert.IsTrue(await _databaseService.DropTableAsync("test_typed_async")); + } + + [TestMethod] + public async Task Case010_Cleanup() { var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test.Database", "app.db"); Assert.IsNotNull(_databaseService); diff --git a/Nickvision.Desktop/Application/DatabaseService.cs b/Nickvision.Desktop/Application/DatabaseService.cs index 240bcb3..815ba05 100644 --- a/Nickvision.Desktop/Application/DatabaseService.cs +++ b/Nickvision.Desktop/Application/DatabaseService.cs @@ -32,58 +32,62 @@ public DatabaseService(ILogger logger, AppInfo appInfo, ISecret Dispose(false); } - public bool ContainsInTable(string tableName, string columnName, string matchingValue) + public int CountInTable(string tableName) { EnsureDatabase(); - _logger.LogInformation($"Checking if {tableName} contains value in column ({columnName})..."); + _logger.LogInformation($"Counting rows in table {tableName}..."); + using var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT COUNT(*) FROM {tableName}"; + var count = Convert.ToInt32(command.ExecuteScalar()); + _logger.LogInformation($"Counted ({count}) rows in table {tableName}."); + return count; + } + + public async Task CountInTableAsync(string tableName) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Counting rows in table {tableName}..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = $"SELECT COUNT(*) FROM {tableName}"; + var count = Convert.ToInt32(await command.ExecuteScalarAsync()); + _logger.LogInformation($"Counted ({count}) rows in table {tableName}."); + return count; + } + + public bool ContainsInTable(string tableName, string columnName, T matchingValue) + { + EnsureDatabase(); + _logger.LogInformation($"Checking if {tableName} contains typed value ({typeof(T).Name}) in column ({columnName})..."); using var command = _connection!.CreateCommand(); command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $matchingValueParam"; command.Parameters.AddWithValue("$matchingValueParam", matchingValue); - using var reader = command.ExecuteReader(); - var result = false; - while (reader.Read()) - { - if (reader.GetInt32(0) >= 1) - { - result = true; - break; - } - } + var result = Convert.ToInt32(command.ExecuteScalar()) >= 1; if (result) { - _logger.LogInformation($"Found matching column ({columnName}) value in {tableName}."); + _logger.LogInformation($"Found matching typed column ({columnName}) value in {tableName}."); } else { - _logger.LogInformation($"Failed to find matching column ({columnName}) value in {tableName}."); + _logger.LogInformation($"Failed to find matching typed column ({columnName}) value in {tableName}."); } return result; } - public async Task ContainsInTableAsync(string tableName, string columnName, string matchingValue) + public async Task ContainsInTableAsync(string tableName, string columnName, T matchingValue) { await EnsureDatabaseAsync(); - _logger.LogInformation($"Checking if {tableName} contains value in column ({columnName})..."); + _logger.LogInformation($"Checking if {tableName} contains typed value in column ({columnName})..."); await using var command = _connection!.CreateCommand(); command.CommandText = $"SELECT COUNT(*) FROM {tableName} WHERE {columnName} = $matchingValueParam"; command.Parameters.AddWithValue("$matchingValueParam", matchingValue); - await using var reader = await command.ExecuteReaderAsync(); - var result = false; - while (await reader.ReadAsync()) - { - if (reader.GetInt32(0) >= 1) - { - result = true; - break; - } - } + var result = Convert.ToInt32(await command.ExecuteScalarAsync()) >= 1; if (result) { - _logger.LogInformation($"Found matching column ({columnName}) value in {tableName}."); + _logger.LogInformation($"Found matching typed column ({columnName}) value in {tableName}."); } else { - _logger.LogInformation($"Failed to find matching column ({columnName}) value in {tableName}."); + _logger.LogInformation($"Failed to find matching typed column ({columnName}) value in {tableName}."); } return result; } @@ -102,7 +106,7 @@ public async Task CreateTransationAsync() return _connection!.BeginTransaction(); } - public bool DeleteFromTable(string tableName, string columnName, string matchingValue) + public bool DeleteFromTable(string tableName, string columnName, T matchingValue) { EnsureDatabase(); _logger.LogInformation($"Deleting row from {tableName}..."); @@ -122,7 +126,7 @@ public bool DeleteFromTable(string tableName, string columnName, string matching return result; } - public async Task DeleteFromTableAsync(string tableName, string columnName, string matchingValue) + public async Task DeleteFromTableAsync(string tableName, string columnName, T matchingValue) { await EnsureDatabaseAsync(); _logger.LogInformation($"Deleting row from {tableName}..."); @@ -199,6 +203,42 @@ public async Task EnsureTableExistsAsync(string tableName, string layout) return true; } + public int ExecuteNonQuery(string sql, Dictionary? parameters = null) + { + EnsureDatabase(); + _logger.LogInformation("Executing SQL non-query command..."); + using var command = _connection!.CreateCommand(); + command.CommandText = sql; + if (parameters is not null) + { + foreach (var pair in parameters) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + } + var affected = command.ExecuteNonQuery(); + _logger.LogInformation($"Executed SQL non-query command successfully. Affected rows: {affected}"); + return affected; + } + + public async Task ExecuteNonQueryAsync(string sql, Dictionary? parameters = null) + { + await EnsureDatabaseAsync(); + _logger.LogInformation("Executing SQL non-query command..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = sql; + if (parameters is not null) + { + foreach (var pair in parameters) + { + command.Parameters.AddWithValue($"${pair.Key}", pair.Value); + } + } + var affected = await command.ExecuteNonQueryAsync(); + _logger.LogInformation($"Executed SQL non-query command successfully. Affected rows: {affected}"); + return affected; + } + public bool InsertIntoTable(string tableName, Dictionary data) { EnsureDatabase(); @@ -287,7 +327,7 @@ public async Task ReplaceIntoTableAsync(string tableName, Dictionary(string tableName, string columnName, T matchingValue) { EnsureDatabase(); _logger.LogInformation($"Selecting data from table {tableName} with matching column ({columnName})..."); @@ -298,7 +338,7 @@ public SqliteCommand SelectFromTable(string tableName, string columnName, string return command; } - public async Task SelectFromTableAsync(string tableName, string columnName, string matchingValue) + public async Task SelectFromTableAsync(string tableName, string columnName, T matchingValue) { await EnsureDatabaseAsync(); _logger.LogInformation($"Selecting data from table {tableName} with matching column ({columnName})..."); @@ -329,7 +369,32 @@ public async Task SelectAllFromTableAsync(string tableName) return command; } - public bool UpdateInTable(string tableName, string columnName, string matchingValue, Dictionary newData) + public bool TableExists(string tableName) + { + EnsureDatabase(); + _logger.LogInformation($"Checking if table ({tableName}) exists..."); + using var command = _connection!.CreateCommand(); + command.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = $tableNameParam"; + command.Parameters.AddWithValue("$tableNameParam", tableName); + var exists = Convert.ToInt32(command.ExecuteScalar()) >= 1; + _logger.LogInformation(exists ? $"Table ({tableName}) exists." : $"Table ({tableName}) does not exist."); + return exists; + } + + public async Task TableExistsAsync(string tableName) + { + await EnsureDatabaseAsync(); + _logger.LogInformation($"Checking if table ({tableName}) exists..."); + await using var command = _connection!.CreateCommand(); + command.CommandText = "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = $tableNameParam"; + command.Parameters.AddWithValue("$tableNameParam", tableName); + var result = await command.ExecuteScalarAsync(); + var exists = Convert.ToInt32(result) >= 1; + _logger.LogInformation(exists ? $"Table ({tableName}) exists." : $"Table ({tableName}) does not exist."); + return exists; + } + + public bool UpdateInTable(string tableName, string columnName, T matchingValue, Dictionary newData) { EnsureDatabase(); _logger.LogInformation($"Updating data in {tableName}..."); @@ -352,7 +417,7 @@ public bool UpdateInTable(string tableName, string columnName, string matchingVa return result; } - public async Task UpdateInTableAsync(string tableName, string columnName, string matchingValue, Dictionary newData) + public async Task UpdateInTableAsync(string tableName, string columnName, T matchingValue, Dictionary newData) { await EnsureDatabaseAsync(); _logger.LogInformation($"Updating data in {tableName}..."); @@ -412,6 +477,7 @@ private void EnsureDatabase() _logger.LogError($"Failed to open application database: {e}"); _connection.Dispose(); _connection = null; + throw; } } @@ -452,6 +518,7 @@ private async Task EnsureDatabaseAsync() _logger.LogError($"Failed to open application database: {e}"); await _connection.DisposeAsync(); _connection = null; + throw; } } diff --git a/Nickvision.Desktop/Application/IDatabaseService.cs b/Nickvision.Desktop/Application/IDatabaseService.cs index 5de02ad..7a65aef 100644 --- a/Nickvision.Desktop/Application/IDatabaseService.cs +++ b/Nickvision.Desktop/Application/IDatabaseService.cs @@ -9,24 +9,30 @@ public interface IDatabaseService { event EventHandler? PasswordRequired; - bool ContainsInTable(string tableName, string columnName, string matchingValue); - Task ContainsInTableAsync(string tableName, string columnName, string matchingValue); + int CountInTable(string tableName); + Task CountInTableAsync(string tableName); + bool ContainsInTable(string tableName, string columnName, T matchingValue); + Task ContainsInTableAsync(string tableName, string columnName, T matchingValue); SqliteTransaction CreateTransation(); Task CreateTransationAsync(); - bool DeleteFromTable(string tableName, string columnName, string matchingValue); - Task DeleteFromTableAsync(string tableName, string columnName, string matchingValue); + bool DeleteFromTable(string tableName, string columnName, T matchingValue); + Task DeleteFromTableAsync(string tableName, string columnName, T matchingValue); bool DropTable(string tableName); Task DropTableAsync(string tableName); bool EnsureTableExists(string tableName, string layout); Task EnsureTableExistsAsync(string tableName, string layout); + int ExecuteNonQuery(string sql, Dictionary? parameters = null); + Task ExecuteNonQueryAsync(string sql, Dictionary? parameters = null); bool InsertIntoTable(string tableName, Dictionary data); Task InsertIntoTableAsync(string tableName, Dictionary data); bool ReplaceIntoTable(string tableName, Dictionary data); Task ReplaceIntoTableAsync(string tableName, Dictionary data); - SqliteCommand SelectFromTable(string tableName, string columnName, string matchingValue); - Task SelectFromTableAsync(string tableName, string columnName, string matchingValue); + SqliteCommand SelectFromTable(string tableName, string columnName, T matchingValue); + Task SelectFromTableAsync(string tableName, string columnName, T matchingValue); SqliteCommand SelectAllFromTable(string tableName); Task SelectAllFromTableAsync(string tableName); - bool UpdateInTable(string tableName, string columnName, string matchingValue, Dictionary newData); - Task UpdateInTableAsync(string tableName, string columnName, string matchingValue, Dictionary newData); + bool TableExists(string tableName); + Task TableExistsAsync(string tableName); + bool UpdateInTable(string tableName, string columnName, T matchingValue, Dictionary newData); + Task UpdateInTableAsync(string tableName, string columnName, T matchingValue, Dictionary newData); } From 188ff8f370a4df7c33540e9c5838c39e0d5fca7f Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 20:32:57 -0400 Subject: [PATCH 13/16] feat: Better configuration service --- .../Nickvision.Desktop.GNOME.csproj | 2 +- .../ConfigurationServiceTests.cs | 73 ++-- .../Nickvision.Desktop.WinUI.csproj | 2 +- .../Application/ConfigurationService.cs | 388 ++++++------------ .../Application/IConfigurationService.cs | 30 +- Nickvision.Desktop/Nickvision.Desktop.csproj | 2 +- 6 files changed, 175 insertions(+), 322 deletions(-) diff --git a/Nickvision.Desktop.GNOME/Nickvision.Desktop.GNOME.csproj b/Nickvision.Desktop.GNOME/Nickvision.Desktop.GNOME.csproj index ffc6f72..d41998e 100644 --- a/Nickvision.Desktop.GNOME/Nickvision.Desktop.GNOME.csproj +++ b/Nickvision.Desktop.GNOME/Nickvision.Desktop.GNOME.csproj @@ -25,7 +25,7 @@ - + diff --git a/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs index 8e97b13..10f5573 100644 --- a/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs +++ b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs @@ -12,6 +12,11 @@ public class TestObj { public string Test { get; set; } + public TestObj() + { + Test = ""; + } + public TestObj(string test) { Test = test; @@ -41,25 +46,25 @@ public void Case001_Init() public async Task Case002_Get() { Assert.IsNotNull(_configurationService); - var val1 = _configurationService.GetBool("nonExistentBool", false); + var val1 = _configurationService.Get("nonExistentBool", false); Assert.AreEqual(false, val1); - var val2 = await _configurationService.GetBoolAsync("nonExistentBoolAsync", true); + var val2 = await _configurationService.GetAsync("nonExistentBoolAsync", true); Assert.AreEqual(true, val2); - var val3 = _configurationService.GetDouble("nonExistentDouble", 1.5); + var val3 = _configurationService.Get("nonExistentDouble", 1.5); Assert.AreEqual(1.5, val3); - var val4 = await _configurationService.GetDoubleAsync("nonExistentDoubleAsync", 2.5); + var val4 = await _configurationService.GetAsync("nonExistentDoubleAsync", 2.5); Assert.AreEqual(2.5, val4); - var val5 = _configurationService.GetInt("nonExistentInt", 42); + var val5 = _configurationService.Get("nonExistentInt", 42); Assert.AreEqual(42, val5); - var val6 = await _configurationService.GetIntAsync("nonExistentIntAsync", 84); + var val6 = await _configurationService.GetAsync("nonExistentIntAsync", 84); Assert.AreEqual(84, val6); - var val7 = _configurationService.GetString("nonExistentString", "default"); + var val7 = _configurationService.Get("nonExistentString", "default"); Assert.AreEqual("default", val7); - var val8 = await _configurationService.GetStringAsync("nonExistentStringAsync", "asyncDefault"); + var val8 = await _configurationService.GetAsync("nonExistentStringAsync", "asyncDefault"); Assert.AreEqual("asyncDefault", val8); - var val9 = _configurationService.GetObject("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); + var val9 = _configurationService.Get("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); Assert.AreEqual("value", val9.Test); - var val10 = await _configurationService.GetObjectAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); + var val10 = await _configurationService.GetAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); Assert.AreEqual("asyncValue", val10.Test); } @@ -97,25 +102,25 @@ public async Task Case005_Save() public async Task Case006_Get() { Assert.IsNotNull(_configurationService); - var val1 = _configurationService.GetBool("nonExistentBool", false); + var val1 = _configurationService.Get("nonExistentBool", false); Assert.AreEqual(true, val1); - var val2 = await _configurationService.GetBoolAsync("nonExistentBoolAsync", true); + var val2 = await _configurationService.GetAsync("nonExistentBoolAsync", true); Assert.AreEqual(false, val2); - var val3 = _configurationService.GetDouble("nonExistentDouble", 1.5); + var val3 = _configurationService.Get("nonExistentDouble", 1.5); Assert.AreEqual(2.5, val3); - var val4 = await _configurationService.GetDoubleAsync("nonExistentDoubleAsync", 2.5); + var val4 = await _configurationService.GetAsync("nonExistentDoubleAsync", 2.5); Assert.AreEqual(1.5, val4); - var val5 = _configurationService.GetInt("nonExistentInt", 42); + var val5 = _configurationService.Get("nonExistentInt", 42); Assert.AreEqual(84, val5); - var val6 = await _configurationService.GetIntAsync("nonExistentIntAsync", 84); + var val6 = await _configurationService.GetAsync("nonExistentIntAsync", 84); Assert.AreEqual(42, val6); - var val7 = _configurationService.GetString("nonExistentString", "default"); + var val7 = _configurationService.Get("nonExistentString", "default"); Assert.AreEqual("default2", val7); - var val8 = await _configurationService.GetStringAsync("nonExistentStringAsync", "asyncDefault"); + var val8 = await _configurationService.GetAsync("nonExistentStringAsync", "asyncDefault"); Assert.AreEqual("asyncDefault2", val8); - var val9 = _configurationService.GetObject("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); + var val9 = _configurationService.Get("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); Assert.AreEqual("value2", val9.Test); - var val10 = await _configurationService.GetObjectAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); + var val10 = await _configurationService.GetAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); Assert.AreEqual("asyncValue2", val10.Test); } @@ -143,25 +148,25 @@ public void Case008_Init() public async Task Case009_Get() { Assert.IsNotNull(_configurationService); - var val1 = _configurationService.GetBool("nonExistentBool", false); + var val1 = _configurationService.Get("nonExistentBool", false); Assert.AreEqual(true, val1); - var val2 = await _configurationService.GetBoolAsync("nonExistentBoolAsync", true); + var val2 = await _configurationService.GetAsync("nonExistentBoolAsync", true); Assert.AreEqual(false, val2); - var val3 = _configurationService.GetDouble("nonExistentDouble", 1.5); + var val3 = _configurationService.Get("nonExistentDouble", 1.5); Assert.AreEqual(2.5, val3); - var val4 = await _configurationService.GetDoubleAsync("nonExistentDoubleAsync", 2.5); + var val4 = await _configurationService.GetAsync("nonExistentDoubleAsync", 2.5); Assert.AreEqual(1.5, val4); - var val5 = _configurationService.GetInt("nonExistentInt", 42); + var val5 = _configurationService.Get("nonExistentInt", 42); Assert.AreEqual(84, val5); - var val6 = await _configurationService.GetIntAsync("nonExistentIntAsync", 84); + var val6 = await _configurationService.GetAsync("nonExistentIntAsync", 84); Assert.AreEqual(42, val6); - var val7 = _configurationService.GetString("nonExistentString", "default"); + var val7 = _configurationService.Get("nonExistentString", "default"); Assert.AreEqual("default2", val7); - var val8 = await _configurationService.GetStringAsync("nonExistentStringAsync", "asyncDefault"); + var val8 = await _configurationService.GetAsync("nonExistentStringAsync", "asyncDefault"); Assert.AreEqual("asyncDefault2", val8); - var val9 = _configurationService.GetObject("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); + var val9 = _configurationService.Get("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj); Assert.AreEqual("value2", val9.Test); - var val10 = await _configurationService.GetObjectAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); + var val10 = await _configurationService.GetAsync("nonExistentObjectAsync", new TestObj("asyncValue"), TestJsonContext.Default.TestObj); Assert.AreEqual("asyncValue2", val10.Test); } @@ -191,10 +196,10 @@ await File.WriteAllTextAsync(path, """ { var imported = await _configurationService.ImportFromJsonFileAsync(path); Assert.AreEqual(4, imported); - Assert.AreEqual(false, await _configurationService.GetBoolAsync("importBool", true)); - Assert.AreEqual(123, await _configurationService.GetIntAsync("importInt", 0)); - Assert.AreEqual("hello", await _configurationService.GetStringAsync("importString", "")); - var importedObject = await _configurationService.GetObjectAsync("importObject", new TestObj("default"), TestJsonContext.Default.TestObj); + Assert.AreEqual(false, await _configurationService.GetAsync("importBool", true)); + Assert.AreEqual(123, await _configurationService.GetAsync("importInt", 0)); + Assert.AreEqual("hello", await _configurationService.GetAsync("importString", "")); + var importedObject = await _configurationService.GetAsync("importObject", new TestObj("default"), TestJsonContext.Default.TestObj); Assert.AreEqual("fromImport", importedObject.Test); } finally diff --git a/Nickvision.Desktop.WinUI/Nickvision.Desktop.WinUI.csproj b/Nickvision.Desktop.WinUI/Nickvision.Desktop.WinUI.csproj index a3193f9..a1ea9b8 100644 --- a/Nickvision.Desktop.WinUI/Nickvision.Desktop.WinUI.csproj +++ b/Nickvision.Desktop.WinUI/Nickvision.Desktop.WinUI.csproj @@ -33,7 +33,7 @@ - + diff --git a/Nickvision.Desktop/Application/ConfigurationService.cs b/Nickvision.Desktop/Application/ConfigurationService.cs index 68c9543..c4bee2e 100644 --- a/Nickvision.Desktop/Application/ConfigurationService.cs +++ b/Nickvision.Desktop/Application/ConfigurationService.cs @@ -1,4 +1,4 @@ -using Microsoft.Data.Sqlite; +using Microsoft.Data.Sqlite; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -92,106 +92,10 @@ public async Task> GetAllRawAsync() return dict; } - public bool GetBool(string name, bool defaultValue = false) + public T Get(string name, T defaultValue) where T : notnull { - _logger.LogInformation($"Getting boolean configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is bool t) - { - _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); - return t; - } - _cache[name] = defaultValue; - EnsureTable(); - using var command = _databaseService.SelectFromTable(TableName, "name", name); - using var reader = command.ExecuteReader(); - while (reader.Read()) - { - try - { - _cache[name] = bool.Parse(reader.GetString(1)); - } - catch { } - } - _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (bool)_cache[name]; - } - - public async Task GetBoolAsync(string name, bool defaultValue = false) - { - _logger.LogInformation($"Getting boolean configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is bool t) - { - _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); - return t; - } - _cache[name] = defaultValue; - await EnsureTableAsync(); - await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); - await using var reader = await command.ExecuteReaderAsync(); - while (await reader.ReadAsync()) - { - try - { - _cache[name] = bool.Parse(reader.GetString(1)); - } - catch { } - } - _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (bool)_cache[name]; - } - - public double GetDouble(string name, double defaultValue = 0.0) - { - _logger.LogInformation($"Getting double configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is double t) - { - _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); - return t; - } - _cache[name] = defaultValue; - EnsureTable(); - using var command = _databaseService.SelectFromTable(TableName, "name", name); - using var reader = command.ExecuteReader(); - while (reader.Read()) - { - try - { - _cache[name] = double.Parse(reader.GetString(1)); - } - catch { } - } - _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (double)_cache[name]; - } - - public async Task GetDoubleAsync(string name, double defaultValue = 0.0) - { - _logger.LogInformation($"Getting double configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is double t) - { - _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); - return t; - } - _cache[name] = defaultValue; - await EnsureTableAsync(); - await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); - await using var reader = await command.ExecuteReaderAsync(); - while (await reader.ReadAsync()) - { - try - { - _cache[name] = double.Parse(reader.GetString(1)); - } - catch { } - } - _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (double)_cache[name]; - } - - public int GetInt(string name, int defaultValue = 0) - { - _logger.LogInformation($"Getting integer configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is int t) + _logger.LogInformation($"Getting configuration property ({name})..."); + if (_cache.TryGetValue(name, out var value) && value is T t) { _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; @@ -204,39 +108,22 @@ public int GetInt(string name, int defaultValue = 0) { try { - _cache[name] = int.Parse(reader.GetString(1)); + _cache[name] = typeof(T) switch + { + var type when type == typeof(bool) => bool.Parse(reader.GetString(1)), + var type when type == typeof(double) => double.Parse(reader.GetString(1)), + var type when type == typeof(int) => int.Parse(reader.GetString(1)), + var type when type == typeof(string) => reader.GetString(1), + _ => throw new NotSupportedException($"Generic Get<{typeof(T).Name}> is only supported for bool, double, int, and string. Use Get(..., JsonTypeInfo) for object values.") + }; } catch { } } _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (int)_cache[name]; - } - - public async Task GetIntAsync(string name, int defaultValue = 0) - { - _logger.LogInformation($"Getting integer configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is int t) - { - _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); - return t; - } - _cache[name] = defaultValue; - await EnsureTableAsync(); - await using var command = await _databaseService.SelectFromTableAsync(TableName, "name", name); - await using var reader = await command.ExecuteReaderAsync(); - while (await reader.ReadAsync()) - { - try - { - _cache[name] = int.Parse(reader.GetString(1)); - } - catch { } - } - _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (int)_cache[name]; + return (T)_cache[name]; } - public T GetObject(string name, T defaultValue, JsonTypeInfo info) where T : notnull + public T Get(string name, T defaultValue, JsonTypeInfo info) where T : notnull, new() { _logger.LogInformation($"Getting object configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is T t) @@ -260,9 +147,9 @@ public T GetObject(string name, T defaultValue, JsonTypeInfo info) where T return (T)_cache[name]; } - public async Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull + public async Task GetAsync(string name, T defaultValue) where T : notnull { - _logger.LogInformation($"Getting object configuration property ({name})..."); + _logger.LogInformation($"Getting configuration property ({name})..."); if (_cache.TryGetValue(name, out var value) && value is T t) { _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); @@ -276,7 +163,14 @@ public async Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo { try { - _cache[name] = JsonSerializer.Deserialize(reader.GetString(1), info)!; + _cache[name] = typeof(T) switch + { + var type when type == typeof(bool) => bool.Parse(reader.GetString(1)), + var type when type == typeof(double) => double.Parse(reader.GetString(1)), + var type when type == typeof(int) => int.Parse(reader.GetString(1)), + var type when type == typeof(string) => reader.GetString(1), + _ => throw new NotSupportedException($"Generic GetAsync<{typeof(T).Name}> is only supported for bool, double, int, and string. Use GetAsync(..., JsonTypeInfo) for object values.") + }; } catch { } } @@ -284,34 +178,10 @@ public async Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo return (T)_cache[name]; } - public string GetString(string name, string defaultValue = "") - { - _logger.LogInformation($"Getting string configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is string t) - { - _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); - return t; - } - _cache[name] = defaultValue; - EnsureTable(); - using var command = _databaseService.SelectFromTable(TableName, "name", name); - using var reader = command.ExecuteReader(); - while (reader.Read()) - { - try - { - _cache[name] = reader.GetString(1); - } - catch { } - } - _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (string)_cache[name]; - } - - public async Task GetStringAsync(string name, string defaultValue = "") + public async Task GetAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull, new() { - _logger.LogInformation($"Getting string configuration property ({name})..."); - if (_cache.TryGetValue(name, out var value) && value is string t) + _logger.LogInformation($"Getting object configuration property ({name})..."); + if (_cache.TryGetValue(name, out var value) && value is T t) { _logger.LogInformation($"Value ({value}) found for configuration property ({name}) in cache."); return t; @@ -324,12 +194,12 @@ public async Task GetStringAsync(string name, string defaultValue = "") { try { - _cache[name] = reader.GetString(1); + _cache[name] = JsonSerializer.Deserialize(reader.GetString(1), info)!; } catch { } } _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database."); - return (string)_cache[name]; + return (T)_cache[name]; } public async Task ImportFromJsonFileAsync(string path) @@ -377,63 +247,109 @@ public async Task SaveAsync() } } - public void Set(string name, bool value) + public void Set(string name, T value) where T : notnull { - _logger.LogInformation($"Setting boolean configuration property ({name}) to value ({value})..."); _cache[name] = value; EnsureTable(); - _databaseService.ReplaceIntoTable(TableName, new Dictionary() - { - { "name", name }, - { "value", value.ToString() } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); - } - - public void Set(string name, double value) - { - _logger.LogInformation($"Setting double configuration property ({name}) to value ({value})..."); - _cache[name] = value; - EnsureTable(); - _databaseService.ReplaceIntoTable(TableName, new Dictionary() - { - { "name", name }, - { "value", value.ToString() } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); - } - - public void Set(string name, int value) - { - _logger.LogInformation($"Setting integer configuration property ({name}) to value ({value})..."); - _cache[name] = value; - EnsureTable(); - _databaseService.ReplaceIntoTable(TableName, new Dictionary() - { - { "name", name }, - { "value", value.ToString() } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); + switch (value) + { + case bool b: + _logger.LogInformation($"Setting boolean configuration property ({name}) to value ({b})..."); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", b.ToString() } + }); + _logger.LogInformation($"Value ({b}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, b, b.GetType())); + return; + case double d: + _logger.LogInformation($"Setting double configuration property ({name}) to value ({d})..."); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", d.ToString() } + }); + _logger.LogInformation($"Value ({d}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, d, d.GetType())); + return; + case int i: + _logger.LogInformation($"Setting integer configuration property ({name}) to value ({i})..."); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", i.ToString() } + }); + _logger.LogInformation($"Value ({i}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, i, i.GetType())); + return; + case string s: + _logger.LogInformation($"Setting string configuration property ({name}) to value ({s})..."); + _databaseService.ReplaceIntoTable(TableName, new Dictionary() + { + { "name", name }, + { "value", s } + }); + _logger.LogInformation($"Value ({s}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, s, s.GetType())); + return; + default: + throw new NotSupportedException($"Generic Set<{typeof(T).Name}> is only supported for bool, double, int, and string. Use Set(..., JsonTypeInfo) for object values."); + } } - public void Set(string name, string value) + public async Task SetAsync(string name, T value) where T : notnull { - _logger.LogInformation($"Setting string configuration property ({name}) to value ({value})..."); _cache[name] = value; - EnsureTable(); - _databaseService.ReplaceIntoTable(TableName, new Dictionary() - { - { "name", name }, - { "value", value } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); + await EnsureTableAsync(); + switch (value) + { + case bool b: + _logger.LogInformation($"Setting boolean configuration property ({name}) to value ({b})..."); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", b.ToString() } + }); + _logger.LogInformation($"Value ({b}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, b, b.GetType())); + return; + case double d: + _logger.LogInformation($"Setting double configuration property ({name}) to value ({d})..."); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", d.ToString() } + }); + _logger.LogInformation($"Value ({d}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, d, d.GetType())); + return; + case int i: + _logger.LogInformation($"Setting integer configuration property ({name}) to value ({i})..."); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", i.ToString() } + }); + _logger.LogInformation($"Value ({i}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, i, i.GetType())); + return; + case string s: + _logger.LogInformation($"Setting string configuration property ({name}) to value ({s})..."); + await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() + { + { "name", name }, + { "value", s } + }); + _logger.LogInformation($"Value ({s}) set for configuration property ({name})."); + Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, s, s.GetType())); + return; + default: + throw new NotSupportedException($"Generic SetAsync<{typeof(T).Name}> is only supported for bool, double, int, and string. Use SetAsync(..., JsonTypeInfo) for object values."); + } } - public void Set(string name, T value, JsonTypeInfo info) where T : notnull + public void Set(string name, T value, JsonTypeInfo info) where T : notnull, new() { _logger.LogInformation($"Setting object configuration property ({name}) to value ({value})..."); _cache[name] = value; @@ -447,63 +363,7 @@ public void Set(string name, T value, JsonTypeInfo info) where T : notnull Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); } - public async Task SetAsync(string name, bool value) - { - _logger.LogInformation($"Setting boolean configuration property ({name}) to value ({value})..."); - _cache[name] = value; - await EnsureTableAsync(); - await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() - { - { "name", name }, - { "value", value.ToString() } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); - } - - public async Task SetAsync(string name, double value) - { - _logger.LogInformation($"Setting double configuration property ({name}) to value ({value})..."); - _cache[name] = value; - await EnsureTableAsync(); - await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() - { - { "name", name }, - { "value", value.ToString() } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); - } - - public async Task SetAsync(string name, int value) - { - _logger.LogInformation($"Setting integer configuration property ({name}) to value ({value})..."); - _cache[name] = value; - await EnsureTableAsync(); - await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() - { - { "name", name }, - { "value", value.ToString() } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); - } - - public async Task SetAsync(string name, string value) - { - _logger.LogInformation($"Setting string configuration property ({name}) to value ({value})..."); - _cache[name] = value; - await EnsureTableAsync(); - await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary() - { - { "name", name }, - { "value", value } - }); - _logger.LogInformation($"Value ({value}) set for configuration property ({name})."); - Saved?.Invoke(this, new ConfigurationSavedEventArgs(name, value, value.GetType())); - } - - public async Task SetAsync(string name, T value, JsonTypeInfo info) where T : notnull + public async Task SetAsync(string name, T value, JsonTypeInfo info) where T : notnull, new() { _logger.LogInformation($"Setting object configuration property ({name}) to value ({value})..."); _cache[name] = value; diff --git a/Nickvision.Desktop/Application/IConfigurationService.cs b/Nickvision.Desktop/Application/IConfigurationService.cs index 55fd8ed..c5a8267 100644 --- a/Nickvision.Desktop/Application/IConfigurationService.cs +++ b/Nickvision.Desktop/Application/IConfigurationService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text.Json.Serialization.Metadata; using System.Threading.Tasks; @@ -11,27 +11,15 @@ public interface IConfigurationService Dictionary GetAllRaw(); Task> GetAllRawAsync(); - bool GetBool(string name, bool defaultValue = false); - Task GetBoolAsync(string name, bool defaultValue = false); - double GetDouble(string name, double defaultValue = 0.0); - Task GetDoubleAsync(string name, double defaultValue = 0.0); - int GetInt(string name, int defaultValue = 0); - Task GetIntAsync(string name, int defaultValue = 0); - T GetObject(string name, T defaultValue, JsonTypeInfo info) where T : notnull; - Task GetObjectAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull; - string GetString(string name, string defaultValue = ""); - Task GetStringAsync(string name, string defaultValue = ""); + T Get(string name, T defaultValue) where T : notnull; + T Get(string name, T defaultValue, JsonTypeInfo info) where T : notnull, new(); + Task GetAsync(string name, T defaultValue) where T : notnull; + Task GetAsync(string name, T defaultValue, JsonTypeInfo info) where T : notnull, new(); Task ImportFromJsonFileAsync(string path); void Save(); Task SaveAsync(); - void Set(string name, bool value); - void Set(string name, double value); - void Set(string name, int value); - void Set(string name, string value); - void Set(string name, T value, JsonTypeInfo info) where T : notnull; - Task SetAsync(string name, bool value); - Task SetAsync(string name, double value); - Task SetAsync(string name, int value); - Task SetAsync(string name, string value); - Task SetAsync(string name, T value, JsonTypeInfo info) where T : notnull; + void Set(string name, T value) where T : notnull; + void Set(string name, T value, JsonTypeInfo info) where T : notnull, new(); + Task SetAsync(string name, T value) where T : notnull; + Task SetAsync(string name, T value, JsonTypeInfo info) where T : notnull, new(); } diff --git a/Nickvision.Desktop/Nickvision.Desktop.csproj b/Nickvision.Desktop/Nickvision.Desktop.csproj index f96f0ba..b8a7244 100644 --- a/Nickvision.Desktop/Nickvision.Desktop.csproj +++ b/Nickvision.Desktop/Nickvision.Desktop.csproj @@ -25,7 +25,7 @@ - + From cf0638eec8849be876d22f128acc39e459a69ada Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 21:17:56 -0400 Subject: [PATCH 14/16] feat: DependencyExecutableService --- Nickvision.Desktop/Application/AppVersion.cs | 6 + .../Application/AppVersionJsonContext.cs | 10 ++ Nickvision.Desktop/Nickvision.Desktop.csproj | 2 +- .../System/DependencyExecutableService.cs | 141 ++++++++++++++++++ .../System/IDependencyExecutableService.cs | 17 +++ 5 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 Nickvision.Desktop/Application/AppVersionJsonContext.cs create mode 100644 Nickvision.Desktop/System/DependencyExecutableService.cs create mode 100644 Nickvision.Desktop/System/IDependencyExecutableService.cs diff --git a/Nickvision.Desktop/Application/AppVersion.cs b/Nickvision.Desktop/Application/AppVersion.cs index 1f6a103..093967a 100644 --- a/Nickvision.Desktop/Application/AppVersion.cs +++ b/Nickvision.Desktop/Application/AppVersion.cs @@ -8,6 +8,12 @@ public class AppVersion : IComparable, IEquatable public Version BaseVersion { get; init; } public string PreviewLabel { get; init; } + public AppVersion() + { + BaseVersion = new Version(0, 0, 0); + PreviewLabel = string.Empty; + } + public AppVersion(string version) { var dashIndex = version.IndexOf('-'); diff --git a/Nickvision.Desktop/Application/AppVersionJsonContext.cs b/Nickvision.Desktop/Application/AppVersionJsonContext.cs new file mode 100644 index 0000000..5f58981 --- /dev/null +++ b/Nickvision.Desktop/Application/AppVersionJsonContext.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Nickvision.Desktop.Application; + +[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.SnakeCaseLower, WriteIndented = true)] +[JsonSerializable(typeof(AppVersion))] +internal partial class AppVersionJsonContext : JsonSerializerContext +{ + +} diff --git a/Nickvision.Desktop/Nickvision.Desktop.csproj b/Nickvision.Desktop/Nickvision.Desktop.csproj index b8a7244..7a5efc9 100644 --- a/Nickvision.Desktop/Nickvision.Desktop.csproj +++ b/Nickvision.Desktop/Nickvision.Desktop.csproj @@ -1,7 +1,7 @@  - net9.0;net10.0 + net10.0 latest disable enable diff --git a/Nickvision.Desktop/System/DependencyExecutableService.cs b/Nickvision.Desktop/System/DependencyExecutableService.cs new file mode 100644 index 0000000..703143a --- /dev/null +++ b/Nickvision.Desktop/System/DependencyExecutableService.cs @@ -0,0 +1,141 @@ +using Microsoft.Extensions.Logging; +using Nickvision.Desktop.Application; +using Nickvision.Desktop.Filesystem; +using Nickvision.Desktop.Network; +using System; +using System.Diagnostics; +using System.IO; +using System.IO.Compression; +using System.Threading.Tasks; + +namespace Nickvision.Desktop.System; + +public abstract class DependencyExecutableService : IDependencyExecutableService +{ + protected readonly ILogger _logger; + protected readonly string _executableName; + protected readonly string _assetName; + protected readonly IConfigurationService _configurationService; + protected readonly IUpdaterService _updaterService; + protected AppVersion? _latestStableVersion; + protected AppVersion? _latestPreviewVersion; + + public AppVersion BundledVersion { get; } + + public DependencyExecutableService(ILogger logger, string executableName, AppVersion bundledVersion, string assetName, IConfigurationService configurationService, IUpdaterService updaterService) + { + _logger = logger; + _executableName = executableName; + _assetName = assetName; + _configurationService = configurationService; + _updaterService = updaterService; + _latestStableVersion = null; + BundledVersion = bundledVersion; + } + + public virtual string ExecutablePath + { + get + { + if (!string.IsNullOrEmpty(field)) + { + return field; + } + _logger.LogInformation($"Searching for {_executableName} executable..."); + var configKey = $"installed_{_executableName}_appversion"; + if (_configurationService.Get(configKey, new AppVersion(), AppVersionJsonContext.Default.AppVersion) > BundledVersion) + { + var local = Environment.FindDependency(_executableName, DependencySearchOption.Local); + if (!string.IsNullOrEmpty(local) && File.Exists(local)) + { + _logger.LogInformation($"Found updated {_executableName} executable: {local}"); + field = local; + return field; + } + else + { + _configurationService.Set(configKey, new AppVersion()); + _configurationService.Save(); + } + } + field = Environment.FindDependency(_executableName, DependencySearchOption.Global); + _logger.LogInformation($"Found bundled {_executableName} executable: {field}"); + return field ?? _executableName; + } + } + + public virtual async Task DownloadUpdateAsync(AppVersion version, IProgress? progress = null) + { + var isZip = Path.GetExtension(_assetName).Equals(".zip", StringComparison.OrdinalIgnoreCase); + var downloadPath = Path.Combine(UserDirectories.LocalData, $"{_executableName}.{(isZip ? "zip" : (OperatingSystem.IsWindows() ? "exe" : string.Empty))}"); + var res = await _updaterService.DownloadReleaseAssetAsync(version, downloadPath, _assetName, true, progress); + if (res) + { + var configKey = $"installed_{_executableName}_appversion"; + var executablePath = Path.Combine(UserDirectories.LocalData, $"{_executableName}.{(OperatingSystem.IsWindows() ? "exe" : string.Empty)}"); + await _configurationService.SetAsync(configKey, version, AppVersionJsonContext.Default.AppVersion); + if (isZip) + { + await ZipFile.ExtractToDirectoryAsync(downloadPath, UserDirectories.LocalData); + File.Delete(downloadPath); + } + if (!OperatingSystem.IsWindows()) + { + using var process = new Process() + { + StartInfo = new ProcessStartInfo("chmod", ["0755", executablePath]) + { + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + await process.WaitForExitAsync(); + return process.ExitCode == 0; + } + } + return res; + } + + public virtual async Task GetExecutableVersionAsync(string versionArgument = "--version") + { + using var process = new Process() + { + StartInfo = new ProcessStartInfo(ExecutablePath, versionArgument) + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + } + }; + process.Start(); + var outputTask = process.StandardOutput.ReadToEndAsync(); + await process.WaitForExitAsync(); + var output = await outputTask; + if (process.ExitCode == 0 && AppVersion.TryParse(output.Trim(), out var version)) + { + return version; + } + return null; + } + + public virtual async Task GetLatestStableVersionAsync() + { + if (_latestStableVersion is null) + { + var _ = ExecutablePath; + _latestStableVersion = await _updaterService.GetLatestStableVersionAsync(); + } + return _latestStableVersion; + } + + public virtual async Task GetLatestPreviewVersionAsync() + { + if (_latestPreviewVersion is null) + { + var _ = ExecutablePath; + _latestPreviewVersion = await _updaterService.GetLatestPreviewVersionAsync(); + } + return _latestPreviewVersion; + } +} diff --git a/Nickvision.Desktop/System/IDependencyExecutableService.cs b/Nickvision.Desktop/System/IDependencyExecutableService.cs new file mode 100644 index 0000000..edcd197 --- /dev/null +++ b/Nickvision.Desktop/System/IDependencyExecutableService.cs @@ -0,0 +1,17 @@ +using Nickvision.Desktop.Application; +using Nickvision.Desktop.Network; +using System; +using System.Threading.Tasks; + +namespace Nickvision.Desktop.System; + +public interface IDependencyExecutableService +{ + AppVersion BundledVersion { get; } + string ExecutablePath { get; } + + Task DownloadUpdateAsync(AppVersion version, IProgress? progress = null); + Task GetExecutableVersionAsync(string versionArgument = "--version"); + Task GetLatestStableVersionAsync(); + Task GetLatestPreviewVersionAsync(); +} From 2037afbe12481adf742604432ce55993e7bf5898 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 21:29:27 -0400 Subject: [PATCH 15/16] feat: Add ExecuteAsync to DependencyExecutableService --- .../System/DependencyExecutableService.cs | 24 +++++++++++++++++++ .../System/IDependencyExecutableService.cs | 3 +++ Nickvision.Desktop/System/ProcessResult.cs | 15 ++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 Nickvision.Desktop/System/ProcessResult.cs diff --git a/Nickvision.Desktop/System/DependencyExecutableService.cs b/Nickvision.Desktop/System/DependencyExecutableService.cs index 703143a..b5d6968 100644 --- a/Nickvision.Desktop/System/DependencyExecutableService.cs +++ b/Nickvision.Desktop/System/DependencyExecutableService.cs @@ -3,9 +3,11 @@ using Nickvision.Desktop.Filesystem; using Nickvision.Desktop.Network; using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.IO.Compression; +using System.Threading; using System.Threading.Tasks; namespace Nickvision.Desktop.System; @@ -97,6 +99,28 @@ public virtual async Task DownloadUpdateAsync(AppVersion version, IProgres return res; } + public virtual async Task ExecuteAsync(IReadOnlyList arguments, CancellationToken cancellationToken = default) + { + using var proc = new Process() + { + EnableRaisingEvents = true, + StartInfo = new ProcessStartInfo(ExecutablePath, arguments) + { + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + } + }; + proc.Start(); + var outputTask = proc.StandardOutput.ReadToEndAsync(cancellationToken); + var errorTask = proc.StandardError.ReadToEndAsync(cancellationToken); + await proc.WaitForExitAsync(cancellationToken); + var output = await outputTask; + var error = await errorTask; + return new ProcessResult(proc.ExitCode, output, error); + } + public virtual async Task GetExecutableVersionAsync(string versionArgument = "--version") { using var process = new Process() diff --git a/Nickvision.Desktop/System/IDependencyExecutableService.cs b/Nickvision.Desktop/System/IDependencyExecutableService.cs index edcd197..8ddd0cf 100644 --- a/Nickvision.Desktop/System/IDependencyExecutableService.cs +++ b/Nickvision.Desktop/System/IDependencyExecutableService.cs @@ -1,6 +1,8 @@ using Nickvision.Desktop.Application; using Nickvision.Desktop.Network; using System; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; namespace Nickvision.Desktop.System; @@ -11,6 +13,7 @@ public interface IDependencyExecutableService string ExecutablePath { get; } Task DownloadUpdateAsync(AppVersion version, IProgress? progress = null); + Task ExecuteAsync(IReadOnlyList arguments, CancellationToken cancellationToken = default); Task GetExecutableVersionAsync(string versionArgument = "--version"); Task GetLatestStableVersionAsync(); Task GetLatestPreviewVersionAsync(); diff --git a/Nickvision.Desktop/System/ProcessResult.cs b/Nickvision.Desktop/System/ProcessResult.cs new file mode 100644 index 0000000..4a34e72 --- /dev/null +++ b/Nickvision.Desktop/System/ProcessResult.cs @@ -0,0 +1,15 @@ +namespace Nickvision.Desktop.System; + +public class ProcessResult +{ + public int ExitCode { get; } + public string Output { get; } + public string Error { get; } + + public ProcessResult(int exitCode, string output, string error) + { + ExitCode = exitCode; + Output = output; + Error = error; + } +} From ba5419dbed2a9bb3f217b4199971c765d3f3d189 Mon Sep 17 00:00:00 2001 From: Nick Logozzo Date: Sat, 28 Mar 2026 21:37:27 -0400 Subject: [PATCH 16/16] fix: Spelling --- Nickvision.Desktop/Application/DatabaseService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Nickvision.Desktop/Application/DatabaseService.cs b/Nickvision.Desktop/Application/DatabaseService.cs index 815ba05..75314cb 100644 --- a/Nickvision.Desktop/Application/DatabaseService.cs +++ b/Nickvision.Desktop/Application/DatabaseService.cs @@ -242,7 +242,7 @@ public async Task ExecuteNonQueryAsync(string sql, Dictionary data) { EnsureDatabase(); - _logger.LogInformation($"Insering data into {tableName}..."); + _logger.LogInformation($"Inserting data into {tableName}..."); using var command = _connection!.CreateCommand(); command.CommandText = $"INSERT INTO {tableName} ({string.Join(", ", data.Keys)}) VALUES ({string.Join(", ", data.Keys.Select(k => $"${k}"))})"; foreach (var pair in data) @@ -264,7 +264,7 @@ public bool InsertIntoTable(string tableName, Dictionary data) public async Task InsertIntoTableAsync(string tableName, Dictionary data) { await EnsureDatabaseAsync(); - _logger.LogInformation($"Insering data into {tableName}..."); + _logger.LogInformation($"Inserting data into {tableName}..."); await using var command = _connection!.CreateCommand(); command.CommandText = $"INSERT INTO {tableName} ({string.Join(", ", data.Keys)}) VALUES ({string.Join(", ", data.Keys.Select(k => $"${k}"))})"; foreach (var pair in data) @@ -308,7 +308,7 @@ public bool ReplaceIntoTable(string tableName, Dictionary data) public async Task ReplaceIntoTableAsync(string tableName, Dictionary data) { await EnsureDatabaseAsync(); - _logger.LogInformation($"Insering data into {tableName}..."); + _logger.LogInformation($"Inserting data into {tableName}..."); await using var command = _connection!.CreateCommand(); command.CommandText = $"INSERT OR REPLACE INTO {tableName} ({string.Join(", ", data.Keys)}) VALUES ({string.Join(", ", data.Keys.Select(k => $"${k}"))})"; foreach (var pair in data)