diff --git a/package-lock.json b/package-lock.json index e22e46c5..dc959ea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "plugins/template-function-uuid", "plugins/template-function-xml", "plugins/themes-yaak", + "src-tauri", "src-tauri/yaak-crypto", "src-tauri/yaak-git", "src-tauri/yaak-fonts", @@ -4251,6 +4252,10 @@ "resolved": "src-tauri/yaak-sync", "link": true }, + "node_modules/@yaakapp-internal/tauri": { + "resolved": "src-tauri", + "link": true + }, "node_modules/@yaakapp-internal/templates": { "resolved": "src-tauri/yaak-templates", "link": true @@ -18991,6 +18996,10 @@ "name": "@yaak/themes-yaak", "version": "0.1.0" }, + "src-tauri": { + "name": "@yaakapp-internal/tauri", + "version": "1.0.0" + }, "src-tauri/yaak-crypto": { "name": "@yaakapp-internal/crypto", "version": "1.0.0" diff --git a/package.json b/package.json index 8124d388..6d8e556c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "plugins/template-function-uuid", "plugins/template-function-xml", "plugins/themes-yaak", + "src-tauri", "src-tauri/yaak-crypto", "src-tauri/yaak-git", "src-tauri/yaak-fonts", @@ -53,7 +54,7 @@ "scripts": { "start": "npm run app-dev", "app-build": "tauri build", - "app-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json", + "app-dev": "tauri dev --no-watch --config ./src-tauri/tauri.development.conf.json", "migration": "node scripts/create-migration.cjs", "build": "npm run --workspaces --if-present build", "build-plugins": "npm run --workspaces --if-present build", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 95b619a9..0bdeb720 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -7868,6 +7868,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tokio-stream", + "ts-rs", "uuid", "yaak-common", "yaak-crypto", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index d806e0ab..f742d1b4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -43,6 +43,7 @@ tauri-build = { version = "2.4.1", features = [] } openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work [dependencies] +charset = "0.1.5" chrono = { workspace = true, features = ["serde"] } cookie = "0.18.1" eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" } @@ -57,19 +58,20 @@ serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true, features = ["raw_value"] } tauri = { workspace = true, features = ["devtools", "protocol-asset"] } tauri-plugin-clipboard-manager = "2.3.0" +tauri-plugin-deep-link = "2.4.3" tauri-plugin-dialog = { workspace = true } tauri-plugin-fs = "2.4.2" tauri-plugin-log = { version = "2.7.0", features = ["colored"] } tauri-plugin-opener = "2.5.0" tauri-plugin-os = "2.3.1" tauri-plugin-shell = { workspace = true } -tauri-plugin-deep-link = "2.4.3" tauri-plugin-single-instance = { version = "2.3.4", features = ["deep-link"] } tauri-plugin-updater = "2.9.0" tauri-plugin-window-state = "2.4.0" thiserror = { workspace = true } tokio = { workspace = true, features = ["sync"] } tokio-stream = "0.1.17" +ts-rs = { workspace = true } uuid = "1.12.1" yaak-common = { workspace = true } yaak-crypto = { workspace = true } @@ -85,7 +87,6 @@ yaak-sse = { workspace = true } yaak-sync = { workspace = true } yaak-templates = { workspace = true } yaak-ws = { path = "yaak-ws" } -charset = "0.1.5" [workspace.dependencies] chrono = "0.4.41" diff --git a/src-tauri/bindings/index.ts b/src-tauri/bindings/index.ts new file mode 100644 index 00000000..9e148369 --- /dev/null +++ b/src-tauri/bindings/index.ts @@ -0,0 +1,11 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type UpdateInfo = { replyEventId: string, version: string, downloaded: boolean, }; + +export type UpdateResponse = { "type": "ack" } | { "type": "action", action: UpdateResponseAction, }; + +export type UpdateResponseAction = "install" | "skip"; + +export type YaakNotification = { timestamp: string, timeout: number | null, id: string, title: string | null, message: string, color: string | null, action: YaakNotificationAction | null, }; + +export type YaakNotificationAction = { label: string, url: string, }; diff --git a/src-tauri/capabilities/capabilities.json b/src-tauri/capabilities/default.json similarity index 91% rename from src-tauri/capabilities/capabilities.json rename to src-tauri/capabilities/default.json index b77c0d6f..ae959bbf 100644 --- a/src-tauri/capabilities/capabilities.json +++ b/src-tauri/capabilities/default.json @@ -1,8 +1,6 @@ { - "$schema": "../gen/schemas/capabilities.json", - "identifier": "main", - "description": "Main permissions", - "local": true, + "identifier": "default", + "description": "Default capabilities for all build variants", "windows": [ "*" ], @@ -55,7 +53,6 @@ "yaak-crypto:default", "yaak-fonts:default", "yaak-git:default", - "yaak-license:default", "yaak-mac-window:default", "yaak-models:default", "yaak-plugins:default", diff --git a/src-tauri/package.json b/src-tauri/package.json new file mode 100644 index 00000000..bfdc28b6 --- /dev/null +++ b/src-tauri/package.json @@ -0,0 +1,6 @@ +{ + "name": "@yaakapp-internal/tauri", + "private": true, + "version": "1.0.0", + "main": "bindings/index.ts" +} diff --git a/src-tauri/src/error.rs b/src-tauri/src/error.rs index da590cb8..65365f90 100644 --- a/src-tauri/src/error.rs +++ b/src-tauri/src/error.rs @@ -19,9 +19,13 @@ pub enum Error { #[error(transparent)] GitError(#[from] yaak_git::error::Error), + #[error(transparent)] + TokioTimeoutElapsed(#[from] tokio::time::error::Elapsed), + #[error(transparent)] WebsocketError(#[from] yaak_ws::error::Error), + #[cfg(feature = "license")] #[error(transparent)] LicenseError(#[from] yaak_license::error::Error), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6f757f62..85db6a83 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -26,6 +26,7 @@ use tauri_plugin_log::{Builder, Target, TargetKind}; use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tokio::sync::Mutex; use tokio::task::block_in_place; +use tokio::time; use yaak_common::window::WorkspaceWindowTrait; use yaak_grpc::manager::{DynamicMessage, GrpcHandle}; use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message}; @@ -687,6 +688,12 @@ async fn cmd_grpc_go( Ok(conn.id) } +#[tauri::command] +async fn cmd_restart(app_handle: AppHandle) -> YaakResult<()> { + app_handle.request_restart(); + Ok(()) +} + #[tauri::command] async fn cmd_send_ephemeral_request( mut request: HttpRequest, @@ -1207,7 +1214,12 @@ async fn cmd_check_for_updates( yaak_updater: State<'_, Mutex>, ) -> YaakResult { let update_mode = get_update_mode(&window).await?; - Ok(yaak_updater.lock().await.check_now(&window, update_mode, UpdateTrigger::User).await?) + let settings = window.db().get_settings(); + Ok(yaak_updater + .lock() + .await + .check_now(&window, update_mode, settings.auto_download_updates, UpdateTrigger::User) + .await?) } #[cfg_attr(mobile, tauri::mobile_entry_point)] @@ -1349,6 +1361,7 @@ pub fn run() { cmd_plugin_info, cmd_reload_plugins, cmd_render_template, + cmd_restart, cmd_save_response, cmd_send_ephemeral_request, cmd_send_http_request, @@ -1393,10 +1406,16 @@ pub fn run() { let w = app_handle.get_webview_window(&label).unwrap(); let h = app_handle.clone(); tauri::async_runtime::spawn(async move { - if w.db().get_settings().autoupdate { + let settings = w.db().get_settings(); + if settings.autoupdate { + time::sleep(Duration::from_secs(3)).await; // Wait a bit so it's not so jarring let val: State<'_, Mutex> = h.state(); let update_mode = get_update_mode(&w).await.unwrap(); - if let Err(e) = val.lock().await.maybe_check(&w, update_mode).await + if let Err(e) = val + .lock() + .await + .maybe_check(&w, settings.auto_download_updates, update_mode) + .await { warn!("Failed to check for updates {e:?}"); } @@ -1472,7 +1491,7 @@ fn monitor_plugin_events(app_handle: &AppHandle) { } async fn call_frontend( - window: WebviewWindow, + window: &WebviewWindow, event: &InternalEvent, ) -> Option { window.emit_to(window.label(), "plugin_event", event.clone()).unwrap(); diff --git a/src-tauri/src/notifications.rs b/src-tauri/src/notifications.rs index 7c51bbba..4a52e4dc 100644 --- a/src-tauri/src/notifications.rs +++ b/src-tauri/src/notifications.rs @@ -7,9 +7,9 @@ use log::debug; use reqwest::Method; use serde::{Deserialize, Serialize}; use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow}; +use ts_rs::TS; use yaak_common::api_client::yaak_api_client; use yaak_common::platform::get_os; -use yaak_license::{LicenseCheckStatus, check_license}; use yaak_models::query_manager::QueryManagerExt; use yaak_models::util::UpdateSource; @@ -24,18 +24,22 @@ pub struct YaakNotifier { last_check: SystemTime, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[serde(default, rename_all = "camelCase")] +#[ts(export, export_to = "index.ts")] pub struct YaakNotification { timestamp: DateTime, timeout: Option, id: String, + title: Option, message: String, + color: Option, action: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[serde(default, rename_all = "camelCase")] +#[ts(export, export_to = "index.ts")] pub struct YaakNotificationAction { label: String, url: String, @@ -73,13 +77,20 @@ impl YaakNotifier { self.last_check = SystemTime::now(); - let license_check = match check_license(window).await { - Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(), - Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(), - Ok(LicenseCheckStatus::InvalidLicense) => "invalid_license".to_string(), - Ok(LicenseCheckStatus::Trialing { .. }) => "trialing".to_string(), - Err(_) => "unknown".to_string(), + #[cfg(feature = "license")] + let license_check = { + use yaak_license::{LicenseCheckStatus, check_license}; + match check_license(window).await { + Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(), + Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(), + Ok(LicenseCheckStatus::InvalidLicense) => "invalid_license".to_string(), + Ok(LicenseCheckStatus::Trialing { .. }) => "trialing".to_string(), + Err(_) => "unknown".to_string(), + } }; + #[cfg(not(feature = "license"))] + let license_check = "disabled".to_string(); + let settings = window.db().get_settings(); let num_launches = get_num_launches(app_handle).await; let info = app_handle.package_info().clone(); diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index 6646787f..126f19f7 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -51,7 +51,7 @@ pub(crate) async fn handle_plugin_event( InternalEventPayload::PromptTextRequest(_) => { let window = get_window_from_window_context(app_handle, &window_context) .expect("Failed to find window for render"); - call_frontend(window, event).await + call_frontend(&window, event).await } InternalEventPayload::FindHttpResponsesRequest(req) => { let http_responses = app_handle diff --git a/src-tauri/src/updates.rs b/src-tauri/src/updates.rs index 23f457bc..088edcf2 100644 --- a/src-tauri/src/updates.rs +++ b/src-tauri/src/updates.rs @@ -1,15 +1,21 @@ use std::fmt::{Display, Formatter}; -use std::time::SystemTime; +use std::path::PathBuf; +use std::time::{Duration, SystemTime}; use crate::error::Result; -use log::info; -use tauri::{Manager, Runtime, WebviewWindow}; +use log::{debug, error, info, warn}; +use serde::{Deserialize, Serialize}; +use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow}; use tauri_plugin_dialog::{DialogExt, MessageDialogButtons}; -use tauri_plugin_updater::UpdaterExt; +use tauri_plugin_updater::{Update, UpdaterExt}; use tokio::task::block_in_place; +use tokio::time::sleep; +use ts_rs::TS; use yaak_models::query_manager::QueryManagerExt; +use yaak_models::util::generate_id; use yaak_plugins::manager::PluginManager; +use crate::error::Error::GenericError; use crate::is_dev; const MAX_UPDATE_CHECK_HOURS_STABLE: u64 = 12; @@ -48,6 +54,7 @@ impl UpdateMode { } } +#[derive(PartialEq)] pub enum UpdateTrigger { Background, User, @@ -64,6 +71,7 @@ impl YaakUpdater { &mut self, window: &WebviewWindow, mode: UpdateMode, + auto_download: bool, update_trigger: UpdateTrigger, ) -> Result { // Only AppImage supports updates on Linux, so skip if it's not @@ -78,7 +86,7 @@ impl YaakUpdater { let update_key = format!("{:x}", md5::compute(settings.id)); self.last_update_check = SystemTime::now(); - info!("Checking for updates mode={}", mode); + info!("Checking for updates mode={} autodl={}", mode, auto_download); let w = window.clone(); let update_check_result = w @@ -113,42 +121,44 @@ impl YaakUpdater { None => false, Some(update) => { let w = window.clone(); - w.dialog() - .message(format!( - "{} is available. Would you like to download and install it now?", - update.version - )) - .buttons(MessageDialogButtons::OkCancelCustom( - "Download".to_string(), - "Later".to_string(), - )) - .title("Update Available") - .show(|confirmed| { - if !confirmed { - return; + tauri::async_runtime::spawn(async move { + // Force native updater if specified (useful if a release broke the UI) + let native_install_mode = + update.raw_json.get("install_mode").map(|v| v.as_str()).unwrap_or_default() + == Some("native"); + if native_install_mode { + start_native_update(&w, &update).await; + return; + } + + // If it's a background update, try downloading it first + if update_trigger == UpdateTrigger::Background && auto_download { + info!("Downloading update {} in background", update.version); + if let Err(e) = download_update_idempotent(&w, &update).await { + error!("Failed to download {}: {}", update.version, e); } - tauri::async_runtime::spawn(async move { - match update.download_and_install(|_, _| {}, || {}).await { - Ok(_) => { - if w.dialog() - .message("Would you like to restart the app?") - .title("Update Installed") - .buttons(MessageDialogButtons::OkCancelCustom( - "Restart".to_string(), - "Later".to_string(), - )) - .blocking_show() - { - w.app_handle().restart(); - } - } - Err(e) => { - w.dialog() - .message(format!("The update failed to install: {}", e)); - } - } - }); - }); + } + + match start_integrated_update(&w, &update).await { + Ok(UpdateResponseAction::Skip) => { + info!("Confirmed {}: skipped", update.version); + } + Ok(UpdateResponseAction::Install) => { + info!("Confirmed {}: install", update.version); + if let Err(e) = install_update_maybe_download(&w, &update).await { + error!("Failed to install: {e}"); + return; + }; + + info!("Installed {}", update.version); + finish_integrated_update(&w, &update).await; + } + Err(e) => { + warn!("Failed to notify frontend, falling back: {e}",); + start_native_update(&w, &update).await; + } + }; + }); true } }; @@ -158,6 +168,7 @@ impl YaakUpdater { pub async fn maybe_check( &mut self, window: &WebviewWindow, + auto_download: bool, mode: UpdateMode, ) -> Result { let update_period_seconds = match mode { @@ -171,11 +182,206 @@ impl YaakUpdater { return Ok(false); } - // Don't check if dev + // Don't check if development (can still with manual user trigger) if is_dev() { return Ok(false); } - self.check_now(window, mode, UpdateTrigger::Background).await + self.check_now(window, mode, auto_download, UpdateTrigger::Background).await } } + +#[derive(Debug, Clone, PartialEq, Serialize, Default, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export, export_to = "index.ts")] +struct UpdateInfo { + reply_event_id: String, + version: String, + downloaded: bool, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, TS)] +#[serde(rename_all = "camelCase", tag = "type")] +#[ts(export, export_to = "index.ts")] +enum UpdateResponse { + Ack, + Action { action: UpdateResponseAction }, +} + +#[derive(Debug, Clone, PartialEq, Deserialize, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export, export_to = "index.ts")] +enum UpdateResponseAction { + Install, + Skip, +} + +async fn finish_integrated_update(window: &WebviewWindow, update: &Update) { + if let Err(e) = window.emit_to(window.label(), "update_installed", update.version.to_string()) { + warn!("Failed to notify frontend of update install: {}", e); + } +} + +async fn start_integrated_update( + window: &WebviewWindow, + update: &Update, +) -> Result { + let download_path = ensure_download_path(window, update)?; + debug!("Download path: {}", download_path.display()); + let downloaded = download_path.exists(); + let ack_wait = Duration::from_secs(3); + let reply_id = generate_id(); + + // 1) Start listening BEFORE emitting to avoid missing a fast reply + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel::(); + let w_for_listener = window.clone(); + + let event_id = w_for_listener.listen(reply_id.clone(), move |ev| { + match serde_json::from_str::(ev.payload()) { + Ok(UpdateResponse::Ack) => { + let _ = tx.send(UpdateResponse::Ack); + } + Ok(UpdateResponse::Action { action }) => { + let _ = tx.send(UpdateResponse::Action { action }); + } + Err(e) => { + warn!("Failed to parse update reply from frontend: {e:?}"); + } + } + }); + + // Make sure we always unlisten + struct Unlisten<'a, R: Runtime> { + win: &'a WebviewWindow, + id: tauri::EventId, + } + impl<'a, R: Runtime> Drop for Unlisten<'a, R> { + fn drop(&mut self) { + self.win.unlisten(self.id); + } + } + let _guard = Unlisten { + win: window, + id: event_id, + }; + + // 2) Emit the event now that listener is in place + let info = UpdateInfo { + version: update.version.to_string(), + downloaded, + reply_event_id: reply_id, + }; + window + .emit_to(window.label(), "update_available", &info) + .map_err(|e| GenericError(format!("Failed to emit update_available: {e}")))?; + + // 3) Two-stage timeout: first wait for ack, then wait for final action + // --- Phase 1: wait for ACK with timeout --- + let ack_timer = sleep(ack_wait); + tokio::pin!(ack_timer); + + loop { + tokio::select! { + msg = rx.recv() => match msg { + Some(UpdateResponse::Ack) => break, // proceed to Phase 2 + Some(UpdateResponse::Action{action}) => return Ok(action), // user was fast + None => return Err(GenericError("frontend channel closed before ack".into())), + }, + _ = &mut ack_timer => { + return Err(GenericError("timed out waiting for frontend ack".into())); + } + } + } + + // --- Phase 2: wait forever for final action --- + loop { + match rx.recv().await { + Some(UpdateResponse::Action { action }) => return Ok(action), + Some(UpdateResponse::Ack) => { /* ignore extra acks */ } + None => return Err(GenericError("frontend channel closed before action".into())), + } + } +} + +async fn start_native_update(window: &WebviewWindow, update: &Update) { + // If the frontend doesn't respond, fallback to native dialogs + let confirmed = window + .dialog() + .message(format!( + "{} is available. Would you like to download and install it now?", + update.version + )) + .buttons(MessageDialogButtons::OkCancelCustom("Download".to_string(), "Later".to_string())) + .title("Update Available") + .blocking_show(); + if !confirmed { + return; + } + + match update.download_and_install(|_, _| {}, || {}).await { + Ok(()) => { + if window + .dialog() + .message("Would you like to restart the app?") + .title("Update Installed") + .buttons(MessageDialogButtons::OkCancelCustom( + "Restart".to_string(), + "Later".to_string(), + )) + .blocking_show() + { + window.app_handle().request_restart(); + } + } + Err(e) => { + window.dialog().message(format!("The update failed to install: {}", e)); + } + } +} + +pub async fn download_update_idempotent( + window: &WebviewWindow, + update: &Update, +) -> Result { + let dl_path = ensure_download_path(window, update)?; + + if dl_path.exists() { + info!("{} already downloaded to {}", update.version, dl_path.display()); + return Ok(dl_path); + } + + info!("{} downloading: {}", update.version, dl_path.display()); + let dl_bytes = update.download(|_, _| {}, || {}).await?; + std::fs::write(&dl_path, dl_bytes) + .map_err(|e| GenericError(format!("Failed to write update: {e}")))?; + + info!("{} downloaded", update.version); + + Ok(dl_path) +} + +pub async fn install_update_maybe_download( + window: &WebviewWindow, + update: &Update, +) -> Result<()> { + let dl_path = download_update_idempotent(window, update).await?; + let update_bytes = std::fs::read(&dl_path)?; + update.install(update_bytes.as_slice())?; + Ok(()) +} + +pub fn ensure_download_path( + window: &WebviewWindow, + update: &Update, +) -> Result { + // Ensure dir exists + let base_dir = window.path().app_cache_dir()?.join("updates"); + std::fs::create_dir_all(&base_dir)?; + + // Generate name based on signature + let sig_digest = md5::compute(&update.signature); + let name = format!("yaak-{}-{:x}", update.version, sig_digest); + let dl_path = base_dir.join(name); + + Ok(dl_path) +} diff --git a/src-tauri/tauri.commercial.conf.json b/src-tauri/tauri.commercial.conf.json new file mode 100644 index 00000000..5e0e8681 --- /dev/null +++ b/src-tauri/tauri.commercial.conf.json @@ -0,0 +1,35 @@ +{ + "build": { + "features": [ + "updater", + "license" + ] + }, + "app": { + "security": { + "capabilities": [ + "default", + { + "identifier": "release", + "windows": [ + "*" + ], + "permissions": [ + "yaak-license:default" + ] + } + ] + } + }, + "plugins": { + "updater": { + "endpoints": [ + "https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}" + ], + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEMxRDJFREQ1MjExQjdGN0IKUldSN2Z4c2gxZTNTd1FHNCtmYnFXMHVVQzhuNkJOM1cwOFBodmdLall3ckhKenpKUytHSTR1MlkK" + } + }, + "bundle": { + "createUpdaterArtifacts": "v1Compatible" + } +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3ea40eec..87b9f253 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -29,12 +29,6 @@ "yaak" ] } - }, - "updater": { - "endpoints": [ - "https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}" - ], - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEMxRDJFREQ1MjExQjdGN0IKUldSN2Z4c2gxZTNTd1FHNCtmYnFXMHVVQzhuNkJOM1cwOFBodmdLall3ckhKenpKUytHSTR1MlkK" } }, "bundle": { @@ -71,15 +65,11 @@ "nsis", "rpm" ], - "createUpdaterArtifacts": "v1Compatible", "macOS": { "minimumSystemVersion": "13.0", "exceptionDomain": "", "entitlements": "macos/entitlements.plist", "frameworks": [] - }, - "windows": { - "signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1" } } } diff --git a/src-tauri/tauri-dev.conf.json b/src-tauri/tauri.development.conf.json similarity index 100% rename from src-tauri/tauri-dev.conf.json rename to src-tauri/tauri.development.conf.json diff --git a/src-tauri/yaak-license/index.ts b/src-tauri/yaak-license/index.ts index 66e6b031..2dac20ec 100644 --- a/src-tauri/yaak-license/index.ts +++ b/src-tauri/yaak-license/index.ts @@ -1,11 +1,14 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { invoke } from '@tauri-apps/api/core'; import { listen } from '@tauri-apps/api/event'; +import { appInfo } from '@yaakapp/app/lib/appInfo'; import { useEffect } from 'react'; import { LicenseCheckStatus } from './bindings/license'; export * from './bindings/license'; +const CHECK_QUERY_KEY = ['license.check']; + export function useLicense() { const queryClient = useQueryClient(); const activate = useMutation({ @@ -30,12 +33,16 @@ export function useLicense() { }; }, []); - const CHECK_QUERY_KEY = ['license.check']; - const check = useQuery({ + const check = useQuery({ refetchInterval: 1000 * 60 * 60 * 12, // Refetch every 12 hours refetchOnWindowFocus: false, queryKey: CHECK_QUERY_KEY, - queryFn: () => invoke('plugin:yaak-license|check'), + queryFn: async () => { + if (!appInfo.featureLicense) { + return null; + } + return invoke('plugin:yaak-license|check'); + }, }); return { diff --git a/src-tauri/yaak-models/bindings/gen_models.ts b/src-tauri/yaak-models/bindings/gen_models.ts index f43a489c..1690ca65 100644 --- a/src-tauri/yaak-models/bindings/gen_models.ts +++ b/src-tauri/yaak-models/bindings/gen_models.ts @@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut export type ProxySettingAuth = { user: string, password: string, }; -export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, }; +export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, }; export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, }; diff --git a/src-tauri/yaak-models/bindings/gen_util.ts b/src-tauri/yaak-models/bindings/gen_util.ts index f87c220c..f1bf4fa2 100644 --- a/src-tauri/yaak-models/bindings/gen_util.ts +++ b/src-tauri/yaak-models/bindings/gen_util.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models.js"; +import type { Environment, Folder, GrpcRequest, HttpRequest, WebsocketRequest, Workspace } from "./gen_models"; export type BatchUpsertResult = { workspaces: Array, environments: Array, folders: Array, httpRequests: Array, grpcRequests: Array, websocketRequests: Array, }; diff --git a/src-tauri/yaak-models/migrations/20251001082054_auto-download.sql b/src-tauri/yaak-models/migrations/20251001082054_auto-download.sql new file mode 100644 index 00000000..94af4bc2 --- /dev/null +++ b/src-tauri/yaak-models/migrations/20251001082054_auto-download.sql @@ -0,0 +1,2 @@ +ALTER TABLE settings + ADD COLUMN auto_download_updates BOOLEAN DEFAULT TRUE; diff --git a/src-tauri/yaak-models/src/models.rs b/src-tauri/yaak-models/src/models.rs index 462415d1..1d07ec31 100644 --- a/src-tauri/yaak-models/src/models.rs +++ b/src-tauri/yaak-models/src/models.rs @@ -122,6 +122,7 @@ pub struct Settings { pub update_channel: String, pub hide_license_badge: bool, pub autoupdate: bool, + pub auto_download_updates: bool, } impl UpsertModelInfo for Settings { @@ -172,6 +173,7 @@ impl UpsertModelInfo for Settings { (UpdateChannel, self.update_channel.into()), (HideLicenseBadge, self.hide_license_badge.into()), (Autoupdate, self.autoupdate.into()), + (AutoDownloadUpdates, self.auto_download_updates.into()), (ColoredMethods, self.colored_methods.into()), (Proxy, proxy.into()), ]) @@ -196,6 +198,7 @@ impl UpsertModelInfo for Settings { SettingsIden::UpdateChannel, SettingsIden::HideLicenseBadge, SettingsIden::Autoupdate, + SettingsIden::AutoDownloadUpdates, SettingsIden::ColoredMethods, ] } @@ -226,6 +229,7 @@ impl UpsertModelInfo for Settings { hide_window_controls: row.get("hide_window_controls")?, update_channel: row.get("update_channel")?, autoupdate: row.get("autoupdate")?, + auto_download_updates: row.get("auto_download_updates")?, hide_license_badge: row.get("hide_license_badge")?, colored_methods: row.get("colored_methods")?, }) diff --git a/src-tauri/yaak-models/src/queries/settings.rs b/src-tauri/yaak-models/src/queries/settings.rs index e26fd090..472a6697 100644 --- a/src-tauri/yaak-models/src/queries/settings.rs +++ b/src-tauri/yaak-models/src/queries/settings.rs @@ -34,6 +34,7 @@ impl<'a> DbContext<'a> { autoupdate: true, colored_methods: false, hide_license_badge: false, + auto_download_updates: true, }; self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings") } diff --git a/src-tauri/yaak-plugins/src/lib.rs b/src-tauri/yaak-plugins/src/lib.rs index e19bf456..1deb0878 100644 --- a/src-tauri/yaak-plugins/src/lib.rs +++ b/src-tauri/yaak-plugins/src/lib.rs @@ -1,7 +1,7 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use crate::commands::{install, search, uninstall, updates}; use crate::manager::PluginManager; use log::info; -use std::process::exit; use tauri::plugin::{Builder, TauriPlugin}; use tauri::{generate_handler, Manager, RunEvent, Runtime, State}; @@ -20,6 +20,8 @@ pub mod api; pub mod install; pub mod plugin_meta; +static EXITING: AtomicBool = AtomicBool::new(false); + pub fn init() -> TauriPlugin { Builder::new("yaak-plugins") .invoke_handler(generate_handler![search, install, uninstall, updates]) @@ -31,12 +33,15 @@ pub fn init() -> TauriPlugin { .on_event(|app, e| match e { // TODO: Also exit when app is force-quit (eg. cmd+r in IntelliJ runner) RunEvent::ExitRequested { api, .. } => { + if EXITING.swap(true, Ordering::SeqCst) { + return; // Only exit once to prevent infinite recursion + } api.prevent_exit(); tauri::async_runtime::block_on(async move { info!("Exiting plugin runtime due to app exit"); let manager: State = app.state(); manager.terminate().await; - exit(0); + app.exit(0); }); } _ => {} diff --git a/src-web/components/LicenseBadge.tsx b/src-web/components/LicenseBadge.tsx index a8a2b838..001fd3f7 100644 --- a/src-web/components/LicenseBadge.tsx +++ b/src-web/components/LicenseBadge.tsx @@ -4,7 +4,6 @@ import { settingsAtom } from '@yaakapp-internal/models'; import { useAtomValue } from 'jotai'; import type { ReactNode } from 'react'; import { openSettings } from '../commands/openSettings'; -import { appInfo } from '../lib/appInfo'; import { CargoFeature } from './CargoFeature'; import { BadgeButton } from './core/BadgeButton'; import type { ButtonProps } from './core/Button'; @@ -31,10 +30,6 @@ function LicenseBadgeCmp() { const { check } = useLicense(); const settings = useAtomValue(settingsAtom); - if (appInfo.isDev) { - return null; - } - if (check.error) { // Failed to check for license. Probably a network or server error so just don't // show anything. diff --git a/src-web/components/Settings/SettingsGeneral.tsx b/src-web/components/Settings/SettingsGeneral.tsx index e88d4ab7..9b194f12 100644 --- a/src-web/components/Settings/SettingsGeneral.tsx +++ b/src-web/components/Settings/SettingsGeneral.tsx @@ -65,6 +65,17 @@ export function SettingsGeneral() { { label: 'Manual', value: 'manual' }, ]} /> + + patchModel(workspace, { autoDownloadUpdates }) + } + /> +