diff --git a/I18N Commander/.idea/.idea.I18N Commander/.idea/dataSources.xml b/I18N Commander/.idea/.idea.I18N Commander/.idea/dataSources.xml new file mode 100644 index 0000000..4c26172 --- /dev/null +++ b/I18N Commander/.idea/.idea.I18N Commander/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/../../../test.i18nc + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/I18N Commander/DataModel/Database/Common/DataContext.cs b/I18N Commander/DataModel/Database/Common/DataContext.cs index 3d9971d..08fe633 100644 --- a/I18N Commander/DataModel/Database/Common/DataContext.cs +++ b/I18N Commander/DataModel/Database/Common/DataContext.cs @@ -4,13 +4,13 @@ namespace DataModel.Database.Common; public sealed class DataContext : DbContext { - public DbSet? Settings { get; set; } + public DbSet Settings { get; set; } - public DbSet
? Sections { get; set; } + public DbSet
Sections { get; set; } - public DbSet? TextElements { get; set; } + public DbSet TextElements { get; set; } - public DbSet? Translations { get; set; } + public DbSet Translations { get; set; } public DataContext(DbContextOptions contextOptions) : base(contextOptions) { @@ -35,6 +35,8 @@ public sealed class DataContext : DbContext modelBuilder.Entity
().HasIndex(n => n.Id); modelBuilder.Entity
().HasIndex(n => n.Name); + modelBuilder.Entity
().HasIndex(n => n.Depth); + modelBuilder.Entity
().HasIndex(n => n.DataKey); #endregion diff --git a/I18N Commander/DataModel/Database/Section.cs b/I18N Commander/DataModel/Database/Section.cs index b7f4c60..195bdba 100644 --- a/I18N Commander/DataModel/Database/Section.cs +++ b/I18N Commander/DataModel/Database/Section.cs @@ -9,7 +9,11 @@ public sealed class Section public string Name { get; set; } = string.Empty; - public Section Parent { get; set; } + public string DataKey { get; set; } = string.Empty; - public List TextElements { get; set; } + public int Depth { get; set; } = 0; + + public Section? Parent { get; set; } + + public List TextElements { get; set; } = new(); } \ No newline at end of file diff --git a/I18N Commander/DataModel/Migrations/20220626195157_202206MadeParentNullable.Designer.cs b/I18N Commander/DataModel/Migrations/20220626195157_202206MadeParentNullable.Designer.cs new file mode 100644 index 0000000..621a1c0 --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220626195157_202206MadeParentNullable.Designer.cs @@ -0,0 +1,184 @@ +// +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("20220626195157_202206MadeParentNullable")] + partial class _202206MadeParentNullable + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id"); + + b.HasIndex("Name"); + + b.HasIndex("ParentId"); + + b.ToTable("Sections"); + }); + + modelBuilder.Entity("DataModel.Database.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoolValue") + .HasColumnType("INTEGER"); + + b.Property("GuidValue") + .HasColumnType("TEXT"); + + b.Property("IntegerValue") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BoolValue"); + + b.HasIndex("GuidValue"); + + b.HasIndex("Id"); + + b.HasIndex("IntegerValue"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("TextValue"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Culture") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextElementId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Culture"); + + b.HasIndex("Id"); + + b.HasIndex("Text"); + + b.HasIndex("TextElementId"); + + b.ToTable("Translations"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.HasOne("DataModel.Database.Section", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.HasOne("DataModel.Database.Section", "Section") + .WithMany("TextElements") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.HasOne("DataModel.Database.TextElement", "TextElement") + .WithMany("Translations") + .HasForeignKey("TextElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TextElement"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Navigation("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Navigation("Translations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/I18N Commander/DataModel/Migrations/20220626195157_202206MadeParentNullable.cs b/I18N Commander/DataModel/Migrations/20220626195157_202206MadeParentNullable.cs new file mode 100644 index 0000000..a04faba --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220626195157_202206MadeParentNullable.cs @@ -0,0 +1,56 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataModel.Migrations +{ + public partial class _202206MadeParentNullable : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Sections_Sections_ParentId", + table: "Sections"); + + migrationBuilder.AlterColumn( + name: "ParentId", + table: "Sections", + type: "INTEGER", + nullable: true, + oldClrType: typeof(int), + oldType: "INTEGER"); + + migrationBuilder.AddForeignKey( + name: "FK_Sections_Sections_ParentId", + table: "Sections", + column: "ParentId", + principalTable: "Sections", + principalColumn: "Id"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Sections_Sections_ParentId", + table: "Sections"); + + migrationBuilder.AlterColumn( + name: "ParentId", + table: "Sections", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(int), + oldType: "INTEGER", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_Sections_Sections_ParentId", + table: "Sections", + column: "ParentId", + principalTable: "Sections", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/I18N Commander/DataModel/Migrations/20220709094035_202207AddSectionDataKey.Designer.cs b/I18N Commander/DataModel/Migrations/20220709094035_202207AddSectionDataKey.Designer.cs new file mode 100644 index 0000000..b0818d3 --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220709094035_202207AddSectionDataKey.Designer.cs @@ -0,0 +1,190 @@ +// +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("20220709094035_202207AddSectionDataKey")] + partial class _202207AddSectionDataKey + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DataKey"); + + b.HasIndex("Id"); + + b.HasIndex("Name"); + + b.HasIndex("ParentId"); + + b.ToTable("Sections"); + }); + + modelBuilder.Entity("DataModel.Database.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoolValue") + .HasColumnType("INTEGER"); + + b.Property("GuidValue") + .HasColumnType("TEXT"); + + b.Property("IntegerValue") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BoolValue"); + + b.HasIndex("GuidValue"); + + b.HasIndex("Id"); + + b.HasIndex("IntegerValue"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("TextValue"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Culture") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextElementId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Culture"); + + b.HasIndex("Id"); + + b.HasIndex("Text"); + + b.HasIndex("TextElementId"); + + b.ToTable("Translations"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.HasOne("DataModel.Database.Section", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.HasOne("DataModel.Database.Section", "Section") + .WithMany("TextElements") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.HasOne("DataModel.Database.TextElement", "TextElement") + .WithMany("Translations") + .HasForeignKey("TextElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TextElement"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Navigation("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Navigation("Translations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/I18N Commander/DataModel/Migrations/20220709094035_202207AddSectionDataKey.cs b/I18N Commander/DataModel/Migrations/20220709094035_202207AddSectionDataKey.cs new file mode 100644 index 0000000..a29ef69 --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220709094035_202207AddSectionDataKey.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataModel.Migrations +{ + public partial class _202207AddSectionDataKey : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "DataKey", + table: "Sections", + type: "TEXT", + nullable: false, + defaultValue: ""); + + migrationBuilder.CreateIndex( + name: "IX_Sections_DataKey", + table: "Sections", + column: "DataKey"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Sections_DataKey", + table: "Sections"); + + migrationBuilder.DropColumn( + name: "DataKey", + table: "Sections"); + } + } +} diff --git a/I18N Commander/DataModel/Migrations/20220709095404_202207AddSectionDepth.Designer.cs b/I18N Commander/DataModel/Migrations/20220709095404_202207AddSectionDepth.Designer.cs new file mode 100644 index 0000000..b3e601f --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220709095404_202207AddSectionDepth.Designer.cs @@ -0,0 +1,195 @@ +// +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("20220709095404_202207AddSectionDepth")] + partial class _202207AddSectionDepth + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Depth") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ParentId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("DataKey"); + + b.HasIndex("Depth"); + + b.HasIndex("Id"); + + b.HasIndex("Name"); + + b.HasIndex("ParentId"); + + b.ToTable("Sections"); + }); + + modelBuilder.Entity("DataModel.Database.Setting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("BoolValue") + .HasColumnType("INTEGER"); + + b.Property("GuidValue") + .HasColumnType("TEXT"); + + b.Property("IntegerValue") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextValue") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BoolValue"); + + b.HasIndex("GuidValue"); + + b.HasIndex("Id"); + + b.HasIndex("IntegerValue"); + + b.HasIndex("Name") + .IsUnique(); + + b.HasIndex("TextValue"); + + b.ToTable("Settings"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("SectionId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Code"); + + b.HasIndex("Id"); + + b.HasIndex("SectionId"); + + b.ToTable("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Culture") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("TextElementId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Culture"); + + b.HasIndex("Id"); + + b.HasIndex("Text"); + + b.HasIndex("TextElementId"); + + b.ToTable("Translations"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.HasOne("DataModel.Database.Section", "Parent") + .WithMany() + .HasForeignKey("ParentId"); + + b.Navigation("Parent"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.HasOne("DataModel.Database.Section", "Section") + .WithMany("TextElements") + .HasForeignKey("SectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Section"); + }); + + modelBuilder.Entity("DataModel.Database.Translation", b => + { + b.HasOne("DataModel.Database.TextElement", "TextElement") + .WithMany("Translations") + .HasForeignKey("TextElementId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("TextElement"); + }); + + modelBuilder.Entity("DataModel.Database.Section", b => + { + b.Navigation("TextElements"); + }); + + modelBuilder.Entity("DataModel.Database.TextElement", b => + { + b.Navigation("Translations"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/I18N Commander/DataModel/Migrations/20220709095404_202207AddSectionDepth.cs b/I18N Commander/DataModel/Migrations/20220709095404_202207AddSectionDepth.cs new file mode 100644 index 0000000..7829b3e --- /dev/null +++ b/I18N Commander/DataModel/Migrations/20220709095404_202207AddSectionDepth.cs @@ -0,0 +1,35 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace DataModel.Migrations +{ + public partial class _202207AddSectionDepth : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Depth", + table: "Sections", + type: "INTEGER", + nullable: false, + defaultValue: 0); + + migrationBuilder.CreateIndex( + name: "IX_Sections_Depth", + table: "Sections", + column: "Depth"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_Sections_Depth", + table: "Sections"); + + migrationBuilder.DropColumn( + name: "Depth", + table: "Sections"); + } + } +} diff --git a/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs b/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs index fd8d8b5..6a285f4 100644 --- a/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs +++ b/I18N Commander/DataModel/Migrations/DataContextModelSnapshot.cs @@ -23,15 +23,26 @@ namespace DataModel.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); + b.Property("DataKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Depth") + .HasColumnType("INTEGER"); + b.Property("Name") .IsRequired() .HasColumnType("TEXT"); - b.Property("ParentId") + b.Property("ParentId") .HasColumnType("INTEGER"); b.HasKey("Id"); + b.HasIndex("DataKey"); + + b.HasIndex("Depth"); + b.HasIndex("Id"); b.HasIndex("Name"); @@ -140,9 +151,7 @@ namespace DataModel.Migrations { b.HasOne("DataModel.Database.Section", "Parent") .WithMany() - .HasForeignKey("ParentId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("ParentId"); b.Navigation("Parent"); }); diff --git a/I18N Commander/DataModel/Setup.cs b/I18N Commander/DataModel/Setup.cs index 44e32d8..ed1388d 100644 --- a/I18N Commander/DataModel/Setup.cs +++ b/I18N Commander/DataModel/Setup.cs @@ -30,7 +30,7 @@ public static class Setup /// public static void AddDatabase(this IServiceCollection serviceCollection, string path2DataFile, bool createWhenNecessary = true) { - serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)}")); + serviceCollection.AddDbContext(options => options.UseSqlite($"Filename={path2DataFile};Mode={(createWhenNecessary ? DB_READ_WRITE_CREATE_MODE : DB_READ_WRITE_MODE)}"), ServiceLifetime.Transient); } /// diff --git a/I18N Commander/Processor/SectionProcessor.cs b/I18N Commander/Processor/SectionProcessor.cs new file mode 100644 index 0000000..be52f29 --- /dev/null +++ b/I18N Commander/Processor/SectionProcessor.cs @@ -0,0 +1,111 @@ +using DataModel.Database; +using DataModel.Database.Common; +using Microsoft.EntityFrameworkCore; + +namespace Processor; + +public static class SectionProcessor +{ + /// + /// Load one layer of the tree by using the specified depth: + /// + public static IAsyncEnumerable
LoadLayer(DataContext db, int depth) + { + return db.Sections.Where(n => n.Depth == depth).OrderBy(n => n.Id).AsAsyncEnumerable(); + } + + /// + /// Determine how deep the tree is. + /// + public static async ValueTask GetDepth(DataContext db) + { + if(!await db.Sections.AnyAsync()) + { + return 0; + } + + return await db.Sections.MaxAsync(s => s.Depth); + } + + /// + /// Compute the new sections key and its depth, then store the section in the database. + /// + public static async Task
AddSection(DataContext db, string text, string? parentKey) + { + // Remove any whitespaces from the section name, regardless of how many e.g. spaces the user typed: + var key = string.Join('_', text.Split(' ', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries)).ToUpperInvariant(); + + // Check, if this key already exists: + if (await db.Sections.AnyAsync(n => n.DataKey == key)) + { + var rng = new Random(); + while (await db.Sections.AnyAsync(n => n.DataKey == key)) + { + // Add a random number to the end of the key: + key += $"_{rng.Next(1, 10_000)}"; + } + } + + // In the case, when the user adds a section to the root, handle the insert differently: + if (string.IsNullOrEmpty(parentKey)) + { + var rootSection = new Section + { + Depth = 0, + DataKey = key, + Parent = null, + Name = text.Trim(), + TextElements = new(), + }; + + db.Sections.Add(rootSection); + await db.SaveChangesAsync(); + return rootSection; + } + + // Read the parent from the database: + var parent = await db.Sections.FirstOrDefaultAsync(n => n.DataKey == parentKey); + if (parent is null) + throw new ArgumentException($"The section's parent with key {parentKey} does not exist in the database."); + + // Add the new section to the database: + var section = new Section + { + Name = text.Trim(), + DataKey = key, + Parent = parent, + TextElements = new(), + Depth = parent.Depth + 1, + }; + + db.Sections.Add(section); + await db.SaveChangesAsync(); + return section; + } + + public static async Task RemoveSection(DataContext db, string selectedKey) + { + // Remove the section from the database: + var section2Delete = await db.Sections.FirstOrDefaultAsync(n => n.DataKey == selectedKey); + if (section2Delete is null) + throw new ArgumentException($"The section with key {selectedKey} does not exist in the database."); + + // Next, remove all children of the section, and the children's children, etc.: + var children = await db.Sections.Where(n => n.Parent == section2Delete).ToListAsync(); + foreach (var child in children) + await RemoveSection(db, child.DataKey); + + db.Sections.Remove(section2Delete); + await db.SaveChangesAsync(); + } + + public static async Task NumberChildren(DataContext db, string selectedKey) + { + // Read the section from the database: + var section = await db.Sections.FirstOrDefaultAsync(n => n.DataKey == selectedKey); + if (section is null) + throw new ArgumentException($"The section with key {selectedKey} does not exist in the database."); + + return await db.Sections.CountAsync(n => n.Parent == section); + } +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/LoaderStart.cs b/I18N Commander/UI WinForms/Components/LoaderStart.cs index ad55a5f..052fe29 100644 --- a/I18N Commander/UI WinForms/Components/LoaderStart.cs +++ b/I18N Commander/UI WinForms/Components/LoaderStart.cs @@ -13,10 +13,10 @@ public partial class LoaderStart : UserControl this.InitializeComponent(); } - private static RegistryKey I18NCommanderKey => Registry.CurrentUser.OpenSubKey("Software", RegistryKeyPermissionCheck.ReadWriteSubTree)!.CreateSubKey("I18N Commander", RegistryKeyPermissionCheck.ReadWriteSubTree); - #region Recent Projects + private static RegistryKey I18NCommanderKey => Registry.CurrentUser.OpenSubKey("Software", RegistryKeyPermissionCheck.ReadWriteSubTree)!.CreateSubKey("I18N Commander", RegistryKeyPermissionCheck.ReadWriteSubTree); + /// /// Gets or sets the list of recent projects without any extras e.g. it does not prune the list of projects. /// @@ -43,7 +43,15 @@ public partial class LoaderStart : UserControl private static void UpdateRecentProjectsWithPruning(string latestUsedProjectPath) { var previousRecentList = LoaderStart.RecentProjects; + + // Check, if the latest project is identical to the next project we open. In that case, we don't add it to the list. + if (previousRecentList.Any() && previousRecentList[0] == latestUsedProjectPath) + return; + + // Add the next project to the list: previousRecentList.Insert(0, latestUsedProjectPath); + + // Prune the list: if (previousRecentList.Count > 6) previousRecentList = previousRecentList.Take(6).ToList(); @@ -67,7 +75,12 @@ public partial class LoaderStart : UserControl foreach (var recentProject in recentProjects) { var fileInfo = new FileInfo(recentProject); - var item = this.contextMenuRecentProjects.Items.Add($"{fileInfo.Directory.GetDirectories().Last()}/{fileInfo.Name}", Resources.Icons.icons8_document_512, (innerSender, args) => this.OpenRecentProject(innerSender)); + + // 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; } @@ -97,6 +110,12 @@ public partial class LoaderStart : UserControl return; var destinationFilePath = saveDialog.FileName; + + // When the user chose an existing file, we delete it: + // (note: the user already accepts overwriting the file) + if (File.Exists(destinationFilePath)) + File.Delete(destinationFilePath); + LoaderStart.UpdateRecentProjectsWithPruning(destinationFilePath); this.OpenProject(destinationFilePath); } diff --git a/I18N Commander/UI WinForms/Components/Main.Designer.cs b/I18N Commander/UI WinForms/Components/Main.Designer.cs new file mode 100644 index 0000000..6e3aef1 --- /dev/null +++ b/I18N Commander/UI WinForms/Components/Main.Designer.cs @@ -0,0 +1,114 @@ +namespace UI_WinForms.Components +{ + partial class Main + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tableLayout = new System.Windows.Forms.TableLayoutPanel(); + this.flowLayoutBottom = new System.Windows.Forms.FlowLayoutPanel(); + this.splitContainer = new System.Windows.Forms.SplitContainer(); + this.sectionTree = new UI_WinForms.Components.SectionTree(); + this.tableLayout.SuspendLayout(); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).BeginInit(); + this.splitContainer.Panel1.SuspendLayout(); + this.splitContainer.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayout + // + this.tableLayout.ColumnCount = 1; + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20F)); + this.tableLayout.Controls.Add(this.flowLayoutBottom, 0, 1); + this.tableLayout.Controls.Add(this.splitContainer, 0, 0); + this.tableLayout.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayout.Location = new System.Drawing.Point(0, 0); + this.tableLayout.Name = "tableLayout"; + this.tableLayout.RowCount = 2; + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 66F)); + this.tableLayout.Size = new System.Drawing.Size(965, 603); + this.tableLayout.TabIndex = 0; + // + // flowLayoutBottom + // + this.flowLayoutBottom.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutBottom.Location = new System.Drawing.Point(0, 537); + this.flowLayoutBottom.Margin = new System.Windows.Forms.Padding(0); + this.flowLayoutBottom.Name = "flowLayoutBottom"; + this.flowLayoutBottom.Size = new System.Drawing.Size(965, 66); + this.flowLayoutBottom.TabIndex = 0; + // + // splitContainer + // + this.splitContainer.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.splitContainer.Dock = System.Windows.Forms.DockStyle.Fill; + this.splitContainer.FixedPanel = System.Windows.Forms.FixedPanel.Panel1; + this.splitContainer.Location = new System.Drawing.Point(3, 3); + this.splitContainer.Name = "splitContainer"; + // + // splitContainer.Panel1 + // + this.splitContainer.Panel1.Controls.Add(this.sectionTree); + this.splitContainer.Panel1MinSize = 300; + this.splitContainer.Size = new System.Drawing.Size(959, 531); + this.splitContainer.SplitterDistance = 319; + this.splitContainer.TabIndex = 1; + // + // sectionTree + // + this.sectionTree.Dock = System.Windows.Forms.DockStyle.Fill; + this.sectionTree.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.sectionTree.Location = new System.Drawing.Point(0, 0); + this.sectionTree.Name = "sectionTree"; + this.sectionTree.Size = new System.Drawing.Size(317, 529); + this.sectionTree.TabIndex = 0; + // + // Main + // + this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.Controls.Add(this.tableLayout); + this.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.Name = "Main"; + this.Size = new System.Drawing.Size(965, 603); + this.tableLayout.ResumeLayout(false); + this.splitContainer.Panel1.ResumeLayout(false); + ((System.ComponentModel.ISupportInitialize)(this.splitContainer)).EndInit(); + this.splitContainer.ResumeLayout(false); + this.ResumeLayout(false); + + } + + #endregion + + private TableLayoutPanel tableLayout; + private FlowLayoutPanel flowLayoutBottom; + private SplitContainer splitContainer; + private SectionTree sectionTree; + } +} diff --git a/I18N Commander/UI WinForms/Components/Main.cs b/I18N Commander/UI WinForms/Components/Main.cs new file mode 100644 index 0000000..a25e38f --- /dev/null +++ b/I18N Commander/UI WinForms/Components/Main.cs @@ -0,0 +1,9 @@ +namespace UI_WinForms.Components; + +public partial class Main : UserControl +{ + public Main() + { + this.InitializeComponent(); + } +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/Main.resx b/I18N Commander/UI WinForms/Components/Main.resx new file mode 100644 index 0000000..b5ae26c --- /dev/null +++ b/I18N Commander/UI WinForms/Components/Main.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/SectionTree.Designer.cs b/I18N Commander/UI WinForms/Components/SectionTree.Designer.cs new file mode 100644 index 0000000..9cf2bb7 --- /dev/null +++ b/I18N Commander/UI WinForms/Components/SectionTree.Designer.cs @@ -0,0 +1,131 @@ +namespace UI_WinForms.Components +{ + partial class SectionTree + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Component Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tableLayout = new System.Windows.Forms.TableLayoutPanel(); + this.flowLayoutBottom = new System.Windows.Forms.FlowLayoutPanel(); + this.buttonAdd = new System.Windows.Forms.Button(); + this.buttonRemove = new System.Windows.Forms.Button(); + this.treeView = new System.Windows.Forms.TreeView(); + this.tableLayout.SuspendLayout(); + this.flowLayoutBottom.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayout + // + this.tableLayout.ColumnCount = 1; + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Absolute, 20F)); + this.tableLayout.Controls.Add(this.flowLayoutBottom, 0, 1); + this.tableLayout.Controls.Add(this.treeView, 0, 0); + this.tableLayout.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayout.Location = new System.Drawing.Point(0, 0); + this.tableLayout.Name = "tableLayout"; + this.tableLayout.RowCount = 2; + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 66F)); + this.tableLayout.Size = new System.Drawing.Size(296, 511); + this.tableLayout.TabIndex = 0; + // + // flowLayoutBottom + // + this.flowLayoutBottom.Controls.Add(this.buttonAdd); + this.flowLayoutBottom.Controls.Add(this.buttonRemove); + this.flowLayoutBottom.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutBottom.Location = new System.Drawing.Point(0, 445); + this.flowLayoutBottom.Margin = new System.Windows.Forms.Padding(0); + this.flowLayoutBottom.Name = "flowLayoutBottom"; + this.flowLayoutBottom.Size = new System.Drawing.Size(296, 66); + this.flowLayoutBottom.TabIndex = 0; + // + // buttonAdd + // + this.buttonAdd.AutoSize = true; + this.buttonAdd.Image = global::UI_WinForms.Resources.Icons.icons8_add_folder_512; + this.buttonAdd.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.buttonAdd.Location = new System.Drawing.Point(3, 3); + this.buttonAdd.Name = "buttonAdd"; + this.buttonAdd.Size = new System.Drawing.Size(138, 60); + this.buttonAdd.TabIndex = 0; + this.buttonAdd.Text = "Add"; + this.buttonAdd.TextImageRelation = System.Windows.Forms.TextImageRelation.ImageBeforeText; + this.buttonAdd.UseVisualStyleBackColor = true; + this.buttonAdd.Click += new System.EventHandler(this.buttonAdd_Click); + // + // buttonRemove + // + this.buttonRemove.AutoSize = true; + this.buttonRemove.Enabled = false; + this.buttonRemove.Image = global::UI_WinForms.Resources.Icons.icons8_delete_folder_512; + this.buttonRemove.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.buttonRemove.Location = new System.Drawing.Point(147, 3); + this.buttonRemove.Name = "buttonRemove"; + this.buttonRemove.Size = new System.Drawing.Size(138, 60); + this.buttonRemove.TabIndex = 1; + this.buttonRemove.Text = "Remove"; + this.buttonRemove.TextImageRelation = System.Windows.Forms.TextImageRelation.ImageBeforeText; + this.buttonRemove.UseVisualStyleBackColor = true; + this.buttonRemove.Click += new System.EventHandler(this.buttonRemove_Click); + // + // treeView + // + this.treeView.BorderStyle = System.Windows.Forms.BorderStyle.FixedSingle; + this.treeView.Dock = System.Windows.Forms.DockStyle.Fill; + this.treeView.HideSelection = false; + this.treeView.ItemHeight = 50; + this.treeView.Location = new System.Drawing.Point(3, 3); + this.treeView.Name = "treeView"; + this.treeView.Size = new System.Drawing.Size(290, 439); + this.treeView.TabIndex = 1; + this.treeView.NodeMouseClick += new System.Windows.Forms.TreeNodeMouseClickEventHandler(this.treeView_NodeMouseClick); + // + // SectionTree + // + this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.Controls.Add(this.tableLayout); + this.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.Name = "SectionTree"; + this.Size = new System.Drawing.Size(296, 511); + this.tableLayout.ResumeLayout(false); + this.flowLayoutBottom.ResumeLayout(false); + this.flowLayoutBottom.PerformLayout(); + this.ResumeLayout(false); + + } + + #endregion + + private TableLayoutPanel tableLayout; + private FlowLayoutPanel flowLayoutBottom; + private Button buttonAdd; + private Button buttonRemove; + private TreeView treeView; + } +} diff --git a/I18N Commander/UI WinForms/Components/SectionTree.cs b/I18N Commander/UI WinForms/Components/SectionTree.cs new file mode 100644 index 0000000..1ecf74a --- /dev/null +++ b/I18N Commander/UI WinForms/Components/SectionTree.cs @@ -0,0 +1,184 @@ +using DataModel.Database.Common; +using Microsoft.Extensions.DependencyInjection; +using Processor; +using UI_WinForms.Dialogs; +using UI_WinForms.Resources; + +namespace UI_WinForms.Components; + +public partial class SectionTree : UserControl +{ + private readonly DataContext db; + + public SectionTree() + { + this.InitializeComponent(); + + // Get the DI context from the main form: + this.db = Program.SERVICE_PROVIDER.GetService()!; + + // Dispose of the context when the control is disposed: + this.Disposed += (_, _) => this.db.Dispose(); + + // Create an image list from a resource: + var imgList = new ImageList(); + imgList.ImageSize = new Size(45, 45); + imgList.ColorDepth = ColorDepth.Depth32Bit; + imgList.Images.Add(Icons.icons8_documents_folder_512); + + // Set the image list to the tree view: + this.treeView.ImageList = imgList; + + // Subscribe to the load event: + this.Load += this.LoadNodes; + } + + private async void LoadNodes(object? sender, EventArgs e) + { + // A dictionary to cache all known tree nodes: + var treeNodes = new Dictionary(); + + // Get the max. depth of the tree: + var maxDepth = await SectionProcessor.GetDepth(this.db); + + // Store nodes, where we cannot find the parents: + var missingParents = new List(); + + // Populate the tree view out of the database, layer by layer: + for (var i = 0; i <= maxDepth; i++) + { + await foreach (var section in SectionProcessor.LoadLayer(this.db, i)) + { + // Create the tree node: + var node = new TreeNode + { + Name = section.DataKey, // [sic] name is the key + Text = section.Name, + StateImageIndex = 1, + }; + + // Cache the node: + treeNodes.Add(section.DataKey, node); + + // Is this a root node? + if (section.Depth is 0) + + // Set the root node: + this.treeView.Nodes.Add(node); + + // Otherwise, attach this section to its parent node: + else + { + // Get the parent from our cache within O(1): + treeNodes.TryGetValue(section.Parent?.DataKey ?? string.Empty, out var parent); + + // If the parent node is not found, skip this section: + if (parent is null) + { + missingParents.Add(node); + continue; + } + + // Add the node to the parent: + parent.Nodes.Add(node); + } + } + } + + // If we found any missing parents, show a dialog: + if (missingParents.Any()) + { + MessageBox.Show($"In {missingParents.Count} case(s) we could not found the matching parent. We added these nodes to a special root node, though.", "Parent not found", MessageBoxButtons.OK, MessageBoxIcon.Error); + + // Create a root node for all missing parents: + var rootMissedParents = new TreeNode + { + Name = "MISSING_PARENTS", + Text = "Missing Parents", + StateImageIndex = 1, + }; + + // Add the root node to the tree: + this.treeView.Nodes.Add(rootMissedParents); + + // Add all missing parents to the root node: + foreach (var node in missingParents) + rootMissedParents.Nodes.Add(node); + } + + // Expand the tree: + this.treeView.ExpandAll(); + } + + private async void buttonAdd_Click(object sender, EventArgs e) + { + var result = InputDialog.Show(new InputDialog.Options( + Message: "Please type the desired section name.", + Title: "Add a section", + Placeholder: "My next section", + ShowQuestionCheckbox: true, + QuestionCheckboxText: "Add a root node (i.e. ignoring the selected node)" + )); + + if(result.DialogResult == DialogResult.Cancel) + return; + + var addRootNode = result.AnswerToQuestion; + + // Get the currently selected section as parent: + var selectedNode = this.treeView.SelectedNode; + + // Add the new section to the database: + var addedSection = await SectionProcessor.AddSection(this.db, result.Text, addRootNode ? null : selectedNode?.Name); + + // Add the new section to the tree control: + var node = new TreeNode + { + Name = addedSection.DataKey, // [sic] name is the key + Text = addedSection.Name, + StateImageIndex = 1, + }; + + if(!addRootNode && selectedNode is not null) + selectedNode.Nodes.Add(node); + else + this.treeView.Nodes.Add(node); + + // Ensure, that the added node is visible and gets the focus: + node.EnsureVisible(); + this.treeView.SelectedNode = node; + } + + private async void buttonRemove_Click(object sender, EventArgs e) + { + // Get the currently selected section, which will be removed: + var selectedNode = this.treeView.SelectedNode; + + // Get the number of children: + // (notice, that the node's name is its key) + var numberChildren = await SectionProcessor.NumberChildren(this.db, selectedNode.Name); + + // Ask the user, if he really wants to remove the section: + if(MessageBox.Show(numberChildren > 0 ? $"Are you sure, you want to remove the section '{selectedNode.Text}', its {numberChildren} children and so on?" : $"Are you sure, you want to remove the section '{selectedNode.Text}'?", "Remove section", MessageBoxButtons.YesNo, MessageBoxIcon.Question) == DialogResult.No) + return; + + // Remove the section from the database: + // (notice, that the node's name is its key) + await SectionProcessor.RemoveSection(this.db, selectedNode.Name); + + // Remove all nodes from the tree control: + this.treeView.Nodes.Clear(); + + // Reload the tree: + this.LoadNodes(this, EventArgs.Empty); + } + + private void treeView_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e) + { + // Get the currently selected section: + var selectedNode = this.treeView.SelectedNode; + + // If the selected node is not null, enable the remove button: + this.buttonRemove.Enabled = selectedNode is not null; + } +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Components/SectionTree.resx b/I18N Commander/UI WinForms/Components/SectionTree.resx new file mode 100644 index 0000000..b5ae26c --- /dev/null +++ b/I18N Commander/UI WinForms/Components/SectionTree.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Dialogs/InputDialog.Designer.cs b/I18N Commander/UI WinForms/Dialogs/InputDialog.Designer.cs new file mode 100644 index 0000000..7426653 --- /dev/null +++ b/I18N Commander/UI WinForms/Dialogs/InputDialog.Designer.cs @@ -0,0 +1,165 @@ +namespace UI_WinForms.Dialogs +{ + partial class InputDialog + { + /// + /// Required designer variable. + /// + private System.ComponentModel.IContainer components = null; + + /// + /// Clean up any resources being used. + /// + /// true if managed resources should be disposed; otherwise, false. + protected override void Dispose(bool disposing) + { + if (disposing && (components != null)) + { + components.Dispose(); + } + base.Dispose(disposing); + } + + #region Windows Form Designer generated code + + /// + /// Required method for Designer support - do not modify + /// the contents of this method with the code editor. + /// + private void InitializeComponent() + { + this.tableLayout = new System.Windows.Forms.TableLayoutPanel(); + this.labelHead = new System.Windows.Forms.Label(); + this.flowLayoutBottom = new System.Windows.Forms.FlowLayoutPanel(); + this.buttonOk = new System.Windows.Forms.Button(); + this.buttonCancel = new System.Windows.Forms.Button(); + this.textBoxInput = new System.Windows.Forms.TextBox(); + this.checkBoxQuestion = new System.Windows.Forms.CheckBox(); + this.tableLayout.SuspendLayout(); + this.flowLayoutBottom.SuspendLayout(); + this.SuspendLayout(); + // + // tableLayout + // + this.tableLayout.AutoSize = true; + this.tableLayout.ColumnCount = 1; + this.tableLayout.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.Controls.Add(this.labelHead, 0, 0); + this.tableLayout.Controls.Add(this.flowLayoutBottom, 0, 2); + this.tableLayout.Controls.Add(this.textBoxInput, 0, 1); + this.tableLayout.Dock = System.Windows.Forms.DockStyle.Fill; + this.tableLayout.Location = new System.Drawing.Point(0, 0); + this.tableLayout.Name = "tableLayout"; + this.tableLayout.RowCount = 4; + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle()); + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Absolute, 66F)); + this.tableLayout.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 100F)); + this.tableLayout.Size = new System.Drawing.Size(800, 139); + this.tableLayout.TabIndex = 0; + // + // labelHead + // + this.labelHead.AutoSize = true; + this.labelHead.Dock = System.Windows.Forms.DockStyle.Fill; + this.labelHead.Location = new System.Drawing.Point(3, 0); + this.labelHead.Name = "labelHead"; + this.labelHead.Size = new System.Drawing.Size(794, 28); + this.labelHead.TabIndex = 0; + this.labelHead.Text = "header"; + // + // flowLayoutBottom + // + this.flowLayoutBottom.Controls.Add(this.buttonOk); + this.flowLayoutBottom.Controls.Add(this.buttonCancel); + this.flowLayoutBottom.Controls.Add(this.checkBoxQuestion); + this.flowLayoutBottom.Dock = System.Windows.Forms.DockStyle.Fill; + this.flowLayoutBottom.Location = new System.Drawing.Point(3, 68); + this.flowLayoutBottom.Margin = new System.Windows.Forms.Padding(3, 0, 0, 0); + this.flowLayoutBottom.Name = "flowLayoutBottom"; + this.flowLayoutBottom.Size = new System.Drawing.Size(797, 66); + this.flowLayoutBottom.TabIndex = 0; + // + // buttonOk + // + this.buttonOk.AutoSize = true; + this.buttonOk.Image = global::UI_WinForms.Resources.Icons.icons8_ok_512; + this.buttonOk.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.buttonOk.Location = new System.Drawing.Point(3, 3); + this.buttonOk.Name = "buttonOk"; + this.buttonOk.Size = new System.Drawing.Size(114, 60); + this.buttonOk.TabIndex = 1; + this.buttonOk.Text = "Ok"; + this.buttonOk.TextImageRelation = System.Windows.Forms.TextImageRelation.ImageBeforeText; + this.buttonOk.UseVisualStyleBackColor = true; + this.buttonOk.Click += new System.EventHandler(this.buttonOk_Click); + // + // buttonCancel + // + this.buttonCancel.AutoSize = true; + this.buttonCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; + this.buttonCancel.Image = global::UI_WinForms.Resources.Icons.icons8_cancel_512; + this.buttonCancel.ImageAlign = System.Drawing.ContentAlignment.MiddleLeft; + this.buttonCancel.Location = new System.Drawing.Point(123, 3); + this.buttonCancel.Name = "buttonCancel"; + this.buttonCancel.Size = new System.Drawing.Size(124, 60); + this.buttonCancel.TabIndex = 2; + this.buttonCancel.Text = "Cancel"; + this.buttonCancel.TextImageRelation = System.Windows.Forms.TextImageRelation.ImageBeforeText; + this.buttonCancel.UseVisualStyleBackColor = true; + // + // textBoxInput + // + this.textBoxInput.Dock = System.Windows.Forms.DockStyle.Fill; + this.textBoxInput.Location = new System.Drawing.Point(6, 31); + this.textBoxInput.Margin = new System.Windows.Forms.Padding(6, 3, 3, 3); + this.textBoxInput.Name = "textBoxInput"; + this.textBoxInput.Size = new System.Drawing.Size(791, 34); + this.textBoxInput.TabIndex = 1; + this.textBoxInput.KeyUp += new System.Windows.Forms.KeyEventHandler(this.textBoxInput_KeyUp); + // + // checkBoxIsRoot + // + this.checkBoxQuestion.AutoSize = true; + this.checkBoxQuestion.Dock = System.Windows.Forms.DockStyle.Left; + this.checkBoxQuestion.Location = new System.Drawing.Point(253, 3); + this.checkBoxQuestion.Name = "checkBoxQuestion"; + this.checkBoxQuestion.Size = new System.Drawing.Size(458, 60); + this.checkBoxQuestion.TabIndex = 3; + this.checkBoxQuestion.UseVisualStyleBackColor = true; + // + // InputDialog + // + this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); + this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; + this.AutoSize = true; + this.CancelButton = this.buttonCancel; + this.ClientSize = new System.Drawing.Size(800, 139); + this.Controls.Add(this.tableLayout); + this.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog; + this.MaximizeBox = false; + this.MinimizeBox = false; + this.Name = "InputDialog"; + this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent; + this.Text = "InputDialog"; + this.tableLayout.ResumeLayout(false); + this.tableLayout.PerformLayout(); + this.flowLayoutBottom.ResumeLayout(false); + this.flowLayoutBottom.PerformLayout(); + this.ResumeLayout(false); + this.PerformLayout(); + + } + + #endregion + + private TableLayoutPanel tableLayout; + private Label labelHead; + private FlowLayoutPanel flowLayoutBottom; + private TextBox textBoxInput; + private Button buttonOk; + private Button buttonCancel; + private CheckBox checkBoxQuestion; + } +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Dialogs/InputDialog.cs b/I18N Commander/UI WinForms/Dialogs/InputDialog.cs new file mode 100644 index 0000000..25afee0 --- /dev/null +++ b/I18N Commander/UI WinForms/Dialogs/InputDialog.cs @@ -0,0 +1,59 @@ +namespace UI_WinForms.Dialogs; + +public partial class InputDialog : Form +{ + public readonly record struct Options( + string Message, + string Title, + string Placeholder = "", + string PreloadedText = "", + string OkButtonText = "Ok", + string CancelButtonText = "Cancel", + bool ShowQuestionCheckbox = false, + string QuestionCheckboxText = "" + ); + + private InputDialog() + { + this.InitializeComponent(); + } + + public static InputResult Show(Options options) + { + using var inputDialog = new InputDialog(); + inputDialog.labelHead.Text = options.Message; + inputDialog.Text = options.Title; + inputDialog.textBoxInput.PlaceholderText = options.Placeholder; + inputDialog.textBoxInput.Text = options.PreloadedText; + inputDialog.buttonOk.Text = options.OkButtonText; + inputDialog.buttonCancel.Text = options.CancelButtonText; + inputDialog.checkBoxQuestion.Visible = options.ShowQuestionCheckbox; + inputDialog.checkBoxQuestion.Text = options.QuestionCheckboxText; + + // Ensure, that the text box is focused on load: + inputDialog.textBoxInput.Select(); + + return new InputResult( + inputDialog.ShowDialog(), + inputDialog.textBoxInput.Text, + inputDialog.checkBoxQuestion.Checked + ); + } + + private void textBoxInput_KeyUp(object sender, KeyEventArgs e) + { + if (e.KeyCode is Keys.Enter or Keys.Return) + this.buttonOk.PerformClick(); + } + + private void buttonOk_Click(object sender, EventArgs e) + { + if (!string.IsNullOrWhiteSpace(this.textBoxInput.Text)) + { + this.DialogResult = DialogResult.OK; + this.Close(); + } + } + + public readonly record struct InputResult(DialogResult DialogResult, string Text, bool AnswerToQuestion); +} \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Dialogs/InputDialog.resx b/I18N Commander/UI WinForms/Dialogs/InputDialog.resx new file mode 100644 index 0000000..b5ae26c --- /dev/null +++ b/I18N Commander/UI WinForms/Dialogs/InputDialog.resx @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Loader.cs b/I18N Commander/UI WinForms/Loader.cs index b18c284..38065c7 100644 --- a/I18N Commander/UI WinForms/Loader.cs +++ b/I18N Commander/UI WinForms/Loader.cs @@ -2,6 +2,8 @@ public partial class Loader : Form { + public string DataFile { get; set; } = string.Empty; + public Loader() { this.InitializeComponent(); @@ -9,7 +11,8 @@ public partial class Loader : Form private void loaderStart_LoadProject(object sender, string projectFilePath) { - // TODO: Call the data model + this.DataFile = projectFilePath; + this.DialogResult = DialogResult.OK; this.Close(); } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Main.Designer.cs b/I18N Commander/UI WinForms/Main.Designer.cs index f8b002f..b15a91a 100644 --- a/I18N Commander/UI WinForms/Main.Designer.cs +++ b/I18N Commander/UI WinForms/Main.Designer.cs @@ -28,13 +28,24 @@ ///
private void InitializeComponent() { + this.mainComponent = new UI_WinForms.Components.Main(); this.SuspendLayout(); // + // mainComponent + // + this.mainComponent.Dock = System.Windows.Forms.DockStyle.Fill; + this.mainComponent.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); + this.mainComponent.Location = new System.Drawing.Point(0, 0); + this.mainComponent.Name = "mainComponent"; + this.mainComponent.Size = new System.Drawing.Size(1071, 755); + this.mainComponent.TabIndex = 0; + // // Main // this.AutoScaleDimensions = new System.Drawing.SizeF(120F, 120F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Dpi; - this.ClientSize = new System.Drawing.Size(800, 450); + this.ClientSize = new System.Drawing.Size(1071, 755); + this.Controls.Add(this.mainComponent); this.Font = new System.Drawing.Font("Segoe UI", 12F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point); this.Name = "Main"; this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; @@ -44,5 +55,7 @@ } #endregion + + private Components.Main mainComponent; } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Program.cs b/I18N Commander/UI WinForms/Program.cs index 50c87bb..5c21401 100644 --- a/I18N Commander/UI WinForms/Program.cs +++ b/I18N Commander/UI WinForms/Program.cs @@ -1,17 +1,64 @@ +using DataModel; +using DataModel.Database.Common; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + namespace UI_WinForms; internal static class Program { + internal const string VERSION = "v0.1.0"; + internal static IServiceProvider SERVICE_PROVIDER = null!; + [STAThread] private static void Main() { ApplicationConfiguration.Initialize(); - + Application.EnableVisualStyles(); + // Start the loader screen: var loader = new Loader(); Application.Run(loader); - // Start the main app: - Application.Run(new Main()); + // Check, if the user closes the loader screen: + if (loader.DialogResult != DialogResult.OK) + return; + + // + // Create the DI system + // + var builder = new HostBuilder(); + + // + // Add services + // + builder.ConfigureServices((hostContext, serviceCollection) => + { + // The main form: + serviceCollection.AddSingleton
(); + + // The database: + serviceCollection.AddDatabase(loader.DataFile, true); + }); + + // Get the host out of the DI system: + var host = builder.Build(); + + // Create a service scope: + using (var scope = host.Services.CreateScope()) + { + // Get a service provider: + SERVICE_PROVIDER = scope.ServiceProvider; + + // Apply database migrations: + using (var database = SERVICE_PROVIDER.GetRequiredService()) + Setup.PerformDataMigration(database).Wait(); + + // Create the main window: + var mainWindow = SERVICE_PROVIDER.GetService
(); + + // Start the app: + Application.Run(mainWindow); + } } } \ No newline at end of file diff --git a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs index 32a2e64..8d0147b 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.Designer.cs +++ b/I18N Commander/UI WinForms/Resources/Icons.Designer.cs @@ -60,6 +60,16 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_add_folder_512 { + get { + object obj = ResourceManager.GetObject("icons8_add_folder_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -70,6 +80,26 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_cancel_512 { + get { + object obj = ResourceManager.GetObject("icons8_cancel_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_delete_folder_512 { + get { + object obj = ResourceManager.GetObject("icons8_delete_folder_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -80,6 +110,16 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_documents_folder_512 { + get { + object obj = ResourceManager.GetObject("icons8_documents_folder_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// @@ -100,6 +140,16 @@ namespace UI_WinForms.Resources { } } + /// + /// Looks up a localized resource of type System.Drawing.Bitmap. + /// + internal static System.Drawing.Bitmap icons8_ok_512 { + get { + object obj = ResourceManager.GetObject("icons8_ok_512", resourceCulture); + return ((System.Drawing.Bitmap)(obj)); + } + } + /// /// Looks up a localized resource of type System.Drawing.Bitmap. /// diff --git a/I18N Commander/UI WinForms/Resources/Icons.resx b/I18N Commander/UI WinForms/Resources/Icons.resx index 17944cf..4fb8808 100644 --- a/I18N Commander/UI WinForms/Resources/Icons.resx +++ b/I18N Commander/UI WinForms/Resources/Icons.resx @@ -118,9 +118,21 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + icons8-add-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-browse-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-cancel-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + icons8-delete-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + + icons8-documents-folder-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-document-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a @@ -130,6 +142,9 @@ icons8-new-window-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + icons8-ok-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + icons8-open-file-under-cursor-512.png;System.Drawing.Bitmap, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a diff --git a/I18N Commander/UI WinForms/Resources/icons8-add-folder-512.png b/I18N Commander/UI WinForms/Resources/icons8-add-folder-512.png new file mode 100644 index 0000000..3a24a0e Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-add-folder-512.png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-cancel-512.png b/I18N Commander/UI WinForms/Resources/icons8-cancel-512.png new file mode 100644 index 0000000..df1ba17 Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-cancel-512.png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-delete-folder-512.png b/I18N Commander/UI WinForms/Resources/icons8-delete-folder-512.png new file mode 100644 index 0000000..6457dc5 Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-delete-folder-512.png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-documents-folder-512.png b/I18N Commander/UI WinForms/Resources/icons8-documents-folder-512.png new file mode 100644 index 0000000..d6eebf0 Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-documents-folder-512.png differ diff --git a/I18N Commander/UI WinForms/Resources/icons8-ok-512.png b/I18N Commander/UI WinForms/Resources/icons8-ok-512.png new file mode 100644 index 0000000..91a829f Binary files /dev/null and b/I18N Commander/UI WinForms/Resources/icons8-ok-512.png differ diff --git a/project for migrations.i18nc b/project for migrations.i18nc deleted file mode 100644 index fce11e3..0000000 Binary files a/project for migrations.i18nc and /dev/null differ