@this.ChildContent
diff --git a/app/MindWork AI Studio/Components/Blocks/InnerScrolling.razor.cs b/app/MindWork AI Studio/Components/Blocks/InnerScrolling.razor.cs
index f9d7ed7c..4dee88c7 100644
--- a/app/MindWork AI Studio/Components/Blocks/InnerScrolling.razor.cs
+++ b/app/MindWork AI Studio/Components/Blocks/InnerScrolling.razor.cs
@@ -1,12 +1,15 @@
+using AIStudio.Components.Layout;
+using AIStudio.Tools;
+
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Blocks;
-public partial class InnerScrolling : ComponentBase
+public partial class InnerScrolling : MSGComponentBase
{
///
/// Set the height of anything above the scrolling content; usually a header.
- /// What we do is calc(100vh - THIS). Means, you can use multiple measures like
+ /// What we do is calc(100vh - HeaderHeight). Means, you can use multiple measures like
/// 230px - 3em. Default is 3em.
///
[Parameter]
@@ -21,5 +24,34 @@ public partial class InnerScrolling : ComponentBase
[Parameter]
public RenderFragment? FooterContent { get; set; }
- private string Height => $"height: calc(100vh - {this.HeaderHeight});";
+ [CascadingParameter]
+ private MainLayout MainLayout { get; set; } = null!;
+
+ #region Overrides of ComponentBase
+
+ protected override async Task OnInitializedAsync()
+ {
+ this.ApplyFilters([], [ Event.STATE_HAS_CHANGED ]);
+ await base.OnInitializedAsync();
+ }
+
+ #endregion
+
+ #region Overrides of MSGComponentBase
+
+ public override Task ProcessMessage
(ComponentBase? sendingComponent, Event triggeredEvent, T? data) where T : default
+ {
+ switch (triggeredEvent)
+ {
+ case Event.STATE_HAS_CHANGED:
+ this.StateHasChanged();
+ break;
+ }
+
+ return Task.CompletedTask;
+ }
+
+ #endregion
+
+ private string Height => $"height: calc(100vh - {this.HeaderHeight} - {this.MainLayout.AdditionalHeight});";
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/CommonDialogs/UpdateDialog.razor b/app/MindWork AI Studio/Components/CommonDialogs/UpdateDialog.razor
new file mode 100644
index 00000000..80544496
--- /dev/null
+++ b/app/MindWork AI Studio/Components/CommonDialogs/UpdateDialog.razor
@@ -0,0 +1,14 @@
+@using AIStudio.Tools
+
+
+
+
+ Update from v@(META_DATA.Version) to v@(this.UpdateResponse.NewVersion)
+
+
+
+
+ Install later
+ Install now
+
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/CommonDialogs/UpdateDialog.razor.cs b/app/MindWork AI Studio/Components/CommonDialogs/UpdateDialog.razor.cs
new file mode 100644
index 00000000..340788d4
--- /dev/null
+++ b/app/MindWork AI Studio/Components/CommonDialogs/UpdateDialog.razor.cs
@@ -0,0 +1,26 @@
+using System.Reflection;
+
+using AIStudio.Tools;
+
+using Microsoft.AspNetCore.Components;
+
+namespace AIStudio.Components.CommonDialogs;
+
+///
+/// The update dialog that is used to inform the user about an available update.
+///
+public partial class UpdateDialog : ComponentBase
+{
+ private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
+ private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute()!;
+
+ [CascadingParameter]
+ private MudDialogInstance MudDialog { get; set; } = null!;
+
+ [Parameter]
+ public UpdateResponse UpdateResponse { get; set; }
+
+ private void Cancel() => this.MudDialog.Cancel();
+
+ private void Confirm() => this.MudDialog.Close(DialogResult.Ok(true));
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/ConfigurationSelectData.cs b/app/MindWork AI Studio/Components/ConfigurationSelectData.cs
index 1fb9a573..1e77a1d4 100644
--- a/app/MindWork AI Studio/Components/ConfigurationSelectData.cs
+++ b/app/MindWork AI Studio/Components/ConfigurationSelectData.cs
@@ -21,4 +21,13 @@ public static class ConfigurationSelectDataFactory
yield return new("Modifier key + enter is sending the input", SendBehavior.MODIFER_ENTER_IS_SENDING);
yield return new("Enter is sending the input", SendBehavior.ENTER_IS_SENDING);
}
+
+ public static IEnumerable> GetUpdateBehaviorData()
+ {
+ yield return new("No automatic update checks", UpdateBehavior.NO_CHECK);
+ yield return new("Once at startup", UpdateBehavior.ONCE_STARTUP);
+ yield return new("Check every hour", UpdateBehavior.HOURLY);
+ yield return new("Check every day", UpdateBehavior.DAILY);
+ yield return new ("Check every week", UpdateBehavior.WEEKLY);
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Layout/MainLayout.razor b/app/MindWork AI Studio/Components/Layout/MainLayout.razor
index 8a0b6a42..8843c671 100644
--- a/app/MindWork AI Studio/Components/Layout/MainLayout.razor
+++ b/app/MindWork AI Studio/Components/Layout/MainLayout.razor
@@ -2,31 +2,57 @@
-
-
-
-
- Home
-
-
- Chats
-
-
- Supporters
-
-
- About
-
-
- Settings
-
-
-
-
+ @if (!this.performingUpdate)
+ {
+
+
+
+
+ Home
+
+
+ Chats
+
+
+ Supporters
+
+
+ About
+
+
+ Settings
+
+
+
+
+ }
- @this.Body
+ @if (!this.performingUpdate && this.IsUpdateAlertVisible)
+ {
+
+
+
+ An update to version @this.updateToVersion is available.
+
+ Show details
+
+
+
+ }
+
+ @if (!this.performingUpdate)
+ {
+
+ @this.Body
+
+ }
+
+
+ Please wait for the update to complete...
+
+
diff --git a/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs b/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs
index de25ecd7..609f8b95 100644
--- a/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs
+++ b/app/MindWork AI Studio/Components/Layout/MainLayout.razor.cs
@@ -1,12 +1,37 @@
+using AIStudio.Components.CommonDialogs;
using AIStudio.Settings;
+using AIStudio.Tools;
+
using Microsoft.AspNetCore.Components;
+using DialogOptions = AIStudio.Components.CommonDialogs.DialogOptions;
+
namespace AIStudio.Components.Layout;
-public partial class MainLayout : LayoutComponentBase
+public partial class MainLayout : LayoutComponentBase, IMessageBusReceiver
{
[Inject]
- private IJSRuntime JsRuntime { get; set; } = null!;
+ private IJSRuntime JsRuntime { get; init; } = null!;
+
+ [Inject]
+ private SettingsManager SettingsManager { get; init; } = null!;
+
+ [Inject]
+ private MessageBus MessageBus { get; init; } = null!;
+
+ [Inject]
+ public IDialogService DialogService { get; init; } = null!;
+
+ [Inject]
+ public Rust Rust { get; init; } = null!;
+
+ public string AdditionalHeight { get; private set; } = "0em";
+
+ private bool isUpdateAvailable;
+ private bool performingUpdate;
+ private bool userDismissedUpdate;
+ private string updateToVersion = string.Empty;
+ private UpdateResponse? currentUpdateResponse;
#region Overrides of ComponentBase
@@ -23,8 +48,98 @@ public partial class MainLayout : LayoutComponentBase
SettingsManager.ConfigDirectory = configDir;
SettingsManager.DataDirectory = dataDir;
+ // Ensure that all settings are loaded:
+ await this.SettingsManager.LoadSettings();
+
+ // Register this component with the message bus:
+ this.MessageBus.RegisterComponent(this);
+ this.MessageBus.ApplyFilters(this, [], [ Event.UPDATE_AVAILABLE, Event.USER_SEARCH_FOR_UPDATE ]);
+
+ // Set the js runtime for the update service:
+ UpdateService.SetJsRuntime(this.JsRuntime);
+
await base.OnInitializedAsync();
}
#endregion
+
+ #region Implementation of IMessageBusReceiver
+
+ public async Task ProcessMessage(ComponentBase? sendingComponent, Event triggeredEvent, T? data)
+ {
+ switch (triggeredEvent)
+ {
+ case Event.USER_SEARCH_FOR_UPDATE:
+ this.userDismissedUpdate = false;
+ break;
+
+ case Event.UPDATE_AVAILABLE:
+ if (data is UpdateResponse updateResponse)
+ {
+ this.currentUpdateResponse = updateResponse;
+ this.isUpdateAvailable = updateResponse.UpdateIsAvailable;
+ this.updateToVersion = updateResponse.NewVersion;
+
+ await this.InvokeAsync(this.StateHasChanged);
+ await this.SendMessage(Event.STATE_HAS_CHANGED);
+ }
+
+ break;
+ }
+ }
+
+ #endregion
+
+ private async Task DismissUpdate()
+ {
+ this.userDismissedUpdate = true;
+ this.AdditionalHeight = "0em";
+
+ await this.SendMessage(Event.STATE_HAS_CHANGED);
+ }
+
+ private bool IsUpdateAlertVisible
+ {
+ get
+ {
+ var state = this.isUpdateAvailable && !this.userDismissedUpdate;
+ this.AdditionalHeight = state ? "3em" : "0em";
+
+ return state;
+ }
+ }
+
+ private async Task ShowUpdateDialog()
+ {
+ if(this.currentUpdateResponse is null)
+ return;
+
+ //
+ // Replace the fir line with `# Changelog`:
+ //
+ var changelog = this.currentUpdateResponse.Value.Changelog;
+ if (!string.IsNullOrWhiteSpace(changelog))
+ {
+ var lines = changelog.Split('\n');
+ if (lines.Length > 0)
+ lines[0] = "# Changelog";
+
+ changelog = string.Join('\n', lines);
+ }
+
+ var updatedResponse = this.currentUpdateResponse.Value with { Changelog = changelog };
+ var dialogParameters = new DialogParameters
+ {
+ { x => x.UpdateResponse, updatedResponse }
+ };
+
+ var dialogReference = await this.DialogService.ShowAsync("Update", dialogParameters, DialogOptions.FULLSCREEN_NO_HEADER);
+ var dialogResult = await dialogReference.Result;
+ if (dialogResult.Canceled)
+ return;
+
+ this.performingUpdate = true;
+ this.StateHasChanged();
+ await this.Rust.InstallUpdate(this.JsRuntime);
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Components/Pages/About.razor b/app/MindWork AI Studio/Components/Pages/About.razor
index fbd31df5..a5cef1ae 100644
--- a/app/MindWork AI Studio/Components/Pages/About.razor
+++ b/app/MindWork AI Studio/Components/Pages/About.razor
@@ -18,6 +18,9 @@
+
+ Check for updates
+
diff --git a/app/MindWork AI Studio/Components/Pages/About.razor.cs b/app/MindWork AI Studio/Components/Pages/About.razor.cs
index d1bfc3c5..e9f74fea 100644
--- a/app/MindWork AI Studio/Components/Pages/About.razor.cs
+++ b/app/MindWork AI Studio/Components/Pages/About.razor.cs
@@ -1,11 +1,16 @@
using System.Reflection;
+using AIStudio.Tools;
+
using Microsoft.AspNetCore.Components;
namespace AIStudio.Components.Pages;
public partial class About : ComponentBase
{
+ [Inject]
+ private MessageBus MessageBus { get; init; } = null!;
+
private static readonly Assembly ASSEMBLY = Assembly.GetExecutingAssembly();
private static readonly MetaDataAttribute META_DATA = ASSEMBLY.GetCustomAttribute()!;
@@ -135,4 +140,9 @@ public partial class About : ComponentBase
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
""";
+
+ private async Task CheckForUpdate()
+ {
+ await this.MessageBus.SendMessage(this, Event.USER_SEARCH_FOR_UPDATE);
+ }
}
diff --git a/app/MindWork AI Studio/Components/Pages/Settings.razor b/app/MindWork AI Studio/Components/Pages/Settings.razor
index 667f4d49..fdc51e4b 100644
--- a/app/MindWork AI Studio/Components/Pages/Settings.razor
+++ b/app/MindWork AI Studio/Components/Pages/Settings.razor
@@ -52,5 +52,6 @@
+
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Program.cs b/app/MindWork AI Studio/Program.cs
index 0edb1520..4c1ad4a2 100644
--- a/app/MindWork AI Studio/Program.cs
+++ b/app/MindWork AI Studio/Program.cs
@@ -30,6 +30,7 @@ builder.Services.AddSingleton();
builder.Services.AddMudMarkdownClipboardService();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+builder.Services.AddHostedService();
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents()
.AddHubOptions(options =>
diff --git a/app/MindWork AI Studio/Settings/Data.cs b/app/MindWork AI Studio/Settings/Data.cs
index 1c629b80..8052c271 100644
--- a/app/MindWork AI Studio/Settings/Data.cs
+++ b/app/MindWork AI Studio/Settings/Data.cs
@@ -6,7 +6,7 @@ namespace AIStudio.Settings;
public sealed class Data
{
///
- /// The version of the settings file. Allows us to upgrade the settings,
+ /// The version of the settings file. Allows us to upgrade the settings
/// when a new version is available.
///
public Version Version { get; init; } = Version.V1;
@@ -36,4 +36,9 @@ public sealed class Data
/// Should we enable spellchecking for all input fields?
///
public bool EnableSpellchecking { get; set; }
+
+ ///
+ /// If and when we should look for updates.
+ ///
+ public UpdateBehavior UpdateBehavior { get; set; } = UpdateBehavior.ONCE_STARTUP;
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Settings/UpdateBehavior.cs b/app/MindWork AI Studio/Settings/UpdateBehavior.cs
new file mode 100644
index 00000000..30579f76
--- /dev/null
+++ b/app/MindWork AI Studio/Settings/UpdateBehavior.cs
@@ -0,0 +1,10 @@
+namespace AIStudio.Settings;
+
+public enum UpdateBehavior
+{
+ NO_CHECK,
+ ONCE_STARTUP,
+ HOURLY,
+ DAILY,
+ WEEKLY,
+}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/MessageBus.cs b/app/MindWork AI Studio/Tools/MessageBus.cs
index 6b584c42..53b215c7 100644
--- a/app/MindWork AI Studio/Tools/MessageBus.cs
+++ b/app/MindWork AI Studio/Tools/MessageBus.cs
@@ -1,6 +1,7 @@
using System.Collections.Concurrent;
using Microsoft.AspNetCore.Components;
+// ReSharper disable RedundantRecordClassKeyword
namespace AIStudio.Tools;
diff --git a/app/MindWork AI Studio/Tools/Rust.cs b/app/MindWork AI Studio/Tools/Rust.cs
index 9763b39d..af85765d 100644
--- a/app/MindWork AI Studio/Tools/Rust.cs
+++ b/app/MindWork AI Studio/Tools/Rust.cs
@@ -39,4 +39,16 @@ public sealed class Rust
};
});
}
+
+ public async Task CheckForUpdate(IJSRuntime jsRuntime)
+ {
+ var cts = new CancellationTokenSource(TimeSpan.FromSeconds(16));
+ return await jsRuntime.InvokeAsync("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");
+ }
}
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/SetClipboardResponse.cs b/app/MindWork AI Studio/Tools/SetClipboardResponse.cs
index 7a65caa9..e7a8fcc3 100644
--- a/app/MindWork AI Studio/Tools/SetClipboardResponse.cs
+++ b/app/MindWork AI Studio/Tools/SetClipboardResponse.cs
@@ -3,6 +3,6 @@ namespace AIStudio.Tools;
///
/// The response from the set clipboard operation.
///
-/// True when the operation was successful.
-/// The issue that occurred during the operation, empty when successful.
+/// True, when the operation was successful.
+/// The issues, which occurred during the operation, empty when successful.
public readonly record struct SetClipboardResponse(bool Success, string Issue);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/UpdateResponse.cs b/app/MindWork AI Studio/Tools/UpdateResponse.cs
new file mode 100644
index 00000000..5a5e3e2b
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/UpdateResponse.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tools;
+
+///
+/// The response of the update check.
+///
+/// True if an update is available.
+/// The new version, when available.
+/// The changelog of the new version, when available.
+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
+);
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/UpdateService.cs b/app/MindWork AI Studio/Tools/UpdateService.cs
new file mode 100644
index 00000000..4b7143df
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/UpdateService.cs
@@ -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(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;
+ }
+}
\ No newline at end of file
diff --git a/runtime/Cargo.lock b/runtime/Cargo.lock
index 3d46acbe..de5a363c 100644
--- a/runtime/Cargo.lock
+++ b/runtime/Cargo.lock
@@ -2319,6 +2319,7 @@ dependencies = [
"flexi_logger",
"keyring",
"log",
+ "once_cell",
"reqwest 0.12.4",
"serde",
"serde_json",
diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml
index e2bb8483..c5095fb6 100644
--- a/runtime/Cargo.toml
+++ b/runtime/Cargo.toml
@@ -18,6 +18,7 @@ arboard = "3.4.0"
tokio = "1.37.0"
flexi_logger = "0.28"
log = "0.4"
+once_cell = "1.19.0"
[target.'cfg(target_os = "linux")'.dependencies]
# See issue https://github.com/tauri-apps/tauri/issues/4470
diff --git a/runtime/src/main.rs b/runtime/src/main.rs
index 0c63bc7a..57d9a4e7 100644
--- a/runtime/src/main.rs
+++ b/runtime/src/main.rs
@@ -5,6 +5,7 @@ extern crate core;
use std::net::TcpListener;
use std::sync::{Arc, Mutex};
+use once_cell::sync::Lazy;
use arboard::Clipboard;
use keyring::Entry;
@@ -15,6 +16,11 @@ use tauri::utils::config::AppUrl;
use tokio::time;
use flexi_logger::{AdaptiveFormat, Logger};
use log::{debug, error, info, warn};
+use tauri::updater::UpdateResponse;
+
+static SERVER: Lazy>>> = Lazy::new(|| Arc::new(Mutex::new(None)));
+static MAIN_WINDOW: Lazy>> = Lazy::new(|| Mutex::new(None));
+static CHECK_UPDATE_RESPONSE: Lazy>>> = Lazy::new(|| Mutex::new(None));
fn main() {
@@ -30,7 +36,14 @@ fn main() {
let tauri_version = metadata_lines.next().unwrap();
let app_commit_hash = metadata_lines.next().unwrap();
- Logger::try_with_str("debug").expect("Cannot create logging")
+ // Set the log level according to the environment:
+ // In debug mode, the log level is set to debug, in release mode to info.
+ let log_level = match is_dev() {
+ true => "debug",
+ false => "info",
+ };
+
+ Logger::try_with_str(log_level).expect("Cannot create logging")
.log_to_stdout()
.adaptive_format_for_stdout(AdaptiveFormat::Detailed)
.start().expect("Cannot start logging");
@@ -69,8 +82,7 @@ fn main() {
info!("Try to start the .NET server on {app_url_log}...");
// Arc for the server process to stop it later:
- let server: Arc>> = Arc::new(Mutex::new(None));
- let server_spawn_clone = server.clone();
+ let server_spawn_clone = SERVER.clone();
// Channel to communicate with the server process:
let (sender, mut receiver) = tauri::async_runtime::channel(100);
@@ -114,9 +126,8 @@ fn main() {
warn!("Running in development mode, no .NET server will be started.");
}
- let main_window: Arc>> = Arc::new(Mutex::new(None));
- let main_window_spawn_clone = main_window.clone();
- let server_receive_clone = server.clone();
+ let main_window_spawn_clone = &MAIN_WINDOW;
+ let server_receive_clone = SERVER.clone();
// Create a thread to handle server events:
tauri::async_runtime::spawn(async move {
@@ -181,29 +192,93 @@ fn main() {
});
info!("Starting Tauri app...");
- tauri::Builder::default()
+ let app = tauri::Builder::default()
.setup(move |app| {
let window = app.get_window("main").expect("Failed to get main window.");
- *main_window.lock().unwrap() = Some(window);
+ *MAIN_WINDOW.lock().unwrap() = Some(window);
Ok(())
})
.plugin(tauri_plugin_window_state::Builder::default().build())
- .invoke_handler(tauri::generate_handler![store_secret, get_secret, delete_secret, set_clipboard])
- .run(tauri::generate_context!())
+ .invoke_handler(tauri::generate_handler![
+ store_secret, get_secret, delete_secret, set_clipboard,
+ check_for_update, install_update
+ ])
+ .build(tauri::generate_context!())
.expect("Error while running Tauri application");
+
+ app.run(|app_handle, event| match event {
+
+ tauri::RunEvent::WindowEvent { event, label, .. } => {
+ match event {
+ tauri::WindowEvent::CloseRequested { .. } => {
+ warn!("Window '{label}': close was requested.");
+ }
+
+ tauri::WindowEvent::Destroyed => {
+ warn!("Window '{label}': was destroyed.");
+ }
+
+ tauri::WindowEvent::FileDrop(files) => {
+ info!("Window '{label}': files were dropped: {files:?}");
+ }
+
+ _ => (),
+ }
+ }
+
+ tauri::RunEvent::Updater(updater_event) => {
+ match updater_event {
+
+ tauri::UpdaterEvent::UpdateAvailable { body, date, version } => {
+ let body_len = body.len();
+ info!("Updater: update available: body size={body_len} time={date:?} version={version}");
+ }
+
+ tauri::UpdaterEvent::Pending => {
+ info!("Updater: update is pending!");
+ }
+
+ tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => {
+ info!("Updater: downloaded {} of {:?}", chunk_length, content_length);
+ }
+
+ tauri::UpdaterEvent::Downloaded => {
+ info!("Updater: update has been downloaded!");
+ warn!("Try to stop the .NET server now...");
+ stop_server();
+ }
+
+ tauri::UpdaterEvent::Updated => {
+ info!("Updater: app has been updated");
+ warn!("Try to restart the app now...");
+ app_handle.restart();
+ }
+
+ tauri::UpdaterEvent::AlreadyUpToDate => {
+ info!("Updater: app is already up to date");
+ }
+
+ tauri::UpdaterEvent::Error(error) => {
+ warn!("Updater: failed to update: {error}");
+ }
+ }
+ }
+
+ tauri::RunEvent::ExitRequested { .. } => {
+ warn!("Run event: exit was requested.");
+ }
+
+ tauri::RunEvent::Ready => {
+ info!("Run event: Tauri app is ready.");
+ }
+
+ _ => {}
+ });
info!("Tauri app was stopped.");
if is_prod() {
info!("Try to stop the .NET server as well...");
- if let Some(server_process) = server.lock().unwrap().take() {
- let server_kill_result = server_process.kill();
- match server_kill_result {
- Ok(_) => info!("The .NET server process was stopped."),
- Err(e) => error!("Failed to stop the .NET server process: {e}."),
- }
- } else {
- warn!("The .NET server process was not started or already stopped.");
- }
+ stop_server();
}
}
@@ -230,6 +305,87 @@ fn get_available_port() -> Option {
.ok()
}
+fn stop_server() {
+ if let Some(server_process) = SERVER.lock().unwrap().take() {
+ let server_kill_result = server_process.kill();
+ match server_kill_result {
+ Ok(_) => info!("The .NET server process was stopped."),
+ Err(e) => error!("Failed to stop the .NET server process: {e}."),
+ }
+ } else {
+ warn!("The .NET server process was not started or already stopped.");
+ }
+}
+
+#[tauri::command]
+async fn check_for_update() -> CheckUpdateResponse {
+ let app_handle = MAIN_WINDOW.lock().unwrap().as_ref().unwrap().app_handle();
+ tauri::async_runtime::spawn(async move {
+ let response = app_handle.updater().check().await;
+ match response {
+ Ok(update_response) => match update_response.is_update_available() {
+ true => {
+ *CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
+ let new_version = update_response.latest_version();
+ info!("Updater: update to version '{new_version}' is available.");
+ let changelog = update_response.body();
+ CheckUpdateResponse {
+ update_is_available: true,
+ error: false,
+ new_version: new_version.to_string(),
+ changelog: match changelog {
+ Some(c) => c.to_string(),
+ None => String::from(""),
+ },
+ }
+ },
+
+ false => {
+ info!("Updater: no updates available.");
+ CheckUpdateResponse {
+ update_is_available: false,
+ error: false,
+ new_version: String::from(""),
+ changelog: String::from(""),
+ }
+ },
+ },
+
+ Err(e) => {
+ warn!("Failed to check updater: {e}.");
+ CheckUpdateResponse {
+ update_is_available: false,
+ error: true,
+ new_version: String::from(""),
+ changelog: String::from(""),
+ }
+ },
+ }
+ }).await.unwrap()
+}
+
+#[derive(Serialize)]
+struct CheckUpdateResponse {
+ update_is_available: bool,
+ error: bool,
+ new_version: String,
+ changelog: String,
+}
+
+#[tauri::command]
+async fn install_update() {
+ let cloned_response_option = CHECK_UPDATE_RESPONSE.lock().unwrap().clone();
+ match cloned_response_option {
+ Some(update_response) => {
+ update_response.download_and_install().await.unwrap();
+ },
+
+ None => {
+ error!("Update installer: no update available to install. Did you check for updates first?");
+ },
+ }
+}
+
#[tauri::command]
fn store_secret(destination: String, user_name: String, secret: String) -> StoreSecretResponse {
let service = format!("mindwork-ai-studio::{}", destination);
diff --git a/runtime/tauri.conf.json b/runtime/tauri.conf.json
index 1c7b2212..63788588 100644
--- a/runtime/tauri.conf.json
+++ b/runtime/tauri.conf.json
@@ -76,7 +76,7 @@
"endpoints": [
"https://github.com/MindWorkAI/AI-Studio/releases/latest/download/latest.json"
],
- "dialog": true,
+ "dialog": false,
"windows": {
"installMode": "passive"
},