diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 000000000..fc6bb453b --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,66 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "inlay_hints": { + "enabled": false + }, + "languages": { + "Rust": { + "enable_language_server": true, + "formatter": "language_server", + "inlay_hints": { + "enabled": true, + "show_type_hints": true, + "show_parameter_hints": true, + "show_other_hints": true, + "show_background": false, + "edit_debounce_ms": 700, + "scroll_debounce_ms": 50 + } + }, + "TOML": { + "formatter": "language_server" + } + }, + "lsp": { + "rust-analyzer": { + "initialization_options": { + "procMacro": { + "enable": true + }, + "diagnostics": { + "experimental": { + "enable": false + } + }, + "showUnlinkedFileNotification": false + } + } + }, + "file_scan_exclusions": [ + "node_modules", + "**/node_modules", + "**/bower_components", + "**/*.code-search", + "**/*.contentlayer", + "**/*.next", + "**/dist", + "apps/mobile/ios/Pods", + "apps/mobile/android", + "apps/mobile/ios", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store" + ], + "format_on_save": "on", + "ensure_final_newline_on_save": true, + "remove_trailing_whitespace_on_save": true, + "tab_size": 4, + "hard_tabs": false, + "show_whitespaces": "selection", + "show_completion_documentation": true +} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45962bf1b..8a6a160fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,17 +38,18 @@ This project uses [Cargo](https://doc.rust-lang.org/cargo/getting-started/instal To make changes locally, follow these steps: 1. Clone & enter the repository: - ``` - git clone https://github.com/spacedriveapp/spacedrive && cd spacedrive - ``` - Alternatively, if you’ve already cloned the repo locally, pull the latest changes with: `git pull` + ` git clone https://github.com/spacedriveapp/spacedrive && cd spacedrive + ` + Alternatively, if you’ve already cloned the repo locally, pull the latest changes with: `git pull` > [!TIP] > Consider running `pnpm clean` after pulling the repository if you're returning to it from previously to avoid old files conflicting. -2. Configure your system environment for Spacedrive development - - For Unix users (Linux / macOS), run: `./scripts/setup.sh` - - For Windows users, run: `.\scripts\setup.ps1` via PowerShell. +3. Configure your system environment for Spacedrive development + +- For Unix users (Linux / macOS), run: `./scripts/setup.sh` +- For Windows users, run: `.\scripts\setup.ps1` via PowerShell. > [!NOTE] > This script ([Unix](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh) / [Windows](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.ps1)) will check for if Rust and pnpm are installed then proceed to install any other required dependencies for Spacedrive to build via your system's respective package manager. + 3. Install NodeJS dependencies: `pnpm i` 4. Prepare the build: `pnpm prep`. This will run all necessary codegen and build required dependencies. @@ -58,13 +59,16 @@ To make changes locally, follow these steps: > The test files will be located in a directory called `test-data` in the root of the Spacedrive repository. To run the **desktop** app, run: + ``` pnpm tauri dev ``` + > [!NOTE] > The Tauri desktop app always runs its own instance of the backend and will not connect to a separately initiated `sd-server` instance. To run the **backend server**, run: + ``` cargo run -p sd-server ``` @@ -73,23 +77,29 @@ cargo run -p sd-server > If necessary, [DevTools](https://tauri.app/v1/guides/debugging/application/#webview-console) for the WebView can be opened by pressing Ctrl+Shift+I (Linux and Windows) or Command+Option+I (macOS) in the desktop app. > > Also, React DevTools can be launched using `pnpx react-devtools`. - However, it must be executed before starting the desktop app for it to connect. +> However, it must be executed before starting the desktop app for it to connect. To run the **web** app (requires the backend to be running), run: + ``` pnpm web dev ``` + > [!TIP] > You can also quickly launch the web interface together with the backend with: +> > ``` > pnpm dev:web > ``` To run the **e2e tests** for the web app: + ``` pnpm web test:e2e ``` + If you are developing a new test, you can execute Cypress in interactive mode with: + ``` pnpm web test:interactive ``` @@ -97,19 +107,21 @@ pnpm web test:interactive #### Troubleshooting - If you encounter any issues, ensure that you are using the following versions of Rust, Node.js and pnpm: - | tool | version | - | ---- | ------- | - | Rust | [`1.81`](rust-toolchain.toml) | + | tool | version | + | ---- | ------- | + | Rust | [`1.81`](rust-toolchain.toml) | | Node.js | [`18.18`](.nvmrc) | - | pnpm | `9.4.0` | + | pnpm | `9.4.0` | - [`rustup`](https://rustup.rs/) & [`nvm`](https://github.com/nvm-sh/nvm) should both pick up on the appropriate versions of the Rust Toolchain & Node respectively from the project automatically. +> **Note**: If you get a local migration error in development, you might need to set the following environment variables: [database documentation](docs/developers/architecture/database.mdx#environment-variables). - - After cleaning out your build artifacts using `pnpm clean`, it's necessary to re-run `pnpm prep`. +[`rustup`](https://rustup.rs/) & [`nvm`](https://github.com/nvm-sh/nvm) should both pick up on the appropriate versions of the Rust Toolchain & Node respectively from the project automatically. - - Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours. +- After cleaning out your build artifacts using `pnpm clean`, it's necessary to re-run `pnpm prep`. - - After you finish making your changes and committing them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code. +- Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours. + +- After you finish making your changes and committing them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code. ### Landing Page @@ -136,53 +148,56 @@ To run the mobile app: > Most modern phones use `arm64-v8a` while the Android Studio embedded emulator runs `x86_64` If you wish to debug directly on a local Android device: - - Install [ADB](https://developer.android.com/tools/adb) - - On macOS use [homebrew](https://brew.sh/): `brew install adb` - - [Configure debugging on your device](https://developer.android.com/tools/adb#Enabling) - - Select "Remember this device" & "Trust" when connecting over USB. - - Run `pnpm mobile android` with your device connected via USB. ->[!TIP] +- Install [ADB](https://developer.android.com/tools/adb) + - On macOS use [homebrew](https://brew.sh/): `brew install adb` +- [Configure debugging on your device](https://developer.android.com/tools/adb#Enabling) + - Select "Remember this device" & "Trust" when connecting over USB. +- Run `pnpm mobile android` with your device connected via USB. + +> [!TIP] > To access the logs from `sd-core` when running on device, run the following command: +> > ``` > adb logcat | grep -i com.spacedrive.app > ``` -#### iOS - - Install the latest version of [Xcode](https://apps.apple.com/au/app/xcode/id497799835) and Simulator if you wish to emulate an iOS device on your Mac. - - When running Xcode for the first time, make sure to select the latest version of iOS. - - Run `pnpm mobile ios` in the terminal to build & run the app on the Simulator. - - To run the app in debug mode with backend (`sd-core`) logging, comment out the following lines before running the above command: - https://github.com/spacedriveapp/spacedrive/blob/d180261ca5a93388486742e8f921e895e9ec26a4/apps/mobile/modules/sd-core/ios/build-rust.sh#L51-L54 +#### iOS + +- Install the latest version of [Xcode](https://apps.apple.com/au/app/xcode/id497799835) and Simulator if you wish to emulate an iOS device on your Mac. + - When running Xcode for the first time, make sure to select the latest version of iOS. +- Run `pnpm mobile ios` in the terminal to build & run the app on the Simulator. + - To run the app in debug mode with backend (`sd-core`) logging, comment out the following lines before running the above command: + https://github.com/spacedriveapp/spacedrive/blob/d180261ca5a93388486742e8f921e895e9ec26a4/apps/mobile/modules/sd-core/ios/build-rust.sh#L51-L54 You can now get backend (`sd-core`) logs from the Simulator by running the following command: - ``` - xcrun simctl launch --console booted com.spacedrive.app - ``` - - If you'd like to run the app on device, run: - ``` - pnpm mobile ios --device - ``` + ``` + xcrun simctl launch --console booted com.spacedrive.app + ``` +- If you'd like to run the app on device, run: `pnpm mobile ios --device` > [!IMPORTANT] > Note that you can only get `sd-core` logs from the app when running it on device by running the frontend and backend separately. To run the backend (`sd-core`) separately, open up Xcode by running: + ``` xed apps/mobile/ios ``` + Select from the top if you wish to start on device or Simulator, and press play. -| Select Device | Run the App | Build & Core logs are found here | -| --- | --- | --- | -|![](./apps/landing/public/images/xcode-run-sd-core.01.png)|![](./apps/landing/public/images/xcode-run-sd-core.02.png)|![](./apps/landing/public/images/xcode-run-sd-core.03.png)| +| Select Device | Run the App | Build & Core logs are found here | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| ![](./apps/landing/public/images/xcode-run-sd-core.01.png) | ![](./apps/landing/public/images/xcode-run-sd-core.02.png) | ![](./apps/landing/public/images/xcode-run-sd-core.03.png) | To run the frontend, run the following: + ``` pnpm mobile start ``` + > [!IMPORTANT] > The frontend is not functional without the sd-core running as well. - ### Pull Request Once you have finished making your changes, create a pull request (PR) to submit them. diff --git a/Cargo.lock b/Cargo.lock index f10466234..74acb0b82 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index 1d53fa440..3d48b2a01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ rust-version = "1.81" [workspace.dependencies] # First party dependencies -sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", rev = "74af2a727c" } +sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", rev = "4e4565bee4" } # Third party dependencies used by one or more of our crates async-channel = "2.3" diff --git a/apps/desktop/crates/macos/src-swift/window.swift b/apps/desktop/crates/macos/src-swift/window.swift index e5f50f871..85ccd3d92 100644 --- a/apps/desktop/crates/macos/src-swift/window.swift +++ b/apps/desktop/crates/macos/src-swift/window.swift @@ -3,16 +3,20 @@ import SwiftRs @objc public enum AppThemeType: Int { - case auto = -1 - case light = 0 - case dark = 1 + case auto = -1 + case light = 0 + case dark = 1 } -var activity: NSObjectProtocol? +private let activityLock = NSLock() +private var activity: NSObjectProtocol? +private var isThemeUpdating = false @_cdecl("disable_app_nap") public func disableAppNap(reason: SRString) -> Bool { - // Check if App Nap is already disabled + activityLock.lock() + defer { activityLock.unlock() } + guard activity == nil else { return false } @@ -26,37 +30,55 @@ public func disableAppNap(reason: SRString) -> Bool { @_cdecl("enable_app_nap") public func enableAppNap() -> Bool { - // Check if App Nap is already enabled - guard let pinfo = activity else { + activityLock.lock() + defer { activityLock.unlock() } + + guard let currentActivity = activity else { return false } - ProcessInfo.processInfo.endActivity(pinfo) + ProcessInfo.processInfo.endActivity(currentActivity) activity = nil return true } @_cdecl("lock_app_theme") public func lockAppTheme(themeType: AppThemeType) { - var theme: NSAppearance? - switch themeType { - case .auto: - theme = nil - case .dark: - theme = NSAppearance(named: .darkAqua)! - case .light: - theme = NSAppearance(named: .aqua)! - } - - DispatchQueue.main.async { - NSApp.appearance = theme - - // Trigger a repaint of the window - if let window = NSApplication.shared.mainWindow { - window.invalidateShadow() - window.displayIfNeeded() + // Prevent concurrent theme updates + guard !isThemeUpdating else { + return + } + + isThemeUpdating = true + + let theme: NSAppearance? + switch themeType { + case .auto: + theme = nil + case .dark: + theme = NSAppearance(named: .darkAqua) + case .light: + theme = NSAppearance(named: .aqua) + } + + // Use sync to ensure completion before return + DispatchQueue.main.sync { + autoreleasepool { + NSApp.appearance = theme + + if let window = NSApplication.shared.mainWindow { + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0 + window.invalidateShadow() + window.displayIfNeeded() + }, completionHandler: { + isThemeUpdating = false + }) + } else { + isThemeUpdating = false + } + } } - } } @_cdecl("set_titlebar_style") diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 31b5cdee0..2d1f8da01 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -12,12 +12,13 @@ "lint": "eslint src --cache" }, "dependencies": { - "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", - "@spacedrive/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&6a77167495", + "@crabnebula/tauri-plugin-drag": "^2.0.0", "@remix-run/router": "=1.13.1", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", + "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", + "@spacedrive/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&6a77167495", "@t3-oss/env-core": "^0.7.1", "@tanstack/react-query": "^5.59", "@tauri-apps/api": "=2.0.3", @@ -31,7 +32,7 @@ "react-dom": "^18.2.0", "react-router-dom": "=6.20.1", "sonner": "^1.0.3", - "supertokens-web-js": "^0.13.0" + "supertokens-web-js": "=0.13.0" }, "devDependencies": { "@sd/config": "workspace:*", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 0f2a3acb2..e65b09eab 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -32,22 +32,25 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["sync"] } tracing = { workspace = true } uuid = { workspace = true, features = ["serde"] } +base64 = { workspace = true } # Specific Desktop dependencies # WARNING: Do NOT enable default features, as that vendors dbus (see below) opener = { version = "0.7.1", features = ["reveal"], default-features = false } specta-typescript = "=0.0.7" tauri-plugin-clipboard-manager = "=2.0.1" +tauri-plugin-cors-fetch = { path = "../../../crates/tauri-plugin-cors-fetch" } tauri-plugin-deep-link = "=2.0.1" tauri-plugin-dialog = "=2.0.3" tauri-plugin-http = "=2.0.3" tauri-plugin-os = "=2.0.1" tauri-plugin-shell = "=2.0.2" tauri-plugin-updater = "=2.0.2" +tauri-plugin-drag = "2.0.0" +drag = "2.0.0" # memory allocator -mimalloc = { workspace = true } -tauri-plugin-cors-fetch = "2.1.1" +mimalloc = { workspace = true } [dependencies.tauri] features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"] diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index eb899e7f2..1f163023b 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -27,6 +27,7 @@ "core:window:allow-start-dragging", "core:webview:allow-internal-toggle-devtools", "cors-fetch:default", + "drag:default", { "identifier": "http:default", "allow": [ diff --git a/apps/desktop/src-tauri/src/drag.rs b/apps/desktop/src-tauri/src/drag.rs new file mode 100644 index 000000000..e4f09b849 --- /dev/null +++ b/apps/desktop/src-tauri/src/drag.rs @@ -0,0 +1,223 @@ +// Import required dependencies for drag and drop operations, serialization, and async functionality +use drag::{DragItem, Image, Options}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use tauri::{ipc::Channel, Manager, PhysicalPosition, State, WebviewWindow}; + +// DragState wraps a thread-safe boolean flag to track drag operation status +#[derive(Clone)] +pub struct DragState(pub Arc>); + +// Default implementation for DragState initializes with false +impl Default for DragState { + fn default() -> Self { + Self(Arc::new(Mutex::new(false))) + } +} + +// Enum to represent the result of a drag operation (serializable for IPC) +#[derive(Serialize, Deserialize, Type, Clone)] +pub enum WrappedDragResult { + Dropped, + Cancel, +} + +// Structure to hold cursor position coordinates (serializable for IPC) +#[derive(Serialize, Deserialize, Type, Clone)] +pub struct WrappedCursorPosition { + x: i32, + y: i32, +} + +// Combined structure for drag operation results (serializable for IPC) +#[derive(Serialize, Deserialize, Type, Clone)] +pub struct CallbackResult { + result: WrappedDragResult, + #[serde(rename = "cursorPos")] + cursor_pos: WrappedCursorPosition, +} + +// Conversion implementations for drag-rs types to our wrapped types +impl From for WrappedDragResult { + fn from(result: drag::DragResult) -> Self { + match result { + drag::DragResult::Dropped => WrappedDragResult::Dropped, + drag::DragResult::Cancel => WrappedDragResult::Cancel, + } + } +} + +impl From for WrappedCursorPosition { + fn from(pos: drag::CursorPosition) -> Self { + WrappedCursorPosition { x: pos.x, y: pos.y } + } +} + +// Global flag to track if position tracking is active +static TRACKING: AtomicBool = AtomicBool::new(false); + +#[tauri::command(async)] +/// Initiates a drag and drop operation with cursor position tracking +/// +/// # Arguments +/// * `window` - The Tauri window instance +/// * `_state` - Current drag state (unused) +/// * `files` - Vector of file paths to be dragged +/// * `icon_path` - Path to the preview icon for the drag operation +/// * `on_event` - Channel for communicating drag operation events back to the frontend +#[specta::specta] +pub async fn start_drag( + window: WebviewWindow, + _state: State<'_, DragState>, + files: Vec, + icon_path: String, + on_event: Channel, +) -> Result<(), String> { + // Fast atomic swap for tracking state + match TRACKING.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) { + Ok(_) => { + println!("Starting position tracking"); + } + Err(_) => { + // If already tracking, stop previous instance quickly + TRACKING.store(false, Ordering::SeqCst); + tokio::time::sleep(tokio::time::Duration::from_millis(16)).await; + TRACKING.store(true, Ordering::SeqCst); + println!("Restarting position tracking"); + } + } + + // Pre-allocate resources before spawning task + let window_handle = Arc::new(window); + let app_handle = window_handle.app_handle(); + + // Initialize control flags + let cancel_flag = Arc::new(AtomicBool::new(false)); + let is_completed = Arc::new(AtomicBool::new(false)); + + // Prepare resources once with minimal cloning + let tracking_resources = Arc::new((files.clone(), icon_path.clone(), Arc::new(on_event))); + + println!("Starting position tracking"); + + // Get handles for window and app management + let window_clone = window_handle.clone(); + let app_handle_owned = app_handle.to_owned(); + let window_owned = window_clone.to_owned(); + + // Control flags for operation state + let is_completed_clone = is_completed.clone(); + + // Spawn background task for cursor tracking + tokio::spawn(async move { + // Initialize tracking state + let mut last_position = (0.0, 0.0); + let mut last_message_time = Instant::now(); + let threshold = 1.0; // Minimum movement threshold + let message_debounce = Duration::from_millis(32); // State update interval + let mut was_inside = false; + + // Main tracking loop + while TRACKING.load(Ordering::SeqCst) && !is_completed.load(Ordering::SeqCst) { + let window_for_check = window_owned.clone(); + // Skip if window is not focused + if !window_for_check.is_focused().unwrap_or(false) { + tokio::time::sleep(tokio::time::Duration::from_millis(8)).await; + continue; + } + + // Get current cursor and window positions + if let (Ok(cursor_position), Ok(window_position), Ok(window_size)) = ( + window_for_check.cursor_position(), + window_for_check.outer_position(), + window_for_check.inner_size(), + ) { + // Calculate cursor position relative to window + let relative_position = PhysicalPosition::new( + cursor_position.x - window_position.x as f64, + cursor_position.y - window_position.y as f64, + ); + + // Check if cursor is inside window boundaries + let is_inside = relative_position.x >= 0.0 + && relative_position.y >= 0.0 + && relative_position.x <= window_size.width as f64 + && relative_position.y <= window_size.height as f64; + + // Process state changes if cursor moved enough + if is_inside != was_inside + && ((relative_position.x - last_position.0).abs() > threshold + || (relative_position.y - last_position.1).abs() > threshold) + { + let now = Instant::now(); + if now.duration_since(last_message_time) >= message_debounce { + // Prepare resources for drag operation + let files_for_drag = tracking_resources.0.clone(); + let icon_path_for_drag = tracking_resources.1.clone(); + let on_event_for_drag = tracking_resources.2.clone(); + let is_completed = is_completed_clone.clone(); + let cancel_flag_clone = cancel_flag.clone(); + let window_for_drag = window_owned.clone(); + + // Execute drag operation on main thread + app_handle_owned + .run_on_main_thread(move || { + if !is_inside { + println!("Starting drag operation"); + // Create drag items + let paths: Vec = + files_for_drag.iter().map(PathBuf::from).collect(); + let item = DragItem::Files(paths); + let preview_icon = + Image::File(PathBuf::from(&icon_path_for_drag)); + + // Start the drag operation + if let Ok(_) = drag::start_drag( + &window_for_drag, + item, + preview_icon, + move |result, cursor_pos| { + // Send result back to frontend + let _ = on_event_for_drag.send(CallbackResult { + result: result.into(), + cursor_pos: cursor_pos.into(), + }); + // Mark operation as completed + is_completed.store(true, Ordering::SeqCst); + TRACKING.store(false, Ordering::SeqCst); + }, + Options::default(), + ) { + println!("Drag operation started"); + } + } else { + println!("Cursor returned to window"); + cancel_flag_clone.store(true, Ordering::SeqCst); + // We have this for now, but technically, it doesn't do anything. + // I'm still trying to figure out how to cancel mid-drag without the user having to cancel the dragging on the frontend too. + // - @Rocky43007 + } + }) + .unwrap_or_default(); + + // Update tracking state + last_message_time = now; + was_inside = is_inside; + last_position = (relative_position.x, relative_position.y); + } + } + } + + // Prevent excessive CPU usage + tokio::time::sleep(tokio::time::Duration::from_millis(8)).await; + } + + println!("Tracking instance stopped"); + }); + + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 2842f7210..6a821d260 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -22,6 +22,7 @@ use tokio::task::block_in_place; use tokio::time::sleep; use tracing::{debug, error}; +mod drag; mod file; mod menu; mod tauri_plugins; @@ -200,6 +201,7 @@ async fn main() -> tauri::Result<()> { set_menu_bar_item_state, request_fda_macos, open_trash_in_os_explorer, + drag::start_drag, file::open_file_paths, file::open_ephemeral_files, file::get_file_path_open_with_apps, @@ -362,6 +364,7 @@ async fn main() -> tauri::Result<()> { .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(updater::plugin()) .manage(updater::State::default()) + .manage(drag::DragState::default()) .build(tauri::generate_context!())? .run(|_, _| {}); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index c3d9e12af..05dbdc4ed 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -3,7 +3,13 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { listen } from '@tauri-apps/api/event'; import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { RspcProvider, useBridgeMutation } from '@sd/client'; +import { + getItemFilePath, + libraryClient, + RspcProvider, + useBridgeMutation, + useSelector +} from '@sd/client'; import { createRoutes, DeeplinkEvent, @@ -16,13 +22,15 @@ import { } from '@sd/interface'; import { RouteTitleContext } from '@sd/interface/hooks/useRouteTitle'; -import '@sd/ui/style/style.scss'; +import '@sd/ui/style'; +import { Channel, invoke } from '@tauri-apps/api/core'; import SuperTokens from 'supertokens-web-js'; import EmailPassword from 'supertokens-web-js/recipe/emailpassword'; import Passwordless from 'supertokens-web-js/recipe/passwordless'; import Session from 'supertokens-web-js/recipe/session'; import ThirdParty from 'supertokens-web-js/recipe/thirdparty'; +import { explorerStore } from '@sd/interface/app/$libraryId/Explorer/store'; // TODO: Bring this back once upstream is fixed up. // const client = hooks.createClient({ // links: [ @@ -37,6 +45,7 @@ import getWindowHandler from '@sd/interface/app/$libraryId/settings/client/accou import { useLocale } from '@sd/interface/hooks'; import { AUTH_SERVER_URL, getTokens } from '@sd/interface/util'; +import { Transparent } from '../../../packages/assets/images'; import { commands } from './commands'; import { platform } from './platform'; import { queryClient } from './query'; @@ -46,6 +55,7 @@ import { createUpdater } from './updater'; declare global { interface Window { enableCORSFetch: (enable: boolean) => void; + useDragAndDrop: () => void; } } @@ -67,11 +77,89 @@ SuperTokens.init({ const startupError = (window as any).__SD_ERROR__ as string | undefined; +function useDragAndDrop() { + const dragState = useSelector(explorerStore, (s) => s.drag); + + useEffect(() => { + console.log('Drag effect triggered:', { + dragStateType: dragState?.type, + itemCount: dragState?.type === 'dragging' ? dragState?.items?.length : undefined + }); + + (async () => { + if (dragState?.type === 'dragging' && dragState.items.length > 0) { + console.log('Starting drag operation with items:', dragState.items); + + const items = await Promise.all( + dragState.items.map(async (item) => { + const data = getItemFilePath(item); + if (!data) { + console.log('No file path data for item:', item); + return; + } + + const file_path = + 'path' in data ? data.path : await libraryClient.query(['files.getPath', data.id]); + + console.log('Resolved file path:', file_path); + return { + type: 'explorer-item', + file_path: file_path + }; + }) + ); + + const image = Transparent.split('/@fs')[1]!; + console.log('Using preview image:', image); + + const validFiles = items.filter(Boolean).map((item) => item?.file_path); + console.log('Invoking start_drag with files:', validFiles); + + try { + const channel = new Channel<{ + result: 'Dropped' | 'Cancelled'; + cursorPos: { x: number; y: number }; + }>(); + + channel.onmessage = (payload) => { + console.log('Drag completed:', { + result: payload.result, + position: payload.cursorPos, + timestamp: new Date().toISOString() + }); + + if (payload.result === 'Dropped') { + console.log('Drop location:', { + x: payload.cursorPos.x, + y: payload.cursorPos.y, + screen: window.screen + }); + } + + explorerStore.drag = null; + }; + + await invoke('start_drag', { + files: validFiles, + iconPath: image, + onEvent: channel + }); + console.log('start_drag invoked successfully'); + } catch (error) { + console.error('Failed to start drag:', error); + explorerStore.drag = null; + } + } + })(); + }, [dragState]); +} + export default function App() { useEffect(() => { // This tells Tauri to show the current window because it's finished loading commands.appReady(); window.enableCORSFetch(true); + window.useDragAndDrop = useDragAndDrop; // .then(() => { // if (import.meta.env.PROD) window.fetch = fetch; // }); @@ -126,13 +214,15 @@ type RedirectPath = { pathname: string; search: string | undefined }; function AppInner() { const [tabs, setTabs] = useState(() => [createTab()]); const [selectedTabIndex, setSelectedTabIndex] = useState(0); - const tokens = getTokens(); const cloudBootstrap = useBridgeMutation('cloud.bootstrap'); useEffect(() => { - // If the access token and/or refresh token are missing, we need to skip the cloud bootstrap - if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return; - cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]); + (async () => { + const tokens = await getTokens(); + // If the access token and/or refresh token are missing, we need to skip the cloud bootstrap + if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return; + cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]); + })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -204,6 +294,43 @@ function AppInner() { }; }, [selectedTab.element]); + const SizeDisplay = () => { + const [size, setSize] = useState({ + width: window.innerWidth, + height: window.innerHeight + }); + + useEffect(() => { + const handleResize = () => { + setSize({ + width: window.innerWidth, + height: window.innerHeight + }); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return ( +
+ {size.width} x {size.height} +
+ ); + }; + return ( { startTransition(() => { setTabs((tabs) => { - const { pathname, search } = - selectedTab.router.state.location; + const { pathname, search } = selectedTab.router.state.location; const newTab = createTab({ pathname, search }); const newTabs = [...tabs, newTab]; @@ -303,6 +429,7 @@ function AppInner() { tab.element ) )} + {/* */}
diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index 81581cf81..cd3b7005f 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -49,6 +49,31 @@ export const commands = { else return { status: 'error', error: e as any }; } }, + /** + * Initiates a drag and drop operation with cursor position tracking + * + * # Arguments + * * `window` - The Tauri window instance + * * `_state` - Current drag state (unused) + * * `files` - Vector of file paths to be dragged + * * `icon_path` - Path to the preview icon for the drag operation + * * `on_event` - Channel for communicating drag operation events back to the frontend + */ + async startDrag( + files: string[], + iconPath: string, + onEvent: TAURI_CHANNEL + ): Promise> { + try { + return { + status: 'ok', + data: await TAURI_INVOKE('start_drag', { files, iconPath, onEvent }) + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: 'error', error: e as any }; + } + }, async openFilePaths( library: string, ids: number[] @@ -162,6 +187,7 @@ export const events = __makeEvents__<{ /** user-defined types **/ export type AppThemeType = 'Auto' | 'Light' | 'Dark'; +export type CallbackResult = { result: WrappedDragResult; cursorPos: WrappedCursorPosition }; export type DragAndDropEvent = | { type: 'Hovered'; paths: string[]; x: number; y: number } | { type: 'Dropped'; paths: string[]; x: number; y: number } @@ -199,6 +225,8 @@ export type RevealItem = | { FilePath: { id: number } } | { Ephemeral: { path: string } }; export type Update = { version: string }; +export type WrappedCursorPosition = { x: number; y: number }; +export type WrappedDragResult = 'Dropped' | 'Cancel'; type __EventObj__ = { listen: (cb: TAURI_API_EVENT.EventCallback) => ReturnType>; diff --git a/apps/landing/package.json b/apps/landing/package.json index 6cc22e363..06579e7f7 100644 --- a/apps/landing/package.json +++ b/apps/landing/package.json @@ -11,6 +11,9 @@ }, "dependencies": { "@docsearch/react": "^3.5.2", + "@fortawesome/fontawesome-svg-core": "^6.6.0", + "@fortawesome/free-brands-svg-icons": "^6.6.0", + "@fortawesome/react-fontawesome": "^0.2.2", "@octokit/webhooks": "^12.0.3", "@phosphor-icons/react": "^2.0.14", "@radix-ui/react-dialog": "^1.0.5", @@ -36,6 +39,7 @@ "react-dom": "^18.2.0", "react-error-boundary": "^4.0.11", "react-github-btn": "^1.4.0", + "react-parallax-tilt": "^1.7.250", "reading-time": "^1.5.0", "rehype-autolink-headings": "^6.1.1", "rehype-external-links": "^2.1.0", diff --git a/apps/landing/public/images/app/app.png b/apps/landing/public/images/app/app.png new file mode 100644 index 000000000..68ac1c915 Binary files /dev/null and b/apps/landing/public/images/app/app.png differ diff --git a/apps/landing/public/images/app/app.webp b/apps/landing/public/images/app/app.webp new file mode 100644 index 000000000..d5e4ec19e Binary files /dev/null and b/apps/landing/public/images/app/app.webp differ diff --git a/apps/landing/public/images/app/wip/MultiDeviceOverview.png b/apps/landing/public/images/app/wip/MultiDeviceOverview.png new file mode 100644 index 000000000..d026af04a Binary files /dev/null and b/apps/landing/public/images/app/wip/MultiDeviceOverview.png differ diff --git a/apps/landing/public/images/bento/spacedrop.svg b/apps/landing/public/images/bento/spacedrop.svg deleted file mode 100644 index 15d50b715..000000000 --- a/apps/landing/public/images/bento/spacedrop.svg +++ /dev/null @@ -1,611 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/landing/public/images/bento/spacedrop.webp b/apps/landing/public/images/bento/spacedrop.webp deleted file mode 100644 index 52f2ebb29..000000000 Binary files a/apps/landing/public/images/bento/spacedrop.webp and /dev/null differ diff --git a/apps/landing/public/images/bento/tags.webp b/apps/landing/public/images/bento/tags.webp deleted file mode 100644 index a159870cf..000000000 Binary files a/apps/landing/public/images/bento/tags.webp and /dev/null differ diff --git a/apps/landing/public/images/careersbg.webp b/apps/landing/public/images/careersbg.webp new file mode 100644 index 000000000..b14f773a7 Binary files /dev/null and b/apps/landing/public/images/careersbg.webp differ diff --git a/apps/landing/public/images/circlebg.webp b/apps/landing/public/images/circlebg.webp new file mode 100644 index 000000000..2a15d4fc5 Binary files /dev/null and b/apps/landing/public/images/circlebg.webp differ diff --git a/apps/landing/public/images/misc/gold-bg.png b/apps/landing/public/images/misc/gold-bg.png new file mode 100644 index 000000000..992ca0f22 Binary files /dev/null and b/apps/landing/public/images/misc/gold-bg.png differ diff --git a/apps/landing/public/images/misc/noise-texture.png b/apps/landing/public/images/misc/noise-texture.png new file mode 100644 index 000000000..8e8e81bbd Binary files /dev/null and b/apps/landing/public/images/misc/noise-texture.png differ diff --git a/apps/landing/public/images/mobile.webp b/apps/landing/public/images/mobile.webp new file mode 100644 index 000000000..737df33ed Binary files /dev/null and b/apps/landing/public/images/mobile.webp differ diff --git a/apps/landing/public/images/new/comet_bg.svg b/apps/landing/public/images/new/comet_bg.svg new file mode 100644 index 000000000..144c3c796 --- /dev/null +++ b/apps/landing/public/images/new/comet_bg.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/landing/public/images/new/particles.svg b/apps/landing/public/images/new/particles.svg new file mode 100644 index 000000000..fb0e07796 --- /dev/null +++ b/apps/landing/public/images/new/particles.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/apps/landing/public/images/roadmapbg.webp b/apps/landing/public/images/roadmapbg.webp new file mode 100644 index 000000000..f62d5afe3 Binary files /dev/null and b/apps/landing/public/images/roadmapbg.webp differ diff --git a/apps/landing/public/images/search.webp b/apps/landing/public/images/search.webp new file mode 100644 index 000000000..b659de959 Binary files /dev/null and b/apps/landing/public/images/search.webp differ diff --git a/apps/landing/public/videos/Spacedrive_QuickPreview.webm b/apps/landing/public/videos/Spacedrive_QuickPreview.webm index bf1f7af19..72e650564 100644 Binary files a/apps/landing/public/videos/Spacedrive_QuickPreview.webm and b/apps/landing/public/videos/Spacedrive_QuickPreview.webm differ diff --git a/apps/landing/public/videos/Spacedrive_search.webm b/apps/landing/public/videos/Spacedrive_search.webm new file mode 100644 index 000000000..044d9fc20 Binary files /dev/null and b/apps/landing/public/videos/Spacedrive_search.webm differ diff --git a/apps/landing/public/videos/Spacedrive_tagmode.webm b/apps/landing/public/videos/Spacedrive_tagmode.webm new file mode 100644 index 000000000..f1e38149f Binary files /dev/null and b/apps/landing/public/videos/Spacedrive_tagmode.webm differ diff --git a/apps/landing/public/videos/Spacedrive_tags.webm b/apps/landing/public/videos/Spacedrive_tags.webm new file mode 100644 index 000000000..e3e876edd Binary files /dev/null and b/apps/landing/public/videos/Spacedrive_tags.webm differ diff --git a/apps/landing/src/app/Background.tsx b/apps/landing/src/app/Background.tsx deleted file mode 100644 index 5a679a278..000000000 --- a/apps/landing/src/app/Background.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import dynamic from 'next/dynamic'; -import { ReactNode, Suspense, useEffect, useState } from 'react'; -import { hasWebGLContext } from '~/utils/util'; - -const FADE = { - start: 300, // start fading out at 100px - end: 1300 // end fading out at 300px -}; - -const Space = dynamic(() => import('~/components/Space'), { ssr: false }); -const Bubbles = dynamic(() => import('~/components/Bubbles').then((m) => m.Bubbles), { - ssr: false -}); - -export function Background() { - const [opacity, setOpacity] = useState(0.6); - const [isWindowResizing, setIsWindowResizing] = useState(false); - const [canUseWebGL, setCanUseWebGL] = useState(hasWebGLContext()); - const [inner, setInner] = useState(null); - - useEffect(() => { - const handleScroll = () => { - const currentScrollY = window.scrollY; - - if (currentScrollY <= FADE.start) { - setOpacity(0.6); - } else if (currentScrollY <= FADE.end) { - const range = FADE.end - FADE.start; - const diff = currentScrollY - FADE.start; - const ratio = diff / range; - setOpacity(0.6 - ratio); - } else { - setOpacity(0); - } - }; - window.addEventListener('scroll', handleScroll); - - return () => { - window.removeEventListener('scroll', handleScroll); - }; - }, []); - - useEffect(() => { - let resizeTimer: ReturnType; - const handleResize = () => { - setIsWindowResizing(true); - clearTimeout(resizeTimer); - resizeTimer = setTimeout(() => { - setIsWindowResizing(false); - }, 100); - }; - window.addEventListener('resize', handleResize); - return () => { - window.removeEventListener('resize', handleResize); - clearTimeout(resizeTimer); - }; - }, []); - - useEffect(() => { - setInner(canUseWebGL ? setCanUseWebGL(false)} /> : ); - }, [canUseWebGL]); - - return ( -
- {!isWindowResizing && inner} -
- ); -} diff --git a/apps/landing/src/app/Footer.tsx b/apps/landing/src/app/Footer.tsx deleted file mode 100644 index 52d718fe5..000000000 --- a/apps/landing/src/app/Footer.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import { - Discord, - Github, - Instagram, - Opencollective, - Twitch, - Twitter -} from '@sd/assets/svgs/brands'; -import Image from 'next/image'; -import Link from 'next/link'; -import { PropsWithChildren } from 'react'; - -import { getLatestRelease } from './docs/changelog/data'; -import Logo from './logo.png'; - -export async function Footer() { - const latestRelease = await getLatestRelease(); - - return ( -
- footer gradient -
-
- Spacedrive logo - -

Spacedrive

-

- © Copyright {new Date().getFullYear()} Spacedrive Technology Inc. -

-
- - - - - - - - - - - - - - - - - - -
-
- -
-

About

- - Team - FAQ - Careers - {latestRelease && ( - - Changelog - - )} - Blog -
-
-

Downloads

-
- - macOS - - - macOS Intel - - - Windows - - - Linux - -
-
- Android - iOS -
-
-
-

Developers

- - Documentation - - - Contribute - -
- Extensions -
-
- Self Host -
-
-
-

Org

- - Open Collective - - - License - -
- Privacy -
-
- Terms -
-
-
-
-
-
-
-
- ); -} - -function FooterLink({ - blank, - link, - ...props -}: PropsWithChildren<{ link: string; blank?: boolean }>) { - return ( - - {props.children} - - ); -} diff --git a/apps/landing/src/app/HomeCTA.tsx b/apps/landing/src/app/HomeCTA.tsx deleted file mode 100644 index 49e396d83..000000000 --- a/apps/landing/src/app/HomeCTA.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { type IconProps } from '@phosphor-icons/react'; -import clsx from 'clsx'; -import { Button, LinkButtonProps } from '@sd/ui'; - -interface Props extends LinkButtonProps { - className?: string; - text: string; - icon?: IconProps; - onClick?: () => void; -} - -export function HomeCTA({ className, text, icon, ...props }: Props) { - return ( - - ); -} - -export default HomeCTA; diff --git a/apps/landing/src/app/NavBar/index.tsx b/apps/landing/src/app/NavBar/index.tsx deleted file mode 100644 index 7b91946ed..000000000 --- a/apps/landing/src/app/NavBar/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { Discord, Github } from '@sd/assets/svgs/brands'; -import Image from 'next/image'; -import Link from 'next/link'; -import { PropsWithChildren } from 'react'; - -import { positions } from '../careers/data'; -import Logo from '../logo.png'; -import { MobileDropdown } from './MobileDropdown'; - -export function NavBar() { - return ( -
-
- - Spacedrive logo -

Spacedrive

- - -
- Roadmap - Team - {/* Pricing */} - Blog - Docs -
- Careers - {positions.length > 0 ? ( - - {` ${positions.length} `} - - ) : null} -
-
-
- -
- - - - - - -
-
-
-
-
-
-
- ); -} - -function NavLink(props: PropsWithChildren<{ link?: string }>) { - return ( - - {props.children} - - ); -} diff --git a/apps/landing/src/app/NewBanner.tsx b/apps/landing/src/app/NewBanner.tsx deleted file mode 100644 index 7c120234d..000000000 --- a/apps/landing/src/app/NewBanner.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Newspaper } from '@phosphor-icons/react/dist/ssr'; -import clsx from 'clsx'; -import Link from 'next/link'; - -export interface NewBannerProps { - headline: string; - href?: string; - link?: string; - className?: string; -} - -export function NewBanner(props: NewBannerProps) { - const { headline, href, link } = props; - - return ( - -
- -

{headline}

-
- {link && ( - <> -
- - {link} - - - )} - - ); -} diff --git a/apps/landing/src/app/api/github/index.ts b/apps/landing/src/app/api/github/index.ts index 9133af53a..e0bc06ef9 100644 --- a/apps/landing/src/app/api/github/index.ts +++ b/apps/landing/src/app/api/github/index.ts @@ -1,7 +1,7 @@ import { type components } from '@octokit/openapi-types'; import { env } from '~/env'; -type Release = components['schemas']['release']; +export type Release = components['schemas']['release']; const FETCH_META = { headers: new Headers({ @@ -32,11 +32,16 @@ export const getRelease = (tag: string) => path: `${RELEASES_PATH}/tags/${tag}` }) as FetchConfig; +export const getRepoStats = { + path: `/repos/${env.GITHUB_ORG}/${env.GITHUB_REPO}` +} as FetchConfig; + export async function githubFetch({ path }: FetchConfig): Promise { return fetch(`https://api.github.com${path}`, { ...FETCH_META, next: { - tags: [path] + tags: [path], + revalidate: 43200 // 12 hours in seconds } }).then((r) => r.json()); } diff --git a/apps/landing/src/app/blog/[slug]/page.tsx b/apps/landing/src/app/blog/[slug]/page.tsx index e22b8a3e4..a1683b416 100644 --- a/apps/landing/src/app/blog/[slug]/page.tsx +++ b/apps/landing/src/app/blog/[slug]/page.tsx @@ -4,7 +4,7 @@ import { Metadata } from 'next'; import { useMDXComponent } from 'next-contentlayer/hooks'; import Image from 'next/image'; import { notFound } from 'next/navigation'; -import { BlogTag } from '~/components/BlogTag'; +import { BlogTag } from '~/components/blog-tag'; import { BlogMDXComponents } from '~/components/mdx'; export function generateStaticParams(): Array { @@ -43,7 +43,7 @@ export default function Page({ params }: Props) { const MDXContent = useMDXComponent(post.body.code); return ( -
+
<>
+

Blog

Get the latest from Spacedrive.

diff --git a/apps/landing/src/app/careers/data.ts b/apps/landing/src/app/careers/data.ts index 6c9e33413..bd63c027a 100644 --- a/apps/landing/src/app/careers/data.ts +++ b/apps/landing/src/app/careers/data.ts @@ -17,7 +17,36 @@ export interface PositionPosting { description: string; } -export const positions: PositionPosting[] = []; +export const positions: PositionPosting[] = [ + { + name: 'Core Engineer', + type: 'Full-time', + salary: '$140,000 - $180,000', + description: + "Lead development of Spacedrive's core storage engine and distributed filesystem in Rust. You'll architect and implement critical systems including storage classification, encrypted data sync, and filesystem operations. Deep knowledge of systems programming, distributed systems, and Rust required. Experience with FUSE filesystems and cloud storage systems highly valued." + }, + { + name: 'Platform Engineer', + type: 'Full-time', + salary: '$130,000 - $170,000', + description: + "Own Spacedrive's cross-platform client applications built with Tauri and React. Focus on performance optimization, native platform integration, and smooth user experience across desktop and mobile. Strong TypeScript/React experience required, with knowledge of Rust and native platform APIs (Windows, macOS, Linux, iOS, Android) highly valued." + }, + { + name: 'Cloud Infrastructure Engineer', + type: 'Full-time', + salary: '$150,000 - $190,000', + description: + "Design and implement Spacedrive's cloud storage infrastructure, focusing on our innovative storage architecture. Build highly scalable systems for data ingestion, storage optimization, and content delivery. Deep experience with cloud infrastructure (AWS/GCP), object storage, and distributed systems required. Knowledge of Rust and TypeScript preferred." + }, + { + name: 'Security Engineer', + type: 'Full-time', + salary: '$160,000 - $200,000', + description: + "Lead security architecture for Spacedrive's zero-knowledge encrypted storage platform. Design and implement end-to-end encryption, secure key management, and audit systems. Expertise in cryptography, secure systems design, and penetration testing required. Experience with Rust and secure storage systems highly valued." + } +]; export const values = [ { diff --git a/apps/landing/src/app/careers/page.tsx b/apps/landing/src/app/careers/page.tsx index b6755a128..b6ed62392 100644 --- a/apps/landing/src/app/careers/page.tsx +++ b/apps/landing/src/app/careers/page.tsx @@ -1,5 +1,5 @@ -import { Clock, CurrencyDollar } from '@phosphor-icons/react/dist/ssr'; -import { Button } from '@sd/ui'; +import { ArrowDown, Clock, CurrencyDollar } from '@phosphor-icons/react/dist/ssr'; +import { CtaSecondaryButton } from '~/components/cta-secondary-button'; import { perks, positions, values } from './data'; @@ -10,28 +10,34 @@ export const metadata = { export default function CareersPage() { return ( -
-
-

- Build the future of files. -

-
-

+

+
+
+

+ Build the future of files. +

+

Spacedrive is redefining the way we think about our personal data, building a open ecosystem to help preserve your digital legacy and make cross-platform file management a breeze.

- -
+ Open Positions + +
+

Our Values

@@ -40,7 +46,7 @@ export default function CareersPage() { {values.map((value, index) => (
{children}; } diff --git a/apps/landing/src/app/cloud/page.tsx b/apps/landing/src/app/cloud/page.tsx new file mode 100644 index 000000000..e36a6f258 --- /dev/null +++ b/apps/landing/src/app/cloud/page.tsx @@ -0,0 +1,7 @@ +import React from 'react'; + +const Page: React.FC = () => { + return

Cloud

; +}; + +export default Page; diff --git a/apps/landing/src/app/docs/Breadcrumbs.tsx b/apps/landing/src/app/docs/Breadcrumbs.tsx index e08a6998b..00e9e60f6 100644 --- a/apps/landing/src/app/docs/Breadcrumbs.tsx +++ b/apps/landing/src/app/docs/Breadcrumbs.tsx @@ -3,7 +3,7 @@ import { CaretRight } from '@phosphor-icons/react'; import { useParams } from 'next/navigation'; import { Fragment } from 'react'; -import { toTitleCase } from '~/utils/util'; +import { toTitleCase } from '~/utils/misc'; export function Breadcrumbs() { const { slug } = useParams<{ slug?: string[] }>(); diff --git a/apps/landing/src/app/docs/Markdown.tsx b/apps/landing/src/app/docs/Markdown.tsx index 148637e3a..304438a6f 100644 --- a/apps/landing/src/app/docs/Markdown.tsx +++ b/apps/landing/src/app/docs/Markdown.tsx @@ -12,7 +12,7 @@ export function Markdown(props: PropsWithChildren) {
diff --git a/apps/landing/src/app/docs/Sidebar/index.tsx b/apps/landing/src/app/docs/Sidebar/index.tsx index 9aea5d5fb..5869c1e7f 100644 --- a/apps/landing/src/app/docs/Sidebar/index.tsx +++ b/apps/landing/src/app/docs/Sidebar/index.tsx @@ -1,5 +1,5 @@ import { iconConfig } from '~/utils/contentlayer'; -import { toTitleCase } from '~/utils/util'; +import { toTitleCase } from '~/utils/misc'; import { getReleasesCategories } from '../changelog/data'; import { navigationMeta } from '../data'; diff --git a/apps/landing/src/app/docs/[...slug]/page.tsx b/apps/landing/src/app/docs/[...slug]/page.tsx index acc448e07..210ea4bfd 100644 --- a/apps/landing/src/app/docs/[...slug]/page.tsx +++ b/apps/landing/src/app/docs/[...slug]/page.tsx @@ -6,7 +6,7 @@ import { getMDXComponent } from 'next-contentlayer/hooks'; import Link from 'next/link'; import { notFound } from 'next/navigation'; import { DocMDXComponents } from '~/components/mdx'; -import { toTitleCase } from '~/utils/util'; +import { toTitleCase } from '~/utils/misc'; import { getDoc } from '../data'; import { Markdown } from '../Markdown'; diff --git a/apps/landing/src/app/docs/changelog/[category]/[tag]/page.tsx b/apps/landing/src/app/docs/changelog/[category]/[tag]/page.tsx index eecdcc15c..e0bd2ecd9 100644 --- a/apps/landing/src/app/docs/changelog/[category]/[tag]/page.tsx +++ b/apps/landing/src/app/docs/changelog/[category]/[tag]/page.tsx @@ -3,7 +3,7 @@ import { getMDXComponent } from 'next-contentlayer/hooks'; import { notFound } from 'next/navigation'; import { getRelease, githubFetch } from '~/app/api/github'; import { DocMDXComponents } from '~/components/mdx'; -import { toTitleCase } from '~/utils/util'; +import { toTitleCase } from '~/utils/misc'; import { Markdown } from '../../../Markdown'; import { getReleasesCategories } from '../../data'; diff --git a/apps/landing/src/app/docs/changelog/data.ts b/apps/landing/src/app/docs/changelog/data.ts index b36b808c3..a7de94665 100644 --- a/apps/landing/src/app/docs/changelog/data.ts +++ b/apps/landing/src/app/docs/changelog/data.ts @@ -1,5 +1,5 @@ import { getRecentReleases, getReleaseFrontmatter, githubFetch } from '~/app/api/github'; -import { toTitleCase } from '~/utils/util'; +import { toTitleCase } from '~/utils/misc'; import { SectionMeta } from '../data'; diff --git a/apps/landing/src/app/docs/layout.tsx b/apps/landing/src/app/docs/layout.tsx index 7972c44a0..237b679da 100644 --- a/apps/landing/src/app/docs/layout.tsx +++ b/apps/landing/src/app/docs/layout.tsx @@ -20,7 +20,9 @@ export default function Layout({ children }: PropsWithChildren) {
{sidebar} -