mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-10 19:16:55 +02:00
Notify of plugin updates and add update UX (#339)
This commit is contained in:
@@ -57,6 +57,7 @@ pub async fn check_plugin_updates<R: Runtime>(
|
||||
.db()
|
||||
.list_plugins()?
|
||||
.into_iter()
|
||||
.filter(|p| p.url.is_some()) // Only check plugins with URLs (from registry)
|
||||
.filter_map(|p| match get_plugin_meta(&Path::new(&p.directory)) {
|
||||
Ok(m) => Some(PluginNameVersion { name: m.name, version: m.version }),
|
||||
Err(e) => {
|
||||
@@ -123,8 +124,8 @@ pub struct PluginSearchResponse {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_api.ts")]
|
||||
pub struct PluginNameVersion {
|
||||
name: String,
|
||||
version: String,
|
||||
pub name: String,
|
||||
pub version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS, PartialEq)]
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use crate::api::{
|
||||
PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates, search_plugins,
|
||||
PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse, check_plugin_updates,
|
||||
search_plugins,
|
||||
};
|
||||
use crate::error::Result;
|
||||
use crate::install::{delete_and_uninstall, download_and_install};
|
||||
use tauri::{AppHandle, Runtime, WebviewWindow, command};
|
||||
use tauri::{AppHandle, Manager, Runtime, WebviewWindow, command};
|
||||
use yaak_models::models::Plugin;
|
||||
|
||||
#[command]
|
||||
@@ -36,3 +37,34 @@ pub(crate) async fn uninstall<R: Runtime>(
|
||||
pub(crate) async fn updates<R: Runtime>(app_handle: AppHandle<R>) -> Result<PluginUpdatesResponse> {
|
||||
check_plugin_updates(&app_handle).await
|
||||
}
|
||||
|
||||
#[command]
|
||||
pub(crate) async fn update_all<R: Runtime>(
|
||||
window: WebviewWindow<R>,
|
||||
) -> Result<Vec<PluginNameVersion>> {
|
||||
use log::info;
|
||||
|
||||
// Get list of available updates (already filtered to only registry plugins)
|
||||
let updates = check_plugin_updates(&window.app_handle()).await?;
|
||||
|
||||
if updates.plugins.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
|
||||
let mut updated = Vec::new();
|
||||
|
||||
for update in updates.plugins {
|
||||
info!("Updating plugin: {} to version {}", update.name, update.version);
|
||||
match download_and_install(&window, &update.name, Some(update.version.clone())).await {
|
||||
Ok(_) => {
|
||||
info!("Successfully updated plugin: {}", update.name);
|
||||
updated.push(update.clone());
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to update plugin {}: {:?}", update.name, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
use crate::commands::{install, search, uninstall, updates};
|
||||
use crate::commands::{install, search, uninstall, update_all, updates};
|
||||
use crate::manager::PluginManager;
|
||||
use log::info;
|
||||
use crate::plugin_updater::PluginUpdater;
|
||||
use log::{info, warn};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::Duration;
|
||||
use tauri::plugin::{Builder, TauriPlugin};
|
||||
use tauri::{Manager, RunEvent, Runtime, State, generate_handler};
|
||||
use tauri::{Manager, RunEvent, Runtime, State, WindowEvent, generate_handler};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub mod api;
|
||||
mod checksum;
|
||||
@@ -16,6 +19,7 @@ pub mod native_template_functions;
|
||||
mod nodejs;
|
||||
pub mod plugin_handle;
|
||||
pub mod plugin_meta;
|
||||
pub mod plugin_updater;
|
||||
mod server_ws;
|
||||
pub mod template_callback;
|
||||
mod util;
|
||||
@@ -24,10 +28,14 @@ static EXITING: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
Builder::new("yaak-plugins")
|
||||
.invoke_handler(generate_handler![search, install, uninstall, updates])
|
||||
.invoke_handler(generate_handler![search, install, uninstall, updates, update_all])
|
||||
.setup(|app_handle, _| {
|
||||
let manager = PluginManager::new(app_handle.clone());
|
||||
app_handle.manage(manager.clone());
|
||||
|
||||
let plugin_updater = PluginUpdater::new();
|
||||
app_handle.manage(Mutex::new(plugin_updater));
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.on_event(|app, e| match e {
|
||||
@@ -44,6 +52,18 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
app.exit(0);
|
||||
});
|
||||
}
|
||||
RunEvent::WindowEvent { event: WindowEvent::Focused(true), label, .. } => {
|
||||
// Check for plugin updates on window focus
|
||||
let w = app.get_webview_window(&label).unwrap();
|
||||
let h = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_secs(3)).await; // Wait a bit so it's not so jarring
|
||||
let val: State<'_, Mutex<PluginUpdater>> = h.state();
|
||||
if let Err(e) = val.lock().await.maybe_check(&w).await {
|
||||
warn!("Failed to check for plugin updates {e:?}");
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
})
|
||||
.build()
|
||||
|
||||
@@ -254,6 +254,10 @@ impl PluginManager {
|
||||
.await?;
|
||||
|
||||
if !matches!(event.payload, InternalEventPayload::BootResponse) {
|
||||
// Add it to the plugin handles anyway...
|
||||
let mut plugin_handles = self.plugin_handles.lock().await;
|
||||
plugin_handles.retain(|p| p.dir != plugin.directory);
|
||||
plugin_handles.push(plugin_handle.clone());
|
||||
return Err(UnknownEventErr);
|
||||
}
|
||||
}
|
||||
|
||||
101
src-tauri/yaak-plugins/src/plugin_updater.rs
Normal file
101
src-tauri/yaak-plugins/src/plugin_updater.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
use std::time::Instant;
|
||||
|
||||
use log::{error, info};
|
||||
use serde::Serialize;
|
||||
use tauri::{Emitter, Manager, Runtime, WebviewWindow};
|
||||
use ts_rs::TS;
|
||||
use yaak_models::query_manager::QueryManagerExt;
|
||||
|
||||
use crate::api::check_plugin_updates;
|
||||
use crate::error::Result;
|
||||
|
||||
const MAX_UPDATE_CHECK_HOURS: u64 = 12;
|
||||
|
||||
pub struct PluginUpdater {
|
||||
last_check: Option<Instant>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "index.ts")]
|
||||
pub struct PluginUpdateNotification {
|
||||
pub update_count: usize,
|
||||
pub plugins: Vec<PluginUpdateInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "index.ts")]
|
||||
pub struct PluginUpdateInfo {
|
||||
pub name: String,
|
||||
pub current_version: String,
|
||||
pub latest_version: String,
|
||||
}
|
||||
|
||||
impl PluginUpdater {
|
||||
pub fn new() -> Self {
|
||||
Self { last_check: None }
|
||||
}
|
||||
|
||||
pub async fn check_now<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<bool> {
|
||||
self.last_check = Some(Instant::now());
|
||||
|
||||
info!("Checking for plugin updates");
|
||||
|
||||
let updates = check_plugin_updates(&window.app_handle()).await?;
|
||||
|
||||
if updates.plugins.is_empty() {
|
||||
info!("No plugin updates available");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Get current plugin versions to build notification
|
||||
let plugins = window.app_handle().db().list_plugins()?;
|
||||
let mut update_infos = Vec::new();
|
||||
|
||||
for update in &updates.plugins {
|
||||
if let Some(plugin) = plugins.iter().find(|p| {
|
||||
if let Ok(meta) =
|
||||
crate::plugin_meta::get_plugin_meta(&std::path::Path::new(&p.directory))
|
||||
{
|
||||
meta.name == update.name
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}) {
|
||||
if let Ok(meta) =
|
||||
crate::plugin_meta::get_plugin_meta(&std::path::Path::new(&plugin.directory))
|
||||
{
|
||||
update_infos.push(PluginUpdateInfo {
|
||||
name: update.name.clone(),
|
||||
current_version: meta.version,
|
||||
latest_version: update.version.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let notification =
|
||||
PluginUpdateNotification { update_count: update_infos.len(), plugins: update_infos };
|
||||
|
||||
info!("Found {} plugin update(s)", notification.update_count);
|
||||
|
||||
if let Err(e) = window.emit_to(window.label(), "plugin_updates_available", ¬ification) {
|
||||
error!("Failed to emit plugin_updates_available event: {}", e);
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn maybe_check<R: Runtime>(&mut self, window: &WebviewWindow<R>) -> Result<bool> {
|
||||
let update_period_seconds = MAX_UPDATE_CHECK_HOURS * 60 * 60;
|
||||
|
||||
if let Some(i) = self.last_check
|
||||
&& i.elapsed().as_secs() < update_period_seconds
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
self.check_now(window).await
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user