c955f30422
Six UI files shift from Plugin.LogProxy to ILogger<T> via constructor injection. Container singletons (each takes a typed ILogger plus, where it owns nested allocations, an ILoggerFactory to spawn child loggers): - Ui/ChatLogWindow (15 sites, plus an ILoggerFactory for the Popout new-call at Ui/ChatLogWindow.cs:2417) - Ui/Settings (SettingsWindow): no own sites, but takes an ILoggerFactory so it can hand typed loggers to its three migrated settings tabs (General, the other six tabs stay unchanged) - Ui/DbViewer (3 sites) Nested instances allocated by parent containers: - Ui/Popout (7 sites, ILogger<Popout> as the new 4th ctor arg passed from ChatLogWindow) - Ui/SettingsTabs/ThemeAndLayout (1 site) - Ui/SettingsTabs/FontsAndColours (1 site) - Ui/SettingsTabs/DataManagement (15 sites) PluginHostFactory factory lambdas updated for ChatLogWindow, SettingsWindow and DbViewer to resolve the new logger args.
645 lines
22 KiB
C#
645 lines
22 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Globalization;
|
|
using System.Numerics;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using Dalamud.Bindings.ImGui;
|
|
using Dalamud.Game.Text.SeStringHandling.Payloads;
|
|
using Dalamud.Interface;
|
|
using Dalamud.Interface.Colors;
|
|
using Dalamud.Interface.Components;
|
|
using Dalamud.Interface.ImGuiNotification;
|
|
using Dalamud.Interface.Utility;
|
|
using Dalamud.Interface.Utility.Raii;
|
|
using Dalamud.Interface.Windowing;
|
|
using HellionChat.Code;
|
|
using HellionChat.Resources;
|
|
using HellionChat.Util;
|
|
using Lumina.Data.Files;
|
|
using Lumina.Text.ReadOnly;
|
|
using Microsoft.Extensions.Logging;
|
|
using MoreLinq;
|
|
|
|
namespace HellionChat.Ui;
|
|
|
|
public class DbViewer : Window
|
|
{
|
|
public const int RowPerPage = 1000;
|
|
|
|
private readonly Plugin Plugin;
|
|
|
|
private static readonly DateTime MinimalDate = new(2021, 1, 1);
|
|
|
|
private DateTime AfterDate;
|
|
private DateTime BeforeDate;
|
|
|
|
private int CurrentPage = 1;
|
|
private string SimpleSearchTerm = "";
|
|
|
|
// v1.4.8 H2: opt-in full-text search across the whole DB via FTS5.
|
|
// Transient UI state (per-session), not persisted -- users opt in fresh
|
|
// every time so they always see the page-filter as the default mode.
|
|
private bool UseFullTextSearch;
|
|
|
|
private bool OnlyCurrentCharacter = true;
|
|
private readonly Dictionary<ChatType, (ChatSource, ChatSource)> SelectedChannels;
|
|
|
|
private bool IsProcessing;
|
|
private long ProcessingStart = Environment.TickCount64;
|
|
|
|
// Bumped per trigger so a late worker drops itself instead of overwriting
|
|
// a newer result.
|
|
private long _ftsFilterSeq;
|
|
private (DateTime Min, DateTime Max, int Page, bool Local, int ChannelCount) LastProcessed;
|
|
|
|
private string MinDateString = "";
|
|
private string MaxDateString = "";
|
|
|
|
private readonly string DateFormat;
|
|
private readonly string DateTimeFormat;
|
|
|
|
private long Count;
|
|
private Message[] Messages = []; // Messages are only touched while processing is false
|
|
private ConcurrentStack<Message> Filtered = []; // Is used every frame, so ConcurrentStack for safety
|
|
|
|
private bool IsExporting;
|
|
private string InputPath = string.Empty;
|
|
private IActiveNotification Notification = null!;
|
|
|
|
private bool NeedsScrollReset;
|
|
|
|
private readonly ILogger<DbViewer> _logger;
|
|
|
|
public DbViewer(Plugin plugin, ILogger<DbViewer> logger)
|
|
: base("DBViewer###chat2-dbviewer")
|
|
{
|
|
Plugin = plugin;
|
|
_logger = logger;
|
|
SelectedChannels = TabsUtil.MostlyPlayer;
|
|
|
|
DateFormat = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
|
|
DateTimeFormat = "ddd, dd MMM yyy HH:mm:ss";
|
|
|
|
LastProcessed = (
|
|
AfterDate,
|
|
BeforeDate,
|
|
CurrentPage,
|
|
OnlyCurrentCharacter,
|
|
SelectedChannels.Count
|
|
);
|
|
DateReset();
|
|
|
|
SizeConstraints = new WindowSizeConstraints
|
|
{
|
|
MinimumSize = new Vector2(475, 600),
|
|
MaximumSize = new Vector2(float.MaxValue, float.MaxValue),
|
|
};
|
|
|
|
RespectCloseHotkey = false;
|
|
DisableWindowSounds = true;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
// Slash-command tear-down moved to Plugin.TearDownCommands.
|
|
}
|
|
|
|
public override void Draw()
|
|
{
|
|
var totalPages = (int)Math.Ceiling((double)Count / RowPerPage);
|
|
if (totalPages < 1)
|
|
totalPages = 1;
|
|
|
|
if (CurrentPage > totalPages)
|
|
CurrentPage = 1;
|
|
|
|
// First row
|
|
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextColored(ImGuiColors.DalamudViolet, Language.DbViewer_DatePicker_FromTo);
|
|
ImGui.SameLine();
|
|
|
|
var spacing = 3.0f * ImGuiHelpers.GlobalScale;
|
|
DateWidget.DatePickerWithInput(
|
|
"##FromDate",
|
|
1,
|
|
ref MinDateString,
|
|
ref AfterDate,
|
|
DateFormat
|
|
);
|
|
DateWidget.DatePickerWithInput(
|
|
"##ToDate",
|
|
2,
|
|
ref MaxDateString,
|
|
ref BeforeDate,
|
|
DateFormat,
|
|
true
|
|
);
|
|
|
|
ImGui.SameLine(0, spacing);
|
|
|
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.Recycle))
|
|
DateReset();
|
|
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip(Language.DbViewer_Date_Reset_Tooltip);
|
|
|
|
ImGui.SameLine(0, spacing);
|
|
|
|
ChannelSelection();
|
|
|
|
var skipText = Language.DbViewer_CharacterOption;
|
|
var textLength =
|
|
ImGui.GetTextLineHeight()
|
|
+ ImGui.CalcTextSize(skipText).X
|
|
+ ImGui.GetStyle().ItemInnerSpacing.X
|
|
+ ImGui.GetStyle().FramePadding.X * 2;
|
|
ImGui.SameLine(ImGui.GetContentRegionMax().X - textLength);
|
|
ImGui.Checkbox(skipText, ref OnlyCurrentCharacter);
|
|
|
|
// Second row
|
|
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextColored(ImGuiColors.DalamudViolet, "Export:");
|
|
ImGui.SameLine(0, spacing);
|
|
ImGui.SetNextItemWidth(ImGui.GetWindowWidth() * 0.4f);
|
|
ImGui.InputText("##InputPath", ref InputPath, 255);
|
|
ImGui.SameLine(0, spacing);
|
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.FolderClosed, "##folderPicker"))
|
|
ImGui.OpenPopup("InputPathDialog");
|
|
|
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
|
ImGui.SetTooltip(Language.Folder_Export_Location_Tooltip);
|
|
|
|
using (var innerPopup = ImRaii.Popup("InputPathDialog"))
|
|
{
|
|
if (innerPopup.Success)
|
|
Plugin.FileDialogManager.OpenFolderDialog(
|
|
Language.Folder_Selection_Header,
|
|
(b, s) =>
|
|
{
|
|
if (b)
|
|
InputPath = s;
|
|
},
|
|
null,
|
|
true
|
|
);
|
|
}
|
|
|
|
ImGui.SameLine(0, spacing);
|
|
using (ImRaii.Disabled(InputPath.Length == 0 || IsExporting))
|
|
{
|
|
if (ImGuiUtil.IconButton(FontAwesomeIcon.Save))
|
|
{
|
|
Notification = Plugin.Notification.AddNotification(
|
|
new Notification
|
|
{
|
|
Title = "Chat2 Text Export",
|
|
Content = Language.ChatExport_Initial,
|
|
Type = NotificationType.Info,
|
|
Minimized = false,
|
|
UserDismissable = false,
|
|
InitialDuration = TimeSpan.FromSeconds(10000),
|
|
Progress = 0.0f,
|
|
}
|
|
);
|
|
CreateTxtBackup();
|
|
}
|
|
}
|
|
|
|
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
|
|
ImGui.SetTooltip(Language.Export_Txt_Tooltip);
|
|
|
|
var width = 350 * ImGuiHelpers.GlobalScale;
|
|
var loadingIndicator = IsProcessing && ProcessingStart < Environment.TickCount64;
|
|
|
|
ImGui.AlignTextToFramePadding();
|
|
ImGui.TextUnformatted(
|
|
string.Format(
|
|
Language.DbViewer_Page,
|
|
CurrentPage,
|
|
totalPages,
|
|
Count,
|
|
loadingIndicator ? Language.DbViewer_LoadingIndicator : ""
|
|
)
|
|
);
|
|
ImGuiUtil.DrawArrows(
|
|
ref CurrentPage,
|
|
1,
|
|
totalPages,
|
|
spacing,
|
|
tooltipLeft: Language.Page_ArrowLeft_Tooltip,
|
|
tooltipRight: Language.Page_ArrowRight_Tooltip
|
|
);
|
|
|
|
// Full-text search toggle (v1.4.8 H2). IsFtsIndexBuilt is a cached
|
|
// volatile bool in MessageStore -- single field read per frame, no
|
|
// SELECT count(*). ImRaii.Disabled blocks any click while the index
|
|
// is still being built, so no defensive force-off branch needed
|
|
// inside the if-body. UseFullTextSearch is transient UI state, so we
|
|
// do not call SaveConfig here.
|
|
var ftsReady = Plugin.MessageManager.Store.IsFtsIndexBuilt;
|
|
using (ImRaii.Disabled(!ftsReady))
|
|
{
|
|
if (ImGui.Checkbox(HellionStrings.DbViewer_FullTextToggle, ref UseFullTextSearch))
|
|
TriggerFilterRefresh();
|
|
}
|
|
ImGuiUtil.HelpMarker(
|
|
ftsReady
|
|
? HellionStrings.DbViewer_FullTextToggle_Hint_PhraseMode
|
|
: HellionStrings.DbViewer_FullTextToggle_Hint_Indexing
|
|
);
|
|
|
|
ImGui.SameLine(ImGui.GetContentRegionMax().X - width);
|
|
ImGui.SetNextItemWidth(width);
|
|
if (
|
|
ImGui.InputTextWithHint(
|
|
"##searchbar",
|
|
Language.DbViewer_SearcHint,
|
|
ref SimpleSearchTerm,
|
|
30
|
|
)
|
|
)
|
|
TriggerFilterRefresh();
|
|
|
|
// Third row
|
|
|
|
if (DateWidget.Validate(MinimalDate, ref AfterDate, ref BeforeDate))
|
|
DateRefresh();
|
|
|
|
if (
|
|
!IsProcessing
|
|
&& LastProcessed
|
|
!= (
|
|
AfterDate,
|
|
BeforeDate,
|
|
CurrentPage,
|
|
OnlyCurrentCharacter,
|
|
SelectedChannels.Count
|
|
)
|
|
)
|
|
{
|
|
// Page hasn't changed, so we reset it back to 1
|
|
if (LastProcessed.Page == CurrentPage)
|
|
CurrentPage = 1;
|
|
|
|
AdjustDates();
|
|
IsProcessing = true;
|
|
ProcessingStart = Environment.TickCount64 + 1_000; // + 1 second
|
|
LastProcessed = (
|
|
AfterDate,
|
|
BeforeDate,
|
|
CurrentPage,
|
|
OnlyCurrentCharacter,
|
|
SelectedChannels.Count
|
|
);
|
|
Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
ulong? character = OnlyCurrentCharacter ? Plugin.PlayerState.ContentId : null;
|
|
var channels = SelectedChannels.Select(pair => (byte)pair.Key).ToArray();
|
|
|
|
// We only want to fetch count if this is the first page
|
|
if (CurrentPage == 1)
|
|
Count = Plugin.MessageManager.Store.CountDateRange(
|
|
AfterDate,
|
|
BeforeDate,
|
|
channels,
|
|
character
|
|
);
|
|
|
|
using var rangeMessageEnumerator =
|
|
Plugin.MessageManager.Store.GetPagedDateRange(
|
|
AfterDate,
|
|
BeforeDate,
|
|
channels,
|
|
character,
|
|
CurrentPage - 1
|
|
);
|
|
Messages = rangeMessageEnumerator.ToArray();
|
|
|
|
Filtered = Filter(Messages);
|
|
NeedsScrollReset = true;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed reading messages from database");
|
|
}
|
|
finally
|
|
{
|
|
IsProcessing = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
ImGuiHelpers.ScaledDummy(5.0f);
|
|
|
|
if (Filtered.IsEmpty)
|
|
{
|
|
ImGui.TextUnformatted(
|
|
SimpleSearchTerm == ""
|
|
? Language.DbViewer_Status_NothingFound
|
|
: Language.DbViewer_Status_NoSearchResult
|
|
);
|
|
return;
|
|
}
|
|
|
|
using var child = ImRaii.Child("##tableChild");
|
|
if (!child.Success)
|
|
return;
|
|
|
|
if (NeedsScrollReset)
|
|
{
|
|
NeedsScrollReset = false;
|
|
ImGui.SetScrollY(0.0f);
|
|
}
|
|
|
|
using var table = ImRaii.Table(
|
|
"##messageHistory",
|
|
4,
|
|
ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg | ImGuiTableFlags.Resizable
|
|
);
|
|
if (!table.Success)
|
|
return;
|
|
|
|
var columnWidth = ImGui.CalcTextSize(Language.DbViewer_TableField_Type);
|
|
ImGui.TableSetupColumn(
|
|
Language.DbViewer_TableField_Date,
|
|
ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize
|
|
);
|
|
ImGui.TableSetupColumn(
|
|
Language.DbViewer_TableField_Type,
|
|
ImGuiTableColumnFlags.WidthFixed | ImGuiTableColumnFlags.NoResize,
|
|
columnWidth.X
|
|
);
|
|
ImGui.TableSetupColumn(Language.DbViewer_TableField_Sender);
|
|
ImGui.TableSetupColumn(Language.DbViewer_TableField_Content);
|
|
|
|
ImGui.TableHeadersRow();
|
|
foreach (var message in Filtered)
|
|
{
|
|
ImGui.TableNextColumn();
|
|
ImGui.TextUnformatted(message.Date.ToLocalTime().ToString(DateTimeFormat));
|
|
|
|
ImGui.TableNextColumn();
|
|
var pos = ImGui.GetCursorPos();
|
|
ImGuiUtil.CenterText($"{(byte)message.Code.Type}");
|
|
ImGui.SetCursorPos(pos);
|
|
ImGui.Dummy(columnWidth);
|
|
if (ImGui.IsItemHovered())
|
|
ImGuiUtil.Tooltip(message.Code.Type.Name());
|
|
|
|
ImGui.TableNextColumn();
|
|
Plugin.ChatLogWindow.DrawChunks(message.Sender);
|
|
|
|
ImGui.TableNextColumn();
|
|
Plugin.ChatLogWindow.DrawChunks(message.Content);
|
|
}
|
|
}
|
|
|
|
private void ChannelSelection()
|
|
{
|
|
const string addTabPopup = "add-channel-popup";
|
|
var spacing = 3.0f * ImGuiHelpers.GlobalScale;
|
|
|
|
if (ImGui.Button("Channels"))
|
|
ImGui.OpenPopup(addTabPopup);
|
|
|
|
using var popup = ImRaii.Popup(addTabPopup);
|
|
if (!popup.Success)
|
|
return;
|
|
|
|
using var channelNode = ImRaii.TreeNode(Language.Options_Tabs_Channels);
|
|
if (!channelNode.Success)
|
|
return;
|
|
|
|
foreach (var (header, types) in ChatTypeExt.SortOrder)
|
|
{
|
|
using var pushedId = ImRaii.PushId(header);
|
|
|
|
if (ImGuiComponents.IconButton(FontAwesomeIcon.Check))
|
|
{
|
|
foreach (var type in types)
|
|
SelectedChannels.TryAdd(type, (ChatSourceExt.All, ChatSourceExt.All));
|
|
}
|
|
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Select all");
|
|
|
|
ImGui.SameLine(0, spacing);
|
|
|
|
if (ImGuiComponents.IconButton(FontAwesomeIcon.Times))
|
|
{
|
|
foreach (var type in types)
|
|
SelectedChannels.Remove(type);
|
|
}
|
|
|
|
if (ImGui.IsItemHovered())
|
|
ImGui.SetTooltip("Unselect all");
|
|
|
|
ImGui.SameLine(0, spacing);
|
|
|
|
using var headerNode = ImRaii.TreeNode(header);
|
|
if (!headerNode.Success)
|
|
continue;
|
|
|
|
foreach (var type in types)
|
|
{
|
|
if (type.IsGm())
|
|
continue;
|
|
|
|
var enabled = SelectedChannels.ContainsKey(type);
|
|
if (ImGui.Checkbox($"##{type.Name()}", ref enabled))
|
|
{
|
|
if (enabled)
|
|
SelectedChannels[type] = (ChatSourceExt.All, ChatSourceExt.All);
|
|
else
|
|
SelectedChannels.Remove(type);
|
|
}
|
|
|
|
ImGui.SameLine();
|
|
ImGui.TextUnformatted(type.Name());
|
|
}
|
|
}
|
|
}
|
|
|
|
// FTS path hits SQLite per keystroke -- dispatch to a worker, drop stale
|
|
// results via _ftsFilterSeq. Page-filter path is in-memory LINQ, stays
|
|
// inline.
|
|
private void TriggerFilterRefresh()
|
|
{
|
|
if (!UseFullTextSearch || !Plugin.MessageManager.Store.IsFtsIndexBuilt)
|
|
{
|
|
Filtered = Filter(Messages);
|
|
return;
|
|
}
|
|
|
|
var snapshot = Messages;
|
|
var mySeq = Interlocked.Increment(ref _ftsFilterSeq);
|
|
Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
var result = Filter(snapshot);
|
|
if (Interlocked.Read(ref _ftsFilterSeq) == mySeq)
|
|
Filtered = result;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "FTS filter worker failed");
|
|
}
|
|
});
|
|
}
|
|
|
|
private ConcurrentStack<Message> Filter(Message[] messages)
|
|
{
|
|
if (SimpleSearchTerm == "")
|
|
return new ConcurrentStack<Message>(messages.Reverse().OrderByDescending(m => m.Date));
|
|
|
|
// Full-text mode bypasses the page-bounded messages array and queries
|
|
// the FTS5 index across the whole DB. IsFtsIndexBuilt re-check guards
|
|
// against the (rare) case of the toggle being on while the index is
|
|
// mid-rebuild -- ImRaii.Disabled prevents the user from flipping it,
|
|
// but a Dispose-and-reopen during indexing could leave UseFullTextSearch
|
|
// true while ftsReady flipped back to false; the local fallback below
|
|
// still serves the page.
|
|
if (UseFullTextSearch && Plugin.MessageManager.Store.IsFtsIndexBuilt)
|
|
{
|
|
var guidHits = Plugin.MessageManager.Store.FullTextSearch(SimpleSearchTerm);
|
|
var resolved = Plugin.MessageManager.Store.LoadByGuids(guidHits);
|
|
return new ConcurrentStack<Message>(resolved.OrderByDescending(m => m.Date));
|
|
}
|
|
|
|
return new ConcurrentStack<Message>(
|
|
messages
|
|
.Reverse()
|
|
.Where(m =>
|
|
ChunkUtil
|
|
.ToRawString(m.Sender)
|
|
.Contains(SimpleSearchTerm, StringComparison.InvariantCultureIgnoreCase)
|
|
|| ChunkUtil
|
|
.ToRawString(m.Content)
|
|
.Contains(SimpleSearchTerm, StringComparison.InvariantCultureIgnoreCase)
|
|
)
|
|
.OrderByDescending(m => m.Date)
|
|
);
|
|
}
|
|
|
|
private void DateRefresh()
|
|
{
|
|
MinDateString = AfterDate.ToString(DateFormat);
|
|
MaxDateString = BeforeDate.ToString(DateFormat);
|
|
}
|
|
|
|
private void AdjustDates()
|
|
{
|
|
AfterDate = new DateTime(AfterDate.Year, AfterDate.Month, AfterDate.Day, 0, 0, 0);
|
|
BeforeDate = new DateTime(BeforeDate.Year, BeforeDate.Month, BeforeDate.Day, 23, 59, 59);
|
|
}
|
|
|
|
private void DateReset()
|
|
{
|
|
AfterDate = DateTime.Now.AddDays(-5);
|
|
BeforeDate = DateTime.Now;
|
|
|
|
AdjustDates();
|
|
DateRefresh();
|
|
}
|
|
|
|
private void CreateTxtBackup()
|
|
{
|
|
IsExporting = true;
|
|
Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
ulong? character = OnlyCurrentCharacter ? Plugin.PlayerState.ContentId : null;
|
|
var channels = SelectedChannels.Select(pair => (byte)pair.Key).ToArray();
|
|
|
|
var rangeMessageEnumerator = Plugin.MessageManager.Store.GetDateRange(
|
|
AfterDate,
|
|
BeforeDate,
|
|
channels,
|
|
character
|
|
);
|
|
var messageHistory = rangeMessageEnumerator.ToArray();
|
|
await rangeMessageEnumerator.DisposeAsync();
|
|
|
|
var filteredHistory = Filter(messageHistory);
|
|
// Materialize Count once — re-enumerating the IEnumerable on
|
|
// every batch (twice per batch in the Notification update)
|
|
// turned the export into an O(N²) hot loop on large histories.
|
|
var totalCount = filteredHistory.Count;
|
|
|
|
var sb = new StringBuilder();
|
|
await using var stream = new StreamWriter(
|
|
Path.Join(InputPath, $"Chat2_{DateTime.Now:yyyy_dd_M__HH_mm_ss}.txt")
|
|
);
|
|
|
|
var batch = 0;
|
|
foreach (var messages in filteredHistory.Batch(5000))
|
|
{
|
|
await Plugin.Framework.RunOnTick(
|
|
() =>
|
|
{
|
|
foreach (var message in messages)
|
|
{
|
|
if (
|
|
!Sheets.LogKindSheet.TryGetRow(
|
|
(uint)message.Code.Type,
|
|
out var logKind
|
|
)
|
|
)
|
|
logKind = Sheets.LogKindSheet.GetRow(10); // default to say
|
|
|
|
var rossSender = new ReadOnlySeString(
|
|
message.SenderSource.Encode()
|
|
);
|
|
var rossMessage = new ReadOnlySeString(
|
|
message.ContentSource.Encode()
|
|
);
|
|
|
|
var timestamp = message.Date.ToLocalTime().ToString(DateTimeFormat);
|
|
var text = Plugin
|
|
.Evaluator.Evaluate(logKind.Format, [rossSender, rossMessage])
|
|
.ToString();
|
|
sb.AppendLine(
|
|
$"[{timestamp}][{message.Code.Type.Name()}] {text}"
|
|
);
|
|
|
|
batch++;
|
|
}
|
|
},
|
|
delayTicks: 5
|
|
);
|
|
|
|
Notification.Progress = (float)batch / totalCount;
|
|
Notification.Content = $"Exported {batch} of {totalCount} messages";
|
|
await stream.WriteAsync(sb.ToString());
|
|
sb.Clear();
|
|
}
|
|
|
|
await stream.WriteAsync(sb.ToString());
|
|
sb.Clear();
|
|
|
|
Notification.Progress = 1.0f;
|
|
Notification.Content = "Done!!!";
|
|
Notification.Type = NotificationType.Success;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Failed creating txt backup");
|
|
|
|
Notification.Content = "Error ...";
|
|
Notification.Type = NotificationType.Error;
|
|
}
|
|
finally
|
|
{
|
|
IsExporting = false;
|
|
Notification.UserDismissable = true;
|
|
}
|
|
});
|
|
}
|
|
}
|