Notify of plugin updates and add update UX (#339)

This commit is contained in:
Gregory Schier
2026-01-02 10:03:08 -08:00
committed by GitHub
parent e751167dfc
commit 0146ee586f
20 changed files with 375 additions and 103 deletions

View File

@@ -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)]

View File

@@ -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)
}

View File

@@ -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()

View File

@@ -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);
}
}

View 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", &notification) {
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
}
}