mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-22 16:58:28 +02:00
Integrated update experience (#259)
This commit is contained in:
9
package-lock.json
generated
9
package-lock.json
generated
@@ -38,6 +38,7 @@
|
|||||||
"plugins/template-function-uuid",
|
"plugins/template-function-uuid",
|
||||||
"plugins/template-function-xml",
|
"plugins/template-function-xml",
|
||||||
"plugins/themes-yaak",
|
"plugins/themes-yaak",
|
||||||
|
"src-tauri",
|
||||||
"src-tauri/yaak-crypto",
|
"src-tauri/yaak-crypto",
|
||||||
"src-tauri/yaak-git",
|
"src-tauri/yaak-git",
|
||||||
"src-tauri/yaak-fonts",
|
"src-tauri/yaak-fonts",
|
||||||
@@ -4251,6 +4252,10 @@
|
|||||||
"resolved": "src-tauri/yaak-sync",
|
"resolved": "src-tauri/yaak-sync",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@yaakapp-internal/tauri": {
|
||||||
|
"resolved": "src-tauri",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@yaakapp-internal/templates": {
|
"node_modules/@yaakapp-internal/templates": {
|
||||||
"resolved": "src-tauri/yaak-templates",
|
"resolved": "src-tauri/yaak-templates",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -18991,6 +18996,10 @@
|
|||||||
"name": "@yaak/themes-yaak",
|
"name": "@yaak/themes-yaak",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
},
|
},
|
||||||
|
"src-tauri": {
|
||||||
|
"name": "@yaakapp-internal/tauri",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
"src-tauri/yaak-crypto": {
|
"src-tauri/yaak-crypto": {
|
||||||
"name": "@yaakapp-internal/crypto",
|
"name": "@yaakapp-internal/crypto",
|
||||||
"version": "1.0.0"
|
"version": "1.0.0"
|
||||||
|
|||||||
@@ -37,6 +37,7 @@
|
|||||||
"plugins/template-function-uuid",
|
"plugins/template-function-uuid",
|
||||||
"plugins/template-function-xml",
|
"plugins/template-function-xml",
|
||||||
"plugins/themes-yaak",
|
"plugins/themes-yaak",
|
||||||
|
"src-tauri",
|
||||||
"src-tauri/yaak-crypto",
|
"src-tauri/yaak-crypto",
|
||||||
"src-tauri/yaak-git",
|
"src-tauri/yaak-git",
|
||||||
"src-tauri/yaak-fonts",
|
"src-tauri/yaak-fonts",
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run app-dev",
|
"start": "npm run app-dev",
|
||||||
"app-build": "tauri build",
|
"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",
|
"migration": "node scripts/create-migration.cjs",
|
||||||
"build": "npm run --workspaces --if-present build",
|
"build": "npm run --workspaces --if-present build",
|
||||||
"build-plugins": "npm run --workspaces --if-present build",
|
"build-plugins": "npm run --workspaces --if-present build",
|
||||||
|
|||||||
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -7868,6 +7868,7 @@ dependencies = [
|
|||||||
"thiserror 2.0.12",
|
"thiserror 2.0.12",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"ts-rs",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaak-common",
|
"yaak-common",
|
||||||
"yaak-crypto",
|
"yaak-crypto",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ tauri-build = { version = "2.4.1", features = [] }
|
|||||||
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
|
openssl-sys = { version = "0.9.105", features = ["vendored"] } # For Ubuntu installation to work
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
charset = "0.1.5"
|
||||||
chrono = { workspace = true, features = ["serde"] }
|
chrono = { workspace = true, features = ["serde"] }
|
||||||
cookie = "0.18.1"
|
cookie = "0.18.1"
|
||||||
eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client", version = "0.14.0" }
|
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"] }
|
serde_json = { workspace = true, features = ["raw_value"] }
|
||||||
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
tauri = { workspace = true, features = ["devtools", "protocol-asset"] }
|
||||||
tauri-plugin-clipboard-manager = "2.3.0"
|
tauri-plugin-clipboard-manager = "2.3.0"
|
||||||
|
tauri-plugin-deep-link = "2.4.3"
|
||||||
tauri-plugin-dialog = { workspace = true }
|
tauri-plugin-dialog = { workspace = true }
|
||||||
tauri-plugin-fs = "2.4.2"
|
tauri-plugin-fs = "2.4.2"
|
||||||
tauri-plugin-log = { version = "2.7.0", features = ["colored"] }
|
tauri-plugin-log = { version = "2.7.0", features = ["colored"] }
|
||||||
tauri-plugin-opener = "2.5.0"
|
tauri-plugin-opener = "2.5.0"
|
||||||
tauri-plugin-os = "2.3.1"
|
tauri-plugin-os = "2.3.1"
|
||||||
tauri-plugin-shell = { workspace = true }
|
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-single-instance = { version = "2.3.4", features = ["deep-link"] }
|
||||||
tauri-plugin-updater = "2.9.0"
|
tauri-plugin-updater = "2.9.0"
|
||||||
tauri-plugin-window-state = "2.4.0"
|
tauri-plugin-window-state = "2.4.0"
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
tokio = { workspace = true, features = ["sync"] }
|
tokio = { workspace = true, features = ["sync"] }
|
||||||
tokio-stream = "0.1.17"
|
tokio-stream = "0.1.17"
|
||||||
|
ts-rs = { workspace = true }
|
||||||
uuid = "1.12.1"
|
uuid = "1.12.1"
|
||||||
yaak-common = { workspace = true }
|
yaak-common = { workspace = true }
|
||||||
yaak-crypto = { workspace = true }
|
yaak-crypto = { workspace = true }
|
||||||
@@ -85,7 +87,6 @@ yaak-sse = { workspace = true }
|
|||||||
yaak-sync = { workspace = true }
|
yaak-sync = { workspace = true }
|
||||||
yaak-templates = { workspace = true }
|
yaak-templates = { workspace = true }
|
||||||
yaak-ws = { path = "yaak-ws" }
|
yaak-ws = { path = "yaak-ws" }
|
||||||
charset = "0.1.5"
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
chrono = "0.4.41"
|
chrono = "0.4.41"
|
||||||
|
|||||||
11
src-tauri/bindings/index.ts
Normal file
11
src-tauri/bindings/index.ts
Normal file
@@ -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, };
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "../gen/schemas/capabilities.json",
|
"identifier": "default",
|
||||||
"identifier": "main",
|
"description": "Default capabilities for all build variants",
|
||||||
"description": "Main permissions",
|
|
||||||
"local": true,
|
|
||||||
"windows": [
|
"windows": [
|
||||||
"*"
|
"*"
|
||||||
],
|
],
|
||||||
@@ -55,7 +53,6 @@
|
|||||||
"yaak-crypto:default",
|
"yaak-crypto:default",
|
||||||
"yaak-fonts:default",
|
"yaak-fonts:default",
|
||||||
"yaak-git:default",
|
"yaak-git:default",
|
||||||
"yaak-license:default",
|
|
||||||
"yaak-mac-window:default",
|
"yaak-mac-window:default",
|
||||||
"yaak-models:default",
|
"yaak-models:default",
|
||||||
"yaak-plugins:default",
|
"yaak-plugins:default",
|
||||||
6
src-tauri/package.json
Normal file
6
src-tauri/package.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "@yaakapp-internal/tauri",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"main": "bindings/index.ts"
|
||||||
|
}
|
||||||
@@ -19,9 +19,13 @@ pub enum Error {
|
|||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
GitError(#[from] yaak_git::error::Error),
|
GitError(#[from] yaak_git::error::Error),
|
||||||
|
|
||||||
|
#[error(transparent)]
|
||||||
|
TokioTimeoutElapsed(#[from] tokio::time::error::Elapsed),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
WebsocketError(#[from] yaak_ws::error::Error),
|
WebsocketError(#[from] yaak_ws::error::Error),
|
||||||
|
|
||||||
|
#[cfg(feature = "license")]
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
LicenseError(#[from] yaak_license::error::Error),
|
LicenseError(#[from] yaak_license::error::Error),
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ use tauri_plugin_log::{Builder, Target, TargetKind};
|
|||||||
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::task::block_in_place;
|
use tokio::task::block_in_place;
|
||||||
|
use tokio::time;
|
||||||
use yaak_common::window::WorkspaceWindowTrait;
|
use yaak_common::window::WorkspaceWindowTrait;
|
||||||
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
use yaak_grpc::manager::{DynamicMessage, GrpcHandle};
|
||||||
use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message};
|
use yaak_grpc::{Code, ServiceDefinition, deserialize_message, serialize_message};
|
||||||
@@ -687,6 +688,12 @@ async fn cmd_grpc_go<R: Runtime>(
|
|||||||
Ok(conn.id)
|
Ok(conn.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_restart<R: Runtime>(app_handle: AppHandle<R>) -> YaakResult<()> {
|
||||||
|
app_handle.request_restart();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_send_ephemeral_request<R: Runtime>(
|
async fn cmd_send_ephemeral_request<R: Runtime>(
|
||||||
mut request: HttpRequest,
|
mut request: HttpRequest,
|
||||||
@@ -1207,7 +1214,12 @@ async fn cmd_check_for_updates<R: Runtime>(
|
|||||||
yaak_updater: State<'_, Mutex<YaakUpdater>>,
|
yaak_updater: State<'_, Mutex<YaakUpdater>>,
|
||||||
) -> YaakResult<bool> {
|
) -> YaakResult<bool> {
|
||||||
let update_mode = get_update_mode(&window).await?;
|
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)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -1349,6 +1361,7 @@ pub fn run() {
|
|||||||
cmd_plugin_info,
|
cmd_plugin_info,
|
||||||
cmd_reload_plugins,
|
cmd_reload_plugins,
|
||||||
cmd_render_template,
|
cmd_render_template,
|
||||||
|
cmd_restart,
|
||||||
cmd_save_response,
|
cmd_save_response,
|
||||||
cmd_send_ephemeral_request,
|
cmd_send_ephemeral_request,
|
||||||
cmd_send_http_request,
|
cmd_send_http_request,
|
||||||
@@ -1393,10 +1406,16 @@ pub fn run() {
|
|||||||
let w = app_handle.get_webview_window(&label).unwrap();
|
let w = app_handle.get_webview_window(&label).unwrap();
|
||||||
let h = app_handle.clone();
|
let h = app_handle.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
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<YaakUpdater>> = h.state();
|
let val: State<'_, Mutex<YaakUpdater>> = h.state();
|
||||||
let update_mode = get_update_mode(&w).await.unwrap();
|
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:?}");
|
warn!("Failed to check for updates {e:?}");
|
||||||
}
|
}
|
||||||
@@ -1472,7 +1491,7 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn call_frontend<R: Runtime>(
|
async fn call_frontend<R: Runtime>(
|
||||||
window: WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
event: &InternalEvent,
|
event: &InternalEvent,
|
||||||
) -> Option<InternalEventPayload> {
|
) -> Option<InternalEventPayload> {
|
||||||
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
|
window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ use log::debug;
|
|||||||
use reqwest::Method;
|
use reqwest::Method;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow};
|
||||||
|
use ts_rs::TS;
|
||||||
use yaak_common::api_client::yaak_api_client;
|
use yaak_common::api_client::yaak_api_client;
|
||||||
use yaak_common::platform::get_os;
|
use yaak_common::platform::get_os;
|
||||||
use yaak_license::{LicenseCheckStatus, check_license};
|
|
||||||
use yaak_models::query_manager::QueryManagerExt;
|
use yaak_models::query_manager::QueryManagerExt;
|
||||||
use yaak_models::util::UpdateSource;
|
use yaak_models::util::UpdateSource;
|
||||||
|
|
||||||
@@ -24,18 +24,22 @@ pub struct YaakNotifier {
|
|||||||
last_check: SystemTime,
|
last_check: SystemTime,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "index.ts")]
|
||||||
pub struct YaakNotification {
|
pub struct YaakNotification {
|
||||||
timestamp: DateTime<Utc>,
|
timestamp: DateTime<Utc>,
|
||||||
timeout: Option<f64>,
|
timeout: Option<f64>,
|
||||||
id: String,
|
id: String,
|
||||||
|
title: Option<String>,
|
||||||
message: String,
|
message: String,
|
||||||
|
color: Option<String>,
|
||||||
action: Option<YaakNotificationAction>,
|
action: Option<YaakNotificationAction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
|
#[ts(export, export_to = "index.ts")]
|
||||||
pub struct YaakNotificationAction {
|
pub struct YaakNotificationAction {
|
||||||
label: String,
|
label: String,
|
||||||
url: String,
|
url: String,
|
||||||
@@ -73,13 +77,20 @@ impl YaakNotifier {
|
|||||||
|
|
||||||
self.last_check = SystemTime::now();
|
self.last_check = SystemTime::now();
|
||||||
|
|
||||||
let license_check = match check_license(window).await {
|
#[cfg(feature = "license")]
|
||||||
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
|
let license_check = {
|
||||||
Ok(LicenseCheckStatus::CommercialUse) => "commercial".to_string(),
|
use yaak_license::{LicenseCheckStatus, check_license};
|
||||||
Ok(LicenseCheckStatus::InvalidLicense) => "invalid_license".to_string(),
|
match check_license(window).await {
|
||||||
Ok(LicenseCheckStatus::Trialing { .. }) => "trialing".to_string(),
|
Ok(LicenseCheckStatus::PersonalUse { .. }) => "personal".to_string(),
|
||||||
Err(_) => "unknown".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 settings = window.db().get_settings();
|
||||||
let num_launches = get_num_launches(app_handle).await;
|
let num_launches = get_num_launches(app_handle).await;
|
||||||
let info = app_handle.package_info().clone();
|
let info = app_handle.package_info().clone();
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ pub(crate) async fn handle_plugin_event<R: Runtime>(
|
|||||||
InternalEventPayload::PromptTextRequest(_) => {
|
InternalEventPayload::PromptTextRequest(_) => {
|
||||||
let window = get_window_from_window_context(app_handle, &window_context)
|
let window = get_window_from_window_context(app_handle, &window_context)
|
||||||
.expect("Failed to find window for render");
|
.expect("Failed to find window for render");
|
||||||
call_frontend(window, event).await
|
call_frontend(&window, event).await
|
||||||
}
|
}
|
||||||
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
InternalEventPayload::FindHttpResponsesRequest(req) => {
|
||||||
let http_responses = app_handle
|
let http_responses = app_handle
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
use std::fmt::{Display, Formatter};
|
use std::fmt::{Display, Formatter};
|
||||||
use std::time::SystemTime;
|
use std::path::PathBuf;
|
||||||
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use crate::error::Result;
|
use crate::error::Result;
|
||||||
use log::info;
|
use log::{debug, error, info, warn};
|
||||||
use tauri::{Manager, Runtime, WebviewWindow};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tauri::{Emitter, Listener, Manager, Runtime, WebviewWindow};
|
||||||
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons};
|
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::task::block_in_place;
|
||||||
|
use tokio::time::sleep;
|
||||||
|
use ts_rs::TS;
|
||||||
use yaak_models::query_manager::QueryManagerExt;
|
use yaak_models::query_manager::QueryManagerExt;
|
||||||
|
use yaak_models::util::generate_id;
|
||||||
use yaak_plugins::manager::PluginManager;
|
use yaak_plugins::manager::PluginManager;
|
||||||
|
|
||||||
|
use crate::error::Error::GenericError;
|
||||||
use crate::is_dev;
|
use crate::is_dev;
|
||||||
|
|
||||||
const MAX_UPDATE_CHECK_HOURS_STABLE: u64 = 12;
|
const MAX_UPDATE_CHECK_HOURS_STABLE: u64 = 12;
|
||||||
@@ -48,6 +54,7 @@ impl UpdateMode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
pub enum UpdateTrigger {
|
pub enum UpdateTrigger {
|
||||||
Background,
|
Background,
|
||||||
User,
|
User,
|
||||||
@@ -64,6 +71,7 @@ impl YaakUpdater {
|
|||||||
&mut self,
|
&mut self,
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
mode: UpdateMode,
|
mode: UpdateMode,
|
||||||
|
auto_download: bool,
|
||||||
update_trigger: UpdateTrigger,
|
update_trigger: UpdateTrigger,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
// Only AppImage supports updates on Linux, so skip if it's not
|
// 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));
|
let update_key = format!("{:x}", md5::compute(settings.id));
|
||||||
self.last_update_check = SystemTime::now();
|
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 w = window.clone();
|
||||||
let update_check_result = w
|
let update_check_result = w
|
||||||
@@ -113,42 +121,44 @@ impl YaakUpdater {
|
|||||||
None => false,
|
None => false,
|
||||||
Some(update) => {
|
Some(update) => {
|
||||||
let w = window.clone();
|
let w = window.clone();
|
||||||
w.dialog()
|
tauri::async_runtime::spawn(async move {
|
||||||
.message(format!(
|
// Force native updater if specified (useful if a release broke the UI)
|
||||||
"{} is available. Would you like to download and install it now?",
|
let native_install_mode =
|
||||||
update.version
|
update.raw_json.get("install_mode").map(|v| v.as_str()).unwrap_or_default()
|
||||||
))
|
== Some("native");
|
||||||
.buttons(MessageDialogButtons::OkCancelCustom(
|
if native_install_mode {
|
||||||
"Download".to_string(),
|
start_native_update(&w, &update).await;
|
||||||
"Later".to_string(),
|
return;
|
||||||
))
|
}
|
||||||
.title("Update Available")
|
|
||||||
.show(|confirmed| {
|
// If it's a background update, try downloading it first
|
||||||
if !confirmed {
|
if update_trigger == UpdateTrigger::Background && auto_download {
|
||||||
return;
|
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(_) => {
|
match start_integrated_update(&w, &update).await {
|
||||||
if w.dialog()
|
Ok(UpdateResponseAction::Skip) => {
|
||||||
.message("Would you like to restart the app?")
|
info!("Confirmed {}: skipped", update.version);
|
||||||
.title("Update Installed")
|
}
|
||||||
.buttons(MessageDialogButtons::OkCancelCustom(
|
Ok(UpdateResponseAction::Install) => {
|
||||||
"Restart".to_string(),
|
info!("Confirmed {}: install", update.version);
|
||||||
"Later".to_string(),
|
if let Err(e) = install_update_maybe_download(&w, &update).await {
|
||||||
))
|
error!("Failed to install: {e}");
|
||||||
.blocking_show()
|
return;
|
||||||
{
|
};
|
||||||
w.app_handle().restart();
|
|
||||||
}
|
info!("Installed {}", update.version);
|
||||||
}
|
finish_integrated_update(&w, &update).await;
|
||||||
Err(e) => {
|
}
|
||||||
w.dialog()
|
Err(e) => {
|
||||||
.message(format!("The update failed to install: {}", e));
|
warn!("Failed to notify frontend, falling back: {e}",);
|
||||||
}
|
start_native_update(&w, &update).await;
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
});
|
});
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -158,6 +168,7 @@ impl YaakUpdater {
|
|||||||
pub async fn maybe_check<R: Runtime>(
|
pub async fn maybe_check<R: Runtime>(
|
||||||
&mut self,
|
&mut self,
|
||||||
window: &WebviewWindow<R>,
|
window: &WebviewWindow<R>,
|
||||||
|
auto_download: bool,
|
||||||
mode: UpdateMode,
|
mode: UpdateMode,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let update_period_seconds = match mode {
|
let update_period_seconds = match mode {
|
||||||
@@ -171,11 +182,206 @@ impl YaakUpdater {
|
|||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't check if dev
|
// Don't check if development (can still with manual user trigger)
|
||||||
if is_dev() {
|
if is_dev() {
|
||||||
return Ok(false);
|
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<R: Runtime>(window: &WebviewWindow<R>, 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<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
update: &Update,
|
||||||
|
) -> Result<UpdateResponseAction> {
|
||||||
|
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::<UpdateResponse>();
|
||||||
|
let w_for_listener = window.clone();
|
||||||
|
|
||||||
|
let event_id = w_for_listener.listen(reply_id.clone(), move |ev| {
|
||||||
|
match serde_json::from_str::<UpdateResponse>(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<R>,
|
||||||
|
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<R: Runtime>(window: &WebviewWindow<R>, 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<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
update: &Update,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
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<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
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<R: Runtime>(
|
||||||
|
window: &WebviewWindow<R>,
|
||||||
|
update: &Update,
|
||||||
|
) -> Result<PathBuf> {
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|||||||
35
src-tauri/tauri.commercial.conf.json
Normal file
35
src-tauri/tauri.commercial.conf.json
Normal file
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,12 +29,6 @@
|
|||||||
"yaak"
|
"yaak"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"updater": {
|
|
||||||
"endpoints": [
|
|
||||||
"https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"
|
|
||||||
],
|
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEMxRDJFREQ1MjExQjdGN0IKUldSN2Z4c2gxZTNTd1FHNCtmYnFXMHVVQzhuNkJOM1cwOFBodmdLall3ckhKenpKUytHSTR1MlkK"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
@@ -71,15 +65,11 @@
|
|||||||
"nsis",
|
"nsis",
|
||||||
"rpm"
|
"rpm"
|
||||||
],
|
],
|
||||||
"createUpdaterArtifacts": "v1Compatible",
|
|
||||||
"macOS": {
|
"macOS": {
|
||||||
"minimumSystemVersion": "13.0",
|
"minimumSystemVersion": "13.0",
|
||||||
"exceptionDomain": "",
|
"exceptionDomain": "",
|
||||||
"entitlements": "macos/entitlements.plist",
|
"entitlements": "macos/entitlements.plist",
|
||||||
"frameworks": []
|
"frameworks": []
|
||||||
},
|
|
||||||
"windows": {
|
|
||||||
"signCommand": "trusted-signing-cli -e https://eus.codesigning.azure.net/ -a Yaak -c yaakapp %1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
|
import { appInfo } from '@yaakapp/app/lib/appInfo';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { LicenseCheckStatus } from './bindings/license';
|
import { LicenseCheckStatus } from './bindings/license';
|
||||||
|
|
||||||
export * from './bindings/license';
|
export * from './bindings/license';
|
||||||
|
|
||||||
|
const CHECK_QUERY_KEY = ['license.check'];
|
||||||
|
|
||||||
export function useLicense() {
|
export function useLicense() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const activate = useMutation<void, string, { licenseKey: string }>({
|
const activate = useMutation<void, string, { licenseKey: string }>({
|
||||||
@@ -30,12 +33,16 @@ export function useLicense() {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const CHECK_QUERY_KEY = ['license.check'];
|
const check = useQuery({
|
||||||
const check = useQuery<void, string, LicenseCheckStatus>({
|
|
||||||
refetchInterval: 1000 * 60 * 60 * 12, // Refetch every 12 hours
|
refetchInterval: 1000 * 60 * 60 * 12, // Refetch every 12 hours
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
queryKey: CHECK_QUERY_KEY,
|
queryKey: CHECK_QUERY_KEY,
|
||||||
queryFn: () => invoke('plugin:yaak-license|check'),
|
queryFn: async () => {
|
||||||
|
if (!appInfo.featureLicense) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return invoke<LicenseCheckStatus>('plugin:yaak-license|check');
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ export type ProxySetting = { "type": "enabled", http: string, https: string, aut
|
|||||||
|
|
||||||
export type ProxySettingAuth = { user: string, password: string, };
|
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, };
|
export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, };
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// 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<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
|
export type BatchUpsertResult = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, websocketRequests: Array<WebsocketRequest>, };
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE settings
|
||||||
|
ADD COLUMN auto_download_updates BOOLEAN DEFAULT TRUE;
|
||||||
@@ -122,6 +122,7 @@ pub struct Settings {
|
|||||||
pub update_channel: String,
|
pub update_channel: String,
|
||||||
pub hide_license_badge: bool,
|
pub hide_license_badge: bool,
|
||||||
pub autoupdate: bool,
|
pub autoupdate: bool,
|
||||||
|
pub auto_download_updates: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UpsertModelInfo for Settings {
|
impl UpsertModelInfo for Settings {
|
||||||
@@ -172,6 +173,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
(UpdateChannel, self.update_channel.into()),
|
(UpdateChannel, self.update_channel.into()),
|
||||||
(HideLicenseBadge, self.hide_license_badge.into()),
|
(HideLicenseBadge, self.hide_license_badge.into()),
|
||||||
(Autoupdate, self.autoupdate.into()),
|
(Autoupdate, self.autoupdate.into()),
|
||||||
|
(AutoDownloadUpdates, self.auto_download_updates.into()),
|
||||||
(ColoredMethods, self.colored_methods.into()),
|
(ColoredMethods, self.colored_methods.into()),
|
||||||
(Proxy, proxy.into()),
|
(Proxy, proxy.into()),
|
||||||
])
|
])
|
||||||
@@ -196,6 +198,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
SettingsIden::UpdateChannel,
|
SettingsIden::UpdateChannel,
|
||||||
SettingsIden::HideLicenseBadge,
|
SettingsIden::HideLicenseBadge,
|
||||||
SettingsIden::Autoupdate,
|
SettingsIden::Autoupdate,
|
||||||
|
SettingsIden::AutoDownloadUpdates,
|
||||||
SettingsIden::ColoredMethods,
|
SettingsIden::ColoredMethods,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -226,6 +229,7 @@ impl UpsertModelInfo for Settings {
|
|||||||
hide_window_controls: row.get("hide_window_controls")?,
|
hide_window_controls: row.get("hide_window_controls")?,
|
||||||
update_channel: row.get("update_channel")?,
|
update_channel: row.get("update_channel")?,
|
||||||
autoupdate: row.get("autoupdate")?,
|
autoupdate: row.get("autoupdate")?,
|
||||||
|
auto_download_updates: row.get("auto_download_updates")?,
|
||||||
hide_license_badge: row.get("hide_license_badge")?,
|
hide_license_badge: row.get("hide_license_badge")?,
|
||||||
colored_methods: row.get("colored_methods")?,
|
colored_methods: row.get("colored_methods")?,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ impl<'a> DbContext<'a> {
|
|||||||
autoupdate: true,
|
autoupdate: true,
|
||||||
colored_methods: false,
|
colored_methods: false,
|
||||||
hide_license_badge: false,
|
hide_license_badge: false,
|
||||||
|
auto_download_updates: true,
|
||||||
};
|
};
|
||||||
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")
|
self.upsert(&settings, &UpdateSource::Background).expect("Failed to upsert settings")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use crate::commands::{install, search, uninstall, updates};
|
use crate::commands::{install, search, uninstall, updates};
|
||||||
use crate::manager::PluginManager;
|
use crate::manager::PluginManager;
|
||||||
use log::info;
|
use log::info;
|
||||||
use std::process::exit;
|
|
||||||
use tauri::plugin::{Builder, TauriPlugin};
|
use tauri::plugin::{Builder, TauriPlugin};
|
||||||
use tauri::{generate_handler, Manager, RunEvent, Runtime, State};
|
use tauri::{generate_handler, Manager, RunEvent, Runtime, State};
|
||||||
|
|
||||||
@@ -20,6 +20,8 @@ pub mod api;
|
|||||||
pub mod install;
|
pub mod install;
|
||||||
pub mod plugin_meta;
|
pub mod plugin_meta;
|
||||||
|
|
||||||
|
static EXITING: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||||
Builder::new("yaak-plugins")
|
Builder::new("yaak-plugins")
|
||||||
.invoke_handler(generate_handler![search, install, uninstall, updates])
|
.invoke_handler(generate_handler![search, install, uninstall, updates])
|
||||||
@@ -31,12 +33,15 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
.on_event(|app, e| match e {
|
.on_event(|app, e| match e {
|
||||||
// TODO: Also exit when app is force-quit (eg. cmd+r in IntelliJ runner)
|
// TODO: Also exit when app is force-quit (eg. cmd+r in IntelliJ runner)
|
||||||
RunEvent::ExitRequested { api, .. } => {
|
RunEvent::ExitRequested { api, .. } => {
|
||||||
|
if EXITING.swap(true, Ordering::SeqCst) {
|
||||||
|
return; // Only exit once to prevent infinite recursion
|
||||||
|
}
|
||||||
api.prevent_exit();
|
api.prevent_exit();
|
||||||
tauri::async_runtime::block_on(async move {
|
tauri::async_runtime::block_on(async move {
|
||||||
info!("Exiting plugin runtime due to app exit");
|
info!("Exiting plugin runtime due to app exit");
|
||||||
let manager: State<PluginManager> = app.state();
|
let manager: State<PluginManager> = app.state();
|
||||||
manager.terminate().await;
|
manager.terminate().await;
|
||||||
exit(0);
|
app.exit(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { settingsAtom } from '@yaakapp-internal/models';
|
|||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { openSettings } from '../commands/openSettings';
|
import { openSettings } from '../commands/openSettings';
|
||||||
import { appInfo } from '../lib/appInfo';
|
|
||||||
import { CargoFeature } from './CargoFeature';
|
import { CargoFeature } from './CargoFeature';
|
||||||
import { BadgeButton } from './core/BadgeButton';
|
import { BadgeButton } from './core/BadgeButton';
|
||||||
import type { ButtonProps } from './core/Button';
|
import type { ButtonProps } from './core/Button';
|
||||||
@@ -31,10 +30,6 @@ function LicenseBadgeCmp() {
|
|||||||
const { check } = useLicense();
|
const { check } = useLicense();
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
|
|
||||||
if (appInfo.isDev) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (check.error) {
|
if (check.error) {
|
||||||
// Failed to check for license. Probably a network or server error so just don't
|
// Failed to check for license. Probably a network or server error so just don't
|
||||||
// show anything.
|
// show anything.
|
||||||
|
|||||||
@@ -65,6 +65,17 @@ export function SettingsGeneral() {
|
|||||||
{ label: 'Manual', value: 'manual' },
|
{ label: 'Manual', value: 'manual' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
<Checkbox
|
||||||
|
className="pl-2 mt-1 ml-[14rem]"
|
||||||
|
checked={settings.autoDownloadUpdates}
|
||||||
|
disabled={!settings.autoupdate}
|
||||||
|
help="Automatically download Yaak updates (!50MB) in the background, so they will be immediately ready to install."
|
||||||
|
title="Automatically download updates"
|
||||||
|
onChange={(autoDownloadUpdates) =>
|
||||||
|
patchModel(workspace, { autoDownloadUpdates })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Separator className="my-4" />
|
||||||
</CargoFeature>
|
</CargoFeature>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { type } from '@tauri-apps/plugin-os';
|
import { type } from '@tauri-apps/plugin-os';
|
||||||
import { useFonts } from '@yaakapp-internal/fonts';
|
import { useFonts } from '@yaakapp-internal/fonts';
|
||||||
import { useLicense } from '@yaakapp-internal/license';
|
import { useLicense } from '@yaakapp-internal/license';
|
||||||
import type { EditorKeymap } from '@yaakapp-internal/models';
|
import type { EditorKeymap, Settings } from '@yaakapp-internal/models';
|
||||||
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
import { patchModel, settingsAtom } from '@yaakapp-internal/models';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||||
import { clamp } from '../../lib/clamp';
|
import { clamp } from '../../lib/clamp';
|
||||||
import { showConfirm } from '../../lib/confirm';
|
import { showConfirm } from '../../lib/confirm';
|
||||||
|
import { CargoFeature } from '../CargoFeature';
|
||||||
import { Checkbox } from '../core/Checkbox';
|
import { Checkbox } from '../core/Checkbox';
|
||||||
import { Icon } from '../core/Icon';
|
import { Icon } from '../core/Icon';
|
||||||
import { Link } from '../core/Link';
|
import { Link } from '../core/Link';
|
||||||
@@ -31,7 +32,6 @@ export function SettingsInterface() {
|
|||||||
const workspace = useAtomValue(activeWorkspaceAtom);
|
const workspace = useAtomValue(activeWorkspaceAtom);
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const fonts = useFonts();
|
const fonts = useFonts();
|
||||||
const license = useLicense();
|
|
||||||
|
|
||||||
if (settings == null || workspace == null) {
|
if (settings == null || workspace == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -127,31 +127,9 @@ export function SettingsInterface() {
|
|||||||
title="Colorize Request Methods"
|
title="Colorize Request Methods"
|
||||||
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
|
onChange={(coloredMethods) => patchModel(settings, { coloredMethods })}
|
||||||
/>
|
/>
|
||||||
{license.check.data?.type === 'personal_use' && (
|
<CargoFeature feature="license">
|
||||||
<Checkbox
|
<LicenseSettings settings={settings} />
|
||||||
checked={!settings.licenseBadge}
|
</CargoFeature>
|
||||||
title="Hide personal use badge"
|
|
||||||
onChange={async (hide) => {
|
|
||||||
if (hide) {
|
|
||||||
const confirmed = await showConfirm({
|
|
||||||
id: 'hide-license-badge',
|
|
||||||
title: 'Hide License Badge',
|
|
||||||
confirmText: 'Hide Badge',
|
|
||||||
description: (
|
|
||||||
<>
|
|
||||||
Only proceed if you’re using Yaak for personal projects only. If you’re using it
|
|
||||||
at work, please <Link href="https://yaak.app/">Purchase a License</Link>.
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
requireTyping: 'Personal Use',
|
|
||||||
color: 'notice',
|
|
||||||
});
|
|
||||||
if (!confirmed) return;
|
|
||||||
}
|
|
||||||
await patchModel(settings, { licenseBadge: !hide });
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{type() !== 'macos' && (
|
{type() !== 'macos' && (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
@@ -164,3 +142,44 @@ export function SettingsInterface() {
|
|||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
function LicenseSettings({ settings }: { settings: Settings }) {
|
||||||
|
const license = useLicense();
|
||||||
|
if (license.check.data?.type !== 'personal_use') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={settings.hideLicenseBadge}
|
||||||
|
title="Hide personal use badge"
|
||||||
|
onChange={async (hideLicenseBadge) => {
|
||||||
|
if (hideLicenseBadge) {
|
||||||
|
const confirmed = await showConfirm({
|
||||||
|
id: 'hide-license-badge',
|
||||||
|
title: 'Confirm Personal Use',
|
||||||
|
confirmText: 'Confirm',
|
||||||
|
description: (
|
||||||
|
<VStack space={3}>
|
||||||
|
<p>Hey there 👋🏼</p>
|
||||||
|
<p>
|
||||||
|
Yaak is free for personal projects and learning.{' '}
|
||||||
|
<strong>If you’re using Yaak at work, a license is required.</strong>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Licenses help keep Yaak independent and sustainable.{' '}
|
||||||
|
<Link href="https://yaak.app/pricing?s=badge">Purchase a License →</Link>
|
||||||
|
</p>
|
||||||
|
</VStack>
|
||||||
|
),
|
||||||
|
requireTyping: 'Personal Use',
|
||||||
|
color: 'info',
|
||||||
|
});
|
||||||
|
if (!confirmed) {
|
||||||
|
return; // Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await patchModel(settings, { hideLicenseBadge });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ function SettingsLicenseCmp() {
|
|||||||
<p>
|
<p>
|
||||||
<Link
|
<Link
|
||||||
noUnderline
|
noUnderline
|
||||||
href="https://yaak.app/pricing"
|
href="https://yaak.app/pricing?s=learn"
|
||||||
className="text-sm text-notice opacity-80 hover:opacity-100"
|
className="text-sm text-notice opacity-80 hover:opacity-100"
|
||||||
>
|
>
|
||||||
Learn More
|
Learn More
|
||||||
@@ -90,7 +90,7 @@ function SettingsLicenseCmp() {
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => openUrl('https://yaak.app/dashboard')}
|
onClick={() => openUrl('https://yaak.app/dashboard?s=support')}
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
>
|
>
|
||||||
Direct Support
|
Direct Support
|
||||||
@@ -104,7 +104,7 @@ function SettingsLicenseCmp() {
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => openUrl('https://yaak.app/pricing?ref=app.yaak.desktop')}
|
onClick={() => openUrl('https://yaak.app/pricing?s=purchase&ref=app.yaak.desktop')}
|
||||||
rightSlot={<Icon icon="external_link" />}
|
rightSlot={<Icon icon="external_link" />}
|
||||||
>
|
>
|
||||||
Purchase
|
Purchase
|
||||||
|
|||||||
25
src-web/components/core/ButtonInfiniteLoading.tsx
Normal file
25
src-web/components/core/ButtonInfiniteLoading.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import type { ButtonProps } from './Button';
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
|
export function ButtonInfiniteLoading({
|
||||||
|
onClick,
|
||||||
|
isLoading,
|
||||||
|
loadingChildren,
|
||||||
|
children,
|
||||||
|
...props
|
||||||
|
}: ButtonProps & { loadingChildren?: string }) {
|
||||||
|
const [localIsLoading, setLocalIsLoading] = useState<boolean>(false);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
isLoading={localIsLoading || isLoading}
|
||||||
|
onClick={(e) => {
|
||||||
|
setLocalIsLoading(true);
|
||||||
|
onClick?.(e);
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{localIsLoading ? (loadingChildren ?? children) : children}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -40,7 +40,7 @@ export function Checkbox({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'appearance-none w-4 h-4 flex-shrink-0 border border-border',
|
'appearance-none w-4 h-4 flex-shrink-0 border border-border',
|
||||||
'rounded outline-none ring-0',
|
'rounded outline-none ring-0',
|
||||||
!disabled && 'hocus:border-border-focus hocus:bg-focus/[5%] ',
|
!disabled && 'hocus:border-border-focus hocus:bg-focus/[5%]',
|
||||||
disabled && 'border-dotted',
|
disabled && 'border-dotted',
|
||||||
)}
|
)}
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -58,7 +58,7 @@ export function Checkbox({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!hideLabel && (
|
{!hideLabel && (
|
||||||
<div className={classNames(fullWidth && 'w-full', disabled && 'opacity-disabled')}>
|
<div className={classNames('text-sm', fullWidth && 'w-full', disabled && 'opacity-disabled')}>
|
||||||
{title}
|
{title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface ToastProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
timeout: number | null;
|
timeout: number | null;
|
||||||
action?: (args: { hide: () => void }) => ReactNode;
|
action?: (args: { hide: () => void }) => ReactNode;
|
||||||
icon?: ShowToastRequest['icon'];
|
icon?: ShowToastRequest['icon'] | null;
|
||||||
color?: ShowToastRequest['color'];
|
color?: ShowToastRequest['color'];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
|||||||
[open],
|
[open],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toastIcon = icon ?? (color && color in ICONS && ICONS[color]);
|
const toastIcon = icon === null ? null : icon ?? (color && color in ICONS && ICONS[color]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<m.div
|
<m.div
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import deepEqual from '@gilbarbara/deep-equal';
|
import deepEqual from '@gilbarbara/deep-equal';
|
||||||
|
import type { UpdateInfo } from '@yaakapp-internal/tauri';
|
||||||
import type { Atom } from 'jotai';
|
import type { Atom } from 'jotai';
|
||||||
|
import { atom } from 'jotai';
|
||||||
import { selectAtom } from 'jotai/utils';
|
import { selectAtom } from 'jotai/utils';
|
||||||
import type { SplitLayoutLayout } from '../components/core/SplitLayout';
|
import type { SplitLayoutLayout } from '../components/core/SplitLayout';
|
||||||
import { atomWithKVStorage } from './atoms/atomWithKVStorage';
|
import { atomWithKVStorage } from './atoms/atomWithKVStorage';
|
||||||
@@ -16,3 +18,5 @@ export const workspaceLayoutAtom = atomWithKVStorage<SplitLayoutLayout>(
|
|||||||
'workspace_layout',
|
'workspace_layout',
|
||||||
'horizontal',
|
'horizontal',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const updateAvailableAtom = atom<Omit<UpdateInfo, 'replyEventId'> | null>(null);
|
||||||
|
|||||||
20
src-web/lib/color.ts
Normal file
20
src-web/lib/color.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Color } from '@yaakapp-internal/plugins';
|
||||||
|
|
||||||
|
const colors: Record<Color, boolean> = {
|
||||||
|
primary: true,
|
||||||
|
secondary: true,
|
||||||
|
success: true,
|
||||||
|
notice: true,
|
||||||
|
warning: true,
|
||||||
|
danger: true,
|
||||||
|
info: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function stringToColor(str: string | null): Color | null {
|
||||||
|
if (!str) return null;
|
||||||
|
const strLower = str.toLowerCase();
|
||||||
|
if (strLower in colors) {
|
||||||
|
return strLower as Color;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -1,12 +1,19 @@
|
|||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import { openUrl } from '@tauri-apps/plugin-opener';
|
import { openUrl } from '@tauri-apps/plugin-opener';
|
||||||
import type { InternalEvent, ShowToastRequest } from '@yaakapp-internal/plugins';
|
import type { InternalEvent, ShowToastRequest } from '@yaakapp-internal/plugins';
|
||||||
|
import type { UpdateInfo, UpdateResponse, YaakNotification } from '@yaakapp-internal/tauri';
|
||||||
import { openSettings } from '../commands/openSettings';
|
import { openSettings } from '../commands/openSettings';
|
||||||
import { Button } from '../components/core/Button';
|
import { Button } from '../components/core/Button';
|
||||||
|
import { ButtonInfiniteLoading } from '../components/core/ButtonInfiniteLoading';
|
||||||
|
import { Icon } from '../components/core/Icon';
|
||||||
|
import { HStack, VStack } from '../components/core/Stacks';
|
||||||
|
|
||||||
// Listen for toasts
|
// Listen for toasts
|
||||||
import { listenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
import { listenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||||
|
import { updateAvailableAtom } from './atoms';
|
||||||
|
import { stringToColor } from './color';
|
||||||
import { generateId } from './generateId';
|
import { generateId } from './generateId';
|
||||||
|
import { jotaiStore } from './jotai';
|
||||||
import { showPrompt } from './prompt';
|
import { showPrompt } from './prompt';
|
||||||
import { invokeCmd } from './tauri';
|
import { invokeCmd } from './tauri';
|
||||||
import { showToast } from './toast';
|
import { showToast } from './toast';
|
||||||
@@ -37,40 +44,125 @@ export function initGlobalListeners() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
listenToTauriEvent<{
|
const UPDATE_TOAST_ID = 'update-info'; // Share the ID to replace the toast
|
||||||
id: string;
|
|
||||||
timestamp: string;
|
listenToTauriEvent<string>('update_installed', async ({ payload: version }) => {
|
||||||
message: string;
|
|
||||||
timeout?: number | null;
|
|
||||||
action?: null | {
|
|
||||||
url: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
}>('notification', ({ payload }) => {
|
|
||||||
console.log('Got notification event', payload);
|
|
||||||
const actionUrl = payload.action?.url;
|
|
||||||
const actionLabel = payload.action?.label;
|
|
||||||
showToast({
|
showToast({
|
||||||
id: payload.id,
|
id: UPDATE_TOAST_ID,
|
||||||
timeout: payload.timeout ?? undefined,
|
color: 'primary',
|
||||||
message: payload.message,
|
timeout: null,
|
||||||
onClose: () => {
|
message: (
|
||||||
invokeCmd('cmd_dismiss_notification', { notificationId: payload.id }).catch(console.error);
|
<VStack>
|
||||||
},
|
<h2 className="font-semibold">Yaak {version} was installed</h2>
|
||||||
action: ({ hide }) =>
|
<p className="text-text-subtle text-sm">Start using the new version now?</p>
|
||||||
actionLabel && actionUrl ? (
|
</VStack>
|
||||||
<Button
|
),
|
||||||
size="xs"
|
action: ({ hide }) => (
|
||||||
color="secondary"
|
<ButtonInfiniteLoading
|
||||||
className="mr-auto min-w-[5rem]"
|
size="xs"
|
||||||
onClick={() => {
|
className="mr-auto min-w-[5rem]"
|
||||||
hide();
|
color="primary"
|
||||||
return openUrl(actionUrl);
|
loadingChildren="Restarting..."
|
||||||
}}
|
onClick={() => {
|
||||||
>
|
hide();
|
||||||
{actionLabel}
|
setTimeout(() => invokeCmd('cmd_restart', {}), 200);
|
||||||
</Button>
|
}}
|
||||||
) : null,
|
>
|
||||||
|
Relaunch Yaak
|
||||||
|
</ButtonInfiniteLoading>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Listen for update events
|
||||||
|
listenToTauriEvent<UpdateInfo>(
|
||||||
|
'update_available',
|
||||||
|
async ({ payload: { version, replyEventId, downloaded } }) => {
|
||||||
|
console.log('Received update available event', { replyEventId, version, downloaded });
|
||||||
|
jotaiStore.set(updateAvailableAtom, { version, downloaded });
|
||||||
|
|
||||||
|
// Acknowledge the event, so we don't time out and try the fallback update logic
|
||||||
|
await emit<UpdateResponse>(replyEventId, { type: 'ack' });
|
||||||
|
|
||||||
|
showToast({
|
||||||
|
id: UPDATE_TOAST_ID,
|
||||||
|
color: 'info',
|
||||||
|
timeout: null,
|
||||||
|
message: (
|
||||||
|
<VStack>
|
||||||
|
<h2 className="font-semibold">Yaak {version} is available</h2>
|
||||||
|
<p className="text-text-subtle text-sm">
|
||||||
|
{downloaded ? 'Do you want to install' : 'Download and install'} the update?
|
||||||
|
</p>
|
||||||
|
</VStack>
|
||||||
|
),
|
||||||
|
action: () => (
|
||||||
|
<HStack space={1.5}>
|
||||||
|
<ButtonInfiniteLoading
|
||||||
|
size="xs"
|
||||||
|
color="info"
|
||||||
|
className="min-w-[10rem]"
|
||||||
|
loadingChildren={downloaded ? 'Installing...' : 'Downloading...'}
|
||||||
|
onClick={async () => {
|
||||||
|
await emit<UpdateResponse>(replyEventId, { type: 'action', action: 'install' });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{downloaded ? 'Install Now' : 'Download and Install'}
|
||||||
|
</ButtonInfiniteLoading>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color="info"
|
||||||
|
variant="border"
|
||||||
|
rightSlot={<Icon icon="external_link" />}
|
||||||
|
onClick={async () => {
|
||||||
|
await openUrl('https://yaak.app/changelog/' + version);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
What's New
|
||||||
|
</Button>
|
||||||
|
</HStack>
|
||||||
|
),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
listenToTauriEvent<YaakNotification>('notification', ({ payload }) => {
|
||||||
|
console.log('Got notification event', payload);
|
||||||
|
showNotificationToast(payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotificationToast(n: YaakNotification) {
|
||||||
|
const actionUrl = n.action?.url;
|
||||||
|
const actionLabel = n.action?.label;
|
||||||
|
showToast({
|
||||||
|
id: n.id,
|
||||||
|
timeout: n.timeout ?? null,
|
||||||
|
color: stringToColor(n.color) ?? undefined,
|
||||||
|
message: (
|
||||||
|
<VStack>
|
||||||
|
{n.title && <h2 className="font-semibold">{n.title}</h2>}
|
||||||
|
<p className="text-text-subtle text-sm">{n.message}</p>
|
||||||
|
</VStack>
|
||||||
|
),
|
||||||
|
onClose: () => {
|
||||||
|
invokeCmd('cmd_dismiss_notification', { notificationId: n.id }).catch(console.error);
|
||||||
|
},
|
||||||
|
action: ({ hide }) => {
|
||||||
|
return actionLabel && actionUrl ? (
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
color={stringToColor(n.color) ?? undefined}
|
||||||
|
className="mr-auto min-w-[5rem]"
|
||||||
|
rightSlot={<Icon icon="external_link" />}
|
||||||
|
onClick={() => {
|
||||||
|
hide();
|
||||||
|
return openUrl(actionUrl);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actionLabel}
|
||||||
|
</Button>
|
||||||
|
) : null;
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ type TauriCmd =
|
|||||||
| 'cmd_import_data'
|
| 'cmd_import_data'
|
||||||
| 'cmd_install_plugin'
|
| 'cmd_install_plugin'
|
||||||
| 'cmd_metadata'
|
| 'cmd_metadata'
|
||||||
|
| 'cmd_restart'
|
||||||
| 'cmd_new_child_window'
|
| 'cmd_new_child_window'
|
||||||
| 'cmd_new_main_window'
|
| 'cmd_new_main_window'
|
||||||
| 'cmd_plugin_info'
|
| 'cmd_plugin_info'
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ function toastColorVariables(color: YaakColor | null): Partial<CSSVariables> {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
text: color.lift(0.8).css(),
|
text: color.lift(0.8).css(),
|
||||||
textSubtle: color.css(),
|
textSubtle: color.lift(0.8).translucify(0.3).css(),
|
||||||
surface: color.translucify(0.9).css(),
|
surface: color.translucify(0.9).css(),
|
||||||
surfaceHighlight: color.translucify(0.8).css(),
|
surfaceHighlight: color.translucify(0.8).css(),
|
||||||
border: color.lift(0.3).translucify(0.6).css(),
|
border: color.lift(0.3).translucify(0.6).css(),
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ export function showToast({
|
|||||||
|
|
||||||
let delay = 0;
|
let delay = 0;
|
||||||
if (toastWithSameId) {
|
if (toastWithSameId) {
|
||||||
console.log('HIDING TOAST', id);
|
|
||||||
hideToast(toastWithSameId);
|
hideToast(toastWithSameId);
|
||||||
// Allow enough time for old toast to animate out
|
// Allow enough time for old toast to animate out
|
||||||
delay = 200;
|
delay = 200;
|
||||||
|
|||||||
Reference in New Issue
Block a user