[MOB-58] Settings routes new design & more (#2103)

* wip: redesigning settings pages

* Edit location redesign & more

* right actions

* cleanup
This commit is contained in:
ameer2468 2024-02-19 19:12:11 +03:00 committed by GitHub
parent e8450821df
commit 43360601da
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 655 additions and 587 deletions

View file

@ -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! })

View file

@ -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;
}
};

View file

@ -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 && (

View file

@ -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>

View file

@ -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`}>

View file

@ -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;

View 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;

View file

@ -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');

View 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;

View file

@ -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>
);
}

View file

@ -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();

View file

@ -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>
);
}

View file

@ -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>

View file

@ -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}

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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>
);
};

View file

@ -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;

View file

@ -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>
);
};

View file

@ -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>
</>
);
};