diff --git a/apps/mobile/package.json b/apps/mobile/package.json
index f915311a1..9b431525d 100644
--- a/apps/mobile/package.json
+++ b/apps/mobile/package.json
@@ -29,6 +29,7 @@
"expo-linking": "~3.2.2",
"expo-splash-screen": "~0.16.2",
"expo-status-bar": "~1.4.0",
+ "immer": "^9.0.15",
"intl": "^1.2.5",
"moti": "^0.18.0",
"phosphor-react-native": "^1.1.2",
diff --git a/apps/mobile/pnpm-lock.yaml b/apps/mobile/pnpm-lock.yaml
index 5fce41495..f13f916a4 100644
Binary files a/apps/mobile/pnpm-lock.yaml and b/apps/mobile/pnpm-lock.yaml differ
diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx
index 14794ffd7..04e70cb9f 100644
--- a/apps/mobile/src/App.tsx
+++ b/apps/mobile/src/App.tsx
@@ -8,12 +8,19 @@ import { SafeAreaProvider } from 'react-native-safe-area-context';
import { useDeviceContext } from 'twrnc';
import { GlobalModals } from './components/modals/GlobalModals';
-import { ReactNativeTransport, queryClient, rspc, useInvalidateQuery } from './hooks/rspc';
+import {
+ ReactNativeTransport,
+ queryClient,
+ rspc,
+ useBridgeQuery,
+ useInvalidateQuery
+} from './hooks/rspc';
import useCachedResources from './hooks/useCachedResources';
import { getItemFromStorage } from './lib/storage';
import tw from './lib/tailwind';
import RootNavigator from './navigation';
import OnboardingNavigator from './navigation/OnboardingNavigator';
+import { useLibraryStore } from './stores/useLibraryStore';
import { useOnboardingStore } from './stores/useOnboardingStore';
import type { Operations } from './types/bindings';
@@ -29,7 +36,7 @@ const NavigatorTheme: Theme = {
}
};
-export default function App() {
+function AppContainer() {
// Enables dark mode, and screen size breakpoints, etc. for tailwind
useDeviceContext(tw, { withDeviceColorScheme: false });
@@ -37,38 +44,62 @@ export default function App() {
const { showOnboarding, hideOnboarding } = useOnboardingStore();
+ const { data: libraries } = useBridgeQuery(['library.get']);
+
+ const { switchLibrary, _hasHydrated } = useLibraryStore();
+
// Runs when the app is launched
useEffect(() => {
- getItemFromStorage('@onboarding').then((value) => {
- value && hideOnboarding();
- });
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
+ async function appLaunch() {
+ // Check if the user went through onboarding
+ const didOnboarding = await getItemFromStorage('@onboarding');
+ // If user did do onboarding, that means they've already have a library
- if (!isLoadingComplete) {
+ // Temporarly set the first library to be the current library
+ if (libraries && libraries.length > 0) {
+ switchLibrary(libraries[0].uuid);
+ }
+
+ if (didOnboarding) {
+ hideOnboarding();
+ }
+ }
+
+ appLaunch();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [libraries]);
+
+ // Might need to move _hasHydrated to useCacheResources hook.
+ if (!isLoadingComplete && !_hasHydrated) {
return null;
} else {
return (
-
- <>
-
-
-
-
-
-
- {showOnboarding ? : }
-
-
-
-
-
- >
-
+
+
+
+
+
+ {showOnboarding ? : }
+
+
+
+
+
);
}
}
+export default function App() {
+ return (
+
+ <>
+
+
+ >
+
+ );
+}
+
function InvalidateQuery() {
useInvalidateQuery();
return null;
diff --git a/apps/mobile/src/components/drawer/DrawerContent.tsx b/apps/mobile/src/components/drawer/DrawerContent.tsx
index 5b7ac971f..f60f0cc64 100644
--- a/apps/mobile/src/components/drawer/DrawerContent.tsx
+++ b/apps/mobile/src/components/drawer/DrawerContent.tsx
@@ -1,14 +1,14 @@
import Layout from '@app/constants/Layout';
import tw from '@app/lib/tailwind';
+import { useCurrentLibrary, useLibraryStore } from '@app/stores/useLibraryStore';
import { DrawerContentScrollView } from '@react-navigation/drawer';
import { DrawerContentComponentProps } from '@react-navigation/drawer/lib/typescript/src/types';
import { getFocusedRouteNameFromRoute } from '@react-navigation/native';
-import React from 'react';
+import React, { useEffect } from 'react';
import { ColorValue, Platform, Pressable, Text, View } from 'react-native';
import { CogIcon } from 'react-native-heroicons/solid';
import CollapsibleView from '../layout/CollapsibleView';
-import { Tooltip } from '../tooltip/Tooltip';
import DrawerLocationItem from './DrawerLocationItem';
import DrawerLogo from './DrawerLogo';
import DrawerTagItem from './DrawerTagItem';
@@ -57,12 +57,22 @@ const getActiveRouteState = function (state: any) {
const DrawerContent = ({ navigation, state }: DrawerContentComponentProps) => {
const stackName = getFocusedRouteNameFromRoute(getActiveRouteState(state)) ?? 'OverviewStack';
+ // initialize libraries
+ const { init: initLibraries, switchLibrary } = useLibraryStore();
+ const { currentLibrary, libraries, currentLibraryUuid } = useCurrentLibrary();
+
+ useEffect(() => {
+ if (libraries && !currentLibraryUuid) initLibraries(libraries);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [libraries, currentLibraryUuid]);
+
return (
TODO: Library Selection
+ {libraries[0].config.name}
{/* Locations */}
{
{/* Settings */}
-
- navigation.navigate('Settings')}>
-
-
-
+ navigation.navigate('Settings')}>
+
+
);
diff --git a/apps/mobile/src/components/primitive/Input.tsx b/apps/mobile/src/components/primitive/Input.tsx
index e69de29bb..4b92cb74e 100644
--- a/apps/mobile/src/components/primitive/Input.tsx
+++ b/apps/mobile/src/components/primitive/Input.tsx
@@ -0,0 +1,32 @@
+import tw from '@app/lib/tailwind';
+import { VariantProps, cva } from 'class-variance-authority';
+import React from 'react';
+import { TextInput as RNTextInput, TextInputProps as RNTextInputProps } from 'react-native';
+
+const input = cva(['text-sm rounded-md border shadow-sm'], {
+ variants: {
+ variant: {
+ default: 'bg-gray-550 border-gray-500 text-white'
+ },
+ size: {
+ default: ['py-2', 'px-3']
+ }
+ },
+ defaultVariants: {
+ variant: 'default',
+ size: 'default'
+ }
+});
+
+type InputProps = VariantProps & RNTextInputProps;
+
+export const TextInput: React.FC = ({ variant, ...props }) => {
+ const { style, ...otherProps } = props;
+ return (
+
+ );
+};
diff --git a/apps/mobile/src/navigation/DrawerNavigator.tsx b/apps/mobile/src/navigation/DrawerNavigator.tsx
index 30e298b36..621a75222 100644
--- a/apps/mobile/src/navigation/DrawerNavigator.tsx
+++ b/apps/mobile/src/navigation/DrawerNavigator.tsx
@@ -13,19 +13,15 @@ export default function DrawerNavigator() {
return (
{
- return {
- headerShown: false,
- drawerStyle: {
- backgroundColor: '#08090D',
- width: '75%'
- },
- overlayColor: 'transparent',
- drawerType: 'slide'
- // swipeEnabled: false
- // drawerHideStatusBarOnOpen: true,
- // drawerStatusBarAnimation: 'slide'
- };
+ screenOptions={{
+ headerShown: false,
+ drawerStyle: {
+ backgroundColor: '#08090D',
+ width: '75%'
+ },
+ overlayColor: 'transparent',
+ drawerType: 'slide',
+ swipeEdgeWidth: 50
}}
drawerContent={(props) => }
>
diff --git a/apps/mobile/src/navigation/OnboardingNavigator.tsx b/apps/mobile/src/navigation/OnboardingNavigator.tsx
index 0a2b07924..96b7f1a04 100644
--- a/apps/mobile/src/navigation/OnboardingNavigator.tsx
+++ b/apps/mobile/src/navigation/OnboardingNavigator.tsx
@@ -1,3 +1,4 @@
+import CreateLibraryScreen from '@app/screens/onboarding/CreateLibrary';
import OnboardingScreen from '@app/screens/onboarding/Onboarding';
import { StackScreenProps, createStackNavigator } from '@react-navigation/stack';
@@ -7,12 +8,14 @@ export default function OnboardingNavigator() {
return (
+
);
}
export type OnboardingStackParamList = {
Onboarding: undefined;
+ CreateLibrary: undefined;
};
export type OnboardingStackScreenProps =
diff --git a/apps/mobile/src/navigation/index.tsx b/apps/mobile/src/navigation/index.tsx
index 2dabc5711..f36c514eb 100644
--- a/apps/mobile/src/navigation/index.tsx
+++ b/apps/mobile/src/navigation/index.tsx
@@ -15,7 +15,11 @@ export default function RootNavigator() {
-
+
{/* Modals */}
diff --git a/apps/mobile/src/navigation/tabs/OverviewStack.tsx b/apps/mobile/src/navigation/tabs/OverviewStack.tsx
index 2245402eb..e1ed17a9d 100644
--- a/apps/mobile/src/navigation/tabs/OverviewStack.tsx
+++ b/apps/mobile/src/navigation/tabs/OverviewStack.tsx
@@ -14,8 +14,7 @@ export default function OverviewStack() {
initialRouteName="Overview"
screenOptions={{
headerStyle: { backgroundColor: '#08090D' },
- headerTintColor: '#fff',
- ...TransitionPresets.ModalFadeTransition
+ headerTintColor: '#fff'
}}
>
diff --git a/apps/mobile/src/navigation/tabs/PhotosStack.tsx b/apps/mobile/src/navigation/tabs/PhotosStack.tsx
index f97e49889..c417dfc45 100644
--- a/apps/mobile/src/navigation/tabs/PhotosStack.tsx
+++ b/apps/mobile/src/navigation/tabs/PhotosStack.tsx
@@ -14,8 +14,7 @@ export default function PhotosStack() {
initialRouteName="Photos"
screenOptions={{
headerStyle: { backgroundColor: '#08090D' },
- headerTintColor: '#fff',
- ...TransitionPresets.ModalFadeTransition
+ headerTintColor: '#fff'
}}
>
diff --git a/apps/mobile/src/navigation/tabs/SpacesStack.tsx b/apps/mobile/src/navigation/tabs/SpacesStack.tsx
index ff9fe6f03..1eb9c9f13 100644
--- a/apps/mobile/src/navigation/tabs/SpacesStack.tsx
+++ b/apps/mobile/src/navigation/tabs/SpacesStack.tsx
@@ -14,8 +14,7 @@ export default function SpacesStack() {
initialRouteName="Spaces"
screenOptions={{
headerStyle: { backgroundColor: '#08090D' },
- headerTintColor: '#fff',
- ...TransitionPresets.ModalFadeTransition
+ headerTintColor: '#fff'
}}
>
diff --git a/apps/mobile/src/screens/Overview.tsx b/apps/mobile/src/screens/Overview.tsx
index f9f7f2747..9ef10e968 100644
--- a/apps/mobile/src/screens/Overview.tsx
+++ b/apps/mobile/src/screens/Overview.tsx
@@ -1,7 +1,9 @@
import Device from '@app/components/device/Device';
import Dialog from '@app/components/layout/Dialog';
import VirtualizedListWrapper from '@app/components/layout/VirtualizedListWrapper';
+import { TextInput } from '@app/components/primitive/Input';
import OverviewStats from '@app/containers/OverviewStats';
+import { useLibraryQuery } from '@app/hooks/rspc';
import tw from '@app/lib/tailwind';
import { OverviewStackScreenProps } from '@app/navigation/tabs/OverviewStack';
import React from 'react';
@@ -61,11 +63,11 @@ export default function OverviewScreen({ navigation }: OverviewStackScreenProps<
Dialog
}
- />
+ >
+
+
{/* Stats */}
- {/* Spacing */}
-
{/* Devices */}
) => {
const [loading, setLoading] = useState(false);
+ // TODO: Animations!
+
return (
{/* Header */}
{/* Search Input */}
-
+
{loading ? (
diff --git a/apps/mobile/src/screens/onboarding/CreateLibrary.tsx b/apps/mobile/src/screens/onboarding/CreateLibrary.tsx
new file mode 100644
index 000000000..00fb93cd7
--- /dev/null
+++ b/apps/mobile/src/screens/onboarding/CreateLibrary.tsx
@@ -0,0 +1,31 @@
+import { AnimatedButton } from '@app/components/primitive/Button';
+import { setItemToStorage } from '@app/lib/storage';
+import tw from '@app/lib/tailwind';
+import { OnboardingStackScreenProps } from '@app/navigation/OnboardingNavigator';
+import { useOnboardingStore } from '@app/stores/useOnboardingStore';
+import React from 'react';
+import { Text, View } from 'react-native';
+
+const CreateLibraryScreen = ({ navigation }: OnboardingStackScreenProps<'CreateLibrary'>) => {
+ const { hideOnboarding } = useOnboardingStore();
+
+ function onButtonPress() {
+ setItemToStorage('@onboarding', '1');
+ // TODO: Add a loading indicator to button as this takes a second or so.
+ hideOnboarding();
+ }
+ return (
+
+
+ Onboarding screen for users to create their first library
+
+
+
+ Create Library
+
+
+
+ );
+};
+
+export default CreateLibraryScreen;
diff --git a/apps/mobile/src/screens/onboarding/Onboarding.tsx b/apps/mobile/src/screens/onboarding/Onboarding.tsx
index f8ca2f43b..dba1dd8fb 100644
--- a/apps/mobile/src/screens/onboarding/Onboarding.tsx
+++ b/apps/mobile/src/screens/onboarding/Onboarding.tsx
@@ -1,21 +1,11 @@
import { FadeInUpAnimation, LogoAnimation } from '@app/components/animation/layout';
import { AnimatedButton } from '@app/components/primitive/Button';
-import { setItemToStorage } from '@app/lib/storage';
import tw from '@app/lib/tailwind';
import { OnboardingStackScreenProps } from '@app/navigation/OnboardingNavigator';
-import { useOnboardingStore } from '@app/stores/useOnboardingStore';
import React from 'react';
import { Image, Text, View } from 'react-native';
const OnboardingScreen = ({ navigation }: OnboardingStackScreenProps<'Onboarding'>) => {
- const { hideOnboarding } = useOnboardingStore();
-
- function onButtonPress() {
- setItemToStorage('@onboarding', '1');
- // TODO: Add a loading indicator to button as this takes a second or so.
- hideOnboarding();
- }
-
return (
{/* Logo */}
@@ -25,7 +15,7 @@ const OnboardingScreen = ({ navigation }: OnboardingStackScreenProps<'Onboarding
{/* Text */}
-
+
A file explorer from the future.
@@ -40,7 +30,7 @@ const OnboardingScreen = ({ navigation }: OnboardingStackScreenProps<'Onboarding
{/* Get Started Button */}
-
+ navigation.navigate('CreateLibrary')}>
Get Started
diff --git a/apps/mobile/src/stores/useLibraryStore.ts b/apps/mobile/src/stores/useLibraryStore.ts
index 77294c4cc..e5be837af 100644
--- a/apps/mobile/src/stores/useLibraryStore.ts
+++ b/apps/mobile/src/stores/useLibraryStore.ts
@@ -1,13 +1,73 @@
+import { useBridgeQuery } from '@app/hooks/rspc';
+import { LibraryConfigWrapped } from '@app/types/bindings';
+import AsyncStorage from '@react-native-async-storage/async-storage';
+import produce from 'immer';
+import { useMemo } from 'react';
import create from 'zustand';
+import { persist } from 'zustand/middleware';
interface LibraryStore {
+ _hasHydrated: boolean;
+ setHasHydrated: (hasHydrated: boolean) => void;
currentLibraryUuid: string | null;
switchLibrary: (id: string) => void;
+ init: (libraries: LibraryConfigWrapped[]) => Promise;
}
-export const useLibraryStore = create()((set) => ({
- currentLibraryUuid: null,
- switchLibrary: (uuid) => {
- set((state) => ({ currentLibraryUuid: uuid }));
- }
-}));
+export const useLibraryStore = create()(
+ persist(
+ (set) => ({
+ _hasHydrated: false,
+ setHasHydrated: (state) => {
+ set({ _hasHydrated: state });
+ },
+ currentLibraryUuid: null,
+ switchLibrary: (uuid) => {
+ set((state) =>
+ produce(state, (draft) => {
+ draft.currentLibraryUuid = uuid;
+ })
+ );
+ // reset other stores
+ },
+ init: async (libraries) => {
+ set((state) =>
+ produce(state, (draft) => {
+ // use first library default if none set
+ if (!state.currentLibraryUuid) {
+ draft.currentLibraryUuid = libraries[0].uuid;
+ }
+ })
+ );
+ }
+ }),
+ {
+ name: 'sd-library-store',
+ getStorage: () => AsyncStorage,
+ // Since storage is async, app needs to stay in loading state until hydrated!
+ onRehydrateStorage: () => (state) => {
+ state.setHasHydrated(true);
+ }
+ }
+ )
+);
+
+// this must be used at least once in the app to correct the initial state
+// is memorized and can be used safely in any component
+export const useCurrentLibrary = () => {
+ const { currentLibraryUuid, switchLibrary } = useLibraryStore();
+ const { data: libraries } = useBridgeQuery(['library.get']);
+
+ // memorize library to avoid re-running find function
+ const currentLibrary = useMemo(() => {
+ const current = libraries?.find((l) => l.uuid === currentLibraryUuid);
+ // switch to first library if none set
+ if (Array.isArray(libraries) && !current && libraries[0]?.uuid) {
+ switchLibrary(libraries[0]?.uuid);
+ }
+ return current;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [libraries, currentLibraryUuid]);
+
+ return { currentLibrary, libraries, currentLibraryUuid };
+};