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

@@ -1,10 +1,14 @@
import React from 'react';
import { Appbar, Text } from 'react-native-paper';
import { RootScrollView } from '../components';
import { Appbar, Text, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import { useDimensions } from '../contexts';
import { ScrollView } from 'react-native';
import styles from '../styles';
const AddMeme = () => {
const navigation = useNavigation();
const { colors } = useTheme();
const { orientation } = useDimensions();
return (
<>
@@ -12,9 +16,18 @@ const AddMeme = () => {
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title="Add Meme" />
</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>
</RootScrollView>
</ScrollView>
</>
);
};

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,12 @@
import React, { useState } from 'react';
import { StyleSheet, View, Text } from 'react-native';
import React, { useEffect, useRef, useState } from 'react';
import {
StyleSheet,
View,
Text,
NativeSyntheticEvent,
NativeScrollEvent,
Animated,
} from 'react-native';
import {
Button,
Divider,
@@ -7,23 +14,33 @@ import {
Menu,
Searchbar,
TouchableRipple,
useTheme,
} from 'react-native-paper';
import { useQuery, useRealm } from '@realm/react';
import { useDispatch, useSelector } from 'react-redux';
import { FlashList } from '@shopify/flash-list';
import { RootView, TagChip } from '../components';
import { TagChip } from '../components';
import { Tag, deleteTag } from '../database';
import styles from '../styles';
import {
RootState,
setNavVisible,
setTagsSort,
setTagsSortDirection,
toggleTagsSortDirection,
} from '../state';
import { SORT_DIRECTION, TAG_SORT, tagSortQuery } from '../types';
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: {
height: 50,
},
@@ -32,7 +49,8 @@ const tagStyles = StyleSheet.create({
justifyContent: 'space-between',
flexDirection: 'row',
alignItems: 'center',
padding: 10,
paddingVertical: 10,
paddingHorizontal: 15,
},
tagView: {
flexShrink: 1,
@@ -41,38 +59,23 @@ const tagStyles = StyleSheet.create({
helperText: {
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 { colors } = useTheme();
const { orientation } = useDimensions();
const realm = useRealm();
const sort = useSelector((state: RootState) => state.tags.sort);
const sortDirection = useSelector(
(state: RootState) => state.tags.sortDirection,
);
const navVisisble = useSelector(
(state: RootState) => state.navigation.navVisible,
);
const dispatch = useDispatch();
const [sortMenuVisible, setSortMenuVisible] = useState(false);
@@ -91,61 +94,141 @@ const Tags = () => {
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 tags = useQuery<Tag>(Tag.schema.name)
.filtered(`name CONTAINS[c] "${search}"`)
.sorted(tagSortQuery(sort), sortDirection === SORT_DIRECTION.DESCENDING);
const tags = useQuery<Tag>(
Tag.schema.name,
collection =>
collection
.filtered(`name CONTAINS[c] "${search}"`)
.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 (
<RootView padded>
<Searchbar
placeholder="Search Tags"
value={search}
onChangeText={(value: string) => {
setSearch(value);
}}
/>
<View
<View
style={[
styles.paddingHorizontal,
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<Animated.View
style={[
styles.flexRow,
styles.alignCenter,
tagStyles.headerButtonView,
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,
},
]}>
<Menu
visible={sortMenuVisible}
onDismiss={() => setSortMenuVisible(false)}
anchor={
<Button
onPress={() => setSortMenuVisible(true)}
icon={getSortIcon(sort, sortDirection)}
contentStyle={styles.flexRowReverse}
compact>
Sort By: {sort}
</Button>
}>
{Object.keys(TAG_SORT).map(key => {
return (
<Menu.Item
key={key}
onPress={() =>
handleSortModeChange(TAG_SORT[key as keyof typeof TAG_SORT])
}
title={TAG_SORT[key as keyof typeof TAG_SORT]}
/>
);
})}
</Menu>
</View>
<Divider />
<Searchbar
placeholder="Search Tags"
value={search}
onChangeText={(value: string) => {
setSearch(value);
}}
/>
<View
style={[
styles.flexRow,
styles.alignCenter,
tagsStyles.headerButtonView,
]}>
<Menu
visible={sortMenuVisible}
onDismiss={() => setSortMenuVisible(false)}
anchor={
<Button
onPress={() => setSortMenuVisible(true)}
icon={getSortIcon(sort, sortDirection)}
contentStyle={styles.flexRowReverse}
compact>
Sort By: {sort}
</Button>
}>
{Object.keys(TAG_SORT).map(key => {
return (
<Menu.Item
key={key}
onPress={() =>
handleSortModeChange(TAG_SORT[key as keyof typeof TAG_SORT])
}
title={TAG_SORT[key as keyof typeof TAG_SORT]}
/>
);
})}
</Menu>
</View>
<Divider />
</Animated.View>
<FlashList
data={tags}
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 />}
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 { 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 { openDocumentTree } from 'react-native-scoped-storage';
import { RootView } from '../components';
import styles from '../styles';
import { noOp } from '../utilities';
import { updateStorageUri } from '../state';
import { useDimensions } from '../contexts';
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
const { colors } = useTheme();
const { orientation, responsive } = useDimensions();
const dispatch = useDispatch();
const { responsive } = useDimensions();
const selectStorageLocation = async () => {
const uri = await openDocumentTree(true).catch(noOp);
@@ -20,7 +21,16 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
};
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
variant="displayMedium"
style={[
@@ -39,7 +49,7 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
}}>
Select Storage Location
</Button>
</RootView>
</View>
);
};