Refactor dimension handling

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-13 19:03:53 +03:00
parent 703155232d
commit 4128b0df20
17 changed files with 406 additions and 250 deletions

View File

@@ -3,17 +3,18 @@ import { AppState, StatusBar, useColorScheme } from 'react-native';
import { PaperProvider } from 'react-native-paper'; import { PaperProvider } from 'react-native-paper';
import { SafeAreaProvider } from 'react-native-safe-area-context'; import { SafeAreaProvider } from 'react-native-safe-area-context';
import { RealmProvider } from '@realm/react'; 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 { PersistGate } from 'redux-persist/integration/react';
import type {} from 'redux-thunk/extend-redux'; import type {} from 'redux-thunk/extend-redux';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { lightTheme, darkTheme } from './theme'; import { lightTheme, darkTheme } from './theme';
import { Meme, Tag } from './database'; import { Meme, Tag } from './database';
import NavigationContainer from './navigation'; import NavigationContainer from './navigation';
import { store, persistor, validateSettings } from './state'; import { store, persistor, validateSettings } from './state';
import { LoadingView } from './components'; import { LoadingView } from './components';
import { Welcome } from './screens'; import { Welcome } from './screens';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import styles from './styles'; import styles from './styles';
import { DimensionsProvider } from './contexts';
const App = () => { const App = () => {
const [showWelcome, setShowWelcome] = useState(false); const [showWelcome, setShowWelcome] = useState(false);
@@ -43,28 +44,30 @@ const App = () => {
return ( return (
<PaperProvider theme={theme}> <PaperProvider theme={theme}>
<Provider store={store}> <DimensionsProvider>
<PersistGate <ReduxProvider store={store}>
loading={<LoadingView />} <PersistGate
persistor={persistor} loading={<LoadingView />}
onBeforeLift={onBeforeLift}> persistor={persistor}
<RealmProvider schema={[Meme, Tag]}> onBeforeLift={onBeforeLift}>
<GestureHandlerRootView style={styles.flex}> <RealmProvider schema={[Meme, Tag]}>
<SafeAreaProvider> <GestureHandlerRootView style={styles.flex}>
<StatusBar <SafeAreaProvider>
barStyle={isDarkMode ? 'light-content' : 'dark-content'} <StatusBar
backgroundColor={theme.colors.background} barStyle={isDarkMode ? 'light-content' : 'dark-content'}
/> backgroundColor={theme.colors.background}
{showWelcome ? ( />
<Welcome onWelcomeComplete={() => setShowWelcome(false)} /> {showWelcome ? (
) : ( <Welcome onWelcomeComplete={() => setShowWelcome(false)} />
<NavigationContainer /> ) : (
)} <NavigationContainer />
</SafeAreaProvider> )}
</GestureHandlerRootView> </SafeAreaProvider>
</RealmProvider> </GestureHandlerRootView>
</PersistGate> </RealmProvider>
</Provider> </PersistGate>
</ReduxProvider>
</DimensionsProvider>
</PaperProvider> </PaperProvider>
); );
}; };

View File

@@ -1,24 +1,22 @@
import * as React from 'react'; import React, { useEffect, useState } from 'react';
import { StyleSheet, Keyboard } from 'react-native'; import { StyleSheet, Keyboard } from 'react-native';
import { FAB, Portal } from 'react-native-paper'; import { FAB } from 'react-native-paper';
import { horizontalScale, verticalScale } from '../styles';
import { ParamListBase, useNavigation } from '@react-navigation/native'; import { ParamListBase, useNavigation } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; import { NativeStackNavigationProp } from '@react-navigation/native-stack';
import { useEffect, useState } from 'react'; import { useDimensions } from '../contexts';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
fab: { fab: {
position: 'absolute', position: 'absolute',
right: horizontalScale(10),
bottom: verticalScale(75),
}, },
}); });
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
const [state, setState] = useState(false);
const { navigate } = const { navigate } =
useNavigation<NativeStackNavigationProp<ParamListBase>>(); useNavigation<NativeStackNavigationProp<ParamListBase>>();
const dimensions = useDimensions();
const [state, setState] = useState(false);
const [keyboardOpen, setKeyboardOpen] = useState(false); const [keyboardOpen, setKeyboardOpen] = useState(false);
useEffect(() => { useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener( const keyboardDidShowListener = Keyboard.addListener(
@@ -37,35 +35,39 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
}, []); }, []);
return ( return (
<Portal> <FAB.Group
<FAB.Group open={state}
open={state} visible={visible && !keyboardOpen}
visible={visible && !keyboardOpen} icon={state ? 'image' : 'plus'}
icon={state ? 'image' : 'plus'} actions={[
actions={[ {
{ icon: 'tag',
icon: 'tag', label: 'Tag',
label: 'Tag', onPress: () => navigate('Add Tag'),
onPress: () => navigate('Add Tag'), },
}, {
{ icon: 'note-text',
icon: 'note-text', label: 'Text',
label: 'Text', onPress: () => navigate('Add Meme'),
onPress: () => navigate('Add Meme'), },
}, {
{ icon: 'image-album',
icon: 'image-album', label: 'Album',
label: 'Album', onPress: () => navigate('Add Meme'),
onPress: () => navigate('Add Meme'), },
}, ]}
]} onStateChange={({ open }) => setState(open)}
onStateChange={({ open }) => setState(open)} onPress={() => {
onPress={() => { if (state) navigate('Add Meme');
if (state) navigate('Add Meme'); }}
}} style={[
style={styles.fab} styles.fab,
/> {
</Portal> paddingRight: dimensions.responsive.horizontalScale(10),
paddingBottom: dimensions.responsive.verticalScale(75),
},
]}
/>
); );
}; };

