Compare commits

..
Author SHA1 Message Date
Anthony StirlingandGitHub e2e01366b8 Enable withCredentials for API client 2025-11-17 14:02:23 +00:00
46 changed files with 244 additions and 1098 deletions
+1 -33
View File
@@ -41,30 +41,6 @@
"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"
},
@@ -353,18 +329,10 @@
"autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
"defaultToolPickerMode": "Default tool picker mode",
"defaultToolPickerModeDescription": "Choose whether the tool picker opens in fullscreen or sidebar by default",
"rememberInputs": "Remember tool inputs",
"rememberInputsDescription": "Store the last settings you used for each tool on this device",
"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",
@@ -323,8 +323,6 @@
"autoUnzipFileLimitTooltip": "Only unzip if the ZIP contains this many files or fewer. Set higher to extract larger ZIPs.",
"defaultToolPickerMode": "Default tool picker mode",
"defaultToolPickerModeDescription": "Choose whether the tool picker opens in fullscreen or sidebar by default",
"rememberInputs": "Remember tool inputs",
"rememberInputsDescription": "Store the last settings you used for each tool on this device",
"mode": {
"fullscreen": "Fullscreen",
"sidebar": "Sidebar"
-11
View File
@@ -643,15 +643,6 @@ 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"
@@ -3950,8 +3941,6 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
name = "stirling-pdf"
version = "0.1.0"
dependencies = [
"core-foundation 0.10.1",
"core-services",
"log",
"reqwest 0.11.27",
"serde",
-4
View File
@@ -31,7 +31,3 @@ 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"
@@ -1,217 +0,0 @@
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))
}
}
+17 -5
View File
@@ -14,11 +14,23 @@ 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> {
// 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();
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());
}
add_log(format!("📂 Returning {} opened file(s)", all_files.len()));
Ok(all_files)
-2
View File
@@ -1,9 +1,7 @@
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};
+7 -38
View File
@@ -3,16 +3,7 @@ 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,
is_default_pdf_handler,
set_as_default_pdf_handler,
};
use commands::{start_backend, check_backend_health, get_opened_files, clear_opened_files, cleanup_backend, add_opened_file};
use utils::{add_log, get_tauri_logs};
#[cfg_attr(mobile, tauri::mobile_entry_point)]
@@ -32,6 +23,9 @@ 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();
@@ -39,34 +33,13 @@ 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,
is_default_pdf_handler,
set_as_default_pdf_handler,
])
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_files, clear_opened_files, get_tauri_logs])
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|app_handle, event| {
@@ -85,7 +58,6 @@ 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://") {
@@ -93,14 +65,11 @@ pub fn run() {
if file_path.ends_with(".pdf") {
add_log(format!("📂 Processing opened PDF: {}", file_path));
add_opened_file(file_path.to_string());
added_files = true;
// Use unified event name for consistency across platforms
let _ = app_handle.emit("file-opened", file_path.to_string());
}
}
}
// 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
+2 -5
View File
@@ -1,6 +1,5 @@
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";
@@ -17,10 +16,8 @@ export default function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<AppProviders>
<AppLayout>
<HomePage />
<OnboardingTour />
</AppLayout>
<HomePage />
<OnboardingTour />
</AppProviders>
</Suspense>
);
@@ -1,31 +0,0 @@
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>
</>
);
}
+26 -29
View File
@@ -16,7 +16,6 @@ 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";
@@ -51,23 +50,22 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
<PreferencesProvider>
<RainbowThemeProvider>
<ErrorBoundary>
<BannerProvider>
<OnboardingProvider>
<AppConfigProvider
retryOptions={appConfigRetryOptions}
{...appConfigProviderProps}
>
<ScarfTrackingInitializer />
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<AppInitializer />
<ToolRegistryProvider>
<NavigationProvider>
<FilesModalProvider>
<ToolWorkflowProvider>
<HotkeyProvider>
<SidebarProvider>
<ViewerProvider>
<PageEditorProvider>
<OnboardingProvider>
<AppConfigProvider
retryOptions={appConfigRetryOptions}
{...appConfigProviderProps}
>
<ScarfTrackingInitializer />
<FileContextProvider enableUrlSync={true} enablePersistence={true}>
<AppInitializer />
<ToolRegistryProvider>
<NavigationProvider>
<FilesModalProvider>
<ToolWorkflowProvider>
<HotkeyProvider>
<SidebarProvider>
<ViewerProvider>
<PageEditorProvider>
<SignatureProvider>
<RightRailProvider>
<TourOrchestrationProvider>
@@ -78,17 +76,16 @@ export function AppProviders({ children, appConfigRetryOptions, appConfigProvide
</RightRailProvider>
</SignatureProvider>
</PageEditorProvider>
</ViewerProvider>
</SidebarProvider>
</HotkeyProvider>
</ToolWorkflowProvider>
</FilesModalProvider>
</NavigationProvider>
</ToolRegistryProvider>
</FileContextProvider>
</AppConfigProvider>
</OnboardingProvider>
</BannerProvider>
</ViewerProvider>
</SidebarProvider>
</HotkeyProvider>
</ToolWorkflowProvider>
</FilesModalProvider>
</NavigationProvider>
</ToolRegistryProvider>
</FileContextProvider>
</AppConfigProvider>
</OnboardingProvider>
</ErrorBoundary>
</RainbowThemeProvider>
</PreferencesProvider>
@@ -1,72 +0,0 @@
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>
);
};
@@ -114,23 +114,6 @@ const GeneralSection: React.FC<GeneralSectionProps> = ({ hideTitle = false }) =>
]}
/>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
<div>
<Text fw={500} size="sm">
{t('settings.general.rememberInputs', 'Remember tool inputs')}
</Text>
<Text size="xs" c="dimmed" mt={4}>
{t(
'settings.general.rememberInputsDescription',
'Store the last settings you used for each tool on this device'
)}
</Text>
</div>
<Switch
checked={preferences.rememberFormInputs}
onChange={(event) => updatePreference('rememberFormInputs', event.currentTarget.checked)}
/>
</div>
<Tooltip
label={t('settings.general.autoUnzipTooltip', 'Automatically extract ZIP files returned from API operations. Disable to keep ZIP files intact. This does not affect automation workflows.')}
multiline
@@ -1,26 +0,0 @@
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;
}
@@ -2,13 +2,10 @@ import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useAddPasswordParameters, defaultParameters, AddPasswordParametersHook } from '@app/hooks/tools/addPassword/useAddPasswordParameters';
import { defaultParameters as defaultChangePermissionsParameters, ChangePermissionsParameters } from '@app/hooks/tools/changePermissions/useChangePermissionsParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderAddPasswordHook = () => renderHook(() => useAddPasswordParameters(), { wrapper: PreferencesTestWrapper });
describe('useAddPasswordParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -18,7 +15,7 @@ describe('useAddPasswordParameters', () => {
{ paramName: 'ownerPassword' as const, value: 'owner-password' },
{ paramName: 'keyLength' as const, value: 256 }
])('should update parameter $paramName', ({ paramName, value }) => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
act(() => {
result.current.updateParameter(paramName, value);
@@ -31,7 +28,7 @@ describe('useAddPasswordParameters', () => {
{ paramName: 'preventAssembly' as const },
{ paramName: 'preventPrinting' as const }
])('should update boolean permission parameter $paramName', ({ paramName }) => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
act(() => {
result.current.permissions.updateParameter(paramName, true);
@@ -41,7 +38,7 @@ describe('useAddPasswordParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
// First, change some parameters
act(() => {
@@ -63,7 +60,7 @@ describe('useAddPasswordParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
expect(result.current.getEndpointName()).toBe('add-password');
});
@@ -115,7 +112,7 @@ describe('useAddPasswordParameters', () => {
}
}
])('should validate parameters correctly $description', ({ setup }) => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
// Default state should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -129,7 +126,7 @@ describe('useAddPasswordParameters', () => {
});
test.each(Object.keys(defaultChangePermissionsParameters) as Array<keyof ChangePermissionsParameters>)('should handle boolean restriction parameter %s', (param) => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
act(() => {
result.current.resetParameters();
@@ -140,7 +137,7 @@ describe('useAddPasswordParameters', () => {
});
test('should handle mixed parameter types in updateParameter', () => {
const { result } = renderAddPasswordHook();
const { result } = renderHook(() => useAddPasswordParameters());
act(() => {
result.current.updateParameter('password', 'test-string');
@@ -1,13 +1,10 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useAdjustPageScaleParameters, defaultParameters, PageSize, AdjustPageScaleParametersHook } from '@app/hooks/tools/adjustPageScale/useAdjustPageScaleParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderAdjustPageScaleHook = () => renderHook(() => useAdjustPageScaleParameters(), { wrapper: PreferencesTestWrapper });
describe('useAdjustPageScaleParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderAdjustPageScaleHook();
const { result } = renderHook(() => useAdjustPageScaleParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
expect(result.current.parameters.scaleFactor).toBe(1.0);
@@ -22,7 +19,7 @@ describe('useAdjustPageScaleParameters', () => {
{ paramName: 'pageSize' as const, value: PageSize.LETTER },
{ paramName: 'pageSize' as const, value: PageSize.LEGAL },
])('should update parameter $paramName to $value', ({ paramName, value }) => {
const { result } = renderAdjustPageScaleHook();
const { result } = renderHook(() => useAdjustPageScaleParameters());
act(() => {
result.current.updateParameter(paramName, value);
@@ -32,7 +29,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderAdjustPageScaleHook();
const { result } = renderHook(() => useAdjustPageScaleParameters());
// First, change some parameters
act(() => {
@@ -52,7 +49,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderAdjustPageScaleHook();
const { result } = renderHook(() => useAdjustPageScaleParameters());
expect(result.current.getEndpointName()).toBe('scale-pages');
});
@@ -99,7 +96,7 @@ describe('useAdjustPageScaleParameters', () => {
expected: false
}
])('should validate parameters correctly $description', ({ setup, expected }) => {
const { result } = renderAdjustPageScaleHook();
const { result } = renderHook(() => useAdjustPageScaleParameters());
act(() => {
setup(result.current);
@@ -109,7 +106,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should handle all PageSize enum values', () => {
const { result } = renderAdjustPageScaleHook();
const { result } = renderHook(() => useAdjustPageScaleParameters());
Object.values(PageSize).forEach(pageSize => {
act(() => {
@@ -122,7 +119,7 @@ describe('useAdjustPageScaleParameters', () => {
});
test('should handle scale factor edge cases', () => {
const { result } = renderAdjustPageScaleHook();
const { result } = renderHook(() => useAdjustPageScaleParameters());
// Test very small valid scale factor
act(() => {
@@ -2,13 +2,10 @@ import { renderHook, act } from '@testing-library/react';
import { defaultParameters, useChangeMetadataParameters } from '@app/hooks/tools/changeMetadata/useChangeMetadataParameters';
import { TrappedStatus } from '@app/types/metadata';
import { describe, expect, test } from 'vitest';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderChangeMetadataHook = () => renderHook(() => useChangeMetadataParameters(), { wrapper: PreferencesTestWrapper });
describe('useChangeMetadataParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -26,7 +23,7 @@ describe('useChangeMetadataParameters', () => {
{ paramName: 'trapped', value: TrappedStatus.TRUE },
{ paramName: 'deleteAll', value: true },
] as const)('should update $paramName parameter', ({ paramName, value }) => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
act(() => {
result.current.updateParameter(paramName, value);
@@ -51,7 +48,7 @@ describe('useChangeMetadataParameters', () => {
{ description: 'no meaningful content', updates: {}, expected: false },
{ description: 'whitespace only', updates: { title: ' ', author: ' ' }, expected: false },
])('should validate correctly when $description', ({ updates, expected }) => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
act(() => {
Object.entries(updates).forEach(([key, value]) => {
@@ -67,7 +64,7 @@ describe('useChangeMetadataParameters', () => {
{ description: 'valid modification date', updates: { title: 'Test', modificationDate: new Date('2025/01/17 14:30:00') }, expected: true },
{ description: 'empty dates are valid', updates: { title: 'Test', creationDate: null, modificationDate: null }, expected: true },
])('should validate dates correctly with $description', ({ updates, expected }) => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
act(() => {
Object.entries(updates).forEach(([key, value]) => {
@@ -81,7 +78,7 @@ describe('useChangeMetadataParameters', () => {
describe('custom metadata', () => {
test('should add custom metadata with sequential IDs', () => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
act(() => {
result.current.addCustomMetadata();
@@ -96,7 +93,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should remove custom metadata by ID', () => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
act(() => {
result.current.addCustomMetadata();
@@ -112,7 +109,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should update custom metadata by ID', () => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
act(() => {
result.current.addCustomMetadata();
@@ -132,7 +129,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should validate with custom metadata', () => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
act(() => {
result.current.addCustomMetadata();
@@ -148,7 +145,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should generate unique IDs for multiple custom entries', () => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
for (let i = 0; i < 3; i++) {
act(() => {
@@ -164,7 +161,7 @@ describe('useChangeMetadataParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderChangeMetadataHook();
const { result } = renderHook(() => useChangeMetadataParameters());
expect(result.current.getEndpointName()).toBe('update-metadata');
});
@@ -1,19 +1,16 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useChangePermissionsParameters, defaultParameters, ChangePermissionsParameters } from '@app/hooks/tools/changePermissions/useChangePermissionsParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderChangePermissionsHook = () => renderHook(() => useChangePermissionsParameters(), { wrapper: PreferencesTestWrapper });
describe('useChangePermissionsParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderChangePermissionsHook();
const { result } = renderHook(() => useChangePermissionsParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should update individual boolean parameters', () => {
const { result } = renderChangePermissionsHook();
const { result } = renderHook(() => useChangePermissionsParameters());
act(() => {
result.current.updateParameter('preventAssembly', true);
@@ -31,7 +28,7 @@ describe('useChangePermissionsParameters', () => {
});
test('should update all permission parameters', () => {
const { result } = renderChangePermissionsHook();
const { result } = renderHook(() => useChangePermissionsParameters());
const permissionKeys = Object.keys(defaultParameters) as Array<keyof ChangePermissionsParameters>;
@@ -59,7 +56,7 @@ describe('useChangePermissionsParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderChangePermissionsHook();
const { result } = renderHook(() => useChangePermissionsParameters());
// First, change some parameters
act(() => {
@@ -81,13 +78,13 @@ describe('useChangePermissionsParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderChangePermissionsHook();
const { result } = renderHook(() => useChangePermissionsParameters());
expect(result.current.getEndpointName()).toBe('add-password');
});
test('should always validate as true', () => {
const { result } = renderChangePermissionsHook();
const { result } = renderHook(() => useChangePermissionsParameters());
// Default state should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -6,16 +6,13 @@ import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useConvertParameters } from '@app/hooks/tools/convert/useConvertParameters';
import { FIT_OPTIONS } from '@app/constants/convertConstants';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderConvertHook = () => renderHook(() => useConvertParameters(), { wrapper: PreferencesTestWrapper });
describe('useConvertParameters', () => {
describe('Parameter Management', () => {
test('should initialize with default parameters', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
expect(result.current.parameters.fromExtension).toBe('');
expect(result.current.parameters.toExtension).toBe('');
@@ -31,7 +28,7 @@ describe('useConvertParameters', () => {
});
test('should update individual parameters', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -42,7 +39,7 @@ describe('useConvertParameters', () => {
});
test('should update nested image options', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('imageOptions', {
@@ -64,7 +61,7 @@ describe('useConvertParameters', () => {
});
test('should update nested HTML options', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('htmlOptions', {
@@ -76,7 +73,7 @@ describe('useConvertParameters', () => {
});
test('should update nested email options', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('emailOptions', {
@@ -94,7 +91,7 @@ describe('useConvertParameters', () => {
});
test('should update nested PDF/A options', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('pdfaOptions', {
@@ -106,7 +103,7 @@ describe('useConvertParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -127,7 +124,7 @@ describe('useConvertParameters', () => {
describe('Parameter Validation', () => {
test('should validate parameters correctly', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
// No parameters - should be invalid
expect(result.current.validateParameters()).toBe(false);
@@ -146,7 +143,7 @@ describe('useConvertParameters', () => {
});
test('should validate unsupported conversions', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -161,7 +158,7 @@ describe('useConvertParameters', () => {
describe('Endpoint Generation', () => {
test('should generate correct endpoint names', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -173,7 +170,7 @@ describe('useConvertParameters', () => {
});
test('should generate correct endpoint URLs', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'pdf');
@@ -185,7 +182,7 @@ describe('useConvertParameters', () => {
});
test('should return empty strings for invalid conversions', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
act(() => {
result.current.updateParameter('fromExtension', 'invalid');
@@ -200,7 +197,7 @@ describe('useConvertParameters', () => {
describe('Available Extensions', () => {
test('should return available extensions for valid source format', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('pdf');
@@ -210,7 +207,7 @@ describe('useConvertParameters', () => {
});
test('should return empty array for invalid source format', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('invalid');
@@ -222,7 +219,7 @@ describe('useConvertParameters', () => {
});
test('should return empty array for empty source format', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const availableExtensions = result.current.getAvailableToExtensions('');
@@ -137,7 +137,6 @@ export const useConvertParameters = (): ConvertParametersHook => {
defaultParameters,
endpointName: getEndpointName,
validateFn: validateParameters,
storageKey: 'convert',
}), []);
const baseHook = useBaseParameters(config);
@@ -6,16 +6,13 @@
import { describe, test, expect } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useConvertParameters } from '@app/hooks/tools/convert/useConvertParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderConvertHook = () => renderHook(() => useConvertParameters(), { wrapper: PreferencesTestWrapper });
describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Single File Detection', () => {
test('should detect single file extension and set auto-target', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const pdfFile = [{ name: 'document.pdf' }];
@@ -30,7 +27,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle unknown file types with file-to-pdf fallback', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const unknownFile = [{ name: 'document.xyz' }, { name: 'image.jpggg' }];
@@ -44,7 +41,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle files without extensions', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const noExtFile = [{ name: 'document' }];
@@ -62,7 +59,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Multiple Identical Files', () => {
test('should detect multiple PDF files and set auto-target', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const pdfFiles = [
{ name: 'doc1.pdf' },
@@ -81,7 +78,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle multiple unknown file types with fallback', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const unknownFiles = [
{ name: 'file1.xyz' },
@@ -101,7 +98,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection - All Images', () => {
test('should detect all image files and enable smart detection', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const imageFiles = [
{ name: 'photo1.jpg' },
@@ -120,7 +117,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle mixed case image extensions', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const imageFiles = [
{ name: 'photo1.JPG' },
@@ -139,7 +136,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection - All Web Files', () => {
test('should detect all web files and enable web smart detection', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const webFiles = [
{ name: 'page1.html' },
@@ -157,7 +154,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle mixed case web extensions', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const webFiles = [
{ name: 'page1.HTML' },
@@ -173,7 +170,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should detect multiple web files and enable web smart detection', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const zipFiles = [
{ name: 'site1.zip' },
@@ -194,7 +191,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection - Mixed File Types', () => {
test('should detect mixed file types and enable smart detection', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const mixedFiles = [
{ name: 'document.pdf' },
@@ -213,7 +210,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should detect mixed images and documents as mixed type', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const mixedFiles = [
{ name: 'photo.jpg' },
@@ -230,7 +227,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle mixed with unknown file types', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const mixedFiles = [
{ name: 'document.pdf' },
@@ -250,7 +247,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Smart Detection Endpoint Resolution', () => {
test('should return correct endpoint for image smart detection', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const imageFiles = [
{ name: 'photo1.jpg' },
@@ -266,7 +263,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should return correct endpoint for web smart detection', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const webFiles = [
{ name: 'page1.html' },
@@ -282,7 +279,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should return correct endpoint for mixed smart detection', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const mixedFiles = [
{ name: 'document.pdf' },
@@ -301,7 +298,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Auto-Target Selection Logic', () => {
test('should select single available target automatically', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
// Markdown has only one conversion target (PDF)
const mdFile = [{ name: 'readme.md' }];
@@ -315,7 +312,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should not auto-select when multiple targets available', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
// PDF has multiple conversion targets, so no auto-selection
const pdfFile = [{ name: 'document.pdf' }];
@@ -333,7 +330,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
describe('Edge Cases', () => {
test('should handle empty file names', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const emptyFiles = [{ name: '' }];
@@ -346,7 +343,7 @@ describe('useConvertParameters - Auto Detection & Smart Conversion', () => {
});
test('should handle malformed file objects', () => {
const { result } = renderConvertHook();
const { result } = renderHook(() => useConvertParameters());
const malformedFiles: Array<{name: string}> = [
{ name: 'valid.pdf' },
@@ -1,13 +1,10 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useMergeParameters, defaultParameters } from '@app/hooks/tools/merge/useMergeParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderMergeHook = () => renderHook(() => useMergeParameters(), { wrapper: PreferencesTestWrapper });
describe('useMergeParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderMergeHook();
const { result } = renderHook(() => useMergeParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -18,7 +15,7 @@ describe('useMergeParameters', () => {
{ paramName: 'generateTableOfContents' as const, value: true },
{ paramName: 'generateTableOfContents' as const, value: false }
])('should update parameter $paramName to $value', ({ paramName, value }) => {
const { result } = renderMergeHook();
const { result } = renderHook(() => useMergeParameters());
act(() => {
result.current.updateParameter(paramName, value);
@@ -28,7 +25,7 @@ describe('useMergeParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderMergeHook();
const { result } = renderHook(() => useMergeParameters());
// First, change some parameters
act(() => {
@@ -48,7 +45,7 @@ describe('useMergeParameters', () => {
});
test('should validate parameters correctly - always returns true', () => {
const { result } = renderMergeHook();
const { result } = renderHook(() => useMergeParameters());
// Default state should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -1,13 +1,10 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRedactParameters, defaultParameters } from '@app/hooks/tools/redact/useRedactParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderRedactHook = () => renderHook(() => useRedactParameters(), { wrapper: PreferencesTestWrapper });
describe('useRedactParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
@@ -21,7 +18,7 @@ describe('useRedactParameters', () => {
{ paramName: 'customPadding' as const, value: 0.5 },
{ paramName: 'convertPDFToImage' as const, value: false }
])('should update parameter $paramName', ({ paramName, value }) => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter(paramName, value);
@@ -31,7 +28,7 @@ describe('useRedactParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
// Modify some parameters
act(() => {
@@ -55,7 +52,7 @@ describe('useRedactParameters', () => {
{ description: 'not validate when wordsToRedact contains only empty strings in automatic mode', wordsToRedact: ['', ' ', ''], expected: false },
{ description: 'validate when wordsToRedact contains at least one non-empty word in automatic mode', wordsToRedact: ['', 'valid', ' '], expected: true },
])('should $description', ({ wordsToRedact, expected }) => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'automatic');
@@ -66,7 +63,7 @@ describe('useRedactParameters', () => {
});
test('should not validate in manual mode (not implemented)', () => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'manual');
@@ -78,7 +75,7 @@ describe('useRedactParameters', () => {
describe('endpoint handling', () => {
test('should return correct endpoint for automatic mode', () => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'automatic');
@@ -88,7 +85,7 @@ describe('useRedactParameters', () => {
});
test('should throw error for manual mode (not implemented)', () => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('mode', 'manual');
@@ -99,7 +96,7 @@ describe('useRedactParameters', () => {
});
test('should maintain parameter state across updates', () => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('redactColor', '#FF0000');
@@ -120,7 +117,7 @@ describe('useRedactParameters', () => {
});
test('should handle array parameter updates correctly', () => {
const { result } = renderRedactHook();
const { result } = renderHook(() => useRedactParameters());
act(() => {
result.current.updateParameter('wordsToRedact', ['initial']);
@@ -43,7 +43,6 @@ export const useRedactParameters = (): RedactParametersHook => {
}
// Manual mode validation would go here when implemented
return false;
},
storageKey: 'redact'
}
});
};
@@ -1,19 +1,16 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRemovePasswordParameters, defaultParameters } from '@app/hooks/tools/removePassword/useRemovePasswordParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderRemovePasswordHook = () => renderHook(() => useRemovePasswordParameters(), { wrapper: PreferencesTestWrapper });
describe('useRemovePasswordParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderRemovePasswordHook();
const { result } = renderHook(() => useRemovePasswordParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should update password parameter', () => {
const { result } = renderRemovePasswordHook();
const { result } = renderHook(() => useRemovePasswordParameters());
act(() => {
result.current.updateParameter('password', 'test-password');
@@ -23,7 +20,7 @@ describe('useRemovePasswordParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderRemovePasswordHook();
const { result } = renderHook(() => useRemovePasswordParameters());
// First, change the password
act(() => {
@@ -41,7 +38,7 @@ describe('useRemovePasswordParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderRemovePasswordHook();
const { result } = renderHook(() => useRemovePasswordParameters());
expect(result.current.getEndpointName()).toBe('remove-password');
});
@@ -73,7 +70,7 @@ describe('useRemovePasswordParameters', () => {
expectedValid: true
}
])('should validate parameters correctly $description', ({ password, expectedValid }) => {
const { result } = renderRemovePasswordHook();
const { result } = renderHook(() => useRemovePasswordParameters());
act(() => {
result.current.updateParameter('password', password);
@@ -1,13 +1,10 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRotateParameters, defaultParameters, normalizeAngle } from '@app/hooks/tools/rotate/useRotateParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderRotateHook = () => renderHook(() => useRotateParameters(), { wrapper: PreferencesTestWrapper });
describe('useRotateParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
expect(result.current.parameters).toEqual(defaultParameters);
expect(result.current.parameters.angle).toBe(0);
@@ -15,7 +12,7 @@ describe('useRotateParameters', () => {
});
test('should validate parameters correctly', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
// Default should be valid
expect(result.current.validateParameters()).toBe(true);
@@ -34,7 +31,7 @@ describe('useRotateParameters', () => {
});
test('should rotate clockwise correctly', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
act(() => {
result.current.rotateClockwise();
@@ -61,7 +58,7 @@ describe('useRotateParameters', () => {
});
test('should rotate anticlockwise correctly', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
act(() => {
result.current.rotateAnticlockwise();
@@ -88,7 +85,7 @@ describe('useRotateParameters', () => {
});
test('should normalize angles correctly', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
expect(result.current.normalizeAngle(360)).toBe(0);
expect(result.current.normalizeAngle(450)).toBe(90);
@@ -97,7 +94,7 @@ describe('useRotateParameters', () => {
});
test('should reset parameters correctly', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
// Set some rotation
act(() => {
@@ -119,7 +116,7 @@ describe('useRotateParameters', () => {
});
test('should update parameters', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
act(() => {
result.current.updateParameter('angle', 450);
@@ -135,13 +132,13 @@ describe('useRotateParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
expect(result.current.getEndpointName()).toBe('rotate-pdf');
});
test('should detect rotation state correctly', () => {
const { result } = renderRotateHook();
const { result } = renderHook(() => useRotateParameters());
// Initially no rotation
expect(result.current.hasRotation).toBe(false);
@@ -1,19 +1,16 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { defaultParameters, useSanitizeParameters } from '@app/hooks/tools/sanitize/useSanitizeParameters';
import { PreferencesTestWrapper } from '@testing/preferencesTestWrapper';
const renderSanitizeHook = () => renderHook(() => useSanitizeParameters(), { wrapper: PreferencesTestWrapper });
describe('useSanitizeParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderSanitizeHook();
const { result } = renderHook(() => useSanitizeParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should update individual parameters', () => {
const { result } = renderSanitizeHook();
const { result } = renderHook(() => useSanitizeParameters());
act(() => {
result.current.updateParameter('removeXMPMetadata', true);
@@ -26,7 +23,7 @@ describe('useSanitizeParameters', () => {
});
test('should reset parameters to defaults', () => {
const { result } = renderSanitizeHook();
const { result } = renderHook(() => useSanitizeParameters());
// First, change some parameters
act(() => {
@@ -46,13 +43,13 @@ describe('useSanitizeParameters', () => {
});
test('should return correct endpoint name', () => {
const { result } = renderSanitizeHook();
const { result } = renderHook(() => useSanitizeParameters());
expect(result.current.getEndpointName()).toBe('sanitize-pdf');
});
test('should validate parameters correctly', () => {
const { result } = renderSanitizeHook();
const { result } = renderHook(() => useSanitizeParameters());
// Default state should be valid (has removeJavaScript and removeEmbeddedFiles enabled)
expect(result.current.validateParameters()).toBe(true);
@@ -74,7 +71,7 @@ describe('useSanitizeParameters', () => {
});
test('should handle all parameter types correctly', () => {
const { result } = renderSanitizeHook();
const { result } = renderHook(() => useSanitizeParameters());
const allParameters = Object.keys(defaultParameters) as (keyof typeof defaultParameters)[];
@@ -1,6 +1,4 @@
import { useState, useCallback, Dispatch, SetStateAction, useRef, useEffect } from 'react';
import { usePreferences } from '@app/contexts/PreferencesContext';
import { loadToolParameters, saveToolParameters } from '@app/services/toolParameterStorage';
import { useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface BaseParametersHook<T> {
parameters: T;
@@ -15,31 +13,10 @@ export interface BaseParametersConfig<T> {
defaultParameters: T;
endpointName: string | ((params: T) => string);
validateFn?: (params: T) => boolean;
storageKey?: string;
}
export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParametersHook<T> {
const { preferences } = usePreferences();
const storageKey = config.storageKey ?? (typeof config.endpointName === 'string' ? config.endpointName : undefined);
const shouldPersist = Boolean(storageKey && preferences.rememberFormInputs);
const hasHydratedFromStorage = useRef(false);
const mergeWithDefaults = useCallback((values?: Partial<T>): T => ({
...(config.defaultParameters as T),
...(values ?? {}),
}), [config.defaultParameters]);
const [parameters, setParameters] = useState<T>(() => {
if (shouldPersist && storageKey) {
const stored = loadToolParameters<T>(storageKey);
if (stored) {
hasHydratedFromStorage.current = true;
return mergeWithDefaults(stored);
}
}
return mergeWithDefaults();
});
const [parameters, setParameters] = useState<T>(config.defaultParameters);
const updateParameter = useCallback(<K extends keyof T>(parameter: K, value: T[K]) => {
setParameters(prev => ({
@@ -56,34 +33,6 @@ export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParam
return config.validateFn ? config.validateFn(parameters) : true;
}, [parameters, config.validateFn]);
useEffect(() => {
if (!storageKey) {
return;
}
if (!preferences.rememberFormInputs) {
hasHydratedFromStorage.current = false;
return;
}
if (hasHydratedFromStorage.current) {
return;
}
const stored = loadToolParameters<T>(storageKey);
if (stored) {
setParameters(mergeWithDefaults(stored));
}
hasHydratedFromStorage.current = true;
}, [mergeWithDefaults, preferences.rememberFormInputs, storageKey]);
useEffect(() => {
if (!storageKey || !preferences.rememberFormInputs) {
return;
}
saveToolParameters(storageKey, parameters);
}, [parameters, preferences.rememberFormInputs, storageKey]);
const endpointName = config.endpointName;
let getEndpointName: () => string;
if (typeof endpointName === "string") {
@@ -57,6 +57,5 @@ export const useSplitParameters = (): SplitParametersHook => {
return false;
}
},
storageKey: 'split',
});
};
@@ -9,7 +9,6 @@ export interface UserPreferences {
toolPanelModePromptSeen: boolean;
showLegacyToolDescriptions: boolean;
hasCompletedOnboarding: boolean;
rememberFormInputs: boolean;
}
export const DEFAULT_PREFERENCES: UserPreferences = {
@@ -20,7 +19,6 @@ export const DEFAULT_PREFERENCES: UserPreferences = {
toolPanelModePromptSeen: false,
showLegacyToolDescriptions: false,
hasCompletedOnboarding: false,
rememberFormInputs: false,
};
const STORAGE_KEY = 'stirlingpdf_preferences';
@@ -1,114 +0,0 @@
const STORAGE_KEY = 'stirlingpdf_tool_parameters';
type SerializableValue = string | number | boolean | null | SerializableValue[] | { [key: string]: SerializableValue };
const isFile = (value: unknown): value is File => typeof File !== 'undefined' && value instanceof File;
const isBlob = (value: unknown): value is Blob => typeof Blob !== 'undefined' && value instanceof Blob;
function sanitizeValue(value: unknown): SerializableValue | undefined {
if (value === null) {
return null;
}
const valueType = typeof value;
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean') {
if (valueType === 'number' && Number.isNaN(value)) {
return null;
}
return value as SerializableValue;
}
if (valueType === 'undefined' || valueType === 'function' || valueType === 'symbol' || valueType === 'bigint') {
return undefined;
}
if (Array.isArray(value)) {
const sanitizedArray = value
.map((item) => sanitizeValue(item))
.filter((item) => item !== undefined) as SerializableValue[];
return sanitizedArray;
}
if (isFile(value) || isBlob(value)) {
return undefined;
}
if (valueType === 'object' && value !== null) {
const entries = Object.entries(value as Record<string, unknown>);
const sanitizedEntries: Record<string, SerializableValue> = {};
for (const [key, entryValue] of entries) {
const sanitized = sanitizeValue(entryValue);
if (sanitized !== undefined) {
sanitizedEntries[key] = sanitized;
}
}
return sanitizedEntries;
}
return undefined;
}
function readStorage(): Record<string, SerializableValue> {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) {
return {};
}
const parsed = JSON.parse(raw);
if (typeof parsed === 'object' && parsed !== null) {
return parsed as Record<string, SerializableValue>;
}
} catch (error) {
console.error('[toolParameterStorage] Failed to read stored parameters', error);
}
return {};
}
function writeStorage(map: Record<string, SerializableValue>): void {
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(map));
} catch (error) {
console.error('[toolParameterStorage] Failed to write stored parameters', error);
}
}
export function loadToolParameters<T>(toolKey: string): Partial<T> | null {
const storage = readStorage();
const stored = storage[toolKey];
if (!stored || typeof stored !== 'object') {
return null;
}
return stored as Partial<T>;
}
export function saveToolParameters<T>(toolKey: string, parameters: T): void {
const sanitized = sanitizeValue(parameters);
if (sanitized === undefined) {
return;
}
const storage = readStorage();
storage[toolKey] = sanitized;
writeStorage(storage);
}
export function clearToolParameters(toolKey: string): void {
const storage = readStorage();
if (Object.prototype.hasOwnProperty.call(storage, toolKey)) {
delete storage[toolKey];
writeStorage(storage);
}
}
export function clearAllToolParameters(): void {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error('[toolParameterStorage] Failed to clear stored parameters', error);
}
}
@@ -1,7 +1,6 @@
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';
/**
@@ -23,7 +22,6 @@ export function AppProviders({ children }: { children: ReactNode }) {
}}
>
<DesktopConfigSync />
<DesktopBannerInitializer />
{children}
</ProprietaryAppProviders>
);
@@ -1,13 +0,0 @@
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;
}
@@ -1,27 +0,0 @@
import React, { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { InfoBanner } from '@app/components/shared/InfoBanner';
import { useDefaultApp } from '@app/hooks/useDefaultApp';
export const DefaultAppBanner: React.FC = () => {
const { t } = useTranslation();
const { isDefault, isLoading, handleSetDefault } = useDefaultApp();
const [dismissed, setDismissed] = useState(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}
/>
);
};
@@ -1,40 +0,0 @@
import React from 'react';
import { Paper, Text, Button, Group } from '@mantine/core';
import { useTranslation } from 'react-i18next';
import { useDefaultApp } from '@app/hooks/useDefaultApp';
export const DefaultAppSettings: React.FC = () => {
const { t } = useTranslation();
const { isDefault, isLoading, handleSetDefault } = useDefaultApp();
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>
);
};
@@ -1,18 +0,0 @@
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 } from 'react';
import { useEffect, useRef } from 'react';
import { useBackendInitializer } from '@app/hooks/useBackendInitializer';
import { useOpenedFile } from '@app/hooks/useOpenedFile';
import { fileOpenService } from '@app/services/fileOpenService';
@@ -20,42 +20,75 @@ 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) {
return;
}
if (openedFilePaths.length > 0 && !openedFileLoading && !initialFilesLoadedRef.current) {
initialFilesLoadedRef.current = true;
const loadOpenedFiles = async () => {
try {
const filesArray: File[] = [];
const loadOpenedFiles = async () => {
try {
const filesArray: File[] = [];
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);
// 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);
}
} 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`);
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();
}
}, [openedFilePaths, openedFileLoading, addFiles]);
// Listen for runtime file-opened events (from second instances on Windows/Linux)
useEffect(() => {
const handleRuntimeFileOpen = async (filePath: string) => {
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'
});
// Add directly to FileContext
await addFiles([file]);
console.log('[Desktop] Runtime opened file added to FileContext:', fileData.fileName);
}
} catch (error) {
console.error('[Desktop] Failed to load opened files:', error);
console.error('[Desktop] Failed to load runtime opened file:', error);
}
};
loadOpenedFiles();
}, [openedFilePaths, openedFileLoading, addFiles]);
// Set up event listener and get cleanup function
const unlisten = fileOpenService.onFileOpened(handleRuntimeFileOpen);
// Clean up listener on unmount
return unlisten;
}, [addFiles]);
}
@@ -1,61 +0,0 @@
import { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { defaultAppService } from '@app/services/defaultAppService';
import { alert } from '@app/components/toast';
export const useDefaultApp = () => {
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 {
isDefault,
isLoading,
checkDefaultStatus,
handleSetDefault,
};
};
+15 -16
View File
@@ -1,46 +1,45 @@
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(() => {
// Function to read and process files from storage
const readFilesFromStorage = async () => {
console.log('🔍 Reading files from storage...');
const checkForOpenedFile = async () => {
console.log('🔍 Checking for opened file(s)...');
try {
const filePaths = await fileOpenService.getOpenedFiles();
console.log('🔍 fileOpenService.getOpenedFiles() returned:', filePaths);
if (filePaths.length > 0) {
console.log(`✅ Found ${filePaths.length} file(s) in storage:`, filePaths);
console.log(`✅ App opened with ${filePaths.length} file(s):`, 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 read files from storage:', error);
console.error('❌ Failed to check for opened files:', error);
} finally {
setLoading(false);
}
};
// Read files on mount
readFilesFromStorage();
checkForOpenedFile();
// 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;
// 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]);
});
// Cleanup function
return () => {
if (unlisten) unlisten();
unlistenRuntimeEvents();
};
}, []);
@@ -1,70 +0,0 @@
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;
},
};
+10 -13
View File
@@ -1,7 +1,6 @@
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";
@@ -23,19 +22,17 @@ export default function App() {
return (
<Suspense fallback={<LoadingFallback />}>
<AppProviders>
<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 />} />
<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 />
</AppLayout>
{/* Main app routes - Landing handles auth logic */}
<Route path="/*" element={<Landing />} />
</Routes>
<OnboardingTour />
</AppProviders>
</Suspense>
);
@@ -1,6 +0,0 @@
import React from 'react';
import { PreferencesProvider } from '@app/contexts/PreferencesContext';
export const PreferencesTestWrapper = ({ children }: { children: React.ReactNode }) => (
<PreferencesProvider>{children}</PreferencesProvider>
);
+1 -2
View File
@@ -4,8 +4,7 @@
"paths": {
"@app/*": [
"src/core/*"
],
"@testing/*": ["src/testing/*"]
]
}
},
"exclude": [
+1 -2
View File
@@ -9,8 +9,7 @@
],
"@desktop/*": ["src/desktop/*"],
"@proprietary/*": ["src/proprietary/*"],
"@core/*": ["src/core/*"],
"@testing/*": ["src/testing/*"]
"@core/*": ["src/core/*"]
}
},
"exclude": [
-1
View File
@@ -36,7 +36,6 @@
"src/proprietary/*",
"src/core/*"
],
"@testing/*": ["src/testing/*"],
"@desktop/*": ["src/desktop/*"],
"@proprietary/*": ["src/proprietary/*"],
"@core/*": ["src/core/*"]
+1 -2
View File
@@ -6,8 +6,7 @@
"src/proprietary/*",
"src/core/*"
],
"@core/*": ["src/core/*"],
"@testing/*": ["src/testing/*"]
"@core/*": ["src/core/*"]
}
},
"exclude": [