Db: Migrate ImportHistory to sqlite

rendering
Liza 2023-02-16 19:51:54 +01:00
parent 7e2ccd3b42
commit c7d5aa1eaa
18 changed files with 415 additions and 71 deletions

View File

@ -7,6 +7,8 @@ using System.Text.Json;
using Dalamud.Logging;
using Dalamud.Plugin;
using ImGuiNET;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
using NJson = Newtonsoft.Json;
namespace Pal.Client.Configuration
@ -14,12 +16,14 @@ namespace Pal.Client.Configuration
internal sealed class ConfigurationManager
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly IServiceProvider _serviceProvider;
public event EventHandler<IPalacePalConfiguration>? Saved;
public ConfigurationManager(DalamudPluginInterface pluginInterface)
public ConfigurationManager(DalamudPluginInterface pluginInterface, IServiceProvider serviceProvider)
{
_pluginInterface = pluginInterface;
_serviceProvider = serviceProvider;
Migrate();
}
@ -61,6 +65,26 @@ namespace Pal.Client.Configuration
var v7 = MigrateToV7(configurationV1);
Save(v7, queue: false);
using (var scope = _serviceProvider.CreateScope())
{
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.Imports.RemoveRange(dbContext.Imports);
foreach (var importHistory in configurationV1.ImportHistory)
{
PluginLog.Information($"Migrating import {importHistory.Id}");
dbContext.Imports.Add(new ImportHistory
{
Id = importHistory.Id,
RemoteUrl = importHistory.RemoteUrl?.Replace(".μ.tv", ".liza.sh"),
ExportedAt = importHistory.ExportedAt,
ImportedAt = importHistory.ImportedAt
});
}
dbContext.SaveChanges();
}
File.Move(_pluginInterface.ConfigFile.FullName, _pluginInterface.ConfigFile.FullName + ".old", true);
}
}

View File

@ -23,9 +23,6 @@ namespace Pal.Client.Configuration
DeepDungeonConfiguration DeepDungeons { get; set; }
RendererConfiguration Renderer { get; set; }
[Obsolete]
List<ConfigurationV1.ImportHistoryEntry> ImportHistory { get; }
IAccountConfiguration CreateAccount(string server, Guid accountId);
IAccountConfiguration? FindAccount(string server);
void RemoveAccount(string server);

View File

@ -0,0 +1,12 @@
using System;
namespace Pal.Client.Database
{
internal sealed class ImportHistory
{
public Guid Id { get; set; }
public string? RemoteUrl { get; set; }
public DateTime ExportedAt { get; set; }
public DateTime ImportedAt { get; set; }
}
}

View File

@ -0,0 +1,45 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Pal.Client.Database;
#nullable disable
namespace Pal.Client.Database.Migrations
{
[DbContext(typeof(PalClientContext))]
[Migration("20230216154417_AddImportHistory")]
partial class AddImportHistory
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("ExportedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedAt")
.HasColumnType("TEXT");
b.Property<string>("RemoteUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Imports");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,36 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Pal.Client.Database.Migrations
{
/// <inheritdoc />
public partial class AddImportHistory : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Imports",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
RemoteUrl = table.Column<string>(type: "TEXT", nullable: true),
ExportedAt = table.Column<DateTime>(type: "TEXT", nullable: false),
ImportedAt = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Imports", x => x.Id);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Imports");
}
}
}

View File

