Merge branch '48-ascii-export-and-import-for-git' into 'main'

Resolve "ASCII export and import for Git"

Closes #48

See merge request open-source/dotnet/i18n-commander!25
This commit is contained in:
Thorsten 2023-02-12 12:55:07 +00:00
commit 945a6e303a
32 changed files with 1332 additions and 101 deletions

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>

View File

@ -1,4 +1,7 @@
using Microsoft.EntityFrameworkCore;
using System.Linq.Expressions;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;
namespace DataModel.Database.Common;
@ -63,4 +66,318 @@ public sealed class DataContext : DbContext, IDataContext
#endregion
}
#region Export and import
private readonly record struct JsonData(
IList<JsonSetting> Settings,
IList<JsonSection> Sections,
IList<JsonTextElement> TextElements,
IList<JsonTranslation> Translations
);
/// <summary>
/// Represents a unique identifier for a JSON export and import.
/// </summary>
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();
}
/// <summary>
/// A JSON converter to serialize and deserialize JsonUniqueId instances.
/// </summary>
private sealed class JsonUniqueIdConverter : JsonConverter<JsonUniqueId>
{
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<JsonUniqueId> TextElements
);
internal readonly record struct JsonTextElement(
JsonUniqueId UniqueId,
string Code,
string Name,
bool IsMultiLine,
JsonUniqueId SectionUniqueId,
List<JsonUniqueId> Translations
);
internal readonly record struct JsonTranslation(
JsonUniqueId UniqueId,
string Culture,
string Text,
bool TranslateManual,
JsonUniqueId TextElementUniqueId
);
/// <summary>
/// Exports this database to a JSON file.
/// </summary>
/// <param name="path">The path to the JSON file.</param>
/// <param name="includeSensitiveData">When false, exclude sensitive data from export.</param>
public async Task ExportAsync(string path, bool includeSensitiveData = false)
{
Console.WriteLine("Exporting database to JSON file...");
var jsonSettings = new JsonSerializerOptions
{
WriteIndented = true,
Converters = { new JsonUniqueIdConverter() },
};
// Maintained list of sensitive data to be removed from the export:
var sensitiveSettingCodes = new HashSet<string>
{
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<JsonSetting> FilterSensitiveSettings(IEnumerable<JsonSetting> 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;
}
}
// 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(),
// 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.");
}
/// <summary>
/// Stores data needed to resolve a parent-child relationship.
/// </summary>
/// <param name="ParentId">The parent id we want to resolve.</param>
/// <param name="Entity">The entity for which we want to resolve the parent.</param>
/// <typeparam name="T">The type of the entity.</typeparam>
private readonly record struct TreeResolver<T>(Guid ParentId, T Entity);
/// <summary>
/// Imports data from a JSON file into an empty database.
/// </summary>
/// <param name="path">The path to the JSON export.</param>
/// <exception cref="InvalidOperationException">When the database is not empty.</exception>
public async Task ImportAsync(string path)
{
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.");
Console.WriteLine("Start importing data from JSON file...");
// 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<JsonData>(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<Guid, TreeResolver<Section>>();
var sectionToTextElements = new Dictionary<Guid, List<Guid>>();
// Read the data from the JSON file:
foreach (var section in jsonData.Sections)
{
// 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));
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)
{
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:
// -------------------------
// 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<Guid, TextElement>();
var textElementToTranslations = new Dictionary<Guid, List<Guid>>();
// 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<Guid, Translation>();
// 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();
Console.WriteLine("Finished importing data from JSON file.");
}
#endregion
#region Tools
internal Task LoadElementsAsync<TEntry, TProp>(TEntry entry, Expression<Func<TEntry, TProp?>> selector) where TEntry : class where TProp : class => this.Entry<TEntry>(entry).Reference(selector).LoadAsync();
#endregion
}

View File

@ -1,11 +1,28 @@
namespace DataModel.Database.Common;
using Microsoft.EntityFrameworkCore.Design;
public sealed class DataContextFactory
namespace DataModel.Database.Common;
/// <summary>
/// This factory is used by the EF tooling e.g. to create migrations.
/// </summary>
public sealed class DataContextFactory : IDesignTimeDbContextFactory<DataContext>
{
public IDataContext DataContext { get; private set; }
private const string ENV_EF_TOOLING_DATABASE = "ENV_EF_TOOLING_DATABASE";
#region Implementation of IDesignTimeDbContextFactory<out DataContext>
public void CreateDataContext(string path2Database)
/// <inheritdoc />
public DataContext CreateDbContext(string[] args)
{
this.DataContext = Setup.CreateDatabaseInstance(path2Database, 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);
}
return Setup.CreateDatabaseInstance4Tooling(dataFile, false);
}
#endregion
}

View File

