Refactor dimension handling
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
51
src/app.tsx
51
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 (
|
||||
<PaperProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<PersistGate
|
||||
loading={<LoadingView />}
|
||||
persistor={persistor}
|
||||
onBeforeLift={onBeforeLift}>
|
||||
<RealmProvider schema={[Meme, Tag]}>
|
||||
<GestureHandlerRootView style={styles.flex}>
|
||||
<SafeAreaProvider>
|
||||
<StatusBar
|
||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||
backgroundColor={theme.colors.background}
|
||||
/>
|
||||
{showWelcome ? (
|
||||
<Welcome onWelcomeComplete={() => setShowWelcome(false)} />
|
||||
) : (
|
||||
<NavigationContainer />
|
||||
)}
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
</RealmProvider>
|
||||
</PersistGate>
|
||||
</Provider>
|
||||
<DimensionsProvider>
|
||||
<ReduxProvider store={store}>
|
||||
<PersistGate
|
||||
loading={<LoadingView />}
|
||||
persistor={persistor}
|
||||
onBeforeLift={onBeforeLift}>
|
||||
<RealmProvider schema={[Meme, Tag]}>
|
||||
<GestureHandlerRootView style={styles.flex}>
|
||||
<SafeAreaProvider>
|
||||
<StatusBar
|
||||
barStyle={isDarkMode ? 'light-content' : 'dark-content'}
|
||||
backgroundColor={theme.colors.background}
|
||||
/>
|
||||
{showWelcome ? (
|
||||
<Welcome onWelcomeComplete={() => setShowWelcome(false)} />
|
||||
) : (
|
||||
<NavigationContainer />
|
||||
)}
|
||||
</SafeAreaProvider>
|
||||
</GestureHandlerRootView>
|
||||
</RealmProvider>
|
||||
</PersistGate>
|
||||
</ReduxProvider>
|
||||
</DimensionsProvider>
|
||||
</PaperProvider>
|
||||
);
|
||||
};
|
||||
|
@@ -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<NativeStackNavigationProp<ParamListBase>>();
|
||||
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 (
|
||||
<Portal>
|
||||
<FAB.Group
|
||||
open={state}
|
||||
visible={visible && !keyboardOpen}
|
||||
icon={state ? 'image' : 'plus'}
|
||||
actions={[
|
||||
{
|
||||
icon: 'tag',
|
||||
label: 'Tag',
|
||||
onPress: () => 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}
|
||||
/>
|
||||
</Portal>
|
||||
<FAB.Group
|
||||
open={state}
|
||||
visible={visible && !keyboardOpen}
|
||||
icon={state ? 'image' : 'plus'}
|
||||
actions={[
|
||||
{
|
||||
icon: 'tag',
|
||||
label: 'Tag',
|
||||
onPress: () => 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),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -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';
|
||||
|
@@ -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 (
|
||||
<PaddedView centered>
|
||||
<RootView centered>
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
</PaddedView>
|
||||
</RootView>
|
||||
);
|
||||
};
|
||||
|
||||
|
37
src/components/rootScrollView.tsx
Normal file
37
src/components/rootScrollView.tsx
Normal 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;
|
@@ -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<ViewStyle>;
|
||||
centered?: boolean;
|
||||
padded?: boolean;
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.padding,
|
||||
centered && styles.centered,
|
||||
padded && styles.padding,
|
||||
centered && [styles.centered, styles.flex],
|
||||
{ backgroundColor: colors.background },
|
||||
style,
|
||||
]}>
|
||||
@@ -27,4 +29,4 @@ const PaddedView = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default PaddedView;
|
||||
export default RootView;
|
48
src/components/tagPreview.tsx
Normal file
48
src/components/tagPreview.tsx
Normal 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;
|
90
src/contexts/dimensions.tsx
Normal file
90
src/contexts/dimensions.tsx
Normal 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
1
src/contexts/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { DimensionsProvider, useDimensions } from './dimensions';
|
@@ -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 (
|
||||
<>
|
||||
<Portal.Host>
|
||||
<TabNavigatorBase.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
tabBar={({ navigation, state, descriptors, insets }) => (
|
||||
<BottomNavigation.Bar
|
||||
navigationState={state}
|
||||
safeAreaInsets={insets}
|
||||
onTabPress={({ route, preventDefault }) => {
|
||||
const event = navigation.emit({
|
||||
type: 'tabPress',
|
||||
target: route.key,
|
||||
canPreventDefault: true,
|
||||
<TabNavigatorBase.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
}}
|
||||
tabBar={({ navigation, state, descriptors, insets }) => (
|
||||
<BottomNavigation.Bar
|
||||
navigationState={state}
|
||||
safeAreaInsets={insets}
|
||||
onTabPress={({ route, preventDefault }) => {
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
)}>
|
||||
<TabNavigatorBase.Screen
|
||||
name="Home"
|
||||
component={Home}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<FontAwesome5 name="home" color={color} size={size} />
|
||||
),
|
||||
}
|
||||
}}
|
||||
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;
|
||||
}}
|
||||
/>
|
||||
<TabNavigatorBase.Screen
|
||||
name="Tags"
|
||||
component={Tags}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<FontAwesome5 name="tags" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TabNavigatorBase.Screen
|
||||
name="Settings"
|
||||
component={Settings}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<FontAwesome5 name="cog" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</TabNavigatorBase.Navigator>
|
||||
<FloatingActionButton visible={fabVisible} />
|
||||
</Portal.Host>
|
||||
)}>
|
||||
<TabNavigatorBase.Screen
|
||||
name="Home"
|
||||
component={Home}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<FontAwesome5 name="home" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TabNavigatorBase.Screen
|
||||
name="Tags"
|
||||
component={Tags}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<FontAwesome5 name="tags" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
<TabNavigatorBase.Screen
|
||||
name="Settings"
|
||||
component={Settings}
|
||||
options={{
|
||||
tabBarIcon: ({ color, size }) => (
|
||||
<FontAwesome5 name="cog" color={color} size={size} />
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</TabNavigatorBase.Navigator>
|
||||
<FloatingActionButton />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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 = () => {
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.Content title="Add Meme" />
|
||||
</Appbar.Header>
|
||||
<PaddedView centered>
|
||||
<RootScrollView centered padded>
|
||||
<Text>Add Meme</Text>
|
||||
</PaddedView>
|
||||
</RootScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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 = () => {
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.Content title="Add Tag" />
|
||||
</Appbar.Header>
|
||||
<PaddedView style={[styles.flex, styles.flexColumnSpaceBetween]}>
|
||||
<View>
|
||||
<View style={[tagStyles.preview]}>
|
||||
<Chip
|
||||
icon={() => {
|
||||
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>
|
||||
<RootScrollView
|
||||
padded
|
||||
style={[styles.flexGrow, styles.flexColumnSpaceBetween]}>
|
||||
<View style={[styles.flex, styles.justifyStart]}>
|
||||
<TagPreview name={tagName} color={validatedTagColor} />
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Tag Name"
|
||||
value={tagName}
|
||||
onChangeText={handleTagNameChange}
|
||||
error={!!tagNameError}
|
||||
autoCapitalize="none"
|
||||
selectTextOnFocus
|
||||
/>
|
||||
<HelperText type="error" visible={!!tagNameError}>
|
||||
{tagNameError}
|
||||
@@ -125,6 +85,7 @@ const AddTag = () => {
|
||||
value={tagColor}
|
||||
onChangeText={handleTagColorChange}
|
||||
error={!!tagColorError}
|
||||
autoCorrect={false}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon="palette"
|
||||
@@ -136,14 +97,16 @@ const AddTag = () => {
|
||||
{tagColorError}
|
||||
</HelperText>
|
||||
</View>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
disabled={!!tagNameError || !!tagColorError}>
|
||||
Save
|
||||
</Button>
|
||||
</PaddedView>
|
||||
<View style={[styles.flex, styles.justifyEnd]}>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
disabled={!!tagNameError || !!tagColorError}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
</RootScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -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 (
|
||||
<PaddedView>
|
||||
<RootScrollView padded>
|
||||
<Searchbar placeholder="Search" value={search} onChangeText={setSearch} />
|
||||
<View style={[styles.flexRowSpaceBetween, styles.centeredVertical]}>
|
||||
<View style={[styles.flexRow, styles.centeredVertical]}>
|
||||
@@ -96,13 +97,13 @@ const Home = () => {
|
||||
<IconButton
|
||||
icon={getViewIcon(view)}
|
||||
iconColor={theme.colors.primary}
|
||||
size={verticalScale(16)}
|
||||
size={dimensions.static.verticalScale(16)}
|
||||
onPress={() => dispatch(cycleView())}
|
||||
/>
|
||||
<IconButton
|
||||
icon={favoritesOnly ? 'heart' : 'heart-outline'}
|
||||
iconColor={theme.colors.primary}
|
||||
size={verticalScale(16)}
|
||||
size={dimensions.static.verticalScale(16)}
|
||||
onPress={() => dispatch(toggleFavoritesOnly())}
|
||||
/>
|
||||
<Menu
|
||||
@@ -113,7 +114,7 @@ const Home = () => {
|
||||
onPress={() => setFilterMenuVisible(true)}
|
||||
icon={filter ? 'filter' : 'filter-outline'}
|
||||
iconColor={theme.colors.primary}
|
||||
size={verticalScale(16)}
|
||||
size={dimensions.static.verticalScale(16)}
|
||||
/>
|
||||
}>
|
||||
<Menu.Item
|
||||
@@ -138,7 +139,7 @@ const Home = () => {
|
||||
</View>
|
||||
</View>
|
||||
<Divider />
|
||||
</PaddedView>
|
||||
</RootScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -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 (
|
||||
<>
|
||||
<PaddedView>
|
||||
<RootScrollView padded>
|
||||
<View>
|
||||
<List.Section>
|
||||
<List.Subheader>Database</List.Subheader>
|
||||
<Button
|
||||
mode="elevated"
|
||||
style={styles.marginBottom}
|
||||
style={{
|
||||
marginBottom: dimensions.responsive.verticalScale(15),
|
||||
}}
|
||||
loading={optimizingDatabase}
|
||||
onPress={optimizeDatabase}>
|
||||
Optimize Database Now
|
||||
@@ -61,7 +64,9 @@ const SettingsScreen = () => {
|
||||
<List.Subheader>Media Storage</List.Subheader>
|
||||
<Button
|
||||
mode="elevated"
|
||||
style={styles.marginBottom}
|
||||
style={{
|
||||
marginBottom: dimensions.responsive.verticalScale(15),
|
||||
}}
|
||||
onPress={async () => {
|
||||
const { uri } = await openDocumentTree(true);
|
||||
void dispatch(updateStorageUri(uri));
|
||||
@@ -72,7 +77,9 @@ const SettingsScreen = () => {
|
||||
style={[
|
||||
styles.flexRowSpaceBetween,
|
||||
styles.smallPaddingHorizontal,
|
||||
styles.marginBottom,
|
||||
{
|
||||
marginBottom: dimensions.responsive.verticalScale(15),
|
||||
},
|
||||
]}>
|
||||
<Text>Hide media from gallery</Text>
|
||||
<Switch
|
||||
@@ -84,7 +91,7 @@ const SettingsScreen = () => {
|
||||
</View>
|
||||
</List.Section>
|
||||
</View>
|
||||
</PaddedView>
|
||||
</RootScrollView>
|
||||
<Snackbar
|
||||
visible={snackbarVisible}
|
||||
onDismiss={() => setSnackbarVisible(false)}
|
||||
|
@@ -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>('Tag');
|
||||
|
||||
return (
|
||||
<PaddedView centered>
|
||||
<RootScrollView centered padded>
|
||||
{tags.map(tag => (
|
||||
<Text key={tag.id.toHexString()} style={{ color: tag.color }}>
|
||||
{tag.name}
|
||||
</Text>
|
||||
))}
|
||||
<Button onPress={() => deleteAllTags(realm)}>Delete All Tags</Button>
|
||||
</PaddedView>
|
||||
</RootScrollView>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -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 (
|
||||
<PaddedView centered>
|
||||
<RootView centered padded>
|
||||
<Text
|
||||
variant="displayMedium"
|
||||
style={[styles.bigMarginBottom, styles.centerText]}>
|
||||
style={[
|
||||
{
|
||||
marginBottom: dimensions.responsive.verticalScale(30),
|
||||
},
|
||||
styles.centerText,
|
||||
]}>
|
||||
Welcome to Terminally Online!
|
||||
</Text>
|
||||
<Button
|
||||
mode="contained"
|
||||
onPress={selectStorageLocation}
|
||||
style={styles.extremeMarginBottom}>
|
||||
style={{
|
||||
marginBottom: dimensions.responsive.verticalScale(100),
|
||||
}}>
|
||||
Select Storage Location
|
||||
</Button>
|
||||
</PaddedView>
|
||||
</RootView>
|
||||
);
|
||||
};
|
||||
|
||||
|
@@ -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;
|
||||
|
Reference in New Issue
Block a user