mirror of
https://github.com/Frooodle/Stirling-PDF.git
synced 2025-11-16 01:21:16 +01:00
Merge e367af47ef into 5c9e590856
This commit is contained in:
commit
dd57459d73
@ -41,6 +41,30 @@
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Extract"
|
||||
},
|
||||
"defaultApp": {
|
||||
"title": "Set as Default PDF App",
|
||||
"message": "Would you like to set Stirling PDF as your default PDF editor?",
|
||||
"description": "You can change this later in your system settings.",
|
||||
"notNow": "Not Now",
|
||||
"setDefault": "Set Default",
|
||||
"dismiss": "Dismiss",
|
||||
"prompt": {
|
||||
"title": "Set as Default PDF Editor",
|
||||
"message": "Make Stirling PDF your default application for opening PDF files."
|
||||
},
|
||||
"success": {
|
||||
"title": "Default App Set",
|
||||
"message": "Stirling PDF is now your default PDF editor"
|
||||
},
|
||||
"settingsOpened": {
|
||||
"title": "Settings Opened",
|
||||
"message": "Please select Stirling PDF in your system settings"
|
||||
},
|
||||
"error": {
|
||||
"title": "Error",
|
||||
"message": "Failed to set default PDF handler"
|
||||
}
|
||||
},
|
||||
"language": {
|
||||
"direction": "ltr"
|
||||
},
|
||||
@ -332,7 +356,13 @@
|
||||
"mode": {
|
||||
"fullscreen": "Fullscreen",
|
||||
"sidebar": "Sidebar"
|
||||
}
|
||||
},
|
||||
"defaultPdfEditor": "Default PDF editor",
|
||||
"defaultPdfEditorActive": "Stirling PDF is your default PDF editor",
|
||||
"defaultPdfEditorInactive": "Another application is set as default",
|
||||
"defaultPdfEditorChecking": "Checking...",
|
||||
"defaultPdfEditorSet": "Already Default",
|
||||
"setAsDefault": "Set as Default"
|
||||
},
|
||||
"hotkeys": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
|
||||
11
frontend/src-tauri/Cargo.lock
generated
11
frontend/src-tauri/Cargo.lock
generated
@ -643,6 +643,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-services"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0aa845ab21b847ee46954be761815f18f16469b29ef3ba250241b1b8bab659a"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
@ -3941,6 +3950,8 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
name = "stirling-pdf"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"core-foundation 0.10.1",
|
||||
"core-services",
|
||||
"log",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
|
||||
@ -31,3 +31,7 @@ tauri-plugin-fs = "2.4.4"
|
||||
tauri-plugin-single-instance = "2.0.1"
|
||||
tokio = { version = "1.0", features = ["time"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
core-foundation = "0.10"
|
||||
core-services = "1.0"
|
||||
|
||||
217
frontend/src-tauri/src/commands/default_app.rs
Normal file
217
frontend/src-tauri/src/commands/default_app.rs
Normal file
@ -0,0 +1,217 @@
|
||||
use crate::utils::add_log;
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
use std::process::Command;
|
||||
|
||||
/// Check if Stirling PDF is the default PDF handler
|
||||
#[tauri::command]
|
||||
pub fn is_default_pdf_handler() -> Result<bool, String> {
|
||||
add_log("🔍 Checking if app is default PDF handler".to_string());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
check_default_windows()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
check_default_macos()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
check_default_linux()
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to set/prompt for Stirling PDF as default PDF handler
|
||||
#[tauri::command]
|
||||
pub fn set_as_default_pdf_handler() -> Result<String, String> {
|
||||
add_log("⚙️ Attempting to set as default PDF handler".to_string());
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
set_default_windows()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
set_default_macos()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
set_default_linux()
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Windows Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn check_default_windows() -> Result<bool, String> {
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
// Query the default handler for .pdf extension
|
||||
let output = Command::new("cmd")
|
||||
.args(["/C", "assoc .pdf"])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to check default app: {}", e))?;
|
||||
|
||||
let assoc = String::from_utf8_lossy(&output.stdout);
|
||||
add_log(format!("Windows PDF association: {}", assoc.trim()));
|
||||
|
||||
// Get the ProgID for .pdf files
|
||||
if let Some(prog_id) = assoc.trim().strip_prefix(".pdf=") {
|
||||
// Query what application handles this ProgID
|
||||
let output = Command::new("cmd")
|
||||
.args(["/C", &format!("ftype {}", prog_id)])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to query file type: {}", e))?;
|
||||
|
||||
let ftype = String::from_utf8_lossy(&output.stdout);
|
||||
add_log(format!("Windows file type: {}", ftype.trim()));
|
||||
|
||||
// Check if it contains "Stirling" or our app name
|
||||
let is_default = ftype.to_lowercase().contains("stirling");
|
||||
Ok(is_default)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn set_default_windows() -> Result<String, String> {
|
||||
// On Windows 10+, we need to open the Default Apps settings
|
||||
// as programmatic setting requires a signed installer
|
||||
Command::new("cmd")
|
||||
.args(["/C", "start", "ms-settings:defaultapps"])
|
||||
.spawn()
|
||||
.map_err(|e| format!("Failed to open default apps settings: {}", e))?;
|
||||
|
||||
add_log("Opened Windows Default Apps settings".to_string());
|
||||
Ok("opened_settings".to_string())
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// macOS Implementation (using LaunchServices framework)
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn check_default_macos() -> Result<bool, String> {
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::string::{CFString, CFStringRef};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
// Define the LSCopyDefaultRoleHandlerForContentType function
|
||||
#[link(name = "CoreServices", kind = "framework")]
|
||||
extern "C" {
|
||||
fn LSCopyDefaultRoleHandlerForContentType(
|
||||
content_type: CFStringRef,
|
||||
role: c_int,
|
||||
) -> CFStringRef;
|
||||
}
|
||||
|
||||
const K_LS_ROLES_ALL: c_int = 0xFFFFFFFF_u32 as c_int;
|
||||
|
||||
unsafe {
|
||||
// Query the default handler for "com.adobe.pdf" (PDF UTI - standard macOS identifier)
|
||||
let pdf_uti = CFString::new("com.adobe.pdf");
|
||||
let handler_ref = LSCopyDefaultRoleHandlerForContentType(pdf_uti.as_concrete_TypeRef(), K_LS_ROLES_ALL);
|
||||
|
||||
if handler_ref.is_null() {
|
||||
add_log("No default PDF handler found".to_string());
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let handler = CFString::wrap_under_create_rule(handler_ref);
|
||||
let handler_str = handler.to_string();
|
||||
add_log(format!("macOS PDF handler: {}", handler_str));
|
||||
|
||||
// Check if it's our bundle identifier
|
||||
let is_default = handler_str == "stirling.pdf.dev";
|
||||
Ok(is_default)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
fn set_default_macos() -> Result<String, String> {
|
||||
use core_foundation::base::TCFType;
|
||||
use core_foundation::string::{CFString, CFStringRef};
|
||||
use std::os::raw::c_int;
|
||||
|
||||
// Define the LSSetDefaultRoleHandlerForContentType function
|
||||
#[link(name = "CoreServices", kind = "framework")]
|
||||
extern "C" {
|
||||
fn LSSetDefaultRoleHandlerForContentType(
|
||||
content_type: CFStringRef,
|
||||
role: c_int,
|
||||
handler_bundle_id: CFStringRef,
|
||||
) -> c_int; // OSStatus
|
||||
}
|
||||
|
||||
const K_LS_ROLES_ALL: c_int = 0xFFFFFFFF_u32 as c_int;
|
||||
|
||||
unsafe {
|
||||
// Set our app as the default handler for PDF files
|
||||
let pdf_uti = CFString::new("com.adobe.pdf");
|
||||
let our_bundle_id = CFString::new("stirling.pdf.dev");
|
||||
|
||||
let status = LSSetDefaultRoleHandlerForContentType(
|
||||
pdf_uti.as_concrete_TypeRef(),
|
||||
K_LS_ROLES_ALL,
|
||||
our_bundle_id.as_concrete_TypeRef(),
|
||||
);
|
||||
|
||||
if status == 0 {
|
||||
add_log("Successfully triggered default app dialog".to_string());
|
||||
Ok("set_successfully".to_string())
|
||||
} else {
|
||||
let error_msg = format!("LaunchServices returned status: {}", status);
|
||||
add_log(error_msg.clone());
|
||||
Err(error_msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Linux Implementation
|
||||
// ============================================================================
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn check_default_linux() -> Result<bool, String> {
|
||||
// Use xdg-mime to check the default application for PDF files
|
||||
let output = Command::new("xdg-mime")
|
||||
.args(["query", "default", "application/pdf"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to check default app: {}", e))?;
|
||||
|
||||
let handler = String::from_utf8_lossy(&output.stdout);
|
||||
add_log(format!("Linux PDF handler: {}", handler.trim()));
|
||||
|
||||
// Check if it's our .desktop file
|
||||
let is_default = handler.trim() == "stirling-pdf.desktop";
|
||||
Ok(is_default)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn set_default_linux() -> Result<String, String> {
|
||||
// Use xdg-mime to set the default application for PDF files
|
||||
let result = Command::new("xdg-mime")
|
||||
.args(["default", "stirling-pdf.desktop", "application/pdf"])
|
||||
.output()
|
||||
.map_err(|e| format!("Failed to set default app: {}", e))?;
|
||||
|
||||
if result.status.success() {
|
||||
add_log("Set as default PDF handler on Linux".to_string());
|
||||
Ok("set_successfully".to_string())
|
||||
} else {
|
||||
let error = String::from_utf8_lossy(&result.stderr);
|
||||
add_log(format!("Failed to set default: {}", error));
|
||||
Err(format!("Failed to set as default: {}", error))
|
||||
}
|
||||
}
|
||||
@ -14,23 +14,11 @@ pub fn add_opened_file(file_path: String) {
|
||||
// Command to get opened file paths (if app was launched with files)
|
||||
#[tauri::command]
|
||||
pub async fn get_opened_files() -> Result<Vec<String>, String> {
|
||||
let mut all_files: Vec<String> = Vec::new();
|
||||
|
||||
// Get files from command line arguments (Windows/Linux 'Open With Stirling' behaviour)
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let pdf_files: Vec<String> = args.iter()
|
||||
.skip(1)
|
||||
.filter(|arg| std::path::Path::new(arg).exists())
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
all_files.extend(pdf_files);
|
||||
|
||||
// Add any files sent via events or other instances (macOS 'Open With Stirling' behaviour, also Windows/Linux extra files)
|
||||
{
|
||||
let opened_files = OPENED_FILES.lock().unwrap();
|
||||
all_files.extend(opened_files.clone());
|
||||
}
|
||||
// Get all files from the OPENED_FILES store
|
||||
// Command line args are processed in setup() callback and added to this store
|
||||
// Additional files from second instances or events are also added here
|
||||
let opened_files = OPENED_FILES.lock().unwrap();
|
||||
let all_files = opened_files.clone();
|
||||
|
||||
add_log(format!("📂 Returning {} opened file(s)", all_files.len()));
|
||||
Ok(all_files)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
pub mod backend;
|
||||
pub mod health;
|
||||
pub mod files;
|
||||
pub mod default_app;
|
||||
|
||||
pub use backend::{start_backend, cleanup_backend};
|
||||
pub use health::check_backend_health;
|
||||
pub use files::{get_opened_files, clear_opened_files, add_opened_file};
|
||||
pub use default_app::{is_default_pdf_handler, set_as_default_pdf_handler};
|
||||
|
||||
@ -3,7 +3,16 @@ use tauri::{RunEvent, WindowEvent, Emitter, Manager};
|
||||
mod utils;
|
||||
mod commands;
|
||||
|
||||
use commands::{start_backend, check_backend_health, get_opened_files, clear_opened_files, cleanup_backend, add_opened_file};
|
||||
use commands::{
|
||||
start_backend,
|
||||
check_backend_health,
|
||||
get_opened_files,
|
||||
clear_opened_files,
|
||||
cleanup_backend,
|
||||
add_opened_file,
|
||||
is_default_pdf_handler,
|
||||
set_as_default_pdf_handler,
|
||||
};
|
||||
use utils::{add_log, get_tauri_logs};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
@ -23,9 +32,6 @@ pub fn run() {
|
||||
// Store file for later retrieval (in case frontend isn't ready yet)
|
||||
add_opened_file(arg.clone());
|
||||
|
||||
// Also emit event for immediate handling if frontend is ready
|
||||
let _ = app.emit("file-opened", arg.clone());
|
||||
|
||||
// Bring the existing window to front
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.set_focus();
|
||||
@ -33,13 +39,34 @@ pub fn run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Emit a generic notification that files were added (frontend will re-read storage)
|
||||
let _ = app.emit("files-changed", ());
|
||||
}))
|
||||
.setup(|_app| {
|
||||
add_log("🚀 Tauri app setup started".to_string());
|
||||
|
||||
// Process command line arguments on first launch
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
for arg in args.iter().skip(1) {
|
||||
if std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 Initial file from command line: {}", arg));
|
||||
add_opened_file(arg.clone());
|
||||
}
|
||||
}
|
||||
|
||||
add_log("🔍 DEBUG: Setup completed".to_string());
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_files, clear_opened_files, get_tauri_logs])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
start_backend,
|
||||
check_backend_health,
|
||||
get_opened_files,
|
||||
clear_opened_files,
|
||||
get_tauri_logs,
|
||||
is_default_pdf_handler,
|
||||
set_as_default_pdf_handler,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| {
|
||||
@ -58,6 +85,7 @@ pub fn run() {
|
||||
#[cfg(target_os = "macos")]
|
||||
RunEvent::Opened { urls } => {
|
||||
add_log(format!("📂 Tauri file opened event: {:?}", urls));
|
||||
let mut added_files = false;
|
||||
for url in urls {
|
||||
let url_str = url.as_str();
|
||||
if url_str.starts_with("file://") {
|
||||
@ -65,11 +93,14 @@ pub fn run() {
|
||||
if file_path.ends_with(".pdf") {
|
||||
add_log(format!("📂 Processing opened PDF: {}", file_path));
|
||||
add_opened_file(file_path.to_string());
|
||||
// Use unified event name for consistency across platforms
|
||||
let _ = app_handle.emit("file-opened", file_path.to_string());
|
||||
added_files = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Emit a generic notification that files were added (frontend will re-read storage)
|
||||
if added_files {
|
||||
let _ = app_handle.emit("files-changed", ());
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Only log unhandled events in debug mode to reduce noise
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Suspense } from "react";
|
||||
import { AppProviders } from "@app/components/AppProviders";
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
||||
import HomePage from "@app/pages/HomePage";
|
||||
import OnboardingTour from "@app/components/onboarding/OnboardingTour";
|
||||
@ -16,8 +17,10 @@ export default function App() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<AppProviders>
|
||||
<HomePage />
|
||||
<OnboardingTour />
|
||||
<AppLayout>
|
||||
<HomePage />
|
||||
<OnboardingTour />
|
||||
</AppLayout>
|
||||
</AppProviders>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
31
frontend/src/core/components/AppLayout.tsx
Normal file
31
frontend/src/core/components/AppLayout.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import { ReactNode } from 'react';
|
||||
import { useBanner } from '@app/contexts/BannerContext';
|
||||
|
||||
interface AppLayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* App layout wrapper that handles banner rendering and viewport sizing
|
||||
* Automatically adjusts child components to fit remaining space after banner
|
||||
*/
|
||||
export function AppLayout({ children }: AppLayoutProps) {
|
||||
const { banner } = useBanner();
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>{`
|
||||
.h-screen,
|
||||
.right-rail {
|
||||
height: 100% !important;
|
||||
}
|
||||
`}</style>
|
||||
<div style={{ height: '100vh', display: 'flex', flexDirection: 'column' }}>
|
||||
{banner}
|
||||
<div style={{ flex: 1, minHeight: 0, height: 0 }}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -16,6 +16,7 @@ import { OnboardingProvider } from "@app/contexts/OnboardingContext";
|
||||
import { TourOrchestrationProvider } from "@app/contexts/TourOrchestrationContext";
|
||||
import { AdminTourOrchestrationProvider } from "@app/contexts/AdminTourOrchestrationContext";
|
||||
import { PageEditorProvider } from "@app/contexts/PageEditorContext";
|
||||
import { BannerProvider } from "@app/contexts/BannerContext";
|
||||
import ErrorBoundary from "@app/components/shared/ErrorBoundary";
|
||||
import { useScarfTracking } from "@app/hooks/useScarfTracking";
|
||||
import { useAppInitialization } from "@app/hooks/useAppInitialization";
|
||||
@ -50,22 +51,23 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
|
||||
<PreferencesProvider>
|
||||
<RainbowThemeProvider>
|
||||
<ErrorBoundary>
|
||||
<OnboardingProvider>
|
||||
<AppConfigProvider
|
||||
retryOptions={appConfigRetryOptions}
|
||||
{...appConfigProviderProps}
|
||||
>
|
||||
<ScarfTrackingInitializer />
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<AppInitializer />
|
||||
<ToolRegistryProvider>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<HotkeyProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<PageEditorProvider>
|
||||
<BannerProvider>
|
||||
<OnboardingProvider>
|
||||
<AppConfigProvider
|
||||
retryOptions={appConfigRetryOptions}
|
||||
{...appConfigProviderProps}
|
||||
>
|
||||
<ScarfTrackingInitializer />
|
||||
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
|
||||
<AppInitializer />
|
||||
<ToolRegistryProvider>
|
||||
<NavigationProvider>
|
||||
<FilesModalProvider>
|
||||
<ToolWorkflowProvider>
|
||||
<HotkeyProvider>
|
||||
<SidebarProvider>
|
||||
<ViewerProvider>
|
||||
<PageEditorProvider>
|
||||
<SignatureProvider>
|
||||
<RightRailProvider>
|
||||
<TourOrchestrationProvider>
|
||||
@ -76,16 +78,17 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
|
||||
</RightRailProvider>
|
||||
</SignatureProvider>
|
||||
</PageEditorProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</HotkeyProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</ToolRegistryProvider>
|
||||
</FileContextProvider>
|
||||
</AppConfigProvider>
|
||||
</OnboardingProvider>
|
||||
</ViewerProvider>
|
||||
</SidebarProvider>
|
||||
</HotkeyProvider>
|
||||
</ToolWorkflowProvider>
|
||||
</FilesModalProvider>
|
||||
</NavigationProvider>
|
||||
</ToolRegistryProvider>
|
||||
</FileContextProvider>
|
||||
</AppConfigProvider>
|
||||
</OnboardingProvider>
|
||||
</BannerProvider>
|
||||
</ErrorBoundary>
|
||||
</RainbowThemeProvider>
|
||||
</PreferencesProvider>
|
||||
|
||||
72
frontend/src/core/components/shared/InfoBanner.tsx
Normal file
72
frontend/src/core/components/shared/InfoBanner.tsx
Normal file
@ -0,0 +1,72 @@
|
||||
import React from 'react';
|
||||
import { Paper, Group, Text, Button, ActionIcon } from '@mantine/core';
|
||||
import LocalIcon from '@app/components/shared/LocalIcon';
|
||||
|
||||
interface InfoBannerProps {
|
||||
icon: string;
|
||||
message: string;
|
||||
buttonText: string;
|
||||
buttonIcon?: string;
|
||||
onButtonClick: () => void;
|
||||
onDismiss: () => void;
|
||||
loading?: boolean;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic info banner component for displaying dismissible messages at the top of the app
|
||||
*/
|
||||
export const InfoBanner: React.FC<InfoBannerProps> = ({
|
||||
icon,
|
||||
message,
|
||||
buttonText,
|
||||
buttonIcon = 'check-circle-rounded',
|
||||
onButtonClick,
|
||||
onDismiss,
|
||||
loading = false,
|
||||
show = true,
|
||||
}) => {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper
|
||||
p="sm"
|
||||
radius={0}
|
||||
style={{
|
||||
background: 'var(--mantine-color-blue-0)',
|
||||
borderBottom: '1px solid var(--mantine-color-blue-2)',
|
||||
position: 'relative',
|
||||
}}
|
||||
>
|
||||
<Group gap="sm" align="center" wrap="nowrap">
|
||||
<LocalIcon icon={icon} width="1.2rem" height="1.2rem" style={{ color: 'var(--mantine-color-blue-6)', flexShrink: 0 }} />
|
||||
<Text fw={500} size="sm" style={{ color: 'var(--mantine-color-blue-9)' }}>
|
||||
{message}
|
||||
</Text>
|
||||
<Button
|
||||
variant="light"
|
||||
color="blue"
|
||||
size="xs"
|
||||
onClick={onButtonClick}
|
||||
loading={loading}
|
||||
leftSection={<LocalIcon icon={buttonIcon} width="0.9rem" height="0.9rem" />}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
</Group>
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
color="gray"
|
||||
size="sm"
|
||||
onClick={onDismiss}
|
||||
aria-label="Dismiss"
|
||||
style={{ position: 'absolute', top: '50%', right: '0.5rem', transform: 'translateY(-50%)' }}
|
||||
>
|
||||
<LocalIcon icon="close-rounded" width="1rem" height="1rem" />
|
||||
</ActionIcon>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
26
frontend/src/core/contexts/BannerContext.tsx
Normal file
26
frontend/src/core/contexts/BannerContext.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { createContext, useContext, useState, ReactNode } from 'react';
|
||||
|
||||
interface BannerContextType {
|
||||
banner: ReactNode;
|
||||
setBanner: (banner: ReactNode) => void;
|
||||
}
|
||||
|
||||
const BannerContext = createContext<BannerContextType | undefined>(undefined);
|
||||
|
||||
export function BannerProvider({ children }: { children: ReactNode }) {
|
||||
const [banner, setBanner] = useState<ReactNode>(null);
|
||||
|
||||
return (
|
||||
<BannerContext.Provider value={{ banner, setBanner }}>
|
||||
{children}
|
||||
</BannerContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useBanner() {
|
||||
const context = useContext(BannerContext);
|
||||
if (!context) {
|
||||
throw new Error('useBanner must be used within BannerProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { ReactNode } from "react";
|
||||
import { AppProviders as ProprietaryAppProviders } from "@proprietary/components/AppProviders";
|
||||
import { DesktopConfigSync } from '@app/components/DesktopConfigSync';
|
||||
import { DesktopBannerInitializer } from '@app/components/DesktopBannerInitializer';
|
||||
import { DESKTOP_DEFAULT_APP_CONFIG } from '@app/config/defaultAppConfig';
|
||||
|
||||
/**
|
||||
@ -22,6 +23,7 @@ export function AppProviders({ children }: { children: ReactNode }) {
|
||||
}}
|
||||
>
|
||||
<DesktopConfigSync />
|
||||
<DesktopBannerInitializer />
|
||||
{children}
|
||||
</ProprietaryAppProviders>
|
||||
);
|
||||
|
||||
13
frontend/src/desktop/components/DesktopBannerInitializer.tsx
Normal file
13
frontend/src/desktop/components/DesktopBannerInitializer.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useBanner } from '@app/contexts/BannerContext';
|
||||
import { DefaultAppBanner } from '@app/components/shared/DefaultAppBanner';
|
||||
|
||||
export function DesktopBannerInitializer() {
|
||||
const { setBanner } = useBanner();
|
||||
|
||||
useEffect(() => {
|
||||
setBanner(<DefaultAppBanner />);
|
||||
}, [setBanner]);
|
||||
|
||||
return null;
|
||||
}
|
||||
73
frontend/src/desktop/components/shared/DefaultAppBanner.tsx
Normal file
73
frontend/src/desktop/components/shared/DefaultAppBanner.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InfoBanner } from '@app/components/shared/InfoBanner';
|
||||
import { defaultAppService } from '@app/services/defaultAppService';
|
||||
import { alert } from '@app/components/toast';
|
||||
|
||||
export const DefaultAppBanner: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isDefault, setIsDefault] = useState<boolean | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkDefaultStatus();
|
||||
}, []);
|
||||
|
||||
const checkDefaultStatus = async () => {
|
||||
try {
|
||||
const status = await defaultAppService.isDefaultPdfHandler();
|
||||
setIsDefault(status);
|
||||
} catch (error) {
|
||||
console.error('Failed to check default status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefault = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await defaultAppService.setAsDefaultPdfHandler();
|
||||
|
||||
if (result === 'set_successfully') {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('defaultApp.success.title', 'Default App Set'),
|
||||
body: t('defaultApp.success.message', 'Stirling PDF is now your default PDF editor'),
|
||||
});
|
||||
setIsDefault(true);
|
||||
} else if (result === 'opened_settings') {
|
||||
alert({
|
||||
alertType: 'neutral',
|
||||
title: t('defaultApp.settingsOpened.title', 'Settings Opened'),
|
||||
body: t('defaultApp.settingsOpened.message', 'Please select Stirling PDF in your system settings'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set default:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('defaultApp.error.title', 'Error'),
|
||||
body: t('defaultApp.error.message', 'Failed to set default PDF handler'),
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDismissPrompt = () => {
|
||||
setDismissed(true);
|
||||
};
|
||||
|
||||
return (
|
||||
<InfoBanner
|
||||
icon="picture-as-pdf-rounded"
|
||||
message={t('defaultApp.prompt.message', 'Make Stirling PDF your default application for opening PDF files.')}
|
||||
buttonText={t('defaultApp.setDefault', 'Set Default')}
|
||||
buttonIcon="check-circle-rounded"
|
||||
onButtonClick={handleSetDefault}
|
||||
onDismiss={handleDismissPrompt}
|
||||
loading={isLoading}
|
||||
show={!dismissed && isDefault === false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,86 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Paper, Text, Button, Group } from '@mantine/core';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { defaultAppService } from '@app/services/defaultAppService';
|
||||
import { alert } from '@app/components/toast';
|
||||
|
||||
export const DefaultAppSettings: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [isDefault, setIsDefault] = useState<boolean | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
checkDefaultStatus();
|
||||
}, []);
|
||||
|
||||
const checkDefaultStatus = async () => {
|
||||
try {
|
||||
const status = await defaultAppService.isDefaultPdfHandler();
|
||||
setIsDefault(status);
|
||||
} catch (error) {
|
||||
console.error('Failed to check default status:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefault = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await defaultAppService.setAsDefaultPdfHandler();
|
||||
|
||||
if (result === 'set_successfully') {
|
||||
alert({
|
||||
alertType: 'success',
|
||||
title: t('defaultApp.success.title', 'Default App Set'),
|
||||
body: t('defaultApp.success.message', 'Stirling PDF is now your default PDF editor'),
|
||||
});
|
||||
setIsDefault(true);
|
||||
} else if (result === 'opened_settings') {
|
||||
alert({
|
||||
alertType: 'neutral',
|
||||
title: t('defaultApp.settingsOpened.title', 'Settings Opened'),
|
||||
body: t('defaultApp.settingsOpened.message', 'Please select Stirling PDF in your system settings'),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to set default:', error);
|
||||
alert({
|
||||
alertType: 'error',
|
||||
title: t('defaultApp.error.title', 'Error'),
|
||||
body: t('defaultApp.error.message', 'Failed to set default PDF handler'),
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper withBorder p="md" radius="md">
|
||||
<Group justify="space-between" align="center">
|
||||
<div>
|
||||
<Text fw={500} size="sm">
|
||||
{t('settings.general.defaultPdfEditor', 'Default PDF editor')}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed" mt={4}>
|
||||
{isDefault === true
|
||||
? t('settings.general.defaultPdfEditorActive', 'Stirling PDF is your default PDF editor')
|
||||
: isDefault === false
|
||||
? t('settings.general.defaultPdfEditorInactive', 'Another application is set as default')
|
||||
: t('settings.general.defaultPdfEditorChecking', 'Checking...')}
|
||||
</Text>
|
||||
</div>
|
||||
<Button
|
||||
variant={isDefault ? 'light' : 'filled'}
|
||||
color="blue"
|
||||
size="sm"
|
||||
onClick={handleSetDefault}
|
||||
loading={isLoading}
|
||||
disabled={isDefault === true}
|
||||
>
|
||||
{isDefault
|
||||
? t('settings.general.defaultPdfEditorSet', 'Already Default')
|
||||
: t('settings.general.setAsDefault', 'Set as Default')}
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
import { Stack } from '@mantine/core';
|
||||
import CoreGeneralSection from '@core/components/shared/config/configSections/GeneralSection';
|
||||
import { DefaultAppSettings } from '@app/components/shared/config/configSections/DefaultAppSettings';
|
||||
|
||||
/**
|
||||
* Desktop extension of GeneralSection that adds default PDF editor settings
|
||||
*/
|
||||
const GeneralSection: React.FC = () => {
|
||||
return (
|
||||
<Stack gap="lg">
|
||||
<DefaultAppSettings />
|
||||
<CoreGeneralSection />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default GeneralSection;
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
|
||||
import { useOpenedFile } from '@app/hooks/useOpenedFile';
|
||||
import { fileOpenService } from '@app/services/fileOpenService';
|
||||
@ -20,75 +20,42 @@ export function useAppInitialization(): void {
|
||||
// Handle files opened with app (Tauri mode)
|
||||
const { openedFilePaths, loading: openedFileLoading } = useOpenedFile();
|
||||
|
||||
// Track if we've already loaded the initial files to prevent duplicate loads
|
||||
const initialFilesLoadedRef = useRef(false);
|
||||
|
||||
// Load opened files and add directly to FileContext
|
||||
useEffect(() => {
|
||||
if (openedFilePaths.length > 0 && !openedFileLoading && !initialFilesLoadedRef.current) {
|
||||
initialFilesLoadedRef.current = true;
|
||||
|
||||
const loadOpenedFiles = async () => {
|
||||
try {
|
||||
const filesArray: File[] = [];
|
||||
|
||||
// Load all files in parallel
|
||||
await Promise.all(
|
||||
openedFilePaths.map(async (filePath) => {
|
||||
try {
|
||||
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
|
||||
if (fileData) {
|
||||
const file = new File([fileData.arrayBuffer], fileData.fileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
filesArray.push(file);
|
||||
console.log('[Desktop] Loaded file:', fileData.fileName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Desktop] Failed to load file:', filePath, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (filesArray.length > 0) {
|
||||
// Add all files to FileContext at once
|
||||
await addFiles(filesArray);
|
||||
console.log(`[Desktop] ${filesArray.length} opened file(s) added to FileContext`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Desktop] Failed to load opened files:', error);
|
||||
}
|
||||
};
|
||||
|
||||
loadOpenedFiles();
|
||||
if (openedFilePaths.length === 0 || openedFileLoading) {
|
||||
return;
|
||||
}
|
||||
}, [openedFilePaths, openedFileLoading, addFiles]);
|
||||
|
||||
// Listen for runtime file-opened events (from second instances on Windows/Linux)
|
||||
useEffect(() => {
|
||||
const handleRuntimeFileOpen = async (filePath: string) => {
|
||||
const loadOpenedFiles = async () => {
|
||||
try {
|
||||
console.log('[Desktop] Runtime file-opened event received:', filePath);
|
||||
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
|
||||
if (fileData) {
|
||||
// Create a File object from the ArrayBuffer
|
||||
const file = new File([fileData.arrayBuffer], fileData.fileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
const filesArray: File[] = [];
|
||||
|
||||
// Add directly to FileContext
|
||||
await addFiles([file]);
|
||||
console.log('[Desktop] Runtime opened file added to FileContext:', fileData.fileName);
|
||||
await Promise.all(
|
||||
openedFilePaths.map(async (filePath) => {
|
||||
try {
|
||||
const fileData = await fileOpenService.readFileAsArrayBuffer(filePath);
|
||||
if (fileData) {
|
||||
const file = new File([fileData.arrayBuffer], fileData.fileName, {
|
||||
type: 'application/pdf'
|
||||
});
|
||||
filesArray.push(file);
|
||||
console.log('[Desktop] Loaded file:', fileData.fileName);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Desktop] Failed to load file:', filePath, error);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
if (filesArray.length > 0) {
|
||||
await addFiles(filesArray);
|
||||
console.log(`[Desktop] ${filesArray.length} opened file(s) added to FileContext`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Desktop] Failed to load runtime opened file:', error);
|
||||
console.error('[Desktop] Failed to load opened files:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Set up event listener and get cleanup function
|
||||
const unlisten = fileOpenService.onFileOpened(handleRuntimeFileOpen);
|
||||
|
||||
// Clean up listener on unmount
|
||||
return unlisten;
|
||||
}, [addFiles]);
|
||||
loadOpenedFiles();
|
||||
}, [openedFilePaths, openedFileLoading, addFiles]);
|
||||
}
|
||||
|
||||
@ -1,45 +1,46 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { fileOpenService } from '@app/services/fileOpenService';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
|
||||
export function useOpenedFile() {
|
||||
const [openedFilePaths, setOpenedFilePaths] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkForOpenedFile = async () => {
|
||||
console.log('🔍 Checking for opened file(s)...');
|
||||
// Function to read and process files from storage
|
||||
const readFilesFromStorage = async () => {
|
||||
console.log('🔍 Reading files from storage...');
|
||||
try {
|
||||
const filePaths = await fileOpenService.getOpenedFiles();
|
||||
console.log('🔍 fileOpenService.getOpenedFiles() returned:', filePaths);
|
||||
|
||||
if (filePaths.length > 0) {
|
||||
console.log(`✅ App opened with ${filePaths.length} file(s):`, filePaths);
|
||||
console.log(`✅ Found ${filePaths.length} file(s) in storage:`, filePaths);
|
||||
setOpenedFilePaths(filePaths);
|
||||
|
||||
// Clear the files from service state after consuming them
|
||||
await fileOpenService.clearOpenedFiles();
|
||||
} else {
|
||||
console.log('ℹ️ No files were opened with the app');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to check for opened files:', error);
|
||||
console.error('❌ Failed to read files from storage:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
checkForOpenedFile();
|
||||
// Read files on mount
|
||||
readFilesFromStorage();
|
||||
|
||||
// Listen for runtime file open events (abstracted through service)
|
||||
const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath: string) => {
|
||||
console.log('📂 Runtime file open event:', filePath);
|
||||
setOpenedFilePaths(prev => [...prev, filePath]);
|
||||
// Listen for files-changed events (when new files are added to storage)
|
||||
let unlisten: (() => void) | undefined;
|
||||
listen('files-changed', async () => {
|
||||
console.log('📂 files-changed event received, re-reading storage...');
|
||||
await readFilesFromStorage();
|
||||
}).then(unlistenFn => {
|
||||
unlisten = unlistenFn;
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
unlistenRuntimeEvents();
|
||||
if (unlisten) unlisten();
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
70
frontend/src/desktop/services/defaultAppService.ts
Normal file
70
frontend/src/desktop/services/defaultAppService.ts
Normal file
@ -0,0 +1,70 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
/**
|
||||
* Service for managing default PDF handler settings
|
||||
* Note: Uses localStorage for machine-specific preferences (not synced to server)
|
||||
*/
|
||||
export const defaultAppService = {
|
||||
/**
|
||||
* Check if Stirling PDF is the default PDF handler
|
||||
*/
|
||||
async isDefaultPdfHandler(): Promise<boolean> {
|
||||
try {
|
||||
const result = await invoke<boolean>('is_default_pdf_handler');
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('[DefaultApp] Failed to check default handler:', error);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Set or prompt to set Stirling PDF as default PDF handler
|
||||
* Returns a status string indicating what happened
|
||||
*/
|
||||
async setAsDefaultPdfHandler(): Promise<'set_successfully' | 'opened_settings' | 'error'> {
|
||||
try {
|
||||
const result = await invoke<string>('set_as_default_pdf_handler');
|
||||
return result as 'set_successfully' | 'opened_settings';
|
||||
} catch (error) {
|
||||
console.error('[DefaultApp] Failed to set default handler:', error);
|
||||
return 'error';
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if user has dismissed the default app prompt (machine-specific)
|
||||
*/
|
||||
hasUserDismissedPrompt(): boolean {
|
||||
try {
|
||||
const dismissed = localStorage.getItem('stirlingpdf_default_app_prompt_dismissed');
|
||||
return dismissed === 'true';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Mark that user has dismissed the default app prompt (machine-specific)
|
||||
*/
|
||||
setPromptDismissed(dismissed: boolean): void {
|
||||
try {
|
||||
localStorage.setItem('stirlingpdf_default_app_prompt_dismissed', dismissed ? 'true' : 'false');
|
||||
} catch (error) {
|
||||
console.error('[DefaultApp] Failed to save prompt preference:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if we should show the default app prompt
|
||||
* Returns true if: user hasn't dismissed it AND app is not default handler
|
||||
*/
|
||||
async shouldShowPrompt(): Promise<boolean> {
|
||||
if (this.hasUserDismissedPrompt()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isDefault = await this.isDefaultPdfHandler();
|
||||
return !isDefault;
|
||||
},
|
||||
};
|
||||
@ -1,6 +1,7 @@
|
||||
import { Suspense } from "react";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { AppProviders } from "@app/components/AppProviders";
|
||||
import { AppLayout } from "@app/components/AppLayout";
|
||||
import { LoadingFallback } from "@app/components/shared/LoadingFallback";
|
||||
import Landing from "@app/routes/Landing";
|
||||
import Login from "@app/routes/Login";
|
||||
@ -22,17 +23,19 @@ export default function App() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<AppProviders>
|
||||
<Routes>
|
||||
{/* Auth routes - no nested providers needed */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="/invite/:token" element={<InviteAccept />} />
|
||||
<AppLayout>
|
||||
<Routes>
|
||||
{/* Auth routes - no nested providers needed */}
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/signup" element={<Signup />} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route path="/invite/:token" element={<InviteAccept />} />
|
||||
|
||||
{/* Main app routes - Landing handles auth logic */}
|
||||
<Route path="/*" element={<Landing />} />
|
||||
</Routes>
|
||||
<OnboardingTour />
|
||||
{/* Main app routes - Landing handles auth logic */}
|
||||
<Route path="/*" element={<Landing />} />
|
||||
</Routes>
|
||||
<OnboardingTour />
|
||||
</AppLayout>
|
||||
</AppProviders>
|
||||
</Suspense>
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user