mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-02 11:13:29 +00:00
[MOB-58] Settings routes new design & more (#2103)
* wip: redesigning settings pages * Edit location redesign & more * right actions * cleanup
This commit is contained in:
parent
e8450821df
commit
43360601da
|
@ -1,55 +1,135 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { DotsThreeOutlineVertical, Eye, Plus } from 'phosphor-react-native';
|
||||
import React, { useRef } from 'react';
|
||||
import { Pressable, Text, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { DotsThreeOutlineVertical, Eye, Pen, Plus, Trash } from 'phosphor-react-native';
|
||||
import React, { useRef } from 'react';
|
||||
import { Animated, Pressable, Text, View } from 'react-native';
|
||||
import { FlatList, Swipeable } from 'react-native-gesture-handler';
|
||||
import { ClassInput } from 'twrnc/dist/esm/types';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
||||
import { Icon } from '../icons/Icon';
|
||||
import Fade from '../layout/Fade';
|
||||
import DeleteTagModal from '../modal/confirmModals/DeleteTagModal';
|
||||
import CreateTagModal from '../modal/tag/CreateTagModal';
|
||||
import { TagModal } from '../modal/tag/TagModal';
|
||||
import UpdateTagModal from '../modal/tag/UpdateTagModal';
|
||||
import { AnimatedButton, FakeButton } from '../primitive/Button';
|
||||
|
||||
type BrowseTagItemProps = {
|
||||
type TagItemProps = {
|
||||
tag: Tag;
|
||||
onPress: () => void;
|
||||
tagStyle?: string;
|
||||
tagStyle?: ClassInput;
|
||||
viewStyle?: 'grid' | 'list';
|
||||
rightActions?: () => void;
|
||||
};
|
||||
|
||||
export const BrowseTagItem: React.FC<BrowseTagItemProps> = ({ tag, onPress, tagStyle }) => {
|
||||
export const TagItem = ({
|
||||
tag,
|
||||
onPress,
|
||||
rightActions,
|
||||
tagStyle,
|
||||
viewStyle = 'grid'
|
||||
}: TagItemProps) => {
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const renderTagView = () => (
|
||||
<View
|
||||
style={twStyle(
|
||||
`h-auto flex-col justify-center gap-2.5 rounded-md border border-app-line/50 bg-app-box/50 p-2`,
|
||||
viewStyle === 'grid' ? 'w-[90px]' : 'w-full',
|
||||
tagStyle
|
||||
)}
|
||||
>
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
<View
|
||||
style={twStyle('h-[28px] w-[28px] rounded-full', {
|
||||
backgroundColor: tag.color!
|
||||
})}
|
||||
/>
|
||||
<Pressable onPress={() => modalRef.current?.present()}>
|
||||
<DotsThreeOutlineVertical
|
||||
weight="fill"
|
||||
size={20}
|
||||
color={tw.color('ink-faint')}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text style={tw`w-full max-w-[75px] text-xs font-bold text-white`} numberOfLines={1}>
|
||||
{tag.name}
|
||||
</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
const renderRightActions = (
|
||||
progress: Animated.AnimatedInterpolation<number>,
|
||||
_dragX: Animated.AnimatedInterpolation<number>,
|
||||
swipeable: Swipeable
|
||||
) => {
|
||||
const translate = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 0],
|
||||
extrapolate: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
tw`ml-5 flex flex-row items-center`,
|
||||
{ transform: [{ translateX: translate }] }
|
||||
]}
|
||||
>
|
||||
<UpdateTagModal tag={tag} ref={modalRef} onSubmit={() => swipeable.close()} />
|
||||
<AnimatedButton onPress={() => modalRef.current?.present()}>
|
||||
<Pen size={18} color="white" />
|
||||
</AnimatedButton>
|
||||
<DeleteTagModal
|
||||
tagId={tag.id}
|
||||
trigger={
|
||||
<FakeButton style={tw`mx-2`}>
|
||||
<Trash size={18} color="white" />
|
||||
</FakeButton>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress} testID="browse-tag">
|
||||
<View
|
||||
style={twStyle(
|
||||
'h-auto w-[90px] flex-col justify-center gap-2.5 rounded-md border border-app-line/50 bg-app-box/50 p-2',
|
||||
tagStyle
|
||||
)}
|
||||
>
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
<View
|
||||
style={twStyle('h-[28px] w-[28px] rounded-full', {
|
||||
backgroundColor: tag.color!
|
||||
})}
|
||||
/>
|
||||
<Pressable onPress={() => modalRef.current?.present()}>
|
||||
<DotsThreeOutlineVertical
|
||||
weight="fill"
|
||||
size={20}
|
||||
color={tw.color('ink-faint')}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Text
|
||||
style={tw`w-full max-w-[75px] text-xs font-bold text-white`}
|
||||
numberOfLines={1}
|
||||
{viewStyle === 'grid' ? (
|
||||
renderTagView()
|
||||
) : (
|
||||
<Swipeable
|
||||
containerStyle={tw`rounded-md border border-app-line/50 bg-app-box/50 p-3`}
|
||||
enableTrackpadTwoFingerGesture
|
||||
renderRightActions={renderRightActions}
|
||||
>
|
||||
{tag.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={twStyle('h-auto flex-row items-center justify-between', tagStyle)}>
|
||||
<View style={tw`flex-1 flex-row items-center gap-2`}>
|
||||
<View
|
||||
style={twStyle('h-[28px] w-[28px] rounded-full', {
|
||||
backgroundColor: tag.color!
|
||||
})}
|
||||
/>
|
||||
<Text
|
||||
style={tw`w-full max-w-[75px] text-xs font-bold text-white`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{tag.name}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => modalRef.current?.present()}>
|
||||
<DotsThreeOutlineVertical
|
||||
weight="fill"
|
||||
size={20}
|
||||
color={tw.color('ink-faint')}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</Swipeable>
|
||||
)}
|
||||
<TagModal ref={modalRef} tag={tag} />
|
||||
</Pressable>
|
||||
);
|
||||
|
@ -102,7 +182,7 @@ const BrowseTags = () => {
|
|||
</View>
|
||||
)}
|
||||
renderItem={({ item }) => (
|
||||
<BrowseTagItem
|
||||
<TagItem
|
||||
tag={item}
|
||||
onPress={() =>
|
||||
navigation.navigate('Tag', { id: item.id, color: item.color! })
|
||||
|
|
|
@ -44,33 +44,6 @@ export default function Header({
|
|||
const explorerStore = useExplorerStore();
|
||||
const routeParams = route?.route.params as any;
|
||||
|
||||
const SearchType = () => {
|
||||
switch (searchType) {
|
||||
case 'explorer':
|
||||
return 'Explorer'; //TODO
|
||||
case 'location':
|
||||
return <Search placeholder="Location name..." />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
const HeaderIconKind = () => {
|
||||
switch (headerKind) {
|
||||
case 'location':
|
||||
return <Icon size={32} name="Folder" />;
|
||||
case 'tag':
|
||||
return (
|
||||
<View
|
||||
style={twStyle('h-6 w-6 rounded-full', {
|
||||
backgroundColor: routeParams.color
|
||||
})}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
style={twStyle(
|
||||
|
@ -91,7 +64,7 @@ export default function Header({
|
|||
</Pressable>
|
||||
)}
|
||||
<View style={tw`flex-row items-center gap-2`}>
|
||||
<HeaderIconKind />
|
||||
<HeaderIconKind headerKind={headerKind} routeParams={routeParams} />
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={tw`max-w-[190px] text-[22px] font-bold text-white`}
|
||||
|
@ -126,8 +99,45 @@ export default function Header({
|
|||
</View>
|
||||
|
||||
{showLibrary && <BrowseLibraryManager style="mt-4" />}
|
||||
{searchType && <SearchType />}
|
||||
{searchType && <HeaderSearchType searchType={searchType} />}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
interface HeaderSearchTypeProps {
|
||||
searchType: HeaderProps['searchType'];
|
||||
}
|
||||
|
||||
const HeaderSearchType = ({ searchType }: HeaderSearchTypeProps) => {
|
||||
switch (searchType) {
|
||||
case 'explorer':
|
||||
return 'Explorer'; //TODO
|
||||
case 'location':
|
||||
return <Search placeholder="Location name..." />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
interface HeaderIconKindProps {
|
||||
headerKind: HeaderProps['headerKind'];
|
||||
routeParams?: any;
|
||||
}
|
||||
|
||||
const HeaderIconKind = ({ headerKind, routeParams }: HeaderIconKindProps) => {
|
||||
switch (headerKind) {
|
||||
case 'location':
|
||||
return <Icon size={32} name="Folder" />;
|
||||
case 'tag':
|
||||
return (
|
||||
<View
|
||||
style={twStyle('h-6 w-6 rounded-full', {
|
||||
backgroundColor: routeParams.color
|
||||
})}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ interface Props {
|
|||
const Categories = ({ kinds }: Props) => {
|
||||
return (
|
||||
<View>
|
||||
<Text style={tw`pb-5 text-lg font-bold text-white px-7`}>Categories</Text>
|
||||
<Text style={tw`px-7 pb-5 text-lg font-bold text-white`}>Categories</Text>
|
||||
<View>
|
||||
<Fade color="mobile-screen" width={30} height="100%">
|
||||
<VirtualizedListWrapper horizontal>
|
||||
|
@ -29,7 +29,7 @@ const Categories = ({ kinds }: Props) => {
|
|||
key={kinds.data?.statistics ? 'kinds' : '_'} //needed to update numColumns when data is available
|
||||
keyExtractor={(item) => item.name}
|
||||
scrollEnabled={false}
|
||||
ItemSeparatorComponent={() => <View style={tw`w-3 h-3`} />}
|
||||
ItemSeparatorComponent={() => <View style={tw`h-3 w-3`} />}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
renderItem={({ item }) => {
|
||||
const { kind, name, count } = item;
|
||||
|
@ -79,7 +79,7 @@ const KindItem = ({ name, icon, items }: KindItemProps) => {
|
|||
}}
|
||||
>
|
||||
<View style={twStyle('shrink-0 flex-row items-center', 'gap-2 rounded-lg text-sm')}>
|
||||
<Icon name={icon} size={40} style={tw`w-12 h-12 mr-3`} />
|
||||
<Icon name={icon} size={40} style={tw`mr-3 h-12 w-12`} />
|
||||
<View>
|
||||
<Text style={tw`text-sm font-medium text-ink`}>{name}</Text>
|
||||
{items !== undefined && (
|
||||
|
|
|
@ -39,7 +39,7 @@ const StatItem = ({ title, bytes, isLoading, style }: StatItemProps) => {
|
|||
)}
|
||||
>
|
||||
<Text style={tw`text-sm font-bold text-gray-400`}>{title}</Text>
|
||||
<View style={tw`flex-row items-baseline mt-1`}>
|
||||
<View style={tw`mt-1 flex-row items-baseline`}>
|
||||
<Text style={twStyle('text-xl font-bold tabular-nums text-white')}>{count}</Text>
|
||||
<Text style={tw`ml-1 text-sm text-gray-400`}>{unit}</Text>
|
||||
</View>
|
||||
|
|
|
@ -58,7 +58,7 @@ const StatCard = ({ icon, name, connectionType, ...stats }: StatCardProps) => {
|
|||
<View
|
||||
style={tw`absolute flex-row items-end gap-0.5 text-lg font-semibold`}
|
||||
>
|
||||
<Text style={tw`mx-auto font-semibold text-md text-ink`}>
|
||||
<Text style={tw`mx-auto text-md font-semibold text-ink`}>
|
||||
{fill.toFixed(0)}
|
||||
</Text>
|
||||
<Text style={tw`text-xs font-bold text-ink-dull opacity-60`}>
|
||||
|
|
|
@ -30,6 +30,7 @@ const button = cva(['items-center justify-center rounded-md border shadow-sm'],
|
|||
});
|
||||
|
||||
type ButtonProps = VariantProps<typeof button> & PressableProps;
|
||||
export type ButtonVariants = ButtonProps['variant'];
|
||||
|
||||
export const Button: FC<ButtonProps> = ({ variant, size, disabled, ...props }) => {
|
||||
const { style, ...otherProps } = props;
|
||||
|
|
46
apps/mobile/src/components/settings/SettingsButton.tsx
Normal file
46
apps/mobile/src/components/settings/SettingsButton.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { Text, View } from 'react-native';
|
||||
import { ClassInput } from 'twrnc/dist/esm/types';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
||||
import { Button, ButtonVariants } from '../primitive/Button';
|
||||
|
||||
interface Props {
|
||||
buttonText: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
buttonPress?: () => void;
|
||||
buttonVariant?: ButtonVariants;
|
||||
buttonTextStyle?: string;
|
||||
buttonIcon?: JSX.Element;
|
||||
infoContainerStyle?: ClassInput;
|
||||
}
|
||||
|
||||
const SettingsButton = ({
|
||||
buttonText,
|
||||
title,
|
||||
description,
|
||||
buttonVariant,
|
||||
buttonTextStyle,
|
||||
buttonIcon,
|
||||
infoContainerStyle,
|
||||
buttonPress
|
||||
}: Props) => {
|
||||
return (
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
<View style={twStyle('w-73%', infoContainerStyle)}>
|
||||
<Text style={tw`text-sm font-medium text-ink`}>{title}</Text>
|
||||
{description && <Text style={tw`mt-1 text-xs text-ink-dull`}>{description}</Text>}
|
||||
</View>
|
||||
<Button
|
||||
style={tw`flex-row items-center gap-2`}
|
||||
variant={buttonVariant}
|
||||
onPress={buttonPress}
|
||||
>
|
||||
{buttonIcon}
|
||||
<Text style={twStyle(buttonTextStyle)}>{buttonText}</Text>
|
||||
</Button>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsButton;
|
|
@ -10,14 +10,12 @@ type SettingsContainerProps = PropsWithChildren<{
|
|||
export function SettingsContainer({ children, title, description }: SettingsContainerProps) {
|
||||
return (
|
||||
<View>
|
||||
{title && (
|
||||
<Text style={tw`pb-2 pl-3 text-sm font-semibold text-ink-dull`}>{title}</Text>
|
||||
)}
|
||||
{title && <Text style={tw`pb-2 text-sm font-semibold text-ink-dull`}>{title}</Text>}
|
||||
{children}
|
||||
{description && <Text style={tw`px-3 pt-2 text-sm text-ink-dull`}>{description}</Text>}
|
||||
{description && <Text style={tw`pt-2 text-sm text-ink-dull`}>{description}</Text>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export const SettingsTitle = styled(Text, 'text-ink mb-1.5 ml-1 text-sm font-medium');
|
||||
export const SettingsTitle = styled(Text, 'text-ink text-sm font-medium');
|
||||
export const SettingsInputInfo = styled(Text, 'mt-2 text-xs text-ink-faint');
|
||||
|
|
65
apps/mobile/src/components/settings/SettingsToggle.tsx
Normal file
65
apps/mobile/src/components/settings/SettingsToggle.tsx
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { useState } from 'react';
|
||||
import { Control, Controller } from 'react-hook-form';
|
||||
import { Switch, Text, View } from 'react-native';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
|
||||
type Props =
|
||||
| {
|
||||
title: string; // Title of the setting
|
||||
description?: string; // This is to display a description below the title
|
||||
onEnabledChange?: (enabled: boolean) => void; // This is to receive the value of the toggle when it changes
|
||||
control: Control<any>; //Zod form control
|
||||
name: string; //Name of the field for zod form controller
|
||||
}
|
||||
| {
|
||||
title: string;
|
||||
description?: string;
|
||||
onEnabledChange?: (enabled: boolean) => void;
|
||||
control?: never;
|
||||
name?: never;
|
||||
};
|
||||
|
||||
const SettingsToggle = ({ title, description, onEnabledChange, control, name }: Props) => {
|
||||
const [isEnabled, setIsEnabled] = useState(false);
|
||||
|
||||
return (
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
<View style={tw`w-[75%]`}>
|
||||
<Text style={tw`text-sm font-medium text-ink`}>{title}</Text>
|
||||
{description && <Text style={tw`mt-1 text-xs text-ink-dull`}>{description}</Text>}
|
||||
</View>
|
||||
{control && name ? (
|
||||
<Controller
|
||||
name={name}
|
||||
control={control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch
|
||||
trackColor={{
|
||||
true: tw.color('accent')
|
||||
}}
|
||||
value={value ?? isEnabled}
|
||||
onValueChange={(val) => {
|
||||
setIsEnabled(val);
|
||||
onChange(val);
|
||||
onEnabledChange?.(val);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Switch
|
||||
trackColor={{
|
||||
true: tw.color('accent')
|
||||
}}
|
||||
value={isEnabled}
|
||||
onValueChange={() => {
|
||||
setIsEnabled((prev) => !prev);
|
||||
onEnabledChange?.(!isEnabled);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default SettingsToggle;
|
|
@ -43,55 +43,55 @@ export default function SettingsStack() {
|
|||
<Stack.Screen
|
||||
name="GeneralSettings"
|
||||
component={GeneralSettingsScreen}
|
||||
options={{ headerTitle: 'General Settings' }}
|
||||
options={{ header: () => <Header navBack title="General" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LibrarySettings"
|
||||
component={LibrarySettingsScreen}
|
||||
options={{ headerTitle: 'Libraries' }}
|
||||
options={{ header: () => <Header navBack title="Libraries" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="AppearanceSettings"
|
||||
component={AppearanceSettingsScreen}
|
||||
options={{ headerTitle: 'Appearance' }}
|
||||
options={{ header: () => <Header navBack title="Appearance" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PrivacySettings"
|
||||
component={PrivacySettingsScreen}
|
||||
options={{ headerTitle: 'Privacy' }}
|
||||
options={{ header: () => <Header navBack title="Privacy" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ExtensionsSettings"
|
||||
component={ExtensionsSettingsScreen}
|
||||
options={{ headerTitle: 'Extensions' }}
|
||||
options={{ header: () => <Header navBack title="Extensions" /> }}
|
||||
/>
|
||||
{/* Library */}
|
||||
<Stack.Screen
|
||||
name="LibraryGeneralSettings"
|
||||
component={LibraryGeneralSettingsScreen}
|
||||
options={{ headerTitle: 'Library Settings' }}
|
||||
options={{ header: () => <Header navBack title="Library Settings" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="LocationSettings"
|
||||
component={LocationSettingsScreen}
|
||||
options={{ headerTitle: 'Locations' }}
|
||||
options={{
|
||||
header: () => <Header searchType="location" navBack title="Locations" />
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="EditLocationSettings"
|
||||
component={EditLocationSettingsScreen}
|
||||
options={{ headerTitle: 'Edit Location' }}
|
||||
options={{ header: () => <Header navBack title="Edit Location" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="NodesSettings"
|
||||
component={NodesSettingsScreen}
|
||||
options={{
|
||||
headerTitle: 'Nodes'
|
||||
}}
|
||||
options={{ header: () => <Header navBack title="Nodes" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="TagsSettings"
|
||||
component={TagsSettingsScreen}
|
||||
options={{ headerTitle: 'Tags' }}
|
||||
options={{ header: () => <Header navBack title="Tags" /> }}
|
||||
/>
|
||||
{/* <Stack.Screen
|
||||
name="KeysSettings"
|
||||
|
@ -99,13 +99,21 @@ export default function SettingsStack() {
|
|||
options={{ headerTitle: 'Keys' }}
|
||||
/> */}
|
||||
{/* Info */}
|
||||
<Stack.Screen name="About" component={AboutScreen} options={{ headerTitle: 'About' }} />
|
||||
<Stack.Screen
|
||||
name="About"
|
||||
component={AboutScreen}
|
||||
options={{ header: () => <Header navBack title="About" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Support"
|
||||
component={SupportScreen}
|
||||
options={{ headerTitle: 'Support' }}
|
||||
options={{ header: () => <Header navBack title="Support" /> }}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="Debug"
|
||||
component={DebugScreen}
|
||||
options={{ header: () => <Header navBack title="Debug" /> }}
|
||||
/>
|
||||
<Stack.Screen name="Debug" component={DebugScreen} options={{ headerTitle: 'Debug' }} />
|
||||
</Stack.Navigator>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { DotsThreeOutlineVertical } from 'phosphor-react-native';
|
||||
import { DotsThreeOutlineVertical, Pen, Plus, Trash } from 'phosphor-react-native';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { FlatList, Pressable, Text, View } from 'react-native';
|
||||
import { Animated, FlatList, Pressable, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import {
|
||||
arraysEqual,
|
||||
|
@ -16,17 +17,24 @@ import FolderIcon from '~/components/icons/FolderIcon';
|
|||
import Fade from '~/components/layout/Fade';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import DeleteLocationModal from '~/components/modal/confirmModals/DeleteLocationModal';
|
||||
import ImportModal from '~/components/modal/ImportModal';
|
||||
import { LocationModal } from '~/components/modal/location/LocationModal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||
import { useSearchStore } from '~/stores/searchStore';
|
||||
|
||||
export const Locations = () => {
|
||||
interface Props {
|
||||
redirectToLocationSettings?: boolean;
|
||||
}
|
||||
|
||||
export const Locations = ({ redirectToLocationSettings }: Props) => {
|
||||
const locationsQuery = useLibraryQuery(['locations.list']);
|
||||
useNodes(locationsQuery.data?.nodes);
|
||||
const locations = useCache(locationsQuery.data?.items);
|
||||
const { search } = useSearchStore();
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
const [debouncedSearch] = useDebounce(search, 200);
|
||||
const filteredLocations = useMemo(
|
||||
() =>
|
||||
|
@ -41,7 +49,15 @@ export const Locations = () => {
|
|||
SettingsStackScreenProps<'Settings'>['navigation']
|
||||
>();
|
||||
return (
|
||||
<ScreenContainer scrollview={false} style={tw`relative py-0 px-7`}>
|
||||
<ScreenContainer scrollview={false} style={tw`relative px-7 py-0`}>
|
||||
<Pressable
|
||||
style={tw`absolute bottom-7 right-7 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-accent`}
|
||||
onPress={() => {
|
||||
modalRef.current?.present();
|
||||
}}
|
||||
>
|
||||
<Plus size={20} weight="bold" style={tw`text-ink`} />
|
||||
</Pressable>
|
||||
<Fade
|
||||
fadeSides="top-bottom"
|
||||
orientation="vertical"
|
||||
|
@ -57,18 +73,32 @@ export const Locations = () => {
|
|||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item }) => (
|
||||
<LocationItem
|
||||
navigation={navigation}
|
||||
editLocation={() =>
|
||||
navigation.navigate('SettingsStack', {
|
||||
screen: 'EditLocationSettings',
|
||||
params: { id: item.id }
|
||||
})
|
||||
}
|
||||
onPress={() => navigation.navigate('Location', { id: item.id })}
|
||||
onPress={() => {
|
||||
if (redirectToLocationSettings) {
|
||||
navigation.navigate('SettingsStack', {
|
||||
screen: 'EditLocationSettings',
|
||||
params: { id: item.id }
|
||||
});
|
||||
} else {
|
||||
navigation.navigate('BrowseStack', {
|
||||
screen: 'Location',
|
||||
params: { id: item.id }
|
||||
});
|
||||
}
|
||||
}}
|
||||
location={item}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Fade>
|
||||
<ImportModal ref={modalRef} />
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
@ -77,56 +107,109 @@ interface LocationItemProps {
|
|||
location: Location;
|
||||
onPress: () => void;
|
||||
editLocation: () => void;
|
||||
navigation: SettingsStackScreenProps<'LocationSettings'>['navigation'];
|
||||
}
|
||||
|
||||
const LocationItem: React.FC<LocationItemProps> = ({
|
||||
export const LocationItem = ({
|
||||
location,
|
||||
editLocation,
|
||||
onPress
|
||||
onPress,
|
||||
navigation
|
||||
}: LocationItemProps) => {
|
||||
const onlineLocations = useOnlineLocations();
|
||||
const online = onlineLocations.some((l) => arraysEqual(location.pub_id, l));
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const renderRightActions = (
|
||||
progress: Animated.AnimatedInterpolation<number>,
|
||||
_: any,
|
||||
swipeable: Swipeable
|
||||
) => {
|
||||
const translate = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 0],
|
||||
extrapolate: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
tw`ml-5 mr-3 flex flex-row items-center gap-2`,
|
||||
{ transform: [{ translateX: translate }] }
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={tw`items-center justify-center rounded-md border border-app-line bg-app-button px-3 py-1.5 shadow-sm`}
|
||||
onPress={() => {
|
||||
navigation.navigate('EditLocationSettings', { id: location.id });
|
||||
swipeable.close();
|
||||
}}
|
||||
>
|
||||
<Pen size={18} color="white" />
|
||||
</Pressable>
|
||||
<DeleteLocationModal
|
||||
locationId={location.id}
|
||||
trigger={
|
||||
<View
|
||||
style={tw`items-center justify-center rounded-md border border-app-line bg-app-button px-3 py-1.5 shadow-sm`}
|
||||
>
|
||||
<Trash size={18} color="white" />
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Pressable onPress={onPress}>
|
||||
<View
|
||||
style={tw`h-auto w-full flex-row justify-between gap-3 rounded-md border border-sidebar-line/50 bg-sidebar-box p-2`}
|
||||
<Swipeable
|
||||
containerStyle={tw`rounded-md border border-sidebar-line/50 bg-sidebar-box`}
|
||||
enableTrackpadTwoFingerGesture
|
||||
renderRightActions={renderRightActions}
|
||||
>
|
||||
<View style={tw`flex-row items-center gap-2`}>
|
||||
<View style={tw`relative`}>
|
||||
<FolderIcon size={42} />
|
||||
<View
|
||||
style={twStyle(
|
||||
'z-5 absolute bottom-[6px] right-[2px] h-2 w-2 rounded-full',
|
||||
online ? 'bg-green-500' : 'bg-red-500'
|
||||
)}
|
||||
/>
|
||||
<View style={tw`h-auto flex-row justify-between gap-3 p-2`}>
|
||||
<View style={tw`w-[50%] flex-row items-center gap-2`}>
|
||||
<View style={tw`relative`}>
|
||||
<FolderIcon size={42} />
|
||||
<View
|
||||
style={twStyle(
|
||||
'z-5 absolute bottom-[6px] right-[2px] h-2 w-2 rounded-full',
|
||||
online ? 'bg-green-500' : 'bg-red-500'
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<Text
|
||||
style={tw`w-auto max-w-[160px] text-sm font-bold text-white`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{location.name}
|
||||
</Text>
|
||||
<Text numberOfLines={1} style={tw`text-xs text-ink-dull`}>
|
||||
{location.path}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
<Text
|
||||
style={tw`w-auto max-w-[160px] text-sm font-bold text-white`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{location.name}
|
||||
</Text>
|
||||
</View>
|
||||
<View style={tw`flex-row items-center gap-3`}>
|
||||
<View style={tw`rounded-md bg-app-input p-1.5`}>
|
||||
<Text
|
||||
style={tw`text-left text-xs font-bold text-ink-dull`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{`${byteSize(location.size_in_bytes)}`}
|
||||
</Text>
|
||||
<View style={tw`flex-row items-center gap-3`}>
|
||||
<View style={tw`rounded-md bg-app-input p-1.5`}>
|
||||
<Text
|
||||
style={tw`text-left text-xs font-bold text-ink-dull`}
|
||||
numberOfLines={1}
|
||||
>
|
||||
{`${byteSize(location.size_in_bytes)}`}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={() => modalRef.current?.present()}>
|
||||
<DotsThreeOutlineVertical
|
||||
weight="fill"
|
||||
size={20}
|
||||
color={tw.color('ink-faint')}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
<Pressable onPress={() => modalRef.current?.present()}>
|
||||
<DotsThreeOutlineVertical
|
||||
weight="fill"
|
||||
size={20}
|
||||
color={tw.color('ink-faint')}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</Swipeable>
|
||||
<LocationModal
|
||||
editLocation={() => {
|
||||
editLocation();
|
||||
|
|
|
@ -1,21 +1,38 @@
|
|||
import { useNavigation } from '@react-navigation/native';
|
||||
import { View } from 'react-native';
|
||||
import { Plus } from 'phosphor-react-native';
|
||||
import { useRef } from 'react';
|
||||
import { Pressable, View } from 'react-native';
|
||||
import { FlatList } from 'react-native-gesture-handler';
|
||||
import { useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { BrowseTagItem } from '~/components/browse/BrowseTags';
|
||||
import { TagItem } from '~/components/browse/BrowseTags';
|
||||
import Fade from '~/components/layout/Fade';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import CreateTagModal from '~/components/modal/tag/CreateTagModal';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { BrowseStackScreenProps } from '~/navigation/tabs/BrowseStack';
|
||||
|
||||
export default function Tags() {
|
||||
interface Props {
|
||||
viewStyle?: 'grid' | 'list';
|
||||
}
|
||||
|
||||
export default function Tags({ viewStyle = 'list' }: Props) {
|
||||
const tags = useLibraryQuery(['tags.list']);
|
||||
const navigation = useNavigation<BrowseStackScreenProps<'Browse'>['navigation']>();
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
useNodes(tags.data?.nodes);
|
||||
const tagData = useCache(tags.data?.items);
|
||||
return (
|
||||
<ScreenContainer scrollview={false} style={tw`relative py-0 px-7`}>
|
||||
<ScreenContainer scrollview={false} style={tw`relative px-7 py-0`}>
|
||||
<Pressable
|
||||
style={tw`absolute bottom-7 right-7 z-10 flex h-12 w-12 items-center justify-center rounded-full bg-accent`}
|
||||
onPress={() => {
|
||||
modalRef.current?.present();
|
||||
}}
|
||||
>
|
||||
<Plus size={20} weight="bold" style={tw`text-ink`} />
|
||||
</Pressable>
|
||||
<Fade
|
||||
fadeSides="top-bottom"
|
||||
orientation="vertical"
|
||||
|
@ -26,16 +43,20 @@ export default function Tags() {
|
|||
<FlatList
|
||||
data={tagData}
|
||||
renderItem={({ item }) => (
|
||||
<BrowseTagItem
|
||||
tagStyle="w-[105px]"
|
||||
<TagItem
|
||||
tagStyle={twStyle(viewStyle === 'grid' ? 'w-[105px]' : 'w-full')}
|
||||
viewStyle={viewStyle}
|
||||
tag={item}
|
||||
onPress={() =>
|
||||
navigation.navigate('Tag', { id: item.id, color: item.color! })
|
||||
}
|
||||
onPress={() => {
|
||||
navigation.navigate('BrowseStack', {
|
||||
screen: 'Tag',
|
||||
params: { id: item.id, color: item.color! }
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
numColumns={3}
|
||||
columnWrapperStyle={tw`gap-2.5`}
|
||||
numColumns={viewStyle === 'grid' ? 3 : 1}
|
||||
columnWrapperStyle={viewStyle === 'grid' && tw`justify-between`}
|
||||
horizontal={false}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
showsHorizontalScrollIndicator={false}
|
||||
|
@ -43,6 +64,7 @@ export default function Tags() {
|
|||
contentContainerStyle={tw`py-5`}
|
||||
/>
|
||||
</Fade>
|
||||
<CreateTagModal ref={modalRef} />
|
||||
</ScreenContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ export default function NetworkScreen({ navigation }: NetworkStackScreenProps<'N
|
|||
<ScreenContainer scrollview={false} style={tw`items-center justify-center gap-0`}>
|
||||
<Icon name="Globe" size={128} />
|
||||
<Text style={tw`mt-4 text-lg font-bold text-white`}>Your Local Network</Text>
|
||||
<Text style={tw`max-w-sm mt-1 text-sm text-center text-ink-dull`}>
|
||||
<Text style={tw`mt-1 max-w-sm text-center text-sm text-ink-dull`}>
|
||||
Other Spacedrive nodes on your LAN will appear here, along with your default OS
|
||||
network mounts.
|
||||
</Text>
|
||||
|
|
|
@ -141,10 +141,10 @@ export default function SettingsScreen({ navigation }: SettingsStackScreenProps<
|
|||
const debugState = useDebugState();
|
||||
|
||||
return (
|
||||
<ScreenContainer tabHeight={false} scrollview={false} style={tw`relative gap-0 py-0 px-7`}>
|
||||
<ScreenContainer tabHeight={false} scrollview={false} style={tw`relative gap-0 px-7 py-0`}>
|
||||
<SectionList
|
||||
sections={sections(debugState)}
|
||||
contentContainerStyle={tw`h-auto pt-3 pb-5`}
|
||||
contentContainerStyle={tw`h-auto pb-5 pt-3`}
|
||||
renderItem={({ item }) => (
|
||||
<SettingsItem
|
||||
title={item.title}
|
||||
|
|
|
@ -2,6 +2,7 @@ import { CheckCircle } from 'phosphor-react-native';
|
|||
import React, { useState } from 'react';
|
||||
import { ColorValue, Pressable, ScrollView, Text, View, ViewStyle } from 'react-native';
|
||||
import { Themes, useThemeStore } from '@sd/client';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||
import Colors from '~/constants/style/Colors';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
@ -70,7 +71,7 @@ function Theme(props: ThemeProps) {
|
|||
{/* Checkmark */}
|
||||
{props.isSelected && (
|
||||
<CheckCircle
|
||||
color={props.textColor as string}
|
||||
color={tw.color('accent')}
|
||||
weight="fill"
|
||||
size={24}
|
||||
style={tw`absolute bottom-1.5 right-1.5`}
|
||||
|
@ -83,7 +84,7 @@ function Theme(props: ThemeProps) {
|
|||
|
||||
function SystemTheme(props: { isSelected: boolean }) {
|
||||
return (
|
||||
<View style={tw`h-[90px] w-[110px] flex-1 flex-row overflow-hidden rounded-xl`}>
|
||||
<View style={tw`h-[80px] w-[110px] flex-row overflow-hidden rounded-xl`}>
|
||||
<View
|
||||
style={twStyle('flex-1 overflow-hidden', {
|
||||
backgroundColor: themes[1]!.outsideColor
|
||||
|
@ -94,7 +95,7 @@ function SystemTheme(props: { isSelected: boolean }) {
|
|||
</View>
|
||||
</View>
|
||||
<View
|
||||
style={twStyle(' flex-1 overflow-hidden', {
|
||||
style={twStyle('flex-1 overflow-hidden', {
|
||||
backgroundColor: themes[0]!.outsideColor
|
||||
})}
|
||||
>
|
||||
|
@ -103,7 +104,7 @@ function SystemTheme(props: { isSelected: boolean }) {
|
|||
{/* Checkmark */}
|
||||
{props.isSelected && (
|
||||
<CheckCircle
|
||||
color={'black'}
|
||||
color={tw.color('accent')}
|
||||
weight="fill"
|
||||
size={24}
|
||||
style={tw`absolute bottom-1.5 right-1.5`}
|
||||
|
@ -125,33 +126,30 @@ const AppearanceSettingsScreen = ({
|
|||
// TODO: Hook this up to the theme store once light theme is fixed.
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 pt-4`}>
|
||||
<View style={tw`px-4`}>
|
||||
<SettingsTitle>Theme</SettingsTitle>
|
||||
<View style={tw`mb-4 border-b border-b-app-line`} />
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={tw`gap-x-3`}
|
||||
>
|
||||
{themes.map((theme) => (
|
||||
<Pressable
|
||||
key={theme.themeValue}
|
||||
onPress={() => setSelectedTheme(theme.themeValue)}
|
||||
>
|
||||
{theme.themeValue === 'system' ? (
|
||||
<SystemTheme isSelected={selectedTheme === 'system'} />
|
||||
) : (
|
||||
<Theme {...theme} isSelected={selectedTheme === theme.themeValue} />
|
||||
)}
|
||||
<Text style={tw`mt-1.5 text-center font-medium text-white`}>
|
||||
{theme.themeName}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</View>
|
||||
<ScreenContainer scrollview={false} style={tw`gap-2 px-7`}>
|
||||
<SettingsTitle>Theme</SettingsTitle>
|
||||
<ScrollView
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
contentContainerStyle={tw`gap-x-3`}
|
||||
>
|
||||
{themes.map((theme) => (
|
||||
<Pressable
|
||||
key={theme.themeValue}
|
||||
onPress={() => setSelectedTheme(theme.themeValue)}
|
||||
>
|
||||
{theme.themeValue === 'system' ? (
|
||||
<SystemTheme isSelected={selectedTheme === 'system'} />
|
||||
) : (
|
||||
<Theme {...theme} isSelected={selectedTheme === theme.themeValue} />
|
||||
)}
|
||||
<Text style={tw`mt-1.5 text-center font-medium text-white`}>
|
||||
{theme.themeName}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</ScrollView>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||
|
||||
|
@ -7,9 +8,9 @@ const ExtensionsSettingsScreen = ({
|
|||
navigation
|
||||
}: SettingsStackScreenProps<'ExtensionsSettings'>) => {
|
||||
return (
|
||||
<View>
|
||||
<ScreenContainer style={tw`px-7`}>
|
||||
<Text style={tw`text-ink`}>TODO</Text>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Text, View } from 'react-native';
|
|||
import { useBridgeQuery, useDebugState } from '@sd/client';
|
||||
import { Input } from '~/components/form/Input';
|
||||
import Card from '~/components/layout/Card';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { Divider } from '~/components/primitive/Divider';
|
||||
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
|
@ -15,7 +16,7 @@ const GeneralSettingsScreen = ({ navigation }: SettingsStackScreenProps<'General
|
|||
if (!node) return null;
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 p-4`}>
|
||||
<ScreenContainer style={tw`justify-start gap-0 px-7`} scrollview={false}>
|
||||
<Card style={tw`bg-app-box`}>
|
||||
{/* Card Header */}
|
||||
<View style={tw`flex flex-row justify-between`}>
|
||||
|
@ -34,9 +35,9 @@ const GeneralSettingsScreen = ({ navigation }: SettingsStackScreenProps<'General
|
|||
{/* Divider */}
|
||||
<Divider style={tw`mb-4 mt-2`} />
|
||||
{/* Node Name and Port */}
|
||||
<SettingsTitle>Node Name</SettingsTitle>
|
||||
<SettingsTitle style={tw`mb-1`}>Node Name</SettingsTitle>
|
||||
<Input value={node.name} />
|
||||
<SettingsTitle style={tw`mt-3`}>Node Port</SettingsTitle>
|
||||
<SettingsTitle style={tw`mb-1 mt-3`}>Node Port</SettingsTitle>
|
||||
<Input value={node.p2p_port?.toString() ?? '5795'} keyboardType="numeric" />
|
||||
</Card>
|
||||
{debugState.enabled && (
|
||||
|
@ -45,12 +46,12 @@ const GeneralSettingsScreen = ({ navigation }: SettingsStackScreenProps<'General
|
|||
<Text style={tw`font-semibold text-ink`}>Debug</Text>
|
||||
{/* Divider */}
|
||||
<Divider style={tw`mb-4 mt-2`} />
|
||||
<SettingsTitle>Data Folder</SettingsTitle>
|
||||
<SettingsTitle style={tw`mb-1`}>Data Folder</SettingsTitle>
|
||||
{/* Useful for simulator, not so for real devices. */}
|
||||
<Input value={node.data_path} />
|
||||
</Card>
|
||||
)}
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,9 @@ import React, { useEffect, useRef } from 'react';
|
|||
import { Animated, FlatList, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { LibraryConfigWrapped, useBridgeQuery, useCache, useNodes } from '@sd/client';
|
||||
import Fade from '~/components/layout/Fade';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import DeleteLibraryModal from '~/components/modal/confirmModals/DeleteLibraryModal';
|
||||
import { AnimatedButton, FakeButton } from '~/components/primitive/Button';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
|
@ -56,12 +58,12 @@ function LibraryItem({
|
|||
enableTrackpadTwoFingerGesture
|
||||
renderRightActions={renderRightActions}
|
||||
>
|
||||
<View style={tw`flex flex-row items-center justify-between`}>
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
<View>
|
||||
<Text style={tw`font-semibold text-ink`}>{library.config.name}</Text>
|
||||
<Text style={tw`mt-0.5 text-xs text-ink-dull`}>{library.uuid}</Text>
|
||||
<Text style={tw`text-md font-semibold text-ink`}>{library.config.name}</Text>
|
||||
<Text style={tw`mt-1 text-xs text-ink-dull`}>{library.uuid}</Text>
|
||||
</View>
|
||||
<CaretRight color={tw.color('ink-dull')} size={18} />
|
||||
<CaretRight color={tw.color('ink')} size={20} />
|
||||
</View>
|
||||
</Swipeable>
|
||||
);
|
||||
|
@ -90,15 +92,24 @@ const LibrarySettingsScreen = ({ navigation }: SettingsStackScreenProps<'Library
|
|||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 px-3 py-4`}>
|
||||
<FlatList
|
||||
data={libraries}
|
||||
keyExtractor={(item) => item.uuid}
|
||||
renderItem={({ item, index }) => (
|
||||
<LibraryItem navigation={navigation} library={item} index={index} />
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<ScreenContainer style={tw`justify-start gap-0 px-7 py-0`} scrollview={false}>
|
||||
<Fade
|
||||
fadeSides="top-bottom"
|
||||
orientation="vertical"
|
||||
color="mobile-screen"
|
||||
width={30}
|
||||
height="100%"
|
||||
>
|
||||
<FlatList
|
||||
data={libraries}
|
||||
contentContainerStyle={tw`py-5`}
|
||||
keyExtractor={(item) => item.uuid}
|
||||
renderItem={({ item, index }) => (
|
||||
<LibraryItem navigation={navigation} library={item} index={index} />
|
||||
)}
|
||||
/>
|
||||
</Fade>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
|
||||
const PrivacySettingsScreen = () => {
|
||||
return (
|
||||
<View>
|
||||
<ScreenContainer scrollview={false} style={tw`px-7`}>
|
||||
<Text style={tw`text-ink`}>TODO</Text>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import React from 'react';
|
|||
import { Image, Linking, Platform, Text, View } from 'react-native';
|
||||
import { useBridgeQuery } from '@sd/client';
|
||||
import { DiscordIcon, GitHubIcon } from '~/components/icons/Brands';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import { Divider } from '~/components/primitive/Divider';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
|
@ -11,7 +12,7 @@ const AboutScreen = () => {
|
|||
const buildInfo = useBridgeQuery(['buildInfo']);
|
||||
|
||||
return (
|
||||
<View style={tw.style('flex-1 p-5')}>
|
||||
<ScreenContainer style={tw`justify-start gap-0 px-7 py-5`}>
|
||||
<View style={tw.style('flex flex-row items-center')}>
|
||||
<Image
|
||||
source={require('../../../../assets/icon.png')}
|
||||
|
@ -46,19 +47,19 @@ const AboutScreen = () => {
|
|||
<View style={tw.style('h-4 w-4')}>
|
||||
<DiscordIcon fill="white" />
|
||||
</View>
|
||||
<Text style={tw.style('ml-2 text-white')}>Join Discord</Text>
|
||||
<Text style={tw.style('ml-2 text-white font-bold')}>Join Discord</Text>
|
||||
</Button>
|
||||
|
||||
{/* GitHub Button */}
|
||||
<Button
|
||||
onPress={() => Linking.openURL('https://github.com/spacedriveapp/spacedrive')}
|
||||
style={tw.style('flex-row items-center')}
|
||||
style={tw.style('flex-row items-center font-bold')}
|
||||
variant="accent"
|
||||
>
|
||||
<View style={tw.style('h-4 w-4')}>
|
||||
<GitHubIcon fill="white" />
|
||||
</View>
|
||||
<Text style={tw.style('ml-2 text-white')}>Star on GitHub</Text>
|
||||
<Text style={tw.style('ml-2 text-white font-bold')}>Star on GitHub</Text>
|
||||
</Button>
|
||||
|
||||
{/* Website Button */}
|
||||
|
@ -68,9 +69,9 @@ const AboutScreen = () => {
|
|||
variant="accent"
|
||||
>
|
||||
<View style={tw.style('h-4 w-4')}>
|
||||
<Globe size={16} color="white" />
|
||||
<Globe weight="bold" size={16} color="white" />
|
||||
</View>
|
||||
<Text style={tw.style('ml-2 text-white')}>Website</Text>
|
||||
<Text style={tw.style('ml-2 text-white font-bold')}>Website</Text>
|
||||
</Button>
|
||||
</View>
|
||||
<Divider />
|
||||
|
@ -92,16 +93,16 @@ const AboutScreen = () => {
|
|||
<Text style={tw.style('my-5 text-lg font-bold text-ink')}>
|
||||
Meet the contributors behind Spacedrive
|
||||
</Text>
|
||||
{/* For some reason, it won't load. ¯\_(ツ)_/¯ */}
|
||||
{/* Temporary image url approach until a solution is reached */}
|
||||
<Image
|
||||
source={{
|
||||
uri: 'https://contrib.rocks/image?repo=spacedriveapp/spacedrive&columns=12&anon=1'
|
||||
uri: 'https://i.imgur.com/SwUcWHP.png'
|
||||
}}
|
||||
style={{ height: 200, width: '100%' }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -2,20 +2,16 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||
import { Archive, ArrowsClockwise, Trash } from 'phosphor-react-native';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { Alert, ScrollView, Text, View } from 'react-native';
|
||||
import { Alert, Text, View } from 'react-native';
|
||||
import { z } from 'zod';
|
||||
import { useLibraryMutation, useLibraryQuery, useNormalisedCache, useZodForm } from '@sd/client';
|
||||
import { Input } from '~/components/form/Input';
|
||||
import { Switch } from '~/components/form/Switch';
|
||||
import DeleteLocationModal from '~/components/modal/confirmModals/DeleteLocationModal';
|
||||
import { AnimatedButton, FakeButton } from '~/components/primitive/Button';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { AnimatedButton } from '~/components/primitive/Button';
|
||||
import { Divider } from '~/components/primitive/Divider';
|
||||
import {
|
||||
SettingsContainer,
|
||||
SettingsInputInfo,
|
||||
SettingsTitle
|
||||
} from '~/components/settings/SettingsContainer';
|
||||
import { SettingsItem } from '~/components/settings/SettingsItem';
|
||||
import SettingsButton from '~/components/settings/SettingsButton';
|
||||
import { SettingsInputInfo, SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||
import SettingsToggle from '~/components/settings/SettingsToggle';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||
|
||||
|
@ -113,10 +109,10 @@ const EditLocationSettingsScreen = ({
|
|||
const fullRescan = useLibraryMutation('locations.fullRescan');
|
||||
|
||||
return (
|
||||
<ScrollView contentContainerStyle={tw`gap-y-6 pb-12 pt-4`}>
|
||||
<ScreenContainer style={tw`px-7`}>
|
||||
{/* Inputs */}
|
||||
<View style={tw`px-2`}>
|
||||
<SettingsTitle>Display Name</SettingsTitle>
|
||||
<View>
|
||||
<SettingsTitle style={tw`mb-1`}>Display Name</SettingsTitle>
|
||||
<Controller
|
||||
name="displayName"
|
||||
control={form.control}
|
||||
|
@ -129,7 +125,7 @@ const EditLocationSettingsScreen = ({
|
|||
not rename the actual folder on disk.
|
||||
</SettingsInputInfo>
|
||||
|
||||
<SettingsTitle style={tw`mt-3`}>Local Path</SettingsTitle>
|
||||
<SettingsTitle style={tw`mb-1 mt-3`}>Local Path</SettingsTitle>
|
||||
<Controller
|
||||
name="localPath"
|
||||
control={form.control}
|
||||
|
@ -144,94 +140,62 @@ const EditLocationSettingsScreen = ({
|
|||
<Divider style={tw`my-0`} />
|
||||
{/* Switches */}
|
||||
<View style={tw`gap-y-6`}>
|
||||
<SettingsContainer>
|
||||
<SettingsItem
|
||||
title="Generate preview media"
|
||||
rightArea={
|
||||
<Controller
|
||||
name="generatePreviewMedia"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch value={value ?? undefined} onValueChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsItem
|
||||
title="Sync preview media with your devices"
|
||||
rightArea={
|
||||
<Controller
|
||||
name="syncPreviewMedia"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch value={value ?? undefined} onValueChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsItem
|
||||
title="Hide location and contents from view"
|
||||
rightArea={
|
||||
<Controller
|
||||
name="hidden"
|
||||
control={form.control}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<Switch value={value ?? undefined} onValueChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
<SettingsToggle
|
||||
name="generatePreviewMedia"
|
||||
control={form.control}
|
||||
title="Generate preview media"
|
||||
/>
|
||||
<SettingsToggle
|
||||
control={form.control}
|
||||
name="syncPreviewMedia"
|
||||
title="Sync preview media with your devices"
|
||||
/>
|
||||
<SettingsToggle
|
||||
control={form.control}
|
||||
name="hidden"
|
||||
title="Hide location and contents from view"
|
||||
/>
|
||||
</View>
|
||||
{/* Indexer Rules */}
|
||||
<Text style={tw`text-center text-xs font-bold text-white`}>TODO: Indexer Rules</Text>
|
||||
{/* Buttons */}
|
||||
<View style={tw`gap-y-6`}>
|
||||
<SettingsContainer description="Perform a full rescan of this Location.">
|
||||
<SettingsItem
|
||||
title="Full Reindex"
|
||||
rightArea={
|
||||
<AnimatedButton
|
||||
size="sm"
|
||||
onPress={() =>
|
||||
fullRescan.mutate({ location_id: id, reidentify_objects: true })
|
||||
}
|
||||
>
|
||||
<ArrowsClockwise color="white" size={20} />
|
||||
</AnimatedButton>
|
||||
}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
<SettingsContainer description="Extract data from Library as an archive, useful to preserve Location folder structure.">
|
||||
<SettingsItem
|
||||
title="Archive"
|
||||
rightArea={
|
||||
<AnimatedButton
|
||||
size="sm"
|
||||
onPress={() => Alert.alert('Archiving locations is coming soon...')}
|
||||
>
|
||||
<Archive color="white" size={20} />
|
||||
</AnimatedButton>
|
||||
}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
<SettingsContainer description="This will not delete the actual folder on disk. Preview media will be...???">
|
||||
<SettingsItem
|
||||
title="Delete"
|
||||
rightArea={
|
||||
<DeleteLocationModal
|
||||
locationId={id}
|
||||
trigger={
|
||||
<FakeButton size="sm" variant="danger">
|
||||
<Trash color={tw.color('ink')} size={20} />
|
||||
</FakeButton>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</SettingsContainer>
|
||||
<SettingsButton
|
||||
title="Reindex"
|
||||
description="Perform a full rescan of this location"
|
||||
buttonPress={() =>
|
||||
fullRescan.mutate({ location_id: id, reidentify_objects: true })
|
||||
}
|
||||
buttonText="Full Reindex"
|
||||
buttonIcon={<ArrowsClockwise color="white" size={20} />}
|
||||
buttonTextStyle="text-white font-bold"
|
||||
buttonVariant="outline"
|
||||
infoContainerStyle={'w-[50%]'}
|
||||
/>
|
||||
<SettingsButton
|
||||
title="Archive Location"
|
||||
description="Extract data from Library as an archive, useful to preserve Location folder structure."
|
||||
buttonText="Archive"
|
||||
buttonIcon={<Archive color="white" size={20} />}
|
||||
buttonPress={() => Alert.alert('Archiving locations is coming soon...')}
|
||||
buttonVariant="outline"
|
||||
buttonTextStyle="text-white font-bold"
|
||||
infoContainerStyle={'w-[60%]'}
|
||||
/>
|
||||
<SettingsButton
|
||||
title="Delete Location"
|
||||
description="This will not delete the actual folder on disk. Preview media will be...???"
|
||||
buttonText="Delete"
|
||||
buttonIcon={<Trash color="white" size={20} />}
|
||||
buttonPress={() => Alert.alert('Deleting locations is coming soon...')}
|
||||
buttonVariant="danger"
|
||||
buttonTextStyle="text-white font-bold"
|
||||
infoContainerStyle={'w-[60%]'}
|
||||
/>
|
||||
{/* Indexer Rules */}
|
||||
<Text style={tw`text-center text-xs font-bold text-white`}>
|
||||
TODO: Indexer Rules
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import { Trash } from 'phosphor-react-native';
|
||||
import React from 'react';
|
||||
import { Controller } from 'react-hook-form';
|
||||
import { Alert, Text, View } from 'react-native';
|
||||
import { Text, View } from 'react-native';
|
||||
import { TouchableOpacity } from 'react-native-gesture-handler';
|
||||
import { z } from 'zod';
|
||||
import { useBridgeMutation, useLibraryContext, useZodForm } from '@sd/client';
|
||||
import { Input } from '~/components/form/Input';
|
||||
import { Switch } from '~/components/form/Switch';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import DeleteLibraryModal from '~/components/modal/confirmModals/DeleteLibraryModal';
|
||||
import { FakeButton } from '~/components/primitive/Button';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import { Divider } from '~/components/primitive/Divider';
|
||||
import { SettingsContainer, SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||
import { SettingsItem } from '~/components/settings/SettingsItem';
|
||||
import SettingsButton from '~/components/settings/SettingsButton';
|
||||
import { SettingsTitle } from '~/components/settings/SettingsContainer';
|
||||
import SettingsToggle from '~/components/settings/SettingsToggle';
|
||||
import { useAutoForm } from '~/hooks/useAutoForm';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||
|
@ -37,9 +37,9 @@ const LibraryGeneralSettingsScreen = (_: SettingsStackScreenProps<'LibraryGenera
|
|||
});
|
||||
|
||||
return (
|
||||
<View style={tw`gap-4`}>
|
||||
<View style={tw`mt-4 px-2`}>
|
||||
<SettingsTitle>Name</SettingsTitle>
|
||||
<ScreenContainer scrollview={false} style={tw`justify-start px-7 py-0`}>
|
||||
<View style={tw`pt-5`}>
|
||||
<SettingsTitle style={tw`mb-1`}>Name</SettingsTitle>
|
||||
<Controller
|
||||
name="name"
|
||||
control={form.control}
|
||||
|
@ -47,8 +47,7 @@ const LibraryGeneralSettingsScreen = (_: SettingsStackScreenProps<'LibraryGenera
|
|||
<Input onBlur={onBlur} onChangeText={onChange} value={value} />
|
||||
)}
|
||||
/>
|
||||
{/* Description */}
|
||||
<SettingsTitle style={tw`mt-4`}>Description</SettingsTitle>
|
||||
<SettingsTitle style={tw`mb-1 mt-4`}>Description</SettingsTitle>
|
||||
<Controller
|
||||
name="description"
|
||||
control={form.control}
|
||||
|
@ -60,15 +59,46 @@ const LibraryGeneralSettingsScreen = (_: SettingsStackScreenProps<'LibraryGenera
|
|||
<Divider />
|
||||
<View style={tw`gap-y-6`}>
|
||||
{/* Encrypt */}
|
||||
<SettingsContainer description="Enable encryption for this library, this will only encrypt the Spacedrive database, not the files themselves.">
|
||||
<SettingsItem title="Encrypt Library" rightArea={<Switch value={true} />} />
|
||||
</SettingsContainer>
|
||||
<SettingsToggle
|
||||
onEnabledChange={(enabled) => {
|
||||
//TODO: Enable encryption
|
||||
}}
|
||||
title="Encrypt Library"
|
||||
description="Enable encryption for this library, this will only encrypt the Spacedrive database, not the files themselves."
|
||||
/>
|
||||
{/* Export */}
|
||||
<SettingsItem title="Export Library" onPress={() => Alert.alert('TODO')} />
|
||||
<SettingsButton
|
||||
description="Export this library to a file."
|
||||
buttonText="Export"
|
||||
buttonPress={() => {
|
||||
//TODO: Export library
|
||||
}}
|
||||
buttonTextStyle="font-bold text-ink-dull"
|
||||
title="Export Library"
|
||||
/>
|
||||
{/* Delete Library */}
|
||||
<DeleteLibraryModal trigger={<Text>Delete</Text>} libraryUuid={library.uuid} />
|
||||
<View style={tw`flex-row items-center justify-between`}>
|
||||
<View style={tw`w-[73%]`}>
|
||||
<Text style={tw`text-sm font-medium text-ink`}>Delete Library</Text>
|
||||
<Text style={tw`mt-1 text-xs text-ink-dull`}>
|
||||
This is permanent, your files not be deleted, only the Spacedrive
|
||||
library.
|
||||
</Text>
|
||||
</View>
|
||||
<DeleteLibraryModal
|
||||
trigger={
|
||||
//Needed to make button work
|
||||
<TouchableOpacity activeOpacity={1}>
|
||||
<Button variant="danger">
|
||||
<Text style={tw`font-bold text-ink`}>Delete</Text>
|
||||
</Button>
|
||||
</TouchableOpacity>
|
||||
}
|
||||
libraryUuid={library.uuid}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,170 +1,7 @@
|
|||
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 {
|
||||
arraysEqual,
|
||||
Location,
|
||||
useCache,
|
||||
useLibraryMutation,
|
||||
useLibraryQuery,
|
||||
useNodes,
|
||||
useOnlineLocations
|
||||
} from '@sd/client';
|
||||
import FolderIcon from '~/components/icons/FolderIcon';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import DeleteLocationModal from '~/components/modal/confirmModals/DeleteLocationModal';
|
||||
import ImportModal from '~/components/modal/ImportModal';
|
||||
import { AnimatedButton } from '~/components/primitive/Button';
|
||||
import { tw, twStyle } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||
import { Locations } from '~/screens/Locations';
|
||||
|
||||
type LocationItemProps = {
|
||||
location: Location;
|
||||
index: number;
|
||||
navigation: SettingsStackScreenProps<'LocationSettings'>['navigation'];
|
||||
};
|
||||
|
||||
function LocationItem({ location, index, navigation }: LocationItemProps) {
|
||||
const fullRescan = useLibraryMutation('locations.fullRescan', {
|
||||
onMutate: () => {
|
||||
// TODO: Show Toast
|
||||
}
|
||||
});
|
||||
|
||||
const onlineLocations = useOnlineLocations();
|
||||
|
||||
const renderRightActions = (
|
||||
progress: Animated.AnimatedInterpolation<number>,
|
||||
_: any,
|
||||
swipeable: Swipeable
|
||||
) => {
|
||||
const translate = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 0],
|
||||
extrapolate: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
tw`mr-3 flex flex-row items-center gap-2`,
|
||||
{ transform: [{ translateX: translate }] }
|
||||
]}
|
||||
>
|
||||
<Pressable
|
||||
style={tw`items-center justify-center rounded-md border border-app-line bg-app-button px-3 py-1.5 shadow-sm`}
|
||||
onPress={() => {
|
||||
navigation.navigate('EditLocationSettings', { id: location.id });
|
||||
swipeable.close();
|
||||
}}
|
||||
>
|
||||
<Pen size={18} color="white" />
|
||||
</Pressable>
|
||||
<DeleteLocationModal
|
||||
locationId={location.id}
|
||||
trigger={
|
||||
<View
|
||||
style={tw`items-center justify-center rounded-md border border-app-line bg-app-button px-3 py-1.5 shadow-sm`}
|
||||
>
|
||||
<Trash size={18} color="white" />
|
||||
</View>
|
||||
}
|
||||
/>
|
||||
{/* Full Re-scan IS too much here */}
|
||||
<Pressable
|
||||
style={tw`items-center justify-center rounded-md border border-app-line bg-app-button px-3 py-1.5 shadow-sm`}
|
||||
onPress={() =>
|
||||
fullRescan.mutate({ location_id: location.id, reidentify_objects: true })
|
||||
}
|
||||
>
|
||||
<Repeat size={18} color="white" />
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
containerStyle={twStyle(
|
||||
'rounded-lg border border-app-line bg-app-overlay px-4 py-3',
|
||||
index !== 0 && 'mt-2'
|
||||
)}
|
||||
enableTrackpadTwoFingerGesture
|
||||
renderRightActions={renderRightActions}
|
||||
>
|
||||
<View style={tw`flex flex-row items-center`}>
|
||||
<View style={tw`relative`}>
|
||||
<FolderIcon size={32} />
|
||||
{/* Online/Offline Indicator */}
|
||||
<View
|
||||
style={twStyle(
|
||||
'absolute bottom-0.5 right-0 h-2 w-2 rounded-full',
|
||||
onlineLocations.some((l) => arraysEqual(location.pub_id, l))
|
||||
? 'bg-green-500'
|
||||
: 'bg-red-500'
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
<View style={tw`mx-4 flex-1`}>
|
||||
<Text numberOfLines={1} style={tw`text-sm font-semibold text-ink`}>
|
||||
{location.name}
|
||||
</Text>
|
||||
{/* // TODO: This is ephemeral so it should not come from the DB. Eg. a external USB can move between nodes */}
|
||||
{/* {location.node && (
|
||||
<View style={tw`mt-0.5 self-start rounded bg-app-highlight px-1 py-[1px]`}>
|
||||
<Text numberOfLines={1} style={tw`text-xs font-semibold text-ink-dull`}>
|
||||
{location.node.name}
|
||||
</Text>
|
||||
</View>
|
||||
)} */}
|
||||
<Text
|
||||
numberOfLines={1}
|
||||
style={tw`mt-0.5 text-[10px] font-semibold text-ink-dull`}
|
||||
>
|
||||
{location.path}
|
||||
</Text>
|
||||
</View>
|
||||
<CaretRight color={tw.color('ink-dull')} size={18} />
|
||||
</View>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
|
||||
const LocationSettingsScreen = ({ navigation }: SettingsStackScreenProps<'LocationSettings'>) => {
|
||||
const result = useLibraryQuery(['locations.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const locations = useCache(result.data?.items);
|
||||
|
||||
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
|
||||
data={locations}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item, index }) => (
|
||||
<LocationItem navigation={navigation} location={item} index={index} />
|
||||
)}
|
||||
/>
|
||||
<ImportModal ref={modalRef} />
|
||||
</View>
|
||||
);
|
||||
const LocationSettingsScreen = () => {
|
||||
return <Locations redirectToLocationSettings />;
|
||||
};
|
||||
|
||||
export default LocationSettingsScreen;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Text, View } from 'react-native';
|
||||
import { isEnabled, useBridgeMutation, useDiscoveredPeers } from '@sd/client';
|
||||
import { Button } from '~/components/primitive/Button';
|
||||
import { useDiscoveredPeers } from '@sd/client';
|
||||
import ScreenContainer from '~/components/layout/ScreenContainer';
|
||||
import { tw } from '~/lib/tailwind';
|
||||
import { SettingsStackScreenProps } from '~/navigation/tabs/SettingsStack';
|
||||
|
||||
|
@ -9,7 +9,7 @@ const NodesSettingsScreen = ({ navigation }: SettingsStackScreenProps<'NodesSett
|
|||
const onlineNodes = useDiscoveredPeers();
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ScreenContainer scrollview={false} style={tw`gap-0 px-7`}>
|
||||
<Text style={tw`text-ink`}>Pairing</Text>
|
||||
|
||||
{[...onlineNodes.entries()].map(([id, node]) => (
|
||||
|
@ -17,7 +17,7 @@ const NodesSettingsScreen = ({ navigation }: SettingsStackScreenProps<'NodesSett
|
|||
<Text style={tw`text-ink`}>{node.name}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</ScreenContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,105 +1,16 @@
|
|||
import { CaretRight, Pen, Trash } from 'phosphor-react-native';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Animated, FlatList, Text, View } from 'react-native';
|
||||
import { Swipeable } from 'react-native-gesture-handler';
|
||||
import { Tag, useCache, useLibraryQuery, useNodes } from '@sd/client';
|
||||
import { useRef } from 'react';
|
||||
import { ModalRef } from '~/components/layout/Modal';
|
||||
import DeleteTagModal from '~/components/modal/confirmModals/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/tabs/SettingsStack';
|
||||
|
||||
function TagItem({ tag, index }: { tag: Tag; index: number }) {
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
const renderRightActions = (
|
||||
progress: Animated.AnimatedInterpolation<number>,
|
||||
_dragX: Animated.AnimatedInterpolation<number>,
|
||||
swipeable: Swipeable
|
||||
) => {
|
||||
const translate = progress.interpolate({
|
||||
inputRange: [0, 1],
|
||||
outputRange: [100, 0],
|
||||
extrapolate: 'clamp'
|
||||
});
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
style={[tw`flex flex-row items-center`, { transform: [{ translateX: translate }] }]}
|
||||
>
|
||||
<UpdateTagModal tag={tag} ref={modalRef} onSubmit={() => swipeable.close()} />
|
||||
<AnimatedButton onPress={() => modalRef.current?.present()}>
|
||||
<Pen size={18} color="white" />
|
||||
</AnimatedButton>
|
||||
<DeleteTagModal
|
||||
tagId={tag.id}
|
||||
trigger={
|
||||
<FakeButton style={tw`mx-2`}>
|
||||
<Trash size={18} color="white" />
|
||||
</FakeButton>
|
||||
}
|
||||
/>
|
||||
</Animated.View>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Swipeable
|
||||
containerStyle={twStyle(
|
||||
'rounded-lg border border-app-line bg-app-overlay px-4 py-3',
|
||||
index !== 0 && 'mt-2'
|
||||
)}
|
||||
enableTrackpadTwoFingerGesture
|
||||
renderRightActions={renderRightActions}
|
||||
>
|
||||
<View style={tw`flex flex-row items-center justify-between`}>
|
||||
<View style={tw`flex flex-row`}>
|
||||
<View
|
||||
style={twStyle({ backgroundColor: tag.color! }, 'h-4 w-4 rounded-full')}
|
||||
/>
|
||||
<Text style={tw`ml-3 text-ink`}>{tag.name}</Text>
|
||||
</View>
|
||||
<CaretRight color={tw.color('ink-dull')} size={18} />
|
||||
</View>
|
||||
</Swipeable>
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: Add "New Tag" button
|
||||
|
||||
const TagsSettingsScreen = ({ navigation }: SettingsStackScreenProps<'TagsSettings'>) => {
|
||||
const result = useLibraryQuery(['tags.list']);
|
||||
useNodes(result.data?.nodes);
|
||||
const tags = useCache(result.data?.items);
|
||||
|
||||
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]);
|
||||
import Tags from '~/screens/Tags';
|
||||
|
||||
const TagsSettingsScreen = () => {
|
||||
const modalRef = useRef<ModalRef>(null);
|
||||
|
||||
return (
|
||||
<View style={tw`flex-1 px-3 py-4`}>
|
||||
<FlatList
|
||||
data={tags}
|
||||
keyExtractor={(item) => item.id.toString()}
|
||||
renderItem={({ item, index }) => <TagItem tag={item} index={index} />}
|
||||
/>
|
||||
<>
|
||||
<Tags viewStyle="list" />
|
||||
<CreateTagModal ref={modalRef} />
|
||||
</View>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue