From 4128b0df20d2670215f44f7759f3b250e7f04434 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Thu, 13 Jul 2023 19:03:53 +0300 Subject: [PATCH] Refactor dimension handling Signed-off-by: Nikolaos Karaolidis --- src/app.tsx | 51 ++++--- src/components/floatingActionButton.tsx | 74 ++++----- src/components/index.ts | 4 +- src/components/loadingView.tsx | 6 +- src/components/rootScrollView.tsx | 37 +++++ .../{paddedView.tsx => rootView.tsx} | 10 +- src/components/tagPreview.tsx | 48 ++++++ src/contexts/dimensions.tsx | 90 +++++++++++ src/contexts/index.ts | 1 + src/navigation.tsx | 144 +++++++++--------- src/screens/addMeme.tsx | 6 +- src/screens/addTag.tsx | 83 +++------- src/screens/home.tsx | 17 ++- src/screens/settings.tsx | 23 ++- src/screens/tags.tsx | 6 +- src/screens/welcome.tsx | 19 ++- src/styles.tsx | 37 ++--- 17 files changed, 406 insertions(+), 250 deletions(-) create mode 100644 src/components/rootScrollView.tsx rename src/components/{paddedView.tsx => rootView.tsx} (75%) create mode 100644 src/components/tagPreview.tsx create mode 100644 src/contexts/dimensions.tsx create mode 100644 src/contexts/index.ts diff --git a/src/app.tsx b/src/app.tsx index 211d2f4..787e8c2 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -3,17 +3,18 @@ import { AppState, StatusBar, useColorScheme } from 'react-native'; import { PaperProvider } from 'react-native-paper'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { RealmProvider } from '@realm/react'; -import { Provider } from 'react-redux'; +import { Provider as ReduxProvider } from 'react-redux'; import { PersistGate } from 'redux-persist/integration/react'; import type {} from 'redux-thunk/extend-redux'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; import { lightTheme, darkTheme } from './theme'; import { Meme, Tag } from './database'; import NavigationContainer from './navigation'; import { store, persistor, validateSettings } from './state'; import { LoadingView } from './components'; import { Welcome } from './screens'; -import { GestureHandlerRootView } from 'react-native-gesture-handler'; import styles from './styles'; +import { DimensionsProvider } from './contexts'; const App = () => { const [showWelcome, setShowWelcome] = useState(false); @@ -43,28 +44,30 @@ const App = () => { return ( - - } - persistor={persistor} - onBeforeLift={onBeforeLift}> - - - - - {showWelcome ? ( - setShowWelcome(false)} /> - ) : ( - - )} - - - - - + + + } + persistor={persistor} + onBeforeLift={onBeforeLift}> + + + + + {showWelcome ? ( + setShowWelcome(false)} /> + ) : ( + + )} + + + + + + ); }; diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index d6f6cc2..1bd365d 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -1,24 +1,22 @@ -import * as React from 'react'; +import React, { useEffect, useState } from 'react'; import { StyleSheet, Keyboard } from 'react-native'; -import { FAB, Portal } from 'react-native-paper'; -import { horizontalScale, verticalScale } from '../styles'; +import { FAB } from 'react-native-paper'; import { ParamListBase, useNavigation } from '@react-navigation/native'; import { NativeStackNavigationProp } from '@react-navigation/native-stack'; -import { useEffect, useState } from 'react'; +import { useDimensions } from '../contexts'; const styles = StyleSheet.create({ fab: { position: 'absolute', - right: horizontalScale(10), - bottom: verticalScale(75), }, }); const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { - const [state, setState] = useState(false); const { navigate } = useNavigation>(); + const dimensions = useDimensions(); + const [state, setState] = useState(false); const [keyboardOpen, setKeyboardOpen] = useState(false); useEffect(() => { const keyboardDidShowListener = Keyboard.addListener( @@ -37,35 +35,39 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { }, []); return ( - - navigate('Add Tag'), - }, - { - icon: 'note-text', - label: 'Text', - onPress: () => navigate('Add Meme'), - }, - { - icon: 'image-album', - label: 'Album', - onPress: () => navigate('Add Meme'), - }, - ]} - onStateChange={({ open }) => setState(open)} - onPress={() => { - if (state) navigate('Add Meme'); - }} - style={styles.fab} - /> - + navigate('Add Tag'), + }, + { + icon: 'note-text', + label: 'Text', + onPress: () => navigate('Add Meme'), + }, + { + icon: 'image-album', + label: 'Album', + onPress: () => navigate('Add Meme'), + }, + ]} + onStateChange={({ open }) => setState(open)} + onPress={() => { + if (state) navigate('Add Meme'); + }} + style={[ + styles.fab, + { + paddingRight: dimensions.responsive.horizontalScale(10), + paddingBottom: dimensions.responsive.verticalScale(75), + }, + ]} + /> ); }; diff --git a/src/components/index.ts b/src/components/index.ts index f9f66d0..da66b71 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,5 @@ export { default as FloatingActionButton } from './floatingActionButton'; export { default as LoadingView } from './loadingView'; -export { default as PaddedView } from './paddedView'; +export { default as RootScrollView } from './rootScrollView'; +export { default as RootView } from './rootView'; +export { default as TagPreview } from './tagPreview'; diff --git a/src/components/loadingView.tsx b/src/components/loadingView.tsx index 4d07df6..b98008d 100644 --- a/src/components/loadingView.tsx +++ b/src/components/loadingView.tsx @@ -1,15 +1,15 @@ import React from 'react'; import { ActivityIndicator } from 'react-native'; import { useTheme } from 'react-native-paper'; -import PaddedView from './paddedView'; +import { RootView } from '.'; const LoadingView = () => { const { colors } = useTheme(); return ( - + - + ); }; diff --git a/src/components/rootScrollView.tsx b/src/components/rootScrollView.tsx new file mode 100644 index 0000000..c451e17 --- /dev/null +++ b/src/components/rootScrollView.tsx @@ -0,0 +1,37 @@ +import React, { ReactNode } from 'react'; +import { + StyleProp, + ScrollView, + ViewStyle, +} from 'react-native'; +import { useTheme } from 'react-native-paper'; +import styles from '../styles'; + +const RootScrollView = ({ + children, + style, + centered, + padded, +}: { + children: ReactNode; + style?: StyleProp; + centered?: boolean; + padded?: boolean; +}) => { + const { colors } = useTheme(); + + return ( + + {children} + + ); +}; + +export default RootScrollView; diff --git a/src/components/paddedView.tsx b/src/components/rootView.tsx similarity index 75% rename from src/components/paddedView.tsx rename to src/components/rootView.tsx index 642737e..79c8c03 100644 --- a/src/components/paddedView.tsx +++ b/src/components/rootView.tsx @@ -3,22 +3,24 @@ import { StyleProp, View, ViewStyle } from 'react-native'; import { useTheme } from 'react-native-paper'; import styles from '../styles'; -const PaddedView = ({ +const RootView = ({ children, style, centered, + padded, }: { children: ReactNode; style?: StyleProp; centered?: boolean; + padded?: boolean; }) => { const { colors } = useTheme(); return ( @@ -27,4 +29,4 @@ const PaddedView = ({ ); }; -export default PaddedView; +export default RootView; diff --git a/src/components/tagPreview.tsx b/src/components/tagPreview.tsx new file mode 100644 index 0000000..ae0722e --- /dev/null +++ b/src/components/tagPreview.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { View } from 'react-native'; +import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import { Chip } from 'react-native-paper'; +import styles from '../styles'; +import { useDimensions } from '../contexts'; +import { getContrastColor } from '../utilities'; + +const TagPreview = (properties: { name: string; color: string }) => { + const dimensions = useDimensions(); + + return ( + + { + return ( + + ); + }} + elevated + style={[ + { + backgroundColor: properties.color, + padding: dimensions.static.verticalScale(5), + }, + ]} + textStyle={[ + { fontSize: dimensions.static.horizontalScale(15) }, + { color: getContrastColor(properties.color) }, + ]}> + {'#' + properties.name} + + + ); +}; + +export default TagPreview; diff --git a/src/contexts/dimensions.tsx b/src/contexts/dimensions.tsx new file mode 100644 index 0000000..2d617a3 --- /dev/null +++ b/src/contexts/dimensions.tsx @@ -0,0 +1,90 @@ +import React, { + ReactNode, + createContext, + useContext, + useEffect, + useState, +} from 'react'; +import { Dimensions, ScaledSize } from 'react-native'; + +const guidelineBaseWidth = 350; +const guidelineBaseHeight = 680; + +interface ScaleFunctions { + horizontalScale: (size: number) => number; + verticalScale: (size: number) => number; + moderateScale: (size: number, factor?: number) => number; +} + +interface DimensionsContext { + orientation: 'portrait' | 'landscape'; + responsive: ScaleFunctions; + static: ScaleFunctions; +} + +const createScaleFunctions = (dimensionsIn: ScaledSize) => { + const horizontalScale = (size: number) => + (dimensionsIn.width / guidelineBaseWidth) * size; + const verticalScale = (size: number) => + (dimensionsIn.height / guidelineBaseHeight) * size; + const moderateScale = (size: number, factor = 0.5) => + size + (horizontalScale(size) - size) * factor; + + return { horizontalScale, verticalScale, moderateScale }; +}; + +const DimensionsContext = createContext( + undefined, +); + +const DimensionsProvider = ({ children }: { children: ReactNode }) => { + const [dimensions, setDimensions] = useState(Dimensions.get('window')); + const orientation = + dimensions.width > dimensions.height ? 'landscape' : 'portrait'; + + const [initialDimensions, setInitialDimensions] = useState(dimensions); + const [initialOrientation] = useState(orientation); + + if (initialOrientation === 'landscape') { + setInitialDimensions({ + width: initialDimensions.height, + height: initialDimensions.width, + } as ScaledSize); + } + + const responsiveScale = createScaleFunctions(dimensions); + const staticScale = createScaleFunctions(initialDimensions); + + useEffect(() => { + const onChange = ({ window }: { window: ScaledSize }) => { + setDimensions(window); + }; + + const subscription = Dimensions.addEventListener('change', onChange); + + return () => { + subscription.remove(); + }; + }, []); + + return ( + + {children} + + ); +}; + +const useDimensions = (): DimensionsContext => { + const context = useContext(DimensionsContext); + if (!context) { + throw new Error('useDimensions must be used within a DimensionsProvider'); + } + return context; +}; + +export { DimensionsProvider, useDimensions }; diff --git a/src/contexts/index.ts b/src/contexts/index.ts new file mode 100644 index 0000000..99a4edd --- /dev/null +++ b/src/contexts/index.ts @@ -0,0 +1 @@ +export { DimensionsProvider, useDimensions } from './dimensions'; diff --git a/src/navigation.tsx b/src/navigation.tsx index 21f0e93..7de29d3 100644 --- a/src/navigation.tsx +++ b/src/navigation.tsx @@ -6,91 +6,87 @@ import { import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; -import { BottomNavigation, Portal, useTheme } from 'react-native-paper'; +import { BottomNavigation, useTheme } from 'react-native-paper'; import { Home, Tags, Settings, AddMeme, AddTag } from './screens'; -import { horizontalScale } from './styles'; -import { FloatingActionButton } from './components'; import { darkNavigationTheme, lightNavigationTheme } from './theme'; +import { useDimensions } from './contexts'; +import { FloatingActionButton } from './components'; const TabNavigator = () => { + const dimensions = useDimensions(); + const TabNavigatorBase = createBottomTabNavigator(); - const [fabVisible, setFabVisible] = React.useState(true); return ( <> - - ( - { - const event = navigation.emit({ - type: 'tabPress', - target: route.key, - canPreventDefault: true, + ( + { + 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 (event.defaultPrevented) { - preventDefault(); - } else { - navigation.dispatch({ - ...CommonActions.navigate(route.name, route.params), - target: state.key, - }); - } - route.name === 'Settings' - ? setFabVisible(false) - : setFabVisible(true); - }} - renderIcon={({ route, focused, color }) => { - const { options } = descriptors[route.key]; - if (options.tabBarIcon) { - return options.tabBarIcon({ - focused, - color, - size: horizontalScale(20), - }); - } - }} - getLabelText={({ route }) => { - const { options } = descriptors[route.key]; - return options.title ?? route.name; - }} - /> - )}> - ( - - ), + } + }} + renderIcon={({ route, focused, color }) => { + const { options } = descriptors[route.key]; + if (options.tabBarIcon) { + return options.tabBarIcon({ + focused, + color, + size: dimensions.static.horizontalScale(20), + }); + } + }} + getLabelText={({ route }) => { + const { options } = descriptors[route.key]; + return options.title ?? route.name; }} /> - ( - - ), - }} - /> - ( - - ), - }} - /> - - - + )}> + ( + + ), + }} + /> + ( + + ), + }} + /> + ( + + ), + }} + /> + + ); }; diff --git a/src/screens/addMeme.tsx b/src/screens/addMeme.tsx index cc23a1d..afaf871 100644 --- a/src/screens/addMeme.tsx +++ b/src/screens/addMeme.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Appbar, Text } from 'react-native-paper'; -import { PaddedView } from '../components'; +import { RootScrollView } from '../components'; import { useNavigation } from '@react-navigation/native'; const AddMeme = () => { @@ -12,9 +12,9 @@ const AddMeme = () => { navigation.goBack()} /> - + Add Meme - + ); }; diff --git a/src/screens/addTag.tsx b/src/screens/addTag.tsx index 91f5da0..0de061c 100644 --- a/src/screens/addTag.tsx +++ b/src/screens/addTag.tsx @@ -1,37 +1,12 @@ import React, { useState } from 'react'; -import { View, StyleSheet } from 'react-native'; -import { - Chip, - TextInput, - Appbar, - HelperText, - Button, -} from 'react-native-paper'; +import { View } from 'react-native'; +import { TextInput, Appbar, HelperText, Button } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import { BSON } from 'realm'; import { useRealm } from '@realm/react'; -import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; -import { PaddedView } from '../components'; -import styles, { horizontalScale, verticalScale } from '../styles'; -import { - generateRandomColor, - getContrastColor, - isValidColor, -} from '../utilities'; - -const tagStyles = StyleSheet.create({ - preview: { - justifyContent: 'center', - flexDirection: 'row', - marginVertical: verticalScale(75), - }, - chip: { - padding: horizontalScale(5), - }, - chipText: { - fontSize: horizontalScale(15), - }, -}); +import { RootScrollView, TagPreview } from '../components'; +import styles from '../styles'; +import { generateRandomColor, isValidColor } from '../utilities'; const AddTag = () => { const navigation = useNavigation(); @@ -87,34 +62,19 @@ const AddTag = () => { navigation.goBack()} /> - - - - { - return ( - - ); - }} - elevated - style={[tagStyles.chip, { backgroundColor: validatedTagColor }]} - textStyle={[ - tagStyles.chipText, - { color: getContrastColor(validatedTagColor) }, - ]}> - {'#' + tagName} - - + + + {tagNameError} @@ -125,6 +85,7 @@ const AddTag = () => { value={tagColor} onChangeText={handleTagColorChange} error={!!tagColorError} + autoCorrect={false} right={ { {tagColorError} - - + + + + ); }; diff --git a/src/screens/home.tsx b/src/screens/home.tsx index 8a29ba4..c5470c0 100644 --- a/src/screens/home.tsx +++ b/src/screens/home.tsx @@ -9,8 +9,8 @@ import { Searchbar, } from 'react-native-paper'; import { useDispatch, useSelector } from 'react-redux'; -import { PaddedView } from '../components'; -import styles, { verticalScale } from '../styles'; +import { RootScrollView } from '../components'; +import styles from '../styles'; import { SORT, SORT_DIRECTION } from '../types'; import { getSortIcon, getViewIcon } from '../utilities'; import { @@ -23,10 +23,11 @@ import { setFilter, } from '../state'; import { MEME_TYPE, memeTypePlural } from '../database'; +import { useDimensions } from '../contexts'; const Home = () => { const theme = useTheme(); - + const dimensions = useDimensions(); const sort = useSelector((state: RootState) => state.home.sort); const sortDirection = useSelector( (state: RootState) => state.home.sortDirection, @@ -63,7 +64,7 @@ const Home = () => { const [search, setSearch] = useState(''); return ( - + @@ -96,13 +97,13 @@ const Home = () => { dispatch(cycleView())} /> dispatch(toggleFavoritesOnly())} /> { onPress={() => setFilterMenuVisible(true)} icon={filter ? 'filter' : 'filter-outline'} iconColor={theme.colors.primary} - size={verticalScale(16)} + size={dimensions.static.verticalScale(16)} /> }> { - + ); }; diff --git a/src/screens/settings.tsx b/src/screens/settings.tsx index 62ccff1..d3c5e23 100644 --- a/src/screens/settings.tsx +++ b/src/screens/settings.tsx @@ -4,18 +4,19 @@ import { Button, List, Snackbar, Switch, Text } from 'react-native-paper'; import { useRealm } from '@realm/react'; import { openDocumentTree } from 'react-native-scoped-storage'; import { useDispatch, useSelector } from 'react-redux'; -import { PaddedView } from '../components'; +import { RootScrollView } from '../components'; import styles from '../styles'; import { Meme } from '../database'; import { RootState, updateNoMedia, updateStorageUri } from '../state'; import type {} from 'redux-thunk/extend-redux'; +import { useDimensions } from '../contexts'; const SettingsScreen = () => { - const [optimizingDatabase, setOptimizingDatabase] = useState(false); - const noMedia = useSelector((state: RootState) => state.settings.noMedia); const dispatch = useDispatch(); + const dimensions = useDimensions(); + const [optimizingDatabase, setOptimizingDatabase] = useState(false); const [snackbarVisible, setSnackbarVisible] = useState(false); const [snackbarMessage, setSnackbarMessage] = useState(''); @@ -45,13 +46,15 @@ const SettingsScreen = () => { return ( <> - + Database - + ); }; diff --git a/src/screens/welcome.tsx b/src/screens/welcome.tsx index 84d3035..83a0da7 100644 --- a/src/screens/welcome.tsx +++ b/src/screens/welcome.tsx @@ -2,13 +2,15 @@ import React from 'react'; import { Button, Text } from 'react-native-paper'; import { useDispatch } from 'react-redux'; import { openDocumentTree } from 'react-native-scoped-storage'; -import { PaddedView } from '../components'; +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 dispatch = useDispatch(); + const dimensions = useDimensions(); const selectStorageLocation = async () => { const uri = await openDocumentTree(true).catch(noOp); @@ -18,19 +20,26 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { }; return ( - + + style={[ + { + marginBottom: dimensions.responsive.verticalScale(30), + }, + styles.centerText, + ]}> Welcome to Terminally Online! - + ); }; diff --git a/src/styles.tsx b/src/styles.tsx index 31ac461..a0fe2d2 100644 --- a/src/styles.tsx +++ b/src/styles.tsx @@ -1,24 +1,8 @@ -import { StyleSheet, Dimensions } from 'react-native'; - -const { width, height } = Dimensions.get('window'); - -const guidelineBaseWidth = 350; -const guidelineBaseHeight = 680; - -const horizontalScale = (size: number) => (width / guidelineBaseWidth) * size; -const verticalScale = (size: number) => (height / guidelineBaseHeight) * size; -const moderateScale = (size: number, factor = 0.5) => - size + (horizontalScale(size) - size) * factor; +import { StyleSheet } from 'react-native'; const styles = StyleSheet.create({ - marginBottom: { - marginBottom: verticalScale(15), - }, - bigMarginBottom: { - marginBottom: verticalScale(30), - }, - extremeMarginBottom: { - marginBottom: verticalScale(100), + smallPadding: { + padding: '2.5%', }, padding: { padding: '5%', @@ -27,19 +11,24 @@ const styles = StyleSheet.create({ paddingHorizontal: '2.5%', }, centered: { - flex: 1, justifyContent: 'center', alignItems: 'center', }, centeredVertical: { alignItems: 'center', }, + centeredHorizontal: { + justifyContent: 'center', + }, centerText: { textAlign: 'center', }, flex: { flex: 1, }, + flexGrow: { + flexGrow: 1, + }, flexRow: { flexDirection: 'row', }, @@ -57,6 +46,12 @@ const styles = StyleSheet.create({ flexRowReverse: { flexDirection: 'row-reverse', }, + justifyStart: { + justifyContent: 'flex-start', + }, + justifyEnd: { + justifyContent: 'flex-end', + }, }); -export { horizontalScale, verticalScale, moderateScale, styles as default }; +export default styles;