mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2026-10-13 18:53:48 +00:00
Added update system to the app (#13)
This commit is contained in:
1 parent
50a03e6c9d
commit
d6e80a4563
33 files changed
+786
-81
No files matched your search
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user