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.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
new file mode 100644
index 0000000..10f5573
--- /dev/null
+++ b/Nickvision.Desktop.Tests/ConfigurationServiceTests.cs
@@ -0,0 +1,225 @@
+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()
+ {
+ Test = "";
+ }
+
+ public TestObj(string test)
+ {
+ Test = test;
+ }
+}
+
+[JsonSourceGenerationOptions(DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, WriteIndented = true)]
+[JsonSerializable(typeof(TestObj))]
+internal partial class TestJsonContext : JsonSerializerContext { }
+
+[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.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.Get("nonExistentBool", false);
+ Assert.AreEqual(false, val1);
+ var val2 = await _configurationService.GetAsync("nonExistentBoolAsync", true);
+ Assert.AreEqual(true, val2);
+ var val3 = _configurationService.Get("nonExistentDouble", 1.5);
+ Assert.AreEqual(1.5, val3);
+ var val4 = await _configurationService.GetAsync("nonExistentDoubleAsync", 2.5);
+ Assert.AreEqual(2.5, val4);
+ var val5 = _configurationService.Get("nonExistentInt", 42);
+ Assert.AreEqual(42, val5);
+ var val6 = await _configurationService.GetAsync("nonExistentIntAsync", 84);
+ Assert.AreEqual(84, val6);
+ var val7 = _configurationService.Get("nonExistentString", "default");
+ Assert.AreEqual("default", val7);
+ var val8 = await _configurationService.GetAsync("nonExistentStringAsync", "asyncDefault");
+ Assert.AreEqual("asyncDefault", val8);
+ var val9 = _configurationService.Get("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj);
+ Assert.AreEqual("value", val9.Test);
+ var val10 = await _configurationService.GetAsync("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.Get("nonExistentBool", false);
+ Assert.AreEqual(true, val1);
+ var val2 = await _configurationService.GetAsync("nonExistentBoolAsync", true);
+ Assert.AreEqual(false, val2);
+ var val3 = _configurationService.Get("nonExistentDouble", 1.5);
+ Assert.AreEqual(2.5, val3);
+ var val4 = await _configurationService.GetAsync("nonExistentDoubleAsync", 2.5);
+ Assert.AreEqual(1.5, val4);
+ var val5 = _configurationService.Get("nonExistentInt", 42);
+ Assert.AreEqual(84, val5);
+ var val6 = await _configurationService.GetAsync("nonExistentIntAsync", 84);
+ Assert.AreEqual(42, val6);
+ var val7 = _configurationService.Get("nonExistentString", "default");
+ Assert.AreEqual("default2", val7);
+ var val8 = await _configurationService.GetAsync("nonExistentStringAsync", "asyncDefault");
+ Assert.AreEqual("asyncDefault2", val8);
+ var val9 = _configurationService.Get("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj);
+ Assert.AreEqual("value2", val9.Test);
+ var val10 = await _configurationService.GetAsync("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.Get("nonExistentBool", false);
+ Assert.AreEqual(true, val1);
+ var val2 = await _configurationService.GetAsync("nonExistentBoolAsync", true);
+ Assert.AreEqual(false, val2);
+ var val3 = _configurationService.Get("nonExistentDouble", 1.5);
+ Assert.AreEqual(2.5, val3);
+ var val4 = await _configurationService.GetAsync("nonExistentDoubleAsync", 2.5);
+ Assert.AreEqual(1.5, val4);
+ var val5 = _configurationService.Get("nonExistentInt", 42);
+ Assert.AreEqual(84, val5);
+ var val6 = await _configurationService.GetAsync("nonExistentIntAsync", 84);
+ Assert.AreEqual(42, val6);
+ var val7 = _configurationService.Get("nonExistentString", "default");
+ Assert.AreEqual("default2", val7);
+ var val8 = await _configurationService.GetAsync("nonExistentStringAsync", "asyncDefault");
+ Assert.AreEqual("asyncDefault2", val8);
+ var val9 = _configurationService.Get("nonExistentObject", new TestObj("value"), TestJsonContext.Default.TestObj);
+ Assert.AreEqual("value2", val9.Test);
+ var val10 = await _configurationService.GetAsync("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_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.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
+ {
+ File.Delete(path);
+ }
+ }
+
+ [TestMethod]
+ public async Task Case012_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.Tests/DatabaseServiceTests.cs b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs
new file mode 100644
index 0000000..69df1f7
--- /dev/null
+++ b/Nickvision.Desktop.Tests/DatabaseServiceTests.cs
@@ -0,0 +1,234 @@
+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;
+
+[TestClass]
+public class DatabaseServiceTests
+{
+ private static DatabaseService? _databaseService;
+
+ [TestMethod]
+ public void Case001_Init()
+ {
+ _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"));
+ Assert.IsTrue(_databaseService.TableExists("test_table"));
+ Assert.IsFalse(_databaseService.TableExists("missing_table"));
+ 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"));
+ Assert.IsTrue(await _databaseService.TableExistsAsync(asyncTable));
+ Assert.IsFalse(await _databaseService.TableExistsAsync("missing_table_async"));
+ 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 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);
+ await _databaseService.DisposeAsync();
+ File.Delete(path);
+ Directory.Delete(Path.GetDirectoryName(path)!);
+ Assert.IsFalse(File.Exists(path));
+ _databaseService = null;
+ }
+}
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..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.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 ee71dd8..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.Tests/KeyringServiceTests.cs b/Nickvision.Desktop.Tests/KeyringServiceTests.cs
index 9660a1a..01cb471 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,21 +13,25 @@ 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()));
+ 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);
- Assert.IsTrue(_keyringService.IsSavingToDisk);
}
[TestMethod]
- public void Case002_Check()
+ public async Task Case002_Get()
{
Assert.IsNotNull(_keyringService);
- Assert.IsTrue(_keyringService.IsSavingToDisk);
+ Assert.AreEqual(0, (await _keyringService.GetAllCredentialAsync()).Count);
}
[TestMethod]
@@ -35,43 +39,55 @@ 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(_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"))));
+ 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_Update()
+ public async Task Case004_Get()
+ {
+ Assert.IsNotNull(_keyringService);
+ Assert.AreEqual(3, (await _keyringService.GetAllCredentialAsync()).Count);
+ }
+
+ [TestMethod]
+ 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"))));
- 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);
}
[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"))));
- 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()
+ public async Task Case007_Cleanup()
{
+ var path = Path.Combine(UserDirectories.Config, "Nickvision.Desktop.Test.Keyring", "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.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/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..093967a 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;
@@ -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/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/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
new file mode 100644
index 0000000..c4bee2e
--- /dev/null
+++ b/Nickvision.Desktop/Application/ConfigurationService.cs
@@ -0,0 +1,430 @@
+using Microsoft.Data.Sqlite;
+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;
+
+namespace Nickvision.Desktop.Application;
+
+public class ConfigurationService : IAsyncDisposable, IConfigurationService, IDisposable
+{
+ private static readonly string TableName;
+
+ private readonly ILogger _logger;
+ private readonly IDatabaseService _databaseService;
+ private readonly Dictionary _cache;
+ private bool _tableEnsured;
+ private SqliteTransaction? _transaction;
+
+ public event EventHandler? Saved;
+
+ static ConfigurationService()
+ {
+ TableName = "configuration";
+ }
+
+ public ConfigurationService(ILogger logger, IDatabaseService databaseService)
+ {
+ _logger = logger;
+ _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 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 T Get(string name, T defaultValue) where T : notnull
+ {
+ _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;
+ }
+ _cache[name] = defaultValue;
+ EnsureTable();
+ using var command = _databaseService.SelectFromTable(TableName, "name", name);
+ using var reader = command.ExecuteReader();
+ while (reader.Read())
+ {
+ try
+ {
+ _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 (T)_cache[name];
+ }
+
+ 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)
+ {
+ _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] = JsonSerializer.Deserialize(reader.GetString(1), info)!;
+ }
+ catch { }
+ }
+ _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database.");
+ return (T)_cache[name];
+ }
+
+ public async Task GetAsync(string name, T defaultValue) where T : notnull
+ {
+ _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;
+ }
+ _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] = 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 { }
+ }
+ _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database.");
+ return (T)_cache[name];
+ }
+
+ public async Task GetAsync(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)
+ {
+ _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] = JsonSerializer.Deserialize(reader.GetString(1), info)!;
+ }
+ catch { }
+ }
+ _logger.LogInformation($"Value ({_cache[name]}) found for configuration property ({name}) in database.");
+ return (T)_cache[name];
+ }
+
+ public async Task ImportFromJsonFileAsync(string path)
+ {
+ if (!File.Exists(path))
+ {
+ _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())
+ {
+ 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++;
+ }
+ await SaveAsync();
+ _logger.LogInformation($"Imported {imported} configuration properties from JSON file ({path}).");
+ return imported;
+ }
+
+ public void Save()
+ {
+ _transaction?.Commit();
+ _transaction?.Dispose();
+ _transaction = null;
+ }
+
+ public async Task SaveAsync()
+ {
+ if (_transaction is not null)
+ {
+ await _transaction.CommitAsync();
+ await _transaction.DisposeAsync().ConfigureAwait(false);
+ _transaction = null;
+ }
+ }
+
+ public void Set(string name, T value) where T : notnull
+ {
+ _cache[name] = value;
+ EnsureTable();
+ 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 async Task SetAsync(string name, T value) where T : notnull
+ {
+ _cache[name] = value;
+ 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, new()
+ {
+ _logger.LogInformation($"Setting object configuration property ({name}) to value ({value})...");
+ _cache[name] = value;
+ EnsureTable();
+ _databaseService.ReplaceIntoTable(TableName, new Dictionary()
+ {
+ { "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, T value, JsonTypeInfo info) where T : notnull, new()
+ {
+ _logger.LogInformation($"Setting object configuration property ({name}) to value ({value})...");
+ _cache[name] = value;
+ await EnsureTableAsync();
+ await _databaseService.ReplaceIntoTableAsync(TableName, new Dictionary()
+ {
+ { "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()));
+ }
+
+ 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;
+ }
+
+ private async Task EnsureTableAsync()
+ {
+ if (_tableEnsured)
+ {
+ if (_transaction is null)
+ {
+ _transaction = await _databaseService.CreateTransationAsync();
+ }
+ return;
+ }
+ await _databaseService.EnsureTableExistsAsync(TableName, "name TEXT PRIMARY KEY, value TEXT");
+ _transaction = await _databaseService.CreateTransationAsync();
+ _tableEnsured = true;
+ }
+}
diff --git a/Nickvision.Desktop/Application/DatabaseService.cs b/Nickvision.Desktop/Application/DatabaseService.cs
new file mode 100644
index 0000000..75314cb
--- /dev/null
+++ b/Nickvision.Desktop/Application/DatabaseService.cs
@@ -0,0 +1,545 @@
+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 int CountInTable(string tableName)
+ {
+ EnsureDatabase();
+ _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);
+ var result = Convert.ToInt32(command.ExecuteScalar()) >= 1;
+ if (result)
+ {
+ _logger.LogInformation($"Found matching typed column ({columnName}) value in {tableName}.");
+ }
+ else
+ {
+ _logger.LogInformation($"Failed to find matching typed column ({columnName}) value in {tableName}.");
+ }
+ return result;
+ }
+
+ public async Task ContainsInTableAsync(string tableName, string columnName, T matchingValue)
+ {
+ await EnsureDatabaseAsync();
+ _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);
+ var result = Convert.ToInt32(await command.ExecuteScalarAsync()) >= 1;
+ if (result)
+ {
+ _logger.LogInformation($"Found matching typed column ({columnName}) value in {tableName}.");
+ }
+ else
+ {
+ _logger.LogInformation($"Failed to find matching typed 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, T matchingValue)
+ {
+ EnsureDatabase();
+ _logger.LogInformation($"Deleting row from {tableName}...");
+ using var command = _connection!.CreateCommand();
+ command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $matchingValueParam";
+ command.Parameters.AddWithValue("$matchingValueParam", 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, T matchingValue)
+ {
+ await EnsureDatabaseAsync();
+ _logger.LogInformation($"Deleting row from {tableName}...");
+ await using var command = _connection!.CreateCommand();
+ command.CommandText = $"DELETE FROM {tableName} WHERE {columnName} = $matchingValueParam";
+ command.Parameters.AddWithValue("$matchingValueParam", 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 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();
+ _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 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();
+ _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)
+ {
+ 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($"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)
+ {
+ 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($"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)
+ {
+ 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, T matchingValue)
+ {
+ EnsureDatabase();
+ _logger.LogInformation($"Selecting data from table {tableName} with matching column ({columnName})...");
+ var command = _connection!.CreateCommand();
+ 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;
+ }
+
+ public async Task SelectFromTableAsync(string tableName, string columnName, T 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} = $matchingValueParam";
+ command.Parameters.AddWithValue("$matchingValueParam", 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 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}...");
+ using var command = _connection!.CreateCommand();
+ 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);
+ }
+ 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, T 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}"))} WHERE {columnName} = $matchingValueParam";
+ command.Parameters.AddWithValue("$matchingValueParam", matchingValue);
+ 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;
+ throw;
+ }
+ }
+
+ 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;
+ throw;
+ }
+ }
+
+ 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..c5a8267
--- /dev/null
+++ b/Nickvision.Desktop/Application/IConfigurationService.cs
@@ -0,0 +1,25 @@
+using System;
+using System.Collections.Generic;
+using System.Text.Json.Serialization.Metadata;
+using System.Threading.Tasks;
+
+namespace Nickvision.Desktop.Application;
+
+public interface IConfigurationService
+{
+ event EventHandler? Saved;
+
+ Dictionary GetAllRaw();
+ Task> GetAllRawAsync();
+ 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, 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/Application/IDatabaseService.cs b/Nickvision.Desktop/Application/IDatabaseService.cs
new file mode 100644
index 0000000..7a65aef
--- /dev/null
+++ b/Nickvision.Desktop/Application/IDatabaseService.cs
@@ -0,0 +1,38 @@
+using Microsoft.Data.Sqlite;
+using System;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace Nickvision.Desktop.Application;
+
+public interface IDatabaseService
+{
+ event EventHandler? PasswordRequired;
+
+ 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, 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, T matchingValue);
+ Task SelectFromTableAsync(string tableName, string columnName, T matchingValue);
+ SqliteCommand SelectAllFromTable(string tableName);
+ Task SelectAllFromTableAsync(string tableName);
+ 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);
+}
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
deleted file mode 100644
index 0c6dc7f..0000000
--- a/Nickvision.Desktop/Filesystem/IJsonFileService.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Text.Json.Serialization.Metadata;
-using System.Threading.Tasks;
-
-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
deleted file mode 100644
index cba3ad2..0000000
--- a/Nickvision.Desktop/Filesystem/JsonFileSavedEventArgs.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using System;
-
-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)
- {
- 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 e926c84..0000000
--- a/Nickvision.Desktop/Filesystem/JsonFileService.cs
+++ /dev/null
@@ -1,173 +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;
-
-///
-/// 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;
- if (!Directory.Exists(directory))
- {
- Directory.CreateDirectory(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");
- _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();
- }
- }
-
- ///
- /// 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");
- _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();
- }
- }
-
- ///
- /// 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)
- {
- 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;
- }
- }
-
- ///
- /// 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)
- {
- 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
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..62d7f43 100644
--- a/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs
+++ b/Nickvision.Desktop/Helpers/HostApplicationBuilderExtensions.cs
@@ -1,8 +1,7 @@
-using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection;
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,7 +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();
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..f091d61 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
+public class Credential : IEquatable
{
- ///
- /// 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;
@@ -42,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 a2b9642..83046f2 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..271b9fa 100644
--- a/Nickvision.Desktop/Keyring/KeyringService.cs
+++ b/Nickvision.Desktop/Keyring/KeyringService.cs
@@ -11,144 +11,51 @@
namespace Nickvision.Desktop.Keyring;
-///
-/// A service for managing credentials in a database keyring.
-///
-public class KeyringService : IAsyncDisposable, IDisposable, IKeyringService
+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 readonly List _credentials;
- private readonly string _path;
- private SqliteConnection? _connection;
+ private bool _tableEnsured;
- ///
- /// 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)
+ static KeyringService()
{
- _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);
- }
-
- ///
- /// Disposes a KeyringService asynchronously.
- ///
- public async ValueTask DisposeAsync()
- {
- await DisposeAsyncCore().ConfigureAwait(false);
- Dispose(false);
- GC.SuppressFinalize(this);
+ TableName = "credentials";
}
- ///
- /// Disposes a KeyringService.
- ///
- public void Dispose()
+ public KeyringService(ILogger logger, AppInfo appInfo, IDatabaseService databaseService, ISecretService secretService)
{
- Dispose(true);
- GC.SuppressFinalize(this);
+ _logger = logger;
+ _appInfo = appInfo;
+ _databaseService = databaseService;
+ _secretService = secretService;
+ _credentials = [];
+ _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)
{
- _logger.LogInformation($"Adding keyring credential ({credential.Name}).");
+ await EnsureTableAsync();
+ _logger.LogInformation($"Adding keyring credential ({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;
}
- _credentials.Add(credential);
- if (_connection is null)
+ var result = await _databaseService.InsertIntoTableAsync(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)
{
+ _credentials.Add(credential);
_logger.LogInformation($"Added keyring credential ({credential.Name}) successfully.");
}
else
@@ -158,92 +65,48 @@ 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.");
+ _credentials.Remove(credential);
+ _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)
- {
- _logger.LogError($"Unable to remove keyring credential ({credential.Name}) from disk as the database connection is unavailable.");
- return false;
- }
- 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;
+ await EnsureTableAsync();
+ 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)
+ await EnsureTableAsync();
+ _logger.LogInformation($"Updating keyring credential ({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;
}
- _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)
{
+ _credentials[index] = credential;
_logger.LogInformation($"Updated keyring credential ({credential.Name}) successfully.");
}
else
@@ -253,29 +116,59 @@ public async Task UpdateCredentialAsync(Credential credential)
return result;
}
- ///
- /// Disposes a KeyringService asynchronously.
- ///
- protected virtual async ValueTask DisposeAsyncCore()
+ private async Task EnsureTableAsync()
{
- if (_connection is not null)
+ if (_tableEnsured)
{
- await _connection.DisposeAsync().ConfigureAwait(false);
+ return;
}
- _connection = null;
- }
-
- ///
- /// Disposes a KeyringService.
- ///
- /// Whether to dispose managed resources
- private void Dispose(bool disposing)
- {
- if (!disposing)
+ 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())
{
- return;
+ _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))
+ {
+ _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}).");
}
- _connection?.Dispose();
- _connection = null;
}
}
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/Nickvision.Desktop.csproj b/Nickvision.Desktop/Nickvision.Desktop.csproj
index f96f0ba..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
@@ -25,7 +25,7 @@
-
+
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/DependencyExecutableService.cs b/Nickvision.Desktop/System/DependencyExecutableService.cs
new file mode 100644
index 0000000..b5d6968
--- /dev/null
+++ b/Nickvision.Desktop/System/DependencyExecutableService.cs
@@ -0,0 +1,165 @@
+using Microsoft.Extensions.Logging;
+using Nickvision.Desktop.Application;
+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;
+
+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 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()
+ {
+ 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/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;
-///