mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-08 06:02:49 +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 { MenuProvider } from 'react-native-popup-menu';
|
||||||
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
import { SafeAreaProvider } from 'react-native-safe-area-context';
|
||||||
import { useDeviceContext } from 'twrnc';
|
import { useDeviceContext } from 'twrnc';
|
||||||
|
import { proxy, useSnapshot } from 'valtio';
|
||||||
import {
|
import {
|
||||||
|
ClientContextProvider,
|
||||||
LibraryContextProvider,
|
LibraryContextProvider,
|
||||||
getDebugState,
|
getDebugState,
|
||||||
queryClient,
|
queryClient,
|
||||||
rspc,
|
rspc,
|
||||||
useCurrentLibrary,
|
useClientContext,
|
||||||
useInvalidateQuery
|
useInvalidateQuery
|
||||||
} from '@sd/client';
|
} from '@sd/client';
|
||||||
import { GlobalModals } from './components/modal/GlobalModals';
|
import { GlobalModals } from './components/modal/GlobalModals';
|
||||||
|
@ -25,6 +27,7 @@ import { reactNativeLink } from './lib/rspcReactNativeTransport';
|
||||||
import { tw } from './lib/tailwind';
|
import { tw } from './lib/tailwind';
|
||||||
import RootNavigator from './navigation';
|
import RootNavigator from './navigation';
|
||||||
import OnboardingNavigator from './navigation/OnboardingNavigator';
|
import OnboardingNavigator from './navigation/OnboardingNavigator';
|
||||||
|
import { currentLibraryStore } from './utils/nav';
|
||||||
|
|
||||||
dayjs.extend(advancedFormat);
|
dayjs.extend(advancedFormat);
|
||||||
dayjs.extend(relativeTime);
|
dayjs.extend(relativeTime);
|
||||||
|
@ -35,33 +38,47 @@ const NavigatorTheme: Theme = {
|
||||||
colors: {
|
colors: {
|
||||||
...DefaultTheme.colors,
|
...DefaultTheme.colors,
|
||||||
// Default screen background
|
// 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() {
|
function AppContainer() {
|
||||||
// Enables dark mode, and screen size breakpoints, etc. for tailwind
|
// Enables dark mode, and screen size breakpoints, etc. for tailwind
|
||||||
useDeviceContext(tw, { withDeviceColorScheme: false });
|
useDeviceContext(tw, { withDeviceColorScheme: false });
|
||||||
|
|
||||||
useInvalidateQuery();
|
useInvalidateQuery();
|
||||||
|
|
||||||
const { library } = useCurrentLibrary();
|
const { id } = useSnapshot(currentLibraryStore);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeAreaProvider style={tw`bg-app flex-1`}>
|
<SafeAreaProvider style={tw`bg-app flex-1`}>
|
||||||
<GestureHandlerRootView style={tw`flex-1`}>
|
<GestureHandlerRootView style={tw`flex-1`}>
|
||||||
<MenuProvider>
|
<MenuProvider>
|
||||||
<BottomSheetModalProvider>
|
<BottomSheetModalProvider>
|
||||||
<StatusBar style="light" />
|
<StatusBar style="light" />
|
||||||
<NavigationContainer theme={NavigatorTheme}>
|
<ClientContextProvider currentLibraryId={id}>
|
||||||
{!library ? (
|
<AppNavigation />
|
||||||
<OnboardingNavigator />
|
</ClientContextProvider>
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<RootNavigator />
|
|
||||||
<GlobalModals />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</NavigationContainer>
|
|
||||||
</BottomSheetModalProvider>
|
</BottomSheetModalProvider>
|
||||||
</MenuProvider>
|
</MenuProvider>
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
@ -85,13 +102,7 @@ export default function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<rspc.Provider client={client} queryClient={queryClient}>
|
<rspc.Provider client={client} queryClient={queryClient}>
|
||||||
<LibraryContextProvider
|
<AppContainer />
|
||||||
onNoLibrary={() => {
|
|
||||||
console.log('TODO');
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<AppContainer />
|
|
||||||
</LibraryContextProvider>
|
|
||||||
</rspc.Provider>
|
</rspc.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { queryClient, useBridgeMutation, useCurrentLibrary } from '@sd/client';
|
import { queryClient, useBridgeMutation } from '@sd/client';
|
||||||
import Dialog from '~/components/layout/Dialog';
|
import Dialog from '~/components/layout/Dialog';
|
||||||
import { Input } from '~/components/primitive/Input';
|
import { Input } from '~/components/primitive/Input';
|
||||||
|
import { currentLibraryStore } from '~/utils/nav';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
|
@ -14,8 +15,6 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
|
||||||
const [libName, setLibName] = useState('');
|
const [libName, setLibName] = useState('');
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
||||||
const { switchLibrary } = useCurrentLibrary();
|
|
||||||
|
|
||||||
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
|
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
|
||||||
'library.create',
|
'library.create',
|
||||||
{
|
{
|
||||||
|
@ -27,7 +26,7 @@ const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props
|
||||||
queryClient.setQueryData(['library.list'], (libraries: any) => [...(libraries || []), lib]);
|
queryClient.setQueryData(['library.list'], (libraries: any) => [...(libraries || []), lib]);
|
||||||
|
|
||||||
// Switch to the new library
|
// Switch to the new library
|
||||||
switchLibrary(lib.uuid);
|
currentLibraryStore.id = lib.uuid;
|
||||||
|
|
||||||
onSubmit?.();
|
onSubmit?.();
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,8 +4,9 @@ import { MotiView } from 'moti';
|
||||||
import { CaretDown, Gear, Lock, Plus } from 'phosphor-react-native';
|
import { CaretDown, Gear, Lock, Plus } from 'phosphor-react-native';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Alert, Pressable, Text, View } from 'react-native';
|
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 { tw, twStyle } from '~/lib/tailwind';
|
||||||
|
import { currentLibraryStore } from '~/utils/nav';
|
||||||
import { AnimatedHeight } from '../animation/layout';
|
import { AnimatedHeight } from '../animation/layout';
|
||||||
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
|
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
|
||||||
import Divider from '../primitive/Divider';
|
import Divider from '../primitive/Divider';
|
||||||
|
@ -19,7 +20,7 @@ const DrawerLibraryManager = () => {
|
||||||
if (!isDrawerOpen) setDropdownClosed(true);
|
if (!isDrawerOpen) setDropdownClosed(true);
|
||||||
}, [isDrawerOpen]);
|
}, [isDrawerOpen]);
|
||||||
|
|
||||||
const { library: currentLibrary, libraries, switchLibrary } = useCurrentLibrary();
|
const { library: currentLibrary, libraries } = useClientContext();
|
||||||
|
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
@ -34,7 +35,7 @@ const DrawerLibraryManager = () => {
|
||||||
: 'border-b-app-box border-sidebar-line bg-sidebar-button rounded-t-md'
|
: '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
|
<MotiView
|
||||||
animate={{
|
animate={{
|
||||||
rotate: dropdownClosed ? '0deg' : '180deg',
|
rotate: dropdownClosed ? '0deg' : '180deg',
|
||||||
|
@ -47,27 +48,30 @@ const DrawerLibraryManager = () => {
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<AnimatedHeight hide={dropdownClosed}>
|
<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 */}
|
||||||
{libraries?.map((library) => (
|
{libraries.data?.map((library) => {
|
||||||
<Pressable key={library.uuid} onPress={() => switchLibrary(library.uuid)}>
|
console.log('library', library);
|
||||||
<View
|
return (
|
||||||
style={twStyle(
|
<Pressable key={library.uuid} onPress={() => (currentLibraryStore.id = library.uuid)}>
|
||||||
'mt-1 p-2',
|
<View
|
||||||
currentLibrary.uuid === library.uuid && 'bg-accent rounded'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={twStyle(
|
style={twStyle(
|
||||||
'text-ink text-sm font-semibold',
|
'mt-1 p-2',
|
||||||
currentLibrary.uuid === library.uuid && 'text-white'
|
currentLibrary.uuid === library.uuid && 'bg-accent rounded'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{library.config.name}
|
<Text
|
||||||
</Text>
|
style={twStyle(
|
||||||
</View>
|
'text-ink text-sm font-semibold',
|
||||||
</Pressable>
|
currentLibrary.uuid === library.uuid && 'text-white'
|
||||||
))}
|
)}
|
||||||
|
>
|
||||||
|
{library.config.name}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
);
|
||||||
|
})}
|
||||||
<Divider style={tw`my-2`} />
|
<Divider style={tw`my-2`} />
|
||||||
{/* Menu */}
|
{/* Menu */}
|
||||||
{/* Create Library */}
|
{/* Create Library */}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { Trash } from 'phosphor-react-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { Alert, Text, View } from 'react-native';
|
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 { Button } from '~/components/primitive/Button';
|
||||||
import { Input } from '~/components/primitive/Input';
|
import { Input } from '~/components/primitive/Input';
|
||||||
import { Switch } from '~/components/primitive/Switch';
|
import { Switch } from '~/components/primitive/Switch';
|
||||||
|
@ -15,7 +15,7 @@ import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
|
||||||
const LibraryGeneralSettingsScreen = ({
|
const LibraryGeneralSettingsScreen = ({
|
||||||
navigation
|
navigation
|
||||||
}: SettingsStackScreenProps<'LibraryGeneralSettings'>) => {
|
}: SettingsStackScreenProps<'LibraryGeneralSettings'>) => {
|
||||||
const { library } = useCurrentLibrary();
|
const { library } = useLibraryContext();
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: { name: library.config.name, description: library.config.description }
|
defaultValues: { name: library.config.name, description: library.config.description }
|
||||||
|
|
|
@ -3,6 +3,11 @@ import {
|
||||||
ParamListBase,
|
ParamListBase,
|
||||||
getFocusedRouteNameFromRoute
|
getFocusedRouteNameFromRoute
|
||||||
} from '@react-navigation/native';
|
} 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) {
|
export const getActiveRouteFromState = function (state: any) {
|
||||||
if (!state.routes || state.routes.length === 0 || state.index >= state.routes.length) {
|
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';
|
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 { internal_createReactHooksFactory } from '@rspc/react';
|
||||||
import { QueryClient } from '@tanstack/react-query';
|
import { QueryClient } from '@tanstack/react-query';
|
||||||
import { LibraryArgs, Procedures } from './core';
|
import { LibraryArgs, Procedures } from './core';
|
||||||
import { getLibraryIdRaw } from './index';
|
import { currentLibraryCache } from './hooks';
|
||||||
import { normiCustomHooks } from './normi';
|
import { normiCustomHooks } from './normi';
|
||||||
|
|
||||||
type NonLibraryProcedure<T extends keyof Procedures> =
|
type NonLibraryProcedure<T extends keyof Procedures> =
|
||||||
|
@ -24,6 +24,10 @@ type StripLibraryArgsFromInput<T extends ProcedureDef> = T extends any
|
||||||
: never
|
: never
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
|
let getLibraryId: () => string | null;
|
||||||
|
|
||||||
|
export const setLibraryIdGetter = (g: typeof getLibraryId) => (getLibraryId = g);
|
||||||
|
|
||||||
export const hooks = internal_createReactHooksFactory();
|
export const hooks = internal_createReactHooksFactory();
|
||||||
|
|
||||||
const nonLibraryHooks = hooks.createHooks<
|
const nonLibraryHooks = hooks.createHooks<
|
||||||
|
@ -50,16 +54,16 @@ const libraryHooks = hooks.createHooks<
|
||||||
customHooks: normiCustomHooks({ contextSharing: true }, () => {
|
customHooks: normiCustomHooks({ contextSharing: true }, () => {
|
||||||
return {
|
return {
|
||||||
mapQueryKey: (keyAndInput) => {
|
mapQueryKey: (keyAndInput) => {
|
||||||
const library_id = getLibraryIdRaw();
|
const libraryId = currentLibraryCache.id;
|
||||||
if (library_id === null)
|
if (libraryId === null)
|
||||||
throw new Error('Attempted to do library operation with no library set!');
|
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) => {
|
doMutation: (keyAndInput, next) => {
|
||||||
const library_id = getLibraryIdRaw();
|
const libraryId = currentLibraryCache.id;
|
||||||
if (library_id === null)
|
if (libraryId === null)
|
||||||
throw new Error('Attempted to do library operation with no library set!');
|
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 duration from 'dayjs/plugin/duration';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
import { MemoryRouter, useLocation, useNavigate } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { LibraryContextProvider, queryClient, useDebugState } from '@sd/client';
|
import { queryClient, useDebugState } from '@sd/client';
|
||||||
import { Dialogs } from '@sd/ui';
|
import { Dialogs } from '@sd/ui';
|
||||||
import { AppRouter } from './AppRouter';
|
import { AppRouter } from './AppRouter';
|
||||||
import { ErrorFallback } from './ErrorFallback';
|
import { ErrorFallback } from './ErrorFallback';
|
||||||
|
@ -35,8 +35,9 @@ export default function SpacedriveInterface() {
|
||||||
<QueryClientProvider client={queryClient} contextSharing={true}>
|
<QueryClientProvider client={queryClient} contextSharing={true}>
|
||||||
<Devtools />
|
<Devtools />
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AppRouterWrapper />
|
<AppRouter />
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
|
<Dialogs />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
|
@ -56,20 +57,3 @@ function Devtools() {
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : 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 clsx from 'clsx';
|
||||||
import { Suspense } from 'react';
|
import { Suspense } from 'react';
|
||||||
import { Outlet } from 'react-router-dom';
|
import { Navigate, Outlet } from 'react-router-dom';
|
||||||
import { useCurrentLibrary } from '@sd/client';
|
import { ClientContextProvider, LibraryContextProvider, useClientContext } from '@sd/client';
|
||||||
import { Sidebar } from '~/components/layout/Sidebar';
|
import { Sidebar } from '~/components/layout/Sidebar';
|
||||||
import { Toasts } from '~/components/primitive/Toasts';
|
import { Toasts } from '~/components/primitive/Toasts';
|
||||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||||
|
import { useLibraryId } from './util';
|
||||||
|
|
||||||
|
function AppLayout() {
|
||||||
|
const { libraries, library } = useClientContext();
|
||||||
|
|
||||||
export function AppLayout() {
|
|
||||||
const { libraries } = useCurrentLibrary();
|
|
||||||
const os = useOperatingSystem();
|
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 (library === null && libraries.data)
|
||||||
if (libraries?.length === 0) {
|
return <Navigate to={`${libraries.data[0].uuid}/overview`} />;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -32,11 +32,27 @@ export function AppLayout() {
|
||||||
>
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div className="relative flex w-full">
|
<div className="relative flex w-full">
|
||||||
<Suspense fallback={<div className="bg-app h-screen w-screen" />}>
|
{library ? (
|
||||||
<Outlet />
|
<LibraryContextProvider library={library}>
|
||||||
</Suspense>
|
<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>
|
</div>
|
||||||
<Toasts />
|
<Toasts />
|
||||||
</div>
|
</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 { Navigate, useRoutes } from 'react-router-dom';
|
||||||
import { useCurrentLibrary, useInvalidateQuery } from '@sd/client';
|
import { currentLibraryCache, useCachedLibraries, useInvalidateQuery } from '@sd/client';
|
||||||
import { AppLayout } from '~/AppLayout';
|
import AppLayout from '~/AppLayout';
|
||||||
import { useKeybindHandler } from '~/hooks/useKeyboardHandler';
|
import { useKeybindHandler } from '~/hooks/useKeyboardHandler';
|
||||||
import screens from '~/screens';
|
import screens from '~/screens';
|
||||||
import { lazyEl } from '~/util';
|
import OnboardingRoot, { ONBOARDING_ROUTES } from './components/onboarding/OnboardingRoot';
|
||||||
import OnboardingRoot, { ONBOARDING_SCREENS } 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() {
|
export function AppRouter() {
|
||||||
const { library } = useCurrentLibrary();
|
|
||||||
|
|
||||||
useKeybindHandler();
|
useKeybindHandler();
|
||||||
useInvalidateQuery();
|
useInvalidateQuery();
|
||||||
|
|
||||||
return (
|
return useRoutes([
|
||||||
<Routes>
|
{
|
||||||
<Route path="onboarding" element={<OnboardingRoot />}>
|
index: true,
|
||||||
<Route index element={<Navigate to="start" />} />
|
element: <Index />
|
||||||
{ONBOARDING_SCREENS.map(({ key, component: ScreenComponent }, index) => (
|
},
|
||||||
<Route key={key} path={key} element={<ScreenComponent />} />
|
{
|
||||||
))}
|
path: 'onboarding',
|
||||||
</Route>
|
element: <OnboardingRoot />,
|
||||||
|
children: ONBOARDING_ROUTES
|
||||||
<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 ? (
|
path: ':libraryId',
|
||||||
<Route
|
element: <AppLayout />,
|
||||||
path="*"
|
children: screens
|
||||||
element={
|
}
|
||||||
<h1 className="p-4 text-white">Please select or create a library in the sidebar.</h1>
|
]);
|
||||||
}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{screens}
|
|
||||||
<Route path="*" element={NotFound} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Route>
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useCallback, useEffect, useState } from 'react';
|
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 { useExplorerStore } from '~/hooks/useExplorerStore';
|
||||||
import { Inspector } from '../explorer/Inspector';
|
import { Inspector } from '../explorer/Inspector';
|
||||||
import { ExplorerContextMenu } from './ExplorerContextMenu';
|
import { ExplorerContextMenu } from './ExplorerContextMenu';
|
||||||
|
@ -12,7 +12,7 @@ interface Props {
|
||||||
|
|
||||||
export default function Explorer(props: Props) {
|
export default function Explorer(props: Props) {
|
||||||
const expStore = useExplorerStore();
|
const expStore = useExplorerStore();
|
||||||
const { library } = useCurrentLibrary();
|
const { library } = useLibraryContext();
|
||||||
|
|
||||||
const [scrollSegments, setScrollSegments] = useState<{ [key: string]: number }>({});
|
const [scrollSegments, setScrollSegments] = useState<{ [key: string]: number }>({});
|
||||||
const [separateTopBar, setSeparateTopBar] = useState<boolean>(false);
|
const [separateTopBar, setSeparateTopBar] = useState<boolean>(false);
|
||||||
|
|
|
@ -19,8 +19,8 @@ import {
|
||||||
import { PropsWithChildren, useMemo } from 'react';
|
import { PropsWithChildren, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
ExplorerItem,
|
ExplorerItem,
|
||||||
getLibraryIdRaw,
|
|
||||||
isObject,
|
isObject,
|
||||||
|
useLibraryContext,
|
||||||
useLibraryMutation,
|
useLibraryMutation,
|
||||||
useLibraryQuery
|
useLibraryQuery
|
||||||
} from '@sd/client';
|
} from '@sd/client';
|
||||||
|
@ -28,6 +28,7 @@ import { ContextMenu as CM, dialogManager } from '@sd/ui';
|
||||||
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
import { getExplorerStore, useExplorerStore } from '~/hooks/useExplorerStore';
|
||||||
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
import { useOperatingSystem } from '~/hooks/useOperatingSystem';
|
||||||
import { useExplorerParams } from '~/screens/LocationExplorer';
|
import { useExplorerParams } from '~/screens/LocationExplorer';
|
||||||
|
import { useLibraryId } from '~/util';
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
import { showAlertDialog } from '~/util/dialog';
|
import { showAlertDialog } from '~/util/dialog';
|
||||||
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
|
import { DecryptFileDialog } from '../dialog/DecryptFileDialog';
|
||||||
|
@ -211,18 +212,15 @@ export interface FileItemContextMenuProps extends PropsWithChildren {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps) {
|
export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps) {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
const store = useExplorerStore();
|
const store = useExplorerStore();
|
||||||
const params = useExplorerParams();
|
const params = useExplorerParams();
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
const objectData = data ? (isObject(data) ? data.item : data.item.object) : null;
|
||||||
|
|
||||||
const isUnlockedQuery = useLibraryQuery(['keys.isUnlocked']);
|
const keyManagerUnlocked = useLibraryQuery(['keys.isUnlocked']).data ?? false;
|
||||||
const isUnlocked =
|
const mountedKeys = useLibraryQuery(['keys.listMounted']);
|
||||||
isUnlockedQuery.data !== undefined && isUnlockedQuery.data === true ? true : false;
|
const hasMountedKeys = mountedKeys.data?.length ?? 0 > 0;
|
||||||
|
|
||||||
const mountedUuids = useLibraryQuery(['keys.listMounted']);
|
|
||||||
const hasMountedKeys =
|
|
||||||
mountedUuids.data !== undefined && mountedUuids.data.length > 0 ? true : false;
|
|
||||||
|
|
||||||
const copyFiles = useLibraryMutation('files.copyFiles');
|
const copyFiles = useLibraryMutation('files.copyFiles');
|
||||||
|
|
||||||
|
@ -232,10 +230,10 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
||||||
<CM.Item
|
<CM.Item
|
||||||
label="Open"
|
label="Open"
|
||||||
keybind="⌘O"
|
keybind="⌘O"
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
// TODO: Replace this with a proper UI
|
// TODO: Replace this with a proper UI
|
||||||
window.location.href = platform.getFileUrl(
|
window.location.href = platform.getFileUrl(
|
||||||
getLibraryIdRaw()!,
|
library.uuid,
|
||||||
store.locationId!,
|
store.locationId!,
|
||||||
data.item.id
|
data.item.id
|
||||||
);
|
);
|
||||||
|
@ -255,7 +253,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
||||||
<CM.Item
|
<CM.Item
|
||||||
label="Duplicate"
|
label="Duplicate"
|
||||||
keybind="⌘D"
|
keybind="⌘D"
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
copyFiles.mutate({
|
copyFiles.mutate({
|
||||||
source_location_id: store.locationId!,
|
source_location_id: store.locationId!,
|
||||||
source_path_id: data.item.id,
|
source_path_id: data.item.id,
|
||||||
|
@ -269,7 +267,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
||||||
<CM.Item
|
<CM.Item
|
||||||
label="Cut"
|
label="Cut"
|
||||||
keybind="⌘X"
|
keybind="⌘X"
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
getExplorerStore().cutCopyState = {
|
getExplorerStore().cutCopyState = {
|
||||||
sourceLocationId: store.locationId!,
|
sourceLocationId: store.locationId!,
|
||||||
sourcePathId: data.item.id,
|
sourcePathId: data.item.id,
|
||||||
|
@ -283,7 +281,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
||||||
<CM.Item
|
<CM.Item
|
||||||
label="Copy"
|
label="Copy"
|
||||||
keybind="⌘C"
|
keybind="⌘C"
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
getExplorerStore().cutCopyState = {
|
getExplorerStore().cutCopyState = {
|
||||||
sourceLocationId: store.locationId!,
|
sourceLocationId: store.locationId!,
|
||||||
sourcePathId: data.item.id,
|
sourcePathId: data.item.id,
|
||||||
|
@ -297,7 +295,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
||||||
<CM.Item
|
<CM.Item
|
||||||
label="Deselect"
|
label="Deselect"
|
||||||
hidden={!store.cutCopyState.active}
|
hidden={!store.cutCopyState.active}
|
||||||
onClick={(e) => {
|
onClick={() => {
|
||||||
getExplorerStore().cutCopyState = {
|
getExplorerStore().cutCopyState = {
|
||||||
...store.cutCopyState,
|
...store.cutCopyState,
|
||||||
active: false
|
active: false
|
||||||
|
@ -334,7 +332,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
||||||
icon={LockSimple}
|
icon={LockSimple}
|
||||||
keybind="⌘E"
|
keybind="⌘E"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isUnlocked && hasMountedKeys) {
|
if (keyManagerUnlocked && hasMountedKeys) {
|
||||||
dialogManager.create((dp) => (
|
dialogManager.create((dp) => (
|
||||||
<EncryptFileDialog
|
<EncryptFileDialog
|
||||||
{...dp}
|
{...dp}
|
||||||
|
@ -342,7 +340,7 @@ export function FileItemContextMenu({ data, ...props }: FileItemContextMenuProps
|
||||||
path_id={data.item.id}
|
path_id={data.item.id}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
} else if (!isUnlocked) {
|
} else if (!keyManagerUnlocked) {
|
||||||
showAlertDialog({
|
showAlertDialog({
|
||||||
title: 'Key manager locked',
|
title: 'Key manager locked',
|
||||||
value: 'The key manager is currently locked. Please unlock it and try again.'
|
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}
|
icon={LockSimpleOpen}
|
||||||
keybind="⌘D"
|
keybind="⌘D"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isUnlocked) {
|
if (keyManagerUnlocked) {
|
||||||
dialogManager.create((dp) => (
|
dialogManager.create((dp) => (
|
||||||
<DecryptFileDialog
|
<DecryptFileDialog
|
||||||
{...dp}
|
{...dp}
|
||||||
|
|
|
@ -277,7 +277,7 @@ export const TopBar: React.FC<TopBarProps> = (props) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="block w-[350px]">
|
<div className="block w-[350px]">
|
||||||
<KeyManager className={TOP_BAR_ICON_STYLE} />
|
<KeyManager /* className={TOP_BAR_ICON_STYLE} */ />
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popover>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
|
@ -2,17 +2,18 @@ import { Eye, EyeSlash, Gear, Lock } from 'phosphor-react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
import { Button, ButtonLink, Input, Tabs } from '@sd/ui';
|
import { Button, ButtonLink, Input, Tabs } from '@sd/ui';
|
||||||
|
import { useLibraryId } from '~/util';
|
||||||
import { showAlertDialog } from '~/util/dialog';
|
import { showAlertDialog } from '~/util/dialog';
|
||||||
import { DefaultProps } from '../primitive/types';
|
|
||||||
import { KeyList } from './KeyList';
|
import { KeyList } from './KeyList';
|
||||||
import { KeyMounter } from './KeyMounter';
|
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 isUnlocked = useLibraryQuery(['keys.isUnlocked']);
|
||||||
const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' });
|
const keyringSk = useLibraryQuery(['keys.getSecretKey'], { initialData: '' });
|
||||||
const isKeyManagerUnlocking = useLibraryQuery(['keys.isKeyManagerUnlocking']);
|
const isKeyManagerUnlocking = useLibraryQuery(['keys.isKeyManagerUnlocking']);
|
||||||
|
|
||||||
const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', {
|
const unlockKeyManager = useLibraryMutation('keys.unlockKeyManager', {
|
||||||
onError: () => {
|
onError: () => {
|
||||||
showAlertDialog({
|
showAlertDialog({
|
||||||
|
@ -94,12 +95,7 @@ export function KeyManager(props: KeyManagerProps) {
|
||||||
</Button>
|
</Button>
|
||||||
{!enterSkManually && (
|
{!enterSkManually && (
|
||||||
<div className="relative flex grow">
|
<div className="relative flex grow">
|
||||||
<p
|
<p className="text-accent mt-2" onClick={() => setEnterSkManually(true)}>
|
||||||
className="text-accent mt-2"
|
|
||||||
onClick={(e) => {
|
|
||||||
setEnterSkManually(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
or enter secret key manually
|
or enter secret key manually
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
@ -131,7 +127,7 @@ export function KeyManager(props: KeyManagerProps) {
|
||||||
<Lock className="text-ink-faint h-4 w-4" />
|
<Lock className="text-ink-faint h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
to="/settings/keys"
|
to={`/${libraryId}/settings/overview`}
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
className="text-ink-faint"
|
className="text-ink-faint"
|
||||||
|
|
|
@ -11,14 +11,14 @@ import {
|
||||||
UsersThree
|
UsersThree
|
||||||
} from 'phosphor-react';
|
} from 'phosphor-react';
|
||||||
import React, { PropsWithChildren, useEffect } from 'react';
|
import React, { PropsWithChildren, useEffect } from 'react';
|
||||||
import { NavLink, NavLinkProps, useNavigate } from 'react-router-dom';
|
import { NavLink, NavLinkProps, useLocation } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Location,
|
Location,
|
||||||
LocationCreateArgs,
|
LocationCreateArgs,
|
||||||
arraysEqual,
|
arraysEqual,
|
||||||
getDebugState,
|
getDebugState,
|
||||||
useBridgeQuery,
|
useBridgeQuery,
|
||||||
useCurrentLibrary,
|
useClientContext,
|
||||||
useDebugState,
|
useDebugState,
|
||||||
useLibraryMutation,
|
useLibraryMutation,
|
||||||
useLibraryQuery,
|
useLibraryQuery,
|
||||||
|
@ -56,10 +56,10 @@ const SidebarFooter = tw.div`flex flex-col mb-3 px-2.5`;
|
||||||
|
|
||||||
export function Sidebar() {
|
export function Sidebar() {
|
||||||
// DO NOT DO LIBRARY QUERIES OR MUTATIONS HERE. This is rendered before a library is set.
|
// DO NOT DO LIBRARY QUERIES OR MUTATIONS HERE. This is rendered before a library is set.
|
||||||
|
|
||||||
const os = useOperatingSystem();
|
const os = useOperatingSystem();
|
||||||
const { library, libraries, isLoading: isLoadingLibraries, switchLibrary } = useCurrentLibrary();
|
const { library, libraries, currentLibraryId } = useClientContext();
|
||||||
const debugState = useDebugState();
|
const debugState = useDebugState();
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Prevent the dropdown button to be auto focused on launch
|
// Prevent the dropdown button to be auto focused on launch
|
||||||
|
@ -69,7 +69,9 @@ export function Sidebar() {
|
||||||
|
|
||||||
(document.activeElement.blur as () => void)();
|
(document.activeElement.blur as () => void)();
|
||||||
});
|
});
|
||||||
});
|
}, []);
|
||||||
|
|
||||||
|
console.log(useLocation());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SidebarBody className={macOnly(os, 'bg-opacity-[0.75]')}>
|
<SidebarBody className={macOnly(os, 'bg-opacity-[0.75]')}>
|
||||||
|
@ -87,24 +89,21 @@ export function Sidebar() {
|
||||||
// these classname overrides are messy
|
// these classname overrides are messy
|
||||||
// but they work
|
// 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`,
|
`!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">
|
<span className="truncate">
|
||||||
{isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '}
|
{libraries.isLoading ? 'Loading...' : library ? library.config.name : ' '}
|
||||||
</span>
|
</span>
|
||||||
</Dropdown.Button>
|
</Dropdown.Button>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Dropdown.Section>
|
<Dropdown.Section>
|
||||||
{libraries?.map((lib) => (
|
{libraries.data?.map((lib) => (
|
||||||
<Dropdown.Item
|
<Dropdown.Item
|
||||||
selected={lib.uuid === library?.uuid}
|
to={`/${lib.uuid}/overview`}
|
||||||
key={lib.uuid}
|
key={lib.uuid}
|
||||||
onClick={() => {
|
selected={lib.uuid === currentLibraryId}
|
||||||
switchLibrary(lib.uuid);
|
|
||||||
navigate('/');
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{lib.config.name}
|
{lib.config.name}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
|
@ -129,7 +128,7 @@ export function Sidebar() {
|
||||||
</Dropdown.Root>
|
</Dropdown.Root>
|
||||||
<SidebarContents>
|
<SidebarContents>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<SidebarLink to="/overview">
|
<SidebarLink to="overview">
|
||||||
<Icon component={Planet} />
|
<Icon component={Planet} />
|
||||||
Overview
|
Overview
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
@ -146,13 +145,13 @@ export function Sidebar() {
|
||||||
Media
|
Media
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
</div>
|
</div>
|
||||||
{library && <LibraryScopedSection />}
|
{library && <LibraryScopedSection key={library.uuid} />}
|
||||||
<div className="grow" />
|
<div className="grow" />
|
||||||
</SidebarContents>
|
</SidebarContents>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<ButtonLink
|
<ButtonLink
|
||||||
to="/settings/general"
|
to="settings/general"
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
className="text-ink-faint ring-offset-sidebar"
|
className="text-ink-faint ring-offset-sidebar"
|
||||||
|
@ -308,11 +307,12 @@ export const SidebarLink = (props: PropsWithChildren<NavLinkProps>) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SidebarSection: React.FC<{
|
const SidebarSection = (
|
||||||
name: string;
|
props: PropsWithChildren<{
|
||||||
actionArea?: React.ReactNode;
|
name: string;
|
||||||
children: React.ReactNode;
|
actionArea?: React.ReactNode;
|
||||||
}> = (props) => {
|
}>
|
||||||
|
) => {
|
||||||
return (
|
return (
|
||||||
<div className="group mt-5">
|
<div className="group mt-5">
|
||||||
<div className="mb-1 flex items-center justify-between">
|
<div className="mb-1 flex items-center justify-between">
|
||||||
|
@ -354,7 +354,7 @@ function LibraryScopedSection() {
|
||||||
actionArea={
|
actionArea={
|
||||||
<>
|
<>
|
||||||
{/* <SidebarHeadingOptionsButton to="/settings/locations" icon={CogIcon} /> */}
|
{/* <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) {
|
function SidebarLocation({ location, online }: SidebarLocationProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center">
|
<div className="flex flex-row items-center">
|
||||||
<SidebarLink
|
<SidebarLink className="group relative w-full" to={`location/${location.id}`}>
|
||||||
className="group relative w-full"
|
|
||||||
to={{
|
|
||||||
pathname: `location/${location.id}`
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="relative -mt-0.5 mr-1 shrink-0 grow-0">
|
<div className="relative -mt-0.5 mr-1 shrink-0 grow-0">
|
||||||
<Folder size={18} />
|
<Folder size={18} />
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -28,7 +28,7 @@ export default function LocationListItem({ location }: LocationListItemProps) {
|
||||||
<Card
|
<Card
|
||||||
className="hover:bg-app-box/70 cursor-pointer"
|
className="hover:bg-app-box/70 cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`/settings/locations/location/${location.id}`);
|
navigate(`${location.id}`);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Folder size={30} className="mr-3" />
|
<Folder size={30} className="mr-3" />
|
||||||
|
|
|
@ -2,7 +2,7 @@ import clsx from 'clsx';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { getOnboardingStore, unlockOnboardingScreen, useOnboardingStore } from '@sd/client';
|
import { getOnboardingStore, unlockOnboardingScreen, useOnboardingStore } from '@sd/client';
|
||||||
import { ONBOARDING_SCREENS } from './OnboardingRoot';
|
import { ONBOARDING_ROUTES } from './OnboardingRoot';
|
||||||
import { useCurrentOnboardingScreenKey } from './helpers/screens';
|
import { useCurrentOnboardingScreenKey } from './helpers/screens';
|
||||||
|
|
||||||
// screens are locked to prevent users from skipping ahead
|
// screens are locked to prevent users from skipping ahead
|
||||||
|
@ -24,21 +24,25 @@ export default function OnboardingProgress() {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-center">
|
<div className="flex w-full items-center justify-center">
|
||||||
<div className="flex items-center justify-center space-x-1">
|
<div className="flex items-center justify-center space-x-1">
|
||||||
{ONBOARDING_SCREENS.map(({ isSkippable, key }) => (
|
{ONBOARDING_ROUTES.map(({ path }) => {
|
||||||
<div
|
if (!path) return null;
|
||||||
key={key}
|
|
||||||
onClick={() => {
|
return (
|
||||||
if (ob_store.unlockedScreens.includes(key)) {
|
<div
|
||||||
navigate(`/onboarding/${key}`);
|
key={path}
|
||||||
}
|
onClick={() => {
|
||||||
}}
|
if (ob_store.unlockedScreens.includes(path)) {
|
||||||
className={clsx(
|
navigate(`/onboarding/${path}`);
|
||||||
'hover:bg-ink h-2 w-2 rounded-full transition',
|
}
|
||||||
currentScreenKey === key ? 'bg-ink' : 'bg-ink-faint',
|
}}
|
||||||
!ob_store.unlockedScreens.includes(key) && 'opacity-10'
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import BloomOne from '@sd/assets/images/bloom-one.png';
|
import BloomOne from '@sd/assets/images/bloom-one.png';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { ComponentType, useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { Outlet, useNavigate } from 'react-router';
|
import { Navigate, Outlet, RouteObject, useNavigate } from 'react-router';
|
||||||
import { getOnboardingStore } from '@sd/client';
|
import { getOnboardingStore } from '@sd/client';
|
||||||
import { tw } from '@sd/ui';
|
import { tw } from '@sd/ui';
|
||||||
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
|
import { useOperatingSystem } from '../../hooks/useOperatingSystem';
|
||||||
|
@ -12,42 +12,30 @@ import OnboardingPrivacy from './OnboardingPrivacy';
|
||||||
import OnboardingProgress from './OnboardingProgress';
|
import OnboardingProgress from './OnboardingProgress';
|
||||||
import OnboardingStart from './OnboardingStart';
|
import OnboardingStart from './OnboardingStart';
|
||||||
|
|
||||||
interface OnboardingScreen {
|
export const ONBOARDING_ROUTES: RouteObject[] = [
|
||||||
/**
|
|
||||||
* 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[] = [
|
|
||||||
{
|
{
|
||||||
component: OnboardingStart,
|
index: true,
|
||||||
key: 'start'
|
element: <Navigate to="start" />
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: OnboardingNewLibrary,
|
element: <OnboardingStart />,
|
||||||
key: 'new-library'
|
path: 'start'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: OnboardingMasterPassword,
|
element: <OnboardingNewLibrary />,
|
||||||
key: 'master-password'
|
path: 'new-library'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: OnboardingPrivacy,
|
element: <OnboardingMasterPassword />,
|
||||||
key: 'privacy'
|
path: 'master-password'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
component: OnboardingCreatingLibrary,
|
element: <OnboardingPrivacy />,
|
||||||
key: 'creating-library'
|
path: 'privacy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
element: <OnboardingCreatingLibrary />,
|
||||||
|
path: 'creating-library'
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -29,66 +29,66 @@ export const SettingsSidebar = () => {
|
||||||
)}
|
)}
|
||||||
<div className="px-4 pb-2.5">
|
<div className="px-4 pb-2.5">
|
||||||
<SettingsHeading className="!mt-2">Client</SettingsHeading>
|
<SettingsHeading className="!mt-2">Client</SettingsHeading>
|
||||||
<SidebarLink to="/settings/general">
|
<SidebarLink to="general">
|
||||||
<SettingsIcon component={GearSix} />
|
<SettingsIcon component={GearSix} />
|
||||||
General
|
General
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/libraries">
|
<SidebarLink to="libraries">
|
||||||
<SettingsIcon component={Books} />
|
<SettingsIcon component={Books} />
|
||||||
Libraries
|
Libraries
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/privacy">
|
<SidebarLink to="privacy">
|
||||||
<SettingsIcon component={ShieldCheck} />
|
<SettingsIcon component={ShieldCheck} />
|
||||||
Privacy
|
Privacy
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/appearance">
|
<SidebarLink to="appearance">
|
||||||
<SettingsIcon component={PaintBrush} />
|
<SettingsIcon component={PaintBrush} />
|
||||||
Appearance
|
Appearance
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/keybindings">
|
<SidebarLink to="keybindings">
|
||||||
<SettingsIcon component={KeyReturn} />
|
<SettingsIcon component={KeyReturn} />
|
||||||
Keybinds
|
Keybinds
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/extensions">
|
<SidebarLink to="extensions">
|
||||||
<SettingsIcon component={PuzzlePiece} />
|
<SettingsIcon component={PuzzlePiece} />
|
||||||
Extensions
|
Extensions
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
||||||
<SettingsHeading>Library</SettingsHeading>
|
<SettingsHeading>Library</SettingsHeading>
|
||||||
<SidebarLink to="/settings/library">
|
<SidebarLink to="library">
|
||||||
<SettingsIcon component={GearSix} />
|
<SettingsIcon component={GearSix} />
|
||||||
General
|
General
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/nodes">
|
<SidebarLink to="nodes">
|
||||||
<SettingsIcon component={ShareNetwork} />
|
<SettingsIcon component={ShareNetwork} />
|
||||||
Nodes
|
Nodes
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/locations">
|
<SidebarLink to="locations">
|
||||||
<SettingsIcon component={HardDrive} />
|
<SettingsIcon component={HardDrive} />
|
||||||
Locations
|
Locations
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/tags">
|
<SidebarLink to="tags">
|
||||||
<SettingsIcon component={TagSimple} />
|
<SettingsIcon component={TagSimple} />
|
||||||
Tags
|
Tags
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/keys">
|
<SidebarLink to="keys">
|
||||||
<SettingsIcon component={Key} />
|
<SettingsIcon component={Key} />
|
||||||
Keys
|
Keys
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SettingsHeading>Resources</SettingsHeading>
|
<SettingsHeading>Resources</SettingsHeading>
|
||||||
<SidebarLink to="/settings/about">
|
<SidebarLink to="about">
|
||||||
<SettingsIcon component={FlyingSaucer} />
|
<SettingsIcon component={FlyingSaucer} />
|
||||||
About
|
About
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/changelog">
|
<SidebarLink to="changelog">
|
||||||
<SettingsIcon component={Receipt} />
|
<SettingsIcon component={Receipt} />
|
||||||
Changelog
|
Changelog
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/dependencies">
|
<SidebarLink to="dependencies">
|
||||||
<SettingsIcon component={Graph} />
|
<SettingsIcon component={Graph} />
|
||||||
Dependencies
|
Dependencies
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
<SidebarLink to="/settings/support">
|
<SidebarLink to="support">
|
||||||
<SettingsIcon component={Heart} />
|
<SettingsIcon component={Heart} />
|
||||||
Support
|
Support
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
import { useEffect } from 'react';
|
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 { useDebouncedCallback } from 'use-debounce';
|
||||||
import { useCurrentLibrary } from '@sd/client';
|
|
||||||
|
|
||||||
export function useDebouncedForm<TFieldValues extends FieldValues = FieldValues, TContext = any>(
|
export function useDebouncedFormWatch<
|
||||||
form: UseFormReturn<{ id: string } & object, TContext>,
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
callback: (data: any) => void,
|
TContext = any
|
||||||
args?: { disableResetOnLibraryChange?: boolean }
|
>(form: UseFormReturn<TFieldValues, TContext>, callback: WatchObserver<TFieldValues>) {
|
||||||
) {
|
|
||||||
const { library } = useCurrentLibrary();
|
|
||||||
const debounced = useDebouncedCallback(callback, 500);
|
const debounced = useDebouncedCallback(callback, 500);
|
||||||
|
|
||||||
// listen for any form changes
|
// listen for any form changes
|
||||||
|
@ -16,12 +13,4 @@ export function useDebouncedForm<TFieldValues extends FieldValues = FieldValues,
|
||||||
|
|
||||||
// persist unchanged data when the component is unmounted
|
// persist unchanged data when the component is unmounted
|
||||||
useEffect(() => () => debounced.flush(), [debounced]);
|
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 { proxy, useSnapshot } from 'valtio';
|
||||||
import { onLibraryChange } from '@sd/client';
|
import { useLibraryContext } from '@sd/client';
|
||||||
import { resetStore } from '@sd/client/src/stores/util';
|
import { resetStore } from '@sd/client/src/stores/util';
|
||||||
|
|
||||||
export type ExplorerLayoutMode = 'list' | 'grid' | 'columns' | 'media';
|
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.
|
// Keep the private and use `useExplorerState` or `getExplorerStore` or you will get production build issues.
|
||||||
const explorerStore = proxy({
|
const explorerStore = proxy({
|
||||||
...state,
|
...state,
|
||||||
|
@ -53,6 +52,12 @@ const explorerStore = proxy({
|
||||||
});
|
});
|
||||||
|
|
||||||
export function useExplorerStore() {
|
export function useExplorerStore() {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
explorerStore.reset();
|
||||||
|
}, [library.uuid]);
|
||||||
|
|
||||||
return useSnapshot(explorerStore);
|
return useSnapshot(explorerStore);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useParams, useSearchParams } from 'react-router-dom';
|
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 Explorer from '~/components/explorer/Explorer';
|
||||||
import { getExplorerStore } from '~/hooks/useExplorerStore';
|
import { getExplorerStore } from '~/hooks/useExplorerStore';
|
||||||
|
|
||||||
export function useExplorerParams() {
|
export function useExplorerParams() {
|
||||||
const { id } = useParams();
|
const { id } = useParams<{ id?: string }>();
|
||||||
const location_id = id ? Number(id) : -1;
|
const location_id = id ? Number(id) : null;
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const path = searchParams.get('path') || '';
|
const path = searchParams.get('path') || '';
|
||||||
|
@ -17,16 +17,17 @@ export function useExplorerParams() {
|
||||||
|
|
||||||
export default function LocationExplorer() {
|
export default function LocationExplorer() {
|
||||||
const { location_id, path } = useExplorerParams();
|
const { location_id, path } = useExplorerParams();
|
||||||
const { library } = useCurrentLibrary();
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getExplorerStore().locationId = location_id;
|
getExplorerStore().locationId = location_id;
|
||||||
}, [location_id]);
|
}, [location_id]);
|
||||||
|
|
||||||
|
if (location_id === null) throw new Error(`location_id is null!`);
|
||||||
|
|
||||||
const explorerData = useLibraryQuery([
|
const explorerData = useLibraryQuery([
|
||||||
'locations.getExplorerData',
|
'locations.getExplorerData',
|
||||||
{
|
{
|
||||||
location_id: location_id,
|
location_id,
|
||||||
path: path,
|
path: path,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
cursor: null
|
cursor: null
|
||||||
|
|
|
@ -11,19 +11,12 @@ import {
|
||||||
MusicNote,
|
MusicNote,
|
||||||
Wrench
|
Wrench
|
||||||
} from 'phosphor-react';
|
} from 'phosphor-react';
|
||||||
import { useEffect } from 'react';
|
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
import 'react-loading-skeleton/dist/skeleton.css';
|
import 'react-loading-skeleton/dist/skeleton.css';
|
||||||
import { proxy } from 'valtio';
|
import { Statistics, useLibraryQuery } from '@sd/client';
|
||||||
import {
|
|
||||||
Statistics,
|
|
||||||
onLibraryChange,
|
|
||||||
queryClient,
|
|
||||||
useCurrentLibrary,
|
|
||||||
useLibraryQuery
|
|
||||||
} from '@sd/client';
|
|
||||||
import { Card } from '@sd/ui';
|
import { Card } from '@sd/ui';
|
||||||
import useCounter from '~/hooks/useCounter';
|
import useCounter from '~/hooks/useCounter';
|
||||||
|
import { useLibraryId } from '~/util';
|
||||||
import { usePlatform } from '~/util/Platform';
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
|
||||||
interface StatItemProps {
|
interface StatItemProps {
|
||||||
|
@ -39,64 +32,33 @@ const StatItemNames: Partial<Record<keyof Statistics, string>> = {
|
||||||
total_bytes_free: 'Free space'
|
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;
|
const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames;
|
||||||
|
|
||||||
export const state = proxy({
|
let overviewMounted = false;
|
||||||
lastRenderedLibraryId: undefined as string | undefined
|
|
||||||
});
|
|
||||||
|
|
||||||
onLibraryChange((newLibraryId) => {
|
const StatItem = (props: StatItemProps) => {
|
||||||
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 { title, bytes = BigInt('0'), isLoading } = props;
|
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 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({
|
const count = useCounter({
|
||||||
name: title,
|
name: title,
|
||||||
end: +size.value,
|
end: +size.value,
|
||||||
duration: state.lastRenderedLibraryId === library?.uuid ? 0 : undefined,
|
duration: overviewMounted ? 0 : 1,
|
||||||
saveState: false
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
|
@ -126,23 +88,13 @@ const StatItem: React.FC<StatItemProps> = (props) => {
|
||||||
|
|
||||||
export default function OverviewScreen() {
|
export default function OverviewScreen() {
|
||||||
const platform = usePlatform();
|
const platform = usePlatform();
|
||||||
const { library } = useCurrentLibrary();
|
const libraryId = useLibraryId();
|
||||||
const { data: overviewStats, isLoading: isStatisticsLoading } = useLibraryQuery(
|
|
||||||
['library.getStatistics'],
|
const stats = useLibraryQuery(['library.getStatistics'], {
|
||||||
{
|
initialData: { ...EMPTY_STATISTICS }
|
||||||
initialData: {
|
});
|
||||||
id: 0,
|
|
||||||
date_captured: '',
|
overviewMounted = true;
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="custom-scroll page-scroll app-background flex h-screen w-full flex-col overflow-x-hidden">
|
<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">
|
<div className="flex w-full">
|
||||||
{/* STAT CONTAINER */}
|
{/* STAT CONTAINER */}
|
||||||
<div className="-mb-1 flex h-20 overflow-hidden">
|
<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;
|
if (!displayableStatItems.includes(key)) return null;
|
||||||
return (
|
return (
|
||||||
<StatItem
|
<StatItem
|
||||||
key={library?.uuid + ' ' + key}
|
key={`${libraryId} ${key}`}
|
||||||
title={StatItemNames[key as keyof Statistics]!}
|
title={StatItemNames[key as keyof Statistics]!}
|
||||||
bytes={BigInt(value)}
|
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={MusicNote} category="Music" />
|
||||||
<CategoryButton icon={Image} category="Albums" />
|
<CategoryButton icon={Image} category="Albums" />
|
||||||
<CategoryButton icon={Heart} category="Favorites" />
|
<CategoryButton icon={Heart} category="Favorites" />
|
||||||
<Debug />
|
|
||||||
</div>
|
</div>
|
||||||
<Card className="text-ink-dull">
|
<Card className="text-ink-dull">
|
||||||
<b>Note: </b> This is a pre-alpha build of Spacedrive, many features are yet to be
|
<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 {
|
interface CategoryButtonProps {
|
||||||
category: string;
|
category: string;
|
||||||
icon: any;
|
icon: any;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
import { useCurrentLibrary, useLibraryQuery } from '@sd/client';
|
import { useLibraryContext, useLibraryQuery } from '@sd/client';
|
||||||
import Explorer from '~/components/explorer/Explorer';
|
import Explorer from '~/components/explorer/Explorer';
|
||||||
|
|
||||||
export default function TagExplorer() {
|
export default function TagExplorer() {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const { library } = useCurrentLibrary();
|
const { library } = useLibraryContext();
|
||||||
|
|
||||||
const explorerData = useLibraryQuery(['tags.getExplorerData', Number(id)]);
|
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 { lazyEl } from '~/util';
|
||||||
import settingsScreens from './settings';
|
import settingsScreens from './settings';
|
||||||
|
|
||||||
const routes: RouteProps[] = [
|
const screens: RouteObject[] = [
|
||||||
{
|
|
||||||
index: true,
|
|
||||||
element: <Navigate to="overview" relative="route" />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'overview',
|
path: 'overview',
|
||||||
element: lazyEl(() => import('./Overview'))
|
element: lazyEl(() => import('./Overview'))
|
||||||
|
@ -21,13 +17,8 @@ const routes: RouteProps[] = [
|
||||||
path: 'settings',
|
path: 'settings',
|
||||||
element: lazyEl(() => import('./settings/Layout')),
|
element: lazyEl(() => import('./settings/Layout')),
|
||||||
children: settingsScreens
|
children: settingsScreens
|
||||||
}
|
},
|
||||||
|
{ path: '*', element: lazyEl(() => import('./NotFound')) }
|
||||||
];
|
];
|
||||||
|
|
||||||
export default (
|
export default screens;
|
||||||
<>
|
|
||||||
{routes.map((route) => (
|
|
||||||
<Route key={route.path} {...route} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,11 +1,7 @@
|
||||||
import { Navigate, Route, RouteProps } from 'react-router-dom';
|
import { RouteObject } from 'react-router-dom';
|
||||||
import { lazyEl } from '~/util';
|
import { lazyEl } from '~/util';
|
||||||
import SettingsSubPage from './SettingsSubPage';
|
|
||||||
import LocationsSettings from './library/LocationsSettings';
|
|
||||||
import EditLocation from './library/location/EditLocation';
|
|
||||||
|
|
||||||
const routes: RouteProps[] = [
|
const screens: RouteObject[] = [
|
||||||
{ index: true, element: <Navigate to="general" relative="route" /> },
|
|
||||||
{ path: 'general', element: lazyEl(() => import('./client/GeneralSettings')) },
|
{ path: 'general', element: lazyEl(() => import('./client/GeneralSettings')) },
|
||||||
{ path: 'appearance', element: lazyEl(() => import('./client/AppearanceSettings')) },
|
{ path: 'appearance', element: lazyEl(() => import('./client/AppearanceSettings')) },
|
||||||
{ path: 'keybindings', element: lazyEl(() => import('./client/KeybindingSettings')) },
|
{ path: 'keybindings', element: lazyEl(() => import('./client/KeybindingSettings')) },
|
||||||
|
@ -27,18 +23,15 @@ const routes: RouteProps[] = [
|
||||||
{ path: 'about', element: lazyEl(() => import('./info/AboutSpacedrive')) },
|
{ path: 'about', element: lazyEl(() => import('./info/AboutSpacedrive')) },
|
||||||
{ path: 'changelog', element: lazyEl(() => import('./info/Changelog')) },
|
{ path: 'changelog', element: lazyEl(() => import('./info/Changelog')) },
|
||||||
{ path: 'dependencies', element: lazyEl(() => import('./info/Dependencies')) },
|
{ 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 (
|
export default screens;
|
||||||
<>
|
|
||||||
{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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
|
@ -1,25 +1,24 @@
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { useBridgeMutation } from '@sd/client';
|
import { useBridgeMutation, useLibraryContext } from '@sd/client';
|
||||||
import { useCurrentLibrary } from '@sd/client';
|
|
||||||
import { Button, Input, Switch } from '@sd/ui';
|
import { Button, Input, Switch } from '@sd/ui';
|
||||||
import { InputContainer } from '~/components/primitive/InputContainer';
|
import { InputContainer } from '~/components/primitive/InputContainer';
|
||||||
import { SettingsContainer } from '~/components/settings/SettingsContainer';
|
import { SettingsContainer } from '~/components/settings/SettingsContainer';
|
||||||
import { SettingsHeader } from '~/components/settings/SettingsHeader';
|
import { SettingsHeader } from '~/components/settings/SettingsHeader';
|
||||||
import { useDebouncedForm } from '~/hooks/useDebouncedForm';
|
import { useDebouncedFormWatch } from '~/hooks/useDebouncedForm';
|
||||||
|
|
||||||
export default function LibraryGeneralSettings() {
|
export default function LibraryGeneralSettings() {
|
||||||
const { library } = useCurrentLibrary();
|
const { library } = useLibraryContext();
|
||||||
const { mutate: editLibrary } = useBridgeMutation('library.edit');
|
const editLibrary = useBridgeMutation('library.edit');
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
defaultValues: { id: library!.uuid, ...library?.config }
|
defaultValues: { id: library!.uuid, ...library?.config }
|
||||||
});
|
});
|
||||||
|
|
||||||
useDebouncedForm(form, (value) =>
|
useDebouncedFormWatch(form, (value) =>
|
||||||
editLibrary({
|
editLibrary.mutate({
|
||||||
id: library!.uuid,
|
id: library.uuid,
|
||||||
name: value.name,
|
name: value.name ?? null,
|
||||||
description: value.description
|
description: value.description ?? null
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Archive, ArrowsClockwise, Info, Trash } from 'phosphor-react';
|
||||||
import { useFormState } from 'react-hook-form';
|
import { useFormState } from 'react-hook-form';
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
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 { Divider } from '~/components/explorer/inspector/Divider';
|
||||||
import { SettingsSubPage } from '~/components/settings/SettingsSubPage';
|
import { SettingsSubPage } from '~/components/settings/SettingsSubPage';
|
||||||
import { Tooltip } from '~/components/tooltip/Tooltip';
|
import { Tooltip } from '~/components/tooltip/Tooltip';
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Database, DotsSixVertical, Pencil, Trash } from 'phosphor-react';
|
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 { LibraryConfigWrapped } from '@sd/client';
|
||||||
import { Button, ButtonLink, Card, dialogManager, tw } from '@sd/ui';
|
import { Button, ButtonLink, Card, dialogManager, tw } from '@sd/ui';
|
||||||
import CreateLibraryDialog from '~/components/dialog/CreateLibraryDialog';
|
import CreateLibraryDialog from '~/components/dialog/CreateLibraryDialog';
|
||||||
|
@ -27,7 +27,7 @@ function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolea
|
||||||
<Database className="h-4 w-4" />
|
<Database className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</Button>
|
</Button>
|
||||||
<ButtonLink className="!p-1.5" to="/settings/library" variant="gray">
|
<ButtonLink className="!p-1.5" to="../library" variant="gray">
|
||||||
<Tooltip label="Edit Library">
|
<Tooltip label="Edit Library">
|
||||||
<Pencil className="h-4 w-4" />
|
<Pencil className="h-4 w-4" />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
@ -53,7 +53,7 @@ function LibraryListItem(props: { library: LibraryConfigWrapped; current: boolea
|
||||||
export default function LibrarySettings() {
|
export default function LibrarySettings() {
|
||||||
const libraries = useBridgeQuery(['library.list']);
|
const libraries = useBridgeQuery(['library.list']);
|
||||||
|
|
||||||
const { library: currentLibrary } = useCurrentLibrary();
|
const { library } = useLibraryContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsContainer>
|
<SettingsContainer>
|
||||||
|
@ -78,13 +78,13 @@ export default function LibrarySettings() {
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{libraries.data
|
{libraries.data
|
||||||
?.sort((a, b) => {
|
?.sort((a, b) => {
|
||||||
if (a.uuid === currentLibrary?.uuid) return -1;
|
if (a.uuid === library.uuid) return -1;
|
||||||
if (b.uuid === currentLibrary?.uuid) return 1;
|
if (b.uuid === library.uuid) return 1;
|
||||||
return 0;
|
return 0;
|
||||||
})
|
})
|
||||||
.map((library) => (
|
.map((library) => (
|
||||||
<LibraryListItem
|
<LibraryListItem
|
||||||
current={library.uuid === currentLibrary?.uuid}
|
current={library.uuid === library.uuid}
|
||||||
key={library.uuid}
|
key={library.uuid}
|
||||||
library={library}
|
library={library}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
import { lazy } from '@loadable/component';
|
import { lazy } from '@loadable/component';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
export function lazyEl(fn: Parameters<typeof lazy>[0]): ReactNode {
|
export function lazyEl(fn: Parameters<typeof lazy>[0]): ReactNode {
|
||||||
const Element = lazy(fn);
|
const Element = lazy(fn);
|
||||||
return <Element />;
|
return <Element />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useLibraryId() {
|
||||||
|
return useParams<{ libraryId?: string }>().libraryId;
|
||||||
|
}
|
||||||
|
|
|
@ -60,8 +60,7 @@ class DialogManager {
|
||||||
if (state.open === false) {
|
if (state.open === false) {
|
||||||
delete this.dialogs[id];
|
delete this.dialogs[id];
|
||||||
delete this.state[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) {
|
export function useDialog(props: UseDialogProps) {
|
||||||
|
const state = dialogManager.getState(props.id);
|
||||||
|
|
||||||
|
if (!state) throw new Error(`Dialog ${props.id} does not exist!`);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...props,
|
...props,
|
||||||
state: dialogManager.getState(props.id)
|
state
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,16 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
function twFactory(element: any) {
|
const twFactory =
|
||||||
return ([className, ..._]: TemplateStringsArray) => {
|
(element: any) =>
|
||||||
return restyle(element)(() => className);
|
([newClassNames, ..._]: TemplateStringsArray) =>
|
||||||
};
|
React.forwardRef(({ className, ...props }: any, ref) =>
|
||||||
}
|
React.createElement(element, {
|
||||||
|
...props,
|
||||||
|
className: clsx(newClassNames, className),
|
||||||
|
ref
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
type ClassnameFactory<T> = (s: TemplateStringsArray) => T;
|
type ClassnameFactory<T> = (s: TemplateStringsArray) => T;
|
||||||
|
|
||||||
|
@ -22,21 +27,3 @@ export const tw = new Proxy((() => {}) as unknown as TailwindFactory, {
|
||||||
get: (_, property: string) => twFactory(property),
|
get: (_, property: string) => twFactory(property),
|
||||||
apply: (_, __, [el]: [React.ReactElement]) => twFactory(el)
|
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