From 18235c6f095d682db50e2813b98e1f57d091b060 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Tue, 18 Jun 2024 11:46:29 +0300 Subject: [PATCH] [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> --- CONTRIBUTING.md | 2 +- .../drawer/DrawerLibraryManager.tsx | 12 +- .../src/components/layout/ScreenContainer.tsx | 2 +- .../components/modal/ImportLibraryModal.tsx | 139 ++++++++++ .../src/navigation/BackfillWaitingStack.tsx | 26 ++ apps/mobile/src/navigation/index.tsx | 7 + .../src/navigation/tabs/SettingsStack.tsx | 14 + apps/mobile/src/screens/BackfillWaiting.tsx | 87 +++++++ apps/mobile/src/screens/settings/Settings.tsx | 14 +- .../settings/client/GeneralSettings.tsx | 2 +- .../src/screens/settings/info/Debug.tsx | 45 +++- .../library/CloudSettings/CloudSettings.tsx | 127 ++++++++++ .../library/CloudSettings/Instance.tsx | 44 ++++ .../library/CloudSettings/Library.tsx | 66 +++++ .../settings/library/CloudSettings/Login.tsx | 40 +++ .../library/CloudSettings/ThisInstance.tsx | 54 ++++ .../screens/settings/library/SyncSettings.tsx | 133 ++++++++++ apps/mobile/src/stores/auth.ts | 100 ++++++++ core/src/api/auth.rs | 10 +- core/src/lib.rs | 4 +- docs/product/guides/folder-sync.mdx | 5 - .../SidebarLayout/LibrariesDropdown.tsx | 10 +- interface/app/$libraryId/debug/cloud.tsx | 239 ++++++++++++++---- .../settings/node/libraries/JoinDialog.tsx | 105 ++++++++ .../settings/node/libraries/index.tsx | 21 +- interface/app/onboarding/join-library.tsx | 2 +- interface/locales/ar/common.json | 2 + interface/locales/be/common.json | 2 + interface/locales/de/common.json | 2 + interface/locales/en/common.json | 4 + interface/locales/es/common.json | 2 + interface/locales/fr/common.json | 2 + interface/locales/it/common.json | 2 + interface/locales/ja/common.json | 2 + interface/locales/nl/common.json | 2 + interface/locales/ru/common.json | 2 + interface/locales/tr/common.json | 2 + interface/locales/zh-CN/common.json | 2 + interface/locales/zh-TW/common.json | 2 + interface/util/hardware.ts | 4 + packages/client/src/stores/auth.ts | 8 +- packages/ui/src/Select.tsx | 2 +- 42 files changed, 1277 insertions(+), 75 deletions(-) create mode 100644 apps/mobile/src/components/modal/ImportLibraryModal.tsx create mode 100644 apps/mobile/src/navigation/BackfillWaitingStack.tsx create mode 100644 apps/mobile/src/screens/BackfillWaiting.tsx create mode 100644 apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx create mode 100644 apps/mobile/src/screens/settings/library/CloudSettings/Instance.tsx create mode 100644 apps/mobile/src/screens/settings/library/CloudSettings/Library.tsx create mode 100644 apps/mobile/src/screens/settings/library/CloudSettings/Login.tsx create mode 100644 apps/mobile/src/screens/settings/library/CloudSettings/ThisInstance.tsx create mode 100644 apps/mobile/src/screens/settings/library/SyncSettings.tsx create mode 100644 apps/mobile/src/stores/auth.ts delete mode 100644 docs/product/guides/folder-sync.mdx create mode 100644 interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index efa04a378..09a0f8ae1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -91,7 +91,7 @@ If you encounter any issues, ensure that you are using the following versions of - Rust version: **1.78** - 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. diff --git a/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx b/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx index 15e05ff1d..62c08d11a 100644 --- a/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx +++ b/apps/mobile/src/components/drawer/DrawerLibraryManager.tsx @@ -1,7 +1,7 @@ import { useDrawerStatus } from '@react-navigation/drawer'; import { useNavigation } from '@react-navigation/native'; 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 { Alert, Pressable, Text, View } from 'react-native'; import { useClientContext } from '@sd/client'; @@ -12,6 +12,7 @@ import { AnimatedHeight } from '../animation/layout'; import { ModalRef } from '../layout/Modal'; import CreateLibraryModal from '../modal/CreateLibraryModal'; import { Divider } from '../primitive/Divider'; +import ImportModalLibrary from '../modal/ImportLibraryModal'; const DrawerLibraryManager = () => { const [dropdownClosed, setDropdownClosed] = useState(true); @@ -27,6 +28,7 @@ const DrawerLibraryManager = () => { const navigation = useNavigation(); const modalRef = useRef(null); + const modalRef_import = useRef(null); return ( @@ -91,6 +93,14 @@ const DrawerLibraryManager = () => { New Library + modalRef_import.current?.present()} + > + + Import Library + + {/* Manage Library */} { diff --git a/apps/mobile/src/components/layout/ScreenContainer.tsx b/apps/mobile/src/components/layout/ScreenContainer.tsx index eecee3b4d..6dea4bfec 100644 --- a/apps/mobile/src/components/layout/ScreenContainer.tsx +++ b/apps/mobile/src/components/layout/ScreenContainer.tsx @@ -32,7 +32,7 @@ const ScreenContainer = ({ }} contentContainerStyle={twStyle('justify-between gap-10 py-6', style)} style={twStyle( - 'flex-1 bg-black', + 'bg-black', tabHeight && { marginBottom: bottomTabBarHeight } )} > diff --git a/apps/mobile/src/components/modal/ImportLibraryModal.tsx b/apps/mobile/src/components/modal/ImportLibraryModal.tsx new file mode 100644 index 000000000..0e47d20ad --- /dev/null +++ b/apps/mobile/src/components/modal/ImportLibraryModal.tsx @@ -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((_, ref) => { + const navigation = useNavigation>(); + 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 ( + + + {cloudLibraries.isLoading ? ( + + + + ) : ( + + } + ListEmptyComponent={ + + } + keyExtractor={(item) => item.uuid} + showsVerticalScrollIndicator={false} + renderItem={({ item }) => ( + + )} + /> + + )} + + + ); +}); + +interface Props { + data: CloudLibrary; + modalRef: React.RefObject; + navigation: NavigationProp; +} + +const CloudLibraryCard = ({ data, modalRef, navigation }: Props) => { + const rspc = useRspcContext().queryClient; + const joinLibrary = useBridgeMutation(['cloud.library.join']); + return ( + + + {data.name} + + + + ); +}; + +export default ImportModalLibrary; diff --git a/apps/mobile/src/navigation/BackfillWaitingStack.tsx b/apps/mobile/src/navigation/BackfillWaitingStack.tsx new file mode 100644 index 000000000..dcf772f23 --- /dev/null +++ b/apps/mobile/src/navigation/BackfillWaitingStack.tsx @@ -0,0 +1,26 @@ +import { createNativeStackNavigator, NativeStackScreenProps } from '@react-navigation/native-stack'; +import React from 'react'; +import BackfillWaiting from '~/screens/BackfillWaiting'; + +const Stack = createNativeStackNavigator(); + +export default function BackfillWaitingStack() { + return ( + + + + ); +} + +export type BackfillWaitingStackParamList = { + BackfillWaiting: undefined; +}; + +export type BackfillWaitingStackScreenProps = + NativeStackScreenProps; diff --git a/apps/mobile/src/navigation/index.tsx b/apps/mobile/src/navigation/index.tsx index 68bbb2630..39e9d190a 100644 --- a/apps/mobile/src/navigation/index.tsx +++ b/apps/mobile/src/navigation/index.tsx @@ -4,6 +4,7 @@ import NotFoundScreen from '~/screens/NotFound'; import DrawerNavigator, { DrawerNavParamList } from './DrawerNavigator'; import SearchStack, { SearchStackParamList } from './SearchStack'; +import BackfillWaitingStack, { BackfillWaitingStackParamList } from './BackfillWaitingStack'; const Stack = createNativeStackNavigator(); // This is the main navigator we nest everything under. @@ -20,6 +21,11 @@ export default function RootNavigator() { component={SearchStack} options={{ headerShown: false }} /> + ); @@ -28,6 +34,7 @@ export default function RootNavigator() { export type RootStackParamList = { Root: NavigatorScreenParams; SearchStack: NavigatorScreenParams; + BackfillWaitingStack: NavigatorScreenParams; NotFound: undefined; }; diff --git a/apps/mobile/src/navigation/tabs/SettingsStack.tsx b/apps/mobile/src/navigation/tabs/SettingsStack.tsx index 36d12cd35..8fb152147 100644 --- a/apps/mobile/src/navigation/tabs/SettingsStack.tsx +++ b/apps/mobile/src/navigation/tabs/SettingsStack.tsx @@ -12,10 +12,12 @@ 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'; import NodesSettingsScreen from '~/screens/settings/library/NodesSettings'; +import SyncSettingsScreen from '~/screens/settings/library/SyncSettings'; import TagsSettingsScreen from '~/screens/settings/library/TagsSettings'; import SettingsScreen from '~/screens/settings/Settings'; @@ -87,6 +89,16 @@ export default function SettingsStack() { component={TagsSettingsScreen} options={{ header: () =>
}} /> +
}} + /> +
}} + /> {/* { + 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 ( + + + + + + + + + + + + + + + Library is being backfilled right now for Sync! + Please hold + while this process takes place. + + + ); +}; + +export default BackfillWaiting; diff --git a/apps/mobile/src/screens/settings/Settings.tsx b/apps/mobile/src/screens/settings/Settings.tsx index 613f3d2ef..900b6696f 100644 --- a/apps/mobile/src/screens/settings/Settings.tsx +++ b/apps/mobile/src/screens/settings/Settings.tsx @@ -1,5 +1,8 @@ +import { DebugState, useDebugState, useDebugStateEnabler } from '@sd/client'; import { + ArrowsClockwise, Books, + Cloud, FlyingSaucer, Gear, GearSix, @@ -14,7 +17,6 @@ import { } from 'phosphor-react-native'; import React from 'react'; import { Platform, SectionList, Text, TouchableWithoutFeedback, View } from 'react-native'; -import { DebugState, useDebugState, useDebugStateEnabler } from '@sd/client'; import ScreenContainer from '~/components/layout/ScreenContainer'; import { SettingsItem } from '~/components/settings/SettingsItem'; import { tw, twStyle } from '~/lib/tailwind'; @@ -86,6 +88,16 @@ const sections: (debugState: DebugState) => SectionType[] = (debugState) => [ icon: TagSimple, navigateTo: 'TagsSettings', title: 'Tags', + }, + { + icon: Cloud, + navigateTo: 'CloudSettings', + title: 'Cloud', + }, + { + icon: ArrowsClockwise, + navigateTo: 'SyncSettings', + title: 'Sync', rounded: 'bottom' } // { diff --git a/apps/mobile/src/screens/settings/client/GeneralSettings.tsx b/apps/mobile/src/screens/settings/client/GeneralSettings.tsx index bcfa70823..ca7e6df26 100644 --- a/apps/mobile/src/screens/settings/client/GeneralSettings.tsx +++ b/apps/mobile/src/screens/settings/client/GeneralSettings.tsx @@ -1,5 +1,5 @@ -import { Text, View } from 'react-native'; import { useBridgeQuery, useDebugState } from '@sd/client'; +import { Text, View } from 'react-native'; import Card from '~/components/layout/Card'; import ScreenContainer from '~/components/layout/ScreenContainer'; import { Divider } from '~/components/primitive/Divider'; diff --git a/apps/mobile/src/screens/settings/info/Debug.tsx b/apps/mobile/src/screens/settings/info/Debug.tsx index 83a5f5973..994aae7c0 100644 --- a/apps/mobile/src/screens/settings/info/Debug.tsx +++ b/apps/mobile/src/screens/settings/info/Debug.tsx @@ -1,6 +1,14 @@ +import { useQueryClient } from '@tanstack/react-query'; import React from 'react'; 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 { Button } from '~/components/primitive/Button'; import { tw } from '~/lib/tailwind'; @@ -9,6 +17,10 @@ import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack'; const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => { const debugState = useDebugState(); const featureFlags = useFeatureFlags(); + const origin = useBridgeQuery(['cloud.getApiOrigin']); + const setOrigin = useBridgeMutation(['cloud.setApiOrigin']); + + const queryClient = useQueryClient(); return ( @@ -28,6 +40,37 @@ const DebugScreen = ({ navigation }: SettingsStackScreenProps<'Debug'>) => { > Disable Debug Mode + + + ); diff --git a/apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx b/apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx new file mode 100644 index 000000000..e26e7a7f5 --- /dev/null +++ b/apps/mobile/src/screens/settings/library/CloudSettings/CloudSettings.tsx @@ -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 ( + + + + ); +}; + +const AuthSensitiveChild = () => { + const authState = useAuthStateSnapshot(); + if (authState.status === 'loggedIn') return ; + if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn') return ; + + 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 ( + + + + ); + } + + return ( + + {cloudLibrary.data ? ( + + + + + + + + + {cloudInstances?.length} + + + Instances + + + + + } + contentContainerStyle={twStyle( + cloudInstances?.length === 0 && 'flex-row' + )} + showsHorizontalScrollIndicator={false} + ItemSeparatorComponent={() => } + renderItem={({ item }) => ( + + )} + keyExtractor={(item) => item.id} + numColumns={(cloudInstances?.length ?? 0) > 1 ? 2 : 1} + {...((cloudInstances?.length ?? 0) > 1 + ? { columnWrapperStyle: tw`w-full justify-between` } + : {})} + /> + + + + ) : ( + + + + )} + + ); +}; + +export default CloudSettings; diff --git a/apps/mobile/src/screens/settings/library/CloudSettings/Instance.tsx b/apps/mobile/src/screens/settings/library/CloudSettings/Instance.tsx new file mode 100644 index 000000000..6ae58e587 --- /dev/null +++ b/apps/mobile/src/screens/settings/library/CloudSettings/Instance.tsx @@ -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 ( + 1 ? 'w-[49%]' : 'w-full', 'gap-4')}> + + Id + + + {data.id} + + + + + UUID + + + {data.uuid} + + + + + Public Key + + + {data.identity} + + + + + ); +}; + +export default Instance; diff --git a/apps/mobile/src/screens/settings/library/CloudSettings/Library.tsx b/apps/mobile/src/screens/settings/library/CloudSettings/Library.tsx new file mode 100644 index 000000000..965b5be27 --- /dev/null +++ b/apps/mobile/src/screens/settings/library/CloudSettings/Library.tsx @@ -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 ( + + + Library + {authState.status === 'loggedIn' && ( + + )} + + + Name + + {cloudLibrary?.name} + + + + ); +}; + +export default Library; diff --git a/apps/mobile/src/screens/settings/library/CloudSettings/Login.tsx b/apps/mobile/src/screens/settings/library/CloudSettings/Login.tsx new file mode 100644 index 000000000..f993c968f --- /dev/null +++ b/apps/mobile/src/screens/settings/library/CloudSettings/Login.tsx @@ -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 ( + + + + To access cloud related features, please login + + {(authState.status === 'notLoggedIn' || authState.status === 'loggingIn') && ( + + )} + + + ); +}; + +export default Login; diff --git a/apps/mobile/src/screens/settings/library/CloudSettings/ThisInstance.tsx b/apps/mobile/src/screens/settings/library/CloudSettings/ThisInstance.tsx new file mode 100644 index 000000000..1a2ab3697 --- /dev/null +++ b/apps/mobile/src/screens/settings/library/CloudSettings/ThisInstance.tsx @@ -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 ( + + + This Instance + + + + Id + + {thisInstance.id} + + + + UUID + + {thisInstance.uuid} + + + + Public Key + + + {thisInstance.identity} + + + + + ); +}; + +export default ThisInstance; diff --git a/apps/mobile/src/screens/settings/library/SyncSettings.tsx b/apps/mobile/src/screens/settings/library/SyncSettings.tsx new file mode 100644 index 000000000..c2ac34dfa --- /dev/null +++ b/apps/mobile/src/screens/settings/library/SyncSettings.tsx @@ -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>({}); + + const [startBackfill, setStart] = useState(false); + + useLibrarySubscription(['library.actors'], { onData: setData }); + + useEffect(() => { + if (startBackfill === true) { + navigation.navigate('BackfillWaitingStack', { + screen: 'BackfillWaiting' + }); + } + }, [startBackfill, navigation]); + + return ( + + {syncEnabled.data === false ? ( + + + + + + ) : ( + + {Object.keys(data).map((key) => { + return ( + + + + {key} + + {data[key] ? : } + + ); + })} + + )} + + ); +}; + +export default SyncSettingsScreen; + +function OnlineIndicator({ online }: { online: boolean }) { + const size = 6; + return ( + + {online ? ( + + + + + ) : ( + + )} + + ); +} + +function StartButton({ name }: { name: string }) { + const startActor = useLibraryMutation(['library.startActor']); + return ( + + ); +} + +function StopButton({ name }: { name: string }) { + const stopActor = useLibraryMutation(['library.stopActor']); + return ( + + ); +} diff --git a/apps/mobile/src/stores/auth.ts b/apps/mobile/src/stores/auth.ts new file mode 100644 index 000000000..665a5b3d9 --- /dev/null +++ b/apps/mobile/src/stores/auth.ts @@ -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({ + 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((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' }; +} diff --git a/core/src/api/auth.rs b/core/src/api/auth.rs index 79a672514..52673323e 100644 --- a/core/src/api/auth.rs +++ b/core/src/api/auth.rs @@ -32,13 +32,21 @@ pub(crate) fn mount() -> AlphaRouter { } 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 .http .post(&format!( "{}/login/device/code", &node.env.api_url.lock().await )) - .form(&[("client_id", &node.env.client_id)]) + .form(&[("client_id", &node.env.client_id), ("device", &device_type)]) .send() .await .map_err(|e| e.to_string()) diff --git a/core/src/lib.rs b/core/src/lib.rs index cdfda558b..48f131116 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -141,7 +141,9 @@ impl Node { config, event_bus, 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(), env, #[cfg(feature = "ai")] diff --git a/docs/product/guides/folder-sync.mdx b/docs/product/guides/folder-sync.mdx deleted file mode 100644 index 18f1b64a5..000000000 --- a/docs/product/guides/folder-sync.mdx +++ /dev/null @@ -1,5 +0,0 @@ ---- -index: 100 ---- - -# Folder Sync diff --git a/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/LibrariesDropdown.tsx b/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/LibrariesDropdown.tsx index 50ed7c3c3..5ba210717 100644 --- a/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/LibrariesDropdown.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/SidebarLayout/LibrariesDropdown.tsx @@ -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 { useClientContext } from '@sd/client'; import { dialogManager, Dropdown, DropdownMenu } from '@sd/ui'; @@ -6,6 +6,7 @@ import { useLocale } from '~/hooks'; import CreateDialog from '../../../settings/node/libraries/CreateDialog'; import { useSidebarContext } from './Context'; +import JoinDialog from '~/app/$libraryId/settings/node/libraries/JoinDialog'; export default () => { const { library, libraries, currentLibraryId } = useClientContext(); @@ -62,6 +63,13 @@ export default () => { onClick={() => dialogManager.create((dp) => )} className="font-medium" /> + dialogManager.create((dp) => )} + className="font-medium" + /> { useRouteTitle('Cloud'); @@ -12,82 +24,203 @@ export const Component = () => { const authSensitiveChild = () => { if (authState.status === 'loggedIn') return ; if (authState.status === 'notLoggedIn' || authState.status === 'loggingIn') - return ; + return ( +
+ +

To access cloud related features, please login

+ +
+
+ ); return null; }; - return
{authSensitiveChild()}
; + return
{authSensitiveChild()}
; }; +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 { t } = useLocale(); - 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 ( - <> + + + + } + > {cloudLibrary.data ? ( -
-
-

Library

-

Name: {cloudLibrary.data.name}

-
- - - - {thisInstance && ( -
-

This Instance

-

Id: {thisInstance.id}

-

UUID: {thisInstance.uuid}

-

Public Key: {thisInstance.identity}

-
- )} -
-

Instances

-
    - {cloudLibrary.data.instances - .filter((instance) => instance.uuid !== library.instance_id) - .map((instance) => ( -
  • -

    Id: {instance.id}

    -

    UUID: {instance.uuid}

    -

    Public Key: {instance.identity}

    -
  • - ))} -
-
+
+ + {thisInstance && } +
) : ( -
+
)} - + ); } + +const Instances = ({ instances }: { instances: CloudInstance[] }) => { + const { library } = useLibraryContext(); + const filteredInstances = instances.filter((instance) => instance.uuid !== library.instance_id); + return ( +
+
+

Instances

+ {filteredInstances.length} +
+
+ {filteredInstances.map((instance) => ( + +
+ +

+ {instance.metadata.name} +

+
+
+ +

+ Id:{' '} + {instance.id} +

+
+ +

+ UUID:{' '} + + {instance.uuid} + +

+
+ +

+ Public Key:{' '} + + {instance.identity} + +

+
+
+
+ ))} +
+
+ ); +}; + +interface LibraryProps { + cloudLibrary: CloudLibrary; + thisInstance: CloudInstance | undefined; +} + +const Library = ({ thisInstance, cloudLibrary }: LibraryProps) => { + const syncLibrary = useLibraryMutation(['cloud.library.sync']); + return ( +
+

Library

+ +

+ Name: {cloudLibrary.name} +

+ +
+
+ ); +}; + +interface ThisInstanceProps { + instance: CloudInstance; +} + +const ThisInstance = ({ instance }: ThisInstanceProps) => { + return ( +
+

This Instance

+ +
+ +

+ {instance.metadata.name} +

+
+
+ +

+ Id: {instance.id} +

+
+ +

+ UUID: {instance.uuid} +

+
+ +

+ Public Key:{' '} + {instance.identity} +

+
+
+
+
+ ); +}; diff --git a/interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx b/interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx new file mode 100644 index 000000000..b32fd7109 --- /dev/null +++ b/interface/app/$libraryId/settings/node/libraries/JoinDialog.tsx @@ -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 ( + +
+ {cloudLibraries.isLoading && {t('loading')}...} + {cloudLibraries.data && ( + + )} +
+
+ ); +}; diff --git a/interface/app/$libraryId/settings/node/libraries/index.tsx b/interface/app/$libraryId/settings/node/libraries/index.tsx index 517cf59c2..dc8ff541b 100644 --- a/interface/app/$libraryId/settings/node/libraries/index.tsx +++ b/interface/app/$libraryId/settings/node/libraries/index.tsx @@ -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 { useLocale } from '~/hooks'; import { Heading } from '../../Layout'; import CreateDialog from './CreateDialog'; +import JoinDialog from './JoinDialog'; import ListItem from './ListItem'; export const Component = () => { const librariesQuery = useBridgeQuery(['library.list']); const libraries = librariesQuery.data; + const cloudEnabled = useFeatureFlag('cloudSync'); + const { library } = useLibraryContext(); + const { libraries: librariesCtx } = useClientContext(); + const librariesCtxData = librariesCtx.data; const { t } = useLocale(); @@ -30,10 +35,22 @@ export const Component = () => { > {t('add_library')} + {cloudEnabled && ( + + )}
} /> -
{libraries ?.sort((a, b) => { diff --git a/interface/app/onboarding/join-library.tsx b/interface/app/onboarding/join-library.tsx index 4ed91c0f0..917eaaebe 100644 --- a/interface/app/onboarding/join-library.tsx +++ b/interface/app/onboarding/join-library.tsx @@ -27,7 +27,7 @@ export function JoinLibrary() {
Cloud Libraries -
    +
    diff --git a/interface/locales/ar/common.json b/interface/locales/ar/common.json index 2cca3cd02..bdc3c88b9 100644 --- a/interface/locales/ar/common.json +++ b/interface/locales/ar/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Connect your cloud accounts to Spacedrive.", "connect_device": "Connect a device", "connect_device_description": "Spacedrive works best on all your devices.", + "connect_library_to_cloud": "قم بتوصيل المكتبة بـ Spacedrive Cloud", "connected": "متصل", + "connecting_library_to_cloud": "جارٍ توصيل المكتبة بـ Spacedrive Cloud...", "contacts": "جهات الاتصال", "contacts_description": "إدارة جهات الاتصال الخاصة بك في Spacedrive.", "contains": "contains", diff --git a/interface/locales/be/common.json b/interface/locales/be/common.json index db3c1e119..2a0c79f26 100644 --- a/interface/locales/be/common.json +++ b/interface/locales/be/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Падключыце воблачныя акаўнты да Spacedrive.", "connect_device": "Падключыце прыладу", "connect_device_description": "Spacedrive лепш за ўсё працуе пры выкарыстанні на ўсіх вашых прыладах.", + "connect_library_to_cloud": "Падключыце бібліятэку да Spacedrive Cloud", "connected": "Падключана", + "connecting_library_to_cloud": "Падключэнне бібліятэкі да Spacedrive Cloud...", "contacts": "Кантакты", "contacts_description": "Кіруйце кантактамі ў Spacedrive.", "contains": "змяшчае", diff --git a/interface/locales/de/common.json b/interface/locales/de/common.json index 4c473f773..ebc769556 100644 --- a/interface/locales/de/common.json +++ b/interface/locales/de/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Verbinde deine Cloud-Konten mit Spacedrive.", "connect_device": "Ein Gerät anschließen", "connect_device_description": "Spacedrive funktioniert am besten auf all deinen Geräten.", + "connect_library_to_cloud": "Bibliothek mit Spacedrive Cloud verbinden", "connected": "Verbunden", + "connecting_library_to_cloud": "Bibliothek mit Spacedrive Cloud verbinden …", "contacts": "Kontakte", "contacts_description": "Verwalte deine Kontakte in Spacedrive.", "contains": "enthält", diff --git a/interface/locales/en/common.json b/interface/locales/en/common.json index 5d0b0c7f3..7167481ba 100644 --- a/interface/locales/en/common.json +++ b/interface/locales/en/common.json @@ -90,7 +90,9 @@ "connect_cloud_description": "Connect your cloud accounts to Spacedrive.", "connect_device": "Connect a device", "connect_device_description": "Spacedrive works best on all your devices.", + "connect_library_to_cloud": "Connect library to Spacedrive Cloud", "connected": "Connected", + "connecting_library_to_cloud": "Connecting library to Spacedrive Cloud...", "contacts": "Contacts", "contacts_description": "Manage your contacts in Spacedrive.", "contains": "contains", @@ -356,6 +358,7 @@ "join_discord": "Join Discord", "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.", + "joining": "Joining", "key": "Key", "key_manager": "Key Manager", "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.", "see_less": "See less", "see_more": "See more", + "select_library": "Select a Cloud Library", "send": "Send", "send_report": "Send Report", "sender": "Sender", diff --git a/interface/locales/es/common.json b/interface/locales/es/common.json index 9152389f7..2051ce55c 100644 --- a/interface/locales/es/common.json +++ b/interface/locales/es/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Conecta tus cuentas en la nube a Spacedrive.", "connect_device": "Conectar un dispositivo", "connect_device_description": "Spacedrive funciona mejor en todos tus dispositivos.", + "connect_library_to_cloud": "Conecte la biblioteca a Spacedrive Cloud", "connected": "Conectado", + "connecting_library_to_cloud": "Conectando la biblioteca a Spacedrive Cloud...", "contacts": "Contactos", "contacts_description": "Administra tus contactos en Spacedrive.", "contains": "contiene", diff --git a/interface/locales/fr/common.json b/interface/locales/fr/common.json index 4a741c9b0..936d712f0 100644 --- a/interface/locales/fr/common.json +++ b/interface/locales/fr/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Connectez vos comptes cloud à Spacedrive.", "connect_device": "Connecter un appareil", "connect_device_description": "Spacedrive fonctionne mieux sur tous vos appareils.", + "connect_library_to_cloud": "Connecter la bibliothèque à Spacedrive Cloud", "connected": "Connecté", + "connecting_library_to_cloud": "Connexion de la bibliothèque à Spacedrive Cloud...", "contacts": "Contacts", "contacts_description": "Gérez vos contacts dans Spacedrive.", "contains": "contient", diff --git a/interface/locales/it/common.json b/interface/locales/it/common.json index 137d38728..24d1164c6 100644 --- a/interface/locales/it/common.json +++ b/interface/locales/it/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Collegate i vostri account cloud a Spacedrive.", "connect_device": "Collegare un dispositivo", "connect_device_description": "Spacedrive funziona al meglio su tutti i dispositivi.", + "connect_library_to_cloud": "Connetti la libreria a Spacedrive Cloud", "connected": "Connesso", + "connecting_library_to_cloud": "Collegamento della libreria a Spacedrive Cloud...", "contacts": "Contatti", "contacts_description": "Gestisci i tuoi contatti su Spacedrive.", "contains": "contiene", diff --git a/interface/locales/ja/common.json b/interface/locales/ja/common.json index 51f6cf0b3..89a6bc18a 100644 --- a/interface/locales/ja/common.json +++ b/interface/locales/ja/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "クラウドアカウントをSpacedriveに接続する。", "connect_device": "デバイスを接続する", "connect_device_description": "Spacedriveはすべてのデバイスで最適に機能します。", + "connect_library_to_cloud": "ライブラリをSpacedrive Cloudに接続する", "connected": "接続中", + "connecting_library_to_cloud": "ライブラリを Spacedrive Cloud に接続しています...", "contacts": "連絡先", "contacts_description": "Spacedriveで連絡先を管理。", "contains": "が次を含む", diff --git a/interface/locales/nl/common.json b/interface/locales/nl/common.json index 74a0be05a..56ec29a97 100644 --- a/interface/locales/nl/common.json +++ b/interface/locales/nl/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Verbind uw cloudaccounts met Spacedrive.", "connect_device": "Een apparaat aansluiten", "connect_device_description": "Spacedrive werkt het beste op al uw apparaten.", + "connect_library_to_cloud": "Verbind de bibliotheek met Spacedrive Cloud", "connected": "Verbonden", + "connecting_library_to_cloud": "Bibliotheek verbinden met Spacedrive Cloud...", "contacts": "Contacten", "contacts_description": "Beheer je contacten in Spacedrive.", "contains": "bevatten", diff --git a/interface/locales/ru/common.json b/interface/locales/ru/common.json index a4fc6c6a4..16cff4359 100644 --- a/interface/locales/ru/common.json +++ b/interface/locales/ru/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Подключите облачные аккаунты к Spacedrive.", "connect_device": "Подключите устройство", "connect_device_description": "Spacedrive лучше всего работает при использовании на всех ваших устройствах.", + "connect_library_to_cloud": "Подключите библиотеку к Spacedrive Cloud", "connected": "Подключено", + "connecting_library_to_cloud": "Подключение библиотеки к Spacedrive Cloud...", "contacts": "Контакты", "contacts_description": "Управляйте контактами в Spacedrive.", "contains": "содержит", diff --git a/interface/locales/tr/common.json b/interface/locales/tr/common.json index a132d005d..35319e6f2 100644 --- a/interface/locales/tr/common.json +++ b/interface/locales/tr/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "Bulut hesaplarınızı Spacedrive'a 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_library_to_cloud": "Kitaplığı Spacedrive Cloud'a bağlayın", "connected": "Bağlı", + "connecting_library_to_cloud": "Kitaplık Spacedrive Cloud'a bağlanıyor...", "contacts": "Kişiler", "contacts_description": "Kişilerinizi Spacedrive'da yönetin.", "contains": "içerir", diff --git a/interface/locales/zh-CN/common.json b/interface/locales/zh-CN/common.json index 22b448f28..08ed5797f 100644 --- a/interface/locales/zh-CN/common.json +++ b/interface/locales/zh-CN/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "将您的云帐户连接到 Spacedrive。", "connect_device": "连接设备", "connect_device_description": "Spacedrive 在您的所有设备上都能发挥最佳效果。", + "connect_library_to_cloud": "将图书馆连接到 Spacedrive Cloud", "connected": "已连接", + "connecting_library_to_cloud": "将图书馆连接到 Spacedrive Cloud...", "contacts": "联系人", "contacts_description": "在 Spacedrive 中管理您的联系人。", "contains": "包含", diff --git a/interface/locales/zh-TW/common.json b/interface/locales/zh-TW/common.json index 9ba476984..22746df81 100644 --- a/interface/locales/zh-TW/common.json +++ b/interface/locales/zh-TW/common.json @@ -89,7 +89,9 @@ "connect_cloud_description": "將您的雲端帳戶連接到 Spacedrive。", "connect_device": "連接裝置", "connect_device_description": "Spacedrive 在您的所有裝置上都能發揮最佳效果。", + "connect_library_to_cloud": "將圖書館連接到 Spacedrive Cloud", "connected": "已連接", + "connecting_library_to_cloud": "正在將圖書館連接到 Spacedrive Cloud...", "contacts": "聯繫人", "contacts_description": "在Spacedrive中管理您的聯繫人。", "contains": "包含", diff --git a/interface/util/hardware.ts b/interface/util/hardware.ts index a4c37c20e..99d6691ce 100644 --- a/interface/util/hardware.ts +++ b/interface/util/hardware.ts @@ -8,6 +8,10 @@ export function hardwareModelToIcon(hardwareModel: HardwareModel) { return 'Laptop'; case 'MacStudio': return 'SilverBox'; + case 'IPhone': + return 'Mobile'; + case 'Android': + return 'MobileAndroid'; case 'MacMini': return 'MiniSilverBox'; case 'Other': diff --git a/packages/client/src/stores/auth.ts b/packages/client/src/stores/auth.ts index 2cdab08a7..d261fc845 100644 --- a/packages/client/src/stores/auth.ts +++ b/packages/client/src/stores/auth.ts @@ -41,7 +41,7 @@ function onError(error: string) { loginCallbacks.forEach((cb) => cb({ error })); } -export function login(config: ProviderConfig) { +export async function login(config: ProviderConfig) { if (store.state.status !== 'notLoggedIn') return; store.state = { status: 'loggingIn' }; @@ -86,10 +86,10 @@ export function login(config: ProviderConfig) { }); } -export function logout() { +export async function logout() { store.state = { status: 'loggingOut' }; - nonLibraryClient.mutation(['auth.logout']); - nonLibraryClient.query(['auth.me']); + await nonLibraryClient.mutation(['auth.logout']); + await nonLibraryClient.query(['auth.me']); store.state = { status: 'notLoggedIn' }; } diff --git a/packages/ui/src/Select.tsx b/packages/ui/src/Select.tsx index c6f959d2a..bed7b6d71 100644 --- a/packages/ui/src/Select.tsx +++ b/packages/ui/src/Select.tsx @@ -65,7 +65,7 @@ export const Select = forwardRef( - + {props.children}