cleanups and cloud desktop design improvements

This commit is contained in:
ameer2468 2024-06-14 17:02:26 +03:00
parent 480f383ac9
commit eb5ac73990
11 changed files with 644 additions and 418 deletions

View file

@ -1,5 +1,7 @@
import { BottomSheetFlatList } from '@gorhom/bottom-sheet';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { forwardRef } from 'react';
import { ActivityIndicator, Text, View } from 'react-native';
import {
CloudLibrary,
useBridgeMutation,
@ -7,14 +9,13 @@ import {
useClientContext,
useRspcContext
} from '@sd/client';
import { forwardRef } from 'react';
import { Text, View } from 'react-native';
import { Modal, ModalRef } from '~/components/layout/Modal';
import { Button } from '~/components/primitive/Button';
import useForwardedRef from '~/hooks/useForwardedRef';
import { tw } from '~/lib/tailwind';
import { RootStackParamList } from '~/navigation';
import { currentLibraryStore } from '~/utils/nav';
import Empty from '../layout/Empty';
import Fade from '../layout/Fade';
@ -27,7 +28,7 @@ const ImportModalLibrary = forwardRef<ModalRef, unknown>((_, ref) => {
const cloudLibraries = useBridgeQuery(['cloud.library.list']);
const cloudLibrariesData = cloudLibraries.data?.filter(
(cloudLibrary) => !libraries.data?.find((l) => l.uuid === cloudLibrary.uuid)
)
);
return (
<Modal
@ -37,100 +38,102 @@ const ImportModalLibrary = forwardRef<ModalRef, unknown>((_, ref) => {
showCloseButton
>
<View style={tw`relative flex-1`}>
<Fade width={20} height="100%" fadeSides="top-bottom" orientation="vertical" color="bg-app-modal"
>
<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"
{cloudLibraries.isLoading ? (
<View style={tw`mt-10 items-center justify-center`}>
<ActivityIndicator size="small" />
</View>
) : (
<Fade
width={20}
height="100%"
fadeSides="top-bottom"
orientation="vertical"
color="bg-app-modal"
>
<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}
/>
)}
/>
}
keyExtractor={(item) => item.uuid}
showsVerticalScrollIndicator={false}
renderItem={({ item }) => (
<CloudLibraryCard
data={item}
navigation={navigation}
modalRef={modalRef}
/>
)}
/>
</Fade>
</View>
</Fade>
)}
</View>
</Modal>
);
});
interface Props {
data: CloudLibrary
modalRef: React.RefObject<ModalRef>
navigation: NavigationProp<RootStackParamList>
data: CloudLibrary;
modalRef: React.RefObject<ModalRef>;
navigation: NavigationProp<RootStackParamList>;
}
const CloudLibraryCard = ({data, modalRef, navigation}: Props) => {
const CloudLibraryCard = ({ data, modalRef, navigation }: Props) => {
const rspc = useRspcContext().queryClient;
const joinLibrary = useBridgeMutation(['cloud.library.join']);
return (
<View
key={data.uuid}
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>
<Button
size="sm"
variant="accent"
disabled={joinLibrary.isLoading}
onPress={async () => {
const library = await joinLibrary.mutateAsync(
data.uuid
);
return (
<View
key={data.uuid}
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>
<Button
size="sm"
variant="accent"
disabled={joinLibrary.isLoading}
onPress={async () => {
const library = await joinLibrary.mutateAsync(data.uuid);
rspc.setQueryData(
['library.list'],
(libraries: any) => {
rspc.setQueryData(['library.list'], (libraries: any) => {
// The invalidation system beat us to it
if (
(libraries || []).find(
(l: any) => l.uuid === library.uuid
)
)
if ((libraries || []).find((l: any) => l.uuid === library.uuid))
return libraries;
return [...(libraries || []), library];
}
);
});
currentLibraryStore.id = library.uuid;
currentLibraryStore.id = library.uuid;
navigation.navigate('Root', {
screen: 'Home',
params: {
screen: 'OverviewStack',
navigation.navigate('Root', {
screen: 'Home',
params: {
screen: 'Overview'
screen: 'OverviewStack',
params: {
screen: 'Overview'
}
}
}
});
});
modalRef.current?.dismiss();
}}
>
<Text style={tw`text-sm font-medium text-white`}>
{joinLibrary.isLoading &&
joinLibrary.variables === data.uuid
? 'Joining...'
: 'Join'}
</Text>
</Button>
</View>
)
}
modalRef.current?.dismiss();
}}
>
<Text style={tw`text-sm font-medium text-white`}>
{joinLibrary.isLoading && joinLibrary.variables === data.uuid
? 'Joining...'
: 'Join'}
</Text>
</Button>
</View>
);
};
export default ImportModalLibrary;

View file

