Documentation Index
Fetch the complete documentation index at: https://v2.spacedrive.com/llms.txt
Use this file to discover all available pages before exploring further.
The platform abstraction layer enables the Spacedrive interface to run on multiple platforms (Web, Tauri, React Native) while accessing platform-specific features.
Problem
Spacedrive runs on:
- Desktop via Tauri (native file pickers, native dialogs)
- Web via browser (limited native APIs)
- Mobile via React Native (different native APIs)
We need a single codebase that works everywhere without if (isTauri) checks scattered throughout.
Solution
The Platform type and usePlatform() hook provide a clean abstraction:
// Component doesn't know or care which platform it's on
function FilePicker() {
const platform = usePlatform();
const handlePickFile = async () => {
if (platform.openFilePickerDialog) {
const path = await platform.openFilePickerDialog({ multiple: false });
console.log('Selected:', path);
} else {
// Fallback for platforms without native picker
console.log('Use web file input instead');
}
};
return <button onClick={handlePickFile}>Pick File</button>;
}
type Platform = {
// Platform discriminator
platform: "web" | "tauri";
// Open native directory picker dialog (Tauri only)
openDirectoryPickerDialog?(opts?: {
title?: string;
multiple?: boolean;
}): Promise<string | string[] | null>;
// Open native file picker dialog (Tauri only)
openFilePickerDialog?(opts?: {
title?: string;
multiple?: boolean;
}): Promise<string | string[] | null>;
// Save file picker dialog (Tauri only)
saveFilePickerDialog?(opts?: {
title?: string;
defaultPath?: string;
}): Promise<string | null>;
// Open a URL in the default browser
openLink(url: string): void;
// Show native confirmation dialog
confirm(message: string, callback: (result: boolean) => void): void;
};
Usage
Setup
Wrap your app with PlatformProvider:
import { PlatformProvider, Platform } from '@sd/interface';
// Tauri implementation
const tauriPlatform: Platform = {
platform: 'tauri',
openDirectoryPickerDialog: async (opts) => {
return await window.__TAURI__.dialog.open({
directory: true,
title: opts?.title,
multiple: opts?.multiple,
});
},
openFilePickerDialog: async (opts) => {
return await window.__TAURI__.dialog.open({
directory: false,
title: opts?.title,
multiple: opts?.multiple,
});
},
saveFilePickerDialog: async (opts) => {
return await window.__TAURI__.dialog.save({
title: opts?.title,
defaultPath: opts?.defaultPath,
});
},
openLink: (url) => {
window.__TAURI__.shell.open(url);
},
confirm: (message, callback) => {
window.__TAURI__.dialog.confirm(message).then(callback);
},
};
// Web implementation
const webPlatform: Platform = {
platform: 'web',
// No native dialogs
openLink: (url) => {
window.open(url, '_blank');
},
confirm: (message, callback) => {
callback(window.confirm(message));
},
};
function App() {
const platform = window.__TAURI__ ? tauriPlatform : webPlatform;
return (
<PlatformProvider platform={platform}>
<YourApp />
</PlatformProvider>
);
}
Using in Components
import { usePlatform } from '@sd/interface';
function AddLocationButton() {
const platform = usePlatform();
const handleAddLocation = async () => {
// Check if native picker is available
if (platform.openDirectoryPickerDialog) {
const path = await platform.openDirectoryPickerDialog({
title: 'Select folder to index',
multiple: false,
});
if (path && typeof path === 'string') {
// Create location with selected path
await createLocation({ path });
}
} else {
// Web fallback: show manual path input
showManualPathDialog();
}
};
return (
<button onClick={handleAddLocation}>
Add Location
</button>
);
}
Opening External Links
function HelpButton() {
const platform = usePlatform();
return (
<button onClick={() => platform.openLink('https://spacedrive.com/docs')}>
Open Documentation
</button>
);
}
Confirmation Dialogs
function DeleteButton({ itemName }: { itemName: string }) {
const platform = usePlatform();
const handleDelete = () => {
platform.confirm(
`Are you sure you want to delete "${itemName}"?`,
(confirmed) => {
if (confirmed) {
performDelete();
}
}
);
};
return <button onClick={handleDelete}>Delete</button>;
}
function FeatureGate() {
const platform = usePlatform();
if (platform.platform === 'web') {
return <div>Web-specific UI</div>;
}
return <div>Desktop-specific UI</div>;
}
Optional Methods Pattern
All platform-specific methods are optional (marked with ?). This enables:
- Graceful degradation - Components can check availability and provide fallbacks
- Type safety - TypeScript enforces checking before calling
- Platform flexibility - New platforms can implement only what they support
// Good - checks availability
if (platform.openFilePickerDialog) {
await platform.openFilePickerDialog();
} else {
// Fallback for platforms without native picker
}
// Bad - will error at runtime on web
await platform.openFilePickerDialog(); // Type error!
To add a new platform capability:
- Update the
Platform type in platform.tsx
- Implement for each platform (Tauri, Web, React Native)
- Use with optional chaining in components
// 1. Add to Platform type
type Platform = {
// ... existing methods
// New method
showNotification?(opts: {
title: string;
body: string;
}): void;
};
// 2. Implement for Tauri
const tauriPlatform: Platform = {
// ... existing methods
showNotification: (opts) => {
window.__TAURI__.notification.sendNotification(opts);
},
};
// 3. Implement for Web
const webPlatform: Platform = {
// ... existing methods
showNotification: (opts) => {
if ('Notification' in window && Notification.permission === 'granted') {
new Notification(opts.title, { body: opts.body });
}
},
};
// 4. Use in components
function NotifyButton() {
const platform = usePlatform();
const notify = () => {
platform.showNotification?.({
title: 'Hello',
body: 'World',
});
};
return <button onClick={notify}>Notify</button>;
}
Best Practices
Provide Fallbacks
Always have a fallback for platforms without the feature:
// Good
const pickFile = async () => {
if (platform.openFilePickerDialog) {
return await platform.openFilePickerDialog();
} else {
// Show web file input or manual path entry
return await showWebFilePicker();
}
};
// Bad - feature just doesn't work on some platforms
const pickFile = async () => {
return await platform.openFilePickerDialog?.();
// Returns undefined on web, no fallback!
};
Avoid checking platform.platform directly. Check method availability instead:
// Good - feature detection
if (platform.openDirectoryPickerDialog) {
// Use native picker
}
// Bad - platform detection
if (platform.platform === 'tauri') {
// Assumes Tauri = has picker (maybe not in future)
}
Platform-specific code should be minimal. Most logic should be platform-agnostic:
// Good - only picker is platform-specific
async function addLocation() {
const path = await pickDirectory(); // Platform abstraction
const location = createLocationFromPath(path); // Platform-agnostic
await saveLocation(location); // Platform-agnostic
showSuccessMessage(); // Platform-agnostic
}
// Bad - too much platform-specific code
async function addLocationTauri() {
// Entire flow is Tauri-specific, can't reuse
}
async function addLocationWeb() {
// Duplicate logic for web
}
Use Context, Not Props
Use usePlatform() hook instead of prop drilling:
// Good
function DeepComponent() {
const platform = usePlatform(); // Available anywhere
}
// Bad
function Parent() {
return <Child platform={platform} />;
}
function Child({ platform }: { platform: Platform }) {
return <GrandChild platform={platform} />;
}
Tauri
const tauriPlatform: Platform = {
platform: 'tauri',
openDirectoryPickerDialog: async (opts) => {
return await window.__TAURI__.dialog.open({
directory: true,
title: opts?.title,
multiple: opts?.multiple,
});
},
openFilePickerDialog: async (opts) => {
return await window.__TAURI__.dialog.open({
directory: false,
title: opts?.title,
multiple: opts?.multiple,
});
},
saveFilePickerDialog: async (opts) => {
return await window.__TAURI__.dialog.save({
title: opts?.title,
defaultPath: opts?.defaultPath,
});
},
openLink: (url) => {
window.__TAURI__.shell.open(url);
},
confirm: (message, callback) => {
window.__TAURI__.dialog.confirm(message).then(callback);
},
};
Web
const webPlatform: Platform = {
platform: 'web',
openLink: (url) => {
window.open(url, '_blank');
},
confirm: (message, callback) => {
callback(window.confirm(message));
},
// Native pickers not available
};
React Native (Future)
const rnPlatform: Platform = {
platform: 'mobile',
openDirectoryPickerDialog: async (opts) => {
// Use react-native-document-picker or similar
return await DocumentPicker.pickDirectory();
},
openLink: (url) => {
Linking.openURL(url);
},
confirm: (message, callback) => {
Alert.alert(
'Confirm',
message,
[
{ text: 'Cancel', onPress: () => callback(false) },
{ text: 'OK', onPress: () => callback(true) },
]
);
},
};
Error Handling
async function pickFile() {
const platform = usePlatform();
if (!platform.openFilePickerDialog) {
throw new Error('File picker not available on this platform');
}
try {
const path = await platform.openFilePickerDialog();
if (!path) {
// User cancelled
return null;
}
return path;
} catch (error) {
console.error('Failed to pick file:', error);
return null;
}
}
Summary
The platform abstraction layer:
- Single codebase works on Web, Tauri, and React Native
- Clean API via
usePlatform() hook
- Type-safe with optional methods
- Graceful degradation with feature detection
- Minimal boilerplate using React Context
- Easy to extend with new platform methods
Use it whenever you need platform-specific functionality like native dialogs, file pickers, or shell commands.