@ -11,4 +11,6 @@ public interface IDataContext
public DbSet<TextElement> TextElements { get; set; }
public DbSet<Translation> Translations { get; set; }
public Task ExportAsync(string path, bool includeSensitiveData = false);
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using DataModel.Database.Common;
namespace DataModel.Database;
@ -6,6 +7,8 @@ public sealed class Section
{
[Key]
public int Id { get; set; }
public Guid UniqueId { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
@ -16,4 +19,33 @@ public sealed class Section
public Section? Parent { get; set; }
public List<TextElement> TextElements { get; set; } = new();
internal DataContext.JsonUniqueId JsonUniqueId => new(this.DataKey, this.UniqueId, "Sec");
internal DataContext.JsonSection ToJsonSection(DataContext db)
{
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,
Name = jsonSection.Name,
DataKey = jsonSection.DataKey,
Depth = jsonSection.Depth,
Parent = null,
TextElements = new(),
};
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using DataModel.Database.Common;
namespace DataModel.Database;
@ -7,6 +8,8 @@ public sealed class Setting
[Key]
public int Id { get; set; }
public Guid UniqueId { get; set; } = Guid.NewGuid();
public string Code { get; set; } = string.Empty;
public string TextValue { get; set; } = string.Empty;
@ -16,4 +19,26 @@ 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,
};
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,
};
}

View File

@ -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";
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel.DataAnnotations;
using DataModel.Database.Common;
namespace DataModel.Database;
@ -6,6 +7,8 @@ public sealed class TextElement
{
[Key]
public int Id { get; set; }
public Guid UniqueId { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty;
@ -13,7 +16,29 @@ public sealed class TextElement
public bool IsMultiLine { get; set; } = false;
public Section Section { get; set; }
public Section Section { get; set; } = new();
public List<Translation> 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(),
};
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(),
};
}

View File

@ -1,14 +1,38 @@
namespace DataModel.Database;
using DataModel.Database.Common;
namespace DataModel.Database;
public sealed class Translation
{
public int Id { get; set; }
public Guid UniqueId { get; set; } = Guid.NewGuid();
public TextElement TextElement { get; set; }
public TextElement TextElement { get; set; } = new();
public string Culture { get; set; } = "en-US";
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,
};
internal static Translation FromJsonTranslation(DataContext.JsonTranslation jsonTranslation) => new()
{
UniqueId = jsonTranslation.UniqueId.UniqueId,
Culture = jsonTranslation.Culture,
TranslateManual = jsonTranslation.TranslateManual,
Text = jsonTranslation.Text,
TextElement = new(),
};
}

View File

@ -0,0 +1,35 @@
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)
{
if(setting.UniqueId == Guid.Empty)
setting.UniqueId = Guid.NewGuid();
}
await foreach(var section in db.Sections)
{
if(section.UniqueId == Guid.Empty)
section.UniqueId = Guid.NewGuid();
}
await foreach (var textElement in db.TextElements)
{
if(textElement.UniqueId == Guid.Empty)
textElement.UniqueId = Guid.NewGuid();
}
await foreach (var translation in db.Translations)
{
if(translation.UniqueId == Guid.Empty)
translation.UniqueId = Guid.NewGuid();
}
await db.SaveChangesAsync();
}
}

View File

@ -0,0 +1,223 @@
// <auto-generated />
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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("DataKey")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("Depth")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int?>("ParentId")
.HasColumnType("INTEGER");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<bool>("BoolValue")
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("GuidValue")
.HasColumnType("TEXT");
b.Property<int>("IntegerValue")
.HasColumnType("INTEGER");
b.Property<string>("TextValue")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Code")
.IsRequired()
.HasColumnType("TEXT");
b.Property<bool>("IsMultiLine")
.HasColumnType("INTEGER");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("SectionId")
.HasColumnType("INTEGER");
b.Property<Guid>("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<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<string>("Culture")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Text")
.IsRequired()
.HasColumnType("TEXT");
b.Property<int>("TextElementId")
.HasColumnType("INTEGER");
b.Property<bool>("TranslateManual")
.HasColumnType("INTEGER");
b.Property<Guid>("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
}
}
}

View File

@ -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<Guid>(
name: "UniqueId",
table: "Translations",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "UniqueId",
table: "TextElements",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
name: "UniqueId",
table: "Settings",
type: "TEXT",
nullable: false,
defaultValue: new Guid("00000000-0000-0000-0000-000000000000"));
migrationBuilder.AddColumn<Guid>(
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");
}
}
}

View File

