Add navigation element animations

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-16 15:50:55 +03:00
parent 622d88cf40
commit 6e1f7bd81f
19 changed files with 450 additions and 277 deletions

View File

@@ -0,0 +1,101 @@
import React, { useEffect, useRef } from 'react';
import { BottomNavigation } from 'react-native-paper';
import { Animated, StyleSheet } from 'react-native';
import {
CommonActions,
NavigationHelpers,
ParamListBase,
TabNavigationState,
} from '@react-navigation/native';
import { EdgeInsets } from 'react-native-safe-area-context';
import { BottomTabNavigationEventMap } from '@react-navigation/bottom-tabs';
import { BottomTabDescriptorMap } from '@react-navigation/bottom-tabs/lib/typescript/src/types';
import { ROUTE } from '../types';
const hideableBottomNavigationBarStyles = StyleSheet.create({
bar: {
position: 'absolute',
bottom: 0,
left: 0,
right: 0,
},
});
const HideableBottomNavigationBar = ({
navigation,
state,
descriptors,
insets,
visible = true,
routeCallback,
}: {
navigation: NavigationHelpers<ParamListBase, BottomTabNavigationEventMap>;
state: TabNavigationState<ParamListBase>;
descriptors: BottomTabDescriptorMap;
insets: EdgeInsets;
visible?: boolean;
routeCallback?: (route: ROUTE) => void;
}) => {
const visibleAnim = useRef(new Animated.Value(visible ? 0 : 1)).current;
useEffect(() => {
Animated.timing(visibleAnim, {
toValue: visible ? 0 : 1,
duration: visible ? 200 : 150,
useNativeDriver: true,
}).start();
}, [visible, visibleAnim]);
return (
<Animated.View
style={{
marginBottom: insets.bottom,
transform: [
{
translateY: visibleAnim.interpolate({
inputRange: [0, 1],
outputRange: [0, 80],
}),
},
],
...hideableBottomNavigationBarStyles.bar,
}}>
<BottomNavigation.Bar
navigationState={state}
safeAreaInsets={insets}
onTabPress={({ route, preventDefault }) => {
const event = navigation.emit({
type: 'tabPress',
target: route.key,
canPreventDefault: true,
});
if (event.defaultPrevented) {
preventDefault();
} else {
navigation.dispatch({
...CommonActions.navigate(route.name, route.params),
target: state.key,
});
if (routeCallback) routeCallback(route.name as ROUTE);
}
}}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarIcon) {
return options.tabBarIcon({
focused,
color,
size: 22,
});
}
}}
getLabelText={({ route }) => {
const { options } = descriptors[route.key];
return options.title ?? route.name;
}}
/>
</Animated.View>
);
};
export default HideableBottomNavigationBar;

View File

@@ -1,6 +1,5 @@
export { default as FloatingActionButton } from './floatingActionButton'; export { default as FloatingActionButton } from './floatingActionButton';
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
export { default as LoadingView } from './loadingView'; export { default as LoadingView } from './loadingView';
export { default as RootScrollView } from './rootScrollView';
export { default as RootView } from './rootView';
export { default as TagChip } from './tagChip'; export { default as TagChip } from './tagChip';
export { default as TagPreview } from './tagPreview'; export { default as TagPreview } from './tagPreview';

View File

@@ -1,15 +1,21 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator } from 'react-native'; import { ActivityIndicator, View } from 'react-native';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import { RootView } from '.'; import styles from '../styles';
const LoadingView = () => { const LoadingView = () => {
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<RootView centered> <View
style={[
styles.centered,
styles.flex,
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={colors.primary} />
</RootView> </View>
); );
}; };

View File

@@ -1,47 +0,0 @@
import React, { ReactNode } from 'react';
import { StyleProp, ScrollView, ViewStyle } from 'react-native';
import { useTheme } from 'react-native-paper';
import styles from '../styles';
import { useDimensions } from '../contexts';
const RootScrollView = ({
children,
style,
centered,
padded,
}: {
children: ReactNode;
style?: StyleProp<ViewStyle>;
centered?: boolean;
padded?: boolean;
}) => {
const { colors } = useTheme();
const { orientation } = useDimensions();
return (
<ScrollView
contentContainerStyle={[
padded &&
orientation == 'portrait' && [
styles.paddingHorizontal,
styles.paddingTop,
],
padded &&
orientation == 'landscape' && [
styles.paddingHorizontal,
styles.smallPaddingTop,
],
centered && [styles.centered, styles.flex],
styles.fullSize,
centered && [styles.centered, styles.flex],
styles.fullSize,
{ backgroundColor: colors.background },
style,
]}
nestedScrollEnabled>
{children}
</ScrollView>
);
};
export default RootScrollView;

