diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index c4016aca..6c0e6b43 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,6 @@ #![cfg_attr( -all(not(debug_assertions), target_os = "windows"), -windows_subsystem = "windows" + all(not(debug_assertions), target_os = "windows"), + windows_subsystem = "windows" )] #[cfg(target_os = "macos")] @@ -32,16 +32,16 @@ use window_ext::TrafficLightWindowExt; use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event}; use crate::plugin::{ImportResources, ImportResult}; use crate::send::actually_send_request; -use crate::updates::YaakUpdater; +use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater}; mod analytics; mod models; mod plugin; mod render; +mod send; +mod updates; mod window_ext; mod window_menu; -mod updates; -mod send; #[derive(serde::Serialize)] pub struct CustomResponse { @@ -100,7 +100,7 @@ async fn import_data( plugin_name, file_paths.first().unwrap(), ) - .await + .await { result = Some(r); break; @@ -196,8 +196,12 @@ async fn send_request( let pool2 = pool.clone(); tokio::spawn(async move { - if let Err(e) = actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2).await { - response_err(&response2, e, &app_handle2, &pool2).await.expect("Failed to update response"); + if let Err(e) = + actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2).await + { + response_err(&response2, e, &app_handle2, &pool2) + .await + .expect("Failed to update response"); } }); @@ -220,6 +224,15 @@ async fn response_err( Ok(response) } +#[tauri::command] +async fn set_update_mode( + update_mode: &str, + window: Window, + db_instance: State<'_, Mutex>>, +) -> Result { + set_key_value("app", "update_mode", update_mode, window, db_instance).await +} + #[tauri::command] async fn get_key_value( namespace: &str, @@ -263,8 +276,8 @@ async fn create_workspace( ..Default::default() }, ) - .await - .expect("Failed to create Workspace"); + .await + .expect("Failed to create Workspace"); emit_and_return(&window, "created_model", created_workspace) } @@ -287,8 +300,8 @@ async fn create_environment( ..Default::default() }, ) - .await - .expect("Failed to create environment"); + .await + .expect("Failed to create environment"); emit_and_return(&window, "created_model", created_environment) } @@ -314,8 +327,8 @@ async fn create_request( ..Default::default() }, ) - .await - .expect("Failed to create request"); + .await + .expect("Failed to create request"); emit_and_return(&window, "created_model", created_request) } @@ -420,8 +433,8 @@ async fn create_folder( ..Default::default() }, ) - .await - .expect("Failed to create folder"); + .await + .expect("Failed to create folder"); emit_and_return(&window, "created_model", created_request) } @@ -586,8 +599,8 @@ async fn list_workspaces( ..Default::default() }, ) - .await - .expect("Failed to create Workspace"); + .await + .expect("Failed to create Workspace"); Ok(vec![workspace]) } else { Ok(workspaces) @@ -614,9 +627,19 @@ async fn delete_workspace( } #[tauri::command] -async fn check_for_updates(app_handle: AppHandle, yaak_updater: State<'_, Mutex>, +async fn check_for_updates( + app_handle: AppHandle, + db_instance: State<'_, Mutex>>, + yaak_updater: State<'_, Mutex>, ) -> Result<(), String> { - yaak_updater.lock().await.check(&app_handle).await.map_err(|e| e.to_string()) + let pool = &*db_instance.lock().await; + let update_mode = get_update_mode(pool).await; + yaak_updater + .lock() + .await + .force_check(&app_handle, update_mode) + .await + .map_err(|e| e.to_string()) } fn main() { @@ -659,7 +682,6 @@ fn main() { .expect("Failed to migrate database"); app.manage(m); - let yaak_updater = YaakUpdater::new(); app.manage(Mutex::new(yaak_updater)); @@ -697,6 +719,7 @@ fn main() { send_ephemeral_request, send_request, set_key_value, + set_update_mode, update_environment, update_folder, update_request, @@ -740,12 +763,19 @@ fn main() { None, ); } - RunEvent::WindowEvent { label: _label, event: WindowEvent::Focused(true), .. } => { + RunEvent::WindowEvent { + label: _label, + event: WindowEvent::Focused(true), + .. + } => { let h = app_handle.clone(); // Run update check whenever window is focused tauri::async_runtime::spawn(async move { let val: State<'_, Mutex> = h.state(); - _ = val.lock().await.check(&h).await; + let db_instance: State<'_, Mutex>> = h.state(); + let pool = &*db_instance.lock().await; + let update_mode = get_update_mode(pool).await; + _ = val.lock().await.check(&h, update_mode).await; }); } _ => {} @@ -783,16 +813,16 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> Window { window_id, WindowUrl::App(url.unwrap_or_default().into()), ) - .menu(app_menu) - .fullscreen(false) - .resizable(true) - .inner_size(1100.0, 600.0) - .position( - // Randomly offset so windows don't stack exactly - 100.0 + random::() * 30.0, - 100.0 + random::() * 30.0, - ) - .title(handle.package_info().name.to_string()); + .menu(app_menu) + .fullscreen(false) + .resizable(true) + .inner_size(1100.0, 600.0) + .position( + // Randomly offset so windows don't stack exactly + 100.0 + random::() * 30.0, + 100.0 + random::() * 30.0, + ) + .title(handle.package_info().name.to_string()); // Add macOS-only things #[cfg(target_os = "macos")] @@ -868,3 +898,12 @@ fn emit_and_return( fn emit_side_effect(app_handle: &AppHandle, event: &str, payload: S) { app_handle.emit_all(event, &payload).unwrap(); } + +async fn get_update_mode(pool: &Pool) -> UpdateMode { + let mode = models::get_key_value_string("app", "update_mode", pool) + .await; + match mode { + Some(mode) => update_mode_from_str(&mode), + None => UpdateMode::Stable, + } +} diff --git a/src-tauri/src/models.rs b/src-tauri/src/models.rs index e4d70906..6bcde653 100644 --- a/src-tauri/src/models.rs +++ b/src-tauri/src/models.rs @@ -193,6 +193,18 @@ pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool) -> O .ok() } +pub async fn get_key_value_string(namespace: &str, key: &str, pool: &Pool) -> Option { + let kv = get_key_value(namespace, key, pool).await?; + let result = serde_json::from_str(&kv.value); + match result { + Ok(v) => Some(v), + Err(e) => { + println!("Failed to parse key value: {}", e); + None + } + } +} + pub async fn find_workspaces(pool: &Pool) -> Result, sqlx::Error> { sqlx::query_as!( Workspace, diff --git a/src-tauri/src/updates.rs b/src-tauri/src/updates.rs index 65bb7444..43cad3f1 100644 --- a/src-tauri/src/updates.rs +++ b/src-tauri/src/updates.rs @@ -1,5 +1,6 @@ use std::time::SystemTime; +use log::info; use tauri::{AppHandle, updater, Window, Wry}; use tauri::api::dialog; @@ -11,19 +12,31 @@ pub struct YaakUpdater { last_update_check: SystemTime, } +pub enum UpdateMode { + Stable, + Beta, +} + impl YaakUpdater { pub fn new() -> Self { Self { last_update_check: SystemTime::UNIX_EPOCH, } } - pub async fn check(&mut self, app_handle: &AppHandle) -> Result<(), updater::Error> { - if self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS { - return Ok(()); - } - + pub async fn force_check( + &mut self, + app_handle: &AppHandle, + mode: UpdateMode, + ) -> Result<(), updater::Error> { self.last_update_check = SystemTime::now(); - match app_handle.updater().check().await { + let update_mode = get_update_mode_str(mode); + info!("Checking for updates mode={}", update_mode); + match app_handle + .updater() + .header("X-Update-Mode", update_mode)? + .check() + .await + { Ok(update) => { if dialog::blocking::ask( None::<&Window>, @@ -38,4 +51,29 @@ impl YaakUpdater { Err(e) => Err(e), } } + pub async fn check( + &mut self, + app_handle: &AppHandle, + mode: UpdateMode, + ) -> Result<(), updater::Error> { + if self.last_update_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS { + return Ok(()); + } + + self.force_check(app_handle, mode).await + } +} + +pub fn update_mode_from_str(mode: &str) -> UpdateMode { + match mode { + "beta" => UpdateMode::Beta, + _ => UpdateMode::Stable, + } +} + +fn get_update_mode_str(mode: UpdateMode) -> &'static str { + match mode { + UpdateMode::Stable => "stable", + UpdateMode::Beta => "beta", + } } diff --git a/src-web/components/WorkspaceActionsDropdown.tsx b/src-web/components/WorkspaceActionsDropdown.tsx index 0d56f8b2..c777cc2c 100644 --- a/src-web/components/WorkspaceActionsDropdown.tsx +++ b/src-web/components/WorkspaceActionsDropdown.tsx @@ -10,6 +10,7 @@ import { useImportData } from '../hooks/useImportData'; import { usePrompt } from '../hooks/usePrompt'; import { getRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useTheme } from '../hooks/useTheme'; +import { useUpdateMode } from '../hooks/useUpdateMode'; import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace'; import { useWorkspaces } from '../hooks/useWorkspaces'; import type { ButtonProps } from './core/Button'; @@ -39,6 +40,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ const dialog = useDialog(); const prompt = usePrompt(); const routes = useAppRoutes(); + const [updateMode, setUpdateMode] = useUpdateMode(); const items: DropdownItem[] = useMemo(() => { const workspaceItems: DropdownItem[] = workspaces.map((w) => ({ @@ -145,12 +147,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ createWorkspace.mutate({ name }); }, }, - { - key: 'appearance', - label: 'Toggle Theme', - onSelect: toggleAppearance, - leftSlot: , - }, { key: 'import-data', label: 'Import Data', @@ -163,6 +159,25 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ leftSlot: , onSelect: () => exportData.mutate(), }, + { type: 'separator' }, + { + key: 'appearance', + label: 'Toggle Theme', + onSelect: toggleAppearance, + leftSlot: , + }, + { + key: 'update-mode', + label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta', + onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'), + leftSlot: , + }, + { + key: 'update-check', + label: 'Check for Updates', + onSelect: () => invoke('check_for_updates'), + leftSlot: , + }, ]; }, [ activeWorkspace?.name, @@ -175,7 +190,9 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({ importData, prompt, routes, + setUpdateMode, toggleAppearance, + updateMode, updateWorkspace, workspaces, ]); diff --git a/src-web/hooks/useUpdateMode.ts b/src-web/hooks/useUpdateMode.ts new file mode 100644 index 00000000..5fc8637e --- /dev/null +++ b/src-web/hooks/useUpdateMode.ts @@ -0,0 +1,12 @@ +import { NAMESPACE_APP } from '../lib/keyValueStore'; +import { useKeyValue } from './useKeyValue'; + +export function useUpdateMode() { + const kv = useKeyValue<'stable' | 'beta'>({ + namespace: NAMESPACE_APP, + key: 'update_mode', + defaultValue: 'stable', + }); + + return [kv.value, kv.set] as const; +} diff --git a/src-web/lib/keyValueStore.ts b/src-web/lib/keyValueStore.ts index f34c6145..69235d91 100644 --- a/src-web/lib/keyValueStore.ts +++ b/src-web/lib/keyValueStore.ts @@ -1,6 +1,7 @@ import { invoke } from '@tauri-apps/api'; import type { KeyValue } from './models'; +export const NAMESPACE_APP = 'app'; export const NAMESPACE_GLOBAL = 'global'; export const NAMESPACE_NO_SYNC = 'no_sync';