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())}
/>
-
+
);
};
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
-
+
setSnackbarVisible(false)}
diff --git a/src/screens/tags.tsx b/src/screens/tags.tsx
index eac253d..508dd65 100644
--- a/src/screens/tags.tsx
+++ b/src/screens/tags.tsx
@@ -1,7 +1,7 @@
import React from 'react';
import { Button, Text } from 'react-native-paper';
-import { PaddedView } from '../components';
+import { RootScrollView } from '../components';
import { useQuery, useRealm } from '@realm/react';
import { Tag, deleteAllTags } from '../database';
@@ -10,14 +10,14 @@ const Tags = () => {
const tags = useQuery('Tag');
return (
-
+
{tags.map(tag => (
{tag.name}
))}
deleteAllTags(realm)}>Delete All Tags
-
+
);
};
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!
+ style={{
+ marginBottom: dimensions.responsive.verticalScale(100),
+ }}>
Select Storage Location
-
+
);
};
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;