View File

@@ -1,3 +1,5 @@
export { default as FloatingActionButton } from './floatingActionButton'; export { default as FloatingActionButton } from './floatingActionButton';
export { default as LoadingView } from './loadingView'; 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';

View File

@@ -1,15 +1,15 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator } from 'react-native'; import { ActivityIndicator } from 'react-native';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import PaddedView from './paddedView'; import { RootView } from '.';
const LoadingView = () => { const LoadingView = () => {
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<PaddedView centered> <RootView centered>
<ActivityIndicator size="large" color={colors.primary} /> <ActivityIndicator size="large" color={colors.primary} />
</PaddedView> </RootView>
); );
}; };

View File

@@ -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<ViewStyle>;
centered?: boolean;
padded?: boolean;
}) => {
const { colors } = useTheme();
return (
<ScrollView
contentContainerStyle={[
padded && styles.padding,
centered && [styles.centered, styles.flex],
{ backgroundColor: colors.background },
style,
]}
nestedScrollEnabled>
{children}
</ScrollView>
);
};
export default RootScrollView;

View File

@@ -3,22 +3,24 @@ import { StyleProp, View, ViewStyle } from 'react-native';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import styles from '../styles'; import styles from '../styles';
const PaddedView = ({ const RootView = ({
children, children,
style, style,
centered, centered,
padded,
}: { }: {
children: ReactNode; children: ReactNode;
style?: StyleProp<ViewStyle>; style?: StyleProp<ViewStyle>;
centered?: boolean; centered?: boolean;
padded?: boolean;
}) => { }) => {
const { colors } = useTheme(); const { colors } = useTheme();
return ( return (
<View <View
style={[ style={[
styles.padding, padded && styles.padding,
centered && styles.centered, centered && [styles.centered, styles.flex],
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
style, style,
]}> ]}>
@@ -27,4 +29,4 @@ const PaddedView = ({
); );
}; };
export default PaddedView; export default RootView;

View File

@@ -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 (
<View
style={[
styles.centeredHorizontal,
styles.flexRow,
{
margin: dimensions.responsive.verticalScale(50),
},
]}>
<Chip
icon={() => {
return (
<FontAwesome5
name="tag"
size={dimensions.static.horizontalScale(12)}
color={getContrastColor(properties.color)}
/>
);
}}
elevated
style={[
{
backgroundColor: properties.color,
padding: dimensions.static.verticalScale(5),
},
]}
textStyle={[
{ fontSize: dimensions.static.horizontalScale(15) },
{ color: getContrastColor(properties.color) },
]}>
{'#' + properties.name}
</Chip>
</View>
);
};
export default TagPreview;

View File

@@ -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<DimensionsContext | undefined>(
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 (
<DimensionsContext.Provider
value={{
orientation,
responsive: responsiveScale,
static: staticScale,
}}>
{children}
</DimensionsContext.Provider>
);
};
const useDimensions = (): DimensionsContext => {
const context = useContext(DimensionsContext);
if (!context) {
throw new Error('useDimensions must be used within a DimensionsProvider');
}
return context;
};
export { DimensionsProvider, useDimensions };

1
src/contexts/index.ts Normal file
View File

@@ -0,0 +1 @@
export { DimensionsProvider, useDimensions } from './dimensions';

View File

@@ -6,91 +6,87 @@ import {
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, Portal, useTheme } from 'react-native-paper'; import { BottomNavigation, useTheme } from 'react-native-paper';
import { Home, Tags, Settings, AddMeme, AddTag } from './screens'; import { Home, Tags, Settings, AddMeme, AddTag } from './screens';
import { horizontalScale } from './styles';
import { FloatingActionButton } from './components';
import { darkNavigationTheme, lightNavigationTheme } from './theme'; import { darkNavigationTheme, lightNavigationTheme } from './theme';
import { useDimensions } from './contexts';
import { FloatingActionButton } from './components';
const TabNavigator = () => { const TabNavigator = () => {
const dimensions = useDimensions();
const TabNavigatorBase = createBottomTabNavigator(); const TabNavigatorBase = createBottomTabNavigator();
const [fabVisible, setFabVisible] = React.useState(true);
return ( return (
<> <>
<Portal.Host> <TabNavigatorBase.Navigator
<TabNavigatorBase.Navigator screenOptions={{
screenOptions={{ headerShown: false,
headerShown: false, }}
}} tabBar={({ navigation, state, descriptors, insets }) => (
tabBar={({ navigation, state, descriptors, insets }) => ( <BottomNavigation.Bar
<BottomNavigation.Bar navigationState={state}
navigationState={state} safeAreaInsets={insets}
safeAreaInsets={insets} onTabPress={({ route, preventDefault }) => {
onTabPress={({ route, preventDefault }) => { const event = navigation.emit({
const event = navigation.emit({ type: 'tabPress',
type: 'tabPress', target: route.key,
target: route.key, canPreventDefault: true,
canPreventDefault: true, });
if (event.defaultPrevented) {
preventDefault();
} else {
navigation.dispatch({
...CommonActions.navigate(route.name, route.params),
target: state.key,
}); });
if (event.defaultPrevented) { }
preventDefault(); }}
} else { renderIcon={({ route, focused, color }) => {
navigation.dispatch({ const { options } = descriptors[route.key];
...CommonActions.navigate(route.name, route.params), if (options.tabBarIcon) {
target: state.key, return options.tabBarIcon({
}); focused,
} color,
route.name === 'Settings' size: dimensions.static.horizontalScale(20),
? setFabVisible(false) });
: setFabVisible(true); }
}} }}
renderIcon={({ route, focused, color }) => { getLabelText={({ route }) => {
const { options } = descriptors[route.key]; const { options } = descriptors[route.key];
if (options.tabBarIcon) { return options.title ?? route.name;
return options.tabBarIcon({
focused,
color,
size: horizontalScale(20),
});
}
}}
getLabelText={({ route }) => {
const { options } = descriptors[route.key];
return options.title ?? route.name;
}}
/>
)}>
<TabNavigatorBase.Screen
name="Home"
component={Home}
options={{
tabBarIcon: ({ color, size }) => (
<FontAwesome5 name="home" color={color} size={size} />
),
}} }}
/> />
<TabNavigatorBase.Screen )}>
name="Tags" <TabNavigatorBase.Screen
component={Tags} name="Home"
options={{ component={Home}
tabBarIcon: ({ color, size }) => ( options={{
<FontAwesome5 name="tags" color={color} size={size} /> tabBarIcon: ({ color, size }) => (
), <FontAwesome5 name="home" color={color} size={size} />
}} ),
/> }}
<TabNavigatorBase.Screen />
name="Settings" <TabNavigatorBase.Screen
component={Settings} name="Tags"
options={{ component={Tags}
tabBarIcon: ({ color, size }) => ( options={{
<FontAwesome5 name="cog" color={color} size={size} /> tabBarIcon: ({ color, size }) => (
), <FontAwesome5 name="tags" color={color} size={size} />
}} ),
/> }}
</TabNavigatorBase.Navigator> />
<FloatingActionButton visible={fabVisible} /> <TabNavigatorBase.Screen
</Portal.Host> name="Settings"
component={Settings}
options={{
tabBarIcon: ({ color, size }) => (
<FontAwesome5 name="cog" color={color} size={size} />
),
}}
/>
</TabNavigatorBase.Navigator>
<FloatingActionButton />
</> </>
); );
}; };

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { Appbar, Text } from 'react-native-paper'; import { Appbar, Text } from 'react-native-paper';
import { PaddedView } from '../components'; import { RootScrollView } from '../components';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
const AddMeme = () => { const AddMeme = () => {
@@ -12,9 +12,9 @@ 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>
<PaddedView centered> <RootScrollView centered padded>
<Text>Add Meme</Text> <Text>Add Meme</Text>
</PaddedView> </RootScrollView>
</> </>
); );
}; };