@ -37,6 +37,9 @@ namespace DataModel.Migrations
b.Property<int?>("ParentId")
.HasColumnType("INTEGER");
b.Property<Guid>("UniqueId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("DataKey");
@ -75,6 +78,9 @@ namespace DataModel.Migrations
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid>("UniqueId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("BoolValue");
@ -113,6 +119,9 @@ namespace DataModel.Migrations
b.Property<int>("SectionId")
.HasColumnType("INTEGER");
b.Property<Guid>("UniqueId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Code");
@ -148,6 +157,9 @@ namespace DataModel.Migrations
b.Property<bool>("TranslateManual")
.HasColumnType("INTEGER");
b.Property<Guid>("UniqueId")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("Culture");

View File

@ -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);
}

View File

@ -1,22 +1,24 @@
using DataModel.Database.Common;
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;
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";
private static string usedDataFile = string.Empty;
public static string DataFile => Setup.usedDataFile;
private static string USED_DATA_FILE = string.Empty;
public static SetupMaintenance SETUP_MAINTENANCE = new();
public static string DataFile => Setup.USED_DATA_FILE;
/// <summary>
/// Tries to migrate the data file.
/// Tries to migrate the database.
/// </summary>
public static async Task PerformDataMigration(DataContext dbContext)
{
@ -27,24 +29,94 @@ public static class Setup
}
await dbContext.Database.MigrateAsync();
//
// Post migration actions:
//
if (pendingMigrations.Contains("20221106193544_202211AddUniqueIds"))
await Script202211AddUniqueIds.PostMigrationAsync(dbContext);
}
/// <summary>
/// Imports a JSON file into a new database.
/// </summary>
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();
Console.WriteLine($"The temporary database file is: {tempPath}");
serviceCollection.AddDbContext<DataContext>(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<DataContext>();
Setup.USED_DATA_FILE = tempPath;
Setup.SETUP_MAINTENANCE = new(tempPath, true);
// 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);
//
// 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:
//
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:
//
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 _ = 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();
}
/// <summary>
/// Add the database to the DI system
/// Creates and adds the database instance to the DI system (extension method).
/// </summary>
public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true)
{
Setup.usedDataFile = path2DataFile;
Setup.USED_DATA_FILE = path2DataFile;
Setup.SETUP_MAINTENANCE = new(path2DataFile, false);
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient);
}
/// <summary>
/// Create the database instance from the given path.
/// Create the database instance from the given path. Used for the EF tooling.
/// </summary>
public static DataContext CreateDatabaseInstance(string path2DataFile, bool createWhenNecessary = true)
public static DataContext CreateDatabaseInstance4Tooling(string path2DataFile, bool createWhenNecessary = true)
{
// Store the path to the database:
Setup.usedDataFile = path2DataFile;
Setup.USED_DATA_FILE = path2DataFile;
Setup.SETUP_MAINTENANCE = new(path2DataFile, false);
// Create a database builder:
var builder = new DbContextOptionsBuilder<DataContext>();
@ -57,24 +129,34 @@ public static class Setup
return dbContext;
}
/// <summary>
/// Sets up the DI & db context ready for the EF tooling.
/// </summary>
public static IHostBuilder Setup4EFTooling(string[] args)
public readonly record struct SetupMaintenance(string PathToDataFile = "", bool RemoveTempDatabaseAfterwards = false) : IDisposable
{
var dataFile = Environment.GetEnvironmentVariable(ENV_EF_TOOLING_DATABASE);
if (string.IsNullOrWhiteSpace(dataFile))
public void Dispose()
{
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);
}
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices((hostContext, serviceCollection) =>
{
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={dataFile};Mode=ReadWriteCreate"));
});
if (!this.RemoveTempDatabaseAfterwards)
return;
return builder;
Console.WriteLine("Removing the temporary database file...");
try
{
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)
{
Console.WriteLine($"Failed to remove the temporary database file: {e.Message} // {e.InnerException?.Message}");
}
}
}
}

View File

@ -1,2 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=EF/@EntryIndexedValue">EF</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=JSON/@EntryIndexedValue">JSON</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=hwnd/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@ -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<SettingDeepLMode> 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<string> GetDeepLAPIKey() => await AppSettings.GetSetting(SettingNames.DEEPL_API_KEY, string.Empty);
@ -418,4 +426,40 @@ public static class AppSettings
#endregion
#endregion
#region Auto-Export Settings
#region Auto-Export Enabled/Disabled
public static async Task<bool> 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<string> 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<string> 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<bool> 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
}

View File

@ -54,19 +54,28 @@ 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;
}
}
public static void ResetState() => DEEPL_NOT_AVAILABLE = false;
}

View File

