namespace AIStudio.Tools;
///
/// Tells the user that a file is being exported, and afterward where it went.
///
///
/// Start it once the user chose the file, and dispose of it when the export is over, also after an error:
/// disposing removes the notification that the export runs, in case it still stands. That notification
/// appears only after a moment, so that a fast export shows its success right away instead of two
/// notifications which flicker one after the other.
///
/// The notification that the export runs must never appear after its end, where it would stay until the
/// user closes it. The delayed notification and the end of the export therefore take the same lock, and
/// whichever comes second sees what the first one did. That settles the order in which they are sent;
/// FileExportNotifications settles the order in which they take effect.
///
public sealed class FileExportFeedback : IAsyncDisposable
{
///
/// How long an export runs before the user hears about it.
///
private static readonly TimeSpan RUNNING_MESSAGE_DELAY = TimeSpan.FromMilliseconds(500);
private readonly Func send;
private readonly Func waitForRunningMessage;
private readonly string key = Guid.NewGuid().ToString("N");
private readonly SemaphoreSlim sendLock = new(1, 1);
private readonly CancellationTokenSource runningMessageCancellation = new();
private DataFileExportRunningMessage runningMessage;
private Task delayedRunningMessage = Task.CompletedTask;
private bool isRunningMessageShown;
private bool isFinished;
public FileExportFeedback() : this((triggeredEvent, data) => MessageBus.INSTANCE.SendMessage(null, triggeredEvent, data), token => Task.Delay(RUNNING_MESSAGE_DELAY, token))
{
}
///
/// Creates the feedback with what it needs from outside, so that tests can stand in for both.
///
/// Sends a notification, as the message bus does.
/// Waits until the notification that the export runs is due.
internal FileExportFeedback(Func send, Func waitForRunningMessage)
{
this.send = send;
this.waitForRunningMessage = waitForRunningMessage;
}
///
/// Shows that the given file is being exported, unless the export ends before that is due.
///
/// The file the export writes.
public void ShowRunning(string filePath)
{
this.runningMessage = new DataFileExportRunningMessage(this.key, filePath);
this.delayedRunningMessage = this.ShowRunningWhenDueAsync(this.runningMessageCancellation.Token);
}
///
/// Ends the export with the notification of its success, which replaces the one that it runs.
///
/// The icon of the notification.
/// What was exported.
/// The file which was written.
/// Whether the file may be opened with the default program.
public Task SucceededAsync(string icon, string message, string filePath, bool offersOpening) => this.FinishAsync(new DataFileExportSuccessMessage(this.key, icon, message, filePath, offersOpening));
private async Task ShowRunningWhenDueAsync(CancellationToken token)
{
try
{
await this.waitForRunningMessage(token);
}
catch (OperationCanceledException)
{
return;
}
await this.sendLock.WaitAsync();
try
{
if (this.isFinished)
return;
await this.send(Event.SHOW_FILE_EXPORT_RUNNING, this.runningMessage);
this.isRunningMessageShown = true;
}
finally
{
this.sendLock.Release();
}
}
///
/// Ends the export, with the notification of its success or without any.
///
/// The notification of the success, or null when the export failed.
private async Task FinishAsync(DataFileExportSuccessMessage? success)
{
await this.runningMessageCancellation.CancelAsync();
await this.sendLock.WaitAsync();
try
{
if (this.isFinished)
return;
this.isFinished = true;
if (success is { } successMessage)
await this.send(Event.SHOW_FILE_EXPORT_SUCCESS, successMessage);
else if (this.isRunningMessageShown)
await this.send(Event.HIDE_FILE_EXPORT_RUNNING, this.runningMessage);
}
finally
{
this.sendLock.Release();
}
}
#region Implementation of IAsyncDisposable
public async ValueTask DisposeAsync()
{
await this.FinishAsync(null);
// The delayed notification may still wait for the lock; it sees the end and gives up:
await this.delayedRunningMessage;
this.runningMessageCancellation.Dispose();
this.sendLock.Dispose();
}
#endregion
}