@ -12,6 +12,7 @@ import PrivacySettingsScreen from '~/screens/settings/client/PrivacySettings';
import AboutScreen from '~/screens/settings/info/About';
import DebugScreen from '~/screens/settings/info/Debug';
import SupportScreen from '~/screens/settings/info/Support';
import CloudSettings from '~/screens/settings/library/CloudSettings/CloudSettings';
import EditLocationSettingsScreen from '~/screens/settings/library/EditLocationSettings';
import LibraryGeneralSettingsScreen from '~/screens/settings/library/LibraryGeneralSettings';
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 SettingsScreen from '~/screens/settings/Settings';
import CloudSettings from '~/screens/settings/library/CloudSettings';
import { TabScreenProps } from '../TabNavigator';
const Stack = createNativeStackNavigator<SettingsStackParamList>();

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -1,14 +1,14 @@
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 {
Procedures,
useLibraryMutation,
useLibraryQuery,
useLibrarySubscription
} 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 ScreenContainer from '~/components/layout/ScreenContainer';
import { Button } from '~/components/primitive/Button';
@ -21,7 +21,7 @@ const SyncSettingsScreen = ({ navigation }: SettingsStackScreenProps<'SyncSettin
const [startBackfill, setStart] = useState(false);
useLibrarySubscription(['library.actors'], { onData: setData});
useLibrarySubscription(['library.actors'], { onData: setData });
useEffect(() => {
if (startBackfill === true) {
@ -34,58 +34,70 @@ const SyncSettingsScreen = ({ navigation }: SettingsStackScreenProps<'SyncSettin
return (
<ScreenContainer scrollview={false} style={tw`gap-0 px-6`}>
{syncEnabled.data === false ? (
<Button
variant={'accent'}
onPress={() => setStart(true)}
>
<Text>Start Backfill Operations</Text>
</Button>
<View style={tw`flex-1 justify-center`}>
<Card style={tw`relative py-10`}>
<Button
variant={'accent'}
style={tw`mx-auto max-w-[82%]`}
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`}>
{Object.keys(data).map((key) => {
return (
<Card style={tw`w-[48%]`} key={key}>
<OnlineIndicator online={data[key] ?? false} />
<Text
key={key}
style={tw`mb-3 mt-1 flex-col items-center justify-center text-left text-xs text-white`}
>
{key}
</Text>
{data[key] ? (
<StopButton name={key} />
) : (
<StartButton name={key} />
)}
<OnlineIndicator online={data[key] ?? false} />
<Text
key={key}
style={tw`mb-3 mt-1 flex-col items-center justify-center text-left text-xs text-white`}
>
{key}
</Text>
{data[key] ? <StopButton name={key} /> : <StartButton name={key} />}
</Card>
)
})}
);
})}
</View>
)}
</ScreenContainer>
)}
</ScreenContainer>
);
}
};
export default SyncSettingsScreen;
function OnlineIndicator({ online }: { online: boolean }) {
const size = 6;
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`}>
{online ? (
<View style={tw`relative items-center justify-center`}>
<MotiView
from={{ scale: 0, opacity: 1 }}
animate={{ scale: 3, opacity: 0}}
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
style={tw`mb-1 h-6 w-6 items-center justify-center rounded-full border border-app-inputborder bg-app-input p-2`}
>
{online ? (
<View style={tw`relative items-center justify-center`}>
<MotiView
from={{ scale: 0, opacity: 1 }}
animate={{ scale: 3, opacity: 0 }}
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>
) : (
<Circle size={size} color={tw.color('red-400')} weight="fill" />
)}
</View>
)
);
}
function StartButton({ name }: { name: string }) {

View file

@ -1,8 +1,20 @@
import { auth, useLibraryContext, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { Button } from '@sd/ui';
import { CheckCircle, XCircle } from '@phosphor-icons/react';
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 { LoginButton } from '~/components/LoginButton';
import { useRouteTitle } from '~/hooks';
import { hardwareModelToIcon } from '~/util/hardware';
export const Component = () => {
useRouteTitle('Cloud');
@ -20,64 +32,41 @@ export const Component = () => {
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() {
const { library } = useLibraryContext();
const cloudLibrary = useLibraryQuery(['cloud.library.get'], { suspense: true, retry: false });
const createLibrary = useLibraryMutation(['cloud.library.create']);
const syncLibrary = useLibraryMutation(['cloud.library.sync']);
const thisInstance = cloudLibrary.data?.instances.find(
(instance) => instance.uuid === library.instance_id
);
const thisInstance = useMemo(() => {
if (!cloudLibrary.data) return undefined;
return cloudLibrary.data.instances.find(
(instance) => instance.uuid === library.instance_id
);
}, [cloudLibrary.data, library.instance_id]);
return (
<>
<Suspense
fallback={
<div className="flex size-full items-center justify-center">
<Loader />
</div>
}
>
{cloudLibrary.data ? (
<div className="flex flex-col items-start space-y-2">
<div>
<p>Library</p>
<p>Name: {cloudLibrary.data.name}</p>
</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 className="flex flex-col items-start gap-10">
<Library thisInstance={thisInstance} cloudLibrary={cloudLibrary.data} />
{thisInstance && <ThisInstance instance={thisInstance} />}
<Instances instances={cloudLibrary.data.instances} />
</div>
) : (
<div className="relative">
<div className="relative flex size-full flex-col items-center justify-center">
<AuthRequiredOverlay />
<Button
disabled={createLibrary.isLoading}
variant="accent"
onClick={() => {
createLibrary.mutateAsync(null);
}}
@ -88,6 +77,142 @@ function Authenticated() {
</Button>
</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>
);
};

View file

@ -8,6 +8,10 @@ export function hardwareModelToIcon(hardwareModel: HardwareModel) {
return 'Laptop';
case 'MacStudio':
return 'SilverBox';
case 'IPhone':
return 'Mobile';
case 'Android':
return 'Mobile-Android';
case 'MacMini':
return 'MiniSilverBox';
case 'Other':