Implement some todos from #27
This commit is contained in:
@@ -84,27 +84,41 @@ internal class LegacyMessageImporterEligibility
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal LegacyMessageImporter StartImport(MessageStore targetStore, bool noLog = false)
|
internal LegacyMessageImporter StartImport(MessageStore targetStore, bool noLog = false, Plugin? plugin = null)
|
||||||
{
|
{
|
||||||
if (Status != LegacyMessageImporterEligibilityStatus.Eligible)
|
if (Status != LegacyMessageImporterEligibilityStatus.Eligible)
|
||||||
throw new InvalidOperationException($"Migration not eligible: status is {Status}");
|
throw new InvalidOperationException($"Migration not eligible: status is {Status}");
|
||||||
|
|
||||||
return new LegacyMessageImporter(targetStore, originalDbPath: OriginalDbPath, migrationDbPath: MigrationDbPath, noLog: noLog);
|
return new LegacyMessageImporter(targetStore, originalDbPath: OriginalDbPath, migrationDbPath: MigrationDbPath, noLog: noLog, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Makes the migration ineligible so the user won't be asked again.
|
/// Makes the migration ineligible so the user won't be asked again.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
internal void RenameOldDatabase()
|
internal bool RenameOldDatabase()
|
||||||
{
|
{
|
||||||
File.Move(OriginalDbPath, MigrationDbPath);
|
try
|
||||||
Status = LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists;
|
{
|
||||||
AdditionalIneligibilityInfo = "User chose to rename the old database file";
|
File.Move(OriginalDbPath, MigrationDbPath);
|
||||||
|
Status = LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists;
|
||||||
|
AdditionalIneligibilityInfo = "User chose to rename the old database file";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Plugin.Log.Error(ex, "Unable to move the old database");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class LegacyMessageImporter : IDisposable
|
internal class LegacyMessageImporter : IAsyncDisposable
|
||||||
{
|
{
|
||||||
|
private readonly Plugin? Plugin;
|
||||||
|
|
||||||
|
private readonly CancellationTokenSource CancellationToken = new();
|
||||||
|
private Thread? WorkingThread = null;
|
||||||
|
|
||||||
internal const string MessagesCollection = "messages";
|
internal const string MessagesCollection = "messages";
|
||||||
private const int MaxFailedMessageLogs = 10;
|
private const int MaxFailedMessageLogs = 10;
|
||||||
|
|
||||||
@@ -130,16 +144,17 @@ internal class LegacyMessageImporter : IDisposable
|
|||||||
// This can be set by the user to limit the rate at which messages are
|
// This can be set by the user to limit the rate at which messages are
|
||||||
// imported. If the rate exceeds this value, the importer will sleep for the
|
// imported. If the rate exceeds this value, the importer will sleep for the
|
||||||
// remainder of the second.
|
// remainder of the second.
|
||||||
internal int MaxMessageRate { get; set; } = 250; // start low
|
internal int MaxMessageRate = 250; // start low
|
||||||
|
|
||||||
// Do not call this directly, use
|
// Do not call this directly, use
|
||||||
// LegacyMessageImporterEligibility.StartImport instead.
|
// LegacyMessageImporterEligibility.StartImport instead.
|
||||||
internal LegacyMessageImporter(MessageStore targetStore, string? originalDbPath = null, string? migrationDbPath = null, bool noLog = false)
|
internal LegacyMessageImporter(MessageStore targetStore, string? originalDbPath = null, string? migrationDbPath = null, bool noLog = false, Plugin? plugin = null)
|
||||||
{
|
{
|
||||||
_targetStore = targetStore;
|
_targetStore = targetStore;
|
||||||
originalDbPath ??= Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db");
|
originalDbPath ??= Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat.db");
|
||||||
migrationDbPath ??= migrationDbPath ?? Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db");
|
migrationDbPath ??= migrationDbPath ?? Path.Join(Plugin.Interface.ConfigDirectory.FullName, "chat-litedb.db");
|
||||||
_log = noLog ? null : Plugin.Log;
|
_log = noLog ? null : Plugin.Log;
|
||||||
|
Plugin = plugin;
|
||||||
|
|
||||||
_log?.Info($"[Migration] Moving '{originalDbPath}' to '{migrationDbPath}'");
|
_log?.Info($"[Migration] Moving '{originalDbPath}' to '{migrationDbPath}'");
|
||||||
File.Move(originalDbPath, migrationDbPath);
|
File.Move(originalDbPath, migrationDbPath);
|
||||||
@@ -147,12 +162,30 @@ internal class LegacyMessageImporter : IDisposable
|
|||||||
_database = Connect(migrationDbPath);
|
_database = Connect(migrationDbPath);
|
||||||
|
|
||||||
ImportStart = Environment.TickCount64;
|
ImportStart = Environment.TickCount64;
|
||||||
new Thread(DoImport).Start();
|
WorkingThread = new Thread(() => DoImport(CancellationToken.Token));
|
||||||
|
WorkingThread.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
// TODO: cancel thread and wait for it to close
|
_database?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
await CancellationToken.CancelAsync();
|
||||||
|
|
||||||
|
var timeout = 10_000; // 10s
|
||||||
|
while (WorkingThread != null && timeout > 0)
|
||||||
|
{
|
||||||
|
if (!WorkingThread.IsAlive)
|
||||||
|
break;
|
||||||
|
|
||||||
|
timeout -= 100;
|
||||||
|
await Task.Delay(100);
|
||||||
|
Plugin.Log.Information("Sleeping because thread still alive");
|
||||||
|
}
|
||||||
|
|
||||||
_database?.Dispose();
|
_database?.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +281,7 @@ internal class LegacyMessageImporter : IDisposable
|
|||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DoImport()
|
private void DoImport(CancellationToken token)
|
||||||
{
|
{
|
||||||
var importRateTimer = Stopwatch.StartNew();
|
var importRateTimer = Stopwatch.StartNew();
|
||||||
var messagesInLastSecond = 0;
|
var messagesInLastSecond = 0;
|
||||||
@@ -261,6 +294,9 @@ internal class LegacyMessageImporter : IDisposable
|
|||||||
var messages = messagesCollection.Query().OrderBy(msg => msg.Date).ToDocuments();
|
var messages = messagesCollection.Query().OrderBy(msg => msg.Date).ToDocuments();
|
||||||
foreach (var messageDoc in messages)
|
foreach (var messageDoc in messages)
|
||||||
{
|
{
|
||||||
|
if (token.IsCancellationRequested)
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var message = BsonDocumentToMessage(messageDoc);
|
var message = BsonDocumentToMessage(messageDoc);
|
||||||
@@ -271,8 +307,7 @@ internal class LegacyMessageImporter : IDisposable
|
|||||||
{
|
{
|
||||||
FailedMessages++;
|
FailedMessages++;
|
||||||
if (FailedMessages <= MaxFailedMessageLogs)
|
if (FailedMessages <= MaxFailedMessageLogs)
|
||||||
_log?.Error(
|
_log?.Error($"[Migration] Failed to import message '{messageDoc["_id"].AsObjectId}' (usually due to corruption): {e}");
|
||||||
$"[Migration] Failed to import message '{messageDoc["_id"].AsObjectId}' (usually due to corruption): {e}");
|
|
||||||
if (FailedMessages == MaxFailedMessageLogs)
|
if (FailedMessages == MaxFailedMessageLogs)
|
||||||
_log?.Error("[Migration] Further failed message logs will be suppressed");
|
_log?.Error("[Migration] Further failed message logs will be suppressed");
|
||||||
}
|
}
|
||||||
@@ -293,19 +328,19 @@ internal class LegacyMessageImporter : IDisposable
|
|||||||
|
|
||||||
// Log every 1,000 messages
|
// Log every 1,000 messages
|
||||||
if ((SuccessfulMessages + FailedMessages) % 1000 == 0)
|
if ((SuccessfulMessages + FailedMessages) % 1000 == 0)
|
||||||
_log?.Information(
|
_log?.Information($"[Migration] Progress: successfully imported {SuccessfulMessages}/{totalMessages} messages ({FailedMessages} failures)");
|
||||||
$"[Migration] Progress: successfully imported {SuccessfulMessages}/{totalMessages} messages ({FailedMessages} failures)");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_log?.Information($"[Migration] Imported {SuccessfulMessages}/{FailedMessages} messages, {FailedMessages} failed");
|
_log?.Information($"[Migration] Imported {SuccessfulMessages}/{FailedMessages} messages, {FailedMessages} failed");
|
||||||
|
|
||||||
if (ProcessedMessages != totalMessages)
|
if (ProcessedMessages != totalMessages)
|
||||||
_log?.Warning(
|
_log?.Warning($"[Migration] Total message count mismatch: expected {totalMessages}, got {SuccessfulMessages + FailedMessages}");
|
||||||
$"[Migration] Total message count mismatch: expected {totalMessages}, got {SuccessfulMessages + FailedMessages}");
|
|
||||||
|
|
||||||
ImportComplete = Environment.TickCount64;
|
ImportComplete = Environment.TickCount64;
|
||||||
_database.Dispose();
|
_database.Dispose();
|
||||||
_database = null;
|
_database = null;
|
||||||
|
|
||||||
|
if (Plugin != null)
|
||||||
|
Plugin.Framework.Run(() => Plugin.MessageManager.FilterAllTabs(false), token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Message BsonDocumentToMessage(BsonDocument doc)
|
private static Message BsonDocumentToMessage(BsonDocument doc)
|
||||||
|
|||||||
+4
-4
@@ -49,7 +49,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
public ChatLogWindow ChatLogWindow { get; }
|
public ChatLogWindow ChatLogWindow { get; }
|
||||||
public CommandHelpWindow CommandHelpWindow { get; }
|
public CommandHelpWindow CommandHelpWindow { get; }
|
||||||
public SeStringDebugger SeStringDebugger { get; }
|
public SeStringDebugger SeStringDebugger { get; }
|
||||||
internal LegacyMesasgeImporterWindow LegacyMesasgeImporterWindow { get; }
|
internal LegacyMessageImporterWindow LegacyMessageImporterWindow { get; }
|
||||||
|
|
||||||
internal Configuration Config { get; }
|
internal Configuration Config { get; }
|
||||||
internal Commands Commands { get; }
|
internal Commands Commands { get; }
|
||||||
@@ -106,8 +106,8 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
MessageManager = new MessageManager(this); // requires Ui
|
MessageManager = new MessageManager(this); // requires Ui
|
||||||
|
|
||||||
// Requires MessageManager
|
// Requires MessageManager
|
||||||
LegacyMesasgeImporterWindow = new LegacyMesasgeImporterWindow(MessageManager.Store);
|
LegacyMessageImporterWindow = new LegacyMessageImporterWindow(this);
|
||||||
WindowSystem.AddWindow(LegacyMesasgeImporterWindow);
|
WindowSystem.AddWindow(LegacyMessageImporterWindow);
|
||||||
|
|
||||||
// let all the other components register, then initialise commands
|
// let all the other components register, then initialise commands
|
||||||
Commands.Initialise();
|
Commands.Initialise();
|
||||||
@@ -143,7 +143,7 @@ public sealed class Plugin : IDalamudPlugin
|
|||||||
ChatLogWindow?.Dispose();
|
ChatLogWindow?.Dispose();
|
||||||
SettingsWindow?.Dispose();
|
SettingsWindow?.Dispose();
|
||||||
SeStringDebugger?.Dispose();
|
SeStringDebugger?.Dispose();
|
||||||
LegacyMesasgeImporterWindow?.Dispose();
|
LegacyMessageImporterWindow?.Dispose();
|
||||||
|
|
||||||
ExtraChat?.Dispose();
|
ExtraChat?.Dispose();
|
||||||
Ipc?.Dispose();
|
Ipc?.Dispose();
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
using System.Numerics;
|
|
||||||
using ChatTwo.Resources;
|
|
||||||
using ChatTwo.Util;
|
|
||||||
using Dalamud.Interface.ImGuiNotification;
|
|
||||||
using Dalamud.Interface.Internal.Notifications;
|
|
||||||
using Dalamud.Interface.Windowing;
|
|
||||||
using ImGuiNET;
|
|
||||||
|
|
||||||
namespace ChatTwo.Ui;
|
|
||||||
|
|
||||||
internal class LegacyMesasgeImporterWindow : Window
|
|
||||||
{
|
|
||||||
private readonly MessageStore _store;
|
|
||||||
|
|
||||||
private LegacyMessageImporterEligibility Eligibility { get; set; }
|
|
||||||
private LegacyMessageImporter? Importer { get; set; }
|
|
||||||
|
|
||||||
internal LegacyMesasgeImporterWindow(MessageStore store) : base("Chat 2 Legacy Importer###chat2-legacy-importer")
|
|
||||||
{
|
|
||||||
_store = store;
|
|
||||||
Eligibility = LegacyMessageImporterEligibility.CheckEligibility();
|
|
||||||
LogAndNotify();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Importer?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void LogAndNotify()
|
|
||||||
{
|
|
||||||
Plugin.Log.Info(
|
|
||||||
$"[Migration] Checked migration eligibility: {Eligibility.Status} - '{Eligibility.AdditionalIneligibilityInfo}'");
|
|
||||||
|
|
||||||
switch (Eligibility.Status)
|
|
||||||
{
|
|
||||||
case LegacyMessageImporterEligibilityStatus.Eligible:
|
|
||||||
{
|
|
||||||
var notification = Plugin.Notification.AddNotification(new Notification
|
|
||||||
{
|
|
||||||
Type = NotificationType.Info,
|
|
||||||
// The user needs to dismiss this for it to go away.
|
|
||||||
InitialDuration = TimeSpan.FromHours(6),
|
|
||||||
Title = "Chat 2 Migration",
|
|
||||||
Content = "Import messages from old database into new database? Click for more information.",
|
|
||||||
});
|
|
||||||
// TODO: clicking does not dismiss
|
|
||||||
notification.Click += _ => IsOpen = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed:
|
|
||||||
{
|
|
||||||
var notification = Plugin.Notification.AddNotification(new Notification
|
|
||||||
{
|
|
||||||
Type = NotificationType.Warning,
|
|
||||||
InitialDuration = TimeSpan.FromMinutes(1),
|
|
||||||
Title = "Chat Two Migration",
|
|
||||||
Content =
|
|
||||||
"Migration is not possible because the old database could not be opened. Click for more information."
|
|
||||||
});
|
|
||||||
// TODO: clicking does not dismiss
|
|
||||||
notification.Click += _ => IsOpen = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void Draw()
|
|
||||||
{
|
|
||||||
if (Importer != null)
|
|
||||||
{
|
|
||||||
DrawImportStatus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.Eligible)
|
|
||||||
DrawEligible();
|
|
||||||
else
|
|
||||||
DrawIneligible();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawEligible()
|
|
||||||
{
|
|
||||||
// TODO: pretty
|
|
||||||
ImGui.Text("Import database messages from legacy LiteDB database to Sqlite database?");
|
|
||||||
ImGui.Text($"Message count: {Eligibility.MessageCount}");
|
|
||||||
ImGui.Text($"Database size: {Eligibility.DatabaseSizeBytes}");
|
|
||||||
|
|
||||||
if (ImGui.Button("Yes, import messages"))
|
|
||||||
{
|
|
||||||
// Next draw call will run DrawImportStatus().
|
|
||||||
Importer = Eligibility.StartImport(_store);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui.SameLine();
|
|
||||||
|
|
||||||
if (ImGuiUtil.CtrlShiftButton("No, do not import messages",
|
|
||||||
"Ctrl+Shift: renames old database to avoid prompting again"))
|
|
||||||
{
|
|
||||||
Eligibility.RenameOldDatabase();
|
|
||||||
IsOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawIneligible()
|
|
||||||
{
|
|
||||||
// TODO: pretty
|
|
||||||
ImGui.Text("Your legacy LiteDB database is not eligible for import:");
|
|
||||||
switch (Eligibility.Status)
|
|
||||||
{
|
|
||||||
case LegacyMessageImporterEligibilityStatus.IneligibleOriginalDbNotExists:
|
|
||||||
ImGui.Text("The old database could not be found.");
|
|
||||||
break;
|
|
||||||
case LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists:
|
|
||||||
ImGui.Text("The migration process was already started.");
|
|
||||||
break;
|
|
||||||
case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed:
|
|
||||||
ImGui.Text("The old database could not be opened.");
|
|
||||||
break;
|
|
||||||
case LegacyMessageImporterEligibilityStatus.IneligibleNoMessages:
|
|
||||||
ImGui.Text("The old database contains no messages.");
|
|
||||||
break;
|
|
||||||
case LegacyMessageImporterEligibilityStatus.Eligible:
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
if (!string.IsNullOrWhiteSpace(Eligibility.AdditionalIneligibilityInfo))
|
|
||||||
ImGui.Text(Eligibility.AdditionalIneligibilityInfo);
|
|
||||||
|
|
||||||
// LiteDB failures notify the user, so give them a chance to rename the
|
|
||||||
// database to avoid prompting again.
|
|
||||||
if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed)
|
|
||||||
{
|
|
||||||
if (ImGuiUtil.CtrlShiftButton("Rename old database",
|
|
||||||
"Ctrl+Shift: rename old database to avoid import prompt in the future"))
|
|
||||||
{
|
|
||||||
Eligibility.RenameOldDatabase();
|
|
||||||
// TODO: notify success as this changes the status
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void DrawImportStatus()
|
|
||||||
{
|
|
||||||
// TODO: pretty
|
|
||||||
if (Importer == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var importStart = Importer.ImportStart;
|
|
||||||
var importEnd = Importer.ImportComplete;
|
|
||||||
var total = Importer.ImportCount;
|
|
||||||
var successful = Importer!.SuccessfulMessages;
|
|
||||||
var failed = Importer.FailedMessages;
|
|
||||||
var remaining = Importer.RemainingMessages;
|
|
||||||
|
|
||||||
if (importEnd != null)
|
|
||||||
{
|
|
||||||
ImGui.Text($"Completed migration in {Duration(importStart, importEnd.Value)}");
|
|
||||||
ImGui.Text($"Successfully imported: {successful} messages");
|
|
||||||
ImGui.Text($"Failed to import: {failed} messages");
|
|
||||||
ImGui.Text($"Unaccounted for: {remaining}");
|
|
||||||
ImGui.Text("See logs for more details: /xllog");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: implement Importer.MaxMessageRate slider in UI, values 0 (infinity) => 10000
|
|
||||||
|
|
||||||
ImGui.Text($"Importing messages... {Importer.Progress:P}%");
|
|
||||||
ImGui.Text($"Duration: {Duration(importStart, Environment.TickCount64)}");
|
|
||||||
ImGui.Text($"Successfully imported: {successful} messages");
|
|
||||||
ImGui.Text($"Failed to import: {failed} messages");
|
|
||||||
ImGui.Text($"Progress: {Importer.ProcessedMessages}/{total} messages");
|
|
||||||
ImGui.Text($"Remaining: {remaining} messages");
|
|
||||||
ImGui.Text($"Messages per second: {Importer.CurrentMessageRate}");
|
|
||||||
ImGui.Text($"Estimated time remaining: {Importer.EstimatedTimeRemaining}");
|
|
||||||
ImGui.Text("See logs for more details: /xllog");
|
|
||||||
|
|
||||||
// TODO: this doesn't render properly
|
|
||||||
ImGui.ProgressBar(Importer.Progress, new Vector2(0.0f, 0.0f), $"{Importer.Progress:P}%");
|
|
||||||
|
|
||||||
if (ImGuiUtil.CtrlShiftButton("Cancel import", "Ctrl+Shift: cancel import and close window"))
|
|
||||||
{
|
|
||||||
// TODO: This currently crashes the whole game because we don't ask
|
|
||||||
// the importer thread to stop and wait for it to stop before
|
|
||||||
// disposing it.
|
|
||||||
// See LegacyMessageImporter.Dispose() for more details.
|
|
||||||
/*
|
|
||||||
Importer.Dispose();
|
|
||||||
Importer = null;
|
|
||||||
Eligibility = LegacyMessageImporterEligibility.CheckEligibility();
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static TimeSpan Duration(long startTicks, long endTicks)
|
|
||||||
{
|
|
||||||
return endTicks < startTicks ? TimeSpan.Zero : TimeSpan.FromTicks(endTicks - startTicks);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,225 @@
|
|||||||
|
using System.Numerics;
|
||||||
|
using ChatTwo.Util;
|
||||||
|
using Dalamud.Interface.Colors;
|
||||||
|
using Dalamud.Interface.Components;
|
||||||
|
using Dalamud.Interface.ImGuiNotification;
|
||||||
|
using Dalamud.Interface.ImGuiNotification.EventArgs;
|
||||||
|
using Dalamud.Interface.Internal.Notifications;
|
||||||
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
|
using Dalamud.Interface.Windowing;
|
||||||
|
using ImGuiNET;
|
||||||
|
|
||||||
|
namespace ChatTwo.Ui;
|
||||||
|
|
||||||
|
internal class LegacyMessageImporterWindow : Window
|
||||||
|
{
|
||||||
|
private readonly Plugin Plugin;
|
||||||
|
private readonly MessageStore _store;
|
||||||
|
|
||||||
|
private LegacyMessageImporterEligibility Eligibility { get; set; }
|
||||||
|
private LegacyMessageImporter? Importer { get; set; }
|
||||||
|
|
||||||
|
internal LegacyMessageImporterWindow(Plugin plugin) : base("Chat 2 Legacy Importer###chat2-legacy-importer")
|
||||||
|
{
|
||||||
|
Plugin = plugin;
|
||||||
|
|
||||||
|
Flags = ImGuiWindowFlags.NoResize;
|
||||||
|
Size = new Vector2(500, 400);
|
||||||
|
|
||||||
|
_store = plugin.MessageManager.Store;
|
||||||
|
Eligibility = LegacyMessageImporterEligibility.CheckEligibility();
|
||||||
|
LogAndNotify();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Importer?.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NotificationClicked(INotificationClickArgs args)
|
||||||
|
{
|
||||||
|
IsOpen = true;
|
||||||
|
args.Notification.DismissNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogAndNotify()
|
||||||
|
{
|
||||||
|
Plugin.Log.Info($"[Migration] Checked migration eligibility: {Eligibility.Status} - '{Eligibility.AdditionalIneligibilityInfo}'");
|
||||||
|
|
||||||
|
switch (Eligibility.Status)
|
||||||
|
{
|
||||||
|
case LegacyMessageImporterEligibilityStatus.Eligible:
|
||||||
|
{
|
||||||
|
var notification = Plugin.Notification.AddNotification(new Notification
|
||||||
|
{
|
||||||
|
// The user needs to dismiss this for it to go away.
|
||||||
|
Type = NotificationType.Info,
|
||||||
|
InitialDuration = TimeSpan.FromHours(24),
|
||||||
|
Title = "Chat2 Migration",
|
||||||
|
Content = "Import messages from old database into new database?\nClick for more information.",
|
||||||
|
Minimized = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.Click += NotificationClicked;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed:
|
||||||
|
{
|
||||||
|
var notification = Plugin.Notification.AddNotification(new Notification
|
||||||
|
{
|
||||||
|
Type = NotificationType.Warning,
|
||||||
|
InitialDuration = TimeSpan.FromMinutes(1),
|
||||||
|
Title = "Chat2 Migration",
|
||||||
|
Content = "Migration is not possible because the old database could not be opened.\nClick for more information.",
|
||||||
|
Minimized = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.Click += NotificationClicked;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Draw()
|
||||||
|
{
|
||||||
|
if (Importer != null)
|
||||||
|
{
|
||||||
|
DrawImportStatus();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.Eligible)
|
||||||
|
DrawEligible();
|
||||||
|
else
|
||||||
|
DrawIneligible();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawEligible()
|
||||||
|
{
|
||||||
|
ImGui.TextWrapped("Import database messages from legacy LiteDB database to SQLite database?");
|
||||||
|
ImGui.Text($"Message count: {Eligibility.MessageCount:N0}");
|
||||||
|
ImGui.Text($"Database size: {StringUtil.BytesToString(Eligibility.DatabaseSizeBytes)}");
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
var colorNormal = new Vector4(0.0f, 0.70f, 0.0f, 1.0f);
|
||||||
|
var colorHovered = new Vector4(0.059f, 0.49f, 0.0f, 1.0f);
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, colorNormal))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, colorHovered))
|
||||||
|
{
|
||||||
|
if (ImGui.Button("Yes, import messages"))
|
||||||
|
{
|
||||||
|
// Next draw call will run DrawImportStatus().
|
||||||
|
Importer = Eligibility.StartImport(_store, plugin: Plugin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.SameLine();
|
||||||
|
|
||||||
|
if (ImGuiUtil.CtrlShiftButtonColored("No, do not import messages", "Ctrl+Shift: renames old database to avoid prompting again"))
|
||||||
|
{
|
||||||
|
Eligibility.RenameOldDatabase();
|
||||||
|
IsOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawIneligible()
|
||||||
|
{
|
||||||
|
ImGui.Text("Your legacy LiteDB database is not eligible for import:");
|
||||||
|
switch (Eligibility.Status)
|
||||||
|
{
|
||||||
|
case LegacyMessageImporterEligibilityStatus.IneligibleOriginalDbNotExists:
|
||||||
|
ImGui.Text("The old database could not be found.");
|
||||||
|
break;
|
||||||
|
case LegacyMessageImporterEligibilityStatus.IneligibleMigrationDbExists:
|
||||||
|
ImGui.Text("The migration process was already started.");
|
||||||
|
break;
|
||||||
|
case LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed:
|
||||||
|
ImGui.Text("The old database could not be opened.");
|
||||||
|
break;
|
||||||
|
case LegacyMessageImporterEligibilityStatus.IneligibleNoMessages:
|
||||||
|
ImGui.Text("The old database contains no messages.");
|
||||||
|
break;
|
||||||
|
case LegacyMessageImporterEligibilityStatus.Eligible:
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(Eligibility.AdditionalIneligibilityInfo))
|
||||||
|
ImGui.Text(Eligibility.AdditionalIneligibilityInfo);
|
||||||
|
|
||||||
|
// LiteDB failures notify the user, so give them a chance to rename the
|
||||||
|
// database to avoid prompting again.
|
||||||
|
if (Eligibility.Status == LegacyMessageImporterEligibilityStatus.IneligibleLiteDbFailed)
|
||||||
|
{
|
||||||
|
if (ImGuiUtil.CtrlShiftButton("Rename old database", "Ctrl+Shift: rename old database to avoid import prompt in the future"))
|
||||||
|
{
|
||||||
|
if (Eligibility.RenameOldDatabase())
|
||||||
|
WrapperUtil.AddNotification("Successfully renamed the old database.", NotificationType.Success);
|
||||||
|
else
|
||||||
|
WrapperUtil.AddNotification("Rename failed, please check /xllog for more information.", NotificationType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawImportStatus()
|
||||||
|
{
|
||||||
|
if (Importer == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (Importer.ImportComplete != null)
|
||||||
|
{
|
||||||
|
ImGui.TextUnformatted($"Completed migration in {Duration(Importer.ImportStart, Importer.ImportComplete.Value):g}");
|
||||||
|
ImGui.TextUnformatted($"Successfully imported: {Importer.SuccessfulMessages} messages");
|
||||||
|
ImGui.TextUnformatted($"Failed to import: {Importer.FailedMessages} messages");
|
||||||
|
ImGui.TextUnformatted($"Unaccounted for: {Importer.RemainingMessages}");
|
||||||
|
ImGui.TextUnformatted("See logs for more details: /xllog");
|
||||||
|
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
if (ImGui.Button("Finish"))
|
||||||
|
IsOpen = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui.TextUnformatted($"Importing messages ... {Importer.Progress:P}");
|
||||||
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
|
ImGui.TextUnformatted($"Duration: {Duration(Importer.ImportStart, Environment.TickCount64):g}");
|
||||||
|
ImGui.TextUnformatted($"Progress: {Importer.ProcessedMessages}/{Importer.ImportCount} messages ({Importer.FailedMessages} failed)");
|
||||||
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
|
var width = ImGui.GetContentRegionAvail().X / 2;
|
||||||
|
ImGui.AlignTextToFramePadding();
|
||||||
|
ImGui.TextUnformatted("Import speed:");
|
||||||
|
ImGui.SameLine();
|
||||||
|
ImGui.SetNextItemWidth(width);
|
||||||
|
ImGui.SliderInt("##speedSlider", ref Importer.MaxMessageRate, 1, 10000, "%d m/sec", ImGuiSliderFlags.AlwaysClamp);
|
||||||
|
ImGui.TextUnformatted($"Current speed: {Importer.CurrentMessageRate:N0} m/sec");
|
||||||
|
ImGui.TextUnformatted($"Estimated time remaining: {Importer.EstimatedTimeRemaining:g}");
|
||||||
|
ImGui.TextUnformatted("See logs for more details: /xllog");
|
||||||
|
ImGuiHelpers.ScaledDummy(10.0f);
|
||||||
|
|
||||||
|
ImGui.ProgressBar(Importer.Progress, new Vector2(-1, 0), $"{Importer.Progress:P}");
|
||||||
|
ImGui.Spacing();
|
||||||
|
|
||||||
|
if (ImGuiUtil.CtrlShiftButton("Cancel import", "Ctrl+Shift: cancel import and close window"))
|
||||||
|
{
|
||||||
|
Task.Run(async () =>
|
||||||
|
{
|
||||||
|
await Importer.DisposeAsync();
|
||||||
|
Importer = null;
|
||||||
|
Eligibility = LegacyMessageImporterEligibility.CheckEligibility();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static TimeSpan Duration(long startTicks, long endTicks)
|
||||||
|
{
|
||||||
|
return endTicks < startTicks ? TimeSpan.Zero : TimeSpan.FromMilliseconds(endTicks - startTicks);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -108,11 +108,11 @@ internal sealed class Database : ISettingsTab
|
|||||||
|
|
||||||
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Size, StringUtil.BytesToString(DatabaseSize)));
|
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_Size, StringUtil.BytesToString(DatabaseSize)));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip(DatabaseSize.ToString("N0") + "B");
|
ImGui.SetTooltip(StringUtil.BytesToString(DatabaseSize));
|
||||||
|
|
||||||
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_LogSize, StringUtil.BytesToString(DatabaseLogSize)));
|
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_LogSize, StringUtil.BytesToString(DatabaseLogSize)));
|
||||||
if (ImGui.IsItemHovered())
|
if (ImGui.IsItemHovered())
|
||||||
ImGui.SetTooltip(DatabaseLogSize.ToString("N0") + "B");
|
ImGui.SetTooltip(StringUtil.BytesToString(DatabaseLogSize));
|
||||||
|
|
||||||
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount));
|
ImGuiUtil.HelpText(string.Format(Language.Options_Database_Metadata_MessageCount, DatabaseMessageCount));
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using Dalamud.Game.Text.SeStringHandling;
|
|||||||
using Dalamud.Interface;
|
using Dalamud.Interface;
|
||||||
using Dalamud.Interface.Style;
|
using Dalamud.Interface.Style;
|
||||||
using Dalamud.Interface.Utility;
|
using Dalamud.Interface.Utility;
|
||||||
|
using Dalamud.Interface.Utility.Raii;
|
||||||
using ImGuiNET;
|
using ImGuiNET;
|
||||||
|
|
||||||
namespace ChatTwo.Util;
|
namespace ChatTwo.Util;
|
||||||
@@ -262,20 +263,39 @@ internal static class ImGuiUtil
|
|||||||
|
|
||||||
internal static bool CtrlShiftButton(string label, string tooltip = "")
|
internal static bool CtrlShiftButton(string label, string tooltip = "")
|
||||||
{
|
{
|
||||||
var io = ImGui.GetIO();
|
var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true };
|
||||||
var ctrlShiftHeld = io.KeyCtrl && io.KeyShift;
|
if (!ctrlShiftHeld)
|
||||||
if (!ctrlShiftHeld) ImGui.BeginDisabled();
|
ImGui.BeginDisabled();
|
||||||
|
|
||||||
var ret = ImGui.Button(label) && ctrlShiftHeld;
|
var ret = ImGui.Button(label) && ctrlShiftHeld;
|
||||||
if (!ctrlShiftHeld) ImGui.EndDisabled();
|
|
||||||
if (!string.IsNullOrEmpty(tooltip) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled)) {
|
if (!ctrlShiftHeld)
|
||||||
ImGui.BeginTooltip();
|
ImGui.EndDisabled();
|
||||||
ImGui.TextUnformatted(tooltip);
|
|
||||||
ImGui.EndTooltip();
|
if (!string.IsNullOrEmpty(tooltip) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
}
|
ImGui.SetTooltip(tooltip);
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool CtrlShiftButtonColored(string label, string tooltip = "")
|
||||||
|
{
|
||||||
|
var ctrlShiftHeld = ImGui.GetIO() is { KeyCtrl: true, KeyShift: true };
|
||||||
|
|
||||||
|
var colorNormal = new Vector4(0.780f, 0.245f, 0.245f, 1.0f);
|
||||||
|
var colorHovered = new Vector4(0.7f, 0.0f, 0.0f, 1.0f);
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.Button, colorNormal))
|
||||||
|
using (ImRaii.PushColor(ImGuiCol.ButtonHovered, colorHovered))
|
||||||
|
{
|
||||||
|
var ret = ImGui.Button(label) && ctrlShiftHeld;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(tooltip) && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
||||||
|
ImGui.SetTooltip(tooltip);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool TryToImGui(this VirtualKey key, out ImGuiKey result)
|
internal static bool TryToImGui(this VirtualKey key, out ImGuiKey result)
|
||||||
{
|
{
|
||||||
result = key switch {
|
result = key switch {
|
||||||
|
|||||||
Reference in New Issue
Block a user