Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion frontend/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
1 change: 0 additions & 1 deletion frontend/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
"permissions": [
"core:default",
"updater:default",
"dialog:default",
"fs:default",
{
"identifier": "fs:allow-read-file",
Expand Down
1 change: 0 additions & 1 deletion frontend/src-tauri/capabilities/mobile-android.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
"permissions": [
"core:default",
"core:event:default",
"dialog:default",
"deep-link:default",
"fs:default",
{
Expand Down
1 change: 0 additions & 1 deletion frontend/src-tauri/capabilities/mobile-ios.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
"core:default",
"core:event:default",
"updater:default",
"dialog:default",
"deep-link:default",
{
"identifier": "opener:allow-open-url",
Expand Down
55 changes: 21 additions & 34 deletions frontend/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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())
Expand All @@ -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
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -99,6 +100,7 @@ export default function App() {
<TooltipProvider>
<BillingServiceProvider>
<ProxyEventListener />
<UpdateEventListener />
<DeepLinkHandler />
<InnerApp />
</BillingServiceProvider>
Expand Down
70 changes: 54 additions & 16 deletions frontend/src/components/GlobalNotification.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -68,36 +75,67 @@ export function GlobalNotification({ notification, onDismiss }: GlobalNotificati
return <AlertCircle className="h-5 w-5 text-destructive" />;
case "info":
return <Server className="h-5 w-5 text-primary" />;
case "update":
return <Download className="h-5 w-5 text-primary" />;
}
};

const hasActions = notification.actions && notification.actions.length > 0;

return (
<div className="fixed top-4 right-4 z-50 pointer-events-none">
<div
className={cn(
"pointer-events-auto flex items-start gap-3 rounded-lg border bg-card text-card-foreground p-4 shadow-lg transition-all duration-200 min-w-[320px] max-w-md",
"pointer-events-auto flex flex-col gap-3 rounded-lg border bg-card text-card-foreground p-4 shadow-lg transition-all duration-200 min-w-[320px] max-w-md",
isLeaving ? "opacity-0 translate-x-full" : "opacity-100 translate-x-0",
notification.type === "error" && "border-destructive/50 dark:border-destructive",
notification.type === "success" && "border-green-500/50 dark:border-green-500",
notification.type === "info" && "border-primary/50"
notification.type === "info" && "border-primary/50",
notification.type === "update" && "border-primary/50"
)}
>
<div className="flex-shrink-0">{getIcon()}</div>
<div className="flex items-start gap-3">
<div className="flex-shrink-0">{getIcon()}</div>

<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium leading-tight">{notification.title}</h4>
{notification.message && (
<p className="text-xs text-muted-foreground mt-1">{notification.message}</p>
<div className="flex-1 min-w-0">
<h4 className="text-sm font-medium leading-tight">{notification.title}</h4>
{notification.message && (
<p className="text-xs text-muted-foreground mt-1">{notification.message}</p>
)}
</div>

{!hasActions && (
<button
onClick={handleDismiss}
className="flex-shrink-0 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
)}
</div>

<button
onClick={handleDismiss}
className="flex-shrink-0 rounded-sm opacity-70 hover:opacity-100 transition-opacity"
>
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</button>
{hasActions && (
<div className="flex justify-end gap-2">
{notification.actions!.map((action, index) => (
<button
key={index}
onClick={() => {
action.onClick();
handleDismiss();
}}
className={cn(
"px-3 py-1.5 text-xs font-medium rounded-md transition-colors",
action.variant === "primary"
? "bg-primary text-primary-foreground hover:bg-primary/90"
: "text-muted-foreground hover:text-foreground hover:bg-muted"
)}
>
{action.label}
</button>
))}
</div>
)}
</div>
</div>
);
Expand Down
65 changes: 65 additions & 0 deletions frontend/src/components/UpdateEventListener.tsx
Original file line number Diff line number Diff line change
@@ -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<UpdateReadyPayload>("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;
}
Loading