View File

@@ -1,44 +0,0 @@
import React, { ReactNode } from 'react';
import { StyleProp, View, ViewStyle } from 'react-native';
import { useTheme } from 'react-native-paper';
import styles from '../styles';
import { useDimensions } from '../contexts';
const RootView = ({
children,
style,
centered,
padded,
}: {
children: ReactNode;
style?: StyleProp<ViewStyle>;
centered?: boolean;
padded?: boolean;
}) => {
const { colors } = useTheme();
const { orientation } = useDimensions();
return (
<View
style={[
padded &&
orientation == 'portrait' && [
styles.paddingHorizontal,
styles.paddingTop,
],
padded &&
orientation == 'landscape' && [
styles.paddingHorizontal,
styles.smallPaddingTop,
],
centered && [styles.centered, styles.flex],
styles.fullSize,
{ backgroundColor: colors.background },
style,
]}>
{children}
</View>
);
};
export default RootView;

View File

@@ -1,20 +1,26 @@
import React from 'react'; import React from 'react';
import { import { NavigationContainer as NavigationContainerBase } from '@react-navigation/native';
CommonActions,
NavigationContainer as NavigationContainerBase,
} from '@react-navigation/native';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { BottomNavigation, useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import { useSelector } from 'react-redux';
import { Home, Tags, Settings, AddMeme, AddTag } from './screens'; import { Home, Tags, Settings, AddMeme, AddTag } from './screens';
import { darkNavigationTheme, lightNavigationTheme } from './theme'; import { darkNavigationTheme, lightNavigationTheme } from './theme';
import { FloatingActionButton } from './components'; import {
FloatingActionButton,
HideableBottomNavigationBar,
} from './components';
import { ROUTE } from './types'; import { ROUTE } from './types';
import { RootState } from './state';
const TabNavigator = () => { const TabNavigator = () => {
const navVisible = useSelector(
(state: RootState) => state.navigation.navVisible,
);
const [route, setRoute] = React.useState(ROUTE.HOME);
const TabNavigatorBase = createBottomTabNavigator(); const TabNavigatorBase = createBottomTabNavigator();
const [showFab, setShowFab] = React.useState(true);
return ( return (
<> <>
@@ -23,39 +29,13 @@ const TabNavigator = () => {
headerShown: false, headerShown: false,
}} }}
tabBar={({ navigation, state, descriptors, insets }) => ( tabBar={({ navigation, state, descriptors, insets }) => (
<BottomNavigation.Bar <HideableBottomNavigationBar
navigationState={state} navigation={navigation}
safeAreaInsets={insets} state={state}
onTabPress={({ route, preventDefault }) => { descriptors={descriptors}
const event = navigation.emit({ insets={insets}
type: 'tabPress', visible={!!navVisible}
target: route.key, routeCallback={newRoute => setRoute(newRoute)}
canPreventDefault: true,
});
if (event.defaultPrevented) {
preventDefault();
} else {
navigation.dispatch({
...CommonActions.navigate(route.name, route.params),
target: state.key,
});
}
setShowFab((route.name as ROUTE) !== ROUTE.SETTINGS);
}}
renderIcon={({ route, focused, color }) => {
const { options } = descriptors[route.key];
if (options.tabBarIcon) {
return options.tabBarIcon({
focused,
color,
size: 22,
});
}
}}
getLabelText={({ route }) => {
const { options } = descriptors[route.key];
return options.title ?? route.name;
}}
/> />
)}> )}>
<TabNavigatorBase.Screen <TabNavigatorBase.Screen
@@ -86,14 +66,14 @@ const TabNavigator = () => {
}} }}
/> />
</TabNavigatorBase.Navigator> </TabNavigatorBase.Navigator>
<FloatingActionButton visible={showFab} /> <FloatingActionButton visible={navVisible && route !== ROUTE.SETTINGS} />
</> </>
); );
}; };
const NavigationContainer = () => { const NavigationContainer = () => {
const StackNavigatorBase = createNativeStackNavigator();
const theme = useTheme(); const theme = useTheme();
const StackNavigatorBase = createNativeStackNavigator();
return ( return (
<NavigationContainerBase <NavigationContainerBase

View File

@@ -1,10 +1,14 @@
import React from 'react'; import React from 'react';
import { Appbar, Text } from 'react-native-paper'; import { Appbar, Text, useTheme } from 'react-native-paper';
import { RootScrollView } from '../components';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useDimensions } from '../contexts';
import { ScrollView } from 'react-native';
import styles from '../styles';
const AddMeme = () => { const AddMeme = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { colors } = useTheme();
const { orientation } = useDimensions();
return ( return (
<> <>
@@ -12,9 +16,18 @@ const AddMeme = () => {
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title="Add Meme" /> <Appbar.Content title="Add Meme" />
</Appbar.Header> </Appbar.Header>
<RootScrollView centered padded> <ScrollView
contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical,
orientation == 'landscape' && styles.smallPaddingVertical,
styles.paddingHorizontal,
[styles.centered, styles.flex],
styles.fullSize,
{ backgroundColor: colors.background },
]}
nestedScrollEnabled>
<Text>Add Meme</Text> <Text>Add Meme</Text>
</RootScrollView> </ScrollView>
</> </>
); );
}; };

View File

@@ -1,15 +1,24 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View } from 'react-native'; import { ScrollView, View } from 'react-native';
import { TextInput, Appbar, HelperText, Button } from 'react-native-paper'; import {
TextInput,
Appbar,
HelperText,
Button,
useTheme,
} from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { BSON } from 'realm'; import { BSON } from 'realm';
import { useRealm } from '@realm/react'; import { useRealm } from '@realm/react';
import { RootScrollView, TagPreview } from '../components'; import { TagPreview } from '../components';
import styles from '../styles'; import styles from '../styles';
import { generateRandomColor, isValidColor } from '../utilities'; import { generateRandomColor, isValidColor } from '../utilities';
import { useDimensions } from '../contexts';
const AddTag = () => { const AddTag = () => {
const navigation = useNavigation(); const navigation = useNavigation();
const { colors } = useTheme();
const { orientation } = useDimensions();
const realm = useRealm(); const realm = useRealm();
const [tagName, setTagName] = useState('newTag'); const [tagName, setTagName] = useState('newTag');
@@ -61,9 +70,16 @@ const AddTag = () => {
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title="Add Tag" /> <Appbar.Content title="Add Tag" />
</Appbar.Header> </Appbar.Header>
<RootScrollView <ScrollView
padded contentContainerStyle={[
style={[styles.flexGrow, styles.flexColumnSpaceBetween]}> orientation == 'portrait' && styles.paddingVertical,
orientation == 'landscape' && styles.smallPaddingVertical,
styles.paddingHorizontal,
styles.fullSize,
styles.flexGrow,
styles.flexColumnSpaceBetween,
{ backgroundColor: colors.background },
]}>
<View style={[styles.flex, styles.justifyStart]}> <View style={[styles.flex, styles.justifyStart]}>
<TagPreview name={tagName} color={validatedTagColor} /> <TagPreview name={tagName} color={validatedTagColor} />
<TextInput <TextInput
@@ -105,7 +121,7 @@ const AddTag = () => {
Save Save
</Button> </Button>
</View> </View>
</RootScrollView> </ScrollView>
</> </>
); );
}; };