@ -0,0 +1,42 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Pal.Client.Database;
#nullable disable
namespace Pal.Client.Database.Migrations
{
[DbContext(typeof(PalClientContext))]
partial class PalClientContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "7.0.3");
modelBuilder.Entity("Pal.Client.Database.ImportHistory", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<DateTime>("ExportedAt")
.HasColumnType("TEXT");
b.Property<DateTime>("ImportedAt")
.HasColumnType("TEXT");
b.Property<string>("RemoteUrl")
.HasColumnType("TEXT");
b.HasKey("Id");
b.ToTable("Imports");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,14 @@
using Microsoft.EntityFrameworkCore;
namespace Pal.Client.Database
{
internal class PalClientContext : DbContext
{
public DbSet<ImportHistory> Imports { get; set; } = null!;
public PalClientContext(DbContextOptions<PalClientContext> options)
: base(options)
{
}
}
}

View File

@ -0,0 +1,20 @@
#if EF
using System;
using System.IO;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
namespace Pal.Client.Database
{
internal sealed class PalClientContextFactory : IDesignTimeDbContextFactory<PalClientContext>
{
public PalClientContext CreateDbContext(string[] args)
{
var optionsBuilder =
new DbContextOptionsBuilder<PalClientContext>().UseSqlite(
$"Data Source={Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "XIVLauncher", "pluginConfigs", "Palace Pal", "palace-pal.data.sqlite3")}");
return new PalClientContext(optionsBuilder.Options);
}
}
}
#endif

View File

@ -0,0 +1,58 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Database;
namespace Pal.Client.DependencyInjection
{
internal sealed class ImportService
{
private readonly IServiceProvider _serviceProvider;
public ImportService(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Add(ImportHistory history)
{
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.Imports.Add(history);
dbContext.SaveChanges();
}
public ImportHistory? FindLast()
{
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
return dbContext.Imports.OrderByDescending(x => x.ImportedAt).ThenBy(x => x.Id).FirstOrDefault();
}
public List<ImportHistory> FindForServer(string server)
{
if (string.IsNullOrEmpty(server))
return new();
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
return dbContext.Imports.Where(x => x.RemoteUrl == server).ToList();
}
public void RemoveAllByIds(List<Guid> ids)
{
using var scope = _serviceProvider.CreateScope();
using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
dbContext.RemoveRange(dbContext.Imports.Where(x => ids.Contains(x.Id)));
dbContext.SaveChanges();
}
public void RemoveById(Guid id)
=> RemoveAllByIds(new List<Guid> { id });
}
}

View File

@ -1,4 +1,8 @@
using System.Globalization;
using System;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.ClientState;
@ -7,17 +11,22 @@ using Dalamud.Game.ClientState.Objects;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Plugin;
using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Pal.Client.Commands;
using Pal.Client.Configuration;
using Pal.Client.Database;
using Pal.Client.DependencyInjection;
using Pal.Client.Net;
using Pal.Client.Properties;
using Pal.Client.Rendering;
using Pal.Client.Scheduled;
using Pal.Client.Windows;
namespace Pal.Client.DependencyInjection
namespace Pal.Client
{
/// <summary>
/// DI-aware Plugin.
@ -25,6 +34,8 @@ namespace Pal.Client.DependencyInjection
// ReSharper disable once UnusedType.Global
internal sealed class DependencyInjectionContext : IDalamudPlugin
{
private readonly string _sqliteConnectionString;
private readonly CancellationTokenSource _initCts = new();
private ServiceProvider? _serviceProvider;
public string Name => Localization.Palace_Pal;
@ -39,6 +50,9 @@ namespace Pal.Client.DependencyInjection
CommandManager commandManager,
DataManager dataManager)
{
PluginLog.Information("Building service container");
CancellationToken token = _initCts.Token;
IServiceCollection services = new ServiceCollection();
// dalamud
@ -54,6 +68,11 @@ namespace Pal.Client.DependencyInjection
services.AddSingleton(dataManager);
services.AddSingleton(new WindowSystem(typeof(DependencyInjectionContext).AssemblyQualifiedName));
// EF core
_sqliteConnectionString =
$"Data Source={Path.Join(pluginInterface.GetPluginConfigDirectory(), "palace-pal.data.sqlite3")}";
services.AddDbContext<PalClientContext>(o => o.UseSqlite(_sqliteConnectionString));
// plugin-specific
services.AddSingleton<Plugin>();
services.AddSingleton<DebugState>();
@ -64,11 +83,12 @@ namespace Pal.Client.DependencyInjection
services.AddTransient<RepoVerification>();
services.AddSingleton<PalCommand>();
// territory handling
// territory & marker related services
services.AddSingleton<TerritoryState>();
services.AddSingleton<FrameworkService>();
services.AddSingleton<ChatService>();
services.AddSingleton<FloorService>();
services.AddSingleton<ImportService>();
// windows & related services
services.AddSingleton<AgreementWindow>();
@ -97,7 +117,7 @@ namespace Pal.Client.DependencyInjection
ValidateScopes = true,
});
// initialize plugin
#if RELEASE
// You're welcome to remove this code in your fork, but please make sure that:
// - none of the links accessible within FFXIV open the original repo (e.g. in the plugin installer), and
@ -108,8 +128,33 @@ namespace Pal.Client.DependencyInjection
_serviceProvider.GetService<RepoVerification>();
#endif
// This is not ideal as far as loading the plugin goes, because there's no way to check for errors and
// tell Dalamud that no, the plugin isn't ready -- so the plugin will count as properly initialized,
// even if it's not.
//
// There's 2-3 seconds of slowdown primarily caused by the sqlite init, but that needs to happen for
// config stuff.
PluginLog.Information("Service container built, triggering async init");
Task.Run(async () =>
{
try
{
PluginLog.Information("Starting async init");
// initialize database
await using (var scope = _serviceProvider.CreateAsyncScope())
{
PluginLog.Log("Loading database & running migrations");
await using var dbContext = scope.ServiceProvider.GetRequiredService<PalClientContext>();
await dbContext.Database.MigrateAsync();
PluginLog.Log("Completed database migrations");
}
token.ThrowIfCancellationRequested();
// set up legacy services
LocalState.PluginInterface = pluginInterface;
LocalState.PluginConfigDirectory = pluginInterface.GetPluginConfigDirectory();
LocalState.Mode = _serviceProvider.GetRequiredService<IPalacePalConfiguration>().Mode;
// windows that have logic to open on startup
@ -121,11 +166,23 @@ namespace Pal.Client.DependencyInjection
_serviceProvider.GetRequiredService<FrameworkService>();
_serviceProvider.GetRequiredService<ChatService>();
token.ThrowIfCancellationRequested();
_serviceProvider.GetRequiredService<Plugin>();
PluginLog.Information("Async init complete");
}
catch (Exception e)
{
PluginLog.Error(e, "Async load failed");
chatGui.PrintError($"Async loading failed: {e}");
}
});
}
public void Dispose()
{
_initCts.Cancel();
// ensure we're not calling dispose recursively on ourselves
if (_serviceProvider != null)
{
@ -133,6 +190,10 @@ namespace Pal.Client.DependencyInjection
_serviceProvider = null;
serviceProvider.Dispose();
// ensure we're not keeping the file open longer than the plugin is loaded
using (SqliteConnection sqliteConnection = new(_sqliteConnectionString))
SqliteConnection.ClearPool(sqliteConnection);
}
}
}

