mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-05 08:03:28 +00:00
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:
parent
810b5161dc
commit
bfc3ca0f9b
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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?.();
|
||||
},
|
||||
|
|
|
@ -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 */}
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export * from './useCurrentLibrary';
|
||||
export * from './useClientContext';
|
||||
export * from './useLibraryContext';
|
||||
export * from './useOnlineLocations';
|
||||
|
|
73
packages/client/src/hooks/useClientContext.tsx
Normal file
73
packages/client/src/hooks/useClientContext.tsx
Normal 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
|
||||
});
|
|
@ -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
|
||||
};
|
||||
};
|
30
packages/client/src/hooks/useLibraryContext.tsx
Normal file
30
packages/client/src/hooks/useLibraryContext.tsx
Normal 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;
|
||||
};
|
|
@ -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 }]);
|
||||
}
|
||||
};
|
||||
})
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
];
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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]);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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> 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;
|
||||
|
|
|
@ -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)]);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue