diff --git a/frontend/src-tauri/Cargo.toml b/frontend/src-tauri/Cargo.toml index c5a73659..72dbd56b 100644 --- a/frontend/src-tauri/Cargo.toml +++ b/frontend/src-tauri/Cargo.toml @@ -24,7 +24,6 @@ log = "0.4" tauri = { version = "2.9.2", features = [] } tauri-plugin-log = "2.7.1" tauri-plugin-updater = "2.9.0" -tauri-plugin-dialog = "2.4.2" tauri-plugin = "2.1.1" tauri-plugin-deep-link = "2.4.5" tauri-plugin-single-instance = { version = "2.3.6", features = ["deep-link"] } diff --git a/frontend/src-tauri/capabilities/default.json b/frontend/src-tauri/capabilities/default.json index 65f9bb44..3e5456fc 100644 --- a/frontend/src-tauri/capabilities/default.json +++ b/frontend/src-tauri/capabilities/default.json @@ -8,7 +8,6 @@ "permissions": [ "core:default", "updater:default", - "dialog:default", "fs:default", { "identifier": "fs:allow-read-file", diff --git a/frontend/src-tauri/capabilities/mobile-android.json b/frontend/src-tauri/capabilities/mobile-android.json index 4965df7d..f3804bcc 100644 --- a/frontend/src-tauri/capabilities/mobile-android.json +++ b/frontend/src-tauri/capabilities/mobile-android.json @@ -9,7 +9,6 @@ "permissions": [ "core:default", "core:event:default", - "dialog:default", "deep-link:default", "fs:default", { diff --git a/frontend/src-tauri/capabilities/mobile-ios.json b/frontend/src-tauri/capabilities/mobile-ios.json index 8fb36bd8..54fe1864 100644 --- a/frontend/src-tauri/capabilities/mobile-ios.json +++ b/frontend/src-tauri/capabilities/mobile-ios.json @@ -10,7 +10,6 @@ "core:default", "core:event:default", "updater:default", - "dialog:default", "deep-link:default", { "identifier": "opener:allow-open-url", diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index fbb0b6e8..93479e31 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -5,6 +5,13 @@ use tauri_plugin_opener; mod proxy; mod pdf_extractor; +#[cfg(desktop)] +#[tauri::command] +fn restart_for_update(app_handle: tauri::AppHandle) { + log::info!("User requested restart for update"); + app_handle.restart(); +} + // This handles incoming deep links fn handle_deep_link_event(url: &str, app: &tauri::AppHandle) { log::info!("[Deep Link] Received: {}", url); @@ -23,7 +30,6 @@ pub fn run() { log::info!("Single instance detected: {}, {argv:?}, {cwd}", app.package_info().name); })) .plugin(tauri_plugin_log::Builder::default().level(log::LevelFilter::Info).build()) - .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_os::init()) @@ -37,6 +43,7 @@ pub fn run() { proxy::save_proxy_settings, proxy::test_proxy_port, pdf_extractor::extract_document_content, + restart_for_update, ]) .setup(|app| { // Initialize proxy auto-start @@ -232,7 +239,6 @@ pub fn run() { .level(log::LevelFilter::Info) .build(), ) - .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_os::init()) @@ -362,41 +368,22 @@ async fn check_for_updates(app_handle: tauri::AppHandle) -> Result<(), String> { return Ok(()); } - // Mark as downloaded to prevent further update dialogs + // Mark as downloaded to prevent further update notifications UPDATE_DOWNLOADED.store(true, Ordering::SeqCst); - // Show a dialog prompting the user to restart - let message = format!( - "An update to version {} has been downloaded and is ready to install. \ - Would you like to restart the application now to apply the update?", - update.version - ); + // Emit event to frontend for toast notification + #[derive(Clone, serde::Serialize)] + struct UpdateReadyPayload { + version: String, + } - use tauri_plugin_dialog::{ - DialogExt, MessageDialogButtons, MessageDialogKind, - }; - let dialog = app_handle.dialog(); - - // Show a friendly info dialog with Yes/No buttons - dialog - .message(message) - .title("Maple Update") - .kind(MessageDialogKind::Info) // Use info icon for a friendlier look - .buttons(MessageDialogButtons::OkCancelCustom( - "Yes".to_string(), - "No".to_string(), - )) - .show(move |should_restart| { - if should_restart { - log::info!("User chose to restart now for update"); - - // Restart the application instead of just exiting - // This will automatically apply the update - app_handle.restart(); - } else { - log::info!("User chose to postpone update restart"); - } - }); + if let Err(e) = app_handle.emit("update-ready", UpdateReadyPayload { + version: update.version.clone(), + }) { + log::error!("Failed to emit update-ready event: {}", e); + } else { + log::info!("Emitted update-ready event for version {}", update.version); + } } Err(e) => { log::error!("Failed to install update: {}", e); diff --git a/frontend/src/app.tsx b/frontend/src/app.tsx index fa176cf7..793e15c4 100644 --- a/frontend/src/app.tsx +++ b/frontend/src/app.tsx @@ -13,6 +13,7 @@ import { BillingServiceProvider } from "./components/BillingServiceProvider"; import { DeepLinkHandler } from "./components/DeepLinkHandler"; import { NotificationProvider } from "./contexts/NotificationContext"; import { ProxyEventListener } from "./components/ProxyEventListener"; +import { UpdateEventListener } from "./components/UpdateEventListener"; // Create a new router instance const router = createRouter({ @@ -99,6 +100,7 @@ export default function App() { + diff --git a/frontend/src/components/GlobalNotification.tsx b/frontend/src/components/GlobalNotification.tsx index ac07618d..c82beeb5 100644 --- a/frontend/src/components/GlobalNotification.tsx +++ b/frontend/src/components/GlobalNotification.tsx @@ -1,14 +1,21 @@ import { useEffect, useState, useCallback, useRef } from "react"; -import { CheckCircle, AlertCircle, X, Server } from "lucide-react"; +import { CheckCircle, AlertCircle, X, Server, Download } from "lucide-react"; import { cn } from "@/utils/utils"; +export interface NotificationAction { + label: string; + onClick: () => void; + variant?: "primary" | "secondary"; +} + export interface Notification { id: string; - type: "success" | "error" | "info"; + type: "success" | "error" | "info" | "update"; title: string; message?: string; icon?: React.ReactNode; duration?: number; // ms, 0 = permanent + actions?: NotificationAction[]; } interface GlobalNotificationProps { @@ -68,36 +75,67 @@ export function GlobalNotification({ notification, onDismiss }: GlobalNotificati return ; case "info": return ; + case "update": + return ; } }; + const hasActions = notification.actions && notification.actions.length > 0; + return (
-
{getIcon()}
+
+
{getIcon()}
-
-

{notification.title}

- {notification.message && ( -

{notification.message}

+
+

{notification.title}

+ {notification.message && ( +

{notification.message}

+ )} +
+ + {!hasActions && ( + )}
- + {hasActions && ( +
+ {notification.actions!.map((action, index) => ( + + ))} +
+ )}
); diff --git a/frontend/src/components/UpdateEventListener.tsx b/frontend/src/components/UpdateEventListener.tsx new file mode 100644 index 00000000..3e4a2edb --- /dev/null +++ b/frontend/src/components/UpdateEventListener.tsx @@ -0,0 +1,65 @@ +import { useEffect } from "react"; +import { listen } from "@tauri-apps/api/event"; +import { invoke } from "@tauri-apps/api/core"; +import { useNotification } from "@/contexts/NotificationContext"; +import { isTauri } from "@/utils/platform"; + +interface UpdateReadyPayload { + version: string; +} + +export function UpdateEventListener() { + const { showNotification } = useNotification(); + + useEffect(() => { + if (!isTauri()) { + return; + } + + let unlistenUpdateReady: (() => void) | null = null; + + const setupListeners = async () => { + try { + unlistenUpdateReady = await listen("update-ready", (event) => { + const { version } = event.payload; + showNotification({ + type: "update", + title: "Update Ready", + message: `Version ${version} has been downloaded and is ready to install.`, + duration: 0, + actions: [ + { + label: "Later", + variant: "secondary", + onClick: () => { + // Just dismiss - the notification will close automatically + } + }, + { + label: "Restart Now", + variant: "primary", + onClick: async () => { + try { + await invoke("restart_for_update"); + } catch (error) { + console.error("Failed to restart for update:", error); + } + } + } + ] + }); + }); + } catch (error) { + console.error("Failed to setup update event listeners:", error); + } + }; + + setupListeners(); + + return () => { + if (unlistenUpdateReady) unlistenUpdateReady(); + }; + }, [showNotification]); + + return null; +}