mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 13:23:28 +00:00
[MOB-3] Small fixes and improvements (#813)
* add location button * add tag button * library manager arrow points right when open * wip create lib modal * handle .spacedrive file in location * fix location screen title * remove create lib dialog and use a modal instead * clean tsconfig.tsbuildinfo too * update some packages * modal paddings * fix onboarding animations
This commit is contained in:
parent
e2dec80a51
commit
cc8d6a3d24
6
.vscode/settings.json
vendored
6
.vscode/settings.json
vendored
|
@ -52,9 +52,9 @@
|
|||
// Hiding these folders bcs they create a lot of noise in the search results
|
||||
"**/*.contentlayer": true,
|
||||
"**/*.next": true,
|
||||
"apps/mobile/ios/Pods": true
|
||||
// "apps/mobile/android": true
|
||||
// "apps/mobile/ios": true
|
||||
"apps/mobile/ios/Pods": true,
|
||||
"apps/mobile/android": true,
|
||||
"apps/mobile/ios": true
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
"apps/desktop",
|
||||
|
|
|
@ -12,15 +12,15 @@ PODS:
|
|||
- EXMediaLibrary (15.2.3):
|
||||
- ExpoModulesCore
|
||||
- React-Core
|
||||
- Expo (48.0.10):
|
||||
- Expo (48.0.19):
|
||||
- ExpoModulesCore
|
||||
- ExpoKeepAwake (12.0.1):
|
||||
- ExpoModulesCore
|
||||
- ExpoModulesCore (1.2.6):
|
||||
- ExpoModulesCore (1.2.7):
|
||||
- React-Core
|
||||
- React-RCTAppDelegate
|
||||
- ReactCommon/turbomodule/core
|
||||
- EXSplashScreen (0.18.1):
|
||||
- EXSplashScreen (0.18.2):
|
||||
- ExpoModulesCore
|
||||
- React-Core
|
||||
- FBLazyVector (0.71.3)
|
||||
|
@ -292,7 +292,7 @@ PODS:
|
|||
- React-jsinspector (0.71.3)
|
||||
- React-logger (0.71.3):
|
||||
- glog
|
||||
- react-native-document-picker (8.2.0):
|
||||
- react-native-document-picker (8.2.1):
|
||||
- React-Core
|
||||
- react-native-safe-area-context (4.5.1):
|
||||
- RCT-Folly
|
||||
|
@ -610,10 +610,10 @@ SPEC CHECKSUMS:
|
|||
EXFileSystem: 844e86ca9b5375486ecc4ef06d3838d5597d895d
|
||||
EXFont: 6ea3800df746be7233208d80fe379b8ed74f4272
|
||||
EXMediaLibrary: 587cd8aad27a6fc8d7c38b950bc75bc1845a7480
|
||||
Expo: b04d142a2b477a391b47c62d179c1a9e1140e6ad
|
||||
Expo: 8448e3a2aa1b295f029c81551e1ab6d986517fdb
|
||||
ExpoKeepAwake: 69f5f627670d62318410392d03e0b5db0f85759a
|
||||
ExpoModulesCore: 6e0259511f4c4341b6b8357db393624df2280828
|
||||
EXSplashScreen: cd7fb052dff5ba8311d5c2455ecbebffe1b7a8ca
|
||||
ExpoModulesCore: 653958063a301098b541ae4dfed1ac0b98db607b
|
||||
EXSplashScreen: 0e0a9ba0cf7553094e93213099bd7b42e6e237e9
|
||||
FBLazyVector: 60195509584153283780abdac5569feffb8f08cc
|
||||
FBReactNativeSpec: c5a5c4f1b95ae42a17cd22c8c89c482a7b327fe3
|
||||
fmt: ff9d55029c625d3757ed641535fd4a75fedc7ce9
|
||||
|
@ -636,7 +636,7 @@ SPEC CHECKSUMS:
|
|||
React-jsiexecutor: 515b703d23ffadeac7687bc2d12fb08b90f0aaa1
|
||||
React-jsinspector: 9f7c9137605e72ca0343db4cea88006cb94856dd
|
||||
React-logger: 957e5dc96d9dbffc6e0f15e0ee4d2b42829ff207
|
||||
react-native-document-picker: 495c444c0c773c6e83a5d91165890ecb1c0a399a
|
||||
react-native-document-picker: 69ca2094d8780cfc1e7e613894d15290fdc54bba
|
||||
react-native-safe-area-context: f5549f36508b1b7497434baa0cd97d7e470920d4
|
||||
React-perflogger: af8a3d31546077f42d729b949925cc4549f14def
|
||||
React-RCTActionSheet: 57cc5adfefbaaf0aae2cf7e10bccd746f2903673
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"clean:ios": "cd ios && xcodebuild clean && cd ../"
|
||||
},
|
||||
"dependencies": {
|
||||
"@gorhom/bottom-sheet": "^4.4.5",
|
||||
"@gorhom/bottom-sheet": "^4.4.7",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@react-native-async-storage/async-storage": "~1.17.12",
|
||||
"@react-native-masked-view/masked-view": "0.2.8",
|
||||
|
@ -31,12 +31,12 @@
|
|||
"@shopify/flash-list": "1.4.2",
|
||||
"@tanstack/react-query": "^4.29.1",
|
||||
"byte-size": "^8.1.0",
|
||||
"class-variance-authority": "^0.5.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"expo": "~48.0.10",
|
||||
"class-variance-authority": "^0.5.3",
|
||||
"dayjs": "^1.11.8",
|
||||
"expo": "~48.0.19",
|
||||
"expo-linking": "~4.0.1",
|
||||
"expo-media-library": "~15.2.3",
|
||||
"expo-splash-screen": "~0.18.1",
|
||||
"expo-splash-screen": "~0.18.2",
|
||||
"expo-status-bar": "~1.4.4",
|
||||
"intl": "^1.2.5",
|
||||
"lottie-react-native": "5.1.4",
|
||||
|
@ -45,7 +45,7 @@
|
|||
"react": "18.2.0",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-native": "0.71.3",
|
||||
"react-native-document-picker": "^8.2.0",
|
||||
"react-native-document-picker": "^8.2.1",
|
||||
"react-native-fs": "^2.20.0",
|
||||
"react-native-gesture-handler": "~2.9.0",
|
||||
"react-native-popup-menu": "^0.16.1",
|
||||
|
@ -54,20 +54,20 @@
|
|||
"react-native-screens": "~3.20.0",
|
||||
"react-native-svg": "13.4.0",
|
||||
"react-native-wheel-color-picker": "^1.2.0",
|
||||
"twrnc": "^3.6.0",
|
||||
"twrnc": "^3.6.1",
|
||||
"use-count-up": "^3.0.1",
|
||||
"use-debounce": "^9.0.4",
|
||||
"valtio": "^1.10.4",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@rnx-kit/metro-config": "^1.3.5",
|
||||
"@babel/core": "^7.22.1",
|
||||
"@rnx-kit/metro-config": "^1.3.6",
|
||||
"@sd/config": "workspace:*",
|
||||
"@types/react": "~18.0.27",
|
||||
"@types/react": "~18.0.38",
|
||||
"babel-plugin-module-resolver": "^5.0.0",
|
||||
"eslint-plugin-react-native": "^4.0.0",
|
||||
"react-native-svg-transformer": "^1.0.0",
|
||||
"typescript": "^5.0.4"
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,71 +0,0 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useState } from 'react';
|
||||
import { useBridgeMutation, usePlausibleEvent } from '@sd/client';
|
||||
import { Input } from '~/components/form/Input';
|
||||
import Dialog from '~/components/layout/Dialog';
|
||||
import { currentLibraryStore } from '~/utils/nav';
|
||||
|
||||
type Props = {
|
||||
onSubmit?: () => void;
|
||||
disableBackdropClose?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
// TODO: Move to a Modal component
|
||||
const CreateLibraryDialog = ({ children, onSubmit, disableBackdropClose }: Props) => {
|
||||
const queryClient = useQueryClient();
|
||||
const [libName, setLibName] = useState('');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
|
||||
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
|
||||
'library.create',
|
||||
{
|
||||
onSuccess: (lib) => {
|
||||
// Reset form
|
||||
setLibName('');
|
||||
|
||||
// We do this instead of invalidating the query because it triggers a full app re-render??
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => [
|
||||
...(libraries || []),
|
||||
lib
|
||||
]);
|
||||
|
||||
// Switch to the new library
|
||||
currentLibraryStore.id = lib.uuid;
|
||||
|
||||
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
|
||||
|
||||
onSubmit?.();
|
||||
},
|
||||
onSettled: () => {
|
||||
// Close create lib dialog
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
);
|
||||
return (
|
||||
<Dialog
|
||||
isVisible={isOpen}
|
||||
setIsVisible={setIsOpen}
|
||||
title="Create New Library"
|
||||
description="Choose a name for your new library, you can configure this and more settings from the library settings later on."
|
||||
ctaLabel="Create"
|
||||
ctaAction={() => createLibrary({ name: libName })}
|
||||
loading={createLibLoading}
|
||||
ctaDisabled={libName.length === 0}
|
||||
trigger={children}
|
||||
disableBackdropClose={disableBackdropClose}
|
||||
onClose={() => setLibName('')} // Resets form onClose
|
||||
>
|
||||
<Input
|
||||
value={libName}
|
||||
onChangeText={(text) => setLibName(text)}
|
||||
placeholder="My Cool Library"
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateLibraryDialog;
|
|
@ -1,14 +1,15 @@
|
|||
import { useDrawerStatus } from '@react-navigation/drawer';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { MotiView } from 'moti';
|
||||
import { CaretDown, Gear, Lock, Plus } from 'phosphor-react-native';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { CaretRight, 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';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { currentLibraryStore } from '~/utils/nav';
|
||||
import { AnimatedHeight } from '../animation/layout';
|
||||
import CreateLibraryDialog from '../dialog/CreateLibraryDialog';
|
||||
import { ModalRef } from '../layout/Modal';
|
||||
import CreateLibraryModal from '../modal/CreateLibraryModal';
|
||||
import { Divider } from '../primitive/Divider';
|
||||
|
||||
const DrawerLibraryManager = () => {
|
||||
|
@ -24,6 +25,8 @@ const DrawerLibraryManager = () => {
|
|||
|
||||
const navigation = useNavigation();
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
return (
|
||||
<View>
|
||||
<Pressable onPress={() => setDropdownClosed((v) => !v)}>
|
||||
|
@ -39,13 +42,10 @@ const DrawerLibraryManager = () => {
|
|||
{currentLibrary?.config.name}
|
||||
</Text>
|
||||
<MotiView
|
||||
animate={{
|
||||
rotate: dropdownClosed ? '0deg' : '180deg',
|
||||
translateX: dropdownClosed ? 0 : -9
|
||||
}}
|
||||
animate={{ rotateZ: dropdownClosed ? '0deg' : '90deg' }}
|
||||
transition={{ type: 'timing', duration: 100 }}
|
||||
>
|
||||
<CaretDown color="white" size={18} weight="bold" style={tw`ml-2`} />
|
||||
<CaretRight color="white" size={18} weight="bold" />
|
||||
</MotiView>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
@ -80,12 +80,14 @@ const DrawerLibraryManager = () => {
|
|||
<Divider style={tw`my-2`} />
|
||||
{/* Menu */}
|
||||
{/* Create Library */}
|
||||
<CreateLibraryDialog>
|
||||
<View style={tw`flex flex-row items-center px-1.5 py-[8px]`}>
|
||||
<Pressable
|
||||
style={tw`flex flex-row items-center px-1.5 py-[8px]`}
|
||||
onPress={() => modalRef.current?.present()}
|
||||
>
|
||||
<Plus size={18} weight="bold" color="white" style={tw`mr-2`} />
|
||||
<Text style={tw`text-sm font-semibold text-white`}>New Library</Text>
|
||||
</View>
|
||||
</CreateLibraryDialog>
|
||||
</Pressable>
|
||||
<CreateLibraryModal ref={modalRef} />
|
||||
{/* Manage Library */}
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
|
|
|
@ -36,7 +36,7 @@ type DrawerLocationsProp = {
|
|||
const DrawerLocations = ({ stackName }: DrawerLocationsProp) => {
|
||||
const navigation = useNavigation<DrawerNavigationHelpers>();
|
||||
|
||||
const importModalRef = useRef<ModalRef>(null);
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const { data: locations } = useLibraryQuery(['locations.list'], { keepPreviousData: true });
|
||||
|
||||
|
@ -62,7 +62,7 @@ const DrawerLocations = ({ stackName }: DrawerLocationsProp) => {
|
|||
))}
|
||||
</View>
|
||||
{/* Add Location */}
|
||||
<Pressable onPress={() => importModalRef.current?.present()}>
|
||||
<Pressable onPress={() => modalRef.current?.present()}>
|
||||
<View style={tw`mt-1 rounded border border-dashed border-app-line/80`}>
|
||||
<Text style={tw`p-2 text-center text-xs font-bold text-gray-400`}>
|
||||
Add Location
|
||||
|
@ -70,7 +70,7 @@ const DrawerLocations = ({ stackName }: DrawerLocationsProp) => {
|
|||
</View>
|
||||
</Pressable>
|
||||
</CollapsibleView>
|
||||
<ImportModal ref={importModalRef} />
|
||||
<ImportModal ref={modalRef} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -37,7 +37,7 @@ const DrawerTags = ({ stackName }: DrawerTagsProp) => {
|
|||
|
||||
const { data: tags } = useLibraryQuery(['tags.list'], { keepPreviousData: true });
|
||||
|
||||
const createTagModalRef = useRef<ModalRef>(null);
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
return (
|
||||
<CollapsibleView
|
||||
|
@ -61,12 +61,12 @@ const DrawerTags = ({ stackName }: DrawerTagsProp) => {
|
|||
))}
|
||||
</View>
|
||||
{/* Add Tag */}
|
||||
<Pressable onPress={() => createTagModalRef.current?.present()}>
|
||||
<Pressable onPress={() => modalRef.current?.present()}>
|
||||
<View style={tw`mt-1 rounded border border-dashed border-app-line/80`}>
|
||||
<Text style={tw`p-2 text-center text-xs font-bold text-gray-400`}>Add Tag</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
<CreateTagModal ref={createTagModalRef} />
|
||||
<CreateTagModal ref={modalRef} />
|
||||
</CollapsibleView>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,19 +16,15 @@ const CollapsibleView = ({ title, titleStyle, containerStyle, children }: Collap
|
|||
|
||||
return (
|
||||
<View style={containerStyle}>
|
||||
<Pressable onPress={toggle} style={tw`flex flex-row items-center justify-between`}>
|
||||
<Pressable onPress={toggle} style={tw`flex flex-row items-center justify-between pr-3`}>
|
||||
<Text style={titleStyle} selectable={false}>
|
||||
{title}
|
||||
</Text>
|
||||
<MotiView
|
||||
animate={{
|
||||
rotateZ: hide ? '0deg' : '90deg',
|
||||
translateX: hide ? 0 : 5,
|
||||
translateY: hide ? 0 : 5
|
||||
}}
|
||||
animate={{ rotateZ: hide ? '0deg' : '90deg' }}
|
||||
transition={{ type: 'timing', duration: 150 }}
|
||||
>
|
||||
<CaretRight color="white" weight="bold" size={16} style={tw`mr-3`} />
|
||||
<CaretRight color="white" weight="bold" size={16} />
|
||||
</MotiView>
|
||||
</Pressable>
|
||||
<AnimatedHeight hide={hide}>{children}</AnimatedHeight>
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
import { MotiView } from 'moti';
|
||||
import { ReactNode, useState } from 'react';
|
||||
import { KeyboardAvoidingView, Modal, Platform, Pressable, Text, View } from 'react-native';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { PulseAnimation } from '../animation/lottie';
|
||||
import { Button } from '../primitive/Button';
|
||||
|
||||
type DialogProps = {
|
||||
title: string;
|
||||
description?: string;
|
||||
trigger?: ReactNode;
|
||||
/**
|
||||
* if `true`, dialog will be visible when mounted.
|
||||
* It can be used when trigger is not provided and/or you need to open the dialog programmatically
|
||||
*/
|
||||
isVisible?: boolean;
|
||||
/**
|
||||
* Like above, it will override the default dialog state for opening/closing the dialog.
|
||||
* It can be used to control dialog state from outside
|
||||
*/
|
||||
setIsVisible?: (v: boolean) => void;
|
||||
children?: ReactNode;
|
||||
ctaAction?: () => void;
|
||||
ctaLabel?: string;
|
||||
ctaDanger?: boolean;
|
||||
ctaDisabled?: boolean;
|
||||
loading?: boolean;
|
||||
/**
|
||||
* Disables backdrop press to close the modal.
|
||||
*/
|
||||
disableBackdropClose?: boolean;
|
||||
/**
|
||||
* Triggered when the dialog is closed (either by backdrop or the close button)
|
||||
*/
|
||||
onClose?: () => void;
|
||||
};
|
||||
|
||||
const Dialog = (props: DialogProps) => {
|
||||
const [visible, setVisible] = useState(props.isVisible ?? false);
|
||||
|
||||
function handleCloseDialog() {
|
||||
props.setIsVisible ? props.setIsVisible(false) : setVisible(false);
|
||||
// Cool undefined check
|
||||
props.onClose?.();
|
||||
}
|
||||
|
||||
return (
|
||||
<View>
|
||||
{props.trigger && (
|
||||
<Pressable
|
||||
onPress={() =>
|
||||
props.setIsVisible ? props.setIsVisible(true) : setVisible(true)
|
||||
}
|
||||
>
|
||||
{props.trigger}
|
||||
</Pressable>
|
||||
)}
|
||||
<Modal renderToHardwareTextureAndroid transparent visible={props.isVisible ?? visible}>
|
||||
{/* Backdrop */}
|
||||
<Pressable
|
||||
style={tw`absolute inset-0 bg-app-box/40`}
|
||||
onPress={handleCloseDialog}
|
||||
disabled={props.disableBackdropClose || props.loading}
|
||||
/>
|
||||
{/* Content */}
|
||||
<KeyboardAvoidingView
|
||||
pointerEvents="box-none"
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
||||
keyboardVerticalOffset={Platform.OS === 'ios' ? 40 : undefined}
|
||||
style={tw`flex-1 items-center justify-center`}
|
||||
>
|
||||
<MotiView
|
||||
from={{ translateY: 40 }}
|
||||
animate={{ translateY: 0 }}
|
||||
transition={{ type: 'timing', duration: 200 }}
|
||||
>
|
||||
{/* TODO: Blur may look cool here */}
|
||||
<View
|
||||
style={tw`min-w-[360px] max-w-[380px] overflow-hidden rounded-md border border-app-line bg-app shadow shadow-app-shade`}
|
||||
>
|
||||
<View style={tw`p-5`}>
|
||||
{/* Title */}
|
||||
<Text style={tw`text-base font-bold text-ink`}>{props.title}</Text>
|
||||
{/* Description */}
|
||||
{props.description && (
|
||||
<Text style={tw`mt-2 text-sm leading-normal text-ink-dull`}>
|
||||
{props.description}
|
||||
</Text>
|
||||
)}
|
||||
{/* Children */}
|
||||
<View style={tw`mt-3`}>{props.children}</View>
|
||||
</View>
|
||||
{/* Actions */}
|
||||
<View
|
||||
style={tw`flex flex-row items-center border-t border-app-line bg-app-highlight p-3`}
|
||||
>
|
||||
{props.loading && <PulseAnimation style={tw`h-7`} />}
|
||||
<View style={tw`grow`} />
|
||||
<Button
|
||||
variant="darkGray"
|
||||
disabled={props.loading} // Disables Close button if loading
|
||||
onPress={handleCloseDialog}
|
||||
>
|
||||
<Text style={tw`text-sm text-ink`}>Close</Text>
|
||||
</Button>
|
||||
{props.ctaAction && (
|
||||
<Button
|
||||
style={tw`ml-2`}
|
||||
variant={props.ctaDanger ? 'danger' : 'accent'}
|
||||
onPress={props.ctaAction}
|
||||
disabled={props.ctaDisabled || props.loading}
|
||||
>
|
||||
<Text style={tw`text-sm text-ink`}>{props.ctaLabel}</Text>
|
||||
</Button>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
</MotiView>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default Dialog;
|
|
@ -45,11 +45,12 @@ export type ModalRef = BottomSheetModal;
|
|||
interface ModalProps extends BottomSheetModalProps {
|
||||
children: React.ReactNode;
|
||||
title?: string;
|
||||
description?: string;
|
||||
showCloseButton?: boolean;
|
||||
}
|
||||
|
||||
export const Modal = forwardRef<ModalRef, ModalProps>((props, ref) => {
|
||||
const { children, title, showCloseButton = false, ...otherProps } = props;
|
||||
const { children, title, description, showCloseButton = false, ...otherProps } = props;
|
||||
|
||||
const modalRef = useForwardedRef(ref);
|
||||
|
||||
|
@ -62,6 +63,9 @@ export const Modal = forwardRef<ModalRef, ModalProps>((props, ref) => {
|
|||
{...otherProps}
|
||||
>
|
||||
{title && <Text style={tw`text-center text-base font-medium text-ink`}>{title}</Text>}
|
||||
{props.description && (
|
||||
<Text style={tw`px-4 py-3 text-sm text-ink-dull`}>{props.description}</Text>
|
||||
)}
|
||||
{children}
|
||||
</BottomSheetModal>
|
||||
);
|
||||
|
|
79
apps/mobile/src/components/modal/CreateLibraryModal.tsx
Normal file
79
apps/mobile/src/components/modal/CreateLibraryModal.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { forwardRef, useState } from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { useBridgeMutation, usePlausibleEvent } from '@sd/client';
|
||||
import { ModalInput } from '~/components/form/Input';
|
||||
import { Modal, ModalRef } from '~/components/layout/Modal';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import useForwardedRef from '~/hooks/useForwardedRef';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { currentLibraryStore } from '~/utils/nav';
|
||||
|
||||
const CreateLibraryModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||
const modalRef = useForwardedRef(ref);
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const [libName, setLibName] = useState('');
|
||||
|
||||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
|
||||
const { mutate: createLibrary, isLoading: createLibLoading } = useBridgeMutation(
|
||||
'library.create',
|
||||
{
|
||||
onSuccess: (lib) => {
|
||||
// Reset form
|
||||
setLibName('');
|
||||
|
||||
// We do this instead of invalidating the query because it triggers a full app re-render??
|
||||
queryClient.setQueryData(['library.list'], (libraries: any) => [
|
||||
...(libraries || []),
|
||||
lib
|
||||
]);
|
||||
|
||||
// Switch to the new library
|
||||
currentLibraryStore.id = lib.uuid;
|
||||
|
||||
submitPlausibleEvent({ event: { type: 'libraryCreate' } });
|
||||
},
|
||||
onSettled: () => {
|
||||
modalRef.current?.dismiss();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
ref={modalRef}
|
||||
snapPoints={['30']}
|
||||
title="Create New Library"
|
||||
description="Choose a name for your new library, you can configure this and more settings
|
||||
from the library settings later on."
|
||||
onDismiss={() => {
|
||||
// Resets form onDismiss
|
||||
setLibName('');
|
||||
}}
|
||||
showCloseButton
|
||||
// Disable panning gestures
|
||||
enableHandlePanningGesture={false}
|
||||
enableContentPanningGesture={false}
|
||||
>
|
||||
<View style={tw`px-4`}>
|
||||
<ModalInput
|
||||
value={libName}
|
||||
onChangeText={(text) => setLibName(text)}
|
||||
placeholder="My Cool Library"
|
||||
/>
|
||||
<Button
|
||||
variant="accent"
|
||||
onPress={() => createLibrary({ name: libName })}
|
||||
style={tw`mt-4`}
|
||||
disabled={libName.length === 0 || createLibLoading}
|
||||
>
|
||||
<Text style={tw`text-sm font-medium text-white`}>Create</Text>
|
||||
</Button>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
});
|
||||
|
||||
export default CreateLibraryModal;
|
|
@ -14,9 +14,21 @@ import { tw } from '~/lib/tailwind';
|
|||
const ImportModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
||||
const modalRef = useForwardedRef(ref);
|
||||
|
||||
const { mutate: createLocation } = useLibraryMutation('locations.create', {
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
const addLocationToLibrary = useLibraryMutation('locations.addLibrary');
|
||||
const relinkLocation = useLibraryMutation('locations.relink');
|
||||
|
||||
const createLocation = useLibraryMutation('locations.create', {
|
||||
onError: (error, variables) => {
|
||||
switch (error.message) {
|
||||
case 'NEED_RELINK':
|
||||
if (!variables.dry_run) relinkLocation.mutate(variables.path);
|
||||
break;
|
||||
case 'ADD_LIBRARY':
|
||||
addLocationToLibrary.mutate(variables);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unimplemented custom remote error handling');
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
// Close the modal
|
||||
|
@ -32,7 +44,7 @@ const ImportModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
|||
|
||||
if (!response) return;
|
||||
|
||||
createLocation({
|
||||
createLocation.mutate({
|
||||
path: decodeURIComponent(response.uri.replace('file://', '')),
|
||||
dry_run: false,
|
||||
indexer_rules_ids: []
|
||||
|
|
|
@ -18,14 +18,13 @@ const DeleteLibraryModal = ({ trigger, onSubmit, libraryUuid }: Props) => {
|
|||
const { mutate: deleteLibrary, isLoading: deleteLibLoading } = useBridgeMutation(
|
||||
'library.delete',
|
||||
{
|
||||
onMutate: () => {
|
||||
console.log('Deleting library');
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['library.list']);
|
||||
onSubmit?.();
|
||||
submitPlausibleEvent({
|
||||
event: {
|
||||
type: 'libraryDelete'
|
||||
}
|
||||
});
|
||||
submitPlausibleEvent({ event: { type: 'libraryDelete' } });
|
||||
},
|
||||
onSettled: () => {
|
||||
modalRef.current?.close();
|
||||
|
|
|
@ -24,6 +24,9 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
|||
const submitPlausibleEvent = usePlausibleEvent();
|
||||
|
||||
const { mutate: createTag } = useLibraryMutation('tags.create', {
|
||||
onMutate: () => {
|
||||
console.log('Creating tag');
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Reset form
|
||||
setTagName('');
|
||||
|
@ -67,7 +70,7 @@ const CreateTagModal = forwardRef<ModalRef, unknown>((_, ref) => {
|
|||
enableContentPanningGesture={false}
|
||||
>
|
||||
<View style={tw`p-4`}>
|
||||
<View style={tw`mt-4 flex flex-row items-center`}>
|
||||
<View style={tw`mt-2 flex flex-row items-center`}>
|
||||
<Pressable
|
||||
onPress={() => setShowPicker(true)}
|
||||
style={twStyle({ backgroundColor: tagColor }, 'h-6 w-6 rounded-full')}
|
||||
|
|
|
@ -24,6 +24,9 @@ const UpdateTagModal = forwardRef<ModalRef, Props>((props, ref) => {
|
|||
const [showPicker, setShowPicker] = useState(false);
|
||||
|
||||
const { mutate: updateTag, isLoading } = useLibraryMutation('tags.update', {
|
||||
onMutate: () => {
|
||||
console.log('Updating tag');
|
||||
},
|
||||
onSuccess: () => {
|
||||
// Reset form
|
||||
setShowPicker(false);
|
||||
|
@ -82,7 +85,7 @@ const UpdateTagModal = forwardRef<ModalRef, Props>((props, ref) => {
|
|||
variant="accent"
|
||||
onPress={() => updateTag({ id: props.tag.id, color: tagColor, name: tagName })}
|
||||
style={tw`mt-6`}
|
||||
disabled={tagName.length === 0}
|
||||
disabled={tagName.length === 0 || tagColor.length === 0 || isLoading}
|
||||
>
|
||||
<Text style={tw`text-sm font-medium text-white`}>Save</Text>
|
||||
</Button>
|
||||
|
|
|
@ -24,7 +24,10 @@ export default function LocationScreen({ navigation, route }: SharedScreenProps<
|
|||
if (path && path !== '') {
|
||||
// Nested location.
|
||||
navigation.setOptions({
|
||||
title: path.split('/')[0]
|
||||
title: path
|
||||
.split('/')
|
||||
.filter((x) => x !== '')
|
||||
.pop()
|
||||
});
|
||||
} else {
|
||||
navigation.setOptions({
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { AppLogo, BloomOne } from '@sd/assets/images';
|
||||
import { useNavigation, useRoute } from '@react-navigation/native';
|
||||
import { MotiView } from 'moti';
|
||||
import { CaretLeft } from 'phosphor-react-native';
|
||||
import { Image, KeyboardAvoidingView, Platform, Pressable, Text, View } from 'react-native';
|
||||
import Animated from 'react-native-reanimated';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
import { FadeInUpAnimation, LogoAnimation } from '~/components/animation/layout';
|
||||
import { AnimatedButton } from '~/components/primitive/Button';
|
||||
|
@ -28,9 +30,11 @@ export function OnboardingContainer({ children }: React.PropsWithChildren) {
|
|||
<KeyboardAvoidingView
|
||||
behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
|
||||
keyboardVerticalOffset={bottom}
|
||||
style={tw`w-full flex-1 items-center justify-center px-4`}
|
||||
style={tw`w-full flex-1 items-center justify-center`}
|
||||
>
|
||||
<MotiView style={tw`w-full items-center justify-center px-4`}>
|
||||
{children}
|
||||
</MotiView>
|
||||
</KeyboardAvoidingView>
|
||||
<Text style={tw`absolute bottom-8 text-xs text-ink-dull/50`}>
|
||||
© 2022 Spacedrive Technology Inc.
|
||||
|
@ -43,7 +47,7 @@ export function OnboardingContainer({ children }: React.PropsWithChildren) {
|
|||
}
|
||||
|
||||
export const OnboardingTitle = styled(
|
||||
Text,
|
||||
Animated.Text,
|
||||
'text-ink text-center text-4xl font-extrabold leading-tight'
|
||||
);
|
||||
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { CaretRight, Pen, Trash } from 'phosphor-react-native';
|
||||
import React from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { Animated, FlatList, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { LibraryConfigWrapped, useBridgeQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import DeleteLibraryModal from '~/components/modal/confirm-modals/DeleteLibraryModal';
|
||||
import { AnimatedButton, FakeButton } from '~/components/primitive/Button';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
@ -69,6 +70,23 @@ function LibraryItem({
|
|||
const LibrarySettingsScreen = ({ navigation }: SettingsStackScreenProps<'LibrarySettings'>) => {
|
||||
const { data: libraries } = useBridgeQuery(['library.list']);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<AnimatedButton
|
||||
variant="accent"
|
||||
style={tw`mr-2`}
|
||||
size="sm"
|
||||
onPress={() => modalRef.current?.present()}
|
||||
>
|
||||
<Text style={tw`text-white`}>New</Text>
|
||||
</AnimatedButton>
|
||||
)
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 px-3 py-4`}>
|
||||
<FlatList
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { CaretRight, Pen, Repeat, Trash } from 'phosphor-react-native';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Animated, FlatList, Pressable, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import {
|
||||
|
@ -10,7 +11,10 @@ import {
|
|||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import FolderIcon from '~/components/icons/FolderIcon';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ImportModal from '~/components/modal/ImportModal';
|
||||
import DeleteLocationModal from '~/components/modal/confirm-modals/DeleteLocationModal';
|
||||
import { AnimatedButton } from '~/components/primitive/Button';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
|
||||
|
||||
|
@ -121,11 +125,26 @@ function LocationItem({ location, index, navigation }: LocationItemProps) {
|
|||
);
|
||||
}
|
||||
|
||||
// TODO: Add new location from here (ImportModal)
|
||||
|
||||
const LocationSettingsScreen = ({ navigation }: SettingsStackScreenProps<'LocationSettings'>) => {
|
||||
const { data: locations } = useLibraryQuery(['locations.list']);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<AnimatedButton
|
||||
variant="accent"
|
||||
style={tw`mr-2`}
|
||||
size="sm"
|
||||
onPress={() => modalRef.current?.present()}
|
||||
>
|
||||
<Text style={tw`text-white`}>New</Text>
|
||||
</AnimatedButton>
|
||||
)
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 px-3 py-4`}>
|
||||
<FlatList
|
||||
|
@ -135,6 +154,7 @@ const LocationSettingsScreen = ({ navigation }: SettingsStackScreenProps<'Locati
|
|||
<LocationItem navigation={navigation} location={item} index={index} />
|
||||
)}
|
||||
/>
|
||||
<ImportModal ref={modalRef} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
import { CaretRight, Pen, Trash } from 'phosphor-react-native';
|
||||
import { useRef } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Animated, FlatList, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { Tag, useLibraryQuery } from '@sd/client';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import DeleteTagModal from '~/components/modal/confirm-modals/DeleteTagModal';
|
||||
import CreateTagModal from '~/components/modal/tag/CreateTagModal';
|
||||
import UpdateTagModal from '~/components/modal/tag/UpdateTagModal';
|
||||
import { AnimatedButton, FakeButton } from '~/components/primitive/Button';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/SettingsNavigator';
|
||||
|
||||
function TagItem({ tag, index }: { tag: Tag; index: number }) {
|
||||
const updateTagModalRef = useRef<ModalRef>(null);
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const renderRightActions = (
|
||||
progress: Animated.AnimatedInterpolation<number>,
|
||||
|
@ -28,12 +29,8 @@ function TagItem({ tag, index }: { tag: Tag; index: number }) {
|
|||
<Animated.View
|
||||
style={[tw`flex flex-row items-center`, { transform: [{ translateX: translate }] }]}
|
||||
>
|
||||
<UpdateTagModal
|
||||
tag={tag}
|
||||
ref={updateTagModalRef}
|
||||
onSubmit={() => swipeable.close()}
|
||||
/>
|
||||
<AnimatedButton onPress={() => updateTagModalRef.current?.present()}>
|
||||
<UpdateTagModal tag={tag} ref={modalRef} onSubmit={() => swipeable.close()} />
|
||||
<AnimatedButton onPress={() => modalRef.current?.present()}>
|
||||
<Pen size={18} color="white" />
|
||||
</AnimatedButton>
|
||||
<DeleteTagModal
|
||||
|
@ -75,6 +72,23 @@ function TagItem({ tag, index }: { tag: Tag; index: number }) {
|
|||
const TagsSettingsScreen = ({ navigation }: SettingsStackScreenProps<'TagsSettings'>) => {
|
||||
const { data: tags } = useLibraryQuery(['tags.list']);
|
||||
|
||||
useEffect(() => {
|
||||
navigation.setOptions({
|
||||
headerRight: () => (
|
||||
<AnimatedButton
|
||||
variant="accent"
|
||||
style={tw`mr-2`}
|
||||
size="sm"
|
||||
onPress={() => modalRef.current?.present()}
|
||||
>
|
||||
<Text style={tw`text-white`}>New</Text>
|
||||
</AnimatedButton>
|
||||
)
|
||||
});
|
||||
}, [navigation]);
|
||||
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 px-3 py-4`}>
|
||||
<FlatList
|
||||
|
@ -82,6 +96,7 @@ const TagsSettingsScreen = ({ navigation }: SettingsStackScreenProps<'TagsSettin
|
|||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item, index }) => <TagItem tag={item} index={index} />}
|
||||
/>
|
||||
<CreateTagModal ref={modalRef} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -40,10 +40,10 @@
|
|||
"@vitejs/plugin-react": "^2.1.0",
|
||||
"autoprefixer": "^10.4.12",
|
||||
"byte-size": "^8.1.0",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"class-variance-authority": "^0.5.3",
|
||||
"clsx": "^1.2.1",
|
||||
"crypto-random-string": "^5.0.0",
|
||||
"dayjs": "^1.11.5",
|
||||
"dayjs": "^1.11.8",
|
||||
"dragselect": "^2.7.4",
|
||||
"framer-motion": "^10.11.5",
|
||||
"phosphor-react": "^1.4.1",
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"typecheck": "pnpm -r typecheck",
|
||||
"lint": "turbo run lint",
|
||||
"lint:fix": "turbo run lint -- --fix",
|
||||
"clean": "rimraf -g \"node_modules/\" \"**/node_modules/\" \"target/\" \"**/.build/\" \"**/.next/\" \"**/.contentlayer/\" \"**/dist/!(.gitignore)**\""
|
||||
"clean": "rimraf -g \"node_modules/\" \"**/node_modules/\" \"target/\" \"**/.build/\" \"**/.next/\" \"**/dist/!(.gitignore)**\""
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
"@react-spring/web": "9.6.0",
|
||||
"@sd/assets": "workspace:*",
|
||||
"@tailwindcss/forms": "^0.5.3",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"class-variance-authority": "^0.5.3",
|
||||
"clsx": "^1.2.1",
|
||||
"phosphor-react": "^1.4.1",
|
||||
"postcss": "^8.4.17",
|
||||
|
|
4096
pnpm-lock.yaml
4096
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue