mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-06-30 12:33:31 +00:00
[MOB-106] Cloud Sync for Mobile (#2549)
* wip + working backfill * Finished BackfillWaiting page + initial auth setup Also, setting up the cloud sync page. * mobile auth * Import Cloud Library Currently, you can import a cloud library, however it seems that the data, such as locations, is not transferring correctly. * Working Mobile Cloud Sync Cloud Sync works for Mobile, and the mobile app can sync files from a cloud library, and other clients can access the data from the phone's cloud library. * Cloud Sync Done * Formatting * Fix new library button * New device type passing to auth * Improve design of cloud settings and import modal * ui adjustments and code cleanup * Update styling if there's only 1 instance * code cleanup, design tweaks * empty state & simple indicator animation * lint * loading indicator and cleanup * Fix to Sync Subscription * Update Cargo.lock * Async logout for debug * tweaks * Update SettingsStack.tsx * cleanups and cloud desktop design improvements * more cleanups and ui improvements * ts * i18n * Cloud Sync Docs * styling * Delete library-sync.mdx Moving docs to a separate branch --------- Co-authored-by: ameer2468 <33054370+ameer2468@users.noreply.github.com>
This commit is contained in:
parent
e3202b3d35
commit
18235c6f09
|
@ -91,7 +91,7 @@ If you encounter any issues, ensure that you are using the following versions of
|
||||||
|
|
||||||
- Rust version: **1.78**
|
- Rust version: **1.78**
|
||||||
- Node version: **18.18**
|
- Node version: **18.18**
|
||||||
- Pnpm version: **9.0.6**
|
- Pnpm version: **9.1.1**
|
||||||
|
|
||||||
After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.
|
After cleaning out your build artifacts using `pnpm clean`, `git clean`, or `cargo clean`, it is necessary to re-run the `setup-system` script.
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useDrawerStatus } from '@react-navigation/drawer';
|
import { useDrawerStatus } from '@react-navigation/drawer';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { MotiView } from 'moti';
|
import { MotiView } from 'moti';
|
||||||
import { CaretRight, Gear, Lock, Plus } from 'phosphor-react-native';
|
import { CaretRight, CloudArrowDown, Gear, Lock, Plus } from 'phosphor-react-native';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { Alert, Pressable, Text, View } from 'react-native';
|
import { Alert, Pressable, Text, View } from 'react-native';
|
||||||
import { useClientContext } from '@sd/client';
|
import { useClientContext } from '@sd/client';
|
||||||
|
@ -12,6 +12,7 @@ import { AnimatedHeight } from '../animation/layout';
|
||||||
import { ModalRef } from '../layout/Modal';
|
import { ModalRef } from '../layout/Modal';
|
||||||
import CreateLibraryModal from '../modal/CreateLibraryModal';
|
import CreateLibraryModal from '../modal/CreateLibraryModal';
|
||||||
import { Divider } from '../primitive/Divider';
|
import { Divider } from '../primitive/Divider';
|
||||||
|
import ImportModalLibrary from '../modal/ImportLibraryModal';
|
||||||
|
|
||||||
const DrawerLibraryManager = () => {
|
const DrawerLibraryManager = () => {
|
||||||
const [dropdownClosed, setDropdownClosed] = useState(true);
|
const [dropdownClosed, setDropdownClosed] = useState(true);
|
||||||
|
@ -27,6 +28,7 @@ const DrawerLibraryManager = () => {
|
||||||
const navigation = useNavigation();
|
const navigation = useNavigation();
|
||||||
|
|
||||||
const modalRef = useRef<ModalRef>(null);
|
const modalRef = useRef<ModalRef>(null);
|
||||||
|
const modalRef_import = useRef<ModalRef>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View>
|
||||||
|
@ -91,6 +93,14 @@ const DrawerLibraryManager = () => {
|
||||||
<Text style={tw`text-sm font-semibold text-white`}>New Library</Text>
|
<Text style={tw`text-sm font-semibold text-white`}>New Library</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<CreateLibraryModal ref={modalRef} />
|
<CreateLibraryModal ref={modalRef} />
|
||||||
|
<Pressable
|
||||||
|
style={tw`flex flex-row items-center px-1.5 py-[8px]`}
|
||||||
|
onPress={() => modalRef_import.current?.present()}
|
||||||
|
>
|
||||||
|
<CloudArrowDown size={18} weight="bold" color="white" style={tw`mr-2`} />
|
||||||
|
<Text style={tw`text-sm font-semibold text-white`}>Import Library</Text>
|
||||||
|
</Pressable>
|
||||||
|
<ImportModalLibrary ref={modalRef_import} />
|
||||||
{/* Manage Library */}
|
{/* Manage Library */}
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
|
|
|
@ -32,7 +32,7 @@ const ScreenContainer = ({
|
||||||
}}
|
}}
|
||||||
contentContainerStyle={twStyle('justify-between gap-10 py-6', style)}
|
contentContainerStyle={twStyle('justify-between gap-10 py-6', style)}
|
||||||
style={twStyle(
|
style={twStyle(
|
||||||
'flex-1 bg-black',
|
'bg-black',
|
||||||
tabHeight && { marginBottom: bottomTabBarHeight }
|
tabHeight && { marginBottom: bottomTabBarHeight }
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
139
apps/mobile/src/components/modal/ImportLibraryModal.tsx
Normal file
139
apps/mobile/src/components/modal/ImportLibraryModal.tsx
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
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,
|
||||||
|
useBridgeQuery,
|
||||||
|
useClientContext,
|
||||||
|
useRspcContext
|
||||||
|
} from '@sd/client';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const ImportModalLibrary = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||||
|
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
const modalRef = useForwardedRef(ref);
|
||||||
|
|
||||||
|
const { libraries } = useClientContext();
|
||||||
|
|
||||||
|
const cloudLibraries = useBridgeQuery(['cloud.library.list']);
|
||||||
|
const cloudLibrariesData = cloudLibraries.data?.filter(
|
||||||
|
(cloudLibrary) => !libraries.data?.find((l) => l.uuid === cloudLibrary.uuid)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
ref={modalRef}
|
||||||
|
snapPoints={cloudLibrariesData?.length !== 0 ? ['30', '50'] : ['30']}
|
||||||
|
title="Join a Cloud Library"
|
||||||
|
showCloseButton
|
||||||
|
>
|
||||||
|
<View style={tw`relative flex-1`}>
|
||||||
|
{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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Fade>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: CloudLibrary;
|
||||||
|
modalRef: React.RefObject<ModalRef>;
|
||||||
|
navigation: NavigationProp<RootStackParamList>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
rspc.setQueryData(['library.list'], (libraries: any) => {
|
||||||
|
// The invalidation system beat us to it
|
||||||
|
if ((libraries || []).find((l: any) => l.uuid === library.uuid))
|
||||||
|
return libraries;
|
||||||
|
|
||||||
|
return [...(libraries || []), library];
|
||||||
|
});
|
||||||
|
|
||||||
|
currentLibraryStore.id = library.uuid;
|
||||||
|
|
||||||
|
navigation.navigate('Root', {
|
||||||
|
screen: 'Home',
|
||||||
|
params: {
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ImportModalLibrary;
|
26
apps/mobile/src/navigation/BackfillWaitingStack.tsx
Normal file
26
apps/mobile/src/navigation/BackfillWaitingStack.tsx
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { createNativeStackNavigator, NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
|
import React from 'react';
|
||||||
|
import BackfillWaiting from '~/screens/BackfillWaiting';
|
||||||
|
|
||||||
|
const Stack = createNativeStackNavigator<BackfillWaitingStackParamList>();
|
||||||
|
|
||||||
|
export default function BackfillWaitingStack() {
|
||||||
|
return (
|
||||||
|
<Stack.Navigator initialRouteName="BackfillWaiting">
|
||||||
|
<Stack.Screen
|
||||||
|
name="BackfillWaiting"
|
||||||
|
component={BackfillWaiting}
|
||||||
|
options={{
|
||||||
|
headerShown: false
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack.Navigator>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type BackfillWaitingStackParamList = {
|
||||||
|
BackfillWaiting: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type BackfillWaitingStackScreenProps<Screen extends keyof BackfillWaitingStackParamList> =
|
||||||
|
NativeStackScreenProps<BackfillWaitingStackParamList, Screen>;
|
|
@ -4,6 +4,7 @@ import NotFoundScreen from '~/screens/NotFound';
|
||||||
|
|
||||||
import DrawerNavigator, { DrawerNavParamList } from './DrawerNavigator';
|
import DrawerNavigator, { DrawerNavParamList } from './DrawerNavigator';
|
||||||
import SearchStack, { SearchStackParamList } from './SearchStack';
|
import SearchStack, { SearchStackParamList } from './SearchStack';
|
||||||
|
import BackfillWaitingStack, { BackfillWaitingStackParamList } from './BackfillWaitingStack';
|
||||||
|
|
||||||
const Stack = createNativeStackNavigator<RootStackParamList>();
|
const Stack = createNativeStackNavigator<RootStackParamList>();
|
||||||
// This is the main navigator we nest everything under.
|
// This is the main navigator we nest everything under.
|
||||||
|
@ -20,6 +21,11 @@ export default function RootNavigator() {
|
||||||
component={SearchStack}
|
component={SearchStack}
|
||||||
options={{ headerShown: false }}
|
options={{ headerShown: false }}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="BackfillWaitingStack"
|
||||||
|
component={BackfillWaitingStack}
|
||||||
|
options={{ headerShown: false }}
|
||||||
|
/>
|
||||||
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
|
<Stack.Screen name="NotFound" component={NotFoundScreen} options={{ title: 'Oops!' }} />
|
||||||
</Stack.Navigator>
|
</Stack.Navigator>
|
||||||
);
|
);
|
||||||
|
@ -28,6 +34,7 @@ export default function RootNavigator() {
|
||||||
export type RootStackParamList = {
|
export type RootStackParamList = {
|
||||||
Root: NavigatorScreenParams<DrawerNavParamList>;
|
Root: NavigatorScreenParams<DrawerNavParamList>;
|
||||||
SearchStack: NavigatorScreenParams<SearchStackParamList>;
|
SearchStack: NavigatorScreenParams<SearchStackParamList>;
|
||||||
|
BackfillWaitingStack: NavigatorScreenParams<BackfillWaitingStackParamList>;
|
||||||
NotFound: undefined;
|
NotFound: undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -12,10 +12,12 @@ 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';
|
||||||
import NodesSettingsScreen from '~/screens/settings/library/NodesSettings';
|
import NodesSettingsScreen from '~/screens/settings/library/NodesSettings';
|
||||||
|
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';
|
||||||
|
|
||||||
|
@ -87,6 +89,16 @@ export default function SettingsStack() {
|
||||||
component={TagsSettingsScreen}
|
component={TagsSettingsScreen}
|
||||||
options={{ header: () => <Header navBack title="Tags" /> }}
|
options={{ header: () => <Header navBack title="Tags" /> }}
|
||||||
/>
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="SyncSettings"
|
||||||
|
component={SyncSettingsScreen}
|
||||||
|
options={{ header: () => <Header navBack title="Sync" /> }}
|
||||||
|
/>
|
||||||
|
<Stack.Screen
|
||||||
|
name="CloudSettings"
|
||||||
|
component={CloudSettings}
|
||||||
|
options={{ header: () => <Header navBack title="Cloud" /> }}
|
||||||
|
/>
|
||||||
{/* <Stack.Screen
|
{/* <Stack.Screen
|
||||||
name="KeysSettings"
|
name="KeysSettings"
|
||||||
component={KeysSettingsScreen}
|
component={KeysSettingsScreen}
|
||||||
|
@ -131,6 +143,8 @@ export type SettingsStackParamList = {
|
||||||
NodesSettings: undefined;
|
NodesSettings: undefined;
|
||||||
TagsSettings: undefined;
|
TagsSettings: undefined;
|
||||||
KeysSettings: undefined;
|
KeysSettings: undefined;
|
||||||
|
SyncSettings: undefined;
|
||||||
|
CloudSettings: undefined;
|
||||||
// Info
|
// Info
|
||||||
About: undefined;
|
About: undefined;
|
||||||
Support: undefined;
|
Support: undefined;
|
||||||
|
|
87
apps/mobile/src/screens/BackfillWaiting.tsx
Normal file
87
apps/mobile/src/screens/BackfillWaiting.tsx
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
import { useNavigation } from '@react-navigation/native';
|
||||||
|
import { AppLogo } from '@sd/assets/images';
|
||||||
|
import { useLibraryMutation, useLibraryQuery } from '@sd/client';
|
||||||
|
import { Image } from 'expo-image';
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { Dimensions, Text, View } from 'react-native';
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withRepeat,
|
||||||
|
withTiming
|
||||||
|
} from 'react-native-reanimated';
|
||||||
|
import { Circle, Defs, RadialGradient, Stop, Svg } from 'react-native-svg';
|
||||||
|
import { tw, twStyle } from '~/lib/tailwind';
|
||||||
|
|
||||||
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
const BackfillWaiting = () => {
|
||||||
|
const animation = useSharedValue(0);
|
||||||
|
const navigation = useNavigation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
animation.value = withRepeat(
|
||||||
|
withTiming(1, { duration: 5000, easing: Easing.inOut(Easing.ease) }),
|
||||||
|
-1,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}, [animation]);
|
||||||
|
|
||||||
|
const animatedStyle = useAnimatedStyle(() => {
|
||||||
|
return {
|
||||||
|
opacity: animation.value
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const enableSync = useLibraryMutation(['sync.backfill'], {
|
||||||
|
onSuccess: () => {
|
||||||
|
syncEnabled.refetch();
|
||||||
|
navigation.navigate('Root', {
|
||||||
|
screen: 'Home',
|
||||||
|
params: {
|
||||||
|
screen: 'SettingsStack',
|
||||||
|
params: {
|
||||||
|
screen: 'SyncSettings'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const syncEnabled = useLibraryQuery(['sync.enabled']);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
await enableSync.mutateAsync(null);
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={tw`flex-1 items-center justify-center bg-black`}>
|
||||||
|
<Animated.View style={[twStyle(`absolute items-center justify-center`, {
|
||||||
|
width: width * 2,
|
||||||
|
height: width * 2,
|
||||||
|
borderRadius: (width * 0.8) / 2,
|
||||||
|
}), animatedStyle]}>
|
||||||
|
<Svg height="100%" width="100%" viewBox="0 0 100 100">
|
||||||
|
<Defs>
|
||||||
|
<RadialGradient id="grad" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
|
||||||
|
<Stop offset="0%" stopColor="#4B0082" stopOpacity="1" />
|
||||||
|
<Stop offset="100%" stopColor="#000000" stopOpacity="0" />
|
||||||
|
</RadialGradient>
|
||||||
|
</Defs>
|
||||||
|
<Circle cx="50" cy="50" r="50" fill="url(#grad)" />
|
||||||
|
</Svg>
|
||||||
|
</Animated.View>
|
||||||
|
<Image source={AppLogo} style={tw`mb-4 h-[100px] w-[100px]`} />
|
||||||
|
<Text style={tw`mx-10 mb-4 text-center text-md leading-6 text-ink`}>
|
||||||
|
Library is being backfilled right now for Sync!
|
||||||
|
<Text style={tw`font-bold`}> Please hold </Text>
|
||||||
|
while this process takes place.
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BackfillWaiting;
|
|
@ -1,5 +1,8 @@
|
||||||
|
import { DebugState, useDebugState, useDebugStateEnabler } from '@sd/client';
|
||||||
import {
|
import {
|
||||||
|
ArrowsClockwise,
|
||||||
Books,
|
Books,
|
||||||
|
Cloud,
|
||||||
FlyingSaucer,
|
FlyingSaucer,
|
||||||
Gear,
|
Gear,
|
||||||
GearSix,
|
GearSix,
|
||||||
|
@ -14,7 +17,6 @@ import {
|
||||||
} from 'phosphor-react-native';
|
} from 'phosphor-react-native';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Platform, SectionList, Text, TouchableWithoutFeedback, View } from 'react-native';
|
import { Platform, SectionList, Text, TouchableWithoutFeedback, View } from 'react-native';
|
||||||
import { DebugState, useDebugState, useDebugStateEnabler } from '@sd/client';
|
|
||||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
import { SettingsItem } from '~/components/settings/SettingsItem';
|
import { SettingsItem } from '~/components/settings/SettingsItem';
|
||||||
import { tw, twStyle } from '~/lib/tailwind';
|
import { tw, twStyle } from '~/lib/tailwind';
|
||||||
|
@ -86,6 +88,16 @@ const sections: (debugState: DebugState) => SectionType[] = (debugState) => [
|
||||||
icon: TagSimple,
|
icon: TagSimple,
|
||||||
navigateTo: 'TagsSettings',
|
navigateTo: 'TagsSettings',
|
||||||
title: 'Tags',
|
title: 'Tags',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: Cloud,
|
||||||
|
navigateTo: 'CloudSettings',
|
||||||
|
title: 'Cloud',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: ArrowsClockwise,
|
||||||
|
navigateTo: 'SyncSettings',
|
||||||
|
title: 'Sync',
|
||||||
rounded: 'bottom'
|
rounded: 'bottom'
|
||||||
}
|
}
|
||||||
// {
|
// {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Text, View } from 'react-native';
|
|
||||||
import { useBridgeQuery, useDebugState } from '@sd/client';
|
import { useBridgeQuery, useDebugState } from '@sd/client';
|
||||||
|
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 { Divider } from '~/components/primitive/Divider';
|
import { Divider } from '~/components/primitive/Divider';
|
||||||
|
|
|
@ -1,6 +1,14 @@
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Text, View } from 'react-native';
|
import { Text, View } from 'react-native';
|
||||||
import { toggleFeatureFlag, useDebugState, useFeatureFlags } from '@sd/client';
|
import {
|
||||||
|
auth,
|
||||||
|
toggleFeatureFlag,
|
||||||
|
useBridgeMutation,
|
||||||
|
useBridgeQuery,
|
||||||
|
useDebugState,
|
||||||
|
useFeatureFlags
|
||||||
|
} from '@sd/client';
|
||||||
import Card from '~/components/layout/Card';
|
import Card from '~/components/layout/Card';
|
||||||
import { Button } from '~/components/primitive/Button';
|
import { Button } from '~/components/primitive/Button';
|
||||||
import { tw } from '~/lib/tailwind';
|
import { tw } from '~/lib/tailwind';
|
||||||
|
@ -9,6 +17,10 @@ import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||||
const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
||||||
const debugState = useDebugState();
|
const debugState = useDebugState();
|
||||||
const featureFlags = useFeatureFlags();
|
const featureFlags = useFeatureFlags();
|
||||||
|
const origin = useBridgeQuery(['cloud.getApiOrigin']);
|
||||||
|
const setOrigin = useBridgeMutation(['cloud.setApiOrigin']);
|
||||||
|
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={tw`flex-1 p-4`}>
|
<View style={tw`flex-1 p-4`}>
|
||||||
|
@ -28,6 +40,37 @@ const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => {
|
||||||
>
|
>
|
||||||
<Text style={tw`text-ink`}>Disable Debug Mode</Text>
|
<Text style={tw`text-ink`}>Disable Debug Mode</Text>
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
const url =
|
||||||
|
origin.data === 'https://app.spacedrive.com'
|
||||||
|
? 'http://localhost:3000'
|
||||||
|
: 'https://app.spacedrive.com';
|
||||||
|
setOrigin.mutateAsync(url).then(async () => {
|
||||||
|
await auth.logout();
|
||||||
|
await queryClient.invalidateQueries();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-ink`}>Toggle API Route ({origin.data})</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={() => {
|
||||||
|
navigation.popToTop();
|
||||||
|
navigation.navigate('BackfillWaitingStack', {
|
||||||
|
screen: 'BackfillWaiting'
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-ink`}>Go to Backfill Waiting Page</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onPress={async () => {
|
||||||
|
await auth.logout();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-ink`}>Logout</Text>
|
||||||
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
133
apps/mobile/src/screens/settings/library/SyncSettings.tsx
Normal file
133
apps/mobile/src/screens/settings/library/SyncSettings.tsx
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
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 Card from '~/components/layout/Card';
|
||||||
|
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||||
|
import { Button } from '~/components/primitive/Button';
|
||||||
|
import { tw } from '~/lib/tailwind';
|
||||||
|
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||||
|
|
||||||
|
const SyncSettingsScreen = ({ navigation }: SettingsStackScreenProps<'SyncSettings'>) => {
|
||||||
|
const syncEnabled = useLibraryQuery(['sync.enabled']);
|
||||||
|
const [data, setData] = useState<inferSubscriptionResult<Procedures, 'library.actors'>>({});
|
||||||
|
|
||||||
|
const [startBackfill, setStart] = useState(false);
|
||||||
|
|
||||||
|
useLibrarySubscription(['library.actors'], { onData: setData });
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startBackfill === true) {
|
||||||
|
navigation.navigate('BackfillWaitingStack', {
|
||||||
|
screen: 'BackfillWaiting'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [startBackfill, navigation]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScreenContainer scrollview={false} style={tw`gap-0 px-6`}>
|
||||||
|
{syncEnabled.data === false ? (
|
||||||
|
<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} />}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</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>
|
||||||
|
) : (
|
||||||
|
<Circle size={size} color={tw.color('red-400')} weight="fill" />
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StartButton({ name }: { name: string }) {
|
||||||
|
const startActor = useLibraryMutation(['library.startActor']);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="accent"
|
||||||
|
size="sm"
|
||||||
|
disabled={startActor.isLoading}
|
||||||
|
onPress={() => startActor.mutate(name)}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-xs font-medium text-ink`}>
|
||||||
|
{startActor.isLoading ? 'Starting' : 'Start'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StopButton({ name }: { name: string }) {
|
||||||
|
const stopActor = useLibraryMutation(['library.stopActor']);
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="accent"
|
||||||
|
size="sm"
|
||||||
|
disabled={stopActor.isLoading}
|
||||||
|
onPress={() => stopActor.mutate(name)}
|
||||||
|
>
|
||||||
|
<Text style={tw`text-xs font-medium text-ink`}>
|
||||||
|
{stopActor.isLoading ? 'Stopping' : 'Stop'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
100
apps/mobile/src/stores/auth.ts
Normal file
100
apps/mobile/src/stores/auth.ts
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
import { RSPCError } from '@oscartbeaumont-sd/rspc-client';
|
||||||
|
import { nonLibraryClient, useSolidStore } from '@sd/client';
|
||||||
|
import { Linking } from 'react-native';
|
||||||
|
import { createMutable } from 'solid-js/store';
|
||||||
|
|
||||||
|
interface Store {
|
||||||
|
state: { status: 'loading' | 'notLoggedIn' | 'loggingIn' | 'loggedIn' | 'loggingOut' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// inner object so we can overwrite it in one assignment
|
||||||
|
const store = createMutable<Store>({
|
||||||
|
state: {
|
||||||
|
status: 'loading'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useAuthStateSnapshot() {
|
||||||
|
return useSolidStore(store).state;
|
||||||
|
}
|
||||||
|
|
||||||
|
nonLibraryClient
|
||||||
|
.query(['auth.me'])
|
||||||
|
.then(() => (store.state = { status: 'loggedIn' }))
|
||||||
|
.catch((e) => {
|
||||||
|
if (e instanceof RSPCError && e.code === 401) {
|
||||||
|
// TODO: handle error?
|
||||||
|
console.error("error", e);
|
||||||
|
}
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
});
|
||||||
|
|
||||||
|
type CallbackStatus = 'success' | { error: string } | 'cancel';
|
||||||
|
const loginCallbacks = new Set<(status: CallbackStatus) => void>();
|
||||||
|
|
||||||
|
function onError(error: string) {
|
||||||
|
loginCallbacks.forEach((cb) => cb({ error }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function login() {
|
||||||
|
if (store.state.status !== 'notLoggedIn') return;
|
||||||
|
|
||||||
|
store.state = { status: 'loggingIn' };
|
||||||
|
|
||||||
|
let authCleanup = nonLibraryClient.addSubscription(['auth.loginSession'], {
|
||||||
|
onData(data) {
|
||||||
|
if (data === 'Complete') {
|
||||||
|
loginCallbacks.forEach((cb) => cb('success'));
|
||||||
|
} else if ('Error' in data) {
|
||||||
|
console.error('[auth] error: ', data.Error);
|
||||||
|
onError(data.Error);
|
||||||
|
} else {
|
||||||
|
console.log('[auth] verification url: ', data.Start.verification_url_complete);
|
||||||
|
Promise.resolve()
|
||||||
|
.then(() => Linking.openURL(data.Start.verification_url_complete))
|
||||||
|
.then(
|
||||||
|
(res) => {
|
||||||
|
authCleanup = res;
|
||||||
|
},
|
||||||
|
(e) => onError(e.message)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(e) {
|
||||||
|
onError(e.message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return new Promise<void>((res, rej) => {
|
||||||
|
const cb = async (status: CallbackStatus) => {
|
||||||
|
loginCallbacks.delete(cb);
|
||||||
|
|
||||||
|
if (status === 'success') {
|
||||||
|
store.state = { status: 'loggedIn' };
|
||||||
|
nonLibraryClient.query(['auth.me']);
|
||||||
|
res();
|
||||||
|
} else {
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
rej(JSON.stringify(status));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loginCallbacks.add(cb);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function set_logged_in() {
|
||||||
|
store.state = { status: 'loggedIn' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function logout() {
|
||||||
|
store.state = { status: 'loggingOut' };
|
||||||
|
nonLibraryClient.mutation(['auth.logout']);
|
||||||
|
nonLibraryClient.query(['auth.me']);
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function cancel() {
|
||||||
|
await loginCallbacks.forEach(async (cb) => await cb('cancel'));
|
||||||
|
await loginCallbacks.clear();
|
||||||
|
store.state = { status: 'notLoggedIn' };
|
||||||
|
}
|
|
@ -32,13 +32,21 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async_stream::stream! {
|
async_stream::stream! {
|
||||||
|
let device_type = if cfg!(target_arch = "wasm32") {
|
||||||
|
"web".to_string()
|
||||||
|
} else if cfg!(target_os = "ios") || cfg!(target_os = "android") {
|
||||||
|
"mobile".to_string()
|
||||||
|
} else {
|
||||||
|
"desktop".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
let auth_response = match match node
|
let auth_response = match match node
|
||||||
.http
|
.http
|
||||||
.post(&format!(
|
.post(&format!(
|
||||||
"{}/login/device/code",
|
"{}/login/device/code",
|
||||||
&node.env.api_url.lock().await
|
&node.env.api_url.lock().await
|
||||||
))
|
))
|
||||||
.form(&[("client_id", &node.env.client_id)])
|
.form(&[("client_id", &node.env.client_id), ("device", &device_type)])
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|
|
@ -141,7 +141,9 @@ impl Node {
|
||||||
config,
|
config,
|
||||||
event_bus,
|
event_bus,
|
||||||
libraries,
|
libraries,
|
||||||
cloud_sync_flag: Arc::new(AtomicBool::new(false)),
|
cloud_sync_flag: Arc::new(AtomicBool::new(
|
||||||
|
cfg!(target_os = "ios") || cfg!(target_os = "android"),
|
||||||
|
)),
|
||||||
http: reqwest::Client::new(),
|
http: reqwest::Client::new(),
|
||||||
env,
|
env,
|
||||||
#[cfg(feature = "ai")]
|
#[cfg(feature = "ai")]
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
---
|
|
||||||
index: 100
|
|
||||||
---
|
|
||||||
|
|
||||||
# Folder Sync
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Gear, Lock, Plus } from '@phosphor-icons/react';
|
import { CloudArrowDown, Gear, Lock, Plus } from '@phosphor-icons/react';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import { useClientContext } from '@sd/client';
|
import { useClientContext } from '@sd/client';
|
||||||
import { dialogManager, Dropdown, DropdownMenu } from '@sd/ui';
|
import { dialogManager, Dropdown, DropdownMenu } from '@sd/ui';
|
||||||
|
@ -6,6 +6,7 @@ import { useLocale } from '~/hooks';
|
||||||
|
|
||||||
import CreateDialog from '../../../settings/node/libraries/CreateDialog';
|
import CreateDialog from '../../../settings/node/libraries/CreateDialog';
|
||||||
import { useSidebarContext } from './Context';
|
import { useSidebarContext } from './Context';
|
||||||
|
import JoinDialog from '~/app/$libraryId/settings/node/libraries/JoinDialog';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
const { library, libraries, currentLibraryId } = useClientContext();
|
const { library, libraries, currentLibraryId } = useClientContext();
|
||||||
|
@ -62,6 +63,13 @@ export default () => {
|
||||||
onClick={() => dialogManager.create((dp) => <CreateDialog {...dp} />)}
|
onClick={() => dialogManager.create((dp) => <CreateDialog {...dp} />)}
|
||||||
className="font-medium"
|
className="font-medium"
|
||||||
/>
|
/>
|
||||||
|
<DropdownMenu.Item
|
||||||
|
label={t('join_library')}
|
||||||
|
icon={CloudArrowDown}
|
||||||
|
iconProps={{ weight: 'bold', size: 16 }}
|
||||||
|
onClick={() => dialogManager.create((dp) => <JoinDialog librariesCtx={libraries.data} {...dp} />)}
|
||||||
|
className="font-medium"
|
||||||
|
/>
|
||||||
<DropdownMenu.Item
|
<DropdownMenu.Item
|
||||||
label={t('manage_library')}
|
label={t('manage_library')}
|
||||||
icon={Gear}
|
icon={Gear}
|
||||||
|
|
|
@ -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 { useLocale, useRouteTitle } from '~/hooks';
|
||||||
|
import { hardwareModelToIcon } from '~/util/hardware';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
useRouteTitle('Cloud');
|
useRouteTitle('Cloud');
|
||||||
|
@ -12,82 +24,203 @@ export const Component = () => {
|
||||||
const authSensitiveChild = () => {
|
const authSensitiveChild = () => {
|
||||||
if (authState.status === 'loggedIn') return <Authenticated />;
|
if (authState.status === 'loggedIn') return <Authenticated />;
|
||||||
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn')
|
if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn')
|
||||||
return <LoginButton />;
|
return (
|
||||||
|
<div className="flex items-center justify-center size-full">
|
||||||
|
<Card className="flex flex-col gap-4 !p-6">
|
||||||
|
<p>To access cloud related features, please login</p>
|
||||||
|
<LoginButton />
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div className="flex size-full flex-col items-start p-4">{authSensitiveChild()}</div>;
|
return <div className="flex flex-col items-start p-4 size-full">{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 { t } = useLocale();
|
||||||
|
|
||||||
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 items-center justify-center size-full">
|
||||||
|
<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 flex-col items-center justify-center size-full">
|
||||||
<AuthRequiredOverlay />
|
<AuthRequiredOverlay />
|
||||||
<Button
|
<Button
|
||||||
disabled={createLibrary.isLoading}
|
disabled={createLibrary.isLoading}
|
||||||
|
variant="accent"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createLibrary.mutateAsync(null);
|
createLibrary.mutateAsync(null);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{createLibrary.isLoading
|
{createLibrary.isLoading
|
||||||
? 'Connecting library to Spacedrive Cloud...'
|
? t('connecting_library_to_cloud')
|
||||||
: 'Connect library to Spacedrive Cloud'}
|
: t('connect_library_to_cloud')}
|
||||||
</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="font-bold text-medium">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="text-xs font-medium truncate">
|
||||||
|
Id:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">{instance.id}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="text-xs font-medium truncate">
|
||||||
|
UUID:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">
|
||||||
|
{instance.uuid}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="text-xs font-medium truncate">
|
||||||
|
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="font-bold text-medium">Library</p>
|
||||||
|
<Card className="flex-row items-center gap-10 !px-2">
|
||||||
|
<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="font-bold text-medium">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="text-xs font-medium truncate">
|
||||||
|
Id: <span className="font-normal text-ink-dull">{instance.id}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="text-xs font-medium truncate">
|
||||||
|
UUID: <span className="font-normal text-ink-dull">{instance.uuid}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
<DataBox>
|
||||||
|
<p className="text-xs font-medium truncate">
|
||||||
|
Public Key:{' '}
|
||||||
|
<span className="font-normal text-ink-dull">{instance.identity}</span>
|
||||||
|
</p>
|
||||||
|
</DataBox>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
105
interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx
Normal file
105
interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
import {
|
||||||
|
LibraryConfigWrapped,
|
||||||
|
useBridgeMutation,
|
||||||
|
useBridgeQuery,
|
||||||
|
useClientContext,
|
||||||
|
useLibraryContext,
|
||||||
|
usePlausibleEvent,
|
||||||
|
useZodForm
|
||||||
|
} from '@sd/client';
|
||||||
|
import { Button, Dialog, Select, SelectOption, toast, useDialog, UseDialogProps, z } from '@sd/ui';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useNavigate } from 'react-router';
|
||||||
|
import { useLocale } from '~/hooks';
|
||||||
|
import { usePlatform } from '~/util/Platform';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
libraryId: z.string().refine((value) => value !== 'select_library', {
|
||||||
|
message: 'Please select a library'
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
export default (props: UseDialogProps & { librariesCtx: LibraryConfigWrapped[] | undefined }) => {
|
||||||
|
const cloudLibraries = useBridgeQuery(['cloud.library.list']);
|
||||||
|
const joinLibrary = useBridgeMutation(['cloud.library.join']);
|
||||||
|
|
||||||
|
const { t } = useLocale();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const platform = usePlatform();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const form = useZodForm({ schema, defaultValues: { libraryId: 'select_library' } });
|
||||||
|
|
||||||
|
// const queryClient = useQueryClient();
|
||||||
|
// const submitPlausibleEvent = usePlausibleEvent();
|
||||||
|
// const platform = usePlatform();
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit(async (data) => {
|
||||||
|
try {
|
||||||
|
const library = await joinLibrary.mutateAsync(data.libraryId);
|
||||||
|
|
||||||
|
queryClient.setQueryData(['library.list'], (libraries: any) => {
|
||||||
|
// The invalidation system beat us to it
|
||||||
|
if ((libraries || []).find((l: any) => l.uuid === library.uuid)) return libraries;
|
||||||
|
|
||||||
|
return [...(libraries || []), library];
|
||||||
|
});
|
||||||
|
|
||||||
|
platform.refreshMenuBar && platform.refreshMenuBar();
|
||||||
|
|
||||||
|
navigate(`/${library.uuid}`, { replace: true });
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e);
|
||||||
|
toast.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
form={form}
|
||||||
|
onSubmit={onSubmit}
|
||||||
|
dialog={useDialog(props)}
|
||||||
|
submitDisabled={!form.formState.isValid}
|
||||||
|
title={t('join_library')}
|
||||||
|
closeLabel={t('close')}
|
||||||
|
cancelLabel={t('cancel')}
|
||||||
|
description={t('join_library_description')}
|
||||||
|
ctaLabel={form.formState.isSubmitting ? t('joining') : t('join')}
|
||||||
|
>
|
||||||
|
<div className="mt-5 space-y-4">
|
||||||
|
{cloudLibraries.isLoading && <span>{t('loading')}...</span>}
|
||||||
|
{cloudLibraries.data && (
|
||||||
|
<Select
|
||||||
|
value={form.watch('libraryId')}
|
||||||
|
size="sm"
|
||||||
|
className="w-full"
|
||||||
|
onChange={(key) => {
|
||||||
|
console.log('Key:', key);
|
||||||
|
// Update the form value
|
||||||
|
form.setValue('libraryId', key, {
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectOption value="select_library">
|
||||||
|
{t('select_library')}
|
||||||
|
</SelectOption>
|
||||||
|
{cloudLibraries.data
|
||||||
|
.filter(
|
||||||
|
(cloudLibrary) =>
|
||||||
|
!props.librariesCtx?.find(
|
||||||
|
(l: any) => l.uuid === cloudLibrary.uuid
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map((cloudLibrary) => (
|
||||||
|
<SelectOption key={cloudLibrary.uuid} value={cloudLibrary.uuid}>
|
||||||
|
{cloudLibrary.name}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,16 +1,21 @@
|
||||||
import { useBridgeQuery, useLibraryContext } from '@sd/client';
|
import { useBridgeQuery, useClientContext, useFeatureFlag, useLibraryContext } from '@sd/client';
|
||||||
import { Button, dialogManager } from '@sd/ui';
|
import { Button, dialogManager } from '@sd/ui';
|
||||||
import { useLocale } from '~/hooks';
|
import { useLocale } from '~/hooks';
|
||||||
|
|
||||||
import { Heading } from '../../Layout';
|
import { Heading } from '../../Layout';
|
||||||
import CreateDialog from './CreateDialog';
|
import CreateDialog from './CreateDialog';
|
||||||
|
import JoinDialog from './JoinDialog';
|
||||||
import ListItem from './ListItem';
|
import ListItem from './ListItem';
|
||||||
|
|
||||||
export const Component = () => {
|
export const Component = () => {
|
||||||
const librariesQuery = useBridgeQuery(['library.list']);
|
const librariesQuery = useBridgeQuery(['library.list']);
|
||||||
const libraries = librariesQuery.data;
|
const libraries = librariesQuery.data;
|
||||||
|
|
||||||
|
const cloudEnabled = useFeatureFlag('cloudSync');
|
||||||
|
|
||||||
const { library } = useLibraryContext();
|
const { library } = useLibraryContext();
|
||||||
|
const { libraries: librariesCtx } = useClientContext();
|
||||||
|
const librariesCtxData = librariesCtx.data;
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
@ -30,10 +35,22 @@ export const Component = () => {
|
||||||
>
|
>
|
||||||
{t('add_library')}
|
{t('add_library')}
|
||||||
</Button>
|
</Button>
|
||||||
|
{cloudEnabled && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
dialogManager.create((dp) => (
|
||||||
|
<JoinDialog librariesCtx={librariesCtxData} {...dp} />
|
||||||
|
));
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('join_library')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{libraries
|
{libraries
|
||||||
?.sort((a, b) => {
|
?.sort((a, b) => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ export function JoinLibrary() {
|
||||||
|
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<span>Cloud Libraries</span>
|
<span>Cloud Libraries</span>
|
||||||
<ul className="relative flex h-32 w-48 flex-col rounded border border-app-frame p-2">
|
<ul className="relative flex flex-col w-48 h-32 p-2 border rounded border-app-frame">
|
||||||
<CloudLibraries />
|
<CloudLibraries />
|
||||||
<AuthRequiredOverlay />
|
<AuthRequiredOverlay />
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
||||||
"connect_device": "Connect a device",
|
"connect_device": "Connect a device",
|
||||||
"connect_device_description": "Spacedrive works best on all your devices.",
|
"connect_device_description": "Spacedrive works best on all your devices.",
|
||||||
|
"connect_library_to_cloud": "قم بتوصيل المكتبة بـ Spacedrive Cloud",
|
||||||
"connected": "متصل",
|
"connected": "متصل",
|
||||||
|
"connecting_library_to_cloud": "جارٍ توصيل المكتبة بـ Spacedrive Cloud...",
|
||||||
"contacts": "جهات الاتصال",
|
"contacts": "جهات الاتصال",
|
||||||
"contacts_description": "إدارة جهات الاتصال الخاصة بك في Spacedrive.",
|
"contacts_description": "إدارة جهات الاتصال الخاصة بك في Spacedrive.",
|
||||||
"contains": "contains",
|
"contains": "contains",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Падключыце воблачныя акаўнты да Spacedrive.",
|
"connect_cloud_description": "Падключыце воблачныя акаўнты да Spacedrive.",
|
||||||
"connect_device": "Падключыце прыладу",
|
"connect_device": "Падключыце прыладу",
|
||||||
"connect_device_description": "Spacedrive лепш за ўсё працуе пры выкарыстанні на ўсіх вашых прыладах.",
|
"connect_device_description": "Spacedrive лепш за ўсё працуе пры выкарыстанні на ўсіх вашых прыладах.",
|
||||||
|
"connect_library_to_cloud": "Падключыце бібліятэку да Spacedrive Cloud",
|
||||||
"connected": "Падключана",
|
"connected": "Падключана",
|
||||||
|
"connecting_library_to_cloud": "Падключэнне бібліятэкі да Spacedrive Cloud...",
|
||||||
"contacts": "Кантакты",
|
"contacts": "Кантакты",
|
||||||
"contacts_description": "Кіруйце кантактамі ў Spacedrive.",
|
"contacts_description": "Кіруйце кантактамі ў Spacedrive.",
|
||||||
"contains": "змяшчае",
|
"contains": "змяшчае",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Verbinde deine Cloud-Konten mit Spacedrive.",
|
"connect_cloud_description": "Verbinde deine Cloud-Konten mit Spacedrive.",
|
||||||
"connect_device": "Ein Gerät anschließen",
|
"connect_device": "Ein Gerät anschließen",
|
||||||
"connect_device_description": "Spacedrive funktioniert am besten auf all deinen Geräten.",
|
"connect_device_description": "Spacedrive funktioniert am besten auf all deinen Geräten.",
|
||||||
|
"connect_library_to_cloud": "Bibliothek mit Spacedrive Cloud verbinden",
|
||||||
"connected": "Verbunden",
|
"connected": "Verbunden",
|
||||||
|
"connecting_library_to_cloud": "Bibliothek mit Spacedrive Cloud verbinden …",
|
||||||
"contacts": "Kontakte",
|
"contacts": "Kontakte",
|
||||||
"contacts_description": "Verwalte deine Kontakte in Spacedrive.",
|
"contacts_description": "Verwalte deine Kontakte in Spacedrive.",
|
||||||
"contains": "enthält",
|
"contains": "enthält",
|
||||||
|
|
|
@ -90,7 +90,9 @@
|
||||||
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
"connect_cloud_description": "Connect your cloud accounts to Spacedrive.",
|
||||||
"connect_device": "Connect a device",
|
"connect_device": "Connect a device",
|
||||||
"connect_device_description": "Spacedrive works best on all your devices.",
|
"connect_device_description": "Spacedrive works best on all your devices.",
|
||||||
|
"connect_library_to_cloud": "Connect library to Spacedrive Cloud",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
|
"connecting_library_to_cloud": "Connecting library to Spacedrive Cloud...",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"contacts_description": "Manage your contacts in Spacedrive.",
|
"contacts_description": "Manage your contacts in Spacedrive.",
|
||||||
"contains": "contains",
|
"contains": "contains",
|
||||||
|
@ -356,6 +358,7 @@
|
||||||
"join_discord": "Join Discord",
|
"join_discord": "Join Discord",
|
||||||
"join_library": "Join a Library",
|
"join_library": "Join a Library",
|
||||||
"join_library_description": "Libraries are a secure, on-device database. Your files remain where they are, the Library catalogs them and stores all Spacedrive related data.",
|
"join_library_description": "Libraries are a secure, on-device database. Your files remain where they are, the Library catalogs them and stores all Spacedrive related data.",
|
||||||
|
"joining": "Joining",
|
||||||
"key": "Key",
|
"key": "Key",
|
||||||
"key_manager": "Key Manager",
|
"key_manager": "Key Manager",
|
||||||
"key_manager_description": "Create encryption keys, mount and unmount your keys to see files decrypted on the fly.",
|
"key_manager_description": "Create encryption keys, mount and unmount your keys to see files decrypted on the fly.",
|
||||||
|
@ -597,6 +600,7 @@
|
||||||
"security_description": "Keep your client safe.",
|
"security_description": "Keep your client safe.",
|
||||||
"see_less": "See less",
|
"see_less": "See less",
|
||||||
"see_more": "See more",
|
"see_more": "See more",
|
||||||
|
"select_library": "Select a Cloud Library",
|
||||||
"send": "Send",
|
"send": "Send",
|
||||||
"send_report": "Send Report",
|
"send_report": "Send Report",
|
||||||
"sender": "Sender",
|
"sender": "Sender",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Conecta tus cuentas en la nube a Spacedrive.",
|
"connect_cloud_description": "Conecta tus cuentas en la nube a Spacedrive.",
|
||||||
"connect_device": "Conectar un dispositivo",
|
"connect_device": "Conectar un dispositivo",
|
||||||
"connect_device_description": "Spacedrive funciona mejor en todos tus dispositivos.",
|
"connect_device_description": "Spacedrive funciona mejor en todos tus dispositivos.",
|
||||||
|
"connect_library_to_cloud": "Conecte la biblioteca a Spacedrive Cloud",
|
||||||
"connected": "Conectado",
|
"connected": "Conectado",
|
||||||
|
"connecting_library_to_cloud": "Conectando la biblioteca a Spacedrive Cloud...",
|
||||||
"contacts": "Contactos",
|
"contacts": "Contactos",
|
||||||
"contacts_description": "Administra tus contactos en Spacedrive.",
|
"contacts_description": "Administra tus contactos en Spacedrive.",
|
||||||
"contains": "contiene",
|
"contains": "contiene",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Connectez vos comptes cloud à Spacedrive.",
|
"connect_cloud_description": "Connectez vos comptes cloud à Spacedrive.",
|
||||||
"connect_device": "Connecter un appareil",
|
"connect_device": "Connecter un appareil",
|
||||||
"connect_device_description": "Spacedrive fonctionne mieux sur tous vos appareils.",
|
"connect_device_description": "Spacedrive fonctionne mieux sur tous vos appareils.",
|
||||||
|
"connect_library_to_cloud": "Connecter la bibliothèque à Spacedrive Cloud",
|
||||||
"connected": "Connecté",
|
"connected": "Connecté",
|
||||||
|
"connecting_library_to_cloud": "Connexion de la bibliothèque à Spacedrive Cloud...",
|
||||||
"contacts": "Contacts",
|
"contacts": "Contacts",
|
||||||
"contacts_description": "Gérez vos contacts dans Spacedrive.",
|
"contacts_description": "Gérez vos contacts dans Spacedrive.",
|
||||||
"contains": "contient",
|
"contains": "contient",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Collegate i vostri account cloud a Spacedrive.",
|
"connect_cloud_description": "Collegate i vostri account cloud a Spacedrive.",
|
||||||
"connect_device": "Collegare un dispositivo",
|
"connect_device": "Collegare un dispositivo",
|
||||||
"connect_device_description": "Spacedrive funziona al meglio su tutti i dispositivi.",
|
"connect_device_description": "Spacedrive funziona al meglio su tutti i dispositivi.",
|
||||||
|
"connect_library_to_cloud": "Connetti la libreria a Spacedrive Cloud",
|
||||||
"connected": "Connesso",
|
"connected": "Connesso",
|
||||||
|
"connecting_library_to_cloud": "Collegamento della libreria a Spacedrive Cloud...",
|
||||||
"contacts": "Contatti",
|
"contacts": "Contatti",
|
||||||
"contacts_description": "Gestisci i tuoi contatti su Spacedrive.",
|
"contacts_description": "Gestisci i tuoi contatti su Spacedrive.",
|
||||||
"contains": "contiene",
|
"contains": "contiene",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "クラウドアカウントをSpacedriveに接続する。",
|
"connect_cloud_description": "クラウドアカウントをSpacedriveに接続する。",
|
||||||
"connect_device": "デバイスを接続する",
|
"connect_device": "デバイスを接続する",
|
||||||
"connect_device_description": "Spacedriveはすべてのデバイスで最適に機能します。",
|
"connect_device_description": "Spacedriveはすべてのデバイスで最適に機能します。",
|
||||||
|
"connect_library_to_cloud": "ライブラリをSpacedrive Cloudに接続する",
|
||||||
"connected": "接続中",
|
"connected": "接続中",
|
||||||
|
"connecting_library_to_cloud": "ライブラリを Spacedrive Cloud に接続しています...",
|
||||||
"contacts": "連絡先",
|
"contacts": "連絡先",
|
||||||
"contacts_description": "Spacedriveで連絡先を管理。",
|
"contacts_description": "Spacedriveで連絡先を管理。",
|
||||||
"contains": "が次を含む",
|
"contains": "が次を含む",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Verbind uw cloudaccounts met Spacedrive.",
|
"connect_cloud_description": "Verbind uw cloudaccounts met Spacedrive.",
|
||||||
"connect_device": "Een apparaat aansluiten",
|
"connect_device": "Een apparaat aansluiten",
|
||||||
"connect_device_description": "Spacedrive werkt het beste op al uw apparaten.",
|
"connect_device_description": "Spacedrive werkt het beste op al uw apparaten.",
|
||||||
|
"connect_library_to_cloud": "Verbind de bibliotheek met Spacedrive Cloud",
|
||||||
"connected": "Verbonden",
|
"connected": "Verbonden",
|
||||||
|
"connecting_library_to_cloud": "Bibliotheek verbinden met Spacedrive Cloud...",
|
||||||
"contacts": "Contacten",
|
"contacts": "Contacten",
|
||||||
"contacts_description": "Beheer je contacten in Spacedrive.",
|
"contacts_description": "Beheer je contacten in Spacedrive.",
|
||||||
"contains": "bevatten",
|
"contains": "bevatten",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Подключите облачные аккаунты к Spacedrive.",
|
"connect_cloud_description": "Подключите облачные аккаунты к Spacedrive.",
|
||||||
"connect_device": "Подключите устройство",
|
"connect_device": "Подключите устройство",
|
||||||
"connect_device_description": "Spacedrive лучше всего работает при использовании на всех ваших устройствах.",
|
"connect_device_description": "Spacedrive лучше всего работает при использовании на всех ваших устройствах.",
|
||||||
|
"connect_library_to_cloud": "Подключите библиотеку к Spacedrive Cloud",
|
||||||
"connected": "Подключено",
|
"connected": "Подключено",
|
||||||
|
"connecting_library_to_cloud": "Подключение библиотеки к Spacedrive Cloud...",
|
||||||
"contacts": "Контакты",
|
"contacts": "Контакты",
|
||||||
"contacts_description": "Управляйте контактами в Spacedrive.",
|
"contacts_description": "Управляйте контактами в Spacedrive.",
|
||||||
"contains": "содержит",
|
"contains": "содержит",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "Bulut hesaplarınızı Spacedrive'a bağlayın.",
|
"connect_cloud_description": "Bulut hesaplarınızı Spacedrive'a bağlayın.",
|
||||||
"connect_device": "Bir cihaz bağlayın",
|
"connect_device": "Bir cihaz bağlayın",
|
||||||
"connect_device_description": "Spacedrive tüm cihazlarınızda en iyi şekilde çalışır.",
|
"connect_device_description": "Spacedrive tüm cihazlarınızda en iyi şekilde çalışır.",
|
||||||
|
"connect_library_to_cloud": "Kitaplığı Spacedrive Cloud'a bağlayın",
|
||||||
"connected": "Bağlı",
|
"connected": "Bağlı",
|
||||||
|
"connecting_library_to_cloud": "Kitaplık Spacedrive Cloud'a bağlanıyor...",
|
||||||
"contacts": "Kişiler",
|
"contacts": "Kişiler",
|
||||||
"contacts_description": "Kişilerinizi Spacedrive'da yönetin.",
|
"contacts_description": "Kişilerinizi Spacedrive'da yönetin.",
|
||||||
"contains": "içerir",
|
"contains": "içerir",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "将您的云帐户连接到 Spacedrive。",
|
"connect_cloud_description": "将您的云帐户连接到 Spacedrive。",
|
||||||
"connect_device": "连接设备",
|
"connect_device": "连接设备",
|
||||||
"connect_device_description": "Spacedrive 在您的所有设备上都能发挥最佳效果。",
|
"connect_device_description": "Spacedrive 在您的所有设备上都能发挥最佳效果。",
|
||||||
|
"connect_library_to_cloud": "将图书馆连接到 Spacedrive Cloud",
|
||||||
"connected": "已连接",
|
"connected": "已连接",
|
||||||
|
"connecting_library_to_cloud": "将图书馆连接到 Spacedrive Cloud...",
|
||||||
"contacts": "联系人",
|
"contacts": "联系人",
|
||||||
"contacts_description": "在 Spacedrive 中管理您的联系人。",
|
"contacts_description": "在 Spacedrive 中管理您的联系人。",
|
||||||
"contains": "包含",
|
"contains": "包含",
|
||||||
|
|
|
@ -89,7 +89,9 @@
|
||||||
"connect_cloud_description": "將您的雲端帳戶連接到 Spacedrive。",
|
"connect_cloud_description": "將您的雲端帳戶連接到 Spacedrive。",
|
||||||
"connect_device": "連接裝置",
|
"connect_device": "連接裝置",
|
||||||
"connect_device_description": "Spacedrive 在您的所有裝置上都能發揮最佳效果。",
|
"connect_device_description": "Spacedrive 在您的所有裝置上都能發揮最佳效果。",
|
||||||
|
"connect_library_to_cloud": "將圖書館連接到 Spacedrive Cloud",
|
||||||
"connected": "已連接",
|
"connected": "已連接",
|
||||||
|
"connecting_library_to_cloud": "正在將圖書館連接到 Spacedrive Cloud...",
|
||||||
"contacts": "聯繫人",
|
"contacts": "聯繫人",
|
||||||
"contacts_description": "在Spacedrive中管理您的聯繫人。",
|
"contacts_description": "在Spacedrive中管理您的聯繫人。",
|
||||||
"contains": "包含",
|
"contains": "包含",
|
||||||
|
|
|
@ -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 'MobileAndroid';
|
||||||
case 'MacMini':
|
case 'MacMini':
|
||||||
return 'MiniSilverBox';
|
return 'MiniSilverBox';
|
||||||
case 'Other':
|
case 'Other':
|
||||||
|
|
|
@ -41,7 +41,7 @@ function onError(error: string) {
|
||||||
loginCallbacks.forEach((cb) => cb({ error }));
|
loginCallbacks.forEach((cb) => cb({ error }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function login(config: ProviderConfig) {
|
export async function login(config: ProviderConfig) {
|
||||||
if (store.state.status !== 'notLoggedIn') return;
|
if (store.state.status !== 'notLoggedIn') return;
|
||||||
|
|
||||||
store.state = { status: 'loggingIn' };
|
store.state = { status: 'loggingIn' };
|
||||||
|
@ -86,10 +86,10 @@ export function login(config: ProviderConfig) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout() {
|
export async function logout() {
|
||||||
store.state = { status: 'loggingOut' };
|
store.state = { status: 'loggingOut' };
|
||||||
nonLibraryClient.mutation(['auth.logout']);
|
await nonLibraryClient.mutation(['auth.logout']);
|
||||||
nonLibraryClient.query(['auth.me']);
|
await nonLibraryClient.query(['auth.me']);
|
||||||
store.state = { status: 'notLoggedIn' };
|
store.state = { status: 'notLoggedIn' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,7 @@ export const Select = forwardRef(
|
||||||
</RS.Trigger>
|
</RS.Trigger>
|
||||||
|
|
||||||
<RS.Portal>
|
<RS.Portal>
|
||||||
<RS.Content className="z-50 rounded-md border border-app-line bg-app-box shadow-2xl shadow-app-shade/20 ">
|
<RS.Content className="z-[100] rounded-md border border-app-line bg-app-box shadow-2xl shadow-app-shade/20 ">
|
||||||
<RS.Viewport className="p-1">{props.children}</RS.Viewport>
|
<RS.Viewport className="p-1">{props.children}</RS.Viewport>
|
||||||
</RS.Content>
|
</RS.Content>
|
||||||
</RS.Portal>
|
</RS.Portal>
|
||||||
|
|
Loading…
Reference in a new issue