Skip to main content

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>;
}

Platform Type

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>
  );
}
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>;
}

Checking Platform Type

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:
  1. Graceful degradation - Components can check availability and provide fallbacks
  2. Type safety - TypeScript enforces checking before calling
  3. 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!

Adding New Platform Methods

To add a new platform capability:
  1. Update the Platform type in platform.tsx
  2. Implement for each platform (Tauri, Web, React Native)
  3. 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!
};

Don’t Check Platform String

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)
}

Keep Platform Logic Minimal

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} />;
}

Platform Implementations

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.