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
This commit is contained in:
parent
7a742a18e7
commit
0587af18f2
@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>Exe</OutputType>
|
<OutputType>Library</OutputType>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
@ -167,11 +167,145 @@ public sealed class DataContext : DbContext, IDataContext
|
|||||||
}, jsonSettings);
|
}, jsonSettings);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task ImportAndLoadAsync(string path)
|
/// <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)
|
||||||
{
|
{
|
||||||
// We import that JSON data file into an new, empty database file
|
if(await this.Settings.AnyAsync() ||
|
||||||
// at a temporary location. Next, we enable the auto export feature
|
await this.Sections.AnyAsync() ||
|
||||||
// to keep the source file up-to-date.
|
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<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);
|
||||||
|
|
||||||
|
// 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<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();
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
@ -31,4 +31,14 @@ public sealed class Section
|
|||||||
ParentUniqueId = this.Parent?.JsonUniqueId ?? new DataContext.JsonUniqueId("null", Guid.Empty, "Sec"),
|
ParentUniqueId = this.Parent?.JsonUniqueId ?? new DataContext.JsonUniqueId("null", Guid.Empty, "Sec"),
|
||||||
TextElements = this.TextElements.Select(n => n.JsonUniqueId).ToList()
|
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(),
|
||||||
|
};
|
||||||
}
|
}
|
@ -31,4 +31,14 @@ public sealed class Setting
|
|||||||
IntegerValue = this.IntegerValue,
|
IntegerValue = this.IntegerValue,
|
||||||
TextValue = this.TextValue,
|
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,
|
||||||
|
};
|
||||||
}
|
}
|
@ -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_DOTNET_DEFAULT_CULTURE = "Generator .NET Default Culture";
|
||||||
public static readonly string GENERATOR_GODOT_ENABLED = "Generator Godot Enabled";
|
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 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";
|
||||||
}
|
}
|
@ -31,4 +31,14 @@ public sealed class TextElement
|
|||||||
SectionUniqueId = this.Section.JsonUniqueId,
|
SectionUniqueId = this.Section.JsonUniqueId,
|
||||||
Translations = this.Translations.Select(n => n.JsonUniqueId).ToList(),
|
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(),
|
||||||
|
};
|
||||||
}
|
}
|
@ -8,7 +8,7 @@ public sealed class Translation
|
|||||||
|
|
||||||
public Guid UniqueId { get; set; }
|
public Guid UniqueId { get; set; }
|
||||||
|
|
||||||
public TextElement TextElement { get; set; }
|
public TextElement TextElement { get; set; } = new();
|
||||||
|
|
||||||
public string Culture { get; set; } = "en-US";
|
public string Culture { get; set; } = "en-US";
|
||||||
|
|
||||||
@ -26,4 +26,13 @@ public sealed class Translation
|
|||||||
TextElementUniqueId = this.TextElement.JsonUniqueId,
|
TextElementUniqueId = this.TextElement.JsonUniqueId,
|
||||||
Text = this.Text,
|
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(),
|
||||||
|
};
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
using DataModel.Database.Common;
|
using DataModel.Database;
|
||||||
|
using DataModel.Database.Common;
|
||||||
using DataModel.MigrationScripts;
|
using DataModel.MigrationScripts;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
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_MODE = "ReadWrite";
|
||||||
private const string DB_READ_WRITE_CREATE_MODE = "ReadWriteCreate";
|
private const string DB_READ_WRITE_CREATE_MODE = "ReadWriteCreate";
|
||||||
|
|
||||||
private static string usedDataFile = string.Empty;
|
private static string USED_DATA_FILE = string.Empty;
|
||||||
|
private static SetupMaintenance SETUP_MAINTENANCE = new();
|
||||||
|
|
||||||
public static string DataFile => Setup.usedDataFile;
|
public static string DataFile => Setup.USED_DATA_FILE;
|
||||||
|
|
||||||
|
public static SetupMaintenance Maintenance => Setup.SETUP_MAINTENANCE;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Tries to migrate the database.
|
/// Tries to migrate the database.
|
||||||
@ -37,31 +41,52 @@ public static class Setup
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Add the database to the DI system
|
/// Imports a JSON file into a new database.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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;
|
var tempPath = Path.GetTempFileName();
|
||||||
serviceCollection.AddDbContext<DataContext>(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)};"), ServiceLifetime.Transient);
|
|
||||||
|
Setup.USED_DATA_FILE = tempPath;
|
||||||
|
Setup.SETUP_MAINTENANCE = new(path2JSONFile, true);
|
||||||
|
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>();
|
||||||
|
|
||||||
|
// 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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Create the database instance from the given path.
|
/// Creates and adds the database instance to the DI system (extension method).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
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.USED_DATA_FILE = path2DataFile;
|
||||||
Setup.usedDataFile = 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);
|
||||||
// Create a database builder:
|
|
||||||
var builder = new DbContextOptionsBuilder<DataContext>();
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -84,4 +109,22 @@ public static class Setup
|
|||||||
|
|
||||||
return builder;
|
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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
46
I18N Commander/Processor/ExportProcessor.cs
Normal file
46
I18N Commander/Processor/ExportProcessor.cs
Normal file
@ -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<DataContext>();
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
@ -2,5 +2,5 @@
|
|||||||
|
|
||||||
public static class Version
|
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}";
|
||||||
}
|
}
|
@ -4,12 +4,19 @@ namespace UI_WinForms;
|
|||||||
|
|
||||||
internal static class AppEvents
|
internal static class AppEvents
|
||||||
{
|
{
|
||||||
|
static AppEvents()
|
||||||
|
{
|
||||||
|
AppEvents.AddSomethingHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
internal static void ResetAllSubscriptions()
|
internal static void ResetAllSubscriptions()
|
||||||
{
|
{
|
||||||
WhenSettingsChanged = null;
|
WhenSettingsChanged = null;
|
||||||
WhenSectionChanged = null;
|
WhenSectionChanged = null;
|
||||||
WhenTextElementChanged = null;
|
WhenTextElementChanged = null;
|
||||||
WhenTranslationChanged = null;
|
WhenTranslationChanged = null;
|
||||||
|
|
||||||
|
AppEvents.AddSomethingHandlers();
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Event: Settings changed
|
#region Event: Settings changed
|
||||||
@ -49,4 +56,20 @@ internal static class AppEvents
|
|||||||
internal static void TranslationChanged(Translation? translation) => WhenTranslationChanged?.Invoke(null, translation);
|
internal static void TranslationChanged(Translation? translation) => WhenTranslationChanged?.Invoke(null, translation);
|
||||||
|
|
||||||
#endregion
|
#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
|
||||||
}
|
}
|
@ -62,6 +62,7 @@ public partial class LoaderStart : UserControl
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
// Opens the recent projects dropdown menu.
|
||||||
private void buttonOpen_Click(object sender, EventArgs e)
|
private void buttonOpen_Click(object sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if(this.DesignMode)
|
if(this.DesignMode)
|
||||||
@ -84,10 +85,20 @@ public partial class LoaderStart : UserControl
|
|||||||
// Split the file's path into each folder's name:
|
// Split the file's path into each folder's name:
|
||||||
var folderNames = fileInfo.DirectoryName!.Split(Path.DirectorySeparatorChar);
|
var folderNames = fileInfo.DirectoryName!.Split(Path.DirectorySeparatorChar);
|
||||||
|
|
||||||
|
// Distinguish between I18N Commander projects and JSON imports:
|
||||||
|
if (fileInfo.Extension == ".i18nc")
|
||||||
|
{
|
||||||
// Render this entry:
|
// Render this entry:
|
||||||
var item = this.contextMenuRecentProjects.Items.Add($"{folderNames.Last()}: {fileInfo.Name}", Resources.Icons.icons8_document_512, (innerSender, args) => this.OpenRecentProject(innerSender));
|
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;
|
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)!;
|
var button = (sender as Button)!;
|
||||||
this.contextMenuRecentProjects.Show(button, 0, button.Height);
|
this.contextMenuRecentProjects.Show(button, 0, button.Height);
|
||||||
@ -125,7 +136,7 @@ public partial class LoaderStart : UserControl
|
|||||||
File.Delete(destinationFilePath);
|
File.Delete(destinationFilePath);
|
||||||
|
|
||||||
LoaderStart.UpdateRecentProjectsWithPruning(destinationFilePath);
|
LoaderStart.UpdateRecentProjectsWithPruning(destinationFilePath);
|
||||||
this.OpenProject(destinationFilePath);
|
this.OpenProject(LoaderAction.CREATE_NEW_PROJECT, destinationFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BrowseForProject()
|
private void BrowseForProject()
|
||||||
@ -137,10 +148,12 @@ public partial class LoaderStart : UserControl
|
|||||||
CheckFileExists = true,
|
CheckFileExists = true,
|
||||||
DereferenceLinks = true,
|
DereferenceLinks = true,
|
||||||
DefaultExt = ".i18nc",
|
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,
|
Multiselect = false,
|
||||||
RestoreDirectory = true,
|
RestoreDirectory = true,
|
||||||
Title = "Open an I18N Commander file",
|
Title = "Open an I18N Commander file or Import a JSON file",
|
||||||
};
|
};
|
||||||
var dialogResult = openDialog.ShowDialog(this);
|
var dialogResult = openDialog.ShowDialog(this);
|
||||||
if (dialogResult != DialogResult.OK)
|
if (dialogResult != DialogResult.OK)
|
||||||
@ -148,23 +161,28 @@ public partial class LoaderStart : UserControl
|
|||||||
|
|
||||||
var projectFilePath = openDialog.FileName;
|
var projectFilePath = openDialog.FileName;
|
||||||
LoaderStart.UpdateRecentProjectsWithPruning(projectFilePath);
|
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)
|
if (sender is not ToolStripItem item)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var path = (item.Tag as string)!;
|
var path = (item.Tag as string)!;
|
||||||
LoaderStart.UpdateRecentProjectsWithPruning(path);
|
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)
|
// 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)
|
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.")]
|
[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,
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,4 +1,6 @@
|
|||||||
namespace UI_WinForms.Components;
|
using Processor;
|
||||||
|
|
||||||
|
namespace UI_WinForms.Components;
|
||||||
|
|
||||||
public partial class Main : UserControl
|
public partial class Main : UserControl
|
||||||
{
|
{
|
||||||
@ -6,6 +8,9 @@ public partial class Main : UserControl
|
|||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
Program.RestartMainApp = false;
|
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)
|
private void tabControl_SelectedIndexChanged(object sender, EventArgs e)
|
||||||
|
@ -690,7 +690,7 @@ public sealed partial class Setting : UserControl
|
|||||||
{
|
{
|
||||||
var currentSetting = await AppSettings.GetAutoExportEnabled();
|
var currentSetting = await AppSettings.GetAutoExportEnabled();
|
||||||
var settingData = new SettingUIData(
|
var settingData = new SettingUIData(
|
||||||
Icon: Icons.icons8_git_svg,
|
Icon: Icons.icons8_git,
|
||||||
SettingName: () => "Git (JSON) Auto-Export: Enabled",
|
SettingName: () => "Git (JSON) Auto-Export: Enabled",
|
||||||
ChangeNeedsRestart: true,
|
ChangeNeedsRestart: true,
|
||||||
SettingExplanation: () => "When enabled, all changes are automatically exported into a Git repository as JSON file.",
|
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:
|
// Set up an checkbox:
|
||||||
var checkbox = new CheckBox();
|
var checkbox = new CheckBox();
|
||||||
checkbox.Checked = currentSetting;
|
checkbox.Checked = currentSetting;
|
||||||
checkbox.CheckedChanged += async (sender, args) => await AppSettings.SetAutoExportEnabled(checkbox.Checked);
|
checkbox.CheckedChanged += async (sender, args) =>
|
||||||
checkbox.CheckedChanged += (sender, args) => changeTrigger();
|
{
|
||||||
checkbox.Text = "Enable Git (JSON) Auto-Export";
|
await AppSettings.SetAutoExportEnabled(checkbox.Checked);
|
||||||
|
await ExportProcessor.TriggerExport();
|
||||||
|
changeTrigger();
|
||||||
|
};
|
||||||
|
checkbox.Text = "Enable Auto-Export";
|
||||||
|
|
||||||
// Apply the desired layout:
|
// Apply the desired layout:
|
||||||
checkbox.Dock = DockStyle.Fill;
|
checkbox.Dock = DockStyle.Fill;
|
||||||
@ -717,7 +721,7 @@ public sealed partial class Setting : UserControl
|
|||||||
{
|
{
|
||||||
var currentSetting = await AppSettings.GetAutoExportDestinationPath();
|
var currentSetting = await AppSettings.GetAutoExportDestinationPath();
|
||||||
var settingData = new SettingUIData(
|
var settingData = new SettingUIData(
|
||||||
Icon: Icons.icons8_git_svg,
|
Icon: Icons.icons8_git,
|
||||||
SettingName: () => "Git (JSON) Auto-Export: Destination Path",
|
SettingName: () => "Git (JSON) Auto-Export: Destination Path",
|
||||||
ChangeNeedsRestart: true,
|
ChangeNeedsRestart: true,
|
||||||
SettingExplanation: () => "The destination path for the Git repository. You might use environment variables like %USERPROFILE%.",
|
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 currentSetting = await AppSettings.GetAutoExportFilename();
|
||||||
var settingData = new SettingUIData(
|
var settingData = new SettingUIData(
|
||||||
Icon: Icons.icons8_git_svg,
|
Icon: Icons.icons8_git,
|
||||||
SettingName: () => "Git (JSON) Auto-Export: Filename",
|
SettingName: () => "Git (JSON) Auto-Export: Filename",
|
||||||
ChangeNeedsRestart: true,
|
ChangeNeedsRestart: true,
|
||||||
SettingExplanation: () => "The filename used for the Git export. You might use environment variables like %USERPROFILE%.",
|
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 currentSetting = await AppSettings.GetAutoExportSensitiveData();
|
||||||
var settingData = new SettingUIData(
|
var settingData = new SettingUIData(
|
||||||
Icon: Icons.icons8_git_svg,
|
Icon: Icons.icons8_git,
|
||||||
SettingName: () => "Git (JSON) Auto-Export: Export Sensitive Data",
|
SettingName: () => "Git (JSON) Auto-Export: Export Sensitive Data",
|
||||||
ChangeNeedsRestart: true,
|
ChangeNeedsRestart: true,
|
||||||
SettingExplanation: () => "When enabled, sensitive data like API tokens are exported into the Git repository. This is not recommended!",
|
SettingExplanation: () => "When enabled, sensitive data like API tokens are exported into the Git repository. This is not recommended!",
|
||||||
|
6
I18N Commander/UI WinForms/Loader.Designer.cs
generated
6
I18N Commander/UI WinForms/Loader.Designer.cs
generated
@ -1,4 +1,6 @@
|
|||||||
namespace UI_WinForms
|
using UI_WinForms.Components;
|
||||||
|
|
||||||
|
namespace UI_WinForms
|
||||||
{
|
{
|
||||||
partial class Loader
|
partial class Loader
|
||||||
{
|
{
|
||||||
@ -57,7 +59,7 @@
|
|||||||
this.loaderStart.Name = "loaderStart";
|
this.loaderStart.Name = "loaderStart";
|
||||||
this.loaderStart.Size = new System.Drawing.Size(794, 444);
|
this.loaderStart.Size = new System.Drawing.Size(794, 444);
|
||||||
this.loaderStart.TabIndex = 0;
|
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
|
// Loader
|
||||||
//
|
//
|
||||||
|
@ -1,20 +1,22 @@
|
|||||||
namespace UI_WinForms;
|
using UI_WinForms.Components;
|
||||||
|
|
||||||
|
namespace UI_WinForms;
|
||||||
|
|
||||||
public partial class Loader : Form
|
public partial class Loader : Form
|
||||||
{
|
{
|
||||||
public string DataFile { get; set; } = string.Empty;
|
public LoaderStart.LoaderResult Result { get; private set; }
|
||||||
|
|
||||||
public Loader()
|
public Loader()
|
||||||
{
|
{
|
||||||
this.InitializeComponent();
|
this.InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loaderStart_LoadProject(object sender, string projectFilePath)
|
private void loaderStart_LoadProject(object sender, LoaderStart.LoaderResult result)
|
||||||
{
|
{
|
||||||
if(this.DesignMode)
|
if(this.DesignMode)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
this.DataFile = projectFilePath;
|
this.Result = result;
|
||||||
this.DialogResult = DialogResult.OK;
|
this.DialogResult = DialogResult.OK;
|
||||||
this.Close();
|
this.Close();
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,12 @@
|
|||||||
|
using System.Text.Json;
|
||||||
using DataModel;
|
using DataModel;
|
||||||
using DataModel.Database.Common;
|
using DataModel.Database.Common;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Processor;
|
using Processor;
|
||||||
|
|
||||||
|
using static UI_WinForms.Components.LoaderStart.LoaderAction;
|
||||||
|
|
||||||
namespace UI_WinForms;
|
namespace UI_WinForms;
|
||||||
|
|
||||||
internal static class Program
|
internal static class Program
|
||||||
@ -36,10 +39,25 @@ internal static class Program
|
|||||||
//
|
//
|
||||||
builder.ConfigureServices((hostContext, serviceCollection) =>
|
builder.ConfigureServices((hostContext, serviceCollection) =>
|
||||||
{
|
{
|
||||||
// The database:
|
if(loader.Result.Action is LOAD_PROJECT or CREATE_NEW_PROJECT)
|
||||||
serviceCollection.AddDatabase(loader.DataFile, true);
|
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:
|
// Get the host out of the DI system:
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
||||||
|
@ -213,9 +213,9 @@ namespace UI_WinForms.Resources {
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
/// Looks up a localized resource of type System.Drawing.Bitmap.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal static System.Drawing.Bitmap icons8_git_svg {
|
internal static System.Drawing.Bitmap icons8_git {
|
||||||
get {
|
get {
|
||||||
object obj = ResourceManager.GetObject("icons8_git_svg", resourceCulture);
|
object obj = ResourceManager.GetObject("icons8_git", resourceCulture);
|
||||||
return ((System.Drawing.Bitmap)(obj));
|
return ((System.Drawing.Bitmap)(obj));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,8 +163,8 @@
|
|||||||
<data name="icons8_folder_tree_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
<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>
|
<value>icons8-folder-tree-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="icons8_git_svg" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
<data name="icons8_git" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
||||||
<value>icons8-git.svg.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
<value>icons8-git.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||||
</data>
|
</data>
|
||||||
<data name="icons8_increase_512" type="System.Resources.ResXFileRef, System.Windows.Forms">
|
<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>
|
<value>icons8-increase-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
|
||||||
|
BIN
I18N Commander/UI WinForms/Resources/icons8-git.png
Normal file
BIN
I18N Commander/UI WinForms/Resources/icons8-git.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
Loading…
Reference in New Issue
Block a user