diff --git a/Cargo.lock b/Cargo.lock index 438804c8d..205756182 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 3b10068ee..c6e3b535e 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -10,24 +10,25 @@ edition = { workspace = true } [dependencies] tauri = { version = "1.5.2", features = [ - "dialog-all", - "linux-protocol-headers", - "macos-private-api", - "os-all", - "path-all", - "protocol-all", - "shell-all", - "updater", - "window-all", - "native-tls-vendored", + "macos-private-api", + "path-all", + "protocol-all", + "os-all", + "shell-all", + "dialog-all", + "linux-protocol-headers", + "updater", + "window-all", + "native-tls-vendored", ] } rspc = { workspace = true, features = ["tauri"] } sd-core = { path = "../../../core", features = [ - "ffmpeg", - "location-watcher", - "heif", + "ffmpeg", + "location-watcher", + "heif", ] } +sd-fda = { path = "../../../crates/fda" } tokio = { workspace = true, features = ["sync"] } tracing = { workspace = true } serde = "1.0.190" diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index a65bd6df8..b39d83a59 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -7,6 +7,7 @@ use std::{fs, path::PathBuf, sync::Arc, time::Duration}; use sd_core::{Node, NodeError}; +use sd_fda::DiskAccess; use tauri::{ api::path, ipc::RemoteDomainAccessScope, window::PlatformWebview, AppHandle, Manager, WindowEvent, @@ -27,10 +28,16 @@ mod updater; #[specta::specta] async fn app_ready(app_handle: AppHandle) { let window = app_handle.get_window("main").unwrap(); - window.show().unwrap(); } +#[tauri::command(async)] +#[specta::specta] +// If this erorrs, we don't have FDA and we need to re-prompt for it +async fn request_fda_macos() { + DiskAccess::request_fda().expect("Unable to request full disk access"); +} + #[tauri::command(async)] #[specta::specta] async fn set_menu_bar_item_state(_window: tauri::Window, _id: String, _enabled: bool) { @@ -302,6 +309,7 @@ async fn main() -> tauri::Result<()> { refresh_menu_bar, reload_webview, set_menu_bar_item_state, + request_fda_macos, file::open_file_paths, file::open_ephemeral_files, file::get_file_path_open_with_apps, diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index bf88f6e04..add9d7116 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -17,6 +17,8 @@ import { getSpacedropState } from '@sd/interface/hooks/useSpacedropState'; import '@sd/ui/style/style.scss'; +import { useOperatingSystem } from '@sd/interface/hooks'; + import * as commands from './commands'; import { platform } from './platform'; import { queryClient } from './query'; @@ -79,9 +81,10 @@ export default function App() { const TAB_CREATE_DELAY = 150; function AppInner() { + const os = useOperatingSystem(); function createTab() { const history = createMemoryHistory(); - const router = createMemoryRouterWithHistory({ routes, history }); + const router = createMemoryRouterWithHistory({ routes: routes(os), history }); const dispose = router.subscribe((event) => { setTabs((routers) => { diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index 9a5279985..69b428610 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -34,6 +34,10 @@ export function setMenuBarItemState(id: string, enabled: boolean) { return invoke()("set_menu_bar_item_state", { id,enabled }) } +export function requestFdaMacos() { + return invoke()("request_fda_macos") +} + export function openFilePaths(library: string, ids: number[]) { return invoke()("open_file_paths", { library,ids }) } diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index b1e7c6bc1..de10f027d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -3,7 +3,7 @@ import { useEffect, useRef, useState } from 'react'; import { createBrowserRouter } from 'react-router-dom'; import { RspcProvider } from '@sd/client'; import { Platform, PlatformProvider, routes, SpacedriveInterface } from '@sd/interface'; -import { useShowControls } from '@sd/interface/hooks'; +import { useOperatingSystem, useShowControls } from '@sd/interface/hooks'; import demoData from './demoData.json'; import ScreenshotWrapper from './ScreenshotWrapper'; @@ -76,8 +76,9 @@ const queryClient = new QueryClient({ }); function App() { + const os = useOperatingSystem(); const [router, setRouter] = useState(() => { - const router = createBrowserRouter(routes); + const router = createBrowserRouter(routes(os)); router.subscribe((event) => { setRouter((router) => { diff --git a/core/src/p2p/operations/mod.rs b/core/src/p2p/operations/mod.rs index 0bfe54690..2083c64f8 100644 --- a/core/src/p2p/operations/mod.rs +++ b/core/src/p2p/operations/mod.rs @@ -2,6 +2,5 @@ pub mod ping; pub mod request_file; pub mod spacedrop; -pub use ping::ping; pub use request_file::request_file; pub use spacedrop::spacedrop; diff --git a/crates/fda/Cargo.toml b/crates/fda/Cargo.toml index d0188fd0f..2c0f82581 100644 --- a/crates/fda/Cargo.toml +++ b/crates/fda/Cargo.toml @@ -7,6 +7,4 @@ repository = { workspace = true } edition = { workspace = true } [dependencies] -dirs = "5.0.1" -tokio = { workspace = true, features = ["rt-multi-thread", "fs", "macros"] } thiserror = "1.0.50" diff --git a/crates/fda/README.md b/crates/fda/README.md index 084028de4..e3d2273cb 100644 --- a/crates/fda/README.md +++ b/crates/fda/README.md @@ -1,7 +1,5 @@ # Spacedrive FDA Handling -## Platforms - -### MacOS +## MacOS On MacOS, we are able to open the "Full disk access" settings prompt to instruct the user to allow Spacedrive full disk access, which should alleviate all permissions issues. diff --git a/crates/fda/src/error.rs b/crates/fda/src/error.rs index ca0986462..f2319ad6c 100644 --- a/crates/fda/src/error.rs +++ b/crates/fda/src/error.rs @@ -1,10 +1,5 @@ -use std::path::PathBuf; - #[derive(Debug, thiserror::Error)] pub enum Error { - #[error("unable to access path: {0}")] - PermissionDenied(PathBuf), - #[cfg(target_os = "macos")] #[error("there was an error while prompting for full disk access")] FDAPromptError, diff --git a/crates/fda/src/lib.rs b/crates/fda/src/lib.rs index 5149d8527..d1a0dfd77 100644 --- a/crates/fda/src/lib.rs +++ b/crates/fda/src/lib.rs @@ -24,43 +24,20 @@ #![forbid(unsafe_code, deprecated_in_future)] #![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)] -use std::{io::ErrorKind, path::PathBuf}; - -use dirs::{ - audio_dir, cache_dir, config_dir, config_local_dir, data_dir, data_local_dir, desktop_dir, - document_dir, download_dir, executable_dir, home_dir, picture_dir, preference_dir, public_dir, - runtime_dir, state_dir, template_dir, video_dir, -}; - pub mod error; use error::Result; -pub struct FullDiskAccess(Vec); +pub struct DiskAccess; -impl FullDiskAccess { - async fn can_access_path(path: PathBuf) -> bool { - match tokio::fs::read_dir(path).await { - Ok(_) => true, - Err(e) => !matches!(e.kind(), ErrorKind::PermissionDenied), - } - } - - pub async fn has_fda() -> bool { - let dirs = Self::default(); - for dir in dirs.0 { - if !Self::can_access_path(dir).await { - return false; - } - } - true - } - - #[allow(clippy::missing_const_for_fn)] +impl DiskAccess { + /// This function is a no-op on non-MacOS systems. + /// + /// Once ran, it will open the "Full Disk Access" prompt. pub fn request_fda() -> Result<()> { #[cfg(target_os = "macos")] { - use error::Error; + use crate::error::Error; use std::process::Command; Command::new("open") @@ -73,49 +50,14 @@ impl FullDiskAccess { } } -impl Default for FullDiskAccess { - fn default() -> Self { - Self( - [ - audio_dir(), - cache_dir(), - config_dir(), - config_local_dir(), - data_dir(), - data_local_dir(), - desktop_dir(), - document_dir(), - download_dir(), - executable_dir(), - home_dir(), - picture_dir(), - preference_dir(), - public_dir(), - runtime_dir(), - state_dir(), - template_dir(), - video_dir(), - ] - .into_iter() - .flatten() - .collect(), - ) - } -} - #[cfg(test)] mod tests { - use super::FullDiskAccess; + use super::DiskAccess; #[test] #[cfg_attr(miri, ignore = "Miri can't run this test")] #[ignore = "CI can't run this due to lack of a GUI"] fn macos_open_full_disk_prompt() { - FullDiskAccess::request_fda().unwrap(); - } - - #[tokio::test] - async fn has_fda() { - FullDiskAccess::has_fda().await; + DiskAccess::request_fda().unwrap(); } } diff --git a/interface/app/$libraryId/settings/client/general.tsx b/interface/app/$libraryId/settings/client/general.tsx index 928fcb744..1a174a545 100644 --- a/interface/app/$libraryId/settings/client/general.tsx +++ b/interface/app/$libraryId/settings/client/general.tsx @@ -11,7 +11,7 @@ import { } from '@sd/client'; import { Button, Card, Input, Select, SelectOption, Slider, Switch, tw, z } from '@sd/ui'; import { Icon } from '~/components'; -import { useDebouncedFormWatch } from '~/hooks'; +import { useDebouncedFormWatch, useOperatingSystem } from '~/hooks'; import { usePlatform } from '~/util/Platform'; import { Heading } from '../Layout'; @@ -29,6 +29,8 @@ export const Component = () => { const debugState = useDebugState(); const editNode = useBridgeMutation('nodes.edit'); const connectedPeers = useConnectedPeers(); + const os = useOperatingSystem(); + const { requestFdaMacos } = usePlatform(); const updateThumbnailerPreferences = useBridgeMutation('nodes.updateThumbnailerPreferences'); const form = useZodForm({ @@ -178,6 +180,18 @@ export const Component = () => { + {os === 'macOS' && ( + + + + )} + { const platform = usePlatform(); const libraryId = useLibraryContext().library.uuid; - const navigate = useNavigate(); const transition = { type: 'keyframes', @@ -41,28 +39,30 @@ export const AddLocationButton = ({ path, className, onClick, ...props }: AddLoc setIsOverflowing(text.scrollWidth > overflow.clientWidth); }, [overflowRef, textRef]); + const locationDialogHandler = async () => { + if (!path) { + path = (await openDirectoryPickerDialog(platform)) ?? undefined; + } + // Remember `path` will be `undefined` on web cause the user has to provide it in the modal + if (path !== '') + dialogManager.create((dp) => ( + + )); + }; + return ( <> + + + + ) : ( +
+
+ )} +
+ + {showVideo && ( + + )} +
+ + ); +}; diff --git a/interface/app/onboarding/index.tsx b/interface/app/onboarding/index.tsx index f2b1da876..280f3359e 100644 --- a/interface/app/onboarding/index.tsx +++ b/interface/app/onboarding/index.tsx @@ -1,9 +1,11 @@ import { Navigate, RouteObject } from 'react-router'; import { getOnboardingStore } from '@sd/client'; +import { OperatingSystem } from '~/util/Platform'; import Alpha from './alpha'; import { useOnboardingContext } from './context'; import CreatingLibrary from './creating-library'; +import { FullDisk } from './full-disk'; import Locations from './locations'; import NewLibrary from './new-library'; import Privacy from './privacy'; @@ -18,30 +20,16 @@ const Index = () => { return ; }; -export default [ - { - index: true, - element: - }, - { path: 'alpha', element: }, - // { - // element: , - // path: 'login' - // }, - { - element: , - path: 'new-library' - }, - { - element: , - path: 'locations' - }, - { - element: , - path: 'privacy' - }, - { - element: , - path: 'creating-library' - } -] satisfies RouteObject[]; +const onboardingRoutes = (os: OperatingSystem) => { + return [ + { index: true, element: }, + { path: 'alpha', element: }, + { path: 'new-library', element: }, + ...(os === 'macOS' ? [{ element: , path: 'full-disk' }] : []), + { path: 'locations', element: }, + { path: 'privacy', element: }, + { path: 'creating-library', element: } + ] satisfies RouteObject[]; +}; + +export default onboardingRoutes; diff --git a/interface/app/onboarding/locations.tsx b/interface/app/onboarding/locations.tsx index c9b2236fc..9ac45ac9b 100644 --- a/interface/app/onboarding/locations.tsx +++ b/interface/app/onboarding/locations.tsx @@ -90,6 +90,19 @@ export default function OnboardingLocations() { className="flex flex-col items-center" > +
+ + + +
Add Locations Enhance your Spacedrive experience by adding your favorite locations to your diff --git a/interface/app/onboarding/new-library.tsx b/interface/app/onboarding/new-library.tsx index fd6b1bbd1..0a2a5479b 100644 --- a/interface/app/onboarding/new-library.tsx +++ b/interface/app/onboarding/new-library.tsx @@ -2,12 +2,14 @@ import { useState } from 'react'; import { useNavigate } from 'react-router'; import { Button, Form, InputField } from '@sd/ui'; import { Icon } from '~/components'; +import { useOperatingSystem } from '~/hooks'; import { OnboardingContainer, OnboardingDescription, OnboardingTitle } from './components'; import { useOnboardingContext } from './context'; export default function OnboardingNewLibrary() { const navigate = useNavigate(); + const os = useOperatingSystem(); const form = useOnboardingContext().forms.useForm('new-library'); const [importMode, setImportMode] = useState(false); @@ -20,11 +22,11 @@ export default function OnboardingNewLibrary() {
{ - navigate('../locations', { replace: true }); + navigate(`../${os === 'macOS' ? 'full-disk' : 'locations'}`, { replace: true }); })} > - + Create a Library Libraries are a secure, on-device database. Your files remain where they are, @@ -32,7 +34,7 @@ export default function OnboardingNewLibrary() { {importMode ? ( -
+
@@ -51,7 +53,7 @@ export default function OnboardingNewLibrary() { placeholder={'e.g. "James\' Library"'} />
-
+