Added update system to the app (#13)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2024-06-30 15:26:28 +02:00
1 parent 50a03e6c9d
commit d6e80a4563
33 files changed
+786 -81

No files matched your search

+13
View File
@@ -0,0 +1,13 @@
namespace AIStudio.Tools;
public enum Event
{
NONE,
// Common events:
STATE_HAS_CHANGED,
// Update events:
USER_SEARCH_FOR_UPDATE,
UPDATE_AVAILABLE,
}
@@ -0,0 +1,8 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Tools;
public interface IMessageBusReceiver
{
public Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
}
@@ -0,0 +1,44 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Tools;
public abstract class MSGComponentBase : ComponentBase, IDisposable, IMessageBusReceiver
{
[Inject]
protected MessageBus MessageBus { get; init; } = null!;
#region Overrides of ComponentBase
protected override void OnInitialized()
{
this.MessageBus.RegisterComponent(this);
base.OnInitialized();
}
#endregion
#region Implementation of IMessageBusReceiver
public abstract Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data);
#endregion
#region Implementation of IDisposable
public void Dispose()
{
this.MessageBus.Unregister(this);
}
#endregion
protected async Task SendMessage<T>(Event triggeredEvent, T? data = default)
{
await this.MessageBus.SendMessage(this, triggeredEvent, data);
}
protected void ApplyFilters(ComponentBase[] components, Event[] events)
{
this.MessageBus.ApplyFilters(this, components, events);
}
}
@@ -0,0 +1,67 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Components;
// ReSharper disable RedundantRecordClassKeyword
namespace AIStudio.Tools;
public sealed class MessageBus
{
public static readonly MessageBus INSTANCE = new();
private readonly ConcurrentDictionary<IMessageBusReceiver, ComponentBase[]> componentFilters = new();
private readonly ConcurrentDictionary<IMessageBusReceiver, Event[]> componentEvents = new();
private readonly ConcurrentQueue<Message> messageQueue = new();
private readonly SemaphoreSlim sendingSemaphore = new(1, 1);
private MessageBus()
{
}
public void ApplyFilters(IMessageBusReceiver receiver, ComponentBase[] components, Event[] events)
{
this.componentFilters[receiver] = components;
this.componentEvents[receiver] = events;
}
public void RegisterComponent(IMessageBusReceiver receiver)
{
this.componentFilters.TryAdd(receiver, []);
this.componentEvents.TryAdd(receiver, []);
}
public void Unregister(IMessageBusReceiver receiver)
{
this.componentFilters.TryRemove(receiver, out _);
this.componentEvents.TryRemove(receiver, out _);
}
private record class Message(ComponentBase? SendingComponent, Event TriggeredEvent, object? Data);
public async Task SendMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data = default)
{
this.messageQueue.Enqueue(new Message(sendingComponent, triggeredEvent, data));
try
{
await this.sendingSemaphore.WaitAsync();
while (this.messageQueue.TryDequeue(out var message))
{
foreach (var (receiver, componentFilter) in this.componentFilters)
{
if (componentFilter.Length > 0 && sendingComponent is not null && !componentFilter.Contains(sendingComponent))
continue;
var eventFilter = this.componentEvents[receiver];
if (eventFilter.Length == 0 || eventFilter.Contains(triggeredEvent))
// We don't await the task here because we don't want to block the message bus:
_ = receiver.ProcessMessage(message.SendingComponent, message.TriggeredEvent, message.Data);
}
}
}
finally
{
this.sendingSemaphore.Release();
}
}
}
@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Components;
namespace AIStudio.Tools;
public static class MessageBusExtensions
{
public static async Task SendMessage<T>(this ComponentBase component, Event triggeredEvent, T? data = default)
{
await MessageBus.INSTANCE.SendMessage(component, triggeredEvent, data);
}
public static void ApplyFilters(this IMessageBusReceiver component, ComponentBase[] components, Event[] events)
{
MessageBus.INSTANCE.ApplyFilters(component, components, events);
}
}
+12
View File
@@ -39,4 +39,16 @@ public sealed class Rust
};
});
}
public async Task<UpdateResponse> CheckForUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(16));
return await jsRuntime.InvokeAsync<UpdateResponse>("window.__TAURI__.invoke", cts.Token, "check_for_update");
}
public async Task InstallUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource();
await jsRuntime.InvokeVoidAsync("window.__TAURI__.invoke", cts.Token, "install_update");
}
}
@@ -3,6 +3,6 @@ namespace AIStudio.Tools;
/// <summary>
/// The response from the set clipboard operation.
/// </summary>
/// <param name="Success">True when the operation was successful.</param>
/// <param name="Issue">The issue that occurred during the operation, empty when successful.</param>
/// <param name="Success">True, when the operation was successful.</param>
/// <param name="Issue">The issues, which occurred during the operation, empty when successful.</param>
public readonly record struct SetClipboardResponse(bool Success, string Issue);
@@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
/// <summary>
/// The response of the update check.
/// </summary>
/// <param name="UpdateIsAvailable">True if an update is available.</param>
/// <param name="NewVersion">The new version, when available.</param>
/// <param name="Changelog">The changelog of the new version, when available.</param>
public readonly record struct UpdateResponse(
[property:JsonPropertyName("update_is_available")] bool UpdateIsAvailable,
[property:JsonPropertyName("error")] bool Error,
[property:JsonPropertyName("new_version")] string NewVersion,
string Changelog
);
@@ -0,0 +1,105 @@
using AIStudio.Settings;
using Microsoft.AspNetCore.Components;
namespace AIStudio.Tools;
public sealed class UpdateService : BackgroundService, IMessageBusReceiver
{
// We cannot inject IJSRuntime into our service. This is due to the fact that
// the service is not a Blaozor component. We need to pass the IJSRuntime from
// the MainLayout component to the service.
private static IJSRuntime? JS_RUNTIME;
private static bool IS_INITALIZED;
private readonly SettingsManager settingsManager;
private readonly TimeSpan updateInterval;
private readonly MessageBus messageBus;
private readonly Rust rust;
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, Rust rust)
{
this.settingsManager = settingsManager;
this.messageBus = messageBus;
this.rust = rust;
this.messageBus.RegisterComponent(this);
this.ApplyFilters([], [ Event.USER_SEARCH_FOR_UPDATE ]);
this.updateInterval = settingsManager.ConfigurationData.UpdateBehavior switch
{
UpdateBehavior.NO_CHECK => Timeout.InfiniteTimeSpan,
UpdateBehavior.ONCE_STARTUP => Timeout.InfiniteTimeSpan,
UpdateBehavior.HOURLY => TimeSpan.FromHours(1),
UpdateBehavior.DAILY => TimeSpan.FromDays(1),
UpdateBehavior.WEEKLY => TimeSpan.FromDays(7),
_ => TimeSpan.FromHours(1)
};
}
#region Overrides of BackgroundService
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested && !IS_INITALIZED)
{
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
await this.settingsManager.LoadSettings();
if(this.settingsManager.ConfigurationData.UpdateBehavior != UpdateBehavior.NO_CHECK)
await this.CheckForUpdate();
while (!stoppingToken.IsCancellationRequested)
{
await Task.Delay(this.updateInterval, stoppingToken);
await this.CheckForUpdate();
}
}
#endregion
#region Implementation of IMessageBusReceiver
public async Task ProcessMessage<T>(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
{
switch (triggeredEvent)
{
case Event.USER_SEARCH_FOR_UPDATE:
await this.CheckForUpdate();
break;
}
}
#endregion
#region Overrides of BackgroundService
public override async Task StopAsync(CancellationToken cancellationToken)
{
this.messageBus.Unregister(this);
await base.StopAsync(cancellationToken);
}
#endregion
private async Task CheckForUpdate()
{
if(!IS_INITALIZED)
return;
var response = await this.rust.CheckForUpdate(JS_RUNTIME!);
if (response.UpdateIsAvailable)
{
await this.messageBus.SendMessage(null, Event.UPDATE_AVAILABLE, response);
}
}
public static void SetJsRuntime(IJSRuntime jsRuntime)
{
JS_RUNTIME = jsRuntime;
IS_INITALIZED = true;
}
}