diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index 9825b96..65eb93d 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -1,17 +1,28 @@ import React, { useEffect, useState } from 'react'; -import { Keyboard } from 'react-native'; +import { Keyboard, StyleSheet } from 'react-native'; import { FAB } from 'react-native-paper'; import { ParamListBase, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { pick } from 'react-native-document-picker'; -import { useDimensions } from '../contexts'; +import { ORIENTATION, useDimensions } from '../contexts'; import { ROUTE } from '../types'; import { allowedMimeTypes, noOp } from '../utilities'; +const floatingActionButtonStyles = StyleSheet.create({ + fab: { + paddingBottom: 90, + paddingRight: 10, + }, + fabLandscape: { + paddingBottom: 40, + paddingRight: 12, + }, +}); + const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { const { navigate } = useNavigation>(); - const { responsive } = useDimensions(); + const { orientation } = useDimensions(); const [state, setState] = useState(false); const [keyboardOpen, setKeyboardOpen] = useState(false); @@ -50,18 +61,6 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { throw new Error('Not yet implemented'); }, }, - { - icon: 'image-album', - label: 'Album', - onPress: async () => { - const res = await pick({ - allowMultiSelection: true, - type: allowedMimeTypes, - }).catch(noOp); - if (!res) return; - navigate(ROUTE.ADD_MEME, { uri: res }); - }, - }, ]} onStateChange={({ open }) => setState(open)} onPress={async () => { @@ -70,10 +69,11 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { if (!res) return; navigate(ROUTE.ADD_MEME, { uri: res }); }} - style={{ - paddingBottom: responsive.verticalScale(75), - paddingRight: responsive.horizontalScale(10), - }} + style={ + orientation === ORIENTATION.PORTRAIT + ? floatingActionButtonStyles.fab + : floatingActionButtonStyles.fabLandscape + } /> ); }; diff --git a/src/components/hideableHeader.tsx b/src/components/hideableHeader.tsx index fa64ca2..b464fd7 100644 --- a/src/components/hideableHeader.tsx +++ b/src/components/hideableHeader.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef } from 'react'; import { Animated, StyleSheet } from 'react-native'; -import { useDimensions } from '../contexts'; +import { ORIENTATION, useDimensions } from '../contexts'; import styles from '../styles'; import { useTheme } from 'react-native-paper'; @@ -38,8 +38,9 @@ const HideableHeader = ({ void; memeDescription: StringValidationResult; @@ -28,14 +41,14 @@ const MemeEditor = ({ memeTags: Map; setMemeTags: (tags: Map) => void; }) => { - const { dimensions, fixed, responsive } = useDimensions(); + const { dimensions } = useDimensions(); const [imageWidth, setImageWidth] = useState(); const [imageHeight, setImageHeight] = useState(); useEffect(() => { - Image.getSize(imageUri[0], (width, height) => { - const paddedWidth = dimensions.width - dimensions.width * 0.08; + Image.getSize(imageUri, (width, height) => { + const paddedWidth = dimensions.width * 0.92; setImageWidth(paddedWidth); setImageHeight((paddedWidth / width) * height); }); @@ -57,33 +70,31 @@ const MemeEditor = ({ {memeTitle.error} setMemeDescription(validateMemeDescription(description)) } + error={!memeDescription.valid} /> ); diff --git a/src/components/memes/memeTagSearchModal.tsx b/src/components/memes/memeTagSearchModal.tsx index 30a452e..7958508 100644 --- a/src/components/memes/memeTagSearchModal.tsx +++ b/src/components/memes/memeTagSearchModal.tsx @@ -5,7 +5,6 @@ import { useQuery, useRealm } from '@realm/react'; import { TAG_SORT, tagSortQuery } from '../../types'; import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper'; import { StyleSheet } from 'react-native'; -import { useDimensions } from '../../contexts'; import styles from '../../styles'; import { FlashList } from '@shopify/flash-list'; import { validateTagName } from '../../utilities'; @@ -14,6 +13,15 @@ const memeTagSearchModalStyles = StyleSheet.create({ modal: { position: 'absolute', bottom: 0, + padding: 10, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + }, + searchbar: { + marginBottom: 12, + }, + tagChip: { + marginRight: 8, }, }); @@ -29,7 +37,6 @@ const MemeTagSearchModal = ({ setMemeTags: (tags: Map) => void; }) => { const { colors } = useTheme(); - const { fixed, responsive } = useDimensions(); const realm = useRealm(); const flashListRef = useRef>(null); @@ -90,9 +97,6 @@ const MemeTagSearchModal = ({ visible={visible} contentContainerStyle={[ { - padding: fixed.horizontalScale(10), - borderTopLeftRadius: fixed.verticalScale(20), - borderTopRightRadius: fixed.verticalScale(20), backgroundColor: colors.surface, }, styles.fullWidth, @@ -103,9 +107,7 @@ const MemeTagSearchModal = ({ placeholder="Search or Create Tags" onChangeText={handleSearch} value={search} - style={{ - marginBottom: responsive.verticalScale(10), - }} + style={memeTagSearchModalStyles.searchbar} autoFocus /> ( handleTagPress(tag)} active={memeTags.has(tag.id.toHexString())} /> diff --git a/src/components/memes/memeTagSelector.tsx b/src/components/memes/memeTagSelector.tsx index a30e783..6bd04af 100644 --- a/src/components/memes/memeTagSelector.tsx +++ b/src/components/memes/memeTagSelector.tsx @@ -1,5 +1,5 @@ import React, { ComponentProps, useState } from 'react'; -import { View } from 'react-native'; +import { StyleSheet, View } from 'react-native'; import { Chip } from 'react-native-paper'; import { TagChip } from '../tags'; import { Tag } from '../../database'; @@ -7,6 +7,12 @@ import { useDimensions } from '../../contexts'; import { MemeTagSearchModal } from '.'; import { FlashList } from '@shopify/flash-list'; +const memeTagSelectorStyles = StyleSheet.create({ + tagChip: { + marginRight: 8, + }, +}); + const MemeTagSelector = ({ memeTags, setMemeTags, @@ -15,8 +21,9 @@ const MemeTagSelector = ({ memeTags: Map; setMemeTags: (tags: Map) => void; } & ComponentProps) => { - const { fixed, dimensions } = useDimensions(); + const { dimensions } = useDimensions(); + const [flashListMargin, setFlashListMargin] = useState(0); const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false); const handleTagPress = (tag: Tag) => { @@ -38,9 +45,7 @@ const MemeTagSelector = ({ handleTagPress(tag)} - style={{ - marginRight: fixed.horizontalScale(5), - }} + style={memeTagSelectorStyles.tagChip} /> )} ListFooterComponent={() => ( @@ -48,8 +53,13 @@ const MemeTagSelector = ({ icon="plus" mode="outlined" onPress={() => setTagSearchModalVisible(true)} + onLayout={event => + setFlashListMargin( + dimensions.width * 0.92 - event.nativeEvent.layout.width, + ) + } style={{ - marginRight: dimensions.width * 0.92 - 105.8, + marginRight: flashListMargin, }}> Add Tag diff --git a/src/components/memes/memesHeader.tsx b/src/components/memes/memesHeader.tsx new file mode 100644 index 0000000..579c906 --- /dev/null +++ b/src/components/memes/memesHeader.tsx @@ -0,0 +1,167 @@ +import React, { ComponentProps, useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { + Button, + Divider, + IconButton, + Menu, + Searchbar, + useTheme, +} from 'react-native-paper'; +import { useDispatch, useSelector } from 'react-redux'; +import { MEME_TYPE, memeTypePlural } from '../../database'; +import { + RootState, + cycleMemesView, + setMemesFilter, + setMemesSort, + setMemesSortDirection, + toggleMemesFavoritesOnly, + toggleMemesSortDirection, +} from '../../state'; +import styles from '../../styles'; +import { MEME_SORT, SORT_DIRECTION } from '../../types'; +import { getSortIcon, getViewIcon } from '../../utilities'; + +const memesHeaderStyles = StyleSheet.create({ + buttonView: { + height: 50, + }, +}); + +const MemesHeader = ({ + search, + setSearch, + ...props +}: { + search: string; + setSearch: (search: string) => void; +} & ComponentProps) => { + const { colors } = useTheme(); + const sort = useSelector((state: RootState) => state.memes.sort); + const sortDirection = useSelector( + (state: RootState) => state.memes.sortDirection, + ); + const view = useSelector((state: RootState) => state.memes.view); + const favoritesOnly = useSelector( + (state: RootState) => state.memes.favoritesOnly, + ); + const filter = useSelector((state: RootState) => state.memes.filter); + const dispatch = useDispatch(); + + const [sortMenuVisible, setSortMenuVisible] = useState(false); + const [filterMenuVisible, setFilterMenuVisible] = useState(false); + + const handleSortModeChange = (newSort: MEME_SORT) => { + if (newSort === sort) { + dispatch(toggleMemesSortDirection()); + } else { + dispatch(setMemesSort(newSort)); + if (newSort === MEME_SORT.TITLE) { + dispatch(setMemesSortDirection(SORT_DIRECTION.ASCENDING)); + } else { + dispatch(setMemesSortDirection(SORT_DIRECTION.DESCENDING)); + } + } + setSortMenuVisible(false); + }; + + const handleFilterChange = (newFilter: MEME_TYPE | undefined) => { + dispatch(setMemesFilter(newFilter)); + setFilterMenuVisible(false); + }; + + return ( + + + + + setSortMenuVisible(false)} + anchor={ + + }> + {Object.keys(MEME_SORT).map(key => { + return ( + + handleSortModeChange( + MEME_SORT[key as keyof typeof MEME_SORT], + ) + } + title={MEME_SORT[key as keyof typeof MEME_SORT]} + /> + ); + })} + + + + dispatch(cycleMemesView())} + /> + dispatch(toggleMemesFavoritesOnly())} + /> + setFilterMenuVisible(false)} + anchor={ + setFilterMenuVisible(true)} + icon={filter ? 'filter' : 'filter-outline'} + iconColor={colors.primary} + size={16} + /> + }> + handleFilterChange(undefined)} + title="All" + /> + {Object.keys(MEME_TYPE).map(key => { + return ( + + handleFilterChange(MEME_TYPE[key as keyof typeof MEME_TYPE]) + } + title={ + memeTypePlural[MEME_TYPE[key as keyof typeof MEME_TYPE]] + } + /> + ); + })} + + + + + + ); +}; + +export default MemesHeader; diff --git a/src/components/tags/index.ts b/src/components/tags/index.ts index 1cba957..6b9fca9 100644 --- a/src/components/tags/index.ts +++ b/src/components/tags/index.ts @@ -1,3 +1,5 @@ export { default as TagChip } from './tagChip'; export { default as TagEditor } from './tagEditor'; export { default as TagPreview } from './tagPreview'; +export { default as TagRow } from './tagRow'; +export { default as TagsHeader } from './tagsHeader'; diff --git a/src/components/tags/tagChip.tsx b/src/components/tags/tagChip.tsx index 0597faf..2b4ebdb 100644 --- a/src/components/tags/tagChip.tsx +++ b/src/components/tags/tagChip.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { ComponentProps, useMemo } from 'react'; import { getContrastColor } from '../../utilities'; import { Chip, useTheme } from 'react-native-paper'; import { Tag } from '../../database'; @@ -20,7 +20,7 @@ const TagChip = ({ tag: Tag; active?: boolean; onPress?: () => void; -} & Omit, 'children'>) => { +} & Omit, 'children'>) => { const theme = useTheme(); const chipTheme = useMemo(() => { diff --git a/src/components/tags/tagEditor.tsx b/src/components/tags/tagEditor.tsx index 940195c..c430ada 100644 --- a/src/components/tags/tagEditor.tsx +++ b/src/components/tags/tagEditor.tsx @@ -35,7 +35,6 @@ const TagEditor = ({ value={tagName.raw} onChangeText={name => setTagName(validateTagName(name))} error={!tagName.valid} - autoCapitalize="none" selectTextOnFocus /> diff --git a/src/components/tags/tagRow.tsx b/src/components/tags/tagRow.tsx new file mode 100644 index 0000000..d438745 --- /dev/null +++ b/src/components/tags/tagRow.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { TouchableRipple, Text } from 'react-native-paper'; +import { useNavigation, NavigationProp } from '@react-navigation/native'; +import { Tag } from '../../database'; +import { ROUTE, RootStackParamList } from '../../types'; +import { TagChip } from '.'; + +const tagRowStyles = StyleSheet.create({ + view: { + justifyContent: 'space-between', + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 10, + paddingHorizontal: 15, + }, + tagChip: { + flexShrink: 1, + maxWidth: '80%', + }, +}); + +const TagRow = ({ tag }: { tag: Tag }) => { + const { navigate } = useNavigation>(); + + return ( + navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })}> + + + {tag.memesLength} + + + ); +}; + +export default TagRow; diff --git a/src/components/tags/tagsHeader.tsx b/src/components/tags/tagsHeader.tsx new file mode 100644 index 0000000..ee9f388 --- /dev/null +++ b/src/components/tags/tagsHeader.tsx @@ -0,0 +1,96 @@ +import React, { ComponentProps, useState } from 'react'; +import { StyleSheet, View } from 'react-native'; +import { Button, Divider, Menu, Searchbar } from 'react-native-paper'; +import styles from '../../styles'; +import { useDispatch, useSelector } from 'react-redux'; +import { + RootState, + setTagsSort, + setTagsSortDirection, + toggleTagsSortDirection, +} from '../../state'; +import { SORT_DIRECTION, TAG_SORT } from '../../types'; +import { getSortIcon } from '../../utilities'; + +const tagsHeaderStyles = StyleSheet.create({ + buttonView: { + height: 50, + }, +}); + +const TagsHeader = ({ + search, + setSearch, + ...props +}: { + search: string; + setSearch: (search: string) => void; +} & ComponentProps) => { + const sort = useSelector((state: RootState) => state.tags.sort); + const sortDirection = useSelector( + (state: RootState) => state.tags.sortDirection, + ); + const dispatch = useDispatch(); + + const [sortMenuVisible, setSortMenuVisible] = useState(false); + + const handleSortModeChange = (newSort: TAG_SORT) => { + if (newSort === sort) { + dispatch(toggleTagsSortDirection()); + } else { + dispatch(setTagsSort(newSort)); + if (newSort === TAG_SORT.NAME) { + dispatch(setTagsSortDirection(SORT_DIRECTION.ASCENDING)); + } else { + dispatch(setTagsSortDirection(SORT_DIRECTION.DESCENDING)); + } + } + setSortMenuVisible(false); + }; + + return ( + + { + setSearch(value); + }} + /> + + setSortMenuVisible(false)} + anchor={ + + }> + {Object.keys(TAG_SORT).map(key => { + return ( + + handleSortModeChange(TAG_SORT[key as keyof typeof TAG_SORT]) + } + title={TAG_SORT[key as keyof typeof TAG_SORT]} + /> + ); + })} + + + + + ); +}; + +export default TagsHeader; diff --git a/src/contexts/dimensions.tsx b/src/contexts/dimensions.tsx index 63d5c47..7a29295 100644 --- a/src/contexts/dimensions.tsx +++ b/src/contexts/dimensions.tsx @@ -11,17 +11,22 @@ import { Dimensions, ScaledSize } from 'react-native'; const guidelineBaseWidth = 350; const guidelineBaseHeight = 680; +enum ORIENTATION { + PORTRAIT = 'portrait', + LANDSCAPE = 'landscape', +} + interface ScaleFunctions { horizontalScale: (size: number) => number; verticalScale: (size: number) => number; - moderateScale: (size: number, factor?: number) => number; + moderateHorizontalScale: (size: number, factor?: number) => number; + moderateVerticalScale: (size: number, factor?: number) => number; } interface DimensionsContext { - orientation: 'portrait' | 'landscape'; + orientation: ORIENTATION; dimensions: ScaledSize; responsive: ScaleFunctions; - fixed: ScaleFunctions; } const createScaleFunctions = (dimensionsIn: ScaledSize) => { @@ -29,10 +34,17 @@ const createScaleFunctions = (dimensionsIn: ScaledSize) => { (dimensionsIn.width / guidelineBaseWidth) * size; const verticalScale = (size: number) => (dimensionsIn.height / guidelineBaseHeight) * size; - const moderateScale = (size: number, factor = 0.5) => + const moderateHorizontalScale = (size: number, factor = 0.5) => size + (horizontalScale(size) - size) * factor; + const moderateVerticalScale = (size: number, factor = 0.5) => + size + (verticalScale(size) - size) * factor; - return { horizontalScale, verticalScale, moderateScale }; + return { + horizontalScale, + verticalScale, + moderateHorizontalScale, + moderateVerticalScale, + }; }; const DimensionsContext = createContext( @@ -41,22 +53,14 @@ const DimensionsContext = createContext( const DimensionsProvider = ({ children }: { children: ReactNode }) => { const [dimensions, setDimensions] = useState(Dimensions.get('window')); - const orientation = - dimensions.width > dimensions.height ? 'landscape' : 'portrait'; - const initialDimensions = useMemo(() => { - if (orientation === 'landscape') { - return { - width: dimensions.height, - height: dimensions.width, - } as ScaledSize; - } - return dimensions; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const orientation = useMemo(() => { + return dimensions.width > dimensions.height + ? ORIENTATION.LANDSCAPE + : ORIENTATION.PORTRAIT; + }, [dimensions]); const responsiveScale = createScaleFunctions(dimensions); - const fixedScale = createScaleFunctions(initialDimensions); useEffect(() => { const onChange = ({ window }: { window: ScaledSize }) => { @@ -76,7 +80,6 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => { orientation, dimensions, responsive: responsiveScale, - fixed: fixedScale, }}> {children} @@ -91,4 +94,4 @@ const useDimensions = (): DimensionsContext => { return context; }; -export { DimensionsProvider, useDimensions }; +export { ORIENTATION, DimensionsProvider, useDimensions }; diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 99a4edd..cb4d26e 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1 +1 @@ -export { DimensionsProvider, useDimensions } from './dimensions'; +export { ORIENTATION, DimensionsProvider, useDimensions } from './dimensions'; diff --git a/src/database/meme.ts b/src/database/meme.ts index e55ae18..c2df2e5 100644 --- a/src/database/meme.ts +++ b/src/database/meme.ts @@ -6,7 +6,6 @@ enum MEME_TYPE { GIF = 'GIF', VIDEO = 'Video', AUDIO = 'Audio', - ALBUM = 'Album', TEXT = 'Text', } @@ -15,7 +14,6 @@ const memeTypePlural = { [MEME_TYPE.GIF]: 'GIFs', [MEME_TYPE.VIDEO]: 'Videos', [MEME_TYPE.AUDIO]: 'Audio', - [MEME_TYPE.ALBUM]: 'Albums', [MEME_TYPE.TEXT]: 'Text', }; diff --git a/src/navigation.tsx b/src/navigation.tsx index 3febe72..680f9f3 100644 --- a/src/navigation.tsx +++ b/src/navigation.tsx @@ -82,6 +82,7 @@ const TabNavigator = () => { const NavigationContainer = () => { const theme = useTheme(); + const StackNavigatorBase = createNativeStackNavigator(); return ( diff --git a/src/screens/addMeme.tsx b/src/screens/addMeme.tsx index 03b4121..900d38a 100644 --- a/src/screens/addMeme.tsx +++ b/src/screens/addMeme.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Appbar, Button, useTheme } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; -import { useDimensions } from '../contexts'; +import { ORIENTATION, useDimensions } from '../contexts'; import { ScrollView, View } from 'react-native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { useRealm } from '@realm/react'; @@ -11,7 +11,7 @@ import { useSelector } from 'react-redux'; import { extension } from 'react-native-mime-types'; import styles from '../styles'; import { ROUTE, RootStackParamList } from '../types'; -import { MEME_TYPE, Meme, Tag } from '../database'; +import { Meme, Tag } from '../database'; import { RootState } from '../state'; import { getMemeType, @@ -32,11 +32,9 @@ const AddMeme = ({ (state: RootState) => state.settings.storageUri, )!; - const { uri } = route.params; - - const memeType = - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(uri[0].type!); + const uri = route.params.uri[0].uri; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const memeType = getMemeType(route.params.uri[0].type!); const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme')); const [memeDescription, setMemeDescription] = useState( @@ -55,7 +53,7 @@ const AddMeme = ({ const hash: string[] = []; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const fileExtension = extension(uri[0].type!); + const fileExtension = extension(memeType!); if (!fileExtension) navigation.goBack(); savedUri.push( @@ -65,7 +63,7 @@ const AddMeme = ({ ), ); - await FileSystem.cp(uri[0].uri, savedUri[0]); + await FileSystem.cp(uri, savedUri[0]); const { size } = await FileSystem.stat(savedUri[0]); hash.push(await FileSystem.hash(savedUri[0], 'MD5')); @@ -107,8 +105,9 @@ const AddMeme = ({ uriIn.uri)} + imageUri={uri} memeTitle={memeTitle} setMemeTitle={setMemeTitle} memeDescription={memeDescription} diff --git a/src/screens/addTag.tsx b/src/screens/addTag.tsx index c6f5410..8671c5f 100644 --- a/src/screens/addTag.tsx +++ b/src/screens/addTag.tsx @@ -9,7 +9,7 @@ import { validateColor, validateTagName, } from '../utilities'; -import { useDimensions } from '../contexts'; +import { ORIENTATION, useDimensions } from '../contexts'; import { Tag } from '../database'; import { TagEditor } from '../components'; @@ -43,8 +43,9 @@ const AddTag = () => { { @@ -18,8 +18,9 @@ const EditMeme = () => { { (state: RootState) => state.memes.favoritesOnly, ); const filter = useSelector((state: RootState) => state.memes.filter); + const navVisisble = useSelector( + (state: RootState) => state.navigation.navVisible, + ); const dispatch = useDispatch(); - const [sortMenuVisible, setSortMenuVisible] = useState(false); - const [filterMenuVisible, setFilterMenuVisible] = useState(false); - - const handleSortModeChange = (newSort: MEME_SORT) => { - if (newSort === sort) { - dispatch(toggleMemesSortDirection()); - } else { - dispatch(setMemesSort(newSort)); - if (newSort === MEME_SORT.TITLE) { - dispatch(setMemesSortDirection(SORT_DIRECTION.ASCENDING)); - } else { - dispatch(setMemesSortDirection(SORT_DIRECTION.DESCENDING)); - } - } - setSortMenuVisible(false); - }; - - const handleFilterChange = (newFilter: MEME_TYPE | undefined) => { - dispatch(setMemesFilter(newFilter)); - setFilterMenuVisible(false); - }; - + const [flashListPadding, setFlashListPadding] = useState(0); const [search, setSearch] = useState(''); const memes = useQuery( @@ -94,110 +65,56 @@ const Memes = () => { [sort, sortDirection, favoritesOnly, filter, search], ); + const [scrollOffset, setScrollOffset] = useState(0); + + const handleScroll = (event: NativeSyntheticEvent) => { + const currentOffset = event.nativeEvent.contentOffset.y; + + if (currentOffset <= 150) { + dispatch(setNavVisible(true)); + } else { + const diff = currentOffset - scrollOffset; + if (Math.abs(diff) > 50) dispatch(setNavVisible(diff < 0)); + } + + setScrollOffset(currentOffset); + }; + + const flashListRef = useRef>(null); + + useFocusEffect( + useCallback(() => { + const handleBackPress = () => { + if (scrollOffset > 0) { + flashListRef.current?.scrollToOffset({ offset: 0, animated: true }); + return true; + } + return false; + }; + + BackHandler.addEventListener('hardwareBackPress', handleBackPress); + + return () => + BackHandler.removeEventListener('hardwareBackPress', handleBackPress); + }, [flashListRef, scrollOffset]), + ); + return ( - - - - setSortMenuVisible(false)} - anchor={ - - }> - {Object.keys(MEME_SORT).map(key => { - return ( - - handleSortModeChange( - MEME_SORT[key as keyof typeof MEME_SORT], - ) - } - title={MEME_SORT[key as keyof typeof MEME_SORT]} - /> - ); - })} - - - - dispatch(cycleMemesView())} - /> - dispatch(toggleMemesFavoritesOnly())} - /> - setFilterMenuVisible(false)} - anchor={ - setFilterMenuVisible(true)} - icon={filter ? 'filter' : 'filter-outline'} - iconColor={colors.primary} - size={16} - /> - }> - handleFilterChange(undefined)} - title="All" - /> - {Object.keys(MEME_TYPE).map(key => { - return ( - - handleFilterChange(MEME_TYPE[key as keyof typeof MEME_TYPE]) - } - title={ - memeTypePlural[MEME_TYPE[key as keyof typeof MEME_TYPE]] - } - /> - ); - })} - - - - - {/* TODO: Meme Views */} - {memes.length === 0 && ( - - No memes found - - )} + + { + setFlashListPadding(event.nativeEvent.layout.height); + }} + /> + ); }; diff --git a/src/screens/settings.tsx b/src/screens/settings.tsx index 440cb30..add1cbe 100644 --- a/src/screens/settings.tsx +++ b/src/screens/settings.tsx @@ -13,12 +13,8 @@ import { openDocumentTree } from 'react-native-scoped-storage'; import { useDispatch, useSelector } from 'react-redux'; import type {} from 'redux-thunk/extend-redux'; import styles from '../styles'; -import { - RootState, - setNoMedia, - setStorageUri, -} from '../state'; -import { useDimensions } from '../contexts'; +import { RootState, setNoMedia, setStorageUri } from '../state'; +import { ORIENTATION, useDimensions } from '../contexts'; const settingsScreenStyles = StyleSheet.create({ snackbar: { @@ -48,8 +44,9 @@ const SettingsScreen = () => { <> diff --git a/src/screens/tags.tsx b/src/screens/tags.tsx index 0b06dea..cdc7074 100644 --- a/src/screens/tags.tsx +++ b/src/screens/tags.tsx @@ -6,70 +6,30 @@ import { NativeScrollEvent, BackHandler, } from 'react-native'; -import { - Button, - Divider, - HelperText, - Menu, - Searchbar, - Text, - TouchableRipple, - useTheme, -} from 'react-native-paper'; +import { Divider, HelperText, useTheme } from 'react-native-paper'; import { useQuery } from '@realm/react'; import { useDispatch, useSelector } from 'react-redux'; import { FlashList } from '@shopify/flash-list'; -import { - NavigationProp, - useFocusEffect, - useNavigation, -} from '@react-navigation/native'; -import { HideableHeader, TagChip } from '../components'; +import { useFocusEffect } from '@react-navigation/native'; +import { HideableHeader, TagRow, TagsHeader } from '../components'; import { Tag } from '../database'; import styles from '../styles'; -import { - RootState, - setNavVisible, - setTagsSort, - setTagsSortDirection, - toggleTagsSortDirection, -} from '../state'; -import { - ROUTE, - RootStackParamList, - SORT_DIRECTION, - TAG_SORT, - tagSortQuery, -} from '../types'; -import { getSortIcon } from '../utilities'; +import { RootState, setNavVisible } from '../state'; +import { SORT_DIRECTION, tagSortQuery } from '../types'; +import { ORIENTATION, useDimensions } from '../contexts'; const tagsStyles = StyleSheet.create({ - headerButtonView: { - height: 50, - }, - tagRow: { - justifyContent: 'space-between', - flexDirection: 'row', - alignItems: 'center', - paddingVertical: 10, - paddingHorizontal: 15, - }, - tagChip: { - flexShrink: 1, - maxWidth: '80%', - }, helperText: { marginVertical: 10, }, flashList: { - paddingTop: 122, - paddingBottom: 25, + paddingBottom: 100, }, }); const Tags = () => { const { colors } = useTheme(); - const { navigate } = useNavigation>(); + const { dimensions, orientation } = useDimensions(); const sort = useSelector((state: RootState) => state.tags.sort); const sortDirection = useSelector( (state: RootState) => state.tags.sortDirection, @@ -79,22 +39,7 @@ const Tags = () => { ); const dispatch = useDispatch(); - const [sortMenuVisible, setSortMenuVisible] = useState(false); - - const handleSortModeChange = (newSort: TAG_SORT) => { - if (newSort === sort) { - dispatch(toggleTagsSortDirection()); - } else { - dispatch(setTagsSort(newSort)); - if (newSort === TAG_SORT.NAME) { - dispatch(setTagsSortDirection(SORT_DIRECTION.ASCENDING)); - } else { - dispatch(setTagsSortDirection(SORT_DIRECTION.DESCENDING)); - } - } - setSortMenuVisible(false); - }; - + const [flashListPadding, setFlashListPadding] = useState(0); const [search, setSearch] = useState(''); const tags = useQuery( @@ -158,73 +103,39 @@ const Tags = () => { { backgroundColor: colors.background }, ]}> - { - setSearch(value); + { + setFlashListPadding(event.nativeEvent.layout.height); }} /> - - setSortMenuVisible(false)} - anchor={ - - }> - {Object.keys(TAG_SORT).map(key => { - return ( - - handleSortModeChange(TAG_SORT[key as keyof typeof TAG_SORT]) - } - title={TAG_SORT[key as keyof typeof TAG_SORT]} - /> - ); - })} - - - - ( - - navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() }) - }> - - - {tag.memesLength} - - - )} - contentContainerStyle={tagsStyles.flashList} - ItemSeparatorComponent={() => } - ListEmptyComponent={() => ( - - No tags found - - )} - onScroll={handleScroll} - /> + {flashListPadding > 0 && ( + } + contentContainerStyle={{ + paddingTop: + flashListPadding + + dimensions.height * + (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), + ...tagsStyles.flashList, + }} + ItemSeparatorComponent={() => } + ListEmptyComponent={() => ( + + No tags found + + )} + onScroll={handleScroll} + /> + )} ); }; diff --git a/src/screens/welcome.tsx b/src/screens/welcome.tsx index b843814..46e3b22 100644 --- a/src/screens/welcome.tsx +++ b/src/screens/welcome.tsx @@ -1,16 +1,25 @@ import React from 'react'; -import { View } from 'react-native'; +import { StyleSheet, 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 styles from '../styles'; import { noOp } from '../utilities'; import { setStorageUri } from '../state'; -import { useDimensions } from '../contexts'; +import { ORIENTATION, useDimensions } from '../contexts'; + +const welcomeStyles = StyleSheet.create({ + text: { + marginBottom: 30, + }, + button: { + marginBottom: 100, + }, +}); const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { const { colors } = useTheme(); - const { orientation, responsive } = useDimensions(); + const { orientation } = useDimensions(); const dispatch = useDispatch(); const selectStorageLocation = async () => { @@ -23,8 +32,9 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { return ( void }) => { ]}> + style={[welcomeStyles.text, styles.centerText]}> Welcome to Terminally Online!