View File

@ -5,7 +5,6 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Dalamud.Plugin;
using Pal.Client.Configuration;
using Pal.Client.Extensions;
@ -19,7 +18,7 @@ namespace Pal.Client
private static readonly JsonSerializerOptions JsonSerializerOptions = new() { IncludeFields = true };
private const int CurrentVersion = 4;
internal static DalamudPluginInterface PluginInterface { get; set; }
internal static string PluginConfigDirectory { get; set; } = null!;
internal static EMode Mode { get; set; }
public uint TerritoryType { get; set; }
@ -126,7 +125,7 @@ namespace Pal.Client
public string GetSaveLocation() => GetSaveLocation(TerritoryType);
private static string GetSaveLocation(uint territoryType) => Path.Join(PluginInterface.GetPluginConfigDirectory(), $"{territoryType}.json");
private static string GetSaveLocation(uint territoryType) => Path.Join(PluginConfigDirectory, $"{territoryType}.json");
public static void ForEach(Action<LocalState> action)
{

View File

@ -30,10 +30,6 @@
<ItemGroup Condition="'$(Configuration)' == 'Release' And Exists('Certificate.pfx')">
<EmbeddedResource Include="Certificate.pfx" />
<EmbeddedResource Update="Properties\Localization.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
@ -68,35 +64,39 @@
<!--You may need to adjust these paths yourself. These point to a Dalamud assembly in AppData.-->
<Reference Include="Dalamud">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Dalamud.dll</HintPath>
<Private>false</Private>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGui.NET.dll</HintPath>
<Private>false</Private>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\ImGuiScene.dll</HintPath>
<Private>false</Private>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.dll</HintPath>
<Private>false</Private>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Lumina.Excel.dll</HintPath>
<Private>false</Private>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(AppData)\XIVLauncher\addon\Hooks\dev\FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
<Private Condition="'$(Configuration)' != 'EF'">false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Properties\Localization.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Localization.Designer.cs</LastGenOutput>
</EmbeddedResource>
<Compile Update="Properties\Localization.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
@ -104,8 +104,11 @@
</Compile>
</ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin" Condition="'$(Configuration)' == 'Release'">
<Exec Command="rename &quot;$(OutDir)$(AssemblyName)\latest.zip&quot; &quot;$(AssemblyName)-$(Version).zip&quot;" />
</Target>
<Target Name="Clean">
<RemoveDir Directories="dist" />
</Target>
</Project>

View File

@ -5,7 +5,6 @@ using Pal.Client.Windows;
using System;
using System.Globalization;
using System.Linq;
using Dalamud.Logging;
using Pal.Client.Properties;
using ECommons;
using Microsoft.Extensions.DependencyInjection;
@ -31,8 +30,6 @@ namespace Pal.Client
IPalacePalConfiguration configuration,
RenderAdapter renderAdapter)
{
PluginLog.Information("Initializing Palace Pal");
_serviceProvider = serviceProvider;
_pluginInterface = pluginInterface;
_configuration = configuration;

15
Pal.Client/README.md Normal file
View File

@ -0,0 +1,15 @@
# Palace Pal
## Client Build Notes
### Database Migrations
Since EF core needs all dll files to be present, including Dalamud ones,
there's a special `EF` configuration that exempts them from setting
`<Private>false</Private>` during the build.
To use with `dotnet ef` commands, specify it as `-c EF`, for example:
```shell
dotnet ef migrations add MigrationName --configuration EF
```

View File

@ -7,10 +7,11 @@ using System.Linq;
using System.Numerics;
using Dalamud.Game.Gui;
using Dalamud.Logging;
using Pal.Client.Configuration;
using Pal.Client.Database;
using Pal.Client.DependencyInjection;
using Pal.Client.Extensions;
using Pal.Client.Properties;
using Pal.Client.Windows;
namespace Pal.Client.Scheduled
{
@ -30,17 +31,20 @@ namespace Pal.Client.Scheduled
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedImport>
{
private readonly ChatGui _chatGui;
private readonly IPalacePalConfiguration _configuration;
private readonly ConfigurationManager _configurationManager;
private readonly FloorService _floorService;
private readonly ImportService _importService;
private readonly ConfigWindow _configWindow;
public Handler(ChatGui chatGui, IPalacePalConfiguration configuration,
ConfigurationManager configurationManager, FloorService floorService)
public Handler(
ChatGui chatGui,
FloorService floorService,
ImportService importService,
ConfigWindow configWindow)
{
_chatGui = chatGui;
_configuration = configuration;
_configurationManager = configurationManager;
_floorService = floorService;
_importService = importService;
_configWindow = configWindow;
}
protected override void Run(QueuedImport import, ref bool recreateLayout, ref bool saveMarkers)
@ -53,11 +57,9 @@ namespace Pal.Client.Scheduled
if (!Validate(import))
return;
var oldExportIds = string.IsNullOrEmpty(import.Export.ServerUrl)
? _configuration.ImportHistory.Where(x => x.RemoteUrl == import.Export.ServerUrl)
List<Guid> oldExportIds = _importService.FindForServer(import.Export.ServerUrl)
.Select(x => x.Id)
.Where(x => x != Guid.Empty).ToList()
: new List<Guid>();
.ToList();
foreach (var remoteFloor in import.Export.Floors)
{
@ -70,17 +72,19 @@ namespace Pal.Client.Scheduled
localState.Save();
}
_configuration.ImportHistory.RemoveAll(hist =>
oldExportIds.Contains(hist.Id) || hist.Id == import.ExportId);
_configuration.ImportHistory.Add(new ConfigurationV1.ImportHistoryEntry
_importService.RemoveAllByIds(oldExportIds);
_importService.RemoveById(import.ExportId);
_importService.Add(new ImportHistory
{
Id = import.ExportId,
RemoteUrl = import.Export.ServerUrl,
ExportedAt = import.Export.CreatedAt.ToDateTime(),
ImportedAt = DateTime.UtcNow,
});
_configurationManager.Save(_configuration);
_configWindow.UpdateLastImport();
PluginLog.Information(
$"Imported {import.ExportId} for {import.ImportedTraps} traps, {import.ImportedHoardCoffers} hoard coffers");
_chatGui.PalMessage(string.Format(Localization.ImportCompleteStatistics, import.ImportedTraps,
import.ImportedHoardCoffers));
}
@ -101,9 +105,9 @@ namespace Pal.Client.Scheduled
return false;
}
if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || import.ExportId == Guid.Empty)
if (!Guid.TryParse(import.Export.ExportId, out Guid exportId) || exportId == Guid.Empty)
{
PluginLog.Error("Import: Invalid export id");
PluginLog.Error($"Import: Invalid export id ({import.Export.ExportId})");
_chatGui.PalError(Localization.Error_ImportFailed_InvalidFile);
return false;
}

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using Pal.Client.Configuration;
using Pal.Client.DependencyInjection;
using Pal.Client.Windows;
using Pal.Common;
namespace Pal.Client.Scheduled
@ -17,13 +18,15 @@ namespace Pal.Client.Scheduled
internal sealed class Handler : IQueueOnFrameworkThread.Handler<QueuedUndoImport>
{
private readonly IPalacePalConfiguration _configuration;
private readonly ImportService _importService;
private readonly FloorService _floorService;
private readonly ConfigWindow _configWindow;
public Handler(IPalacePalConfiguration configuration, FloorService floorService)
public Handler(ImportService importService, FloorService floorService, ConfigWindow configWindow)
{
_configuration = configuration;
_importService = importService;
_floorService = floorService;
_configWindow = configWindow;
}
protected override void Run(QueuedUndoImport queued, ref bool recreateLayout, ref bool saveMarkers)
@ -38,7 +41,8 @@ namespace Pal.Client.Scheduled
localState.Save();
}
_configuration.ImportHistory.RemoveAll(hist => hist.Id == queued.ExportId);
_importService.RemoveById(queued.ExportId);
_configWindow.UpdateLastImport();
}
}
}

