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; -/// -/// Options for searching for dependencies. -/// public enum DependencySearchOption { Global, diff --git a/Nickvision.Desktop/System/DeploymentMode.cs b/Nickvision.Desktop/System/DeploymentMode.cs index 5cdc2ad..4f8fbcb 100644 --- a/Nickvision.Desktop/System/DeploymentMode.cs +++ b/Nickvision.Desktop/System/DeploymentMode.cs @@ -1,8 +1,5 @@ namespace Nickvision.Desktop.System; -/// -/// Modes of deployment for the application. -/// public enum DeploymentMode { Local, diff --git a/Nickvision.Desktop/System/Environment.cs b/Nickvision.Desktop/System/Environment.cs index c1d57f3..df211ab 100644 --- a/Nickvision.Desktop/System/Environment.cs +++ b/Nickvision.Desktop/System/Environment.cs @@ -10,24 +10,15 @@ namespace Nickvision.Desktop.System; -/// -/// Helpers for working with the system environment. -/// public static class Environment { private static readonly Dictionary<(string Dependency, DependencySearchOption Search), string?> Dependencies; - /// - /// Constructs a static Environment. - /// static Environment() { Dependencies = []; } - /// - /// The deployment mode of the application. - /// public static DeploymentMode DeploymentMode { get @@ -44,19 +35,10 @@ public static DeploymentMode DeploymentMode } } - /// - /// The application executable's directory. - /// public static string ExecutingDirectory => Path.GetDirectoryName(ExecutingPath) ?? global::System.Environment.CurrentDirectory; - /// - /// The application executable's path. - /// public static string ExecutingPath => Path.GetFullPath(global::System.Environment.ProcessPath!); - /// - /// The list of directories in the PATH variable. - /// public static IEnumerable PathVariable { get @@ -66,13 +48,6 @@ public static IEnumerable PathVariable } } - /// - /// Finds a dependency on the system. - /// - /// The dependency to find - /// The search options - /// The path of the dependency if found, else null - /// Thrown if the search option is invalid public static string? FindDependency(string dependency, DependencySearchOption search = DependencySearchOption.Global) { if (OperatingSystem.IsWindows()) @@ -145,12 +120,6 @@ public static IEnumerable PathVariable return Dependencies[(dependency, search)]; } - /// - /// Gets the debug information for the application. - /// - /// The AppInfo object for the app - /// Any extra information to include in the debug information string - /// The debug information string public static string GetDebugInformation(AppInfo info, string extra = "") => $""" App: {info.Name} Version: {info.Version} diff --git a/Nickvision.Desktop/System/IDependencyExecutableService.cs b/Nickvision.Desktop/System/IDependencyExecutableService.cs new file mode 100644 index 0000000..8ddd0cf --- /dev/null +++ b/Nickvision.Desktop/System/IDependencyExecutableService.cs @@ -0,0 +1,20 @@ +using Nickvision.Desktop.Application; +using Nickvision.Desktop.Network; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Nickvision.Desktop.System; + +public interface IDependencyExecutableService +{ + AppVersion BundledVersion { get; } + string ExecutablePath { get; } + + Task DownloadUpdateAsync(AppVersion version, IProgress? progress = null); + Task ExecuteAsync(IReadOnlyList arguments, CancellationToken cancellationToken = default); + Task GetExecutableVersionAsync(string versionArgument = "--version"); + Task GetLatestStableVersionAsync(); + Task GetLatestPreviewVersionAsync(); +} diff --git a/Nickvision.Desktop/System/IPowerService.cs b/Nickvision.Desktop/System/IPowerService.cs index 7897ffb..383c51c 100644 --- a/Nickvision.Desktop/System/IPowerService.cs +++ b/Nickvision.Desktop/System/IPowerService.cs @@ -2,20 +2,9 @@ namespace Nickvision.Desktop.System; -/// -/// An interface for a service for managing power options. -/// public interface IPowerService { - /// - /// Allows the system to suspend. - /// - /// True if the action was applied successfully, else false Task AllowSuspendAsync(); - /// - /// Prevents the system from suspending. - /// - /// True if the action was applied successfully, else false Task PreventSuspendAsync(); } diff --git a/Nickvision.Desktop/System/ISecretService.cs b/Nickvision.Desktop/System/ISecretService.cs index 3b04078..c76f046 100644 --- a/Nickvision.Desktop/System/ISecretService.cs +++ b/Nickvision.Desktop/System/ISecretService.cs @@ -2,39 +2,11 @@ namespace Nickvision.Desktop.System; -/// -/// An interface of a service for managing secrets. -/// public interface ISecretService { - /// - /// Adds a secret asynchronously. - /// - /// The secret to add - /// True if the secret was added successfully, else false Task AddAsync(Secret secret); - /// - /// Creates a secret asynchronously with a random but secure value. - /// - /// The name of the secret to create - /// The created secret if successful, else null Task CreateAsync(string name); - /// - /// Deletes a secret asynchronously. - /// - /// The name of the secret to delete - /// True if the secret was deleted successfully, else false Task DeleteAsync(string name); - /// - /// Gets a secret asynchronously. - /// - /// The name of the secret to find - /// The secret if found, else null Task GetAsync(string name); - /// - /// Updates a secret asynchronously. - /// - /// The secret to update - /// True if the secret was updated successfully, else false Task UpdateAsync(Secret secret); } diff --git a/Nickvision.Desktop/System/PowerService.cs b/Nickvision.Desktop/System/PowerService.cs index a45a5ed..74fb248 100644 --- a/Nickvision.Desktop/System/PowerService.cs +++ b/Nickvision.Desktop/System/PowerService.cs @@ -11,9 +11,6 @@ namespace Nickvision.Desktop.System; -/// -/// A server for managing power options. -/// public class PowerService : IDisposable, IPowerService { private readonly ILogger _logger; @@ -22,10 +19,6 @@ public class PowerService : IDisposable, IPowerService private uint _inhibitCookie; private Process? _preventSuspendProcess; - /// - /// Constructs a PowerService. - /// - /// Logger for the service public PowerService(ILogger logger) { _logger = logger; @@ -33,27 +26,17 @@ public PowerService(ILogger logger) _inhibitCookie = 0; } - /// - /// Finalizes a PowerService. - /// ~PowerService() { Dispose(false); } - /// - /// Disposes a PowerService. - /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } - /// - /// Allows the system to suspend. - /// - /// True if the action was applied successfully, else false public async Task AllowSuspendAsync() { _logger.LogInformation("Allowing system suspend..."); @@ -104,10 +87,6 @@ public async Task AllowSuspendAsync() } } - /// - /// Prevents the system from suspending. - /// - /// True if the action was applied successfully, else false public async Task PreventSuspendAsync() { _logger.LogInformation("Preventing system suspend..."); @@ -189,10 +168,6 @@ public async Task PreventSuspendAsync() } } - /// - /// Disposes a PowerService. - /// - /// Whether to dispose managed resources private void Dispose(bool disposing) { if (_disposed) diff --git a/Nickvision.Desktop/System/ProcessResult.cs b/Nickvision.Desktop/System/ProcessResult.cs new file mode 100644 index 0000000..4a34e72 --- /dev/null +++ b/Nickvision.Desktop/System/ProcessResult.cs @@ -0,0 +1,15 @@ +namespace Nickvision.Desktop.System; + +public class ProcessResult +{ + public int ExitCode { get; } + public string Output { get; } + public string Error { get; } + + public ProcessResult(int exitCode, string output, string error) + { + ExitCode = exitCode; + Output = output; + Error = error; + } +} diff --git a/Nickvision.Desktop/System/Secret.cs b/Nickvision.Desktop/System/Secret.cs index 36abf30..5058827 100644 --- a/Nickvision.Desktop/System/Secret.cs +++ b/Nickvision.Desktop/System/Secret.cs @@ -1,8 +1,5 @@ namespace Nickvision.Desktop.System; -/// -/// A class representing a secret. -/// public class Secret { public string Name { get; init; } diff --git a/Nickvision.Desktop/System/SecretService.cs b/Nickvision.Desktop/System/SecretService.cs index 3c15b05..31e9ce4 100644 --- a/Nickvision.Desktop/System/SecretService.cs +++ b/Nickvision.Desktop/System/SecretService.cs @@ -13,27 +13,15 @@ namespace Nickvision.Desktop.System; -/// -/// A service for managing secrets using the system's secret storage. -/// public class SecretService : ISecretService { private readonly ILogger _logger; - /// - /// Constructs a SecretService. - /// - /// Logger for the service public SecretService(ILogger logger) { _logger = logger; } - /// - /// Adds a secret asynchronously. - /// - /// The secret to add - /// True if the secret was added successfully, else false public async Task AddAsync(Secret secret) { _logger.LogInformation($"Adding system secret ({secret.Name})."); @@ -151,11 +139,6 @@ public async Task AddAsync(Secret secret) } } - /// - /// Creates a secret asynchronously with a random but secure value. - /// - /// The name of the secret to create - /// The created secret if successful, else null public async Task CreateAsync(string name) { _logger.LogInformation($"Creating system secret ({name})."); @@ -177,11 +160,6 @@ public async Task AddAsync(Secret secret) return result; } - /// - /// Deletes a secret asynchronously. - /// - /// The name of the secret to delete - /// True if the secret was deleted successfully, else false public async Task DeleteAsync(string name) { _logger.LogInformation($"Deleting system secret ({name})."); @@ -266,11 +244,6 @@ public async Task DeleteAsync(string name) } } - /// - /// Gets a secret asynchronously. - /// - /// The name of the secret to find - /// The secret if found, else null public async Task GetAsync(string name) { _logger.LogInformation($"Getting system secret ({name})."); @@ -376,11 +349,6 @@ public async Task DeleteAsync(string name) } } - /// - /// Updates a secret asynchronously. - /// - /// The secret to update - /// True if the secret was updated successfully, else false public async Task UpdateAsync(Secret secret) { _logger.LogInformation($"Updating system secret ({secret.Name}).");