Another routing overhaul (#577)

* whole lotta routing

* remove stats refreshing

* start integrating new contexts on mobile

* update mobile to please typescript

* fix mobile

---------

Co-authored-by: Utku Bakir <74243531+utkubakir@users.noreply.github.com>
This commit is contained in:
Brendan Allan 2023-02-23 17:56:32 +08:00 committed by GitHub
parent 810b5161dc
commit bfc3ca0f9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 447 additions and 515 deletions

View file

@ -12,12 +12,14 @@ import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { MenuProvider } from 'react-native-popup-menu';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useDeviceContext } from 'twrnc';
import { proxy, useSnapshot } from 'valtio';
import {
ClientContextProvider,
LibraryContextProvider,
getDebugState,
queryClient,
rspc,
useCurrentLibrary,
useClientContext,
useInvalidateQuery
} from '@sd/client';
import { GlobalModals } from './components/modal/GlobalModals';
@ -25,6 +27,7 @@ import { reactNativeLink } from './lib/rspcReactNativeTransport';
import { tw } from './lib/tailwind';
import RootNavigator from './navigation';
import OnboardingNavigator from './navigation/OnboardingNavigator';
import { currentLibraryStore } from './utils/nav';
dayjs.extend(advancedFormat);
dayjs.extend(relativeTime);
@ -35,33 +38,47 @@ const NavigatorTheme: Theme = {
colors: {
...DefaultTheme.colors,
// Default screen background
background: tw.color('app')
background: tw.color('app')!
}
};
function AppNavigation() {
const { library } = useClientContext();
// TODO: Make sure library has actually been loaded by this point - precache with useCachedLibraries?
// if (library === undefined) throw new Error("Tried to render AppNavigation before libraries fetched!")
return (
<NavigationContainer theme={NavigatorTheme}>
{!library ? (
<OnboardingNavigator />
) : (
<LibraryContextProvider library={library}>
<RootNavigator />
<GlobalModals />
</LibraryContextProvider>
)}
</NavigationContainer>
);
}
function AppContainer() {
// Enables dark mode, and screen size breakpoints, etc. for tailwind
useDeviceContext(tw, { withDeviceColorScheme: false });
useInvalidateQuery();
const { library } = useCurrentLibrary();
const { id } = useSnapshot(currentLibraryStore);
return (
<SafeAreaProvider style={tw`bg-app flex-1`}>
<GestureHandlerRootView style={tw`flex-1`}>
<MenuProvider>
<BottomSheetModalProvider>
<StatusBar style="light" />
<NavigationContainer theme={NavigatorTheme}>
{!library ? (
<OnboardingNavigator />
) : (
<>
<RootNavigator />
<GlobalModals />
</>
)}
</NavigationContainer>
<ClientContextProvider currentLibraryId={id}>
<AppNavigation />
</ClientContextProvider>
</BottomSheetModalProvider>
</MenuProvider>
</GestureHandlerRootView>
@ -85,13 +102,7 @@ export default function App() {
return (
<rspc.Provider client={client} queryClient={queryClient}>
<LibraryContextProvider
onNoLibrary={() => {
console.log('TODO');
}}
>
<AppContainer />
</LibraryContextProvider>
<AppContainer />
</rspc.Provider>
);
}

View file

@ -1,7 +1,8 @@
import { useState } from 'react';
import { queryClient, useBridgeMutation, useCurrentLibrary } from '@sd/client';
import { queryClient, useBridgeMutation } from '@sd/client';
import Dialog from '~/components/layout/Dialog';
import { Input } from '~/components/primitive/Input';
import { currentLibraryStore } from '~/utils/nav';
type Props = {
onSubmit?: () => void;
@ -14,8 +15,6 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
const [libName, setLibName] = useState('');
const [isOpen, setIsOpen] = useState(false);
const { switchLibrary } = useCurrentLibrary();
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
'library.create',
{
@ -27,7 +26,7 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
queryClient.setQueryData(['library.list'], (libraries: any) => [...(libraries || []), lib]);
// Switch to the new library
switchLibrary(lib.uuid);
currentLibraryStore.id = lib.uuid;
onSubmit?.();
},

View file

@ -4,8 +4,9 @@ import { MotiView } from 'moti';
import { CaretDown, Gear, Lock, Plus } from 'phosphor-react-native';
import { useEffect, useState } from 'react';
import { Alert, Pressable, Text, View } from 'react-native';
import { useCurrentLibrary } from '~/../../../packages/client/src';
import { useClientContext } from '@sd/client';
import { tw, twStyle } from '~/lib/tailwind';
import { currentLibraryStore } from '~/utils/nav';
import { AnimatedHeight } from '../animation/layout';
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
import Divider from '../primitive/Divider';
@ -19,7 +20,7 @@ const DrawerLibraryManager = () => {
if (!isDrawerOpen) setDropdownClosed(true);
}, [isDrawerOpen]);
const { library: currentLibrary, libraries, switchLibrary } = useCurrentLibrary();
const { library: currentLibrary, libraries } = useClientContext();
const navigation = useNavigation();
@ -34,7 +35,7 @@ const DrawerLibraryManager = () => {
: 'border-b-app-box border-sidebar-line bg-sidebar-button rounded-t-md'
)}
>
<Text style={tw`text-ink text-sm font-semibold`}>{currentLibrary?.config.name}</Text>
<Text style={tw`text-ink text-sm font-semibold`}>{currentLibrary.config.name}</Text>
<MotiView
animate={{
rotate: dropdownClosed ? '0deg' : '180deg',
@ -47,27 +48,30 @@ const DrawerLibraryManager = () => {
</View>
</Pressable>
<AnimatedHeight hide={dropdownClosed}>
<View style={tw`bg-sidebar-button border-sidebar-line rounded-b-md border-x border-b p-2`}>
<View style={tw`bg-sidebar-button border-sidebar-line rounded-b-md p-2`}>
{/* Libraries */}
{libraries?.map((library) => (
<Pressable key={library.uuid} onPress={() => switchLibrary(library.uuid)}>
<View
style={twStyle(
'mt-1 p-2',
currentLibrary.uuid === library.uuid && 'bg-accent rounded'
)}
>
<Text
{libraries.data?.map((library) => {
console.log('library', library);
return (
<Pressable key={library.uuid} onPress={() => (currentLibraryStore.id = library.uuid)}>
<View
style={twStyle(
'text-ink text-sm font-semibold',
currentLibrary.uuid === library.uuid && 'text-white'
'mt-1 p-2',
currentLibrary.uuid === library.uuid && 'bg-accent rounded'
)}
>
{library.config.name}
</Text>
</View>
</Pressable>
))}
<Text
style={twStyle(
'text-ink text-sm font-semibold',
currentLibrary.uuid === library.uuid && 'text-white'
)}
>
{library.config.name}
</Text>
</View>
</Pressable>
);
})}
<Divider style={tw`my-2`} />
{/* Menu */}
{/* Create Library */}

View file

@ -2,7 +2,7 @@ import { Trash } from 'phosphor-react-native';
import React from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Alert, Text, View } from 'react-native';
import { useBridgeMutation, useCurrentLibrary } from '@sd/client';
import { useBridgeMutation, useLibraryContext } from '@sd/client';
import { Button } from '~/components/primitive/Button';
import { Input } from '~/components/primitive/Input';
import { Switch } from '~/components/primitive/Switch';
@ -15,7 +15,7 @@ import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
const LibraryGeneralSettingsScreen = ({
navigation
}: SettingsStackScreenProps<'LibraryGeneralSettings'>) => {
const { library } = useCurrentLibrary();
const { library } = useLibraryContext();
const form = useForm({
defaultValues: { name: library.config.name, description: library.config.description }

View file

@ -3,6 +3,11 @@ import {
ParamListBase,
getFocusedRouteNameFromRoute
} from '@react-navigation/native';
import { valtioPersist } from '@sd/client';
export const currentLibraryStore = valtioPersist('sdActiveLibrary', {
id: null as string | null
});
export const getActiveRouteFromState = function (state: any) {
if (!state.routes || state.routes.length === 0 || state.index >= state.routes.length) {

View file

@ -1,2 +1,3 @@
export * from './useCurrentLibrary';
export * from './useClientContext';
export * from './useLibraryContext';
export * from './useOnlineLocations';

View file

@ -0,0 +1,73 @@
import { PropsWithChildren, createContext, useContext, useMemo } from 'react';
import { LibraryConfigWrapped } from '../core';
import { useBridgeQuery } from '../rspc';
import { valtioPersist } from '../stores';
// The name of the localStorage key for caching library data
const libraryCacheLocalStorageKey = 'sd-library-list';
export const useCachedLibraries = () =>
useBridgeQuery(['library.list'], {
keepPreviousData: true,
initialData: () => {
const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
if (cachedData) {
// If we fail to load cached data, it's fine
try {
return JSON.parse(cachedData);
} catch (e) {
console.error("Error loading cached 'sd-library-list' data", e);
}
}
return undefined;
},
onSuccess: (data) => localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data))
});
export interface ClientContext {
currentLibraryId: string | null;
libraries: ReturnType<typeof useCachedLibraries>;
library: LibraryConfigWrapped | null | undefined;
}
const ClientContext = createContext<ClientContext>(null!);
interface ClientContextProviderProps extends PropsWithChildren {
currentLibraryId: string | null;
}
export const ClientContextProvider = ({
children,
currentLibraryId
}: ClientContextProviderProps) => {
const libraries = useCachedLibraries();
const library = useMemo(() => {
if (libraries.data) return libraries.data.find((l) => l.uuid === currentLibraryId) ?? null;
}, [currentLibraryId, libraries]);
// Doesn't need to be in a useEffect
currentLibraryCache.id = currentLibraryId;
return (
<ClientContext.Provider value={{ currentLibraryId, libraries, library }}>
{children}
</ClientContext.Provider>
);
};
export const useClientContext = () => {
const ctx = useContext(ClientContext);
if (ctx === undefined) throw new Error("'ClientContextProvider' not mounted");
return ctx;
};
export const useCurrentLibraryId = () => useClientContext().currentLibraryId;
export const currentLibraryCache = valtioPersist('sd-current-library', {
id: null as string | null
});

View file

@ -1,90 +0,0 @@
import { PropsWithChildren, createContext, useCallback, useContext, useMemo } from 'react';
import { subscribe, useSnapshot } from 'valtio';
import { useBridgeQuery } from '../rspc';
import { valtioPersist } from '../stores';
// The name of the localStorage key for caching library data
const libraryCacheLocalStorageKey = 'sd-library-list';
type OnNoLibraryFunc = () => void | Promise<void>;
// Keep this private and use `useCurrentLibrary` hook to access or mutate it
const currentLibraryUuidStore = valtioPersist('sdActiveLibrary', {
id: null as string | null
});
const CringeContext = createContext<{
onNoLibrary: OnNoLibraryFunc;
}>(undefined!);
export const LibraryContextProvider = ({
onNoLibrary,
children
}: PropsWithChildren<{ onNoLibrary: OnNoLibraryFunc }>) => {
return <CringeContext.Provider value={{ onNoLibrary }}>{children}</CringeContext.Provider>;
};
export function getLibraryIdRaw(): string | null {
return currentLibraryUuidStore.id;
}
export function onLibraryChange(func: (newLibraryId: string | null) => void) {
subscribe(currentLibraryUuidStore, () => func(currentLibraryUuidStore.id));
}
// this is a hook to get the current library loaded into the UI. It takes care of a bunch of invariants under the hood.
export const useCurrentLibrary = () => {
const currentLibraryUuid = useSnapshot(currentLibraryUuidStore).id;
const ctx = useContext(CringeContext);
if (ctx === undefined)
throw new Error(
"The 'LibraryContextProvider' was not mounted and you attempted do use the 'useCurrentLibrary' hook. Please add the provider in your component tree."
);
const { data: libraries, isLoading } = useBridgeQuery(['library.list'], {
keepPreviousData: true,
initialData: () => {
const cachedData = localStorage.getItem(libraryCacheLocalStorageKey);
if (cachedData) {
// If we fail to load cached data, it's fine
try {
return JSON.parse(cachedData);
} catch (e) {
console.error("Error loading cached 'sd-library-list' data", e);
}
}
return undefined;
},
onSuccess: (data) => {
localStorage.setItem(libraryCacheLocalStorageKey, JSON.stringify(data));
// Redirect to the onboarding flow if the user doesn't have any libraries
if (!data?.length) {
ctx.onNoLibrary();
}
}
});
const switchLibrary = useCallback((libraryUuid: string) => {
currentLibraryUuidStore.id = libraryUuid;
}, []);
// memorize library to avoid re-running find function
const library = useMemo(() => {
const current = libraries?.find((l: any) => l.uuid === currentLibraryUuid);
// switch to first library if none set
if (libraries && !current && libraries[0]?.uuid) {
switchLibrary(libraries[0]?.uuid);
}
return current;
}, [libraries, currentLibraryUuid, switchLibrary]); // TODO: This runs when the 'libraries' change causing the whole app to re-render which is cringe.
return {
library,
libraries,
isLoading,
switchLibrary
};
};

View file

@ -0,0 +1,30 @@
import { PropsWithChildren, createContext, useContext } from 'react';
import { LibraryConfigWrapped } from '../core';
import { ClientContext, useClientContext } from './useClientContext';
export interface LibraryContext {
library: LibraryConfigWrapped;
libraries: ClientContext['libraries'];
}
const LibraryContext = createContext<LibraryContext>(null!);
interface LibraryContextProviderProps extends PropsWithChildren {
library: LibraryConfigWrapped;
}
export const LibraryContextProvider = ({ children, library }: LibraryContextProviderProps) => {
const { libraries } = useClientContext();
return (
<LibraryContext.Provider value={{ library, libraries }}>{children}</LibraryContext.Provider>
);
};
export const useLibraryContext = () => {
const ctx = useContext(LibraryContext);
if (ctx === undefined) throw new Error("'LibraryContextProvider' not mounted");
return ctx;
};

View file

@ -2,7 +2,7 @@ import { ProcedureDef } from '@rspc/client';
import { internal_createReactHooksFactory } from '@rspc/react';
import { QueryClient } from '@tanstack/react-query';
import { LibraryArgs, Procedures } from './core';
import { getLibraryIdRaw } from './index';
import { currentLibraryCache } from './hooks';
import { normiCustomHooks } from './normi';
type NonLibraryProcedure<T extends keyof Procedures> =
@ -24,6 +24,10 @@ type StripLibraryArgsFromInput<T extends ProcedureDef> = T extends any
: never
: never;
let getLibraryId: () => string | null;
export const setLibraryIdGetter = (g: typeof getLibraryId) => (getLibraryId = g);
export const hooks = internal_createReactHooksFactory();
const nonLibraryHooks = hooks.createHooks<
@ -50,16 +54,16 @@ const libraryHooks = hooks.createHooks<
customHooks: normiCustomHooks({ contextSharing: true }, () => {
return {
mapQueryKey: (keyAndInput) => {
const library_id = getLibraryIdRaw();
if (library_id === null)
const libraryId = currentLibraryCache.id;
if (libraryId === null)
throw new Error('Attempted to do library operation with no library set!');
return [keyAndInput[0], { library_id, arg: keyAndInput[1] || null }];
return [keyAndInput[0], { library_id: libraryId, arg: keyAndInput[1] || null }];
},
doMutation: (keyAndInput, next) => {
const library_id = getLibraryIdRaw();
if (library_id === null)
const libraryId = currentLibraryCache.id;
if (libraryId === null)
throw new Error('Attempted to do library operation with no library set!');
return next([keyAndInput[0], { library_id, arg: keyAndInput[1] || null }]);
return next([keyAndInput[0], { library_id: libraryId, arg: keyAndInput[1] || null }]);
}
};
})

View file

@ -11,8 +11,8 @@ import advancedFormat from 'dayjs/plugin/advancedFormat';
import duration from 'dayjs/plugin/duration';
import relativeTime from 'dayjs/plugin/relativeTime';
import { ErrorBoundary } from 'react-error-boundary';
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
import { LibraryContextProvider, queryClient, useDebugState } from '@sd/client';
import { MemoryRouter } from 'react-router-dom';
import { queryClient, useDebugState } from '@sd/client';
import { Dialogs } from '@sd/ui';
import { AppRouter } from './AppRouter';
import { ErrorFallback } from './ErrorFallback';
@ -35,8 +35,9 @@ export default function SpacedriveInterface() {
<QueryClientProvider client={queryClient} contextSharing={true}>
<Devtools />
<MemoryRouter>
<AppRouterWrapper />
<AppRouter />
</MemoryRouter>
<Dialogs />
</QueryClientProvider>
</ErrorBoundary>
);
@ -56,20 +57,3 @@ function Devtools() {
/>
) : null;
}
// This can't go in `<SpacedriveInterface />` cause it needs the router context but it can't go in `<AppRouter />` because that requires this context
function AppRouterWrapper() {
const navigate = useNavigate();
const { pathname } = useLocation();
return (
<LibraryContextProvider
onNoLibrary={() => {
// only redirect to onboarding flow if path doesn't already include onboarding
if (!pathname.includes('onboarding')) navigate('/onboarding');
}}
>
<AppRouter />
<Dialogs />
</LibraryContextProvider>
);
}

View file

@ -1,19 +1,19 @@
import clsx from 'clsx';
import { Suspense } from 'react';
import { Outlet } from 'react-router-dom';
import { useCurrentLibrary } from '@sd/client';
import { Navigate, Outlet } from 'react-router-dom';
import { ClientContextProvider, LibraryContextProvider, useClientContext } from '@sd/client';
import { Sidebar } from '~/components/layout/Sidebar';
import { Toasts } from '~/components/primitive/Toasts';
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
import { useLibraryId } from './util';
function AppLayout() {
const { libraries, library } = useClientContext();
export function AppLayout() {
const { libraries } = useCurrentLibrary();
const os = useOperatingSystem();
// This will ensure nothing is rendered while the `useCurrentLibrary` hook navigates to the onboarding page. This prevents requests with an invalid library id being sent to the backend
if (libraries?.length === 0) {
return null;
}
if (library === null && libraries.data)
return <Navigate to={`${libraries.data[0].uuid}/overview`} />;
return (
<div
@ -32,11 +32,27 @@ export function AppLayout() {
>
<Sidebar />
<div className="relative flex w-full">
<Suspense fallback={<div className="bg-app h-screen w-screen" />}>
<Outlet />
</Suspense>
{library ? (
<LibraryContextProvider library={library}>
<Suspense fallback={<div className="bg-app h-screen w-screen" />}>
<Outlet />
</Suspense>
</LibraryContextProvider>
) : (
<h1 className="p-4 text-white">Please select or create a library in the sidebar.</h1>
)}
</div>
<Toasts />
</div>
);
}
export default () => {
const currentLibraryId = useLibraryId();
return (
<ClientContextProvider currentLibraryId={currentLibraryId ?? null}>
<AppLayout />
</ClientContextProvider>
);
};

View file

@ -1,44 +1,42 @@
import { Navigate, Route, Routes } from 'react-router-dom';
import { useCurrentLibrary, useInvalidateQuery } from '@sd/client';
import { AppLayout } from '~/AppLayout';
import { Navigate, useRoutes } from 'react-router-dom';
import { currentLibraryCache, useCachedLibraries, useInvalidateQuery } from '@sd/client';
import AppLayout from '~/AppLayout';
import { useKeybindHandler } from '~/hooks/useKeyboardHandler';
import screens from '~/screens';
import { lazyEl } from '~/util';
import OnboardingRoot, { ONBOARDING_SCREENS } from './components/onboarding/OnboardingRoot';
import OnboardingRoot, { ONBOARDING_ROUTES } from './components/onboarding/OnboardingRoot';
const NotFound = lazyEl(() => import('./NotFound'));
function Index() {
const libraries = useCachedLibraries();
if (libraries.status !== 'success') return null;
if (libraries.data.length === 0) return <Navigate to="onboarding" />;
const currentLibrary = libraries.data.find((l) => l.uuid === currentLibraryCache.id);
const libraryId = currentLibrary ? currentLibrary.uuid : libraries.data[0].uuid;
return <Navigate to={`${libraryId}/overview`} />;
}
export function AppRouter() {
const { library } = useCurrentLibrary();
useKeybindHandler();
useInvalidateQuery();
return (
<Routes>
<Route path="onboarding" element={<OnboardingRoot />}>
<Route index element={<Navigate to="start" />} />
{ONBOARDING_SCREENS.map(({ key, component: ScreenComponent }, index) => (
<Route key={key} path={key} element={<ScreenComponent />} />
))}
</Route>
<Route element={<AppLayout />}>
{/* As we are caching the libraries in localStore so this *shouldn't* result is visual problems unless something else is wrong */}
{library === undefined ? (
<Route
path="*"
element={
<h1 className="p-4 text-white">Please select or create a library in the sidebar.</h1>
}
/>
) : (
<>
{screens}
<Route path="*" element={NotFound} />
</>
)}
</Route>
</Routes>
);
return useRoutes([
{
index: true,
element: <Index />
},
{
path: 'onboarding',
element: <OnboardingRoot />,
children: ONBOARDING_ROUTES
},
{
path: ':libraryId',
element: <AppLayout />,
children: screens
}
]);
}

View file

@ -1,5 +1,5 @@
import { useCallback, useEffect, useState } from 'react';
import { ExplorerData, rspc, useCurrentLibrary } from '@sd/client';
import { ExplorerData, rspc, useLibraryContext } from '@sd/client';
import { useExplorerStore } from '~/hooks/useExplorerStore';
import { Inspector } from '../explorer/Inspector';
import { ExplorerContextMenu } from './ExplorerContextMenu';
@ -12,7 +12,7 @@ interface Props {
export default function Explorer(props: Props) {
const expStore = useExplorerStore();
const { library } = useCurrentLibrary();
const { library } = useLibraryContext();
const [scrollSegments, setScrollSegments] = useState<{ [key: string]: number }>({});
const [separateTopBar, setSeparateTopBar] = useState<boolean>(false);

View file

@ -19,8 +19,8 @@ import {
import { PropsWithChildren, useMemo } from 'react';
import {
ExplorerItem,
getLibraryIdRaw,
isObject,
useLibraryContext,
useLibraryMutation,
useLibraryQuery
} from '@sd/client';
@ -28,6 +28,7 @@ import { ContextMenu as CM, dialogManager } from '@sd/ui';
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
import { useExplorerParams } from '~/screens/LocationExplorer';
import { useLibraryId } from '~/util';
import { usePlatform } from '~/util/Platform';
import { showAlertDialog } from '~/util/dialog';
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
@ -211,18 +212,15 @@ export interface FileItemContextMenuProps extends PropsWithChildren {
}
export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps) {
const { library } = useLibraryContext();
const store = useExplorerStore();
const params = useExplorerParams();
const platform = usePlatform();
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
const isUnlockedQuery = useLibraryQuery(['keys.isUnlocked']);
const isUnlocked =
isUnlockedQuery.data !== undefined && isUnlockedQuery.data === true ? true : false;
const mountedUuids = useLibraryQuery(['keys.listMounted']);
const hasMountedKeys =
mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false;
const keyManagerUnlocked = useLibraryQuery(['keys.isUnlocked']).data ?? false;
const mountedKeys = useLibraryQuery(['keys.listMounted']);
const hasMountedKeys = mountedKeys.data?.length ?? 0 > 0;
const copyFiles = useLibraryMutation('files.copyFiles');
@ -232,10 +230,10 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
<CM.Item
label="Open"
keybind="⌘O"
onClick={(e) => {
onClick={() => {
// TODO: Replace this with a proper UI
window.location.href = platform.getFileUrl(
getLibraryIdRaw()!,
library.uuid,
store.locationId!,
data.item.id
);
@ -255,7 +253,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
<CM.Item
label="Duplicate"
keybind="⌘D"
onClick={(e) => {
onClick={() => {
copyFiles.mutate({
source_location_id: store.locationId!,
source_path_id: data.item.id,
@ -269,7 +267,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
<CM.Item
label="Cut"
keybind="⌘X"
onClick={(e) => {
onClick={() => {
getExplorerStore().cutCopyState = {
sourceLocationId: store.locationId!,
sourcePathId: data.item.id,
@ -283,7 +281,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
<CM.Item
label="Copy"
keybind="⌘C"
onClick={(e) => {
onClick={() => {
getExplorerStore().cutCopyState = {
sourceLocationId: store.locationId!,
sourcePathId: data.item.id,
@ -297,7 +295,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
<CM.Item
label="Deselect"
hidden={!store.cutCopyState.active}
onClick={(e) => {
onClick={() => {
getExplorerStore().cutCopyState = {
...store.cutCopyState,
active: false
@ -334,7 +332,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
icon={LockSimple}
keybind="⌘E"
onClick={() => {
if (isUnlocked && hasMountedKeys) {
if (keyManagerUnlocked && hasMountedKeys) {
dialogManager.create((dp) => (
<EncryptFileDialog
{...dp}
@ -342,7 +340,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
path_id={data.item.id}
/>
));
} else if (!isUnlocked) {
} else if (!keyManagerUnlocked) {
showAlertDialog({
title: 'Key manager locked',
value: 'The key manager is currently locked. Please unlock it and try again.'
@ -361,7 +359,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
icon={LockSimpleOpen}
keybind="⌘D"
onClick={() => {
if (isUnlocked) {
if (keyManagerUnlocked) {
dialogManager.create((dp) => (
<DecryptFileDialog
{...dp}

View file

@ -277,7 +277,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
}
>
<div className="block w-[350px]">
<KeyManager className={TOP_BAR_ICON_STYLE} />
<KeyManager /* className={TOP_BAR_ICON_STYLE} */ />
</div>
</Popover>
</Tooltip>

View file

@ -2,17 +2,18 @@ import { Eye, EyeSlash, Gear, Lock } from 'phosphor-react';
import { useState } from 'react';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button, ButtonLink, Input, Tabs } from '@sd/ui';
import { useLibraryId } from '~/util';
import { showAlertDialog } from '~/util/dialog';
import { DefaultProps } from '../primitive/types';
import { KeyList } from './KeyList';
import { KeyMounter } from './KeyMounter';
export type KeyManagerProps = DefaultProps;
export function KeyManager() {
const libraryId = useLibraryId();
export function KeyManager(props: KeyManagerProps) {
const isUnlocked = useLibraryQuery(['keys.isUnlocked']);
const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' });
const isKeyManagerUnlocking = useLibraryQuery(['keys.isKeyManagerUnlocking']);
const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', {
onError: () => {
showAlertDialog({
@ -94,12 +95,7 @@ export function KeyManager(props: KeyManagerProps) {
</Button>
{!enterSkManually && (
<div className="relative flex grow">
<p
className="text-accent mt-2"
onClick={(e) => {
setEnterSkManually(true);
}}
>
<p className="text-accent mt-2" onClick={() => setEnterSkManually(true)}>
or enter secret key manually
</p>
</div>
@ -131,7 +127,7 @@ export function KeyManager(props: KeyManagerProps) {
<Lock className="text-ink-faint h-4 w-4" />
</Button>
<ButtonLink
to="/settings/keys"
to={`/${libraryId}/settings/overview`}
size="icon"
variant="subtle"
className="text-ink-faint"

View file

@ -11,14 +11,14 @@ import {
UsersThree
} from 'phosphor-react';
import React, { PropsWithChildren, useEffect } from 'react';
import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom';
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
import {
Location,
LocationCreateArgs,
arraysEqual,
getDebugState,
useBridgeQuery,
useCurrentLibrary,
useClientContext,
useDebugState,
useLibraryMutation,
useLibraryQuery,
@ -56,10 +56,10 @@ const SidebarFooter = tw.div`flex flex-col mb-3 px-2.5`;
export function Sidebar() {
// DO NOT DO LIBRARY QUERIES OR MUTATIONS HERE. This is rendered before a library is set.
const os = useOperatingSystem();
const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary();
const { library, libraries, currentLibraryId } = useClientContext();
const debugState = useDebugState();
const navigate = useNavigate();
useEffect(() => {
// Prevent the dropdown button to be auto focused on launch
@ -69,7 +69,9 @@ export function Sidebar() {
(document.activeElement.blur as () => void)();
});
});
}, []);
console.log(useLocation());
return (
<SidebarBody className={macOnly(os, 'bg-opacity-[0.75]')}>
@ -87,24 +89,21 @@ export function Sidebar() {
// these classname overrides are messy
// but they work
`!bg-sidebar-box !border-sidebar-line/50 active:!border-sidebar-line active:!bg-sidebar-button ui-open:!bg-sidebar-button ui-open:!border-sidebar-line ring-offset-sidebar`,
(library === null || isLoadingLibraries) && '!text-ink-faint'
(library === null || libraries.isLoading) && '!text-ink-faint'
)}
>
<span className="truncate">
{isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '}
{libraries.isLoading ? 'Loading...' : library ? library.config.name : ' '}
</span>
</Dropdown.Button>
}
>
<Dropdown.Section>
{libraries?.map((lib) => (
{libraries.data?.map((lib) => (
<Dropdown.Item
selected={lib.uuid === library?.uuid}
to={`/${lib.uuid}/overview`}
key={lib.uuid}
onClick={() => {
switchLibrary(lib.uuid);
navigate('/');
}}
selected={lib.uuid === currentLibraryId}
>
{lib.config.name}
</Dropdown.Item>
@ -129,7 +128,7 @@ export function Sidebar() {
</Dropdown.Root>
<SidebarContents>
<div className="pt-1">
<SidebarLink to="/overview">
<SidebarLink to="overview">
<Icon component={Planet} />
Overview
</SidebarLink>
@ -146,13 +145,13 @@ export function Sidebar() {
Media
</SidebarLink>
</div>
{library && <LibraryScopedSection />}
{library && <LibraryScopedSection key={library.uuid} />}
<div className="grow" />
</SidebarContents>
<SidebarFooter>
<div className="flex">
<ButtonLink
to="/settings/general"
to="settings/general"
size="icon"
variant="subtle"
className="text-ink-faint ring-offset-sidebar"
@ -308,11 +307,12 @@ export const SidebarLink = (props: PropsWithChildren<NavLinkProps>) => {
);
};
const SidebarSection: React.FC<{
name: string;
actionArea?: React.ReactNode;
children: React.ReactNode;
}> = (props) => {
const SidebarSection = (
props: PropsWithChildren<{
name: string;
actionArea?: React.ReactNode;
}>
) => {
return (
<div className="group mt-5">
<div className="mb-1 flex items-center justify-between">
@ -354,7 +354,7 @@ function LibraryScopedSection() {
actionArea={
<>
{/* <SidebarHeadingOptionsButton to="/settings/locations" icon={CogIcon} /> */}
<SidebarHeadingOptionsButton to="/settings/locations" />
<SidebarHeadingOptionsButton to="settings/locations" />
</>
}
>
@ -426,12 +426,7 @@ interface SidebarLocationProps {
function SidebarLocation({ location, online }: SidebarLocationProps) {
return (
<div className="flex flex-row items-center">
<SidebarLink
className="group relative w-full"
to={{
pathname: `location/${location.id}`
}}
>
<SidebarLink className="group relative w-full" to={`location/${location.id}`}>
<div className="relative -mt-0.5 mr-1 shrink-0 grow-0">
<Folder size={18} />
<div

View file

@ -28,7 +28,7 @@ export default function LocationListItem({ location }: LocationListItemProps) {
<Card
className="hover:bg-app-box/70 cursor-pointer"
onClick={() => {
navigate(`/settings/locations/location/${location.id}`);
navigate(`${location.id}`);
}}
>
<Folder size={30} className="mr-3" />

View file

@ -2,7 +2,7 @@ import clsx from 'clsx';
import { useEffect } from 'react';
import { useNavigate } from 'react-router';
import { getOnboardingStore, unlockOnboardingScreen, useOnboardingStore } from '@sd/client';
import { ONBOARDING_SCREENS } from './OnboardingRoot';
import { ONBOARDING_ROUTES } from './OnboardingRoot';
import { useCurrentOnboardingScreenKey } from './helpers/screens';
// screens are locked to prevent users from skipping ahead
@ -24,21 +24,25 @@ export default function OnboardingProgress() {
return (
<div className="flex w-full items-center justify-center">
<div className="flex items-center justify-center space-x-1">
{ONBOARDING_SCREENS.map(({ isSkippable, key }) => (
<div
key={key}
onClick={() => {
if (ob_store.unlockedScreens.includes(key)) {
navigate(`/onboarding/${key}`);
}
}}
className={clsx(
'hover:bg-ink h-2 w-2 rounded-full transition',
currentScreenKey === key ? 'bg-ink' : 'bg-ink-faint',
!ob_store.unlockedScreens.includes(key) && 'opacity-10'
)}
/>
))}
{ONBOARDING_ROUTES.map(({ path }) => {
if (!path) return null;
return (
<div
key={path}
onClick={() => {
if (ob_store.unlockedScreens.includes(path)) {
navigate(`/onboarding/${path}`);
}
}}
className={clsx(
'hover:bg-ink h-2 w-2 rounded-full transition',
currentScreenKey === path ? 'bg-ink' : 'bg-ink-faint',
!ob_store.unlockedScreens.includes(path) && 'opacity-10'
)}
/>
);
})}
</div>
</div>
);

View file

@ -1,7 +1,7 @@
import BloomOne from '@sd/assets/images/bloom-one.png';
import clsx from 'clsx';
import { ComponentType, useEffect } from 'react';
import { Outlet, useNavigate } from 'react-router';
import { useEffect } from 'react';
import { Navigate, Outlet, RouteObject, useNavigate } from 'react-router';
import { getOnboardingStore } from '@sd/client';
import { tw } from '@sd/ui';
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
@ -12,42 +12,30 @@ import OnboardingPrivacy from './OnboardingPrivacy';
import OnboardingProgress from './OnboardingProgress';
import OnboardingStart from './OnboardingStart';
interface OnboardingScreen {
/**
* React component for rendering this screen.
*/
component: ComponentType<Record<string, never>>;
/**
* Unique key used to record progression to this screen
*/
key: string;
/**
* Sets whether the user is allowed to skip this screen.
* @default false
*/
isSkippable?: boolean;
}
export const ONBOARDING_SCREENS: OnboardingScreen[] = [
export const ONBOARDING_ROUTES: RouteObject[] = [
{
component: OnboardingStart,
key: 'start'
index: true,
element: <Navigate to="start" />
},
{
component: OnboardingNewLibrary,
key: 'new-library'
element: <OnboardingStart />,
path: 'start'
},
{
component: OnboardingMasterPassword,
key: 'master-password'
element: <OnboardingNewLibrary />,
path: 'new-library'
},
{
component: OnboardingPrivacy,
key: 'privacy'
element: <OnboardingMasterPassword />,
path: 'master-password'
},
{
component: OnboardingCreatingLibrary,
key: 'creating-library'
element: <OnboardingPrivacy />,
path: 'privacy'
},
{
element: <OnboardingCreatingLibrary />,
path: 'creating-library'
}
];

View file

@ -29,66 +29,66 @@ export const SettingsSidebar = () => {
)}
<div className="px-4 pb-2.5">
<SettingsHeading className="!mt-2">Client</SettingsHeading>
<SidebarLink to="/settings/general">
<SidebarLink to="general">
<SettingsIcon component={GearSix} />
General
</SidebarLink>
<SidebarLink to="/settings/libraries">
<SidebarLink to="libraries">
<SettingsIcon component={Books} />
Libraries
</SidebarLink>
<SidebarLink to="/settings/privacy">
<SidebarLink to="privacy">
<SettingsIcon component={ShieldCheck} />
Privacy
</SidebarLink>
<SidebarLink to="/settings/appearance">
<SidebarLink to="appearance">
<SettingsIcon component={PaintBrush} />
Appearance
</SidebarLink>
<SidebarLink to="/settings/keybindings">
<SidebarLink to="keybindings">
<SettingsIcon component={KeyReturn} />
Keybinds
</SidebarLink>
<SidebarLink to="/settings/extensions">
<SidebarLink to="extensions">
<SettingsIcon component={PuzzlePiece} />
Extensions
</SidebarLink>
<SettingsHeading>Library</SettingsHeading>
<SidebarLink to="/settings/library">
<SidebarLink to="library">
<SettingsIcon component={GearSix} />
General
</SidebarLink>
<SidebarLink to="/settings/nodes">
<SidebarLink to="nodes">
<SettingsIcon component={ShareNetwork} />
Nodes
</SidebarLink>
<SidebarLink to="/settings/locations">
<SidebarLink to="locations">
<SettingsIcon component={HardDrive} />
Locations
</SidebarLink>
<SidebarLink to="/settings/tags">
<SidebarLink to="tags">
<SettingsIcon component={TagSimple} />
Tags
</SidebarLink>
<SidebarLink to="/settings/keys">
<SidebarLink to="keys">
<SettingsIcon component={Key} />
Keys
</SidebarLink>
<SettingsHeading>Resources</SettingsHeading>
<SidebarLink to="/settings/about">
<SidebarLink to="about">
<SettingsIcon component={FlyingSaucer} />
About
</SidebarLink>
<SidebarLink to="/settings/changelog">
<SidebarLink to="changelog">
<SettingsIcon component={Receipt} />
Changelog
</SidebarLink>
<SidebarLink to="/settings/dependencies">
<SidebarLink to="dependencies">
<SettingsIcon component={Graph} />
Dependencies
</SidebarLink>
<SidebarLink to="/settings/support">
<SidebarLink to="support">
<SettingsIcon component={Heart} />
Support
</SidebarLink>

View file

@ -1,14 +1,11 @@
import { useEffect } from 'react';
import { FieldValues, UseFormReturn } from 'react-hook-form';
import { FieldValues, UseFormReturn, WatchObserver } from 'react-hook-form';
import { useDebouncedCallback } from 'use-debounce';
import { useCurrentLibrary } from '@sd/client';
export function useDebouncedForm<TFieldValues extends FieldValues = FieldValues, TContext = any>(
form: UseFormReturn<{ id: string } & object, TContext>,
callback: (data: any) => void,
args?: { disableResetOnLibraryChange?: boolean }
) {
const { library } = useCurrentLibrary();
export function useDebouncedFormWatch<
TFieldValues extends FieldValues = FieldValues,
TContext = any
>(form: UseFormReturn<TFieldValues, TContext>, callback: WatchObserver<TFieldValues>) {
const debounced = useDebouncedCallback(callback, 500);
// listen for any form changes
@ -16,12 +13,4 @@ export function useDebouncedForm<TFieldValues extends FieldValues = FieldValues,
// persist unchanged data when the component is unmounted
useEffect(() => () => debounced.flush(), [debounced]);
// ensure the form is updated when the library changes
useEffect(() => {
if (args?.disableResetOnLibraryChange !== true && library?.uuid !== form.getValues('id')) {
form.reset({ id: library?.uuid, ...library?.config });
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [library, form.getValues, form.reset, args?.disableResetOnLibraryChange]);
}

View file

@ -1,5 +1,6 @@
import { useEffect } from 'react';
import { proxy, useSnapshot } from 'valtio';
import { onLibraryChange } from '@sd/client';
import { useLibraryContext } from '@sd/client';
import { resetStore } from '@sd/client/src/stores/util';
export type ExplorerLayoutMode = 'list' | 'grid' | 'columns' | 'media';
@ -32,8 +33,6 @@ const state = {
}
};
onLibraryChange(() => getExplorerStore().reset());
// Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues.
const explorerStore = proxy({
...state,
@ -53,6 +52,12 @@ const explorerStore = proxy({
});
export function useExplorerStore() {
const { library } = useLibraryContext();
useEffect(() => {
explorerStore.reset();
}, [library.uuid]);
return useSnapshot(explorerStore);
}

View file

@ -1,12 +1,12 @@
import { useEffect } from 'react';
import { useParams, useSearchParams } from 'react-router-dom';
import { useCurrentLibrary, useLibraryQuery } from '@sd/client';
import { useLibraryQuery } from '@sd/client';
import Explorer from '~/components/explorer/Explorer';
import { getExplorerStore } from '~/hooks/useExplorerStore';
export function useExplorerParams() {
const { id } = useParams();
const location_id = id ? Number(id) : -1;
const { id } = useParams<{ id?: string }>();
const location_id = id ? Number(id) : null;
const [searchParams] = useSearchParams();
const path = searchParams.get('path') || '';
@ -17,16 +17,17 @@ export function useExplorerParams() {
export default function LocationExplorer() {
const { location_id, path } = useExplorerParams();
const { library } = useCurrentLibrary();
useEffect(() => {
getExplorerStore().locationId = location_id;
}, [location_id]);
if (location_id === null) throw new Error(`location_id is null!`);
const explorerData = useLibraryQuery([
'locations.getExplorerData',
{
location_id: location_id,
location_id,
path: path,
limit: 100,
cursor: null

View file

@ -11,19 +11,12 @@ import {
MusicNote,
Wrench
} from 'phosphor-react';
import { useEffect } from 'react';
import Skeleton from 'react-loading-skeleton';
import 'react-loading-skeleton/dist/skeleton.css';
import { proxy } from 'valtio';
import {
Statistics,
onLibraryChange,
queryClient,
useCurrentLibrary,
useLibraryQuery
} from '@sd/client';
import { Statistics, useLibraryQuery } from '@sd/client';
import { Card } from '@sd/ui';
import useCounter from '~/hooks/useCounter';
import { useLibraryId } from '~/util';
import { usePlatform } from '~/util/Platform';
interface StatItemProps {
@ -39,64 +32,33 @@ const StatItemNames: Partial<Record<keyof Statistics, string>> = {
total_bytes_free: 'Free space'
};
const EMPTY_STATISTICS = {
id: 0,
date_captured: '',
total_bytes_capacity: '0',
preview_media_bytes: '0',
library_db_size: '0',
total_object_count: 0,
total_bytes_free: '0',
total_bytes_used: '0',
total_unique_bytes: '0'
};
const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames;
export const state = proxy({
lastRenderedLibraryId: undefined as string | undefined
});
let overviewMounted = false;
onLibraryChange((newLibraryId) => {
state.lastRenderedLibraryId = undefined;
// TODO: Fix
// This is bad solution to the fact that the hooks don't rerun when opening a library that is already cached.
// This is because the count never drops back to zero as their is no loading state given the libraries data was already in the React Query cache.
queryClient.setQueryData(
[
'library.getStatistics',
{
library_id: newLibraryId,
arg: null
}
],
{
id: 0,
date_captured: '',
total_bytes_capacity: '0',
preview_media_bytes: '0',
library_db_size: '0',
total_object_count: 0,
total_bytes_free: '0',
total_bytes_used: '0',
total_unique_bytes: '0'
}
);
queryClient.invalidateQueries(['library.getStatistics']);
});
const StatItem: React.FC<StatItemProps> = (props) => {
const { library } = useCurrentLibrary();
const StatItem = (props: StatItemProps) => {
const { title, bytes = BigInt('0'), isLoading } = props;
const size = byteSize(Number(bytes)); // TODO: This BigInt to Number conversion will truncate the number if the number is too large. `byteSize` doesn't support BigInt so we are gonna need to come up with a longer term solution at some point.
const count = useCounter({
name: title,
end: +size.value,
duration: state.lastRenderedLibraryId === library?.uuid ? 0 : undefined,
duration: overviewMounted ? 0 : 1,
saveState: false
});
if (count !== 0 && count == +size.value) {
state.lastRenderedLibraryId = library?.uuid;
}
useEffect(() => {
return () => {
if (count !== 0) state.lastRenderedLibraryId = library?.uuid;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div
className={clsx(
@ -126,23 +88,13 @@ const StatItem: React.FC<StatItemProps> = (props) => {
export default function OverviewScreen() {
const platform = usePlatform();
const { library } = useCurrentLibrary();
const { data: overviewStats, isLoading: isStatisticsLoading } = useLibraryQuery(
['library.getStatistics'],
{
initialData: {
id: 0,
date_captured: '',
total_bytes_capacity: '0',
preview_media_bytes: '0',
library_db_size: '0',
total_object_count: 0,
total_bytes_free: '0',
total_bytes_used: '0',
total_unique_bytes: '0'
}
}
);
const libraryId = useLibraryId();
const stats = useLibraryQuery(['library.getStatistics'], {
initialData: { ...EMPTY_STATISTICS }
});
overviewMounted = true;
return (
<div className="custom-scroll page-scroll app-background flex h-screen w-full flex-col overflow-x-hidden">
@ -154,14 +106,14 @@ export default function OverviewScreen() {
<div className="flex w-full">
{/* STAT CONTAINER */}
<div className="-mb-1 flex h-20 overflow-hidden">
{Object.entries(overviewStats || []).map(([key, value]) => {
{Object.entries(stats?.data || []).map(([key, value]) => {
if (!displayableStatItems.includes(key)) return null;
return (
<StatItem
key={library?.uuid + ' ' + key}
key={`${libraryId} ${key}`}
title={StatItemNames[key as keyof Statistics]!}
bytes={BigInt(value)}
isLoading={platform.demoMode === true ? false : isStatisticsLoading}
isLoading={platform.demoMode ? false : stats.isLoading}
/>
);
})}
@ -179,7 +131,6 @@ export default function OverviewScreen() {
<CategoryButton icon={MusicNote} category="Music" />
<CategoryButton icon={Image} category="Albums" />
<CategoryButton icon={Heart} category="Favorites" />
<Debug />
</div>
<Card className="text-ink-dull">
<b>Note: </b>&nbsp; This is a pre-alpha build of Spacedrive, many features are yet to be
@ -191,14 +142,6 @@ export default function OverviewScreen() {
);
}
// TODO(@Oscar): Remove this
function Debug() {
// const org = useBridgeQuery(['normi.org']);
// console.log(org.data);
return null;
}
interface CategoryButtonProps {
category: string;
icon: any;

View file

@ -1,10 +1,10 @@
import { useParams } from 'react-router-dom';
import { useCurrentLibrary, useLibraryQuery } from '@sd/client';
import { useLibraryContext, useLibraryQuery } from '@sd/client';
import Explorer from '~/components/explorer/Explorer';
export default function TagExplorer() {
const { id } = useParams();
const { library } = useCurrentLibrary();
const { library } = useLibraryContext();
const explorerData = useLibraryQuery(['tags.getExplorerData', Number(id)]);

View file

@ -1,12 +1,8 @@
import { Navigate, Route, RouteProps } from 'react-router-dom';
import { RouteObject } from 'react-router-dom';
import { lazyEl } from '~/util';
import settingsScreens from './settings';
const routes: RouteProps[] = [
{
index: true,
element: <Navigate to="overview" relative="route" />
},
const screens: RouteObject[] = [
{
path: 'overview',
element: lazyEl(() => import('./Overview'))
@ -21,13 +17,8 @@ const routes: RouteProps[] = [
path: 'settings',
element: lazyEl(() => import('./settings/Layout')),
children: settingsScreens
}
},
{ path: '*', element: lazyEl(() => import('./NotFound')) }
];
export default (
<>
{routes.map((route) => (
<Route key={route.path} {...route} />
))}
</>
);
export default screens;

View file

@ -1,11 +1,7 @@
import { Navigate, Route, RouteProps } from 'react-router-dom';
import { RouteObject } from 'react-router-dom';
import { lazyEl } from '~/util';
import SettingsSubPage from './SettingsSubPage';
import LocationsSettings from './library/LocationsSettings';
import EditLocation from './library/location/EditLocation';
const routes: RouteProps[] = [
{ index: true, element: <Navigate to="general" relative="route" /> },
const screens: RouteObject[] = [
{ path: 'general', element: lazyEl(() => import('./client/GeneralSettings')) },
{ path: 'appearance', element: lazyEl(() => import('./client/AppearanceSettings')) },
{ path: 'keybindings', element: lazyEl(() => import('./client/KeybindingSettings')) },
@ -27,18 +23,15 @@ const routes: RouteProps[] = [
{ path: 'about', element: lazyEl(() => import('./info/AboutSpacedrive')) },
{ path: 'changelog', element: lazyEl(() => import('./info/Changelog')) },
{ path: 'dependencies', element: lazyEl(() => import('./info/Dependencies')) },
{ path: 'support', element: lazyEl(() => import('./info/Support')) }
{ path: 'support', element: lazyEl(() => import('./info/Support')) },
{
path: 'locations',
element: lazyEl(() => import('./SettingsSubPage')),
children: [
{ index: true, element: lazyEl(() => import('./library/LocationsSettings')) },
{ path: ':id', element: lazyEl(() => import('./library/location/EditLocation')) }
]
}
];
export default (
<>
{routes.map((route) => (
<Route key={route.path} {...route} />
))}
{/* Skipping implementing via routes object due to a lack of understanding on how to accomplish the below route setup with this new approach, feel free to fix Brendan */}
<Route path="locations" element={<SettingsSubPage />}>
<Route index element={<LocationsSettings />} />
<Route path="location/:id" element={<EditLocation />} />
</Route>
</>
);
export default screens;

View file

@ -1,25 +1,24 @@
import { useForm } from 'react-hook-form';
import { useBridgeMutation } from '@sd/client';
import { useCurrentLibrary } from '@sd/client';
import { useBridgeMutation, useLibraryContext } from '@sd/client';
import { Button, Input, Switch } from '@sd/ui';
import { InputContainer } from '~/components/primitive/InputContainer';
import { SettingsContainer } from '~/components/settings/SettingsContainer';
import { SettingsHeader } from '~/components/settings/SettingsHeader';
import { useDebouncedForm } from '~/hooks/useDebouncedForm';
import { useDebouncedFormWatch } from '~/hooks/useDebouncedForm';
export default function LibraryGeneralSettings() {
const { library } = useCurrentLibrary();
const { mutate: editLibrary } = useBridgeMutation('library.edit');
const { library } = useLibraryContext();
const editLibrary = useBridgeMutation('library.edit');
const form = useForm({
defaultValues: { id: library!.uuid, ...library?.config }
});
useDebouncedForm(form, (value) =>
editLibrary({
id: library!.uuid,
name: value.name,
description: value.description
useDebouncedFormWatch(form, (value) =>
editLibrary.mutate({
id: library.uuid,
name: value.name ?? null,
description: value.description ?? null
})
);

View file

@ -3,7 +3,7 @@ import { Archive, ArrowsClockwise, Info, Trash } from 'phosphor-react';
import { useFormState } from 'react-hook-form';
import { useParams } from 'react-router';
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button, TextArea, forms, tw } from '@sd/ui';
import { Button, forms, tw } from '@sd/ui';
import { Divider } from '~/components/explorer/inspector/Divider';
import { SettingsSubPage } from '~/components/settings/SettingsSubPage';
import { Tooltip } from '~/components/tooltip/Tooltip';

View file

@ -1,5 +1,5 @@
import { Database, DotsSixVertical, Pencil, Trash } from 'phosphor-react';
import { useBridgeQuery, useCurrentLibrary } from '@sd/client';
import { useBridgeQuery, useLibraryContext } from '@sd/client';
import { LibraryConfigWrapped } from '@sd/client';
import { Button, ButtonLink, Card, dialogManager, tw } from '@sd/ui';
import CreateLibraryDialog from '~/components/dialog/CreateLibraryDialog';
@ -27,7 +27,7 @@ function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolea
<Database className="h-4 w-4" />
</Tooltip>
</Button>
<ButtonLink className="!p-1.5" to="/settings/library" variant="gray">
<ButtonLink className="!p-1.5" to="../library" variant="gray">
<Tooltip label="Edit Library">
<Pencil className="h-4 w-4" />
</Tooltip>
@ -53,7 +53,7 @@ function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolea
export default function LibrarySettings() {
const libraries = useBridgeQuery(['library.list']);
const { library: currentLibrary } = useCurrentLibrary();
const { library } = useLibraryContext();
return (
<SettingsContainer>
@ -78,13 +78,13 @@ export default function LibrarySettings() {
<div className="space-y-2">
{libraries.data
?.sort((a, b) => {
if (a.uuid === currentLibrary?.uuid) return -1;
if (b.uuid === currentLibrary?.uuid) return 1;
if (a.uuid === library.uuid) return -1;
if (b.uuid === library.uuid) return 1;
return 0;
})
.map((library) => (
<LibraryListItem
current={library.uuid === currentLibrary?.uuid}
current={library.uuid === library.uuid}
key={library.uuid}
library={library}
/>

View file

@ -1,7 +1,12 @@
import { lazy } from '@loadable/component';
import { ReactNode } from 'react';
import { useParams } from 'react-router';
export function lazyEl(fn: Parameters<typeof lazy>[0]): ReactNode {
const Element = lazy(fn);
return <Element />;
}
export function useLibraryId() {
return useParams<{ libraryId?: string }>().libraryId;
}

View file

@ -60,8 +60,7 @@ class DialogManager {
if (state.open === false) {
delete this.dialogs[id];
delete this.state[id];
console.log(`Successfully removed state ${id}`);
} else console.log(`Tried to remove state ${id} but wasn't pending!`);
}
}
}
@ -82,9 +81,13 @@ function Remover({ id }: { id: number }) {
}
export function useDialog(props: UseDialogProps) {
const state = dialogManager.getState(props.id);
if (!state) throw new Error(`Dialog ${props.id} does not exist!`);
return {
...props,
state: dialogManager.getState(props.id)
state
};
}

View file

@ -1,11 +1,16 @@
import clsx from 'clsx';
import React from 'react';
function twFactory(element: any) {
return ([className, ..._]: TemplateStringsArray) => {
return restyle(element)(() => className);
};
}
const twFactory =
(element: any) =>
([newClassNames, ..._]: TemplateStringsArray) =>
React.forwardRef(({ className, ...props }: any, ref) =>
React.createElement(element, {
...props,
className: clsx(newClassNames, className),
ref
})
);
type ClassnameFactory<T> = (s: TemplateStringsArray) => T;
@ -22,21 +27,3 @@ export const tw = new Proxy((() => {}) as unknown as TailwindFactory, {
get: (_, property: string) => twFactory(property),
apply: (_, __, [el]: [React.ReactElement]) => twFactory(el)
});
export const restyle = <
T extends
| string
| React.FunctionComponent<{ className: string }>
| React.ComponentClass<{ className: string }>
>(
element: T
) => {
return (cls: () => string) =>
React.forwardRef(({ className, ...props }: any, ref) =>
React.createElement(element, {
...props,
className: clsx(cls(), className),
ref
})
);
};