@ -0,0 +1,52 @@
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 SemaphoreSlim EXPORT_SEMAPHORE_BRICK_WALL = new(2);
private static readonly SemaphoreSlim EXPORT_SEMAPHORE_EXPORTING = new(1);
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_BRICK_WALL.WaitAsync(1))
return;
try
{
await EXPORT_SEMAPHORE_EXPORTING.WaitAsync();
await using var db = ProcessorMeta.ServiceProvider.GetRequiredService<DataContext>();
await db.ExportAsync(
path: Environment.ExpandEnvironmentVariables(
Path.Join(await AppSettings.GetAutoExportDestinationPath(),
await AppSettings.GetAutoExportFilename())
),
includeSensitiveData: await AppSettings.GetAutoExportSensitiveData()
);
}
finally
{
EXPORT_SEMAPHORE_EXPORTING.Release();
EXPORT_SEMAPHORE_BRICK_WALL.Release();
}
}
public static async Task TriggerExport()
{
if (!await AppSettings.GetAutoExportEnabled())
return;
EXPORT_TIMER.Stop();
EXPORT_TIMER.Start();
}
}

View File

@ -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.8.5 (2023-02-12), .NET {Environment.Version}";
}

View File

@ -4,15 +4,22 @@ 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 were
#region Event: Settings changed
internal static event EventHandler? WhenSettingsChanged;
@ -49,4 +56,21 @@ 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);
WhenSettingsChanged += (sender, args) => WhenSomethingChanged?.Invoke(sender, EventArgs.Empty);
}
#endregion
}

View File

@ -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<string>? LoadProject;
public event EventHandler<LoaderResult>? LoadProject;
public readonly record struct LoaderResult(LoaderAction Action, string DataFile);
public enum LoaderAction
{
NONE,
LOAD_PROJECT,
CREATE_NEW_PROJECT,
IMPORT_JSON,
}
}

View File

@ -1,4 +1,8 @@
namespace UI_WinForms.Components;
using DataModel.Database;
using Processor;
using UI_WinForms.Dialogs;
namespace UI_WinForms.Components;
public partial class Main : UserControl
{
@ -6,6 +10,34 @@ 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();
// 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)

View File

@ -685,9 +685,159 @@ public sealed partial class Setting : UserControl
return new Setting(settingData);
}
private static async Task<Setting> ShowGeneratorAutoExportEnabledSettingAsync()
{
var currentSetting = await AppSettings.GetAutoExportEnabled();
var settingData = new SettingUIData(
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.",
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);
await ExportProcessor.TriggerExport();
changeTrigger();
};
checkbox.Text = "Enable Auto-Export";
// Apply the desired layout:
checkbox.Dock = DockStyle.Fill;
return checkbox;
}
);
return new Setting(settingData);
}
private static async Task<Setting> ShowGeneratorAutoExportDestinationPathSettingAsync()
{
var currentSetting = await AppSettings.GetAutoExportDestinationPath();
var settingData = new SettingUIData(
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%.",
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<Setting> ShowGeneratorAutoExportFilenameSettingAsync()
{
var currentSetting = await AppSettings.GetAutoExportFilename();
var settingData = new SettingUIData(
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%.",
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<Setting> ShowGeneratorAutoExportExportSensitiveDataSettingAsync()
{
var currentSetting = await AppSettings.GetAutoExportSensitiveData();
var settingData = new SettingUIData(
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!",
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<Task<Setting>> 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 +845,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;

View File

@ -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<string>(this.loaderStart_LoadProject);
this.loaderStart.LoadProject += new System.EventHandler<LoaderStart.LoaderResult>(this.loaderStart_LoadProject);
//
// Loader
//

View File

@ -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();
}

View File

@ -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,31 +39,53 @@ 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).GetAwaiter().GetResult();
}
catch (JsonException e)
{
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);
}
}
});
// 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<DataContext>())
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<DataContext>())
Setup.PerformDataMigration(database).Wait();
// Start the app:
do
{
Processor.DeepL.ResetState();
Application.Run(new Main());
} while (Program.RestartMainApp);
}
}
// Tear down the setup:
AppDomain.CurrentDomain.ProcessExit += (sender, eventArgs) => Setup.SETUP_MAINTENANCE.Dispose();
}
}

View File

@ -210,6 +210,16 @@ namespace UI_WinForms.Resources {
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>
internal static System.Drawing.Bitmap icons8_git {
get {
object obj = ResourceManager.GetObject("icons8_git", resourceCulture);
return ((System.Drawing.Bitmap)(obj));
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Bitmap.
/// </summary>

View File

@ -163,6 +163,9 @@
<data name="icons8_folder_tree_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-folder-tree-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="icons8_git" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-git.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
<data name="icons8_increase_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>icons8-increase-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<OutputType Condition=" '$(Configuration)' == 'Release' ">WinExe</OutputType>
<OutputType Condition=" '$(Configuration)' == 'Debug' ">Exe</OutputType>
<TargetFramework>net6.0-windows10.0.22000.0</TargetFramework>
<RootNamespace>UI_WinForms</RootNamespace>
<Nullable>enable</Nullable>