View File

@@ -1,37 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native'; import { View } from 'react-native';
import { import { TextInput, Appbar, HelperText, Button } from 'react-native-paper';
Chip,
TextInput,
Appbar,
HelperText,
Button,
} 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 FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import { RootScrollView, TagPreview } from '../components';
import { PaddedView } from '../components'; import styles from '../styles';
import styles, { horizontalScale, verticalScale } from '../styles'; import { generateRandomColor, isValidColor } from '../utilities';
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),
},
});
const AddTag = () => { const AddTag = () => {
const navigation = useNavigation(); const navigation = useNavigation();
@@ -87,34 +62,19 @@ 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>
<PaddedView style={[styles.flex, styles.flexColumnSpaceBetween]}> <RootScrollView
<View> padded
<View style={[tagStyles.preview]}> style={[styles.flexGrow, styles.flexColumnSpaceBetween]}>
<Chip <View style={[styles.flex, styles.justifyStart]}>
icon={() => { <TagPreview name={tagName} color={validatedTagColor} />
return (
<FontAwesome5
name="tag"
size={horizontalScale(12)}
color={getContrastColor(validatedTagColor)}
/>
);
}}
elevated
style={[tagStyles.chip, { backgroundColor: validatedTagColor }]}
textStyle={[
tagStyles.chipText,
{ color: getContrastColor(validatedTagColor) },
]}>
{'#' + tagName}
</Chip>
</View>
<TextInput <TextInput
mode="outlined" mode="outlined"
label="Tag Name" label="Tag Name"
value={tagName} value={tagName}
onChangeText={handleTagNameChange} onChangeText={handleTagNameChange}
error={!!tagNameError} error={!!tagNameError}
autoCapitalize="none"
selectTextOnFocus
/> />
<HelperText type="error" visible={!!tagNameError}> <HelperText type="error" visible={!!tagNameError}>
{tagNameError} {tagNameError}
@@ -125,6 +85,7 @@ const AddTag = () => {
value={tagColor} value={tagColor}
onChangeText={handleTagColorChange} onChangeText={handleTagColorChange}
error={!!tagColorError} error={!!tagColorError}
autoCorrect={false}
right={ right={
<TextInput.Icon <TextInput.Icon
icon="palette" icon="palette"
@@ -136,14 +97,16 @@ const AddTag = () => {
{tagColorError} {tagColorError}
</HelperText> </HelperText>
</View> </View>
<Button <View style={[styles.flex, styles.justifyEnd]}>
mode="contained" <Button
icon="floppy" mode="contained"
onPress={handleSave} icon="floppy"
disabled={!!tagNameError || !!tagColorError}> onPress={handleSave}
Save disabled={!!tagNameError || !!tagColorError}>
</Button> Save
</PaddedView> </Button>
</View>
</RootScrollView>
</> </>
); );
}; };

View File

@@ -9,8 +9,8 @@ import {
Searchbar, Searchbar,
} from 'react-native-paper'; } from 'react-native-paper';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { PaddedView } from '../components'; import { RootScrollView } from '../components';
import styles, { verticalScale } from '../styles'; import styles from '../styles';
import { SORT, SORT_DIRECTION } from '../types'; import { SORT, SORT_DIRECTION } from '../types';
import { getSortIcon, getViewIcon } from '../utilities'; import { getSortIcon, getViewIcon } from '../utilities';
import { import {
@@ -23,10 +23,11 @@ import {
setFilter, setFilter,
} from '../state'; } from '../state';
import { MEME_TYPE, memeTypePlural } from '../database'; import { MEME_TYPE, memeTypePlural } from '../database';
import { useDimensions } from '../contexts';
const Home = () => { const Home = () => {
const theme = useTheme(); const theme = useTheme();
const dimensions = 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,
@@ -63,7 +64,7 @@ const Home = () => {
const [search, setSearch] = useState(''); const [search, setSearch] = useState('');
return ( return (
<PaddedView> <RootScrollView padded>
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} /> <Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
<View style={[styles.flexRowSpaceBetween, styles.centeredVertical]}> <View style={[styles.flexRowSpaceBetween, styles.centeredVertical]}>
<View style={[styles.flexRow, styles.centeredVertical]}> <View style={[styles.flexRow, styles.centeredVertical]}>
@@ -96,13 +97,13 @@ const Home = () => {
<IconButton <IconButton
icon={getViewIcon(view)} icon={getViewIcon(view)}
iconColor={theme.colors.primary} iconColor={theme.colors.primary}
size={verticalScale(16)} size={dimensions.static.verticalScale(16)}
onPress={() => dispatch(cycleView())} onPress={() => dispatch(cycleView())}
/> />
<IconButton <IconButton
icon={favoritesOnly ? 'heart' : 'heart-outline'} icon={favoritesOnly ? 'heart' : 'heart-outline'}
iconColor={theme.colors.primary} iconColor={theme.colors.primary}
size={verticalScale(16)} size={dimensions.static.verticalScale(16)}
onPress={() => dispatch(toggleFavoritesOnly())} onPress={() => dispatch(toggleFavoritesOnly())}
/> />
<Menu <Menu
@@ -113,7 +114,7 @@ const Home = () => {
onPress={() => setFilterMenuVisible(true)} onPress={() => setFilterMenuVisible(true)}
icon={filter ? 'filter' : 'filter-outline'} icon={filter ? 'filter' : 'filter-outline'}
iconColor={theme.colors.primary} iconColor={theme.colors.primary}
size={verticalScale(16)} size={dimensions.static.verticalScale(16)}
/> />
}> }>
<Menu.Item <Menu.Item
@@ -138,7 +139,7 @@ const Home = () => {
</View> </View>
</View> </View>
<Divider /> <Divider />
</PaddedView> </RootScrollView>
); );
}; };

View File

@@ -4,18 +4,19 @@ import { Button, List, Snackbar, Switch, Text } from 'react-native-paper';
import { useRealm } from '@realm/react'; import { useRealm } from '@realm/react';
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 { PaddedView } from '../components'; import { RootScrollView } from '../components';
import styles from '../styles'; import styles from '../styles';
import { Meme } from '../database'; import { Meme } from '../database';
import { RootState, updateNoMedia, updateStorageUri } from '../state'; import { RootState, updateNoMedia, updateStorageUri } from '../state';
import type {} from 'redux-thunk/extend-redux'; import type {} from 'redux-thunk/extend-redux';
import { useDimensions } from '../contexts';
const SettingsScreen = () => { const SettingsScreen = () => {
const [optimizingDatabase, setOptimizingDatabase] = useState(false);
const noMedia = useSelector((state: RootState) => state.settings.noMedia); const noMedia = useSelector((state: RootState) => state.settings.noMedia);
const dispatch = useDispatch(); const dispatch = useDispatch();
const dimensions = useDimensions();
const [optimizingDatabase, setOptimizingDatabase] = useState(false);
const [snackbarVisible, setSnackbarVisible] = useState(false); const [snackbarVisible, setSnackbarVisible] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState(''); const [snackbarMessage, setSnackbarMessage] = useState('');
@@ -45,13 +46,15 @@ const SettingsScreen = () => {
return ( return (
<> <>
<PaddedView> <RootScrollView padded>
<View> <View>
<List.Section> <List.Section>
<List.Subheader>Database</List.Subheader> <List.Subheader>Database</List.Subheader>
<Button <Button
mode="elevated" mode="elevated"
style={styles.marginBottom} style={{
marginBottom: dimensions.responsive.verticalScale(15),
}}
loading={optimizingDatabase} loading={optimizingDatabase}
onPress={optimizeDatabase}> onPress={optimizeDatabase}>
Optimize Database Now Optimize Database Now
@@ -61,7 +64,9 @@ const SettingsScreen = () => {
<List.Subheader>Media Storage</List.Subheader> <List.Subheader>Media Storage</List.Subheader>
<Button <Button
mode="elevated" mode="elevated"
style={styles.marginBottom} style={{
marginBottom: dimensions.responsive.verticalScale(15),
}}
onPress={async () => { onPress={async () => {
const { uri } = await openDocumentTree(true); const { uri } = await openDocumentTree(true);
void dispatch(updateStorageUri(uri)); void dispatch(updateStorageUri(uri));
@@ -72,7 +77,9 @@ const SettingsScreen = () => {
style={[ style={[
styles.flexRowSpaceBetween, styles.flexRowSpaceBetween,
styles.smallPaddingHorizontal, styles.smallPaddingHorizontal,
styles.marginBottom, {
marginBottom: dimensions.responsive.verticalScale(15),
},
]}> ]}>
<Text>Hide media from gallery</Text> <Text>Hide media from gallery</Text>
<Switch <Switch
@@ -84,7 +91,7 @@ const SettingsScreen = () => {
</View> </View>
</List.Section> </List.Section>
</View> </View>
</PaddedView> </RootScrollView>
<Snackbar <Snackbar
visible={snackbarVisible} visible={snackbarVisible}
onDismiss={() => setSnackbarVisible(false)} onDismiss={() => setSnackbarVisible(false)}

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { Button, Text } from 'react-native-paper'; import { Button, Text } from 'react-native-paper';
import { PaddedView } from '../components'; import { RootScrollView } from '../components';
import { useQuery, useRealm } from '@realm/react'; import { useQuery, useRealm } from '@realm/react';
import { Tag, deleteAllTags } from '../database'; import { Tag, deleteAllTags } from '../database';
@@ -10,14 +10,14 @@ const Tags = () => {
const tags = useQuery<Tag>('Tag'); const tags = useQuery<Tag>('Tag');
return ( return (
<PaddedView centered> <RootScrollView centered padded>
{tags.map(tag => ( {tags.map(tag => (
<Text key={tag.id.toHexString()} style={{ color: tag.color }}> <Text key={tag.id.toHexString()} style={{ color: tag.color }}>
{tag.name} {tag.name}
</Text> </Text>
))} ))}
<Button onPress={() => deleteAllTags(realm)}>Delete All Tags</Button> <Button onPress={() => deleteAllTags(realm)}>Delete All Tags</Button>
</PaddedView> </RootScrollView>
); );
}; };

View File

@@ -2,13 +2,15 @@ import React from 'react';
import { Button, Text } from 'react-native-paper'; import { Button, Text } 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 { PaddedView } from '../components'; 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';
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const dimensions = useDimensions();
const selectStorageLocation = async () => { const selectStorageLocation = async () => {
const uri = await openDocumentTree(true).catch(noOp); const uri = await openDocumentTree(true).catch(noOp);
@@ -18,19 +20,26 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
}; };
return ( return (
<PaddedView centered> <RootView centered padded>
<Text <Text
variant="displayMedium" variant="displayMedium"
style={[styles.bigMarginBottom, styles.centerText]}> style={[
{
marginBottom: dimensions.responsive.verticalScale(30),
},
styles.centerText,
]}>
Welcome to Terminally Online! Welcome to Terminally Online!
</Text> </Text>
<Button <Button
mode="contained" mode="contained"
onPress={selectStorageLocation} onPress={selectStorageLocation}
style={styles.extremeMarginBottom}> style={{
marginBottom: dimensions.responsive.verticalScale(100),
}}>
Select Storage Location Select Storage Location
</Button> </Button>
</PaddedView> </RootView>
); );
}; };

View File

@@ -1,24 +1,8 @@
import { StyleSheet, Dimensions } from 'react-native'; import { StyleSheet } 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;
const styles = StyleSheet.create({ const styles = StyleSheet.create({
marginBottom: { smallPadding: {
marginBottom: verticalScale(15), padding: '2.5%',
},
bigMarginBottom: {
marginBottom: verticalScale(30),
},
extremeMarginBottom: {
marginBottom: verticalScale(100),
}, },
padding: { padding: {
padding: '5%', padding: '5%',
@@ -27,19 +11,24 @@ const styles = StyleSheet.create({
paddingHorizontal: '2.5%', paddingHorizontal: '2.5%',
}, },
centered: { centered: {
flex: 1,
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },
centeredVertical: { centeredVertical: {
alignItems: 'center', alignItems: 'center',
}, },
centeredHorizontal: {
justifyContent: 'center',
},
centerText: { centerText: {
textAlign: 'center', textAlign: 'center',
}, },
flex: { flex: {
flex: 1, flex: 1,
}, },
flexGrow: {
flexGrow: 1,
},
flexRow: { flexRow: {
flexDirection: 'row', flexDirection: 'row',
}, },
@@ -57,6 +46,12 @@ const styles = StyleSheet.create({
flexRowReverse: { flexRowReverse: {
flexDirection: 'row-reverse', flexDirection: 'row-reverse',
}, },
justifyStart: {
justifyContent: 'flex-start',
},
justifyEnd: {
justifyContent: 'flex-end',
},
}); });
export { horizontalScale, verticalScale, moderateScale, styles as default }; export default styles;