mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 11:13:29 +00:00
[ENG-1413] Full disk access perms (#1791)
* fda wip * clippy * add tauri invoke fns for FDA * fda wip * clippy * add tauri invoke fns for FDA * wip * fda wip * clippy * add tauri invoke fns for FDA * wip * wip * wip fda * remove imports * hopefully improve FDA * execute only on macos * ts * ts * Update Platform.tsx * Update AddLocationButton.tsx * remove console log * fix: fda and add unit tests * temp commit for Jake * add fda state and keybind handling (so the frontend is kept up to date) * update FDA * update imports * testing purposes * Jakes work * fix fda checks * work in progress (but not working) * remove dead files * attempt #2 * !!!temporarily enable devtools in prod * remove alert * show FDA screen but don't require it * add an FDA button to general client settings * Update AddLocationButton.tsx * remove dead code * unused dep * old errors * remove import * dead code * dead code + typesafety * eslint * remove fda dialog references * remove mp4 vid * hopefully fix onboarding for non-macos OSes * shorter nav --------- Co-authored-by: jake <77554505+brxken128@users.noreply.github.com>
This commit is contained in:
parent
797e4821b6
commit
a7fed83556
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -1677,15 +1677,6 @@ dependencies = [
|
|||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225"
|
||||
dependencies = [
|
||||
"dirs-sys 0.4.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dirs-next"
|
||||
version = "2.0.0"
|
||||
|
@ -6939,6 +6930,7 @@ dependencies = [
|
|||
"sd-desktop-linux",
|
||||
"sd-desktop-macos",
|
||||
"sd-desktop-windows",
|
||||
"sd-fda",
|
||||
"sd-prisma",
|
||||
"serde",
|
||||
"specta",
|
||||
|
@ -6983,9 +6975,7 @@ dependencies = [
|
|||
name = "sd-fda"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"dirs",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -34,6 +34,10 @@ export function setMenuBarItemState(id: string, enabled: boolean) {
|
|||
return invoke()<null>("set_menu_bar_item_state", { id,enabled })
|
||||
}
|
||||
|
||||
export function requestFdaMacos() {
|
||||
return invoke()<null>("request_fda_macos")
|
||||
}
|
||||
|
||||
export function openFilePaths(library: string, ids: number[]) {
|
||||
return invoke()<OpenFilePathResult[]>("open_file_paths", { library,ids })
|
||||
}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<PathBuf>);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = () => {
|
|||
</div>
|
||||
</Card>
|
||||
|
||||
{os === 'macOS' && (
|
||||
<Setting
|
||||
mini
|
||||
title="Full disk access"
|
||||
description="Enable full disk access to allow Spacedrive to index additional files."
|
||||
>
|
||||
<Button onClick={requestFdaMacos} variant="gray" size="sm" className="my-5">
|
||||
Enable
|
||||
</Button>
|
||||
</Setting>
|
||||
)}
|
||||
|
||||
<Setting
|
||||
mini
|
||||
title="Debug mode"
|
||||
|
|
|
@ -2,7 +2,6 @@ import { FolderSimplePlus } from '@phosphor-icons/react';
|
|||
import clsx from 'clsx';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useLibraryContext } from '@sd/client';
|
||||
import { Button, dialogManager, type ButtonProps } from '@sd/ui';
|
||||
import { useCallbackToWatchResize } from '~/hooks';
|
||||
|
@ -19,7 +18,6 @@ interface AddLocationButton extends ButtonProps {
|
|||
export const AddLocationButton = ({ path, className, onClick, ...props }: AddLocationButton) => {
|
||||
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) => (
|
||||
<AddLocationDialog path={path ?? ''} libraryId={libraryId} {...dp} />
|
||||
));
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="dotted"
|
||||
className={clsx('w-full', className)}
|
||||
onClick={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) => (
|
||||
<AddLocationDialog path={path ?? ''} libraryId={libraryId} {...dp} />
|
||||
));
|
||||
|
||||
await locationDialogHandler();
|
||||
onClick?.();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{path ? (
|
||||
<div className="flex h-full w-full flex-row items-end whitespace-nowrap font-mono text-sm">
|
||||
<div className="flex flex-row items-end w-full h-full font-mono text-sm whitespace-nowrap">
|
||||
<FolderSimplePlus size={22} className="shrink-0" />
|
||||
<div className="ml-1 overflow-hidden">
|
||||
<motion.span
|
||||
|
|
|
@ -10,6 +10,10 @@ import { RootContext } from './RootContext';
|
|||
|
||||
import './style.scss';
|
||||
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
|
||||
import { OperatingSystem } from '..';
|
||||
|
||||
const Index = () => {
|
||||
const libraries = useCachedLibraries();
|
||||
|
||||
|
@ -40,28 +44,30 @@ const Wrapper = () => {
|
|||
// the `usePlausiblePageViewMonitor` hook, as early as possible (ideally within the layout itself).
|
||||
// the hook should only be included if there's a valid `ClientContext` (so not onboarding)
|
||||
|
||||
export const routes = [
|
||||
{
|
||||
element: <Wrapper />,
|
||||
errorElement: <RouterErrorBoundary />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Index />
|
||||
},
|
||||
{
|
||||
path: 'onboarding',
|
||||
lazy: () => import('./onboarding/Layout'),
|
||||
children: onboardingRoutes
|
||||
},
|
||||
{
|
||||
path: ':libraryId',
|
||||
lazy: () => import('./$libraryId/Layout'),
|
||||
children: libraryRoutes
|
||||
}
|
||||
]
|
||||
}
|
||||
] satisfies RouteObject[];
|
||||
export const routes = (os: OperatingSystem) => {
|
||||
return [
|
||||
{
|
||||
element: <Wrapper />,
|
||||
errorElement: <RouterErrorBoundary />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Index />
|
||||
},
|
||||
{
|
||||
path: 'onboarding',
|
||||
lazy: () => import('./onboarding/Layout'),
|
||||
children: onboardingRoutes(os)
|
||||
},
|
||||
{
|
||||
path: ':libraryId',
|
||||
lazy: () => import('./$libraryId/Layout'),
|
||||
children: libraryRoutes
|
||||
}
|
||||
]
|
||||
}
|
||||
] satisfies RouteObject[];
|
||||
};
|
||||
|
||||
/**
|
||||
* Combines the `path` segments of the current route into a single string.
|
||||
|
@ -72,6 +78,7 @@ const useRawRoutePath = () => {
|
|||
// `useMatches` returns a list of each matched RouteObject,
|
||||
// we grab the last one as it contains all previous route segments.
|
||||
const lastMatchId = useMatches().slice(-1)[0]?.id;
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const rawPath = useMemo(() => {
|
||||
const [rawPath] =
|
||||
|
@ -93,11 +100,11 @@ const useRawRoutePath = () => {
|
|||
// `path` found, chuck it on the end
|
||||
return [`${rawPath}/${item.path}`, item];
|
||||
},
|
||||
['' as string, { children: routes }] as const
|
||||
['' as string, { children: routes(os) }] as const
|
||||
) ?? [];
|
||||
|
||||
return rawPath ?? '/';
|
||||
}, [lastMatchId]);
|
||||
}, [lastMatchId, os]);
|
||||
|
||||
return rawPath;
|
||||
};
|
||||
|
|
|
@ -2,12 +2,14 @@ import clsx from 'clsx';
|
|||
import { useEffect } from 'react';
|
||||
import { useMatch, useNavigate } from 'react-router';
|
||||
import { getOnboardingStore, unlockOnboardingScreen, useOnboardingStore } from '@sd/client';
|
||||
import { useOperatingSystem } from '~/hooks';
|
||||
|
||||
import routes from '.';
|
||||
|
||||
export default function OnboardingProgress() {
|
||||
const obStore = useOnboardingStore();
|
||||
const navigate = useNavigate();
|
||||
const os = useOperatingSystem();
|
||||
|
||||
const match = useMatch('/onboarding/:screen');
|
||||
|
||||
|
@ -22,7 +24,7 @@ export default function OnboardingProgress() {
|
|||
return (
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<div className="flex items-center justify-center space-x-1">
|
||||
{routes.map(({ path }) => {
|
||||
{routes(os).map(({ path }) => {
|
||||
if (!path) return null;
|
||||
|
||||
return (
|
||||
|
|
63
interface/app/onboarding/full-disk.tsx
Normal file
63
interface/app/onboarding/full-disk.tsx
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Fda } from '@sd/assets/videos';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { Button } from '@sd/ui';
|
||||
import { Icon } from '~/components';
|
||||
import { usePlatform } from '~/util/Platform';
|
||||
|
||||
import { OnboardingContainer, OnboardingDescription, OnboardingTitle } from './components';
|
||||
|
||||
export const FullDisk = () => {
|
||||
const { requestFdaMacos } = usePlatform();
|
||||
const [showVideo, setShowVideo] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<OnboardingContainer>
|
||||
<Icon name="HDD" size={80} />
|
||||
<OnboardingTitle>Full disk access</OnboardingTitle>
|
||||
<OnboardingDescription>
|
||||
To provide the best experience, we need access to your disk in order to index your
|
||||
files. Your files are only available to you.
|
||||
</OnboardingDescription>
|
||||
{!showVideo ? (
|
||||
<>
|
||||
<div className="flex items-center gap-3">
|
||||
<Button onClick={requestFdaMacos} variant="gray" size="sm" className="my-5">
|
||||
Enable access
|
||||
</Button>
|
||||
<Button onClick={() => setShowVideo((t) => !t)} variant="outline">
|
||||
How to enable
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-5 w-full max-w-[450px]">
|
||||
<video className="rounded-md" autoPlay loop muted controls={false} src={Fda} />
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigate('../locations', { replace: true });
|
||||
}}
|
||||
variant="accent"
|
||||
size="sm"
|
||||
className="mt-8"
|
||||
>
|
||||
Continue
|
||||
</Button>
|
||||
{showVideo && (
|
||||
<Button
|
||||
onClick={() => setShowVideo((t) => !t)}
|
||||
variant="gray"
|
||||
size="sm"
|
||||
className="mt-8"
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</OnboardingContainer>
|
||||
);
|
||||
};
|
|
@ -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 <Navigate to="alpha" replace />;
|
||||
};
|
||||
|
||||
export default [
|
||||
{
|
||||
index: true,
|
||||
element: <Index />
|
||||
},
|
||||
{ path: 'alpha', element: <Alpha /> },
|
||||
// {
|
||||
// element: <Login />,
|
||||
// path: 'login'
|
||||
// },
|
||||
{
|
||||
element: <NewLibrary />,
|
||||
path: 'new-library'
|
||||
},
|
||||
{
|
||||
element: <Locations />,
|
||||
path: 'locations'
|
||||
},
|
||||
{
|
||||
element: <Privacy />,
|
||||
path: 'privacy'
|
||||
},
|
||||
{
|
||||
element: <CreatingLibrary />,
|
||||
path: 'creating-library'
|
||||
}
|
||||
] satisfies RouteObject[];
|
||||
const onboardingRoutes = (os: OperatingSystem) => {
|
||||
return [
|
||||
{ index: true, element: <Index /> },
|
||||
{ path: 'alpha', element: <Alpha /> },
|
||||
{ path: 'new-library', element: <NewLibrary /> },
|
||||
...(os === 'macOS' ? [{ element: <FullDisk />, path: 'full-disk' }] : []),
|
||||
{ path: 'locations', element: <Locations /> },
|
||||
{ path: 'privacy', element: <Privacy /> },
|
||||
{ path: 'creating-library', element: <CreatingLibrary /> }
|
||||
] satisfies RouteObject[];
|
||||
};
|
||||
|
||||
export default onboardingRoutes;
|
||||
|
|
|
@ -90,6 +90,19 @@ export default function OnboardingLocations() {
|
|||
className="flex flex-col items-center"
|
||||
>
|
||||
<OnboardingContainer>
|
||||
<div className="flex items-center">
|
||||
<Icon
|
||||
name="Folder"
|
||||
size={40}
|
||||
className="relative right-[-26px] z-0 brightness-[0.5]"
|
||||
/>
|
||||
<Icon name="Folder" size={60} className="relative z-[5] brightness-[0.8]" />
|
||||
<Icon
|
||||
name="Folder"
|
||||
size={46}
|
||||
className="relative left-[-25px] z-[0] brightness-[0.6]"
|
||||
/>
|
||||
</div>
|
||||
<OnboardingTitle>Add Locations</OnboardingTitle>
|
||||
<OnboardingDescription>
|
||||
Enhance your Spacedrive experience by adding your favorite locations to your
|
||||
|
|
|
@ -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() {
|
|||
<Form
|
||||
form={form}
|
||||
onSubmit={form.handleSubmit(() => {
|
||||
navigate('../locations', { replace: true });
|
||||
navigate(`../${os === 'macOS' ? 'full-disk' : 'locations'}`, { replace: true });
|
||||
})}
|
||||
>
|
||||
<OnboardingContainer>
|
||||
<Icon name="Database" size={80} className="mb-2" />
|
||||
<Icon name="Database" size={80} />
|
||||
<OnboardingTitle>Create a Library</OnboardingTitle>
|
||||
<OnboardingDescription>
|
||||
Libraries are a secure, on-device database. Your files remain where they are,
|
||||
|
@ -32,7 +34,7 @@ export default function OnboardingNewLibrary() {
|
|||
</OnboardingDescription>
|
||||
|
||||
{importMode ? (
|
||||
<div className="mt-7 space-x-2">
|
||||
<div className="space-x-2 mt-7">
|
||||
<Button onClick={handleImport} variant="accent" size="sm">
|
||||
Import
|
||||
</Button>
|
||||
|
@ -51,7 +53,7 @@ export default function OnboardingNewLibrary() {
|
|||
placeholder={'e.g. "James\' Library"'}
|
||||
/>
|
||||
<div className="flex grow" />
|
||||
<div className="mt-7 space-x-2">
|
||||
<div className="space-x-2 mt-7">
|
||||
<Button
|
||||
type="submit"
|
||||
variant="accent"
|
||||
|
|
|
@ -36,6 +36,7 @@ export type Platform = {
|
|||
| { Ephemeral: { path: string } }
|
||||
)[]
|
||||
): Promise<unknown>;
|
||||
requestFdaMacos?(): void;
|
||||
getFilePathOpenWithApps?(library: string, ids: number[]): Promise<unknown>;
|
||||
reloadWebview?(): Promise<unknown>;
|
||||
getEphemeralFilesOpenWithApps?(paths: string[]): Promise<unknown>;
|
||||
|
|
|
@ -13,7 +13,7 @@ import { dirname, join } from 'node:path';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import prettier from 'prettier';
|
||||
|
||||
const assetFolders = ['icons', 'images', 'svgs/brands', 'svgs/ext/Extras', 'svgs/ext/Code'];
|
||||
const assetFolders = ['icons', 'images', 'svgs/brands', 'svgs/ext/Extras', 'svgs/ext/Code', 'videos'];
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
|
BIN
packages/assets/videos/fda.mp4
Normal file
BIN
packages/assets/videos/fda.mp4
Normal file
Binary file not shown.
5
packages/assets/videos/index.ts
Normal file
5
packages/assets/videos/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import Fda from './fda.mp4';
|
||||
|
||||
export {
|
||||
Fda
|
||||
}
|
Loading…
Reference in a new issue