View File

@@ -1,5 +1,6 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { useQuery } from '@realm/react';
import { import {
Button, Button,
Menu, Menu,
@@ -10,9 +11,8 @@ import {
HelperText, HelperText,
} from 'react-native-paper'; } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { RootScrollView } from '../components';
import styles from '../styles'; import styles from '../styles';
import { HOME_SORT, SORT_DIRECTION } from '../types'; import { MEME_SORT, SORT_DIRECTION } from '../types';
import { getSortIcon, getViewIcon } from '../utilities'; import { getSortIcon, getViewIcon } from '../utilities';
import { import {
RootState, RootState,
@@ -24,7 +24,7 @@ import {
setHomeFilter, setHomeFilter,
} from '../state'; } from '../state';
import { MEME_TYPE, Meme, memeTypePlural } from '../database'; import { MEME_TYPE, Meme, memeTypePlural } from '../database';
import { useQuery } from '@realm/react'; import { useDimensions } from '../contexts';
const homeStyles = StyleSheet.create({ const homeStyles = StyleSheet.create({
headerButtonView: { headerButtonView: {
@@ -36,7 +36,8 @@ const homeStyles = StyleSheet.create({
}); });
const Home = () => { const Home = () => {
const theme = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions();
const sort = useSelector((state: RootState) => state.home.sort); const sort = useSelector((state: RootState) => state.home.sort);
const sortDirection = useSelector( const sortDirection = useSelector(
(state: RootState) => state.home.sortDirection, (state: RootState) => state.home.sortDirection,
@@ -51,12 +52,12 @@ const Home = () => {
const [sortMenuVisible, setSortMenuVisible] = useState(false); const [sortMenuVisible, setSortMenuVisible] = useState(false);
const [filterMenuVisible, setFilterMenuVisible] = useState(false); const [filterMenuVisible, setFilterMenuVisible] = useState(false);
const handleSortModeChange = (newSort: HOME_SORT) => { const handleSortModeChange = (newSort: MEME_SORT) => {
if (newSort === sort) { if (newSort === sort) {
dispatch(toggleHomeSortDirection()); dispatch(toggleHomeSortDirection());
} else { } else {
dispatch(setHomeSort(newSort)); dispatch(setHomeSort(newSort));
if (newSort === HOME_SORT.TITLE) { if (newSort === MEME_SORT.TITLE) {
dispatch(setHomeSortDirection(SORT_DIRECTION.ASCENDING)); dispatch(setHomeSortDirection(SORT_DIRECTION.ASCENDING));
} else { } else {
dispatch(setHomeSortDirection(SORT_DIRECTION.DESCENDING)); dispatch(setHomeSortDirection(SORT_DIRECTION.DESCENDING));
@@ -74,7 +75,14 @@ const Home = () => {
const memes = useQuery<Meme>(Meme.schema.name); const memes = useQuery<Meme>(Meme.schema.name);
return ( return (
<RootScrollView padded> <View
style={[
orientation == 'portrait' && styles.paddingTop,
orientation == 'landscape' && styles.smallPaddingTop,
styles.paddingHorizontal,
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<Searchbar <Searchbar
placeholder="Search Memes" placeholder="Search Memes"
value={search} value={search}
@@ -99,16 +107,16 @@ const Home = () => {
Sort By: {sort} Sort By: {sort}
</Button> </Button>
}> }>
{Object.keys(HOME_SORT).map(key => { {Object.keys(MEME_SORT).map(key => {
return ( return (
<Menu.Item <Menu.Item
key={key} key={key}
onPress={() => onPress={() =>
handleSortModeChange( handleSortModeChange(
HOME_SORT[key as keyof typeof HOME_SORT], MEME_SORT[key as keyof typeof MEME_SORT],
) )
} }
title={HOME_SORT[key as keyof typeof HOME_SORT]} title={MEME_SORT[key as keyof typeof MEME_SORT]}
/> />
); );
})} })}
@@ -117,13 +125,13 @@ const Home = () => {
<View style={[styles.flexRow, styles.alignCenter]}> <View style={[styles.flexRow, styles.alignCenter]}>
<IconButton <IconButton
icon={getViewIcon(view)} icon={getViewIcon(view)}
iconColor={theme.colors.primary} iconColor={colors.primary}
size={16} size={16}
onPress={() => dispatch(cycleHomeView())} onPress={() => dispatch(cycleHomeView())}
/> />
<IconButton <IconButton
icon={favoritesOnly ? 'heart' : 'heart-outline'} icon={favoritesOnly ? 'heart' : 'heart-outline'}
iconColor={theme.colors.primary} iconColor={colors.primary}
size={16} size={16}
onPress={() => dispatch(toggleHomeFavoritesOnly())} onPress={() => dispatch(toggleHomeFavoritesOnly())}
/> />
@@ -134,7 +142,7 @@ const Home = () => {
<IconButton <IconButton
onPress={() => setFilterMenuVisible(true)} onPress={() => setFilterMenuVisible(true)}
icon={filter ? 'filter' : 'filter-outline'} icon={filter ? 'filter' : 'filter-outline'}
iconColor={theme.colors.primary} iconColor={colors.primary}
size={16} size={16}
/> />
}> }>
@@ -162,13 +170,13 @@ const Home = () => {
<Divider /> <Divider />
{/* TODO: Meme Views */} {/* TODO: Meme Views */}
{memes.length === 0 && ( {memes.length === 0 && (
<View style={styles.alignCenter}> <HelperText
<HelperText type={'info'} style={homeStyles.helperText}> type={'info'}
style={[homeStyles.helperText, styles.centerText]}>
No memes found No memes found
</HelperText> </HelperText>
</View>
)} )}
</RootScrollView> </View>
); );
}; };

View File

@@ -1,5 +1,5 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import { import {
Button, Button,
List, List,
@@ -7,13 +7,13 @@ import {
Snackbar, Snackbar,
Switch, Switch,
Text, Text,
useTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import { openDocumentTree } from 'react-native-scoped-storage'; import { openDocumentTree } from 'react-native-scoped-storage';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { RootScrollView } from '../components'; import type {} from 'redux-thunk/extend-redux';
import styles from '../styles'; import styles from '../styles';
import { RootState, updateNoMedia, updateStorageUri } from '../state'; import { RootState, updateNoMedia, updateStorageUri } from '../state';
import type {} from 'redux-thunk/extend-redux';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
const settingsScreenStyles = StyleSheet.create({ const settingsScreenStyles = StyleSheet.create({
@@ -23,9 +23,10 @@ const settingsScreenStyles = StyleSheet.create({
}); });
const SettingsScreen = () => { const SettingsScreen = () => {
const { colors } = useTheme();
const { orientation, responsive } = useDimensions();
const noMedia = useSelector((state: RootState) => state.settings.noMedia); const noMedia = useSelector((state: RootState) => state.settings.noMedia);
const dispatch = useDispatch(); const dispatch = useDispatch();
const { responsive } = useDimensions();
const [optimizingDatabase, setOptimizingDatabase] = useState(false); const [optimizingDatabase, setOptimizingDatabase] = useState(false);
const [snackbarVisible, setSnackbarVisible] = useState(false); const [snackbarVisible, setSnackbarVisible] = useState(false);
@@ -41,7 +42,14 @@ const SettingsScreen = () => {
return ( return (
<> <>
<RootScrollView padded> <ScrollView
contentContainerStyle={[
orientation == 'portrait' && styles.paddingTop,
orientation == 'landscape' && styles.smallPaddingTop,
styles.paddingHorizontal,
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<View> <View>
<List.Section> <List.Section>
<List.Subheader>Database</List.Subheader> <List.Subheader>Database</List.Subheader>
@@ -86,7 +94,7 @@ const SettingsScreen = () => {
</View> </View>
</List.Section> </List.Section>
</View> </View>
</RootScrollView> </ScrollView>
<Portal> <Portal>
<Snackbar <Snackbar
visible={snackbarVisible} visible={snackbarVisible}

View File

@@ -1,5 +1,12 @@
import React, { useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { StyleSheet, View, Text } from 'react-native'; import {
StyleSheet,
View,
Text,
NativeSyntheticEvent,
NativeScrollEvent,
Animated,
} from 'react-native';
import { import {
Button, Button,
Divider, Divider,
@@ -7,23 +14,33 @@ import {
Menu, Menu,
Searchbar, Searchbar,
TouchableRipple, TouchableRipple,
useTheme,
} from 'react-native-paper'; } from 'react-native-paper';
import { useQuery, useRealm } from '@realm/react'; import { useQuery, useRealm } from '@realm/react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { RootView, TagChip } from '../components'; import { TagChip } from '../components';
import { Tag, deleteTag } from '../database'; import { Tag, deleteTag } from '../database';
import styles from '../styles'; import styles from '../styles';
import { import {
RootState, RootState,
setNavVisible,
setTagsSort, setTagsSort,
setTagsSortDirection, setTagsSortDirection,
toggleTagsSortDirection, toggleTagsSortDirection,
} from '../state'; } from '../state';
import { SORT_DIRECTION, TAG_SORT, tagSortQuery } from '../types'; import { SORT_DIRECTION, TAG_SORT, tagSortQuery } from '../types';
import { getSortIcon } from '../utilities'; import { getSortIcon } from '../utilities';
import { useDimensions } from '../contexts';
const tagStyles = StyleSheet.create({ const tagsStyles = StyleSheet.create({
headerView: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 100,
},
headerButtonView: { headerButtonView: {
height: 50, height: 50,
}, },
@@ -32,7 +49,8 @@ const tagStyles = StyleSheet.create({
justifyContent: 'space-between', justifyContent: 'space-between',
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
padding: 10, paddingVertical: 10,
paddingHorizontal: 15,
}, },
tagView: { tagView: {
flexShrink: 1, flexShrink: 1,
@@ -41,38 +59,23 @@ const tagStyles = StyleSheet.create({
helperText: { helperText: {
marginVertical: 10, marginVertical: 10,
}, },
flashList: {
paddingTop: 125,
paddingBottom: 25,
},
}); });
const TagRow = ({ tag }: { tag: Tag }) => {
const realm = useRealm();
return (
<TouchableRipple onPress={() => deleteTag(realm, tag)}>
<View style={tagStyles.tagRow}>
<View style={tagStyles.tagView}>
<TagChip tag={tag} />
</View>
<Text>{tag.memesLength}</Text>
</View>
</TouchableRipple>
);
};
const ListEmpty = () => {
return (
<View style={styles.alignCenter}>
<HelperText type={'info'} style={tagStyles.helperText}>
No tags found
</HelperText>
</View>
);
};
const Tags = () => { const Tags = () => {
const { colors } = useTheme();
const { orientation } = useDimensions();
const realm = useRealm();
const sort = useSelector((state: RootState) => state.tags.sort); const sort = useSelector((state: RootState) => state.tags.sort);
const sortDirection = useSelector( const sortDirection = useSelector(
(state: RootState) => state.tags.sortDirection, (state: RootState) => state.tags.sortDirection,
); );
const navVisisble = useSelector(
(state: RootState) => state.navigation.navVisible,
);
const dispatch = useDispatch(); const dispatch = useDispatch();
const [sortMenuVisible, setSortMenuVisible] = useState(false); const [sortMenuVisible, setSortMenuVisible] = useState(false);
@@ -91,14 +94,75 @@ const Tags = () => {
setSortMenuVisible(false); setSortMenuVisible(false);
}; };
const sortMenuAnim = useRef(new Animated.Value(navVisisble ? 1 : 0)).current;
useEffect(() => {
Animated.timing(sortMenuAnim, {
toValue: navVisisble ? 1 : 0,
duration: navVisisble ? 200 : 150,
useNativeDriver: true,
}).start();
}, [navVisisble, sortMenuAnim]);
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
const tags = useQuery<Tag>(Tag.schema.name) const tags = useQuery<Tag>(
Tag.schema.name,
collection =>
collection
.filtered(`name CONTAINS[c] "${search}"`) .filtered(`name CONTAINS[c] "${search}"`)
.sorted(tagSortQuery(sort), sortDirection === SORT_DIRECTION.DESCENDING); .sorted(
tagSortQuery(sort),
sortDirection === SORT_DIRECTION.DESCENDING,
),
[search, sort, sortDirection],
);
const [scrollOffset, setScrollOffset] = useState(0);
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentOffset = event.nativeEvent.contentOffset.y;
if (currentOffset <= 150) {
dispatch(setNavVisible(true));
setScrollOffset(0);
return;
}
const diff = currentOffset - scrollOffset;
if (Math.abs(diff) < 50) return;
dispatch(setNavVisible(diff < 0));
setScrollOffset(currentOffset);
};
return ( return (
<RootView padded> <View
style={[
styles.paddingHorizontal,
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<Animated.View
style={[
tagsStyles.headerView,
orientation == 'portrait' && styles.paddingTop,
orientation == 'landscape' && styles.smallPaddingTop,
styles.paddingHorizontal,
{
transform: [
{
translateY: sortMenuAnim.interpolate({
inputRange: [0, 1],
outputRange: [-130, 0],
}),
},
],
},
{
backgroundColor: colors.background,
},
]}>
<Searchbar <Searchbar
placeholder="Search Tags" placeholder="Search Tags"
value={search} value={search}
@@ -110,7 +174,7 @@ const Tags = () => {
style={[ style={[
styles.flexRow, styles.flexRow,
styles.alignCenter, styles.alignCenter,
tagStyles.headerButtonView, tagsStyles.headerButtonView,
]}> ]}>
<Menu <Menu
visible={sortMenuVisible} visible={sortMenuVisible}
@@ -138,14 +202,33 @@ const Tags = () => {
</Menu> </Menu>
</View> </View>
<Divider /> <Divider />
</Animated.View>
<FlashList <FlashList
data={tags} data={tags}
estimatedItemSize={52} estimatedItemSize={52}
renderItem={({ item }) => <TagRow tag={item} />} showsVerticalScrollIndicator={false}
renderItem={({ item: tag }) => (
<TouchableRipple onPress={() => deleteTag(realm, tag)}>
<View style={tagsStyles.tagRow}>
<View style={tagsStyles.tagView}>
<TagChip tag={tag} />
</View>
<Text>{tag.memesLength}</Text>
</View>
</TouchableRipple>
)}
contentContainerStyle={tagsStyles.flashList}
ItemSeparatorComponent={() => <Divider />} ItemSeparatorComponent={() => <Divider />}
ListEmptyComponent={() => <ListEmpty />} ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[tagsStyles.helperText, styles.centerText]}>
No tags found
</HelperText>
)}
onScroll={handleScroll}
/> />
</RootView> </View>
); );
}; };

View File

@@ -1,16 +1,17 @@
import React from 'react'; import React from 'react';
import { Button, Text } from 'react-native-paper'; import { View } from 'react-native';
import { Button, Text, useTheme } from 'react-native-paper';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { openDocumentTree } from 'react-native-scoped-storage'; import { openDocumentTree } from 'react-native-scoped-storage';
import { RootView } from '../components';
import styles from '../styles'; import styles from '../styles';
import { noOp } from '../utilities'; import { noOp } from '../utilities';
import { updateStorageUri } from '../state'; import { updateStorageUri } from '../state';
import { useDimensions } from '../contexts'; import { useDimensions } from '../contexts';
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
const { colors } = useTheme();
const { orientation, responsive } = useDimensions();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { responsive } = useDimensions();
const selectStorageLocation = async () => { const selectStorageLocation = async () => {
const uri = await openDocumentTree(true).catch(noOp); const uri = await openDocumentTree(true).catch(noOp);
@@ -20,7 +21,16 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
}; };
return ( return (
<RootView centered padded> <View
style={[
orientation == 'portrait' && styles.paddingTop,
orientation == 'landscape' && styles.smallPaddingTop,
styles.paddingHorizontal,
styles.centered,
styles.flex,
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<Text <Text
variant="displayMedium" variant="displayMedium"
style={[ style={[
@@ -39,7 +49,7 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
}}> }}>
Select Storage Location Select Storage Location
</Button> </Button>
</RootView> </View>
); );
}; };

View File

@@ -1,9 +1,9 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { HOME_SORT, SORT_DIRECTION, VIEW } from '../types'; import { MEME_SORT, SORT_DIRECTION, VIEW } from '../types';
import { MEME_TYPE } from '../database'; import { MEME_TYPE } from '../database';
interface HomeState { interface HomeState {
sort: HOME_SORT; sort: MEME_SORT;
sortDirection: SORT_DIRECTION; sortDirection: SORT_DIRECTION;
view: VIEW; view: VIEW;
favoritesOnly: boolean; favoritesOnly: boolean;
@@ -11,7 +11,7 @@ interface HomeState {
} }
const initialState: HomeState = { const initialState: HomeState = {
sort: HOME_SORT.TITLE, sort: MEME_SORT.TITLE,
sortDirection: SORT_DIRECTION.ASCENDING, sortDirection: SORT_DIRECTION.ASCENDING,
view: VIEW.MASONRY, view: VIEW.MASONRY,
favoritesOnly: false, favoritesOnly: false,
@@ -22,7 +22,7 @@ const homeSlice = createSlice({
name: 'home', name: 'home',
initialState, initialState,
reducers: { reducers: {
setHomeSort: (state, action: PayloadAction<HOME_SORT>) => { setHomeSort: (state, action: PayloadAction<MEME_SORT>) => {
state.sort = action.payload; state.sort = action.payload;
}, },
setHomeSortDirection: (state, action: PayloadAction<SORT_DIRECTION>) => { setHomeSortDirection: (state, action: PayloadAction<SORT_DIRECTION>) => {

View File

@@ -13,22 +13,26 @@ import { createRealmPersistStorage } from '@bankify/redux-persist-realm';
import settingsReducer from './settings'; import settingsReducer from './settings';
import homeReducer from './home'; import homeReducer from './home';
import tagsReducer from './tags'; import tagsReducer from './tags';
import navigationReducer from './navigation';
const rootReducer = combineReducers({ const rootReducer = combineReducers({
settings: settingsReducer, settings: settingsReducer,
home: homeReducer, home: homeReducer,
tags: tagsReducer, tags: tagsReducer,
navigation: navigationReducer,
}); });
interface RootState { interface RootState {
settings: ReturnType<typeof settingsReducer>; settings: ReturnType<typeof settingsReducer>;
home: ReturnType<typeof homeReducer>; home: ReturnType<typeof homeReducer>;
tags: ReturnType<typeof tagsReducer>; tags: ReturnType<typeof tagsReducer>;
navigation: ReturnType<typeof navigationReducer>;
} }
const persistConfig = { const persistConfig = {
key: 'root', key: 'root',
storage: createRealmPersistStorage({ path: 'redux.realm' }), storage: createRealmPersistStorage({ path: 'redux.realm' }),
blacklist: ['navigation'],
}; };
const persistedReducer = persistReducer(persistConfig, rootReducer); const persistedReducer = persistReducer(persistConfig, rootReducer);
@@ -69,3 +73,8 @@ export {
setTagsSortDirection, setTagsSortDirection,
toggleTagsSortDirection, toggleTagsSortDirection,
} from './tags'; } from './tags';
export {
type NavigationState,
setNavVisible,
toggleNavVisible,
} from './navigation';

31
src/state/navigation.ts Normal file
View File

@@ -0,0 +1,31 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface NavigationState {
navVisible: boolean;
}
const initialState: NavigationState = {
navVisible: true,
};
const navigationSlice = createSlice({
name: 'navigation',
initialState,
reducers: {
setNavVisible: (state, action: PayloadAction<boolean>) => {
state.navVisible = action.payload;
},
toggleNavVisible: state => {
state.navVisible = !state.navVisible;
},
},
});
const { setNavVisible, toggleNavVisible } = navigationSlice.actions;
export {
type NavigationState,
setNavVisible,
toggleNavVisible,
};
export default navigationSlice.reducer;

View File

@@ -8,7 +8,7 @@ interface TagsState {
const initialState: TagsState = { const initialState: TagsState = {
sort: TAG_SORT.NAME, sort: TAG_SORT.NAME,
sortDirection: SORT_DIRECTION.DESCENDING, sortDirection: SORT_DIRECTION.ASCENDING,
}; };
const tagsSlice = createSlice({ const tagsSlice = createSlice({

View File

@@ -1,6 +1,6 @@
export { ROUTE } from './route'; export { ROUTE } from './route';
export { export {
HOME_SORT, MEME_SORT,
homeSortQuery, homeSortQuery,
TAG_SORT, TAG_SORT,
tagSortQuery, tagSortQuery,

View File

@@ -1,4 +1,4 @@
enum HOME_SORT { enum MEME_SORT {
TITLE = 'Title', TITLE = 'Title',
DATE_CREATED = 'Date Created', DATE_CREATED = 'Date Created',
DATE_MODIFIED = 'Date Modified', DATE_MODIFIED = 'Date Modified',
@@ -7,24 +7,24 @@ enum HOME_SORT {
SIZE = 'Size', SIZE = 'Size',
} }
const homeSortQuery = (sort: HOME_SORT) => { const homeSortQuery = (sort: MEME_SORT) => {
switch (sort) { switch (sort) {
case HOME_SORT.TITLE: { case MEME_SORT.TITLE: {
return 'title'; return 'title';
} }
case HOME_SORT.DATE_CREATED: { case MEME_SORT.DATE_CREATED: {
return 'dateCreated'; return 'dateCreated';
} }
case HOME_SORT.DATE_MODIFIED: { case MEME_SORT.DATE_MODIFIED: {
return 'dateModified'; return 'dateModified';
} }
case HOME_SORT.DATE_USED: { case MEME_SORT.DATE_USED: {
return 'dateUsed'; return 'dateUsed';
} }
case HOME_SORT.TIMES_USED: { case MEME_SORT.TIMES_USED: {
return 'timesUsed'; return 'timesUsed';
} }
case HOME_SORT.SIZE: { case MEME_SORT.SIZE: {
return 'size'; return 'size';
} }
} }
@@ -67,4 +67,4 @@ enum SORT_DIRECTION {
DESCENDING = 1, DESCENDING = 1,
} }
export { HOME_SORT, homeSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION }; export { MEME_SORT, homeSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION };

View File

@@ -1,28 +1,28 @@
import { HOME_SORT, SORT_DIRECTION, TAG_SORT, VIEW } from '../types'; import { MEME_SORT, SORT_DIRECTION, TAG_SORT, VIEW } from '../types';
const getSortIcon = ( const getSortIcon = (
sort: HOME_SORT | TAG_SORT, sort: MEME_SORT | TAG_SORT,
sortDirection: SORT_DIRECTION, sortDirection: SORT_DIRECTION,
) => { ) => {
let sortIcon = ''; let sortIcon = '';
switch (sort) { switch (sort) {
case HOME_SORT.TITLE: case MEME_SORT.TITLE:
case TAG_SORT.NAME: case TAG_SORT.NAME:
case TAG_SORT.COLOR: { case TAG_SORT.COLOR: {
sortIcon = 'sort-alphabetical'; sortIcon = 'sort-alphabetical';
break; break;
} }
case HOME_SORT.DATE_CREATED: case MEME_SORT.DATE_CREATED:
case HOME_SORT.DATE_MODIFIED: case MEME_SORT.DATE_MODIFIED:
case HOME_SORT.DATE_USED: case MEME_SORT.DATE_USED:
case TAG_SORT.DATE_CREATED: case TAG_SORT.DATE_CREATED:
case TAG_SORT.DATE_MODIFIED: { case TAG_SORT.DATE_MODIFIED: {
sortIcon = 'sort-calendar'; sortIcon = 'sort-calendar';
break; break;
} }
case HOME_SORT.TIMES_USED: case MEME_SORT.TIMES_USED:
case HOME_SORT.SIZE: case MEME_SORT.SIZE:
case TAG_SORT.MEMES_LENGTH: case TAG_SORT.MEMES_LENGTH:
case TAG_SORT.TIMES_USED: { case TAG_SORT.TIMES_USED: {
sortIcon = 'sort-numeric'; sortIcon = 'sort-numeric';