From c2c8a36ad25ebd036f9d8968e4bb317b8a7364fe Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 6 Nov 2022 21:07:41 +0100 Subject: [PATCH 01/30] Added persistent unique ids to all elements --- I18N Commander/DataModel/Database/Section.cs | 2 + I18N Commander/DataModel/Database/Setting.cs | 2 + .../DataModel/Database/TextElement.cs | 2 + .../DataModel/Database/Translation.cs | 2 + .../MigrationScripts/202211AddUniqueIds.cs | 23 ++ ...21106193544_202211AddUniqueIds.Designer.cs | 223 ++++++++++++++++++ .../20221106193544_202211AddUniqueIds.cs | 60 +++++ .../Migrations/DataContextModelSnapshot.cs | 12 + I18N Commander/DataModel/Setup.cs | 7 + I18N Commander/Processor/Version.cs | 2 +- 10 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs create mode 100644 I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.Designer.cs create mode 100644 I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.cs diff --git a/I18N Commander/DataModel/Database/Section.cs b/I18N Commander/DataModel/Database/Section.cs index 195bdba..bb3a2c1 100644 --- a/I18N Commander/DataModel/Database/Section.cs +++ b/I18N Commander/DataModel/Database/Section.cs @@ -6,6 +6,8 @@ public sealed class Section { [Key] public int Id { get; set; } + + public Guid UniqueId { get; set; } public string Name { get; set; } = string.Empty; diff --git a/I18N Commander/DataModel/Database/Setting.cs b/I18N Commander/DataModel/Database/Setting.cs index 8c2dc74..8d0303a 100644 --- a/I18N Commander/DataModel/Database/Setting.cs +++ b/I18N Commander/DataModel/Database/Setting.cs @@ -7,6 +7,8 @@ public sealed class Setting [Key] public int Id { get; set; } + public Guid UniqueId { get; set; } + public string Code { get; set; } = string.Empty; public string TextValue { get; set; } = string.Empty; diff --git a/I18N Commander/DataModel/Database/TextElement.cs b/I18N Commander/DataModel/Database/TextElement.cs index 7c7a943..e1c2154 100644 --- a/I18N Commander/DataModel/Database/TextElement.cs +++ b/I18N Commander/DataModel/Database/TextElement.cs @@ -6,6 +6,8 @@ public sealed class TextElement { [Key] public int Id { get; set; } + + public Guid UniqueId { get; set; } public string Name { get; set; } = string.Empty; diff --git a/I18N Commander/DataModel/Database/Translation.cs b/I18N Commander/DataModel/Database/Translation.cs index 86d172a..b654f73 100644 --- a/I18N Commander/DataModel/Database/Translation.cs +++ b/I18N Commander/DataModel/Database/Translation.cs @@ -3,6 +3,8 @@ public sealed class Translation { public int Id { get; set; } + + public Guid UniqueId { get; set; } public TextElement TextElement { get; set; } diff --git a/I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs b/I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs new file mode 100644 index 0000000..aa85699 --- /dev/null +++ b/I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs @@ -0,0 +1,23 @@ +using DataModel.Database.Common; + +namespace DataModel.MigrationScripts; + +public static class Script202211AddUniqueIds +{ + public static async Task PostMigrationAsync(DataContext db) + { + await foreach (var setting in db.Settings) + setting.UniqueId = Guid.NewGuid(); + + await foreach(var section in db.Sections) + section.UniqueId = Guid.NewGuid(); + + await foreach (var textElement in db.TextElements) + textElement.UniqueId = Guid.NewGuid(); + + await foreach (var translation in db.Translations) + translation.UniqueId = Guid.NewGuid(); + + await db.SaveChangesAsync(); + } +} \ No newline at end of file diff --git a/I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.Designer.cs b/I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.Designer.cs new file mode 100644 index 0000000..10e305e --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.Designer.cs @@ -0,0 +1,223 @@ +// +using System; +using DataModel.Database.Common; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace DataModel.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20221106193544_202211AddUniqueIds")] + partial class _202211AddUniqueIds + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.9"); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Depth") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DataKey"); + + b.HasIndex("Depth"); + + b.HasIndex("Id"); + + b.HasIndex("Name"); + + b.HasIndex("ParentId"); + + b.ToTable("Sections"); + }); + + modelBuilder.Entity("DataModel.Database.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoolValue") + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("GuidValue") + .HasColumnType("TEXT"); + + b.Property("IntegerValue") + .HasColumnType("INTEGER"); + + b.Property("TextValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BoolValue"); + + b.HasIndex("Code") + .IsUnique(); + + b.HasIndex("GuidValue"); + + b.HasIndex("Id"); + + b.HasIndex("IntegerValue"); + + b.HasIndex("TextValue"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("IsMultiLine") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SectionId") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("Id"); + + b.HasIndex("IsMultiLine"); + + b.HasIndex("Name"); + + b.HasIndex("SectionId"); + + b.ToTable("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Culture") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextElementId") + .HasColumnType("INTEGER"); + + b.Property("TranslateManual") + .HasColumnType("INTEGER"); + + b.Property("UniqueId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Culture"); + + b.HasIndex("Id"); + + b.HasIndex("Text"); + + b.HasIndex("TextElementId"); + + b.HasIndex("TranslateManual"); + + b.ToTable("Translations"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.HasOne("DataModel.Database.Section", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.HasOne("DataModel.Database.Section", "Section") + .WithMany("TextElements") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.HasOne("DataModel.Database.TextElement", "TextElement") + .WithMany("Translations") + .HasForeignKey("TextElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TextElement"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Navigation("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Navigation("Translations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.cs b/I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.cs new file mode 100644 index 0000000..91ad0fd --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20221106193544_202211AddUniqueIds.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataModel.Migrations +{ + public partial class _202211AddUniqueIds : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UniqueId", + table: "Translations", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "UniqueId", + table: "TextElements", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "UniqueId", + table: "Settings", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "UniqueId", + table: "Sections", + type: "TEXT", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "UniqueId", + table: "Translations"); + + migrationBuilder.DropColumn( + name: "UniqueId", + table: "TextElements"); + + migrationBuilder.DropColumn( + name: "UniqueId", + table: "Settings"); + + migrationBuilder.DropColumn( + name: "UniqueId", + table: "Sections"); + } + } +} diff --git a/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs b/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs index 4a0c76a..595f6eb 100644 --- a/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs +++ b/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs @@ -37,6 +37,9 @@ namespace DataModel.Migrations b.Property("ParentId") .HasColumnType("INTEGER"); + b.Property("UniqueId") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.HasIndex("DataKey"); @@ -75,6 +78,9 @@ namespace DataModel.Migrations .IsRequired() .HasColumnType("TEXT"); + b.Property("UniqueId") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.HasIndex("BoolValue"); @@ -113,6 +119,9 @@ namespace DataModel.Migrations b.Property("SectionId") .HasColumnType("INTEGER"); + b.Property("UniqueId") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.HasIndex("Code"); @@ -148,6 +157,9 @@ namespace DataModel.Migrations b.Property("TranslateManual") .HasColumnType("INTEGER"); + b.Property("UniqueId") + .HasColumnType("TEXT"); + b.HasKey("Id"); b.HasIndex("Culture"); diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index 6144126..7fdf71f 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -1,4 +1,5 @@ using DataModel.Database.Common; +using DataModel.MigrationScripts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -27,6 +28,12 @@ public static class Setup } await dbContext.Database.MigrateAsync(); + + // + // Post migration actions: + // + if (pendingMigrations.Contains("20221106193544_202211AddUniqueIds")) + await Script202211AddUniqueIds.PostMigrationAsync(dbContext); } /// diff --git a/I18N Commander/Processor/Version.cs b/I18N Commander/Processor/Version.cs index 31affb9..4bff47f 100644 --- a/I18N Commander/Processor/Version.cs +++ b/I18N Commander/Processor/Version.cs @@ -2,5 +2,5 @@ public static class Version { - public static string Text => $"v0.7.0 (2022-11-06), .NET {Environment.Version}"; + public static string Text => $"v0.7.1 (2022-11-06), .NET {Environment.Version}"; } \ No newline at end of file From cdffce9bbdbc4d7452121d80642feed2ea9d410c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 14 Nov 2022 20:32:41 +0100 Subject: [PATCH 02/30] Implemented the JSON export --- .../DataModel/Database/Common/DataContext.cs | 97 ++++++++++++++++++- .../DataModel/Database/Common/IDataContext.cs | 2 + I18N Commander/DataModel/Database/Section.cs | 13 +++ I18N Commander/DataModel/Database/Setting.cs | 13 +++ .../DataModel/Database/TextElement.cs | 13 +++ .../DataModel/Database/Translation.cs | 15 ++- 6 files changed, 151 insertions(+), 2 deletions(-) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index c277cb1..98794ce 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -1,4 +1,6 @@ -using Microsoft.EntityFrameworkCore; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.EntityFrameworkCore; namespace DataModel.Database.Common; @@ -63,4 +65,97 @@ public sealed class DataContext : DbContext, IDataContext #endregion } + + #region Export and import data structures + + private readonly record struct JsonData( + IEnumerable Settings, + IEnumerable Sections, + IEnumerable TextElements, + IEnumerable Translations + ); + + internal readonly record struct JsonUniqueId(string Code, Guid UniqueId, string Prefix = "") + { + public override string ToString() => string.IsNullOrWhiteSpace(this.Prefix) ? $"{this.Code}::{this.UniqueId}" : $"{this.Prefix}::{this.Code}::{this.UniqueId}"; + + public static implicit operator string(JsonUniqueId id) => id.ToString(); + } + + private sealed class JsonUniqueIdConverter : JsonConverter + { + public override JsonUniqueId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var json = reader.GetString(); + var parts = json?.Split("::"); + return parts?.Length switch + { + 2 => new JsonUniqueId(parts[0], Guid.Parse(parts[1])), + 3 => new JsonUniqueId(parts[1], Guid.Parse(parts[2]), parts[0]), + + _ => throw new JsonException($"Invalid format of JsonUniqueId: {json}") + }; + } + + public override void Write(Utf8JsonWriter writer, JsonUniqueId value, JsonSerializerOptions options) + { + writer.WriteStringValue(value); + } + } + + internal readonly record struct JsonSetting( + JsonUniqueId UniqueId, + string Code, + string TextValue, + int IntegerValue, + bool BoolValue, + Guid GuidValue + ); + + internal readonly record struct JsonSection( + JsonUniqueId UniqueId, + string Name, + string DataKey, + int Depth, + JsonUniqueId ParentUniqueId, + List TextElements + ); + + internal readonly record struct JsonTextElement( + JsonUniqueId UniqueId, + string Code, + string Name, + bool IsMultiLine, + JsonUniqueId SectionUniqueId, + List Translations + ); + + internal readonly record struct JsonTranslation( + JsonUniqueId UniqueId, + string Culture, + string Text, + bool TranslateManual, + JsonUniqueId TextElementUniqueId + ); + + #endregion + + public async Task ExportAsync(string path) + { + var jsonSettings = new JsonSerializerOptions + { + WriteIndented = true, + Converters = { new JsonUniqueIdConverter() }, + }; + + await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); + await JsonSerializer.SerializeAsync(fileStream, + new JsonData + { + Settings = this.Settings.Select(n => n.ToJsonSetting()), + Sections = this.Sections.Select(n => n.ToJsonSection()), + TextElements = this.TextElements.Select(n => n.ToJsonTextElement()), + Translations = this.Translations.Select(n => n.ToJsonTranslation()), + }, jsonSettings); + } } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Common/IDataContext.cs b/I18N Commander/DataModel/Database/Common/IDataContext.cs index a590766..7888beb 100644 --- a/I18N Commander/DataModel/Database/Common/IDataContext.cs +++ b/I18N Commander/DataModel/Database/Common/IDataContext.cs @@ -11,4 +11,6 @@ public interface IDataContext public DbSet TextElements { get; set; } public DbSet Translations { get; set; } + + public Task ExportAsync(string path); } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Section.cs b/I18N Commander/DataModel/Database/Section.cs index bb3a2c1..23cbf8f 100644 --- a/I18N Commander/DataModel/Database/Section.cs +++ b/I18N Commander/DataModel/Database/Section.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using DataModel.Database.Common; namespace DataModel.Database; @@ -18,4 +19,16 @@ public sealed class Section public Section? Parent { get; set; } public List TextElements { get; set; } = new(); + + internal DataContext.JsonUniqueId JsonUniqueId => new(this.DataKey, this.UniqueId, "Sec"); + + internal DataContext.JsonSection ToJsonSection() => new() + { + UniqueId = this.JsonUniqueId, + Name = this.Name, + DataKey = this.DataKey, + Depth = this.Depth, + ParentUniqueId = this.Parent?.JsonUniqueId ?? new DataContext.JsonUniqueId("null", Guid.Empty, "Sec"), + TextElements = this.TextElements.Select(n => n.JsonUniqueId).ToList() + }; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Setting.cs b/I18N Commander/DataModel/Database/Setting.cs index 8d0303a..4c5f1fc 100644 --- a/I18N Commander/DataModel/Database/Setting.cs +++ b/I18N Commander/DataModel/Database/Setting.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using DataModel.Database.Common; namespace DataModel.Database; @@ -18,4 +19,16 @@ public sealed class Setting public int IntegerValue { get; set; } public Guid GuidValue { get; set; } + + internal DataContext.JsonUniqueId JsonUniqueId => new(this.Code, this.UniqueId, "Set"); + + internal DataContext.JsonSetting ToJsonSetting() => new() + { + UniqueId = this.JsonUniqueId, + Code = this.Code, + BoolValue = this.BoolValue, + GuidValue = this.GuidValue, + IntegerValue = this.IntegerValue, + TextValue = this.TextValue, + }; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/TextElement.cs b/I18N Commander/DataModel/Database/TextElement.cs index e1c2154..adf3f86 100644 --- a/I18N Commander/DataModel/Database/TextElement.cs +++ b/I18N Commander/DataModel/Database/TextElement.cs @@ -1,4 +1,5 @@ using System.ComponentModel.DataAnnotations; +using DataModel.Database.Common; namespace DataModel.Database; @@ -18,4 +19,16 @@ public sealed class TextElement public Section Section { get; set; } public List Translations { get; set; } = new(); + + internal DataContext.JsonUniqueId JsonUniqueId => new(this.Code, this.UniqueId, "TXT"); + + internal DataContext.JsonTextElement ToJsonTextElement() => new() + { + UniqueId = this.JsonUniqueId, + Code = this.Code, + Name = this.Name, + IsMultiLine = this.IsMultiLine, + SectionUniqueId = this.Section.JsonUniqueId, + Translations = this.Translations.Select(n => n.JsonUniqueId).ToList(), + }; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Translation.cs b/I18N Commander/DataModel/Database/Translation.cs index b654f73..167d671 100644 --- a/I18N Commander/DataModel/Database/Translation.cs +++ b/I18N Commander/DataModel/Database/Translation.cs @@ -1,4 +1,6 @@ -namespace DataModel.Database; +using DataModel.Database.Common; + +namespace DataModel.Database; public sealed class Translation { @@ -13,4 +15,15 @@ public sealed class Translation public string Text { get; set; } = string.Empty; public bool TranslateManual { get; set; } = false; + + internal DataContext.JsonUniqueId JsonUniqueId => new($"{this.TextElement.Code}::{this.Culture}", this.UniqueId, "Trans"); + + internal DataContext.JsonTranslation ToJsonTranslation() => new() + { + UniqueId = this.JsonUniqueId, + Culture = this.Culture, + TranslateManual = this.TranslateManual, + TextElementUniqueId = this.TextElement.JsonUniqueId, + Text = this.Text, + }; } \ No newline at end of file From 7676bea101897cc9a5fee0e4a98464e2bd37410f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 2 Jan 2023 20:50:11 +0100 Subject: [PATCH 03/30] Moved export code into region --- I18N Commander/DataModel/Database/Common/DataContext.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 98794ce..8d9bac5 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -66,7 +66,7 @@ public sealed class DataContext : DbContext, IDataContext #endregion } - #region Export and import data structures + #region Export and import private readonly record struct JsonData( IEnumerable Settings, @@ -137,8 +137,6 @@ public sealed class DataContext : DbContext, IDataContext bool TranslateManual, JsonUniqueId TextElementUniqueId ); - - #endregion public async Task ExportAsync(string path) { @@ -158,4 +156,6 @@ public sealed class DataContext : DbContext, IDataContext Translations = this.Translations.Select(n => n.ToJsonTranslation()), }, jsonSettings); } + + #endregion } \ No newline at end of file From 79d8deb83726c5c2c70b6d9e83687bbe4f8e31d8 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Mon, 2 Jan 2023 20:56:06 +0100 Subject: [PATCH 04/30] Added method for starting the import process. --- I18N Commander/DataModel/Database/Common/DataContext.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 8d9bac5..b332add 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -157,5 +157,12 @@ public sealed class DataContext : DbContext, IDataContext }, jsonSettings); } + public static async Task ImportAndLoadAsync(string path) + { + // We import that JSON data file into an new, empty database file + // at a temporary location. Next, we enable the auto export feature + // to keep the source file up-to-date. + } + #endregion } \ No newline at end of file From 45e6188d2dc7f028eb2e0e37fc2c62df92ef4cc6 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 18 Jan 2023 19:57:05 +0100 Subject: [PATCH 05/30] Added abbreviations --- I18N Commander/I18N Commander.sln.DotSettings | 2 ++ 1 file changed, 2 insertions(+) diff --git a/I18N Commander/I18N Commander.sln.DotSettings b/I18N Commander/I18N Commander.sln.DotSettings index fe1a45a..886d922 100644 --- a/I18N Commander/I18N Commander.sln.DotSettings +++ b/I18N Commander/I18N Commander.sln.DotSettings @@ -1,2 +1,4 @@  + EF + JSON True \ No newline at end of file From b534dfdf851a9abd63bd6eb704a7d43fccd9f945 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 18 Jan 2023 19:59:18 +0100 Subject: [PATCH 06/30] Spelling --- I18N Commander/DataModel/Setup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index 7fdf71f..f2a3e06 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -17,7 +17,7 @@ public static class Setup public static string DataFile => Setup.usedDataFile; /// - /// Tries to migrate the data file. + /// Tries to migrate the database. /// public static async Task PerformDataMigration(DataContext dbContext) { @@ -35,7 +35,7 @@ public static class Setup if (pendingMigrations.Contains("20221106193544_202211AddUniqueIds")) await Script202211AddUniqueIds.PostMigrationAsync(dbContext); } - + /// /// Add the database to the DI system /// From f7186fd3ccfccaa83807919ad6ab8260a04cdf6c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 18 Jan 2023 20:00:03 +0100 Subject: [PATCH 07/30] Added documentation --- .../DataModel/Database/Common/DataContext.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index b332add..56284ab 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -75,6 +75,9 @@ public sealed class DataContext : DbContext, IDataContext IEnumerable Translations ); + /// + /// Represents a unique identifier for a JSON export and import. + /// internal readonly record struct JsonUniqueId(string Code, Guid UniqueId, string Prefix = "") { public override string ToString() => string.IsNullOrWhiteSpace(this.Prefix) ? $"{this.Code}::{this.UniqueId}" : $"{this.Prefix}::{this.Code}::{this.UniqueId}"; @@ -82,6 +85,9 @@ public sealed class DataContext : DbContext, IDataContext public static implicit operator string(JsonUniqueId id) => id.ToString(); } + /// + /// A JSON converter to serialize and deserialize JsonUniqueId instances. + /// private sealed class JsonUniqueIdConverter : JsonConverter { public override JsonUniqueId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -138,6 +144,10 @@ public sealed class DataContext : DbContext, IDataContext JsonUniqueId TextElementUniqueId ); + /// + /// Exports this database to a JSON file. + /// + /// The path to the JSON file. public async Task ExportAsync(string path) { var jsonSettings = new JsonSerializerOptions From 25b822aaed5ba8d77e12bd253b21b9722eacd5df Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 18 Jan 2023 20:00:25 +0100 Subject: [PATCH 08/30] Changed to IList<> --- .../DataModel/Database/Common/DataContext.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 56284ab..e63c80d 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -69,10 +69,10 @@ public sealed class DataContext : DbContext, IDataContext #region Export and import private readonly record struct JsonData( - IEnumerable Settings, - IEnumerable Sections, - IEnumerable TextElements, - IEnumerable Translations + IList Settings, + IList Sections, + IList TextElements, + IList Translations ); /// @@ -160,10 +160,10 @@ public sealed class DataContext : DbContext, IDataContext await JsonSerializer.SerializeAsync(fileStream, new JsonData { - Settings = this.Settings.Select(n => n.ToJsonSetting()), - Sections = this.Sections.Select(n => n.ToJsonSection()), - TextElements = this.TextElements.Select(n => n.ToJsonTextElement()), - Translations = this.Translations.Select(n => n.ToJsonTranslation()), + Settings = this.Settings.Select(n => n.ToJsonSetting()).ToList(), + Sections = this.Sections.Select(n => n.ToJsonSection()).ToList(), + TextElements = this.TextElements.Select(n => n.ToJsonTextElement()).ToList(), + Translations = this.Translations.Select(n => n.ToJsonTranslation()).ToList(), }, jsonSettings); } From 463c4611d18dd49cde5c6afd474eae450a205a01 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Wed, 18 Jan 2023 20:00:47 +0100 Subject: [PATCH 09/30] Removed not used factory class --- .../DataModel/Database/Common/DataContextFactory.cs | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 I18N Commander/DataModel/Database/Common/DataContextFactory.cs diff --git a/I18N Commander/DataModel/Database/Common/DataContextFactory.cs b/I18N Commander/DataModel/Database/Common/DataContextFactory.cs deleted file mode 100644 index 566624d..0000000 --- a/I18N Commander/DataModel/Database/Common/DataContextFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace DataModel.Database.Common; - -public sealed class DataContextFactory -{ - public IDataContext DataContext { get; private set; } - - public void CreateDataContext(string path2Database) - { - this.DataContext = Setup.CreateDatabaseInstance(path2Database, true); - } -} \ No newline at end of file From 5978614845c7c250180b3eefe91b5f0914f891f9 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 13:01:21 +0100 Subject: [PATCH 10/30] Added a Git icon --- .../UI WinForms/Resources/Icons.Designer.cs | 10 ++++++++++ I18N Commander/UI WinForms/Resources/Icons.resx | 3 +++ .../UI WinForms/Resources/icons8-git.svg.png | Bin 0 -> 7248 bytes 3 files changed, 13 insertions(+) create mode 100644 I18N Commander/UI WinForms/Resources/icons8-git.svg.png diff --git a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs index cf5126b..98d01fb 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs +++ b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs @@ -210,6 +210,16 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_git_svg { + get { + object obj = ResourceManager.GetObject("icons8_git_svg", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/I18N Commander/UI WinForms/Resources/Icons.resx b/I18N Commander/UI WinForms/Resources/Icons.resx index 8e6f5c8..ee2bfe1 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.resx +++ b/I18N Commander/UI WinForms/Resources/Icons.resx @@ -163,6 +163,9 @@ icons8-folder-tree-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-git.svg.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-increase-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/I18N Commander/UI WinForms/Resources/icons8-git.svg.png b/I18N Commander/UI WinForms/Resources/icons8-git.svg.png new file mode 100644 index 0000000000000000000000000000000000000000..206c1104f657fbe3f0050bc277a33f1526ff782c GIT binary patch literal 7248 zcmcgxdpy(a`~Pf6V?`^+C(&V)=8#e<**Yf`Ux%2Sa_k|Yoa({W6H_@pWD407a!A5c z(y(O}DThiChIOEW!=yCK_PeHU{r>#@_w#yrZJ+D#zTWrezVGY0?t2$(4((l_ps9c$ z$O6lKyKE5z4PVj7{CV*4EwW=8K2WD@_wGP$wJ!Yx53+vS4{k@0iWEiBQ8{?V1n(oC zLXd@A=|8G5sK^^Z{`g?IYrB1<+X#=C_=f|F!!N&{ziZXqv3bi2@();AX55KTS+b?s zezi|?=o&j)+roUQ6#i;nbhFm*rz0Ch7`-Rmr+f!Rr?mk z|NPk$Jl(7Jqq8*KsJzwBz09#{h_x){e(!8mRl3oA$^v9HS$(v#^pTRIUsXbB)gwmr zpLIFx8fQbdz}7MIdf{|c3X7s@_7sI7&u-t3s znZy(Hg?bCooMVR8V-`+Ee(c716PM#Aunk9)cqwuS7GEpM{VdD2cV!s3g%Xz`sok%p z-ufIzhHQlYQmqvblIJGIP>NE&i^7nc6B^O^2S<}wSRu$Nn-_w9KMX=$wtuO3hJZqX zW7Fd2G)tK@KD!NvAk{W6f13O#3K5P~$s(j(EnEr%k09c_xSu9R3>HYD=?Ieb`t6)4 z)6zicr@&Jvxr#@qy6R#{3l1T@!b&0_8goBQ5)Vi=DqP7V8I2VE&8I|y|3zP+HULvs zXozQ}>LpyFt5obEya7x|udxz-r7V()=Td|~)%%e^+X%)$HWoF4nv;)}d;+yGhD(_S zbz-xiO%K%9jYShrLCwTUq5#x*K9?u~^(r4>W;(=Lg_V2-ul5AK`w57Z>?zEYQaE?h zQP2JHyo=3J*X_5KwX%~IEXl}}YMjuYpGbuS)OT+TGN41Jo&kVhWh>G)7>n6?JTn*Y zmp;VVSOeffPfU@NhJ1x=hcw>*;jVBu8b`61OuzRNMXsBYDOqCu_GgSG-bB%jC|va% z5)%nhlB{7oG?=5&)Wwp{D0LbrHMPh8@Asxa>3sf_bMmAUM~Viz2R?R5=Z(Kf@#&f0wR~Ih}ly&u_mYO@Y6^|7Z&P{r!CJ@9@HS+utMV(*JGF{Ig)8 zbe(;N`PfkW?H4O*VTXz62N~S~@4Y>?n={-_NnPHdUF+PJB|$&4jz>o4#o zqpA9@EJQyJ-jThTS=G-Cn$8XyjearvZhL6Ca-@$3lYk;QIBSS!oFj4U(=3lktKrE~ zcRIpUs6NfO|JeC7&YdUYSUUBkE2KRH z=-t#1kChkbjT7r_T|D;ET2($IQJpf@+^Nw!LyOn2ybeSkMKrW zhF(aJ92VA#dK{m4poM_y>Cs@#euk~0eu+fS-cau{$w36;V$^bpVk(YNX z*AYd7&9y2l%wk4=())U-Yi?h3yVmGuX*NyPk|}^mUA2hDR; zD55L->!9ep#ItedClFVIX%A%Qiml&TFz42UAGiebAaJ8egmWo0p1qlWw=www4M^SDw349>S}JX zLN+uA+RC8lunH~5y{=VM$&x-`B_jVRJ7hZmWD!s{#ldx_xkR>e{X*&~ilbsyVCtI)CLXllv?gF7OQk~ z_RFc4UF-g{jErX5&?uYlOs`_qjr6ml36~t{$m&GC`{6I~3_}*{)*8pBZ^|p)<<(Zo zl9pp7=m3E^qKD8k@Q8_@F!Rls%`>wpV-}Jk)*smev(mImD#S`E14aU=rLRO`!)B|; z2&hyIE?e~a>6A5(N3P3m3=o;EhIE{Y;j(j`r#zX#tvg|}z$-Fd$7SEI9~r#`-q`61AJ_~8>z-mdnu$DFcdy~uQ5 z&-ORl9;RWeUrT2-_`+L3MC@+vTmn8u=a)aw^gub4AF7X<8LN;j3Zf%fUc!L(Jdez|2Q3ji(N4h?}oHQ>CUt&8m`3HQ>6xxy1h!w9-qsw$#M+7xRr+ijU*t zUqLVD@XHBLqkaWVUnj~4l=~IbBb3Ijyp{SZXrit;IgmcV@KV~fCPCrXC^!QR>GqM8 z7q=k=$Mui@0y?)&or5jI_-VmR@I{5q>)PqpjN2K&|}N;fYSx3Q98M8qRYpMdgT`B55@X@3DF2`{$_f zuV257o*Q-F=5%jc_Qckdhy?D*$-lmh{il+ETKY`j_}rlqi7j+P-=#>hjc*!hU+o)5>s@=88trY^AJVJB?jZR4J#k z;T6vKu5o_&&;lpfG(XJ1RV>B9E>4kDl!4djlCJELK(Tnliy1>k3zfL;4i2Bt%Km?1 z<~Kyw)U^abzHRiu3>X1e>03FSj&7Xtp(UYd9#DML#U)kEYE+v5D^jGD4z{eTLRzIX zfmpYu6SAYs0F_$>mG@YxB-K7FiGbw2rc;e?hO4c*xKvRq6VF`7_jjw1|J#1|0)si- zrXfAakk{WAj4xkuFhDu_>A62lKAUVLy7n=OdNFovFq!wgH;0Fdr4i0TBAS`VQ9xoVGwkr{MtFz7(_*WsKSCbb+BHiPRF_-s*5NV3z@e5Y*yIb9GcIjl~Zi}Jw%<9e-{NG-U zJEBjzQqgmTmUqSWI~ftz!Z9X{c*ZX3?V4z5(gD{ce2h-G-~LyADE0U1mpoXAdCD>+ zt$t}Oj$9L$DS35VmFn9nrX0C2{wS+9=kgm4X ze_pt*=2Y}?Q`dKjBw=65XyXan;mbvSN%aNxtNJs_3In20_uLF(KE@2|)(FYd4K*q} zK0bnvF%H+;f3QP{zCAJnK55oytnJH`;%jD$4@l#R|1s^|>?q0kuBQpM;@*SJke)FPWJ zISWc*8ElyO4@nz!;D{OH+AGKOi$iG~e-lLxgMUCcx;JQ#y8R*KK<~-%lC8Cud?#A! ztVQy9gO$>?6E9smQHuW4QKIr-egUUTYpp}JoJVK)6y^1-@L*Tp;8?!}bfxZy{n>0s zR21>9W-~h2&Pz@9B#PN_HR`#R^Zvn=XZ`gRIYj?=mc2h1`7J>bm72|)AD1Hjs}wo? zrUHFZWQ?LCF|jX3%f&2+5S3WFwYGzUG4a<`OY(fi!O?9UQkad!Ab_9zPqYYf2j6tVw=~^Fj;@@4tc<^*ARyi zP$e*EgDltBo(^8^isR6xhQDyXh^6#-Vr)#w$O+dqYSH5bzQrb@)ACl-k|uPp)6++# zE<~-{TYD&`6s_5sFb^70+XG`WPDU0Qx)>OXs{47pgxI4WYF0bYh}BSHpOcYf{aW_2 z*4s+#u!v26;rM^=cKd35h2M6TfYRTnNNV#{yA>~$jLe9ScAGFAd-Z&m85 z5pj?9fKz>7#Obdi4ScI;TR-8;bUf*i4{D#YFexhHL)}Qo!SB&%{%N0?A&WPYcEe-# z{a6YPT7e=0a!&P-dhfzfKmO&|hKZdKm#>ApcfXuvNwdK%J~5O=yZ~Vis8XMFN5A~& zFWy4HR7hNR`U%%7TTw4Ip_#htMH-j!q&I#jn^PL%#D_TM%_^j*7)#mXi?O*&Mmi4A zh>I&^S3e~n{wuf)K)d9sia0R%*17~#v%Vs=q*b7|o{spW%aLCB3e(qEQ7Zvff4%5Q z4xW?_uwfeFB@H;HCBT+oDGFYgc5gCbe~?CutCT&KMnFmcwt{2 zz1S+aVM0fe=yIe-zCt^I-SY&^T(MqM46x?_Hbg_b0bsWSY!Q~C39udj3v81BHkE+r z1MFj9YpzJGY!k$5Sy9u%EoMsoI)-1Idow8a-<1?bs11!MN`N-S8SZAv;pS-WM7S+K zjU~Mn>V$^0^uusxSDQ|>8CUxn0k!2{>3MWe)cS68`GzckJ~v!LxMp&_kME3dbX@6DXcm6NVJD54*o?CYs7k0*;U20Cun5T~kG zk>143++wXQ9YSx%`w4f_38)WruH#JdqZ*{ZSGe;vJgZo&bboh6GKRBQbla^pCPho} zLl$3Y_iforufkEnV%C=|{(S@w0saHV_7i(0eRvlw<8Z>IBQ)ZYCpeq^BikPI)h-zL z$~g@Yo)b_tn^+&=oOK-f$#;#rUV>1CfSOV@Onk{Z*iy5_y*5#6@xiNnC5H;x_vC~z z$=H;cN3w?425&{8Iefdrjz77=;{-gz3BniJ{Qig<=mUsn4s6&?BW`$rOSR6t5uqI# zVRfqEopJbq4dEbtgiGDakNG&7rVB^Y$Ik@ux@2@=8(NuYsJW7>o#l(Ue3!094PM9W zTK8OCL(HM$J?=ML5x}`MM{qAeYw-f0BRYro$l5p*u`D8D`>6&@YK7Oe&(OQ+_QNHFG6ZfVhiBY*)78vudgfQBQ+qEHCIAYf{o1B=my73*c(91xrD!sG3g&@xn6`yhT?NYLyXix@j6T+lAsb~{KHpDh1?HYRt5_#fjOAX zgNsQwFuM)~Dg{fSK8G?ANF#E*38*YxuC~WXC?MJ3w!n(|_=!NiDH%`VLR4?)AC8)c zP?g}`4Q`%J=u2SEG=t+bkX=p3lNy2461ZvQf?3H?O#6uR6Kc#1L@n{b*qDR)89Fk% z6+SiS2oJb#XvDRj0j^a9R2c->(1vSwrV$Y>E7Dq^So;WP^A|jh!KvSrt4&pcluG9- zaSX_4f1ox1>*NJso2B8I9q`)Va{3aeNh|17TL`@m)}Yi|1`-e~Ne%~c7BDo%S&<9@ zJn2=7hO5w128-c6QDg_+Zt(#LC`U8Rqa<9VteKPZt;^JW4Ns!QD2 z=sIUn@Wgnn(1YU0lJjKkleU!$8Rj3w`w-?TzRwKL@?7bgiT2U!v2eZH9^NJS)=BJN tDEj{S{nJs&g8!c%#vlrFo@Az9BBKzbG+-N}W+shmx%<$rTRS}d{vT;+EGYm0 literal 0 HcmV?d00001 From a19720f98d4dabfb80c65e1740cc94c317f72ad4 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 13:02:08 +0100 Subject: [PATCH 11/30] Added auto-export settings --- I18N Commander/Processor/AppSettings.cs | 36 +++++ .../UI WinForms/Components/Setting.cs | 147 ++++++++++++++++++ 2 files changed, 183 insertions(+) diff --git a/I18N Commander/Processor/AppSettings.cs b/I18N Commander/Processor/AppSettings.cs index 5c177ae..8987196 100644 --- a/I18N Commander/Processor/AppSettings.cs +++ b/I18N Commander/Processor/AppSettings.cs @@ -418,4 +418,40 @@ public static class AppSettings #endregion #endregion + + #region Auto-Export Settings + + #region Auto-Export Enabled/Disabled + + public static async Task GetAutoExportEnabled() => await AppSettings.GetSetting(SettingNames.AUTO_EXPORT_ENABLED, false); + + public static async Task SetAutoExportEnabled(bool enabled) => await AppSettings.SetSetting(SettingNames.AUTO_EXPORT_ENABLED, enabled); + + #endregion + + #region Auto-Export Destination Path + + public static async Task GetAutoExportDestinationPath() => await AppSettings.GetSetting(SettingNames.AUTO_EXPORT_DESTINATION_PATH, string.Empty); + + public static async Task SetAutoExportDestinationPath(string path) => await AppSettings.SetSetting(SettingNames.AUTO_EXPORT_DESTINATION_PATH, path); + + #endregion + + #region Auto-Export Filename + + public static async Task GetAutoExportFilename() => await AppSettings.GetSetting(SettingNames.AUTO_EXPORT_FILENAME, "I18NCommander.json"); + + public static async Task SetAutoExportFilename(string filename) => await AppSettings.SetSetting(SettingNames.AUTO_EXPORT_FILENAME, filename); + + #endregion + + #region Auto-Export Sensitive Data + + public static async Task GetAutoExportSensitiveData() => await AppSettings.GetSetting(SettingNames.AUTO_EXPORT_SENSITIVE_DATA, false); + + public static async Task SetAutoExportSensitiveData(bool enabled) => await AppSettings.SetSetting(SettingNames.AUTO_EXPORT_SENSITIVE_DATA, enabled); + + #endregion + + #endregion } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/Setting.cs b/I18N Commander/UI WinForms/Components/Setting.cs index 0d248b0..5256be2 100644 --- a/I18N Commander/UI WinForms/Components/Setting.cs +++ b/I18N Commander/UI WinForms/Components/Setting.cs @@ -685,9 +685,155 @@ public sealed partial class Setting : UserControl return new Setting(settingData); } + + private static async Task ShowGeneratorAutoExportEnabledSettingAsync() + { + var currentSetting = await AppSettings.GetAutoExportEnabled(); + var settingData = new SettingUIData( + Icon: Icons.icons8_git_svg, + SettingName: () => "Git (JSON) Auto-Export: Enabled", + ChangeNeedsRestart: true, + SettingExplanation: () => "When enabled, all changes are automatically exported into a Git repository as JSON file.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // Set up an checkbox: + var checkbox = new CheckBox(); + checkbox.Checked = currentSetting; + checkbox.CheckedChanged += async (sender, args) => await AppSettings.SetAutoExportEnabled(checkbox.Checked); + checkbox.CheckedChanged += (sender, args) => changeTrigger(); + checkbox.Text = "Enable Git (JSON) Auto-Export"; + + // Apply the desired layout: + checkbox.Dock = DockStyle.Fill; + return checkbox; + } + ); + + return new Setting(settingData); + } + + private static async Task ShowGeneratorAutoExportDestinationPathSettingAsync() + { + var currentSetting = await AppSettings.GetAutoExportDestinationPath(); + var settingData = new SettingUIData( + Icon: Icons.icons8_git_svg, + SettingName: () => "Git (JSON) Auto-Export: Destination Path", + ChangeNeedsRestart: true, + SettingExplanation: () => "The destination path for the Git repository. You might use environment variables like %USERPROFILE%.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // Set up a horizontal layout: + var layout = new TableLayoutPanel(); + layout.ColumnCount = 2; + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100F)); + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 66F)); + layout.RowCount = 1; + layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100F)); + layout.Dock = DockStyle.Fill; + + // Set up a textbox: + var textbox = new TextBox(); + textbox.Text = currentSetting; + textbox.TextChanged += async (sender, args) => await AppSettings.SetAutoExportDestinationPath(textbox.Text); + textbox.TextChanged += (sender, args) => changeTrigger(); + textbox.Dock = DockStyle.Fill; + textbox.Margin = new Padding(0, 13, 0, 13); + layout.Controls.Add(textbox, 0, 0); + + // Set up a button: + var button = new Button(); + button.Text = string.Empty; + button.Image = Icons.icons8_folder_tree_512; + button.FlatStyle = FlatStyle.Flat; + button.FlatAppearance.BorderSize = 0; + button.BackColor = Color.Empty; + button.UseVisualStyleBackColor = true; + button.Size = new Size(60, 60); + button.Click += (sender, args) => + { + var dialog = new FolderBrowserDialog(); + dialog.SelectedPath = textbox.Text; + dialog.InitialDirectory = textbox.Text; + dialog.Description = "Select the destination path for the Git repository."; + dialog.ShowNewFolderButton = true; + if (dialog.ShowDialog() == DialogResult.OK) + textbox.Text = dialog.SelectedPath; + }; + button.Dock = DockStyle.Fill; + layout.Controls.Add(button, 1, 0); + + return layout; + } + ); + + return new Setting(settingData); + } + + private static async Task ShowGeneratorAutoExportFilenameSettingAsync() + { + var currentSetting = await AppSettings.GetAutoExportFilename(); + var settingData = new SettingUIData( + Icon: Icons.icons8_git_svg, + SettingName: () => "Git (JSON) Auto-Export: Filename", + ChangeNeedsRestart: true, + SettingExplanation: () => "The filename used for the Git export. You might use environment variables like %USERPROFILE%.", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + var textbox = new TextBox(); + textbox.Text = currentSetting; + textbox.TextChanged += async (sender, args) => await AppSettings.SetAutoExportFilename(textbox.Text); + textbox.TextChanged += (sender, args) => changeTrigger(); + textbox.Dock = DockStyle.Fill; + textbox.Margin = new Padding(0, 13, 0, 13); + + return textbox; + } + ); + + return new Setting(settingData); + } + + private static async Task ShowGeneratorAutoExportExportSensitiveDataSettingAsync() + { + var currentSetting = await AppSettings.GetAutoExportSensitiveData(); + var settingData = new SettingUIData( + Icon: Icons.icons8_git_svg, + SettingName: () => "Git (JSON) Auto-Export: Export Sensitive Data", + ChangeNeedsRestart: true, + SettingExplanation: () => "When enabled, sensitive data like API tokens are exported into the Git repository. This is not recommended!", + SettingExplanationLink: () => (string.Empty, string.Empty), + SetupDataControl: (changeTrigger) => + { + // Set up an checkbox: + var checkbox = new CheckBox(); + checkbox.Checked = currentSetting; + checkbox.CheckedChanged += async (sender, args) => await AppSettings.SetAutoExportSensitiveData(checkbox.Checked); + checkbox.CheckedChanged += (sender, args) => changeTrigger(); + checkbox.Text = "Export Sensitive Data"; + + // Apply the desired layout: + checkbox.Dock = DockStyle.Fill; + return checkbox; + } + ); + + return new Setting(settingData); + } public static IEnumerable> GetAllSettings() { + // + // Remember: The reverse order is the order in the UI! + // + + yield return ShowGeneratorAutoExportFilenameSettingAsync(); + yield return ShowGeneratorAutoExportDestinationPathSettingAsync(); + yield return ShowGeneratorAutoExportExportSensitiveDataSettingAsync(); + yield return ShowGeneratorAutoExportEnabledSettingAsync(); + yield return ShowGeneratorGodotDestinationPathSettingAsync(); yield return ShowGeneratorGodotEnabledSettingAsync(); yield return ShowGeneratorDotnetDefaultCultureSettingAsync(); @@ -695,6 +841,7 @@ public sealed partial class Setting : UserControl yield return ShowGeneratorDotnetDestinationPathSettingAsync(); yield return ShowGeneratorDotnetEnabledSettingAsync(); yield return ShowGeneratorModeSettingAsync(); + yield return ShowDeepLSourceCultureSettingAsync(); foreach (var setting in ShowCultureSettingsAsync()) yield return setting; From 7a742a18e7e59af1aca7f274562ae9f2ef1d340f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 14:20:40 +0100 Subject: [PATCH 12/30] Spelling --- I18N Commander/UI WinForms/AppEvents.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/I18N Commander/UI WinForms/AppEvents.cs b/I18N Commander/UI WinForms/AppEvents.cs index 1145e8e..16297c4 100644 --- a/I18N Commander/UI WinForms/AppEvents.cs +++ b/I18N Commander/UI WinForms/AppEvents.cs @@ -12,7 +12,7 @@ internal static class AppEvents WhenTranslationChanged = null; } - #region Event: Settings were + #region Event: Settings changed internal static event EventHandler? WhenSettingsChanged; From 0587af18f28b0ec304a35aeaf0d40d376d46ebec Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 19:35:57 +0100 Subject: [PATCH 13/30] Added Git Export Feature - Added import algorithm - Added FromJsonX() methods to data models - Added possibility to work with temp. database file - Added export processor to handle export process & triggers - Added something changed event e.g. as export trigger - Added possibility to import a JSON export - Updated Git icon --- I18N Commander/DataModel/DataModel.csproj | 2 +- .../DataModel/Database/Common/DataContext.cs | 142 +++++++++++++++++- I18N Commander/DataModel/Database/Section.cs | 10 ++ I18N Commander/DataModel/Database/Setting.cs | 10 ++ .../DataModel/Database/SettingNames.cs | 4 + .../DataModel/Database/TextElement.cs | 10 ++ .../DataModel/Database/Translation.cs | 11 +- I18N Commander/DataModel/Setup.cs | 87 ++++++++--- I18N Commander/Processor/ExportProcessor.cs | 46 ++++++ I18N Commander/Processor/Version.cs | 2 +- I18N Commander/UI WinForms/AppEvents.cs | 23 +++ .../UI WinForms/Components/LoaderStart.cs | 53 +++++-- I18N Commander/UI WinForms/Components/Main.cs | 7 +- .../UI WinForms/Components/Setting.cs | 18 ++- I18N Commander/UI WinForms/Loader.Designer.cs | 6 +- I18N Commander/UI WinForms/Loader.cs | 10 +- I18N Commander/UI WinForms/Program.cs | 24 ++- .../UI WinForms/Resources/Icons.Designer.cs | 4 +- .../UI WinForms/Resources/Icons.resx | 4 +- .../UI WinForms/Resources/icons8-git.png | Bin 0 -> 2786 bytes 20 files changed, 411 insertions(+), 62 deletions(-) create mode 100644 I18N Commander/Processor/ExportProcessor.cs create mode 100644 I18N Commander/UI WinForms/Resources/icons8-git.png diff --git a/I18N Commander/DataModel/DataModel.csproj b/I18N Commander/DataModel/DataModel.csproj index 2b1e1d6..4210f08 100644 --- a/I18N Commander/DataModel/DataModel.csproj +++ b/I18N Commander/DataModel/DataModel.csproj @@ -1,7 +1,7 @@ - Exe + Library net6.0 enable enable diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index e63c80d..58872c3 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -167,11 +167,145 @@ public sealed class DataContext : DbContext, IDataContext }, jsonSettings); } - public static async Task ImportAndLoadAsync(string path) + /// + /// Stores data needed to resolve a parent-child relationship. + /// + /// The parent id we want to resolve. + /// The entity for which we want to resolve the parent. + /// The type of the entity. + private readonly record struct TreeResolver(Guid ParentId, T Entity); + + /// + /// Imports data from a JSON file into an empty database. + /// + /// The path to the JSON export. + /// When the database is not empty. + public async Task ImportAsync(string path) { - // We import that JSON data file into an new, empty database file - // at a temporary location. Next, we enable the auto export feature - // to keep the source file up-to-date. + if(await this.Settings.AnyAsync() || + await this.Sections.AnyAsync() || + await this.TextElements.AnyAsync() || + await this.Translations.AnyAsync()) + throw new InvalidOperationException("The database is not empty. In order to import data, the database must be empty."); + + // Start a transaction: + await using var transaction = await this.Database.BeginTransactionAsync(); + + // Configure the JSON serializer: + var jsonSettings = new JsonSerializerOptions + { + Converters = { new JsonUniqueIdConverter() }, + }; + + await using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); + var jsonData = await JsonSerializer.DeserializeAsync(fileStream, jsonSettings); + + // -------------------- + // Import the settings: + // -------------------- + foreach (var setting in jsonData.Settings) + this.Settings.Add(Setting.FromJsonSetting(setting)); + + // -------------------- + // Import the sections: + // -------------------- + + // We must store the intermediate data in a list, because we need to resolve + // the parent-child relationships in a second step. + var allSections = new Dictionary>(); + var sectionToTextElements = new Dictionary>(); + + // Read the data from the JSON file: + foreach (var section in jsonData.Sections) + { + // Convert the next element: + var nextSection = Section.FromJsonSection(section); + + // Store the element: + allSections.Add(nextSection.UniqueId, new (section.ParentUniqueId.UniqueId, nextSection)); + sectionToTextElements.Add(nextSection.UniqueId, section.TextElements.Select(n => n.UniqueId).ToList()); + } + + // Now, resolve the parent-child relationships for the sections: + foreach (var (uniqueId, (parentId, section)) in allSections) + section.Parent = parentId == Guid.Empty ? null : allSections[parentId].Entity; + + // ------------------------- + // Import the text elements: + // ------------------------- + + // We must store the intermediate data in a list, because we need to resolve + // the parent-child relationships in a second step. + var allTextElements = new Dictionary(); + var textElementToTranslations = new Dictionary>(); + + // Read the data from the JSON file: + foreach (var textElement in jsonData.TextElements) + { + // Convert the next element: + var nextTextElement = TextElement.FromJsonTextElement(textElement); + + // We know that the section is already imported, because we imported the sections first: + nextTextElement.Section = allSections[textElement.SectionUniqueId.UniqueId].Entity; + + // Store the element in the list: + allTextElements.Add(nextTextElement.UniqueId, nextTextElement); + textElementToTranslations.Add(nextTextElement.UniqueId, textElement.Translations.Select(n => n.UniqueId).ToList()); + } + + // Now, resolve the parent-child relationships for the text elements to the sections: + foreach (var (sectionUniqueId, textElementsIds) in sectionToTextElements) + { + var section = allSections[sectionUniqueId].Entity; + section.TextElements.AddRange(textElementsIds.Select(n => allTextElements[n])); + } + + // Free the memory: + sectionToTextElements.Clear(); + + // ------------------------ + // Import the translations: + // ------------------------ + + // We must store the intermediate data in a list, because we need to resolve + // the parent-child relationships in a second step. + var allTranslations = new Dictionary(); + + // Read the data from the JSON file: + foreach (var translation in jsonData.Translations) + { + // Convert the next element: + var nextTranslation = Translation.FromJsonTranslation(translation); + + // We know that the text element is already imported, because we imported the text elements first: + nextTranslation.TextElement = allTextElements[translation.TextElementUniqueId.UniqueId]; + + // Store the element in the list: + allTranslations.Add(nextTranslation.UniqueId, nextTranslation); + } + + // Now, resolve the parent-child relationships for the translations to the text elements: + foreach (var (textElementUniqueId, translationsIds) in textElementToTranslations) + { + var textElement = allTextElements[textElementUniqueId]; + textElement.Translations.AddRange(translationsIds.Select(n => allTranslations[n])); + } + + // Free the memory: + textElementToTranslations.Clear(); + + // --------------------------------- + // Add all the data to the database: + // --------------------------------- + this.Sections.AddRange(allSections.Values.Select(n => n.Entity)); + this.TextElements.AddRange(allTextElements.Values); + this.Translations.AddRange(allTranslations.Values); + + // Save the changes: + await this.SaveChangesAsync(); + + // Commit the transaction: + await transaction.CommitAsync(); } #endregion diff --git a/I18N Commander/DataModel/Database/Section.cs b/I18N Commander/DataModel/Database/Section.cs index 23cbf8f..c963a40 100644 --- a/I18N Commander/DataModel/Database/Section.cs +++ b/I18N Commander/DataModel/Database/Section.cs @@ -31,4 +31,14 @@ public sealed class Section ParentUniqueId = this.Parent?.JsonUniqueId ?? new DataContext.JsonUniqueId("null", Guid.Empty, "Sec"), TextElements = this.TextElements.Select(n => n.JsonUniqueId).ToList() }; + + internal static Section FromJsonSection(DataContext.JsonSection jsonSection) => new() + { + UniqueId = jsonSection.UniqueId.UniqueId, + Name = jsonSection.Name, + DataKey = jsonSection.DataKey, + Depth = jsonSection.Depth, + Parent = null, + TextElements = new(), + }; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Setting.cs b/I18N Commander/DataModel/Database/Setting.cs index 4c5f1fc..5c5977b 100644 --- a/I18N Commander/DataModel/Database/Setting.cs +++ b/I18N Commander/DataModel/Database/Setting.cs @@ -31,4 +31,14 @@ public sealed class Setting IntegerValue = this.IntegerValue, TextValue = this.TextValue, }; + + internal static Setting FromJsonSetting(DataContext.JsonSetting jsonSetting) => new() + { + UniqueId = jsonSetting.UniqueId.UniqueId, + Code = jsonSetting.Code, + BoolValue = jsonSetting.BoolValue, + GuidValue = jsonSetting.GuidValue, + IntegerValue = jsonSetting.IntegerValue, + TextValue = jsonSetting.TextValue, + }; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/SettingNames.cs b/I18N Commander/DataModel/Database/SettingNames.cs index 7d419cf..040b428 100644 --- a/I18N Commander/DataModel/Database/SettingNames.cs +++ b/I18N Commander/DataModel/Database/SettingNames.cs @@ -14,4 +14,8 @@ public static class SettingNames public static readonly string GENERATOR_DOTNET_DEFAULT_CULTURE = "Generator .NET Default Culture"; public static readonly string GENERATOR_GODOT_ENABLED = "Generator Godot Enabled"; public static readonly string GENERATOR_GODOT_DESTINATION_PATH = "Generator Godot Destination Path"; + public static readonly string AUTO_EXPORT_ENABLED = "Auto-Export Enabled"; + public static readonly string AUTO_EXPORT_DESTINATION_PATH = "Auto-Export Destination Path"; + public static readonly string AUTO_EXPORT_FILENAME = "Auto-Export Filename"; + public static readonly string AUTO_EXPORT_SENSITIVE_DATA = "Auto-Export Sensitive Data"; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/TextElement.cs b/I18N Commander/DataModel/Database/TextElement.cs index adf3f86..7870238 100644 --- a/I18N Commander/DataModel/Database/TextElement.cs +++ b/I18N Commander/DataModel/Database/TextElement.cs @@ -31,4 +31,14 @@ public sealed class TextElement SectionUniqueId = this.Section.JsonUniqueId, Translations = this.Translations.Select(n => n.JsonUniqueId).ToList(), }; + + internal static TextElement FromJsonTextElement(DataContext.JsonTextElement jsonTextElement) => new() + { + UniqueId = jsonTextElement.UniqueId.UniqueId, + Code = jsonTextElement.Code, + Name = jsonTextElement.Name, + IsMultiLine = jsonTextElement.IsMultiLine, + Section = new(), + Translations = new(), + }; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Translation.cs b/I18N Commander/DataModel/Database/Translation.cs index 167d671..d9f428c 100644 --- a/I18N Commander/DataModel/Database/Translation.cs +++ b/I18N Commander/DataModel/Database/Translation.cs @@ -8,7 +8,7 @@ public sealed class Translation public Guid UniqueId { get; set; } - public TextElement TextElement { get; set; } + public TextElement TextElement { get; set; } = new(); public string Culture { get; set; } = "en-US"; @@ -26,4 +26,13 @@ public sealed class Translation TextElementUniqueId = this.TextElement.JsonUniqueId, Text = this.Text, }; + + internal static Translation FromJsonTranslation(DataContext.JsonTranslation jsonTranslation) => new() + { + UniqueId = jsonTranslation.UniqueId.UniqueId, + Culture = jsonTranslation.Culture, + TranslateManual = jsonTranslation.TranslateManual, + Text = jsonTranslation.Text, + TextElement = new(), + }; } \ No newline at end of file diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index f2a3e06..e687af1 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -1,4 +1,5 @@ -using DataModel.Database.Common; +using DataModel.Database; +using DataModel.Database.Common; using DataModel.MigrationScripts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -12,9 +13,12 @@ public static class Setup private const string DB_READ_WRITE_MODE = "ReadWrite"; private const string DB_READ_WRITE_CREATE_MODE = "ReadWriteCreate"; - private static string usedDataFile = string.Empty; - - public static string DataFile => Setup.usedDataFile; + private static string USED_DATA_FILE = string.Empty; + private static SetupMaintenance SETUP_MAINTENANCE = new(); + + public static string DataFile => Setup.USED_DATA_FILE; + + public static SetupMaintenance Maintenance => Setup.SETUP_MAINTENANCE; /// /// Tries to migrate the database. @@ -37,31 +41,52 @@ public static class Setup } /// - /// Add the database to the DI system + /// Imports a JSON file into a new database. /// - public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true) + public static async Task ImportDataAndAddDatabase(this IServiceCollection serviceCollection, string path2JSONFile) { - Setup.usedDataFile = path2DataFile; - serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient); + var tempPath = Path.GetTempFileName(); + + Setup.USED_DATA_FILE = tempPath; + Setup.SETUP_MAINTENANCE = new(path2JSONFile, true); + serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={tempPath};Mode={DB_READ_WRITE_CREATE_MODE};"), ServiceLifetime.Transient); + + // Get the database service: + await using var serviceProvider = serviceCollection.BuildServiceProvider(); + var dbContext = serviceProvider.GetRequiredService(); + + // Next, we import the data from the provided JSON file: + await dbContext.ImportAsync(path2JSONFile); + + // + // Next, we enable the auto-export feature to keep the source file up to date. + // The auto-export feature might exist, but we enforce it, when we work with a + // temporary database source by a JSON file. + // + + // Enable the auto-export feature: + var autoExportEnabled = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_ENABLED); + autoExportEnabled.BoolValue = true; + + // Set the auto-export path and file: + var autoExportPath = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_DESTINATION_PATH); + autoExportPath.TextValue = Path.GetDirectoryName(path2JSONFile) ?? string.Empty; + + var autoExportFile = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_FILENAME); + autoExportFile.TextValue = Path.GetFileName(path2JSONFile); + + // Save the changes: + await dbContext.SaveChangesAsync(); } /// - /// Create the database instance from the given path. + /// Creates and adds the database instance to the DI system (extension method). /// - public static DataContext CreateDatabaseInstance(string path2DataFile, bool createWhenNecessary = true) + public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true) { - // Store the path to the database: - Setup.usedDataFile = path2DataFile; - - // Create a database builder: - var builder = new DbContextOptionsBuilder(); - - // Add the database configuration to the builder: - builder.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"); - - // Next, construct the database context: - var dbContext = new DataContext(builder.Options); - return dbContext; + Setup.USED_DATA_FILE = path2DataFile; + Setup.SETUP_MAINTENANCE = new(path2DataFile, false); + serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient); } /// @@ -84,4 +109,22 @@ public static class Setup return builder; } + + public readonly record struct SetupMaintenance(string PathToDataFile = "", bool RemoveTempDatabaseAfterwards = false) : IDisposable + { + public void Dispose() + { + if (!this.RemoveTempDatabaseAfterwards) + return; + + try + { + File.Delete(this.PathToDataFile); + } + catch(Exception e) + { + Console.WriteLine($"Failed to remove the temporary database file: {e.Message} // {e.InnerException?.Message}"); + } + } + } } diff --git a/I18N Commander/Processor/ExportProcessor.cs b/I18N Commander/Processor/ExportProcessor.cs new file mode 100644 index 0000000..371f24c --- /dev/null +++ b/I18N Commander/Processor/ExportProcessor.cs @@ -0,0 +1,46 @@ +using DataModel.Database.Common; +using Microsoft.Extensions.DependencyInjection; +using Timer = System.Timers.Timer; + +namespace Processor; + +public static class ExportProcessor +{ + private static readonly Timer EXPORT_TIMER = new(); + private static readonly object EXPORT_LOCK = new(); + private static readonly SemaphoreSlim EXPORT_SEMAPHORE = new(2); + + static ExportProcessor() + { + EXPORT_TIMER.Interval = 6_000; // 6 seconds + EXPORT_TIMER.AutoReset = false; + EXPORT_TIMER.Elapsed += async (sender, args) => await ExportToJson(); + } + + private static async Task ExportToJson() + { + if(!await EXPORT_SEMAPHORE.WaitAsync(1)) + return; + + Monitor.Enter(EXPORT_LOCK); + try + { + await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); + await db.ExportAsync(Environment.ExpandEnvironmentVariables(Path.Join(await AppSettings.GetAutoExportDestinationPath(), await AppSettings.GetAutoExportFilename()))); + } + finally + { + Monitor.Exit(EXPORT_LOCK); + EXPORT_SEMAPHORE.Release(); + } + } + + public static async Task TriggerExport() + { + if (!await AppSettings.GetAutoExportEnabled()) + return; + + EXPORT_TIMER.Stop(); + EXPORT_TIMER.Start(); + } +} \ No newline at end of file diff --git a/I18N Commander/Processor/Version.cs b/I18N Commander/Processor/Version.cs index 4bff47f..62de691 100644 --- a/I18N Commander/Processor/Version.cs +++ b/I18N Commander/Processor/Version.cs @@ -2,5 +2,5 @@ public static class Version { - public static string Text => $"v0.7.1 (2022-11-06), .NET {Environment.Version}"; + public static string Text => $"v0.8.0 (2023-01-22), .NET {Environment.Version}"; } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/AppEvents.cs b/I18N Commander/UI WinForms/AppEvents.cs index 16297c4..b86f556 100644 --- a/I18N Commander/UI WinForms/AppEvents.cs +++ b/I18N Commander/UI WinForms/AppEvents.cs @@ -4,12 +4,19 @@ namespace UI_WinForms; internal static class AppEvents { + static AppEvents() + { + AppEvents.AddSomethingHandlers(); + } + internal static void ResetAllSubscriptions() { WhenSettingsChanged = null; WhenSectionChanged = null; WhenTextElementChanged = null; WhenTranslationChanged = null; + + AppEvents.AddSomethingHandlers(); } #region Event: Settings changed @@ -49,4 +56,20 @@ internal static class AppEvents internal static void TranslationChanged(Translation? translation) => WhenTranslationChanged?.Invoke(null, translation); #endregion + + #region Event: Something was changed + + internal static event EventHandler? WhenSomethingChanged; + + internal static void SomethingChanged() => WhenSomethingChanged?.Invoke(null, EventArgs.Empty); + + private static void AddSomethingHandlers() + { + // Raise the something event when any of the other change-events are raised: + WhenSectionChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty); + WhenTextElementChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty); + WhenTranslationChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty); + } + + #endregion } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/LoaderStart.cs b/I18N Commander/UI WinForms/Components/LoaderStart.cs index 523bc35..ce2b358 100644 --- a/I18N Commander/UI WinForms/Components/LoaderStart.cs +++ b/I18N Commander/UI WinForms/Components/LoaderStart.cs @@ -62,6 +62,7 @@ public partial class LoaderStart : UserControl #endregion + // Opens the recent projects dropdown menu. private void buttonOpen_Click(object sender, EventArgs e) { if(this.DesignMode) @@ -84,9 +85,19 @@ public partial class LoaderStart : UserControl // Split the file's path into each folder's name: var folderNames = fileInfo.DirectoryName!.Split(Path.DirectorySeparatorChar); - // Render this entry: - var item = this.contextMenuRecentProjects.Items.Add($"{folderNames.Last()}: {fileInfo.Name}", Resources.Icons.icons8_document_512, (innerSender, args) => this.OpenRecentProject(innerSender)); - item.Tag = recentProject; + // Distinguish between I18N Commander projects and JSON imports: + if (fileInfo.Extension == ".i18nc") + { + // Render this entry: + var item = this.contextMenuRecentProjects.Items.Add($"{folderNames.Last()}: {fileInfo.Name}", Resources.Icons.icons8_document_512, (innerSender, args) => this.OpenRecentProject(innerSender, LoaderAction.LOAD_PROJECT)); + item.Tag = recentProject; + } + else if (fileInfo.Extension == ".json") + { + // Render this entry: + var item = this.contextMenuRecentProjects.Items.Add($"{folderNames.Last()}: {fileInfo.Name}", Resources.Icons.icons8_git, (innerSender, args) => this.OpenRecentProject(innerSender, LoaderAction.IMPORT_JSON)); + item.Tag = recentProject; + } } var button = (sender as Button)!; @@ -125,7 +136,7 @@ public partial class LoaderStart : UserControl File.Delete(destinationFilePath); LoaderStart.UpdateRecentProjectsWithPruning(destinationFilePath); - this.OpenProject(destinationFilePath); + this.OpenProject(LoaderAction.CREATE_NEW_PROJECT, destinationFilePath); } private void BrowseForProject() @@ -137,10 +148,12 @@ public partial class LoaderStart : UserControl CheckFileExists = true, DereferenceLinks = true, DefaultExt = ".i18nc", - Filter = "I18N Commander Files (*.i18nc)|*.i18nc", + + // I18N Commander files (*.i18nc) or JSON files (*.json): + Filter = "I18N Commander Files (*.i18nc)|*.i18nc|JSON Files (*.json)|*.json", Multiselect = false, RestoreDirectory = true, - Title = "Open an I18N Commander file", + Title = "Open an I18N Commander file or Import a JSON file", }; var dialogResult = openDialog.ShowDialog(this); if (dialogResult != DialogResult.OK) @@ -148,23 +161,28 @@ public partial class LoaderStart : UserControl var projectFilePath = openDialog.FileName; LoaderStart.UpdateRecentProjectsWithPruning(projectFilePath); - this.OpenProject(projectFilePath); + + // Check, if the user chose an I18N Commander file or a JSON file: + if (projectFilePath.ToLowerInvariant().EndsWith(".i18nc", StringComparison.InvariantCulture)) + this.OpenProject(LoaderAction.LOAD_PROJECT, projectFilePath); + else if (projectFilePath.ToLowerInvariant().EndsWith(".json", StringComparison.InvariantCulture)) + this.OpenProject(LoaderAction.IMPORT_JSON, projectFilePath); } - private void OpenRecentProject(object? sender) + private void OpenRecentProject(object? sender, LoaderAction action) { if (sender is not ToolStripItem item) return; var path = (item.Tag as string)!; LoaderStart.UpdateRecentProjectsWithPruning(path); - this.OpenProject(path); + this.OpenProject(action, path); } - private void OpenProject(string path) + private void OpenProject(LoaderAction action, string path) { // Hint: the project file might or might not exist (new project vs. recent project) - this.LoadProject?.Invoke(this, path); + this.LoadProject?.Invoke(this, new LoaderResult(action, path)); } private void contextMenuRecentProjects_Closing(object sender, ToolStripDropDownClosingEventArgs e) @@ -173,5 +191,16 @@ public partial class LoaderStart : UserControl } [Category("Settings"), Description("When the user chooses a project to load.")] - public event EventHandler? LoadProject; + public event EventHandler? LoadProject; + + public readonly record struct LoaderResult(LoaderAction Action, string DataFile); + + public enum LoaderAction + { + NONE, + + LOAD_PROJECT, + CREATE_NEW_PROJECT, + IMPORT_JSON, + } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/Main.cs b/I18N Commander/UI WinForms/Components/Main.cs index b4e1950..ec44a6e 100644 --- a/I18N Commander/UI WinForms/Components/Main.cs +++ b/I18N Commander/UI WinForms/Components/Main.cs @@ -1,4 +1,6 @@ -namespace UI_WinForms.Components; +using Processor; + +namespace UI_WinForms.Components; public partial class Main : UserControl { @@ -6,6 +8,9 @@ public partial class Main : UserControl { this.InitializeComponent(); Program.RestartMainApp = false; + + // Register the something changed event to trigger the export: + AppEvents.WhenSomethingChanged += async (_, _) => await ExportProcessor.TriggerExport(); } private void tabControl_SelectedIndexChanged(object sender, EventArgs e) diff --git a/I18N Commander/UI WinForms/Components/Setting.cs b/I18N Commander/UI WinForms/Components/Setting.cs index 5256be2..9e5a7b9 100644 --- a/I18N Commander/UI WinForms/Components/Setting.cs +++ b/I18N Commander/UI WinForms/Components/Setting.cs @@ -690,7 +690,7 @@ public sealed partial class Setting : UserControl { var currentSetting = await AppSettings.GetAutoExportEnabled(); var settingData = new SettingUIData( - Icon: Icons.icons8_git_svg, + Icon: Icons.icons8_git, SettingName: () => "Git (JSON) Auto-Export: Enabled", ChangeNeedsRestart: true, SettingExplanation: () => "When enabled, all changes are automatically exported into a Git repository as JSON file.", @@ -700,9 +700,13 @@ public sealed partial class Setting : UserControl // Set up an checkbox: var checkbox = new CheckBox(); checkbox.Checked = currentSetting; - checkbox.CheckedChanged += async (sender, args) => await AppSettings.SetAutoExportEnabled(checkbox.Checked); - checkbox.CheckedChanged += (sender, args) => changeTrigger(); - checkbox.Text = "Enable Git (JSON) Auto-Export"; + checkbox.CheckedChanged += async (sender, args) => + { + await AppSettings.SetAutoExportEnabled(checkbox.Checked); + await ExportProcessor.TriggerExport(); + changeTrigger(); + }; + checkbox.Text = "Enable Auto-Export"; // Apply the desired layout: checkbox.Dock = DockStyle.Fill; @@ -717,7 +721,7 @@ public sealed partial class Setting : UserControl { var currentSetting = await AppSettings.GetAutoExportDestinationPath(); var settingData = new SettingUIData( - Icon: Icons.icons8_git_svg, + Icon: Icons.icons8_git, SettingName: () => "Git (JSON) Auto-Export: Destination Path", ChangeNeedsRestart: true, SettingExplanation: () => "The destination path for the Git repository. You might use environment variables like %USERPROFILE%.", @@ -775,7 +779,7 @@ public sealed partial class Setting : UserControl { var currentSetting = await AppSettings.GetAutoExportFilename(); var settingData = new SettingUIData( - Icon: Icons.icons8_git_svg, + Icon: Icons.icons8_git, SettingName: () => "Git (JSON) Auto-Export: Filename", ChangeNeedsRestart: true, SettingExplanation: () => "The filename used for the Git export. You might use environment variables like %USERPROFILE%.", @@ -800,7 +804,7 @@ public sealed partial class Setting : UserControl { var currentSetting = await AppSettings.GetAutoExportSensitiveData(); var settingData = new SettingUIData( - Icon: Icons.icons8_git_svg, + Icon: Icons.icons8_git, SettingName: () => "Git (JSON) Auto-Export: Export Sensitive Data", ChangeNeedsRestart: true, SettingExplanation: () => "When enabled, sensitive data like API tokens are exported into the Git repository. This is not recommended!", diff --git a/I18N Commander/UI WinForms/Loader.Designer.cs b/I18N Commander/UI WinForms/Loader.Designer.cs index 9737b5b..c645492 100644 --- a/I18N Commander/UI WinForms/Loader.Designer.cs +++ b/I18N Commander/UI WinForms/Loader.Designer.cs @@ -1,4 +1,6 @@ -namespace UI_WinForms +using UI_WinForms.Components; + +namespace UI_WinForms { partial class Loader { @@ -57,7 +59,7 @@ this.loaderStart.Name = "loaderStart"; this.loaderStart.Size = new System.Drawing.Size(794, 444); this.loaderStart.TabIndex = 0; - this.loaderStart.LoadProject += new System.EventHandler(this.loaderStart_LoadProject); + this.loaderStart.LoadProject += new System.EventHandler(this.loaderStart_LoadProject); // // Loader // diff --git a/I18N Commander/UI WinForms/Loader.cs b/I18N Commander/UI WinForms/Loader.cs index 6a6ca87..55abd1c 100644 --- a/I18N Commander/UI WinForms/Loader.cs +++ b/I18N Commander/UI WinForms/Loader.cs @@ -1,20 +1,22 @@ -namespace UI_WinForms; +using UI_WinForms.Components; + +namespace UI_WinForms; public partial class Loader : Form { - public string DataFile { get; set; } = string.Empty; + public LoaderStart.LoaderResult Result { get; private set; } public Loader() { this.InitializeComponent(); } - private void loaderStart_LoadProject(object sender, string projectFilePath) + private void loaderStart_LoadProject(object sender, LoaderStart.LoaderResult result) { if(this.DesignMode) return; - this.DataFile = projectFilePath; + this.Result = result; this.DialogResult = DialogResult.OK; this.Close(); } diff --git a/I18N Commander/UI WinForms/Program.cs b/I18N Commander/UI WinForms/Program.cs index 054d7de..2a1296f 100644 --- a/I18N Commander/UI WinForms/Program.cs +++ b/I18N Commander/UI WinForms/Program.cs @@ -1,9 +1,12 @@ +using System.Text.Json; using DataModel; using DataModel.Database.Common; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Processor; +using static UI_WinForms.Components.LoaderStart.LoaderAction; + namespace UI_WinForms; internal static class Program @@ -25,7 +28,7 @@ internal static class Program // Check, if the user closes the loader screen: if (loader.DialogResult != DialogResult.OK) return; - + // // Create the DI system // @@ -36,10 +39,25 @@ internal static class Program // builder.ConfigureServices((hostContext, serviceCollection) => { - // The database: - serviceCollection.AddDatabase(loader.DataFile, true); + if(loader.Result.Action is LOAD_PROJECT or CREATE_NEW_PROJECT) + serviceCollection.AddDatabase(loader.Result.DataFile, true); + else if(loader.Result.Action is IMPORT_JSON) + { + try + { + serviceCollection.ImportDataAndAddDatabase(loader.Result.DataFile).ConfigureAwait(false); + } + catch (JsonException) + { + MessageBox.Show($"The JSON file '{loader.Result.DataFile}' is invalid and cannot be imported.", "Invalid JSON file", MessageBoxButtons.OK, MessageBoxIcon.Error); + Environment.Exit(100); + } + } }); + // Tear down the setup: + using var setupMaintenance = Setup.Maintenance; + // Get the host out of the DI system: var host = builder.Build(); diff --git a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs index 98d01fb..10bf690 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs +++ b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs @@ -213,9 +213,9 @@ namespace UI_WinForms.Resources { /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// - internal static System.Drawing.Bitmap icons8_git_svg { + internal static System.Drawing.Bitmap icons8_git { get { - object obj = ResourceManager.GetObject("icons8_git_svg", resourceCulture); + object obj = ResourceManager.GetObject("icons8_git", resourceCulture); return ((System.Drawing.Bitmap)(obj)); } } diff --git a/I18N Commander/UI WinForms/Resources/Icons.resx b/I18N Commander/UI WinForms/Resources/Icons.resx index ee2bfe1..ec68fe3 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.resx +++ b/I18N Commander/UI WinForms/Resources/Icons.resx @@ -163,8 +163,8 @@ icons8-folder-tree-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a - - icons8-git.svg.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-git.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a icons8-increase-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/I18N Commander/UI WinForms/Resources/icons8-git.png b/I18N Commander/UI WinForms/Resources/icons8-git.png new file mode 100644 index 0000000000000000000000000000000000000000..89518253c1b740535b192d871289a3a115c61c47 GIT binary patch literal 2786 zcmai0dmvP48$aWgOSwf7*)d~7n44QphA_67nvvXEu+?p8-S(lQ=#3m|T1|_AE zNT!4>8^w0fZYt7+luwGX+jd(m-x*rAt-kM%Gc)h|`#rz+d7s;v9ABU1b2Jxd0st_F zMy2>6-*$>eT@CpJ7xeD~0F`PuAVd_xpo1(P2gi)!MM5|+hmYU@;OZ{sGg5v%y3SEJGM(4-!L_A?Ek3W5ZPmY*| zKQlza_hl0?lm8_$gYm!V9L^`#5Q@ALkwAP5>L)(^eSk0^nGfOpAR#Y7z=FIJA+88J z83{#PKpKY`1JNOFj3}0jad7+>5DZ8CdtV{MPEcebkP9P6lV(gpW-v0w#R*TGfkM9s zDMAhU!eWRWLV-CDSBR)idocrseg*X6v3UZ2q^uz_X6mfzJb;rJgBOKB_LFYRKr}Be zUjZ)~W+Rx;Z@DL$=I!M~ba8UBBjKiGkfec#EgYRpVTuqIi9oWqBM|LKqyVBLNOT5C z_BI3}NFYoqPqL9-h+>ME|BtVzXtXP0!C-(?SSaENk|(#O%G@8?FtwUmvf;`4Mx!UQ z3^G}YsFN{#0WT_n1w~CBj>t`+LSD2ei79|QVvq!rF&@#;Fp{5Sw6CIa(IgxJX~|FI zl42o5?CV#A5Xt=*UbH#t=yXz$3A03L=P3<9_kBkuf^bA23(^ClIBBMuu%$PBv zj0^y%3>eAEXW0M%s~rhD8doZN=x)7WA?-IYI8~#rUzMumVX{qv_uV%Cr(of^C;H|_ z+Z(m?*{W+jJnDW6e7gQXRfL_o6eFp+BXwFlDAUt#y;gg=cRVq=yI7THarjvf&G{*O zZ}#@=52uB%#%nEI9sIl&u9LgCe5zv6eDsz1D`B24V-NM9?a ziz6>Ng+0{g`WCLBzoXPY*r}!2Tc6_4b^~WA_%Kv!tf7;6)~CDFsxKB*N%J?_T{QO> zf8*D0-CnFb-aoV#4qY4N#L|o2shf7$I9h@x&z9)i^uQO_9-^kbT{UiSwV+b!+#Xp= z*<|4~QWjmF5*EbxqdwOl^z20XSRDNQkLR8Rnp=G@l63`H-OH+;)AoJ|UsmlkN^VXD zOD{C({KYHdJ&`w?-8%WxkazetZp&TJ;emY7RH?7ubte6Y09p+GG}B~`D!cVojvA9d|Pujtyi^amJ0Q7{XX-xs2@Tab0iGP(khN; zt=n~bntE@D*=i*EI zm+27N3c}+@Z1dU!h8vW8ybYb2-o&jc`tC|knZA10++GdWYI1tO#_^XuuO@2jsp*@p zc^;^KmY5J-5VWGov-TYO{_=|!QWI;^LQVOihVDHP*`*nA%($QTto(3q?QV?l#(beo zL$_{cN_Vx|Ng)8A`qctfFJnDsQF4NKBbk8{( z+B4*ZIrp~$OAnOF+p4gwMJ?Z21)FO&s1lUS`xgC*tU2~oJ2?{K~0?t^Iyu0D$F z3+V%2VzJKz7N_R5Jw0|ehM|{z=cMi#pe^$UrT)N(^{36Wf4yRO(YqYwu<^R1MpI_M zAy2Ev*)X}_b+Rd8i>Yq*?~B%%UTMg!d7f<-l5i>L6Vhxki edoHzPO#qFS=2&s%Sc~GXDb3r5a?&$m(|-X32s}6d literal 0 HcmV?d00001 From c41d000bf070615883b490f662f65c83aa9a2e2b Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 19:56:25 +0100 Subject: [PATCH 14/30] Fixed EF Tooling - Added factory class for EF tooling - Removed no longer needed Program.cs --- .../Database/Common/DataContextFactory.cs | 28 +++++++++++++++++ .../DataModel/Database/TextElement.cs | 2 +- I18N Commander/DataModel/Program.cs | 13 -------- I18N Commander/DataModel/Setup.cs | 31 +++++++++---------- 4 files changed, 44 insertions(+), 30 deletions(-) create mode 100644 I18N Commander/DataModel/Database/Common/DataContextFactory.cs delete mode 100644 I18N Commander/DataModel/Program.cs diff --git a/I18N Commander/DataModel/Database/Common/DataContextFactory.cs b/I18N Commander/DataModel/Database/Common/DataContextFactory.cs new file mode 100644 index 0000000..db940bd --- /dev/null +++ b/I18N Commander/DataModel/Database/Common/DataContextFactory.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Design; + +namespace DataModel.Database.Common; + +/// +/// This factory is used by the EF tooling e.g. to create migrations. +/// +public sealed class DataContextFactory : IDesignTimeDbContextFactory +{ + private const string ENV_EF_TOOLING_DATABASE = "ENV_EF_TOOLING_DATABASE"; + + #region Implementation of IDesignTimeDbContextFactory + + /// + public DataContext CreateDbContext(string[] args) + { + var dataFile = Environment.GetEnvironmentVariable(ENV_EF_TOOLING_DATABASE); + if (string.IsNullOrWhiteSpace(dataFile)) + { + Console.WriteLine("In order to use EF tooling, point the environment variable ENV_EF_TOOLING_DATABASE to the data file, which should be used for the EF tooling."); + Environment.Exit(100); + } + + return Setup.CreateDatabaseInstance4Tooling(dataFile, false); + } + + #endregion +} \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/TextElement.cs b/I18N Commander/DataModel/Database/TextElement.cs index 7870238..538dac1 100644 --- a/I18N Commander/DataModel/Database/TextElement.cs +++ b/I18N Commander/DataModel/Database/TextElement.cs @@ -16,7 +16,7 @@ public sealed class TextElement public bool IsMultiLine { get; set; } = false; - public Section Section { get; set; } + public Section Section { get; set; } = new(); public List Translations { get; set; } = new(); diff --git a/I18N Commander/DataModel/Program.cs b/I18N Commander/DataModel/Program.cs deleted file mode 100644 index d07d548..0000000 --- a/I18N Commander/DataModel/Program.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Microsoft.Extensions.Hosting; -namespace DataModel; - -public static class Program -{ - public static void Main(string[] args) - { - Console.WriteLine("This app is intended for the EF tooling. You cannot start this data project, though."); - Environment.Exit(0); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => Setup.Setup4EFTooling(args); -} \ No newline at end of file diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index e687af1..05f33d6 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -9,7 +9,6 @@ namespace DataModel; public static class Setup { - private const string ENV_EF_TOOLING_DATABASE = "ENV_EF_TOOLING_DATABASE"; private const string DB_READ_WRITE_MODE = "ReadWrite"; private const string DB_READ_WRITE_CREATE_MODE = "ReadWriteCreate"; @@ -90,26 +89,26 @@ public static class Setup } /// - /// Sets up the DI & db context ready for the EF tooling. + /// Create the database instance from the given path. Used for the EF tooling. /// - public static IHostBuilder Setup4EFTooling(string[] args) + public static DataContext CreateDatabaseInstance4Tooling(string path2DataFile, bool createWhenNecessary = true) { - var dataFile = Environment.GetEnvironmentVariable(ENV_EF_TOOLING_DATABASE); - if (string.IsNullOrWhiteSpace(dataFile)) - { - Console.WriteLine("In order to use EF tooling, point the environment variable ENV_EF_TOOLING_DATABASE to the data file, which should be used for the EF tooling."); - Environment.Exit(100); - } + // Store the path to the database: + Setup.USED_DATA_FILE = path2DataFile; + Setup.SETUP_MAINTENANCE = new(path2DataFile, false); - var builder = Host.CreateDefaultBuilder(args); - builder.ConfigureServices((hostContext, serviceCollection) => - { - serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={dataFile};Mode=ReadWriteCreate")); - }); - - return builder; + // Create a database builder: + var builder = new DbContextOptionsBuilder(); + + // Add the database configuration to the builder: + builder.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"); + + // Next, construct the database context: + var dbContext = new DataContext(builder.Options); + return dbContext; } + public readonly record struct SetupMaintenance(string PathToDataFile = "", bool RemoveTempDatabaseAfterwards = false) : IDisposable { public void Dispose() From caaa00f11627b6f1d445cc789dc2487931591a39 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:04:48 +0100 Subject: [PATCH 15/30] Fixed missed awaiting of import process --- I18N Commander/UI WinForms/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/I18N Commander/UI WinForms/Program.cs b/I18N Commander/UI WinForms/Program.cs index 2a1296f..f7e25a5 100644 --- a/I18N Commander/UI WinForms/Program.cs +++ b/I18N Commander/UI WinForms/Program.cs @@ -45,7 +45,7 @@ internal static class Program { try { - serviceCollection.ImportDataAndAddDatabase(loader.Result.DataFile).ConfigureAwait(false); + serviceCollection.ImportDataAndAddDatabase(loader.Result.DataFile).ConfigureAwait(false).GetAwaiter().GetResult(); } catch (JsonException) { From ce5f2ae401b229d664279e89bc6429171d2cc69f Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:05:13 +0100 Subject: [PATCH 16/30] Improved error handling of import process --- I18N Commander/UI WinForms/Program.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/I18N Commander/UI WinForms/Program.cs b/I18N Commander/UI WinForms/Program.cs index f7e25a5..3bedb40 100644 --- a/I18N Commander/UI WinForms/Program.cs +++ b/I18N Commander/UI WinForms/Program.cs @@ -47,11 +47,16 @@ internal static class Program { serviceCollection.ImportDataAndAddDatabase(loader.Result.DataFile).ConfigureAwait(false).GetAwaiter().GetResult(); } - catch (JsonException) + catch (JsonException e) { - MessageBox.Show($"The JSON file '{loader.Result.DataFile}' is invalid and cannot be imported.", "Invalid JSON file", MessageBoxButtons.OK, MessageBoxIcon.Error); + MessageBox.Show($"The JSON file '{loader.Result.DataFile}' is invalid and cannot be imported: {e.Message}", "Invalid JSON file", MessageBoxButtons.OK, MessageBoxIcon.Error); Environment.Exit(100); } + catch(Exception e) + { + MessageBox.Show($"An error occurred while importing the JSON file '{loader.Result.DataFile}': {e.Message}", "Error while importing JSON file", MessageBoxButtons.OK, MessageBoxIcon.Error); + Environment.Exit(101); + } } }); From 574d1ef8bffc0865c048eacf50e605d58fe0910e Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:05:53 +0100 Subject: [PATCH 17/30] Fixed missed migration; no tables were available, though. --- I18N Commander/DataModel/Setup.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index 05f33d6..c8c8ed5 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -54,6 +54,9 @@ public static class Setup await using var serviceProvider = serviceCollection.BuildServiceProvider(); var dbContext = serviceProvider.GetRequiredService(); + // Migrate the database to create the tables etc.: + await Setup.PerformDataMigration(dbContext); + // Next, we import the data from the provided JSON file: await dbContext.ImportAsync(path2JSONFile); From 9f0d6641055fad0442041cc63021f7c1ca980e55 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:06:34 +0100 Subject: [PATCH 18/30] Ensured that auto-export settings are available --- I18N Commander/DataModel/Setup.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index c8c8ed5..5ec56c1 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -67,16 +67,23 @@ public static class Setup // // Enable the auto-export feature: - var autoExportEnabled = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_ENABLED); + var autoExportEnabled = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_ENABLED) ?? new Setting { Code = SettingNames.AUTO_EXPORT_ENABLED }; autoExportEnabled.BoolValue = true; // Set the auto-export path and file: - var autoExportPath = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_DESTINATION_PATH); + var autoExportPath = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_DESTINATION_PATH) ?? new Setting { Code = SettingNames.AUTO_EXPORT_DESTINATION_PATH }; autoExportPath.TextValue = Path.GetDirectoryName(path2JSONFile) ?? string.Empty; - var autoExportFile = await dbContext.Settings.FirstAsync(n => n.Code == SettingNames.AUTO_EXPORT_FILENAME); + var autoExportFile = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_FILENAME) ?? new Setting { Code = SettingNames.AUTO_EXPORT_FILENAME }; autoExportFile.TextValue = Path.GetFileName(path2JSONFile); + // Ensure that the sensitive data setting is present and disabled by default: + var autoExportSensitiveData = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_SENSITIVE_DATA) ?? new Setting + { + Code = SettingNames.AUTO_EXPORT_SENSITIVE_DATA, + BoolValue = false, + }; + // Save the changes: await dbContext.SaveChangesAsync(); } From 639d501906f35b2acfe4e3b75d91d3fdb2377b69 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:07:02 +0100 Subject: [PATCH 19/30] Fixed translation unique id formation --- I18N Commander/DataModel/Database/Translation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/I18N Commander/DataModel/Database/Translation.cs b/I18N Commander/DataModel/Database/Translation.cs index d9f428c..7957c7a 100644 --- a/I18N Commander/DataModel/Database/Translation.cs +++ b/I18N Commander/DataModel/Database/Translation.cs @@ -16,7 +16,7 @@ public sealed class Translation public bool TranslateManual { get; set; } = false; - internal DataContext.JsonUniqueId JsonUniqueId => new($"{this.TextElement.Code}::{this.Culture}", this.UniqueId, "Trans"); + internal DataContext.JsonUniqueId JsonUniqueId => new($"{this.TextElement.Code}@{this.Culture}", this.UniqueId, "Trans"); internal DataContext.JsonTranslation ToJsonTranslation() => new() { From a868087af9f954804b71c0ac69370e00c0b956dc Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:38:47 +0100 Subject: [PATCH 20/30] Fixed auto-export settings set up when importing a JSON file --- I18N Commander/DataModel/Setup.cs | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index 5ec56c1..ebbd986 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -66,19 +66,29 @@ public static class Setup // temporary database source by a JSON file. // + // // Enable the auto-export feature: - var autoExportEnabled = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_ENABLED) ?? new Setting { Code = SettingNames.AUTO_EXPORT_ENABLED }; - autoExportEnabled.BoolValue = true; - + // + if (await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_ENABLED) is { } autoExportEnabled) + autoExportEnabled.BoolValue = true; + else + dbContext.Settings.Add(new Setting {Code = SettingNames.AUTO_EXPORT_ENABLED, BoolValue = true}); + + // // Set the auto-export path and file: - var autoExportPath = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_DESTINATION_PATH) ?? new Setting { Code = SettingNames.AUTO_EXPORT_DESTINATION_PATH }; - autoExportPath.TextValue = Path.GetDirectoryName(path2JSONFile) ?? string.Empty; - - var autoExportFile = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_FILENAME) ?? new Setting { Code = SettingNames.AUTO_EXPORT_FILENAME }; - autoExportFile.TextValue = Path.GetFileName(path2JSONFile); - + // + if(await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_DESTINATION_PATH) is { } autoExportPath) + autoExportPath.TextValue = Path.GetDirectoryName(path2JSONFile) ?? string.Empty; + else + dbContext.Settings.Add(new Setting {Code = SettingNames.AUTO_EXPORT_DESTINATION_PATH, TextValue = Path.GetDirectoryName(path2JSONFile) ?? string.Empty}); + + if(await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_FILENAME) is { } autoExportFile) + autoExportFile.TextValue = Path.GetFileName(path2JSONFile); + else + dbContext.Settings.Add(new Setting {Code = SettingNames.AUTO_EXPORT_FILENAME, TextValue = Path.GetFileName(path2JSONFile)}); + // Ensure that the sensitive data setting is present and disabled by default: - var autoExportSensitiveData = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_SENSITIVE_DATA) ?? new Setting + var _ = await dbContext.Settings.FirstOrDefaultAsync(n => n.Code == SettingNames.AUTO_EXPORT_SENSITIVE_DATA) ?? new Setting { Code = SettingNames.AUTO_EXPORT_SENSITIVE_DATA, BoolValue = false, From 35009dba7cbb3dee341a29ddbe7725c1d708fd2c Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:39:36 +0100 Subject: [PATCH 21/30] Fixed data model classes to generate unique ids when created --- I18N Commander/DataModel/Database/Section.cs | 2 +- I18N Commander/DataModel/Database/Setting.cs | 2 +- I18N Commander/DataModel/Database/TextElement.cs | 2 +- I18N Commander/DataModel/Database/Translation.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/I18N Commander/DataModel/Database/Section.cs b/I18N Commander/DataModel/Database/Section.cs index c963a40..0bd9cb0 100644 --- a/I18N Commander/DataModel/Database/Section.cs +++ b/I18N Commander/DataModel/Database/Section.cs @@ -8,7 +8,7 @@ public sealed class Section [Key] public int Id { get; set; } - public Guid UniqueId { get; set; } + public Guid UniqueId { get; set; } = Guid.NewGuid(); public string Name { get; set; } = string.Empty; diff --git a/I18N Commander/DataModel/Database/Setting.cs b/I18N Commander/DataModel/Database/Setting.cs index 5c5977b..10c7c33 100644 --- a/I18N Commander/DataModel/Database/Setting.cs +++ b/I18N Commander/DataModel/Database/Setting.cs @@ -8,7 +8,7 @@ public sealed class Setting [Key] public int Id { get; set; } - public Guid UniqueId { get; set; } + public Guid UniqueId { get; set; } = Guid.NewGuid(); public string Code { get; set; } = string.Empty; diff --git a/I18N Commander/DataModel/Database/TextElement.cs b/I18N Commander/DataModel/Database/TextElement.cs index 538dac1..2d2a5a1 100644 --- a/I18N Commander/DataModel/Database/TextElement.cs +++ b/I18N Commander/DataModel/Database/TextElement.cs @@ -8,7 +8,7 @@ public sealed class TextElement [Key] public int Id { get; set; } - public Guid UniqueId { get; set; } + public Guid UniqueId { get; set; } = Guid.NewGuid(); public string Name { get; set; } = string.Empty; diff --git a/I18N Commander/DataModel/Database/Translation.cs b/I18N Commander/DataModel/Database/Translation.cs index 7957c7a..b69807f 100644 --- a/I18N Commander/DataModel/Database/Translation.cs +++ b/I18N Commander/DataModel/Database/Translation.cs @@ -6,7 +6,7 @@ public sealed class Translation { public int Id { get; set; } - public Guid UniqueId { get; set; } + public Guid UniqueId { get; set; } = Guid.NewGuid(); public TextElement TextElement { get; set; } = new(); From 52927d47156069837cae18bcf6333bf6786bda94 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:50:15 +0100 Subject: [PATCH 22/30] Fixed export synchronization - Replaced Monitor by another semaphore, because async method calls are not working with monitors! --- I18N Commander/Processor/ExportProcessor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/I18N Commander/Processor/ExportProcessor.cs b/I18N Commander/Processor/ExportProcessor.cs index 371f24c..bffd116 100644 --- a/I18N Commander/Processor/ExportProcessor.cs +++ b/I18N Commander/Processor/ExportProcessor.cs @@ -7,8 +7,8 @@ namespace Processor; public static class ExportProcessor { private static readonly Timer EXPORT_TIMER = new(); - private static readonly object EXPORT_LOCK = new(); - private static readonly SemaphoreSlim EXPORT_SEMAPHORE = new(2); + private static readonly SemaphoreSlim EXPORT_SEMAPHORE_BRICK_WALL = new(2); + private static readonly SemaphoreSlim EXPORT_SEMAPHORE_EXPORTING = new(1); static ExportProcessor() { @@ -19,19 +19,19 @@ public static class ExportProcessor private static async Task ExportToJson() { - if(!await EXPORT_SEMAPHORE.WaitAsync(1)) + if(!await EXPORT_SEMAPHORE_BRICK_WALL.WaitAsync(1)) return; - Monitor.Enter(EXPORT_LOCK); try { + await EXPORT_SEMAPHORE_EXPORTING.WaitAsync(); await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); await db.ExportAsync(Environment.ExpandEnvironmentVariables(Path.Join(await AppSettings.GetAutoExportDestinationPath(), await AppSettings.GetAutoExportFilename()))); } finally { - Monitor.Exit(EXPORT_LOCK); - EXPORT_SEMAPHORE.Release(); + EXPORT_SEMAPHORE_EXPORTING.Release(); + EXPORT_SEMAPHORE_BRICK_WALL.Release(); } } From 28db843a3a42335cb132f71a570a222aabaf44f9 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:53:09 +0100 Subject: [PATCH 23/30] Fixed something changed event; settings changed was missed --- I18N Commander/UI WinForms/AppEvents.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/I18N Commander/UI WinForms/AppEvents.cs b/I18N Commander/UI WinForms/AppEvents.cs index b86f556..9ef59ab 100644 --- a/I18N Commander/UI WinForms/AppEvents.cs +++ b/I18N Commander/UI WinForms/AppEvents.cs @@ -69,6 +69,7 @@ internal static class AppEvents WhenSectionChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty); WhenTextElementChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty); WhenTranslationChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty); + WhenSettingsChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty); } #endregion From bafb325880b1bfa63870ed0973874a8891532d55 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 22 Jan 2023 21:58:01 +0100 Subject: [PATCH 24/30] Improved Git merge performance of exported files The exported entries appear now in a deterministic order, which made merges a lot easier. --- I18N Commander/DataModel/Database/Common/DataContext.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 58872c3..4e9c8be 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -160,10 +160,10 @@ public sealed class DataContext : DbContext, IDataContext await JsonSerializer.SerializeAsync(fileStream, new JsonData { - Settings = this.Settings.Select(n => n.ToJsonSetting()).ToList(), - Sections = this.Sections.Select(n => n.ToJsonSection()).ToList(), - TextElements = this.TextElements.Select(n => n.ToJsonTextElement()).ToList(), - Translations = this.Translations.Select(n => n.ToJsonTranslation()).ToList(), + Settings = this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).ToList(), + Sections = this.Sections.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSection()).ToList(), + TextElements = this.TextElements.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTextElement()).ToList(), + Translations = this.Translations.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTranslation()).ToList(), }, jsonSettings); } From 59da2c23a8708aa9585e5baf8d9a13a5502a4ba1 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Tue, 24 Jan 2023 20:58:42 +0100 Subject: [PATCH 25/30] Filters out sensitive setting data while exporting --- .../DataModel/Database/Common/DataContext.cs | 30 +++++++++++++++++-- .../DataModel/Database/Common/IDataContext.cs | 2 +- I18N Commander/Processor/ExportProcessor.cs | 8 ++++- I18N Commander/Processor/Version.cs | 2 +- 4 files changed, 36 insertions(+), 6 deletions(-) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 4e9c8be..3cfa6dd 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -143,24 +143,48 @@ public sealed class DataContext : DbContext, IDataContext bool TranslateManual, JsonUniqueId TextElementUniqueId ); - + /// /// Exports this database to a JSON file. /// /// The path to the JSON file. - public async Task ExportAsync(string path) + /// When false, exclude sensitive data from export. + public async Task ExportAsync(string path, bool includeSensitiveData = false) { var jsonSettings = new JsonSerializerOptions { WriteIndented = true, Converters = { new JsonUniqueIdConverter() }, }; + + // Maintained list of sensitive data to be removed from the export: + var sensitiveSettingCodes = new HashSet + { + SettingNames.DEEPL_API_KEY, + }; + + // A local filter function to remove sensitive data from the export. + // Removing just the sensitive values instead of the entire setting. + IEnumerable FilterSensitiveSettings(IEnumerable settings) + { + foreach (var setting in settings) + { + if (sensitiveSettingCodes!.Contains(setting.Code)) + yield return new JsonSetting(setting.UniqueId, setting.Code, string.Empty, 0, false, Guid.Empty); + else + yield return setting; + } + } await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); await JsonSerializer.SerializeAsync(fileStream, new JsonData { - Settings = this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).ToList(), + Settings = includeSensitiveData ? + // Include all settings, including sensitive data: + this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).ToList() : + // Exclude sensitive data: + FilterSensitiveSettings(this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).AsEnumerable()).ToList(), Sections = this.Sections.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSection()).ToList(), TextElements = this.TextElements.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTextElement()).ToList(), Translations = this.Translations.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTranslation()).ToList(), diff --git a/I18N Commander/DataModel/Database/Common/IDataContext.cs b/I18N Commander/DataModel/Database/Common/IDataContext.cs index 7888beb..34dd1c1 100644 --- a/I18N Commander/DataModel/Database/Common/IDataContext.cs +++ b/I18N Commander/DataModel/Database/Common/IDataContext.cs @@ -12,5 +12,5 @@ public interface IDataContext public DbSet Translations { get; set; } - public Task ExportAsync(string path); + public Task ExportAsync(string path, bool includeSensitiveData = false); } \ No newline at end of file diff --git a/I18N Commander/Processor/ExportProcessor.cs b/I18N Commander/Processor/ExportProcessor.cs index bffd116..55f2106 100644 --- a/I18N Commander/Processor/ExportProcessor.cs +++ b/I18N Commander/Processor/ExportProcessor.cs @@ -26,7 +26,13 @@ public static class ExportProcessor { await EXPORT_SEMAPHORE_EXPORTING.WaitAsync(); await using var db = ProcessorMeta.ServiceProvider.GetRequiredService(); - await db.ExportAsync(Environment.ExpandEnvironmentVariables(Path.Join(await AppSettings.GetAutoExportDestinationPath(), await AppSettings.GetAutoExportFilename()))); + await db.ExportAsync( + path: Environment.ExpandEnvironmentVariables( + Path.Join(await AppSettings.GetAutoExportDestinationPath(), + await AppSettings.GetAutoExportFilename()) + ), + includeSensitiveData: await AppSettings.GetAutoExportSensitiveData() + ); } finally { diff --git a/I18N Commander/Processor/Version.cs b/I18N Commander/Processor/Version.cs index 62de691..592ab98 100644 --- a/I18N Commander/Processor/Version.cs +++ b/I18N Commander/Processor/Version.cs @@ -2,5 +2,5 @@ public static class Version { - public static string Text => $"v0.8.0 (2023-01-22), .NET {Environment.Version}"; + public static string Text => $"v0.8.1 (2023-01-24), .NET {Environment.Version}"; } \ No newline at end of file From 4c6f5b59f055e9aeec94c41104918609e7f18f54 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 11 Feb 2023 21:22:25 +0100 Subject: [PATCH 26/30] Bugfixes - Added handling for DeepL exceptions - Added debug logging for im- and exporting - When executing the post script for the unique id migration, ensure that new id are generated, only when no id exists - Fixed section export by loading references to parents --- .../DataModel/Database/Common/DataContext.cs | 55 +++++++++++++++++-- I18N Commander/DataModel/Database/Section.cs | 25 ++++++--- .../MigrationScripts/202211AddUniqueIds.cs | 26 ++++++--- I18N Commander/Processor/DeepL.cs | 15 +++-- I18N Commander/Processor/Version.cs | 2 +- 5 files changed, 98 insertions(+), 25 deletions(-) diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 3cfa6dd..0f46a50 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.Linq.Expressions; +using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.EntityFrameworkCore; @@ -151,6 +152,7 @@ public sealed class DataContext : DbContext, IDataContext /// When false, exclude sensitive data from export. public async Task ExportAsync(string path, bool includeSensitiveData = false) { + Console.WriteLine("Exporting database to JSON file..."); var jsonSettings = new JsonSerializerOptions { WriteIndented = true, @@ -176,19 +178,32 @@ public sealed class DataContext : DbContext, IDataContext } } + // Use a local reference to the database to use it in the lambda expression trees below: + // (we cannot use "this" in a lambda expression tree; yields an exception at runtime) + var db = this; + await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None); await JsonSerializer.SerializeAsync(fileStream, new JsonData { + // Settings don't have references to other entities; we can just use them here: Settings = includeSensitiveData ? // Include all settings, including sensitive data: this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).ToList() : // Exclude sensitive data: FilterSensitiveSettings(this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).AsEnumerable()).ToList(), - Sections = this.Sections.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSection()).ToList(), + + // Warning: the parents cannot pre-loaded, thus, we must load them now inside the lambda expression tree: + Sections = this.Sections.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSection(db)).ToList(), + + // All text elements references are pre-loaded, so we can use them here: TextElements = this.TextElements.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTextElement()).ToList(), + + // All translation references are pre-loaded, so we can use them here: Translations = this.Translations.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTranslation()).ToList(), }, jsonSettings); + + Console.WriteLine("Export complete."); } /// @@ -211,6 +226,8 @@ public sealed class DataContext : DbContext, IDataContext await this.TextElements.AnyAsync() || await this.Translations.AnyAsync()) throw new InvalidOperationException("The database is not empty. In order to import data, the database must be empty."); + + Console.WriteLine("Start importing data from JSON file..."); // Start a transaction: await using var transaction = await this.Database.BeginTransactionAsync(); @@ -244,6 +261,8 @@ public sealed class DataContext : DbContext, IDataContext { // Convert the next element: var nextSection = Section.FromJsonSection(section); + + // Notice: the parent id is not yet resolved. // Store the element: allSections.Add(nextSection.UniqueId, new (section.ParentUniqueId.UniqueId, nextSection)); @@ -252,8 +271,28 @@ public sealed class DataContext : DbContext, IDataContext // Now, resolve the parent-child relationships for the sections: foreach (var (uniqueId, (parentId, section)) in allSections) - section.Parent = parentId == Guid.Empty ? null : allSections[parentId].Entity; - + { + if(parentId == Guid.Empty) + { + if(section.Depth != 0) + Console.WriteLine(@$"Section {uniqueId} ""{section.Name}"" has no parent."); + else + Console.WriteLine(@$"Section {uniqueId} ""{section.Name}"" is a root section, thus, has no parent."); + + section.Parent = null; + continue; + } + + if(allSections.TryGetValue(parentId, out var parent)) + section.Parent = parent.Entity; + else + { + Console.WriteLine(@$"Parent of section {uniqueId} ""{section.Name}"" was not found."); + section.Parent = null; + continue; + } + } + // ------------------------- // Import the text elements: // ------------------------- @@ -330,7 +369,15 @@ public sealed class DataContext : DbContext, IDataContext // Commit the transaction: await transaction.CommitAsync(); + + Console.WriteLine("Finished importing data from JSON file."); } #endregion + + #region Tools + + internal Task LoadElementsAsync(TEntry entry, Expression> selector) where TEntry : class where TProp : class => this.Entry(entry).Reference(selector).LoadAsync(); + + #endregion } \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Section.cs b/I18N Commander/DataModel/Database/Section.cs index 0bd9cb0..a990898 100644 --- a/I18N Commander/DataModel/Database/Section.cs +++ b/I18N Commander/DataModel/Database/Section.cs @@ -22,16 +22,23 @@ public sealed class Section internal DataContext.JsonUniqueId JsonUniqueId => new(this.DataKey, this.UniqueId, "Sec"); - internal DataContext.JsonSection ToJsonSection() => new() + internal DataContext.JsonSection ToJsonSection(DataContext db) { - UniqueId = this.JsonUniqueId, - Name = this.Name, - DataKey = this.DataKey, - Depth = this.Depth, - ParentUniqueId = this.Parent?.JsonUniqueId ?? new DataContext.JsonUniqueId("null", Guid.Empty, "Sec"), - TextElements = this.TextElements.Select(n => n.JsonUniqueId).ToList() - }; - + db.LoadElementsAsync(this, n => n.Parent).GetAwaiter().GetResult(); + if(this.Depth > 0 && this.Parent == null) + Console.WriteLine($"Found section with depth > 0 and parent is null ==> {this.JsonUniqueId}"); + + return new() + { + UniqueId = this.JsonUniqueId, + Name = this.Name, + DataKey = this.DataKey, + Depth = this.Depth, + ParentUniqueId = this.Parent?.JsonUniqueId ?? new DataContext.JsonUniqueId("null", Guid.Empty, "Sec"), + TextElements = this.TextElements.Select(n => n.JsonUniqueId).ToList() + }; + } + internal static Section FromJsonSection(DataContext.JsonSection jsonSection) => new() { UniqueId = jsonSection.UniqueId.UniqueId, diff --git a/I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs b/I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs index aa85699..76bcc90 100644 --- a/I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs +++ b/I18N Commander/DataModel/MigrationScripts/202211AddUniqueIds.cs @@ -7,16 +7,28 @@ public static class Script202211AddUniqueIds public static async Task PostMigrationAsync(DataContext db) { await foreach (var setting in db.Settings) - setting.UniqueId = Guid.NewGuid(); - + { + if(setting.UniqueId == Guid.Empty) + setting.UniqueId = Guid.NewGuid(); + } + await foreach(var section in db.Sections) - section.UniqueId = Guid.NewGuid(); - + { + if(section.UniqueId == Guid.Empty) + section.UniqueId = Guid.NewGuid(); + } + await foreach (var textElement in db.TextElements) - textElement.UniqueId = Guid.NewGuid(); - + { + if(textElement.UniqueId == Guid.Empty) + textElement.UniqueId = Guid.NewGuid(); + } + await foreach (var translation in db.Translations) - translation.UniqueId = Guid.NewGuid(); + { + if(translation.UniqueId == Guid.Empty) + translation.UniqueId = Guid.NewGuid(); + } await db.SaveChangesAsync(); } diff --git a/I18N Commander/Processor/DeepL.cs b/I18N Commander/Processor/DeepL.cs index af3ffe5..d942e9c 100644 --- a/I18N Commander/Processor/DeepL.cs +++ b/I18N Commander/Processor/DeepL.cs @@ -54,17 +54,24 @@ public static class DeepL { var sourceCultureIndex = await AppSettings.GetDeepLSourceCultureIndex(); var sourceCulture = await AppSettings.GetCultureCode(sourceCultureIndex); - + // In case of the source culture, we cannot specify the region, so we need to remove it: sourceCulture = sourceCulture.Split('-')[0]; - + using var deepl = new Translator(deepLAPIKey); var translation = await deepl.TranslateTextAsync(text, sourceCulture, targetCulture); - + return translation.Text; } - catch (AuthorizationException) + catch (AuthorizationException e) { + Console.WriteLine($"DeepL authorization failed: {e.Message}"); + DEEPL_NOT_AVAILABLE = true; + return string.Empty; + } + catch (DeepLException e) + { + Console.WriteLine($"DeepL issue: {e.Message}"); DEEPL_NOT_AVAILABLE = true; return string.Empty; } diff --git a/I18N Commander/Processor/Version.cs b/I18N Commander/Processor/Version.cs index 592ab98..d6dce1e 100644 --- a/I18N Commander/Processor/Version.cs +++ b/I18N Commander/Processor/Version.cs @@ -2,5 +2,5 @@ public static class Version { - public static string Text => $"v0.8.1 (2023-01-24), .NET {Environment.Version}"; + public static string Text => $"v0.8.2 (2023-02-11), .NET {Environment.Version}"; } \ No newline at end of file From 18fd1faeddeca6a9a57821696c957fa6e2b78ceb Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 11 Feb 2023 23:01:13 +0100 Subject: [PATCH 27/30] Enables console logging when using debug configuration --- I18N Commander/UI WinForms/UI WinForms.csproj | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/I18N Commander/UI WinForms/UI WinForms.csproj b/I18N Commander/UI WinForms/UI WinForms.csproj index 390c5ab..fda8ad3 100644 --- a/I18N Commander/UI WinForms/UI WinForms.csproj +++ b/I18N Commander/UI WinForms/UI WinForms.csproj @@ -1,7 +1,8 @@  - WinExe + WinExe + Exe net6.0-windows10.0.22000.0 UI_WinForms enable From 96823aa948aae6ec3967cf01a7cf2f6afce09966 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sat, 11 Feb 2023 23:03:14 +0100 Subject: [PATCH 28/30] Fixed removing temp. database file when importing JSON --- I18N Commander/DataModel/Setup.cs | 33 +++++++++++++------- I18N Commander/Processor/Version.cs | 2 +- I18N Commander/UI WinForms/Program.cs | 43 ++++++++++++++------------- 3 files changed, 46 insertions(+), 32 deletions(-) diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index ebbd986..d36d5be 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -1,9 +1,9 @@ -using DataModel.Database; +using System.Diagnostics; +using DataModel.Database; using DataModel.Database.Common; using DataModel.MigrationScripts; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; namespace DataModel; @@ -13,12 +13,10 @@ public static class Setup private const string DB_READ_WRITE_CREATE_MODE = "ReadWriteCreate"; private static string USED_DATA_FILE = string.Empty; - private static SetupMaintenance SETUP_MAINTENANCE = new(); + public static SetupMaintenance SETUP_MAINTENANCE = new(); public static string DataFile => Setup.USED_DATA_FILE; - public static SetupMaintenance Maintenance => Setup.SETUP_MAINTENANCE; - /// /// Tries to migrate the database. /// @@ -44,15 +42,18 @@ public static class Setup /// public static async Task ImportDataAndAddDatabase(this IServiceCollection serviceCollection, string path2JSONFile) { + Console.WriteLine($"Importing the data from the JSON file '{path2JSONFile}' into a new database."); var tempPath = Path.GetTempFileName(); - Setup.USED_DATA_FILE = tempPath; - Setup.SETUP_MAINTENANCE = new(path2JSONFile, true); + Console.WriteLine($"The temporary database file is: {tempPath}"); serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={tempPath};Mode={DB_READ_WRITE_CREATE_MODE};"), ServiceLifetime.Transient); // Get the database service: await using var serviceProvider = serviceCollection.BuildServiceProvider(); var dbContext = serviceProvider.GetRequiredService(); + + Setup.USED_DATA_FILE = tempPath; + Setup.SETUP_MAINTENANCE = new(tempPath, true); // Migrate the database to create the tables etc.: await Setup.PerformDataMigration(dbContext); @@ -128,17 +129,29 @@ public static class Setup return dbContext; } - public readonly record struct SetupMaintenance(string PathToDataFile = "", bool RemoveTempDatabaseAfterwards = false) : IDisposable { public void Dispose() { if (!this.RemoveTempDatabaseAfterwards) return; - + + Console.WriteLine("Removing the temporary database file..."); try { - File.Delete(this.PathToDataFile); + var process = new Process + { + StartInfo = new() + { + FileName = "cmd.exe", + Arguments = $@"/C del /Q /F ""{Setup.SETUP_MAINTENANCE.PathToDataFile}""", + UseShellExecute = false, + CreateNoWindow = true, + } + }; + + process.Start(); + Console.WriteLine($"The temporary database file '{this.PathToDataFile}' has been removed."); } catch(Exception e) { diff --git a/I18N Commander/Processor/Version.cs b/I18N Commander/Processor/Version.cs index d6dce1e..6de87ef 100644 --- a/I18N Commander/Processor/Version.cs +++ b/I18N Commander/Processor/Version.cs @@ -2,5 +2,5 @@ public static class Version { - public static string Text => $"v0.8.2 (2023-02-11), .NET {Environment.Version}"; + public static string Text => $"v0.8.3 (2023-02-11), .NET {Environment.Version}"; } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Program.cs b/I18N Commander/UI WinForms/Program.cs index 3bedb40..68f2106 100644 --- a/I18N Commander/UI WinForms/Program.cs +++ b/I18N Commander/UI WinForms/Program.cs @@ -60,30 +60,31 @@ internal static class Program } }); - // Tear down the setup: - using var setupMaintenance = Setup.Maintenance; - // Get the host out of the DI system: - var host = builder.Build(); - - // Create a service scope: - using (var scope = host.Services.CreateScope()) + using (var host = builder.Build()) { - // Get a service provider: - SERVICE_PROVIDER = scope.ServiceProvider; - - // Set the service provider to the processor: - ProcessorMeta.ServiceProvider = SERVICE_PROVIDER; - - // Apply database migrations: - using (var database = SERVICE_PROVIDER.GetRequiredService()) - Setup.PerformDataMigration(database).Wait(); - - // Start the app: - do + // Create a service scope: + using (var scope = host.Services.CreateScope()) { - Application.Run(new Main()); - } while (Program.RestartMainApp); + // Get a service provider: + SERVICE_PROVIDER = scope.ServiceProvider; + + // Set the service provider to the processor: + ProcessorMeta.ServiceProvider = SERVICE_PROVIDER; + + // Apply database migrations: + using (var database = SERVICE_PROVIDER.GetRequiredService()) + Setup.PerformDataMigration(database).Wait(); + + // Start the app: + do + { + Application.Run(new Main()); + } while (Program.RestartMainApp); + } } + + // Tear down the setup: + AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => Setup.SETUP_MAINTENANCE.Dispose(); } } \ No newline at end of file From 882c2bba2bf299829943d3b6379c6c94c71150ec Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 12 Feb 2023 12:01:27 +0100 Subject: [PATCH 29/30] Fixed DeepL state after changing its settings --- I18N Commander/Processor/AppSettings.cs | 12 ++++++++++-- I18N Commander/Processor/DeepL.cs | 2 ++ I18N Commander/Processor/Version.cs | 2 +- I18N Commander/UI WinForms/Program.cs | 1 + 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/I18N Commander/Processor/AppSettings.cs b/I18N Commander/Processor/AppSettings.cs index 8987196..401be31 100644 --- a/I18N Commander/Processor/AppSettings.cs +++ b/I18N Commander/Processor/AppSettings.cs @@ -168,7 +168,11 @@ public static class AppSettings #region DeepL Mode - public static async Task SetDeepLMode(SettingDeepLMode mode) => await AppSettings.SetSetting(SettingNames.DEEPL_MODE, mode); + public static async Task SetDeepLMode(SettingDeepLMode mode) + { + DeepL.ResetState(); + await AppSettings.SetSetting(SettingNames.DEEPL_MODE, mode); + } public static async Task GetDeepLMode() => await AppSettings.GetSetting(SettingNames.DEEPL_MODE, SettingDeepLMode.DISABLED); @@ -176,7 +180,11 @@ public static class AppSettings #region DeepL API Key - public static async Task SetDeepLAPIKey(string apiKey) => await AppSettings.SetSetting(SettingNames.DEEPL_API_KEY, apiKey); + public static async Task SetDeepLAPIKey(string apiKey) + { + DeepL.ResetState(); + await AppSettings.SetSetting(SettingNames.DEEPL_API_KEY, apiKey); + } public static async Task GetDeepLAPIKey() => await AppSettings.GetSetting(SettingNames.DEEPL_API_KEY, string.Empty); diff --git a/I18N Commander/Processor/DeepL.cs b/I18N Commander/Processor/DeepL.cs index d942e9c..5ec72bd 100644 --- a/I18N Commander/Processor/DeepL.cs +++ b/I18N Commander/Processor/DeepL.cs @@ -76,4 +76,6 @@ public static class DeepL return string.Empty; } } + + public static void ResetState() => DEEPL_NOT_AVAILABLE = false; } \ No newline at end of file diff --git a/I18N Commander/Processor/Version.cs b/I18N Commander/Processor/Version.cs index 6de87ef..d02d278 100644 --- a/I18N Commander/Processor/Version.cs +++ b/I18N Commander/Processor/Version.cs @@ -2,5 +2,5 @@ public static class Version { - public static string Text => $"v0.8.3 (2023-02-11), .NET {Environment.Version}"; + public static string Text => $"v0.8.4 (2023-02-12), .NET {Environment.Version}"; } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Program.cs b/I18N Commander/UI WinForms/Program.cs index 68f2106..2b74052 100644 --- a/I18N Commander/UI WinForms/Program.cs +++ b/I18N Commander/UI WinForms/Program.cs @@ -79,6 +79,7 @@ internal static class Program // Start the app: do { + Processor.DeepL.ResetState(); Application.Run(new Main()); } while (Program.RestartMainApp); } From 58e0d86d5212efebc84a22c8a626c863d08909c3 Mon Sep 17 00:00:00 2001 From: Thorsten Sommer Date: Sun, 12 Feb 2023 13:49:01 +0100 Subject: [PATCH 30/30] Ask user for API token when no one was set --- I18N Commander/Processor/Version.cs | 2 +- I18N Commander/UI WinForms/Components/Main.cs | 29 ++++++++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/I18N Commander/Processor/Version.cs b/I18N Commander/Processor/Version.cs index d02d278..26ad95d 100644 --- a/I18N Commander/Processor/Version.cs +++ b/I18N Commander/Processor/Version.cs @@ -2,5 +2,5 @@ public static class Version { - public static string Text => $"v0.8.4 (2023-02-12), .NET {Environment.Version}"; + public static string Text => $"v0.8.5 (2023-02-12), .NET {Environment.Version}"; } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/Main.cs b/I18N Commander/UI WinForms/Components/Main.cs index ec44a6e..976b26c 100644 --- a/I18N Commander/UI WinForms/Components/Main.cs +++ b/I18N Commander/UI WinForms/Components/Main.cs @@ -1,4 +1,6 @@ -using Processor; +using DataModel.Database; +using Processor; +using UI_WinForms.Dialogs; namespace UI_WinForms.Components; @@ -11,6 +13,31 @@ public partial class Main : UserControl // Register the something changed event to trigger the export: AppEvents.WhenSomethingChanged += async (_, _) => await ExportProcessor.TriggerExport(); + + // Check, if the DeepL integration is enabled, but no API key is set: + this.Load += async (sender, args) => + { + var deepLAPIKey = await AppSettings.GetDeepLAPIKey(); + var deepLMode = await AppSettings.GetDeepLMode(); + if (deepLMode is not SettingDeepLMode.DISABLED && string.IsNullOrWhiteSpace(deepLAPIKey)) + { + // Show the input dialog to ask the user for the DeepL API key: + var inputDialog = InputDialog.Show(new InputDialog.Options + { + Title = "I18NCommander - DeepL API Key", + Message = "The DeepL integration is enabled, but there is no API key set. Do you want to set one now?", + OkButtonText = "Set API key", + CancelButtonText = "No, thanks", + ShowQuestionCheckbox = false, + }); + + if (inputDialog.DialogResult == DialogResult.OK) + { + await AppSettings.SetDeepLAPIKey(inputDialog.Text); + AppEvents.SettingsChanged(); + } + } + }; } private void tabControl_SelectedIndexChanged(object sender, EventArgs e)