View File

@ -21,6 +21,7 @@ using System.Threading.Tasks;
using Dalamud.Game.Gui;
using Pal.Client.Properties;
using Pal.Client.Configuration;
using Pal.Client.Database;
using Pal.Client.DependencyInjection;
using Pal.Client.Extensions;
@ -40,6 +41,7 @@ namespace Pal.Client.Windows
private readonly DebugState _debugState;
private readonly ChatGui _chatGui;
private readonly RemoteApi _remoteApi;
private readonly ImportService _importService;
private int _mode;
private int _renderer;
@ -55,6 +57,7 @@ namespace Pal.Client.Windows
private string? _saveExportDialogStartPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
private readonly FileDialogManager _importDialog;
private readonly FileDialogManager _exportDialog;
private ImportHistory? _lastImport;
private CancellationTokenSource? _testConnectionCts;
@ -68,7 +71,8 @@ namespace Pal.Client.Windows
FloorService floorService,
DebugState debugState,
ChatGui chatGui,
RemoteApi remoteApi)
RemoteApi remoteApi,
ImportService importService)
: base(WindowId)
{
_windowSystem = windowSystem;
@ -81,6 +85,7 @@ namespace Pal.Client.Windows
_debugState = debugState;
_chatGui = chatGui;
_remoteApi = remoteApi;
_importService = importService;
LanguageChanged();
@ -117,6 +122,8 @@ namespace Pal.Client.Windows
_hoardConfig = new ConfigurableMarker(_configuration.DeepDungeons.HoardCoffers);
_silverConfig = new ConfigurableMarker(_configuration.DeepDungeons.SilverCoffers);
_connectionText = null;
UpdateLastImport();
}
public override void OnClose()
@ -278,13 +285,14 @@ namespace Pal.Client.Windows
DoImport(_openImportPath);
ImGui.EndDisabled();
var importHistory = _configuration.ImportHistory.OrderByDescending(x => x.ImportedAt)
.ThenBy(x => x.Id).FirstOrDefault();
ImportHistory? importHistory = _lastImport;
if (importHistory != null)
{
ImGui.Separator();
ImGui.TextWrapped(string.Format(Localization.Config_UndoImportExplanation1,
importHistory.ImportedAt, importHistory.RemoteUrl, importHistory.ExportedAt));
importHistory.ImportedAt.ToLocalTime(),
importHistory.RemoteUrl,
importHistory.ExportedAt.ToUniversalTime()));
ImGui.TextWrapped(Localization.Config_UndoImportExplanation2);
if (ImGui.Button(Localization.Config_UndoImport))
UndoImport(importHistory.Id);
@ -466,6 +474,11 @@ namespace Pal.Client.Windows
_frameworkService.EarlyEventQueue.Enqueue(new QueuedUndoImport(importId));
}
internal void UpdateLastImport()
{
_lastImport = _importService.FindLast();
}
private void DoExport(string destinationPath)
{
Task.Run(async () =>