mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-13 11:54:03 +00:00
cleanups and cloud desktop design improvements
This commit is contained in:
parent
480f383ac9
commit
eb5ac73990
|
@ -1,5 +1,7 @@
|
||||||
import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
|
import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||||
|
import { forwardRef } from 'react';
|
||||||
|
import { ActivityIndicator, Text, View } from 'react-native';
|
||||||
import {
|
import {
|
||||||
CloudLibrary,
|
CloudLibrary,
|
||||||
useBridgeMutation,
|
useBridgeMutation,
|
||||||
|
@ -7,14 +9,13 @@ import {
|
||||||
useClientContext,
|
useClientContext,
|
||||||
useRspcContext
|
useRspcContext
|
||||||
} from '@sd/client';
|
} from '@sd/client';
|
||||||
import { forwardRef } from 'react';
|
|
||||||
import { Text, View } from 'react-native';
|
|
||||||
import { Modal, ModalRef } from '~/components/layout/Modal';
|
import { Modal, ModalRef } from '~/components/layout/Modal';
|
||||||
import { Button } from '~/components/primitive/Button';
|
import { Button } from '~/components/primitive/Button';
|
||||||
import useForwardedRef from '~/hooks/useForwardedRef';
|
import useForwardedRef from '~/hooks/useForwardedRef';
|
||||||
import { tw } from '~/lib/tailwind';
|
import { tw } from '~/lib/tailwind';
|
||||||
import { RootStackParamList } from '~/navigation';
|
import { RootStackParamList } from '~/navigation';
|
||||||
import { currentLibraryStore } from '~/utils/nav';
|
import { currentLibraryStore } from '~/utils/nav';
|
||||||
|
|
||||||
import Empty from '../layout/Empty';
|
import Empty from '../layout/Empty';
|
||||||
import Fade from '../layout/Fade';
|
import Fade from '../layout/Fade';
|
||||||
|
|
||||||
|
@ -27,7 +28,7 @@ const ImportModalLibrary = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||||
const cloudLibraries = useBridgeQuery(['cloud.library.list']);
|
const cloudLibraries = useBridgeQuery(['cloud.library.list']);
|
||||||
const cloudLibrariesData = cloudLibraries.data?.filter(
|
const cloudLibrariesData = cloudLibraries.data?.filter(
|
||||||
(cloudLibrary) => !libraries.data?.find((l) => l.uuid === cloudLibrary.uuid)
|
(cloudLibrary) => !libraries.data?.find((l) => l.uuid === cloudLibrary.uuid)
|
||||||
)
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -37,100 +38,102 @@ const ImportModalLibrary = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||||
showCloseButton
|
showCloseButton
|
||||||
>
|
>
|
||||||
<View style={tw`relative flex-1`}>
|
<View style={tw`relative flex-1`}>
|
||||||
<Fade width={20} height="100%" fadeSides="top-bottom" orientation="vertical" color="bg-app-modal"
|
{cloudLibraries.isLoading ? (
|
||||||
>
|
<View style={tw`mt-10 items-center justify-center`}>
|
||||||
<BottomSheetFlatList
|
<ActivityIndicator size="small" />
|
||||||
data={cloudLibrariesData}
|
</View>
|
||||||
contentContainerStyle={tw`px-4 pb-6 pt-5`}
|
) : (
|
||||||
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
<Fade
|
||||||
ListEmptyComponent={
|
width={20}
|
||||||
<Empty
|
height="100%"
|
||||||
icon="Drive"
|
fadeSides="top-bottom"
|
||||||
style={tw`mt-2 border-0`}
|
orientation="vertical"
|
||||||
iconSize={46}
|
color="bg-app-modal"
|
||||||
description="You don't have any cloud libraries"
|
>
|
||||||
|
<BottomSheetFlatList
|
||||||
|
data={cloudLibrariesData}
|
||||||
|
contentContainerStyle={tw`px-4 pb-6 pt-5`}
|
||||||
|
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Empty
|
||||||
|
icon="Drive"
|
||||||
|
style={tw`mt-2 border-0`}
|
||||||
|
iconSize={46}
|
||||||
|
description="You don't have any cloud libraries"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
keyExtractor={(item) => item.uuid}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<CloudLibraryCard
|
||||||
|
data={item}
|
||||||
|
navigation={navigation}
|
||||||
|
modalRef={modalRef}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
</Fade>
|
||||||
keyExtractor={(item) => item.uuid}
|
)}
|
||||||
showsVerticalScrollIndicator={false}
|
</View>
|
||||||
renderItem={({ item }) => (
|
|
||||||
<CloudLibraryCard
|
|
||||||
data={item}
|
|
||||||
navigation={navigation}
|
|
||||||
modalRef={modalRef}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</Fade>
|
|
||||||
</View>
|
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: CloudLibrary
|
data: CloudLibrary;
|
||||||
modalRef: React.RefObject<ModalRef>
|
modalRef: React.RefObject<ModalRef>;
|
||||||
navigation: NavigationProp<RootStackParamList>
|
navigation: NavigationProp<RootStackParamList>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CloudLibraryCard = ({data, modalRef, navigation}: Props) => {
|
const CloudLibraryCard = ({ data, modalRef, navigation }: Props) => {
|
||||||
const rspc = useRspcContext().queryClient;
|
const rspc = useRspcContext().queryClient;
|
||||||
const joinLibrary = useBridgeMutation(['cloud.library.join']);
|
const joinLibrary = useBridgeMutation(['cloud.library.join']);
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
key={data.uuid}
|
key={data.uuid}
|
||||||
style={tw`flex flex-row items-center justify-between gap-2 rounded-md border border-app-box bg-app p-2`}
|
style={tw`flex flex-row items-center justify-between gap-2 rounded-md border border-app-box bg-app p-2`}
|
||||||
>
|
>
|
||||||
<Text numberOfLines={1} style={tw`max-w-[80%] text-sm font-bold text-ink`}>{data.name}</Text>
|
<Text numberOfLines={1} style={tw`max-w-[80%] text-sm font-bold text-ink`}>
|
||||||
<Button
|
{data.name}
|
||||||
size="sm"
|
</Text>
|
||||||
variant="accent"
|
<Button
|
||||||
disabled={joinLibrary.isLoading}
|
size="sm"
|
||||||
onPress={async () => {
|
variant="accent"
|
||||||
const library = await joinLibrary.mutateAsync(
|
disabled={joinLibrary.isLoading}
|
||||||
data.uuid
|
onPress={async () => {
|
||||||
);
|
const library = await joinLibrary.mutateAsync(data.uuid);
|
||||||
|
|
||||||
rspc.setQueryData(
|
rspc.setQueryData(['library.list'], (libraries: any) => {
|
||||||
['library.list'],
|
|
||||||
(libraries: any) => {
|
|
||||||
// The invalidation system beat us to it
|
// The invalidation system beat us to it
|
||||||
if (
|
if ((libraries || []).find((l: any) => l.uuid === library.uuid))
|
||||||
(libraries || []).find(
|
|
||||||
(l: any) => l.uuid === library.uuid
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return libraries;
|
return libraries;
|
||||||
|
|
||||||
return [...(libraries || []), library];
|
return [...(libraries || []), library];
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
currentLibraryStore.id = library.uuid;
|
currentLibraryStore.id = library.uuid;
|
||||||
|
|
||||||
navigation.navigate('Root', {
|
navigation.navigate('Root', {
|
||||||
screen: 'Home',
|
screen: 'Home',
|
||||||
params: {
|
|
||||||
screen: 'OverviewStack',
|
|
||||||
params: {
|
params: {
|
||||||
screen: 'Overview'
|
screen: 'OverviewStack',
|
||||||
|
params: {
|
||||||
|
screen: 'Overview'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
modalRef.current?.dismiss();
|
modalRef.current?.dismiss();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Text style={tw`text-sm font-medium text-white`}>
|
<Text style={tw`text-sm font-medium text-white`}>
|
||||||
{joinLibrary.isLoading &&
|
{joinLibrary.isLoading && joinLibrary.variables === data.uuid
|
||||||
joinLibrary.variables === data.uuid
|
? 'Joining...'
|
||||||
? 'Joining...'
|
: 'Join'}
|
||||||
: 'Join'}
|
</Text>
|
||||||
</Text>
|
</Button>
|
||||||
</Button>
|
</View>
|
||||||
</View>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export default ImportModalLibrary;
|
export default ImportModalLibrary;
|
||||||
|
|
|
@ -12,6 +12,7 @@ import PrivacySettingsScreen from '~/screens/settings/client/PrivacySettings';
|
||||||
import AboutScreen from '~/screens/settings/info/About';
|
import AboutScreen from '~/screens/settings/info/About';
|
||||||
import DebugScreen from '~/screens/settings/info/Debug';
|
import DebugScreen from '~/screens/settings/info/Debug';
|
||||||
import SupportScreen from '~/screens/settings/info/Support';
|
import SupportScreen from '~/screens/settings/info/Support';
|
||||||
|
import CloudSettings from '~/screens/settings/library/CloudSettings/CloudSettings';
|
||||||
import EditLocationSettingsScreen from '~/screens/settings/library/EditLocationSettings';
|
import EditLocationSettingsScreen from '~/screens/settings/library/EditLocationSettings';
|
||||||
import LibraryGeneralSettingsScreen from '~/screens/settings/library/LibraryGeneralSettings';
|
import LibraryGeneralSettingsScreen from '~/screens/settings/library/LibraryGeneralSettings';
|
||||||
import LocationSettingsScreen from '~/screens/settings/library/LocationSettings';
|
import LocationSettingsScreen from '~/screens/settings/library/LocationSettings';
|
||||||
|
@ -20,7 +21,6 @@ import SyncSettingsScreen from '~/screens/settings/library/SyncSettings';
|
||||||
import TagsSettingsScreen from '~/screens/settings/library/TagsSettings';
|
import TagsSettingsScreen from '~/screens/settings/library/TagsSettings';
|
||||||
import SettingsScreen from '~/screens/settings/Settings';
|
import SettingsScreen from '~/screens/settings/Settings';
|
||||||
|
|
||||||
import CloudSettings from '~/screens/settings/library/CloudSettings';
|
|
||||||
import { TabScreenProps } from '../TabNavigator';
|
import { TabScreenProps } from '../TabNavigator';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<SettingsStackParamList>();
|
const Stack = createNativeStackNavigator<SettingsStackParamList>();
|
||||||
|
|
|
@ -1,249 +0,0 @@
|
||||||
import { CloudInstance, useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
|
||||||
import { CheckCircle } from 'phosphor-react-native';
|
|
||||||
import { useMemo } from 'react';
|
|
||||||
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
|
|
||||||
import Card from '~/components/layout/Card';
|
|
||||||
import Empty from '~/components/layout/Empty';
|
|
||||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
|
||||||
import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
|
|
||||||
import { Button } from '~/components/primitive/Button';
|
|
||||||
import { Divider } from '~/components/primitive/Divider';
|
|
||||||
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
|
||||||
import { styled, tw, twStyle } from '~/lib/tailwind';
|
|
||||||
import { cancel, login, logout, useAuthStateSnapshot } from '~/stores/auth';
|
|
||||||
|
|
||||||
const InfoBox = styled(View, 'rounded-md border border-app bg-transparent p-2');
|
|
||||||
|
|
||||||
const CloudSettings = () => {
|
|
||||||
return (
|
|
||||||
<ScreenContainer scrollview={false} style={tw`gap-0 px-6 py-0`}>
|
|
||||||
<AuthSensitiveChild />
|
|
||||||
</ScreenContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AuthSensitiveChild = () => {
|
|
||||||
const authState = useAuthStateSnapshot();
|
|
||||||
if (authState.status === 'loggedIn') return <Authenticated />;
|
|
||||||
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn') return <Login />;
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Authenticated = () => {
|
|
||||||
const { library } = useLibraryContext();
|
|
||||||
const authState = useAuthStateSnapshot();
|
|
||||||
|
|
||||||
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { retry: false });
|
|
||||||
|
|
||||||
const createLibrary = useLibraryMutation(['cloud.library.create']);
|
|
||||||
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
|
|
||||||
|
|
||||||
const thisInstance = useMemo(() => cloudLibrary.data?.instances.find(
|
|
||||||
(instance) => instance.uuid === library.instance_id
|
|
||||||
), [cloudLibrary.data, library.instance_id]);
|
|
||||||
|
|
||||||
const cloudInstances = useMemo(() =>
|
|
||||||
cloudLibrary.data?.instances.filter(
|
|
||||||
(instance) => instance.uuid !== library.instance_id
|
|
||||||
), [cloudLibrary.data, library.instance_id]);
|
|
||||||
|
|
||||||
const isLibrarySynced = useMemo(() =>
|
|
||||||
cloudLibrary.data?.instances.some(
|
|
||||||
(instance) => instance.uuid === library.instance_id
|
|
||||||
),[cloudLibrary.data, library]);
|
|
||||||
|
|
||||||
if (cloudLibrary.isLoading) {
|
|
||||||
return (
|
|
||||||
<View style={tw`flex-1 items-center justify-center`}>
|
|
||||||
<ActivityIndicator size="small"/>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ScreenContainer tabHeight={false}>
|
|
||||||
{cloudLibrary.data ? (
|
|
||||||
<View style={tw`flex-col items-start gap-5`}>
|
|
||||||
<Card style={tw`w-full`}>
|
|
||||||
<View style={tw`flex-row items-center justify-between`}>
|
|
||||||
<Text style={tw`font-medium text-ink`}>Library</Text>
|
|
||||||
{authState.status === 'loggedIn' && (
|
|
||||||
<Button
|
|
||||||
variant="gray"
|
|
||||||
size="sm"
|
|
||||||
onPress={logout}
|
|
||||||
>
|
|
||||||
<Text style={tw`text-xs font-semibold text-ink`}>Logout</Text>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<Divider style={tw`mb-4 mt-2`}/>
|
|
||||||
<SettingsTitle style={tw`mb-2`}>Name</SettingsTitle>
|
|
||||||
<InfoBox>
|
|
||||||
<Text style={tw`text-ink`}>{cloudLibrary.data.name}</Text>
|
|
||||||
</InfoBox>
|
|
||||||
<Button
|
|
||||||
disabled={syncLibrary.isLoading}
|
|
||||||
variant={isLibrarySynced ? 'dashed' : 'accent'}
|
|
||||||
style={tw`mt-2 flex-row gap-1 py-2`}
|
|
||||||
onPress={() => syncLibrary.mutateAsync(null)}
|
|
||||||
>
|
|
||||||
{isLibrarySynced && <CheckCircle size={13} weight="fill" color={tw.color('green-500')}/>}
|
|
||||||
<Text style={tw`text-xs font-semibold text-ink`}>{
|
|
||||||
isLibrarySynced
|
|
||||||
? 'Library synced'
|
|
||||||
: 'Sync library'
|
|
||||||
}</Text>
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
{thisInstance && (
|
|
||||||
<Card style={tw`w-full gap-4`}>
|
|
||||||
<View>
|
|
||||||
<Text style={tw`mb-1 font-semibold text-ink`}>This Instance</Text>
|
|
||||||
<Divider />
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<SettingsTitle style={tw`mb-2 text-ink`}>Id</SettingsTitle>
|
|
||||||
<InfoBox>
|
|
||||||
<Text style={tw`text-ink-dull`}>{thisInstance.id}</Text>
|
|
||||||
</InfoBox>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<SettingsTitle style={tw`mb-2`}>UUID</SettingsTitle>
|
|
||||||
<InfoBox>
|
|
||||||
<Text style={tw`text-ink-dull`}>{thisInstance.uuid}</Text>
|
|
||||||
</InfoBox>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<SettingsTitle style={tw`mb-2`}>Public Key</SettingsTitle>
|
|
||||||
<InfoBox>
|
|
||||||
<Text numberOfLines={1} style={tw`text-ink-dull`}>
|
|
||||||
{thisInstance.identity}
|
|
||||||
</Text>
|
|
||||||
</InfoBox>
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
<Card style={tw`w-full`}>
|
|
||||||
<View style={tw`flex-row items-center gap-2`}>
|
|
||||||
<View
|
|
||||||
style={tw`self-start rounded border border-app-lightborder bg-app-highlight px-1.5 py-[2px]`}
|
|
||||||
>
|
|
||||||
<Text style={tw`text-xs font-semibold text-ink`}>
|
|
||||||
{cloudInstances?.length}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Text style={tw`font-semibold text-ink`}>Instances</Text>
|
|
||||||
</View>
|
|
||||||
<Divider style={tw`mb-4 mt-2`} />
|
|
||||||
<VirtualizedListWrapper
|
|
||||||
scrollEnabled={false}
|
|
||||||
contentContainerStyle={tw`flex-1`}
|
|
||||||
horizontal
|
|
||||||
>
|
|
||||||
<FlatList
|
|
||||||
data={cloudInstances}
|
|
||||||
scrollEnabled={false}
|
|
||||||
ListEmptyComponent={<Empty
|
|
||||||
textStyle={tw`my-0`}
|
|
||||||
description='No instances found'
|
|
||||||
/>}
|
|
||||||
contentContainerStyle={twStyle(cloudInstances?.length === 0 && 'flex-row')}
|
|
||||||
showsHorizontalScrollIndicator={false}
|
|
||||||
ItemSeparatorComponent={() => <View style={tw`h-2`}/>}
|
|
||||||
renderItem={({ item }) => <Instance data={item} length={cloudInstances?.length ?? 0} />}
|
|
||||||
keyExtractor={(item) => item.id}
|
|
||||||
numColumns={(cloudInstances?.length ?? 0) > 1 ? 2 : 1}
|
|
||||||
{...(cloudInstances?.length ?? 0) > 1 ? {columnWrapperStyle: tw`w-full justify-between`} : {}}
|
|
||||||
/>
|
|
||||||
</VirtualizedListWrapper>
|
|
||||||
</Card>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<Card style={tw`relative py-10`}>
|
|
||||||
<Button
|
|
||||||
style={tw`mx-auto max-w-[82%]`}
|
|
||||||
disabled={createLibrary.isLoading}
|
|
||||||
onPress={async () => await createLibrary.mutateAsync(null)}
|
|
||||||
>
|
|
||||||
{createLibrary.isLoading ? (
|
|
||||||
<Text style={tw`text-ink`}>
|
|
||||||
Connecting library to Spacedrive Cloud...
|
|
||||||
</Text>
|
|
||||||
) : (
|
|
||||||
<Text style={tw`font-medium text-ink`}>Connect library to Spacedrive Cloud</Text>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</ScreenContainer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
data: CloudInstance;
|
|
||||||
length: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Instance = ({data, length}: Props) => {
|
|
||||||
return (
|
|
||||||
<InfoBox style={twStyle(length > 1 ? 'w-[49%]' : 'w-full', 'gap-4')}>
|
|
||||||
<View>
|
|
||||||
<SettingsTitle style={tw`mb-2`}>Id</SettingsTitle>
|
|
||||||
<InfoBox>
|
|
||||||
<Text numberOfLines={1} style={tw`text-ink-dull`}>{data.id}</Text>
|
|
||||||
</InfoBox>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<SettingsTitle style={tw`mb-2`}>UUID</SettingsTitle>
|
|
||||||
<InfoBox>
|
|
||||||
<Text numberOfLines={1} style={tw`text-ink-dull`}>{data.uuid}</Text>
|
|
||||||
</InfoBox>
|
|
||||||
</View>
|
|
||||||
<View>
|
|
||||||
<SettingsTitle style={tw`mb-2`}>Public Key</SettingsTitle>
|
|
||||||
<InfoBox>
|
|
||||||
<Text numberOfLines={1} style={tw`text-ink-dull`}>{data.identity}</Text>
|
|
||||||
</InfoBox>
|
|
||||||
</View>
|
|
||||||
</InfoBox>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const Login = () => {
|
|
||||||
const authState = useAuthStateSnapshot();
|
|
||||||
const buttonText = {
|
|
||||||
notLoggedIn: 'Login',
|
|
||||||
loggingIn: 'Cancel',
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<View style={tw`flex-1 flex-col items-center justify-center gap-2`}>
|
|
||||||
<Card style={tw`w-full items-center justify-center p-6`}>
|
|
||||||
<Text style={tw`mb-4 max-w-[60%] text-center text-ink`}>
|
|
||||||
To access cloud related features, please login
|
|
||||||
</Text>
|
|
||||||
{(authState.status === 'notLoggedIn' || authState.status === 'loggingIn') && (
|
|
||||||
<Button
|
|
||||||
variant="accent"
|
|
||||||
style={tw`mx-auto max-w-[50%]`}
|
|
||||||
onPress={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (authState.status === 'loggingIn') {
|
|
||||||
await cancel();
|
|
||||||
} else {
|
|
||||||
await login();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={tw`font-medium text-ink`}>
|
|
||||||
{buttonText[authState.status]}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CloudSettings;
|
|
|
@ -0,0 +1,127 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { ActivityIndicator, FlatList, Text, View } from 'react-native';
|
||||||
|
import { useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import Empty from '~/components/layout/Empty';
|
||||||
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
|
import VirtualizedListWrapper from '~/components/layout/VirtualizedListWrapper';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
import { styled, tw, twStyle } from '~/lib/tailwind';
|
||||||
|
import { useAuthStateSnapshot } from '~/stores/auth';
|
||||||
|
|
||||||
|
import Instance from './Instance';
|
||||||
|
import Library from './Library';
|
||||||
|
import Login from './Login';
|
||||||
|
import ThisInstance from './ThisInstance';
|
||||||
|
|
||||||
|
export const InfoBox = styled(View, 'rounded-md border border-app bg-transparent p-2');
|
||||||
|
|
||||||
|
const CloudSettings = () => {
|
||||||
|
return (
|
||||||
|
<ScreenContainer scrollview={false} style={tw`gap-0 px-6 py-0`}>
|
||||||
|
<AuthSensitiveChild />
|
||||||
|
</ScreenContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const AuthSensitiveChild = () => {
|
||||||
|
const authState = useAuthStateSnapshot();
|
||||||
|
if (authState.status === 'loggedIn') return <Authenticated />;
|
||||||
|
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn') return <Login />;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Authenticated = () => {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { retry: false });
|
||||||
|
const createLibrary = useLibraryMutation(['cloud.library.create']);
|
||||||
|
|
||||||
|
const cloudInstances = useMemo(
|
||||||
|
() =>
|
||||||
|
cloudLibrary.data?.instances.filter(
|
||||||
|
(instance) => instance.uuid !== library.instance_id
|
||||||
|
),
|
||||||
|
[cloudLibrary.data, library.instance_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cloudLibrary.isLoading) {
|
||||||
|
return (
|
||||||
|
<View style={tw`flex-1 items-center justify-center`}>
|
||||||
|
<ActivityIndicator size="small" />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenContainer tabHeight={false}>
|
||||||
|
{cloudLibrary.data ? (
|
||||||
|
<View style={tw`flex-col items-start gap-5`}>
|
||||||
|
<Library cloudLibrary={cloudLibrary.data} />
|
||||||
|
<ThisInstance cloudLibrary={cloudLibrary.data} />
|
||||||
|
|
||||||
|
<Card style={tw`w-full`}>
|
||||||
|
<View style={tw`flex-row items-center gap-2`}>
|
||||||
|
<View
|
||||||
|
style={tw`self-start rounded border border-app-lightborder bg-app-highlight px-1.5 py-[2px]`}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-xs font-semibold text-ink`}>
|
||||||
|
{cloudInstances?.length}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={tw`font-semibold text-ink`}>Instances</Text>
|
||||||
|
</View>
|
||||||
|
<Divider style={tw`mb-4 mt-2`} />
|
||||||
|
<VirtualizedListWrapper
|
||||||
|
scrollEnabled={false}
|
||||||
|
contentContainerStyle={tw`flex-1`}
|
||||||
|
horizontal
|
||||||
|
>
|
||||||
|
<FlatList
|
||||||
|
data={cloudInstances}
|
||||||
|
scrollEnabled={false}
|
||||||
|
ListEmptyComponent={
|
||||||
|
<Empty textStyle={tw`my-0`} description="No instances found" />
|
||||||
|
}
|
||||||
|
contentContainerStyle={twStyle(
|
||||||
|
cloudInstances?.length === 0 && 'flex-row'
|
||||||
|
)}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
ItemSeparatorComponent={() => <View style={tw`h-2`} />}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<Instance data={item} length={cloudInstances?.length ?? 0} />
|
||||||
|
)}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
numColumns={(cloudInstances?.length ?? 0) > 1 ? 2 : 1}
|
||||||
|
{...((cloudInstances?.length ?? 0) > 1
|
||||||
|
? { columnWrapperStyle: tw`w-full justify-between` }
|
||||||
|
: {})}
|
||||||
|
/>
|
||||||
|
</VirtualizedListWrapper>
|
||||||
|
</Card>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Card style={tw`relative py-10`}>
|
||||||
|
<Button
|
||||||
|
style={tw`mx-auto max-w-[82%]`}
|
||||||
|
disabled={createLibrary.isLoading}
|
||||||
|
onPress={async () => await createLibrary.mutateAsync(null)}
|
||||||
|
>
|
||||||
|
{createLibrary.isLoading ? (
|
||||||
|
<Text style={tw`text-ink`}>
|
||||||
|
Connecting library to Spacedrive Cloud...
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text style={tw`font-medium text-ink`}>
|
||||||
|
Connect library to Spacedrive Cloud
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</ScreenContainer>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CloudSettings;
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import { CloudInstance } from '@sd/client';
|
||||||
|
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||||
|
import { tw, twStyle } from '~/lib/tailwind';
|
||||||
|
|
||||||
|
import { InfoBox } from './CloudSettings';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: CloudInstance;
|
||||||
|
length: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Instance = ({ data, length }: Props) => {
|
||||||
|
return (
|
||||||
|
<InfoBox style={twStyle(length > 1 ? 'w-[49%]' : 'w-full', 'gap-4')}>
|
||||||
|
<View>
|
||||||
|
<SettingsTitle style={tw`mb-2`}>Id</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text numberOfLines={1} style={tw`text-ink-dull`}>
|
||||||
|
{data.id}
|
||||||
|
</Text>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<SettingsTitle style={tw`mb-2`}>UUID</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text numberOfLines={1} style={tw`text-ink-dull`}>
|
||||||
|
{data.uuid}
|
||||||
|
</Text>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<SettingsTitle style={tw`mb-2`}>Public Key</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text numberOfLines={1} style={tw`text-ink-dull`}>
|
||||||
|
{data.identity}
|
||||||
|
</Text>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
</InfoBox>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Instance;
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { CheckCircle, XCircle } from 'phosphor-react-native';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import { CloudLibrary, useLibraryContext, useLibraryMutation } from '@sd/client';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
import { logout, useAuthStateSnapshot } from '~/stores/auth';
|
||||||
|
|
||||||
|
import { InfoBox } from './CloudSettings';
|
||||||
|
|
||||||
|
interface LibraryProps {
|
||||||
|
cloudLibrary?: CloudLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Library = ({ cloudLibrary }: LibraryProps) => {
|
||||||
|
const authState = useAuthStateSnapshot();
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
|
||||||
|
const thisInstance = useMemo(
|
||||||
|
() => cloudLibrary?.instances.find((instance) => instance.uuid === library.instance_id),
|
||||||
|
[cloudLibrary, library.instance_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={tw`w-full`}>
|
||||||
|
<View style={tw`flex-row items-center justify-between`}>
|
||||||
|
<Text style={tw`font-medium text-ink`}>Library</Text>
|
||||||
|
{authState.status === 'loggedIn' && (
|
||||||
|
<Button variant="gray" size="sm" onPress={logout}>
|
||||||
|
<Text style={tw`text-xs font-semibold text-ink`}>Logout</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Divider style={tw`mb-4 mt-2`} />
|
||||||
|
<SettingsTitle style={tw`mb-2`}>Name</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text style={tw`text-ink`}>{cloudLibrary?.name}</Text>
|
||||||
|
</InfoBox>
|
||||||
|
<Button
|
||||||
|
disabled={syncLibrary.isLoading || thisInstance !== undefined}
|
||||||
|
variant={thisInstance ? 'dashed' : 'accent'}
|
||||||
|
onPress={() => syncLibrary.mutate(null)}
|
||||||
|
style={tw`mt-2 flex-row gap-1 py-2`}
|
||||||
|
>
|
||||||
|
{thisInstance ? (
|
||||||
|
<CheckCircle size={13} weight="fill" color={tw.color('green-500')} />
|
||||||
|
) : (
|
||||||
|
<XCircle
|
||||||
|
style={tw`rounded-full`}
|
||||||
|
size={13}
|
||||||
|
weight="fill"
|
||||||
|
color={tw.color('red-500')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text style={tw`text-xs font-semibold text-ink`}>
|
||||||
|
{thisInstance !== undefined ? 'Library synced' : 'Library not synced'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Library;
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
import { cancel, login, useAuthStateSnapshot } from '~/stores/auth';
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const authState = useAuthStateSnapshot();
|
||||||
|
const buttonText = {
|
||||||
|
notLoggedIn: 'Login',
|
||||||
|
loggingIn: 'Cancel'
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<View style={tw`flex-1 flex-col items-center justify-center gap-2`}>
|
||||||
|
<Card style={tw`w-full items-center justify-center p-6`}>
|
||||||
|
<Text style={tw`mb-4 max-w-[60%] text-center text-ink`}>
|
||||||
|
To access cloud related features, please login
|
||||||
|
</Text>
|
||||||
|
{(authState.status === 'notLoggedIn' || authState.status === 'loggingIn') && (
|
||||||
|
<Button
|
||||||
|
variant="accent"
|
||||||
|
style={tw`mx-auto max-w-[50%]`}
|
||||||
|
onPress={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (authState.status === 'loggingIn') {
|
||||||
|
await cancel();
|
||||||
|
} else {
|
||||||
|
await login();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`font-medium text-ink`}>{buttonText[authState.status]}</Text>
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
|
@ -0,0 +1,54 @@
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
|
import { CloudLibrary, useLibraryContext } from '@sd/client';
|
||||||
|
import Card from '~/components/layout/Card';
|
||||||
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
|
||||||
|
import { InfoBox } from './CloudSettings';
|
||||||
|
|
||||||
|
interface ThisInstanceProps {
|
||||||
|
cloudLibrary?: CloudLibrary;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThisInstance = ({ cloudLibrary }: ThisInstanceProps) => {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const thisInstance = useMemo(
|
||||||
|
() => cloudLibrary?.instances.find((instance) => instance.uuid === library.instance_id),
|
||||||
|
[cloudLibrary, library.instance_id]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!thisInstance) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card style={tw`w-full gap-4`}>
|
||||||
|
<View>
|
||||||
|
<Text style={tw`mb-1 font-semibold text-ink`}>This Instance</Text>
|
||||||
|
<Divider />
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<SettingsTitle style={tw`mb-2 text-ink`}>Id</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text style={tw`text-ink-dull`}>{thisInstance.id}</Text>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<SettingsTitle style={tw`mb-2`}>UUID</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text style={tw`text-ink-dull`}>{thisInstance.uuid}</Text>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
<View>
|
||||||
|
<SettingsTitle style={tw`mb-2`}>Public Key</SettingsTitle>
|
||||||
|
<InfoBox>
|
||||||
|
<Text numberOfLines={1} style={tw`text-ink-dull`}>
|
||||||
|
{thisInstance.identity}
|
||||||
|
</Text>
|
||||||
|
</InfoBox>
|
||||||
|
</View>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ThisInstance;
|
|
@ -1,14 +1,14 @@
|
||||||
import { inferSubscriptionResult } from '@oscartbeaumont-sd/rspc-client';
|
import { inferSubscriptionResult } from '@oscartbeaumont-sd/rspc-client';
|
||||||
|
import { MotiView } from 'moti';
|
||||||
|
import { Circle } from 'phosphor-react-native';
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Text, View } from 'react-native';
|
||||||
import {
|
import {
|
||||||
Procedures,
|
Procedures,
|
||||||
useLibraryMutation,
|
useLibraryMutation,
|
||||||
useLibraryQuery,
|
useLibraryQuery,
|
||||||
useLibrarySubscription
|
useLibrarySubscription
|
||||||
} from '@sd/client';
|
} from '@sd/client';
|
||||||
import { MotiView } from 'moti';
|
|
||||||
import { Circle } from 'phosphor-react-native';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { Text, View } from 'react-native';
|
|
||||||
import Card from '~/components/layout/Card';
|
import Card from '~/components/layout/Card';
|
||||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
import { Button } from '~/components/primitive/Button';
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
@ -21,7 +21,7 @@ const SyncSettingsScreen = ({ navigation }: SettingsStackScreenProps<'SyncSettin
|
||||||
|
|
||||||
const [startBackfill, setStart] = useState(false);
|
const [startBackfill, setStart] = useState(false);
|
||||||
|
|
||||||
useLibrarySubscription(['library.actors'], { onData: setData});
|
useLibrarySubscription(['library.actors'], { onData: setData });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (startBackfill === true) {
|
if (startBackfill === true) {
|
||||||
|
@ -34,58 +34,70 @@ const SyncSettingsScreen = ({ navigation }: SettingsStackScreenProps<'SyncSettin
|
||||||
return (
|
return (
|
||||||
<ScreenContainer scrollview={false} style={tw`gap-0 px-6`}>
|
<ScreenContainer scrollview={false} style={tw`gap-0 px-6`}>
|
||||||
{syncEnabled.data === false ? (
|
{syncEnabled.data === false ? (
|
||||||
<Button
|
<View style={tw`flex-1 justify-center`}>
|
||||||
variant={'accent'}
|
<Card style={tw`relative py-10`}>
|
||||||
onPress={() => setStart(true)}
|
<Button
|
||||||
>
|
variant={'accent'}
|
||||||
<Text>Start Backfill Operations</Text>
|
style={tw`mx-auto max-w-[82%]`}
|
||||||
</Button>
|
onPress={() => setStart(true)}
|
||||||
|
>
|
||||||
|
<Text style={tw`font-medium text-white`}>
|
||||||
|
Start Backfill Operations
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={tw`flex-row flex-wrap gap-2`}>
|
<View style={tw`flex-row flex-wrap gap-2`}>
|
||||||
{Object.keys(data).map((key) => {
|
{Object.keys(data).map((key) => {
|
||||||
return (
|
return (
|
||||||
<Card style={tw`w-[48%]`} key={key}>
|
<Card style={tw`w-[48%]`} key={key}>
|
||||||
<OnlineIndicator online={data[key] ?? false} />
|
<OnlineIndicator online={data[key] ?? false} />
|
||||||
<Text
|
<Text
|
||||||
key={key}
|
key={key}
|
||||||
style={tw`mb-3 mt-1 flex-col items-center justify-center text-left text-xs text-white`}
|
style={tw`mb-3 mt-1 flex-col items-center justify-center text-left text-xs text-white`}
|
||||||
>
|
>
|
||||||
{key}
|
{key}
|
||||||
</Text>
|
</Text>
|
||||||
{data[key] ? (
|
{data[key] ? <StopButton name={key} /> : <StartButton name={key} />}
|
||||||
<StopButton name={key} />
|
|
||||||
) : (
|
|
||||||
<StartButton name={key} />
|
|
||||||
)}
|
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
</ScreenContainer>
|
</ScreenContainer>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default SyncSettingsScreen;
|
export default SyncSettingsScreen;
|
||||||
|
|
||||||
function OnlineIndicator({ online }: { online: boolean }) {
|
function OnlineIndicator({ online }: { online: boolean }) {
|
||||||
const size = 6;
|
const size = 6;
|
||||||
return (
|
return (
|
||||||
<View style={tw`mb-1 h-6 w-6 items-center justify-center rounded-full border border-app-inputborder bg-app-input p-2`}>
|
<View
|
||||||
{online ? (
|
style={tw`mb-1 h-6 w-6 items-center justify-center rounded-full border border-app-inputborder bg-app-input p-2`}
|
||||||
<View style={tw`relative items-center justify-center`}>
|
>
|
||||||
<MotiView
|
{online ? (
|
||||||
from={{ scale: 0, opacity: 1 }}
|
<View style={tw`relative items-center justify-center`}>
|
||||||
animate={{ scale: 3, opacity: 0}}
|
<MotiView
|
||||||
transition={{ type: 'timing', duration: 1500, loop: true, repeatReverse: false, delay: 1000}}
|
from={{ scale: 0, opacity: 1 }}
|
||||||
style={tw`absolute z-10 h-2 w-2 items-center justify-center rounded-full bg-green-500`} />
|
animate={{ scale: 3, opacity: 0 }}
|
||||||
<View style={tw`h-2 w-2 rounded-full bg-green-500`} />
|
transition={{
|
||||||
|
type: 'timing',
|
||||||
|
duration: 1500,
|
||||||
|
loop: true,
|
||||||
|
repeatReverse: false,
|
||||||
|
delay: 1000
|
||||||
|
}}
|
||||||
|
style={tw`absolute z-10 h-2 w-2 items-center justify-center rounded-full bg-green-500`}
|
||||||
|
/>
|
||||||
|
<View style={tw`h-2 w-2 rounded-full bg-green-500`} />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<Circle size={size} color={tw.color('red-400')} weight="fill" />
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
) : (
|
);
|
||||||
<Circle size={size} color={tw.color('red-400')} weight="fill" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function StartButton({ name }: { name: string }) {
|
function StartButton({ name }: { name: string }) {
|
||||||
|
|
|
@ -1,8 +1,20 @@
|
||||||
import { auth, useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client';
|
import { CheckCircle, XCircle } from '@phosphor-icons/react';
|
||||||
import { Button } from '@sd/ui';
|
import { Suspense, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
auth,
|
||||||
|
CloudInstance,
|
||||||
|
CloudLibrary,
|
||||||
|
HardwareModel,
|
||||||
|
useLibraryContext,
|
||||||
|
useLibraryMutation,
|
||||||
|
useLibraryQuery
|
||||||
|
} from '@sd/client';
|
||||||
|
import { Button, Card, Loader, tw } from '@sd/ui';
|
||||||
|
import { Icon } from '~/components';
|
||||||
import { AuthRequiredOverlay } from '~/components/AuthRequiredOverlay';
|
import { AuthRequiredOverlay } from '~/components/AuthRequiredOverlay';
|
||||||
import { LoginButton } from '~/components/LoginButton';
|
import { LoginButton } from '~/components/LoginButton';
|
||||||
import { useRouteTitle } from '~/hooks';
|
import { useRouteTitle } from '~/hooks';
|
||||||
|
import { hardwareModelToIcon } from '~/util/hardware';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
useRouteTitle('Cloud');
|
useRouteTitle('Cloud');
|
||||||
|
@ -20,64 +32,41 @@ export const Component = () => {
|
||||||
return <div className="flex size-full flex-col items-start p-4">{authSensitiveChild()}</div>;
|
return <div className="flex size-full flex-col items-start p-4">{authSensitiveChild()}</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DataBox = tw.div`max-w-[300px] rounded-md border border-app-line/50 bg-app-lightBox/20 p-2`;
|
||||||
|
const Count = tw.div`min-w-[20px] flex h-[20px] px-1 items-center justify-center rounded-full border border-app-button/40 text-[9px]`;
|
||||||
|
|
||||||
function Authenticated() {
|
function Authenticated() {
|
||||||
const { library } = useLibraryContext();
|
const { library } = useLibraryContext();
|
||||||
|
|
||||||
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { suspense: true, retry: false });
|
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { suspense: true, retry: false });
|
||||||
|
|
||||||
const createLibrary = useLibraryMutation(['cloud.library.create']);
|
const createLibrary = useLibraryMutation(['cloud.library.create']);
|
||||||
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
|
|
||||||
|
|
||||||
const thisInstance = cloudLibrary.data?.instances.find(
|
const thisInstance = useMemo(() => {
|
||||||
(instance) => instance.uuid === library.instance_id
|
if (!cloudLibrary.data) return undefined;
|
||||||
);
|
return cloudLibrary.data.instances.find(
|
||||||
|
(instance) => instance.uuid === library.instance_id
|
||||||
|
);
|
||||||
|
}, [cloudLibrary.data, library.instance_id]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Suspense
|
||||||
|
fallback={
|
||||||
|
<div className="flex size-full items-center justify-center">
|
||||||
|
<Loader />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
{cloudLibrary.data ? (
|
{cloudLibrary.data ? (
|
||||||
<div className="flex flex-col items-start space-y-2">
|
<div className="flex flex-col items-start gap-10">
|
||||||
<div>
|
<Library thisInstance={thisInstance} cloudLibrary={cloudLibrary.data} />
|
||||||
<p>Library</p>
|
{thisInstance && <ThisInstance instance={thisInstance} />}
|
||||||
<p>Name: {cloudLibrary.data.name}</p>
|
<Instances instances={cloudLibrary.data.instances} />
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
disabled={syncLibrary.isLoading}
|
|
||||||
onClick={() => {
|
|
||||||
syncLibrary.mutateAsync(null);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Sync Library
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{thisInstance && (
|
|
||||||
<div>
|
|
||||||
<p>This Instance</p>
|
|
||||||
<p>Id: {thisInstance.id}</p>
|
|
||||||
<p>UUID: {thisInstance.uuid}</p>
|
|
||||||
<p>Public Key: {thisInstance.identity}</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div>
|
|
||||||
<p>Instances</p>
|
|
||||||
<ul className="space-y-4 pl-4">
|
|
||||||
{cloudLibrary.data.instances
|
|
||||||
.filter((instance) => instance.uuid !== library.instance_id)
|
|
||||||
.map((instance) => (
|
|
||||||
<li key={instance.id}>
|
|
||||||
<p>Id: {instance.id}</p>
|
|
||||||
<p>UUID: {instance.uuid}</p>
|
|
||||||
<p>Public Key: {instance.identity}</p>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="relative">
|
<div className="relative flex size-full flex-col items-center justify-center">
|
||||||
<AuthRequiredOverlay />
|
<AuthRequiredOverlay />
|
||||||
<Button
|
<Button
|
||||||
disabled={createLibrary.isLoading}
|
disabled={createLibrary.isLoading}
|
||||||
|
variant="accent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createLibrary.mutateAsync(null);
|
createLibrary.mutateAsync(null);
|
||||||
}}
|
}}
|
||||||
|
@ -88,6 +77,142 @@ function Authenticated() {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const Instances = ({ instances }: { instances: CloudInstance[] }) => {
|
||||||
|
const { library } = useLibraryContext();
|
||||||
|
const filteredInstances = instances.filter((instance) => instance.uuid !== library.instance_id);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex flex-row items-center gap-3">
|
||||||
|
<p className="text-medium font-bold">Instances</p>
|
||||||
|
<Count>{filteredInstances.length}</Count>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row flex-wrap gap-2">
|
||||||
|
{filteredInstances.map((instance) => (
|
||||||
|
<Card
|
||||||
|
key={instance.id}
|
||||||
|
className="flex-row items-center gap-8 bg-app-box/50 !p-5"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
hardwareModelToIcon(
|
||||||
|
instance.metadata.device_model as HardwareModel
|
||||||
|
) as any
|
||||||
|
}
|
||||||
|
size={70}
|
||||||
|
/>
|
||||||
|
<p className="max-w-[160px] truncate text-xs font-medium">
|
||||||
|
{instance.metadata.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Id:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">{instance.id}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
UUID:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">
|
||||||
|
{instance.uuid}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Public Key:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">
|
||||||
|
{instance.identity}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface LibraryProps {
|
||||||
|
cloudLibrary: CloudLibrary;
|
||||||
|
thisInstance: CloudInstance | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Library = ({ thisInstance, cloudLibrary }: LibraryProps) => {
|
||||||
|
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-medium font-bold">Library</p>
|
||||||
|
<Card className="flex-row items-center gap-10">
|
||||||
|
<p className="font-medium">
|
||||||
|
Name: <span className="font-normal text-ink-dull">{cloudLibrary.name}</span>
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
disabled={syncLibrary.isLoading || thisInstance !== undefined}
|
||||||
|
variant={thisInstance === undefined ? 'accent' : 'gray'}
|
||||||
|
className="flex flex-row items-center gap-1 !text-ink"
|
||||||
|
onClick={() => syncLibrary.mutateAsync(null)}
|
||||||
|
>
|
||||||
|
{thisInstance === undefined ? (
|
||||||
|
<XCircle weight="fill" size={15} className="text-red-400" />
|
||||||
|
) : (
|
||||||
|
<CheckCircle weight="fill" size={15} className="text-green-400" />
|
||||||
|
)}
|
||||||
|
{thisInstance === undefined ? 'Sync Library' : 'Library is synced'}
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ThisInstanceProps {
|
||||||
|
instance: CloudInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThisInstance = ({ instance }: ThisInstanceProps) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<p className="text-medium font-bold">This Instance</p>
|
||||||
|
<Card className="flex-row items-center gap-8 bg-app-box/50 !p-5">
|
||||||
|
<div className="flex flex-col items-center gap-2">
|
||||||
|
<Icon
|
||||||
|
name={
|
||||||
|
hardwareModelToIcon(
|
||||||
|
instance.metadata.device_model as HardwareModel
|
||||||
|
) as any
|
||||||
|
}
|
||||||
|
size={70}
|
||||||
|
/>
|
||||||
|
<p className="max-w-[160px] truncate text-xs font-medium">
|
||||||
|
{instance.metadata.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1.5">
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Id: <span className="font-normal text-ink-dull">{instance.id}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
UUID: <span className="font-normal text-ink-dull">{instance.uuid}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="truncate text-xs font-medium">
|
||||||
|
Public Key:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">{instance.identity}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -8,6 +8,10 @@ export function hardwareModelToIcon(hardwareModel: HardwareModel) {
|
||||||
return 'Laptop';
|
return 'Laptop';
|
||||||
case 'MacStudio':
|
case 'MacStudio':
|
||||||
return 'SilverBox';
|
return 'SilverBox';
|
||||||
|
case 'IPhone':
|
||||||
|
return 'Mobile';
|
||||||
|
case 'Android':
|
||||||
|
return 'Mobile-Android';
|
||||||
case 'MacMini':
|
case 'MacMini':
|
||||||
return 'MiniSilverBox';
|
return 'MiniSilverBox';
|
||||||
case 'Other':
|
case 'Other':
|
||||||
|
|
Loading…
Reference in a new issue