Fix various spacing issues

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-25 22:00:33 +03:00
parent e479e3c0ad
commit caa98736e9
28 changed files with 362 additions and 488 deletions

16
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@bankify/redux-persist-realm": "^0.1.3", "@bankify/redux-persist-realm": "^0.1.3",
"@likashefqet/react-native-image-zoom": "^1.3.0", "@likashefqet/react-native-image-zoom": "^1.3.0",
"@react-native-clipboard/clipboard": "^1.11.2", "@react-native-clipboard/clipboard": "^1.11.2",
"@react-native-community/hooks": "^3.0.0",
"@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7", "@react-navigation/native": "^6.1.7",
"@react-navigation/native-stack": "^6.9.13", "@react-navigation/native-stack": "^6.9.13",
@@ -4112,6 +4113,15 @@
"node": ">= 4.0.0" "node": ">= 4.0.0"
} }
}, },
"node_modules/@react-native-community/hooks": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-native-community/hooks/-/hooks-3.0.0.tgz",
"integrity": "sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==",
"peerDependencies": {
"react": ">=17.0.2",
"react-native": ">=0.65"
}
},
"node_modules/@react-native/assets-registry": { "node_modules/@react-native/assets-registry": {
"version": "0.72.0", "version": "0.72.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz",
@@ -18759,6 +18769,12 @@
"joi": "^17.2.1" "joi": "^17.2.1"
} }
}, },
"@react-native-community/hooks": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@react-native-community/hooks/-/hooks-3.0.0.tgz",
"integrity": "sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==",
"requires": {}
},
"@react-native/assets-registry": { "@react-native/assets-registry": {
"version": "0.72.0", "version": "0.72.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz",

View File

@@ -17,6 +17,7 @@
"@bankify/redux-persist-realm": "^0.1.3", "@bankify/redux-persist-realm": "^0.1.3",
"@likashefqet/react-native-image-zoom": "^1.3.0", "@likashefqet/react-native-image-zoom": "^1.3.0",
"@react-native-clipboard/clipboard": "^1.11.2", "@react-native-clipboard/clipboard": "^1.11.2",
"@react-native-community/hooks": "^3.0.0",
"@react-navigation/bottom-tabs": "^6.5.8", "@react-navigation/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7", "@react-navigation/native": "^6.1.7",
"@react-navigation/native-stack": "^6.9.13", "@react-navigation/native-stack": "^6.9.13",

View File

@@ -14,7 +14,6 @@ import { store, persistor, validateSettings } from './state';
import { LoadingView } from './components'; import { LoadingView } from './components';
import { Welcome } from './screens'; import { Welcome } from './screens';
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);
@@ -44,30 +43,28 @@ const App = () => {
return ( return (
<PaperProvider theme={theme}> <PaperProvider theme={theme}>
<DimensionsProvider> <ReduxProvider store={store}>
<ReduxProvider store={store}> <PersistGate
<PersistGate loading={<LoadingView />}
loading={<LoadingView />} persistor={persistor}
persistor={persistor} onBeforeLift={onBeforeLift}>
onBeforeLift={onBeforeLift}> <RealmProvider schema={[Meme, Tag]}>
<RealmProvider schema={[Meme, Tag]}> <GestureHandlerRootView style={styles.flex}>
<GestureHandlerRootView style={styles.flex}> <SafeAreaProvider>
<SafeAreaProvider> <StatusBar
<StatusBar barStyle={isDarkMode ? 'light-content' : 'dark-content'}
barStyle={isDarkMode ? 'light-content' : 'dark-content'} backgroundColor={theme.colors.background}
backgroundColor={theme.colors.background} />
/> {showWelcome ? (
{showWelcome ? ( <Welcome onWelcomeComplete={() => setShowWelcome(false)} />
<Welcome onWelcomeComplete={() => setShowWelcome(false)} /> ) : (
) : ( <NavigationContainer />
<NavigationContainer /> )}
)} </SafeAreaProvider>
</SafeAreaProvider> </GestureHandlerRootView>
</GestureHandlerRootView> </RealmProvider>
</RealmProvider> </PersistGate>
</PersistGate> </ReduxProvider>
</ReduxProvider>
</DimensionsProvider>
</PaperProvider> </PaperProvider>
); );
}; };

View File

@@ -4,9 +4,9 @@ import { FAB } from 'react-native-paper';
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 { pickSingle } from 'react-native-document-picker'; import { pickSingle } from 'react-native-document-picker';
import { ORIENTATION, useDimensions } from '../contexts';
import { ROUTE } from '../types'; import { ROUTE } from '../types';
import { allowedMimeTypes, noOp } from '../utilities'; import { allowedMimeTypes, noOp } from '../utilities';
import { useDeviceOrientation } from '@react-native-community/hooks';
const floatingActionButtonStyles = StyleSheet.create({ const floatingActionButtonStyles = StyleSheet.create({
fab: { fab: {
@@ -22,7 +22,7 @@ const floatingActionButtonStyles = StyleSheet.create({
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
const { navigate } = const { navigate } =
useNavigation<NativeStackNavigationProp<ParamListBase>>(); useNavigation<NativeStackNavigationProp<ParamListBase>>();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const [state, setState] = useState(false); const [state, setState] = useState(false);
const [keyboardOpen, setKeyboardOpen] = useState(false); const [keyboardOpen, setKeyboardOpen] = useState(false);
@@ -70,7 +70,7 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
navigate(ROUTE.ADD_MEME, { file }); navigate(ROUTE.ADD_MEME, { file });
}} }}
style={ style={
orientation === ORIENTATION.PORTRAIT orientation === 'portrait'
? floatingActionButtonStyles.fab ? floatingActionButtonStyles.fab
: floatingActionButtonStyles.fabLandscape : floatingActionButtonStyles.fabLandscape
} }

View File

@@ -1,8 +1,8 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { Animated, StyleSheet } from 'react-native'; import { Animated, StyleSheet } from 'react-native';
import { ORIENTATION, useDimensions } from '../contexts';
import styles from '../styles';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import { useDeviceOrientation } from '@react-native-community/hooks';
import styles from '../styles';
const hideableHeaderStyles = StyleSheet.create({ const hideableHeaderStyles = StyleSheet.create({
headerView: { headerView: {
@@ -22,7 +22,7 @@ const HideableHeader = ({
children: React.ReactNode; children: React.ReactNode;
}) => { }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const headerAnim = useRef(new Animated.Value(visible ? 1 : 0)).current; const headerAnim = useRef(new Animated.Value(visible ? 1 : 0)).current;
@@ -38,9 +38,7 @@ const HideableHeader = ({
<Animated.View <Animated.View
style={[ style={[
hideableHeaderStyles.headerView, hideableHeaderStyles.headerView,
orientation === ORIENTATION.PORTRAIT orientation === 'portrait' ? styles.paddingTop : styles.smallPaddingTop,
? styles.paddingTop
: styles.smallPaddingTop,
styles.paddingHorizontal, styles.paddingHorizontal,
{ {
transform: [ transform: [

View File

@@ -9,7 +9,7 @@ const LoadingView = () => {
return ( return (
<View <View
style={[ style={[
styles.centered, styles.center,
styles.flex, styles.flex,
styles.fullSize, styles.fullSize,
{ backgroundColor: colors.background }, { backgroundColor: colors.background },

View File

@@ -1,11 +1,12 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { HelperText, TextInput } from 'react-native-paper'; import { HelperText, TextInput } from 'react-native-paper';
import { Image } from 'react-native'; import { Image } from 'react-native';
import { useDimensions } from '../../contexts'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import LoadingView from '../loadingView'; import LoadingView from '../loadingView';
import { MemeTagSelector } from '.'; import { MemeTagSelector } from '.';
import { Tag } from '../../database'; import { Tag } from '../../database';
import { StringValidationResult, validateMemeTitle } from '../../utilities'; import { StringValidationResult, validateMemeTitle } from '../../utilities';
import { useImageDimensions } from '@react-native-community/hooks';
const memeEditorStyles = { const memeEditorStyles = {
image: { image: {
@@ -33,30 +34,11 @@ const MemeEditor = ({
memeTags: Map<string, Tag>; memeTags: Map<string, Tag>;
setMemeTags: (tags: Map<string, Tag>) => void; setMemeTags: (tags: Map<string, Tag>) => void;
}) => { }) => {
const { dimensions } = useDimensions(); const { width } = useSafeAreaFrame();
const [imageWidth, setImageWidth] = useState<number>(); const { dimensions, loading, error } = useImageDimensions({ uri: memeUri });
const [imageHeight, setImageHeight] = useState<number>();
useEffect(() => { if (loading || error || !dimensions) return <LoadingView />;
// eslint-disable-next-line unicorn/no-useless-undefined
setImageWidth(undefined);
// eslint-disable-next-line unicorn/no-useless-undefined
setImageHeight(undefined);
Image.getSize(memeUri, (width, height) => {
const paddedWidth = dimensions.width * 0.92;
const paddedHeight = Math.max(
Math.min((paddedWidth / width) * height, 500),
100,
);
setImageWidth(paddedWidth);
setImageHeight(paddedHeight);
});
}, [memeUri, dimensions.width]);
if (!imageWidth || !imageHeight) return <LoadingView />;
return ( return (
<> <>
@@ -75,8 +57,14 @@ const MemeEditor = ({
source={{ uri: memeUri }} source={{ uri: memeUri }}
style={[ style={[
{ {
width: imageWidth, width: width * 0.92,
height: imageHeight, height: Math.max(
Math.min(
((width * 0.92) / dimensions.width) * dimensions.height,
500,
),
100,
),
}, },
memeEditorStyles.image, memeEditorStyles.image,
]} ]}

View File

@@ -1,12 +1,13 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { TagChip } from '../tags';
import { Tag } from '../../database';
import { useQuery, useRealm } from '@realm/react'; import { useQuery, useRealm } from '@realm/react';
import { TAG_SORT, tagSortQuery } from '../../types';
import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper'; import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import styles from '../../styles';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { TAG_SORT, tagSortQuery } from '../../types';
import { TagChip } from '../tags';
import { Tag } from '../../database';
import styles from '../../styles';
import { validateTagName } from '../../utilities'; import { validateTagName } from '../../utilities';
const memeTagSearchModalStyles = StyleSheet.create({ const memeTagSearchModalStyles = StyleSheet.create({
@@ -36,6 +37,7 @@ const MemeTagSearchModal = ({
memeTags: Map<string, Tag>; memeTags: Map<string, Tag>;
setMemeTags: (tags: Map<string, Tag>) => void; setMemeTags: (tags: Map<string, Tag>) => void;
}) => { }) => {
const { width } = useSafeAreaFrame();
const { colors } = useTheme(); const { colors } = useTheme();
const realm = useRealm(); const realm = useRealm();
@@ -115,6 +117,10 @@ const MemeTagSearchModal = ({
extraData={memeTags} extraData={memeTags}
horizontal horizontal
estimatedItemSize={120} estimatedItemSize={120}
estimatedListSize={{
width: width - 10,
height: 34.5,
}}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
keyboardShouldPersistTaps={'always'} keyboardShouldPersistTaps={'always'}
renderItem={({ item: tag }) => ( renderItem={({ item: tag }) => (
@@ -135,6 +141,7 @@ const MemeTagSearchModal = ({
Create Tag #{tagName.valid ? tagName.parsed : 'newTag'} Create Tag #{tagName.valid ? tagName.parsed : 'newTag'}
</Chip> </Chip>
)} )}
fadingEdgeLength={50}
/> />
</Modal> </Modal>
</Portal> </Portal>

View File

@@ -1,11 +1,11 @@
import React, { ComponentProps, useState } from 'react'; import React, { ComponentProps, useState } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { Chip } from 'react-native-paper'; import { Chip } from 'react-native-paper';
import { FlashList } from '@shopify/flash-list';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { TagChip } from '../tags'; import { TagChip } from '../tags';
import { Tag } from '../../database'; import { Tag } from '../../database';
import { useDimensions } from '../../contexts';
import { MemeTagSearchModal } from '.'; import { MemeTagSearchModal } from '.';
import { FlashList } from '@shopify/flash-list';
const memeTagSelectorStyles = StyleSheet.create({ const memeTagSelectorStyles = StyleSheet.create({
tagChip: { tagChip: {
@@ -21,7 +21,7 @@ const MemeTagSelector = ({
memeTags: Map<string, Tag>; memeTags: Map<string, Tag>;
setMemeTags: (tags: Map<string, Tag>) => void; setMemeTags: (tags: Map<string, Tag>) => void;
} & ComponentProps<typeof View>) => { } & ComponentProps<typeof View>) => {
const { dimensions } = useDimensions(); const { width } = useSafeAreaFrame();
const [flashListMargin, setFlashListMargin] = useState(0); const [flashListMargin, setFlashListMargin] = useState(0);
const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false); const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false);
@@ -37,7 +37,6 @@ const MemeTagSelector = ({
<View {...props}> <View {...props}>
<FlashList <FlashList
data={[...memeTags.values()]} data={[...memeTags.values()]}
extraData={memeTags}
horizontal horizontal
estimatedItemSize={120} estimatedItemSize={120}
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
@@ -55,7 +54,7 @@ const MemeTagSelector = ({
onPress={() => setTagSearchModalVisible(true)} onPress={() => setTagSearchModalVisible(true)}
onLayout={event => onLayout={event =>
setFlashListMargin( setFlashListMargin(
dimensions.width * 0.92 - event.nativeEvent.layout.width, width * 0.92 - event.nativeEvent.layout.width,
) )
} }
style={{ style={{
@@ -64,6 +63,7 @@ const MemeTagSelector = ({
Add Tag Add Tag
</Chip> </Chip>
)} )}
fadingEdgeLength={50}
/> />
</View> </View>
<MemeTagSearchModal <MemeTagSearchModal

View File

@@ -1,51 +1,36 @@
import React, { useEffect, useState } from 'react'; import React from 'react';
import { ActivityIndicator, useTheme } from 'react-native-paper';
import { ImageZoom } from '@likashefqet/react-native-image-zoom'; import { ImageZoom } from '@likashefqet/react-native-image-zoom';
import { Meme } from '../../database'; import { Meme } from '../../database';
import { useDimensions } from '../../contexts'; import { View } from 'react-native';
import { Image, View } from 'react-native';
import styles from '../../styles'; import styles from '../../styles';
import LoadingView from '../loadingView';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { useImageDimensions } from '@react-native-community/hooks';
const MemeViewItem = ({ meme }: { meme: Meme }) => { const MemeViewItem = ({ meme }: { meme: Meme }) => {
const { dimensions } = useDimensions(); const { height, width } = useSafeAreaFrame();
const { colors } = useTheme();
const [imageWidth, setImageWidth] = useState<number>(); const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
const [imageHeight, setImageHeight] = useState<number>();
useEffect(() => { if (loading || error || !dimensions) return <LoadingView />;
Image.getSize(meme.uri, (width, height) => {
const ratio = width / height;
const screenRatio = dimensions.width / dimensions.height - 160;
if (ratio > screenRatio) {
setImageWidth(dimensions.width);
setImageHeight(dimensions.width / ratio);
} else {
setImageWidth(dimensions.height * ratio);
setImageHeight(dimensions.height);
}
});
}, [meme.uri, dimensions.width, dimensions.height]);
return ( return (
<View <View style={[{ width, height }, styles.center]}>
style={[ <ImageZoom
{ source={{ uri: meme.uri }}
width: dimensions.width, style={
height: dimensions.height - 160, dimensions.aspectRatio > width / (height - 128)
backgroundColor: colors.background, ? {
}, width,
styles.centered, height: width / (dimensions.width / dimensions.height),
]}> }
{imageWidth && imageHeight ? ( : {
<ImageZoom width: (height - 128) * (dimensions.width / dimensions.height),
source={{ uri: meme.uri }} height: height - 128,
style={{ width: imageWidth, height: imageHeight }} }
/> }
) : ( minScale={0.5}
<ActivityIndicator size="large" color={colors.primary} /> />
)}
</View> </View>
); );
}; };

View File

@@ -1,8 +1,9 @@
import React, { useState } from 'react'; import React from 'react';
import { Image, TouchableHighlight, View } from 'react-native'; import { Image, TouchableHighlight } from 'react-native';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { useImageDimensions } from '@react-native-community/hooks';
import { Meme } from '../../../database'; import { Meme } from '../../../database';
import { useDimensions } from '../../../contexts';
import { RootState } from '../../../state'; import { RootState } from '../../../state';
const MemesGridItem = ({ const MemesGridItem = ({
@@ -14,33 +15,27 @@ const MemesGridItem = ({
index: number; index: number;
focusMeme: (index: number) => void; focusMeme: (index: number) => void;
}) => { }) => {
const { dimensions } = useDimensions(); const { width } = useSafeAreaFrame();
const gridColumns = useSelector( const gridColumns = useSelector(
(state: RootState) => state.settings.gridColumns, (state: RootState) => state.settings.gridColumns,
); );
const [imageWidth, setImageWidth] = useState<number>(); const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, () => { if (loading || error || !dimensions) return <></>;
const paddedWidth = (dimensions.width * 0.92 - 5) / gridColumns;
setImageWidth(paddedWidth);
setImageHeight(paddedWidth);
});
return ( return (
<> <TouchableHighlight onPress={() => focusMeme(index)}>
{imageWidth && imageHeight && ( <Image
<View> source={{ uri: meme.uri }}
<TouchableHighlight onPress={() => focusMeme(index)}> style={[
<Image {
source={{ uri: meme.uri }} width: (width * 0.92 - 5) / gridColumns,
style={[{ width: imageWidth, height: imageHeight }]} height: (width * 0.92 - 5) / gridColumns,
/> },
</TouchableHighlight> ]}
</View> />
)} </TouchableHighlight>
</>
); );
}; };

View File

@@ -7,7 +7,7 @@ import {
} from 'react-native'; } from 'react-native';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Divider, HelperText } from 'react-native-paper'; import { Divider, HelperText } from 'react-native-paper';
import { useDimensions, ORIENTATION } from '../../../contexts'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { Meme } from '../../../database'; import { Meme } from '../../../database';
import { RootState } from '../../../state'; import { RootState } from '../../../state';
import { VIEW } from '../../../types'; import { VIEW } from '../../../types';
@@ -17,35 +17,32 @@ import MemesMasonryItem from './memesMasonryItem';
import MemesGridItem from './memesGridItem'; import MemesGridItem from './memesGridItem';
import MemesListItem from './memesListItem'; import MemesListItem from './memesListItem';
const memesMasonryListStyles = StyleSheet.create({ const sharedMemesListStyles = StyleSheet.create({
flashList: { flashList: {
paddingBottom: 100, paddingBottom: 100,
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
paddingHorizontal: 0.1,
}, },
helperText: { helperText: {
marginVertical: 15, marginVertical: 15,
}, },
}); });
const memesMasonryListStyles = StyleSheet.create({
flashList: {
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
paddingHorizontal: 0.1,
},
});
const memesGridListStyles = StyleSheet.create({ const memesGridListStyles = StyleSheet.create({
flashList: { flashList: {
paddingBottom: 100,
paddingHorizontal: 2.5, paddingHorizontal: 2.5,
}, },
helperText: {
marginVertical: 12.5,
},
}); });
const memesListListStyles = StyleSheet.create({ const memesListListStyles = StyleSheet.create({
flashList: { flashList: {
paddingBottom: 100,
paddingHorizontal: 5, paddingHorizontal: 5,
}, },
helperText: {
marginVertical: 15,
},
}); });
const MemesList = ({ const MemesList = ({
@@ -61,7 +58,7 @@ const MemesList = ({
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void; handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
focusMeme: (index: number) => void; focusMeme: (index: number) => void;
}) => { }) => {
const { dimensions, orientation } = useDimensions(); const { height, width } = useSafeAreaFrame();
const view = useSelector((state: RootState) => state.memes.view); const view = useSelector((state: RootState) => state.memes.view);
const masonryColumns = useSelector( const masonryColumns = useSelector(
(state: RootState) => state.settings.masonryColumns, (state: RootState) => state.settings.masonryColumns,
@@ -70,10 +67,6 @@ const MemesList = ({
(state: RootState) => state.settings.gridColumns, (state: RootState) => state.settings.gridColumns,
); );
const extraFlashListPadding =
flashListPadding +
dimensions.height * (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04);
return ( return (
<> <>
{view === VIEW.MASONRY && ( {view === VIEW.MASONRY && (
@@ -82,8 +75,8 @@ const MemesList = ({
data={memes} data={memes}
estimatedItemSize={getFlashListItemHeight(masonryColumns)} estimatedItemSize={getFlashListItemHeight(masonryColumns)}
estimatedListSize={{ estimatedListSize={{
height: dimensions.height, height,
width: dimensions.width * 0.92, width: width * 0.92,
}} }}
numColumns={masonryColumns} numColumns={masonryColumns}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
@@ -91,17 +84,19 @@ const MemesList = ({
<MemesMasonryItem meme={meme} index={index} focusMeme={focusMeme} /> <MemesMasonryItem meme={meme} index={index} focusMeme={focusMeme} />
)} )}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: extraFlashListPadding, paddingTop: flashListPadding,
...sharedMemesListStyles.flashList,
...memesMasonryListStyles.flashList, ...memesMasonryListStyles.flashList,
}} }}
ListEmptyComponent={() => ( ListEmptyComponent={() => (
<HelperText <HelperText
type={'info'} type={'info'}
style={[memesMasonryListStyles.helperText, styles.centerText]}> style={[sharedMemesListStyles.helperText, styles.centerText]}>
No memes found No memes found
</HelperText> </HelperText>
)} )}
onScroll={handleScroll} onScroll={handleScroll}
fadingEdgeLength={100}
/> />
)} )}
{view === VIEW.GRID && ( {view === VIEW.GRID && (
@@ -110,8 +105,8 @@ const MemesList = ({
data={memes} data={memes}
estimatedItemSize={getFlashListItemHeight(gridColumns)} estimatedItemSize={getFlashListItemHeight(gridColumns)}
estimatedListSize={{ estimatedListSize={{
height: dimensions.height, height: height,
width: dimensions.width * 0.92, width: width * 0.92,
}} }}
numColumns={gridColumns} numColumns={gridColumns}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
@@ -119,17 +114,19 @@ const MemesList = ({
<MemesGridItem meme={meme} index={index} focusMeme={focusMeme} /> <MemesGridItem meme={meme} index={index} focusMeme={focusMeme} />
)} )}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: extraFlashListPadding + 2.5, paddingTop: flashListPadding,
...sharedMemesListStyles.flashList,
...memesGridListStyles.flashList, ...memesGridListStyles.flashList,
}} }}
ListEmptyComponent={() => ( ListEmptyComponent={() => (
<HelperText <HelperText
type={'info'} type={'info'}
style={[memesGridListStyles.helperText, styles.centerText]}> style={[sharedMemesListStyles.helperText, styles.centerText]}>
No memes found No memes found
</HelperText> </HelperText>
)} )}
onScroll={handleScroll} onScroll={handleScroll}
fadingEdgeLength={100}
/> />
)} )}
{view === VIEW.LIST && ( {view === VIEW.LIST && (
@@ -138,8 +135,8 @@ const MemesList = ({
data={memes} data={memes}
estimatedItemSize={50} estimatedItemSize={50}
estimatedListSize={{ estimatedListSize={{
height: dimensions.height, height: height,
width: dimensions.width * 0.92, width: width * 0.92,
}} }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
renderItem={({ item: meme, index }) => ( renderItem={({ item: meme, index }) => (
@@ -147,17 +144,19 @@ const MemesList = ({
)} )}
ItemSeparatorComponent={() => <Divider />} ItemSeparatorComponent={() => <Divider />}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: extraFlashListPadding, paddingTop: flashListPadding,
...sharedMemesListStyles.flashList,
...memesListListStyles.flashList, ...memesListListStyles.flashList,
}} }}
ListEmptyComponent={() => ( ListEmptyComponent={() => (
<HelperText <HelperText
type={'info'} type={'info'}
style={[memesListListStyles.helperText, styles.centerText]}> style={[sharedMemesListStyles.helperText, styles.centerText]}>
No memes found No memes found
</HelperText> </HelperText>
)} )}
onScroll={handleScroll} onScroll={handleScroll}
fadingEdgeLength={100}
/> />
)} )}
</> </>

View File

@@ -1,15 +1,18 @@
import React, { useState } from 'react'; import React from 'react';
import { Image, StyleSheet, View } from 'react-native'; import { Image, StyleSheet, View } from 'react-native';
import { Text, TouchableRipple } from 'react-native-paper'; import { Text, TouchableRipple } from 'react-native-paper';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { useImageDimensions } from '@react-native-community/hooks';
import { Meme } from '../../../database'; import { Meme } from '../../../database';
import styles from '../../../styles'; import styles from '../../../styles';
import { useDimensions } from '../../../contexts';
const memesListItemStyles = StyleSheet.create({ const memesListItemStyles = StyleSheet.create({
view: { view: {
paddingVertical: 10, paddingVertical: 10,
}, },
image: { image: {
height: 75,
width: 75,
borderRadius: 5, borderRadius: 5,
}, },
detailsView: { detailsView: {
@@ -30,71 +33,54 @@ const MemesListItem = ({
index: number; index: number;
focusMeme: (index: number) => void; focusMeme: (index: number) => void;
}) => { }) => {
const { dimensions } = useDimensions(); const { width } = useSafeAreaFrame();
const [imageWidth, setImageWidth] = useState<number>(); const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, () => { if (loading || error || !dimensions) return <></>;
const paddedWidth = 75;
setImageWidth(paddedWidth);
setImageHeight(paddedWidth);
});
return ( return (
<> <TouchableRipple
{imageWidth && imageHeight && ( onPress={() => focusMeme(index)}
<TouchableRipple style={[memesListItemStyles.view, styles.flexRow]}>
onPress={() => focusMeme(index)} <>
style={[memesListItemStyles.view, styles.flexRow]}> <Image source={{ uri: meme.uri }} style={memesListItemStyles.image} />
<> <View
<View style={{ width: imageWidth, height: imageHeight }}> style={[
<Image memesListItemStyles.detailsView,
source={{ uri: meme.uri }} styles.flexColumn,
{
width: width * 0.92 - 75 - 10,
},
]}>
<Text variant="titleMedium" style={memesListItemStyles.text}>
{meme.title}
</Text>
<View style={styles.flexRow}>
<Text variant="labelSmall" style={memesListItemStyles.text}>
{meme.dateModified.toLocaleDateString()} {meme.size / 1000}
KB
</Text>
</View>
<View style={[styles.flexRow, styles.flexWrap]}>
{meme.tags.map(tag => (
<Text
variant="labelMedium"
key={tag.id.toHexString()}
style={[ style={[
{ width: imageWidth, height: imageHeight }, {
memesListItemStyles.image, color: tag.color,
},
memesListItemStyles.text,
]} ]}
/> numberOfLines={1}>
</View> #{tag.name}
<View
style={[
memesListItemStyles.detailsView,
styles.flexColumn,
{
width: dimensions.width * 0.92 - imageWidth - 10,
},
]}>
<Text variant="titleMedium" style={memesListItemStyles.text}>
{meme.title}
</Text> </Text>
<View style={styles.flexRow}> ))}
<Text variant="labelSmall" style={memesListItemStyles.text}> </View>
{meme.dateModified.toLocaleDateString()} {meme.size / 1000} </View>
KB </>
</Text> </TouchableRipple>
</View>
<View style={[styles.flexRow, styles.flexWrap]}>
{meme.tags.map(tag => (
<Text
variant="labelMedium"
key={tag.id.toHexString()}
style={[
{
color: tag.color,
},
memesListItemStyles.text,
]}
numberOfLines={1}>
#{tag.name}
</Text>
))}
</View>
</View>
</>
</TouchableRipple>
)}
</>
); );
}; };

View File

@@ -1,9 +1,10 @@
import React, { useState } from 'react'; import React from 'react';
import { Image, StyleSheet, TouchableHighlight } from 'react-native'; import { Image, StyleSheet, TouchableHighlight } from 'react-native';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { Meme } from '../../../database'; import { Meme } from '../../../database';
import { useDimensions } from '../../../contexts';
import { RootState } from '../../../state'; import { RootState } from '../../../state';
import { useImageDimensions } from '@react-native-community/hooks';
const memeMasonryItemStyles = StyleSheet.create({ const memeMasonryItemStyles = StyleSheet.create({
view: { view: {
@@ -24,36 +25,31 @@ const MemesMasonryItem = ({
index: number; index: number;
focusMeme: (index: number) => void; focusMeme: (index: number) => void;
}) => { }) => {
const { dimensions } = useDimensions(); const { width } = useSafeAreaFrame();
const masonryColumns = useSelector( const masonryColumns = useSelector(
(state: RootState) => state.settings.masonryColumns, (state: RootState) => state.settings.masonryColumns,
); );
const [imageWidth, setImageWidth] = useState<number>(); const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, (width, height) => { if (loading || error || !dimensions) return <></>;
const paddedWidth = (dimensions.width * 0.92) / masonryColumns - 5;
setImageWidth(paddedWidth);
setImageHeight((paddedWidth / width) * height);
});
return ( return (
<> <TouchableHighlight
{imageWidth && imageHeight && ( onPress={() => focusMeme(index)}
<TouchableHighlight style={memeMasonryItemStyles.view}>
onPress={() => focusMeme(index)} <Image
style={memeMasonryItemStyles.view}> source={{ uri: meme.uri }}
<Image style={[
source={{ uri: meme.uri }} memeMasonryItemStyles.image,
style={[ {
memeMasonryItemStyles.image, width: (width * 0.92) / masonryColumns - 5,
{ width: imageWidth, height: imageHeight }, height:
]} ((width * 0.92) / masonryColumns - 5) / dimensions.aspectRatio,
/> },
</TouchableHighlight> ]}
)} />
</> </TouchableHighlight>
); );
}; };

View File

@@ -14,11 +14,13 @@ const tagChipStyles = StyleSheet.create({
const TagChip = ({ const TagChip = ({
tag, tag,
active = true, active = true,
elevated = false,
onPress, onPress,
...props ...props
}: { }: {
tag: Tag; tag: Tag;
active?: boolean; active?: boolean;
elevated?: boolean;
onPress?: () => void; onPress?: () => void;
} & Omit<ComponentProps<typeof Chip>, 'children'>) => { } & Omit<ComponentProps<typeof Chip>, 'children'>) => {
const theme = useTheme(); const theme = useTheme();
@@ -45,6 +47,7 @@ const TagChip = ({
); );
}} }}
compact compact
elevated={elevated}
theme={chipTheme} theme={chipTheme}
mode={active ? 'flat' : 'outlined'} mode={active ? 'flat' : 'outlined'}
style={[tagChipStyles.chip, props.style]} style={[tagChipStyles.chip, props.style]}

View File

@@ -3,10 +3,12 @@ import { StyleSheet, View } from 'react-native';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { Chip, useTheme } from 'react-native-paper'; import { Chip, useTheme } from 'react-native-paper';
import styles from '../../styles'; import styles from '../../styles';
import { useDimensions } from '../../contexts';
import { getContrastColor } from '../../utilities'; import { getContrastColor } from '../../utilities';
const tagPreviewStyles = StyleSheet.create({ const tagPreviewStyles = StyleSheet.create({
view: {
margin: '10%',
},
chip: { chip: {
padding: 5, padding: 5,
}, },
@@ -17,7 +19,6 @@ const tagPreviewStyles = StyleSheet.create({
const TagPreview = ({ name, color }: { name: string; color: string }) => { const TagPreview = ({ name, color }: { name: string; color: string }) => {
const theme = useTheme(); const theme = useTheme();
const { responsive } = useDimensions();
const chipTheme = useMemo(() => { const chipTheme = useMemo(() => {
return { return {
@@ -32,14 +33,7 @@ const TagPreview = ({ name, color }: { name: string; color: string }) => {
const contrastColor = getContrastColor(color); const contrastColor = getContrastColor(color);
return ( return (
<View <View style={[styles.justifyCenter, styles.flexRow, tagPreviewStyles.view]}>
style={[
styles.justifyCenter,
styles.flexRow,
{
margin: responsive.verticalScale(50),
},
]}>
<Chip <Chip
icon={() => { icon={() => {
return <FontAwesome5 name="tag" size={14} color={contrastColor} />; return <FontAwesome5 name="tag" size={14} color={contrastColor} />;

View File

@@ -1,97 +0,0 @@
import React, {
ReactNode,
createContext,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import { Dimensions, ScaledSize } from 'react-native';
const guidelineBaseWidth = 350;
const guidelineBaseHeight = 680;
enum ORIENTATION {
PORTRAIT = 'portrait',
LANDSCAPE = 'landscape',
}
interface ScaleFunctions {
horizontalScale: (size: number) => number;
verticalScale: (size: number) => number;
moderateHorizontalScale: (size: number, factor?: number) => number;
moderateVerticalScale: (size: number, factor?: number) => number;
}
interface DimensionsContext {
orientation: ORIENTATION;
dimensions: ScaledSize;
responsive: ScaleFunctions;
}
const createScaleFunctions = (dimensionsIn: ScaledSize) => {
const horizontalScale = (size: number) =>
(dimensionsIn.width / guidelineBaseWidth) * size;
const verticalScale = (size: number) =>
(dimensionsIn.height / guidelineBaseHeight) * size;
const moderateHorizontalScale = (size: number, factor = 0.5) =>
size + (horizontalScale(size) - size) * factor;
const moderateVerticalScale = (size: number, factor = 0.5) =>
size + (verticalScale(size) - size) * factor;
return {
horizontalScale,
verticalScale,
moderateHorizontalScale,
moderateVerticalScale,
};
};
const DimensionsContext = createContext<DimensionsContext | undefined>(
undefined,
);
const DimensionsProvider = ({ children }: { children: ReactNode }) => {
const [dimensions, setDimensions] = useState(Dimensions.get('window'));
const orientation = useMemo(() => {
return dimensions.width > dimensions.height
? ORIENTATION.LANDSCAPE
: ORIENTATION.PORTRAIT;
}, [dimensions]);
const responsiveScale = createScaleFunctions(dimensions);
useEffect(() => {
const onChange = ({ window }: { window: ScaledSize }) => {
setDimensions(window);
};
const subscription = Dimensions.addEventListener('change', onChange);
return () => {
subscription.remove();
};
}, []);
return (
<DimensionsContext.Provider
value={{
orientation,
dimensions,
responsive: responsiveScale,
}}>
{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 { ORIENTATION, DimensionsProvider, useDimensions };

View File

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

View File

@@ -1,7 +1,6 @@
import React, { useCallback, useRef, useState } from 'react'; import React, { useCallback, useRef, useState } from 'react';
import { Appbar, Button, useTheme } from 'react-native-paper'; import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { ORIENTATION, useDimensions } from '../contexts';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useRealm } from '@realm/react'; import { useRealm } from '@realm/react';
@@ -9,16 +8,17 @@ import { BSON } from 'realm';
import { AndroidScoped, FileSystem } from 'react-native-file-access'; import { AndroidScoped, FileSystem } from 'react-native-file-access';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { extension } from 'react-native-mime-types'; import { extension } from 'react-native-mime-types';
import { useDeviceOrientation } from '@react-native-community/hooks';
import {
DocumentPickerResponse,
pickSingle,
} from 'react-native-document-picker';
import styles from '../styles'; import styles from '../styles';
import { ROUTE, RootStackParamList } from '../types'; import { ROUTE, RootStackParamList } from '../types';
import { Meme, Tag } from '../database'; import { Meme, Tag } from '../database';
import { RootState } from '../state'; import { RootState } from '../state';
import { allowedMimeTypes, getMemeType, validateMemeTitle } from '../utilities'; import { allowedMimeTypes, getMemeType, validateMemeTitle } from '../utilities';
import { MemeEditor } from '../components'; import { MemeEditor } from '../components';
import {
DocumentPickerResponse,
pickSingle,
} from 'react-native-document-picker';
const addMemeStyles = StyleSheet.create({ const addMemeStyles = StyleSheet.create({
saveAndAddButton: { saveAndAddButton: {
@@ -36,7 +36,7 @@ const AddMeme = ({
}: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => { }: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => {
const { goBack } = useNavigation(); const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const realm = useRealm(); const realm = useRealm();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const storageUri = useSelector( const storageUri = useSelector(
@@ -104,7 +104,7 @@ const AddMeme = ({
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation === ORIENTATION.PORTRAIT orientation === 'portrait'
? styles.paddingVertical ? styles.paddingVertical
: styles.smallPaddingVertical, : styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,

View File

@@ -3,13 +3,13 @@ import { ScrollView, StyleSheet, View } from 'react-native';
import { Appbar, Button, useTheme } from 'react-native-paper'; import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { useRealm } from '@realm/react'; import { useRealm } from '@realm/react';
import { useDeviceOrientation } from '@react-native-community/hooks';
import styles from '../styles'; import styles from '../styles';
import { import {
generateRandomColor, generateRandomColor,
validateColor, validateColor,
validateTagName, validateTagName,
} from '../utilities'; } from '../utilities';
import { ORIENTATION, useDimensions } from '../contexts';
import { Tag } from '../database'; import { Tag } from '../database';
import { TagEditor } from '../components'; import { TagEditor } from '../components';
@@ -27,7 +27,7 @@ const addTagStyles = StyleSheet.create({
const AddTag = () => { const AddTag = () => {
const { goBack } = useNavigation(); const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const realm = useRealm(); const realm = useRealm();
const [tagName, setTagName] = useState(validateTagName('newTag')); const [tagName, setTagName] = useState(validateTagName('newTag'));
@@ -57,7 +57,7 @@ const AddTag = () => {
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation === ORIENTATION.PORTRAIT orientation === 'portrait'
? styles.paddingVertical ? styles.paddingVertical
: styles.smallPaddingVertical, : styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,

View File

@@ -4,8 +4,8 @@ import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native'; import { useNavigation } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useObject, useRealm } from '@realm/react'; import { useObject, useRealm } from '@realm/react';
import { useDeviceOrientation } from '@react-native-community/hooks';
import { BSON } from 'realm'; import { BSON } from 'realm';
import { ORIENTATION, useDimensions } from '../contexts';
import styles from '../styles'; import styles from '../styles';
import { RootStackParamList, ROUTE } from '../types'; import { RootStackParamList, ROUTE } from '../types';
import { Tag, Meme } from '../database'; import { Tag, Meme } from '../database';
@@ -17,7 +17,7 @@ const EditMeme = ({
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_MEME>) => { }: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_MEME>) => {
const { goBack } = useNavigation(); const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const realm = useRealm(); const realm = useRealm();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -80,7 +80,7 @@ const EditMeme = ({
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation === ORIENTATION.PORTRAIT orientation === 'portrait'
? styles.paddingVertical ? styles.paddingVertical
: styles.smallPaddingVertical, : styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,

View File

@@ -5,9 +5,9 @@ import { useNavigation } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BSON } from 'realm'; import { BSON } from 'realm';
import { useObject, useRealm } from '@realm/react'; import { useObject, useRealm } from '@realm/react';
import { useDeviceOrientation } from '@react-native-community/hooks';
import { TagEditor } from '../components'; import { TagEditor } from '../components';
import styles from '../styles'; import styles from '../styles';
import { ORIENTATION, useDimensions } from '../contexts';
import { ROUTE, RootStackParamList } from '../types'; import { ROUTE, RootStackParamList } from '../types';
import { Tag } from '../database'; import { Tag } from '../database';
import { deleteTag, validateColor, validateTagName } from '../utilities'; import { deleteTag, validateColor, validateTagName } from '../utilities';
@@ -17,7 +17,7 @@ const EditTag = ({
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_TAG>) => { }: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_TAG>) => {
const { goBack } = useNavigation(); const { goBack } = useNavigation();
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const realm = useRealm(); const realm = useRealm();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -52,7 +52,7 @@ const EditTag = ({
</Appbar.Header> </Appbar.Header>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation === ORIENTATION.PORTRAIT orientation === 'portrait'
? styles.paddingVertical ? styles.paddingVertical
: styles.smallPaddingVertical, : styles.smallPaddingVertical,
styles.paddingHorizontal, styles.paddingHorizontal,

View File

@@ -1,12 +1,12 @@
import React, { useRef, useState } from 'react'; import React, { useRef, useState } from 'react';
import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { StyleSheet, View } from 'react-native'; import { StyleSheet } from 'react-native';
import { useQuery, useRealm } from '@realm/react'; import { useQuery, useRealm } from '@realm/react';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { Appbar, Portal, Snackbar } from 'react-native-paper'; import { Appbar, Portal, Snackbar } from 'react-native-paper';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { RootStackParamList, ROUTE } from '../types'; import { RootStackParamList, ROUTE } from '../types';
import { Meme } from '../database'; import { Meme } from '../database';
import { useDimensions } from '../contexts';
import { MemeViewItem } from '../components'; import { MemeViewItem } from '../components';
import { import {
copyMeme, copyMeme,
@@ -20,22 +20,32 @@ import { NavigationProp, useNavigation } from '@react-navigation/native';
import styles from '../styles'; import styles from '../styles';
const memeViewStyles = StyleSheet.create({ const memeViewStyles = StyleSheet.create({
// eslint-disable-next-line react-native/no-color-literals
header: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
zIndex: 1,
backgroundColor: 'transparent',
},
// eslint-disable-next-line react-native/no-color-literals
footer: { footer: {
position: 'absolute', position: 'absolute',
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 0, right: 0,
height: 80, backgroundColor: 'transparent',
}, },
snackbar: { snackbar: {
marginBottom: 90, marginBottom: 64,
}, },
}); });
const MemeView = ({ const MemeView = ({
route, route,
}: NativeStackScreenProps<RootStackParamList, ROUTE.MEME_VIEW>) => { }: NativeStackScreenProps<RootStackParamList, ROUTE.MEME_VIEW>) => {
const { orientation, dimensions } = useDimensions(); const { height, width } = useSafeAreaFrame();
const navigation = useNavigation<NavigationProp<RootStackParamList>>(); const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const realm = useRealm(); const realm = useRealm();
@@ -55,35 +65,30 @@ const MemeView = ({
return ( return (
<> <>
<Appbar.Header> <Appbar.Header style={memeViewStyles.header}>
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title={memes[index].title} /> <Appbar.Content title={memes[index].title} />
</Appbar.Header> </Appbar.Header>
<View> <FlashList
<FlashList ref={flashListRef}
ref={flashListRef} key={height}
key={orientation} data={memes}
data={memes} initialScrollIndex={index}
initialScrollIndex={index} onScroll={event => {
onScroll={event => { const newIndex = Math.round(
const newIndex = Math.round( event.nativeEvent.contentOffset.x /
event.nativeEvent.contentOffset.x / event.nativeEvent.layoutMeasurement.width,
event.nativeEvent.layoutMeasurement.width, );
); if (newIndex !== index) setIndex(newIndex);
if (newIndex !== index) setIndex(newIndex); }}
}} estimatedItemSize={width}
estimatedItemSize={dimensions.width} estimatedListSize={{ height, width }}
pagingEnabled pagingEnabled
horizontal horizontal
showsHorizontalScrollIndicator={false} showsHorizontalScrollIndicator={false}
estimatedListSize={{ renderItem={({ item: meme }) => <MemeViewItem meme={meme} />}
height: dimensions.height - 160, />
width: dimensions.width, <Appbar style={[styles.flexRowSpaceEvenly, memeViewStyles.footer]}>
}}
renderItem={({ item: meme }) => <MemeViewItem meme={meme} />}
/>
</View>
<Appbar style={[memeViewStyles.footer, styles.flexRowSpaceEvenly]}>
<Appbar.Action <Appbar.Action
icon={memes[index].isFavorite ? 'heart' : 'heart-outline'} icon={memes[index].isFavorite ? 'heart' : 'heart-outline'}
onPress={() => favoriteMeme(realm, memes[index])} onPress={() => favoriteMeme(realm, memes[index])}

View File

@@ -4,6 +4,7 @@ import {
NativeScrollEvent, NativeScrollEvent,
NativeSyntheticEvent, NativeSyntheticEvent,
View, View,
useWindowDimensions,
} from 'react-native'; } from 'react-native';
import { useQuery } from '@realm/react'; import { useQuery } from '@realm/react';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
@@ -20,11 +21,14 @@ import { ROUTE, SORT_DIRECTION, memesSortQuery } from '../types';
import { RootState, setNavVisible } from '../state'; import { RootState, setNavVisible } from '../state';
import { Meme } from '../database'; import { Meme } from '../database';
import { HideableHeader, MemesHeader, MemesList } from '../components'; import { HideableHeader, MemesHeader, MemesList } from '../components';
import { useDeviceOrientation } from '@react-native-community/hooks';
const Memes = () => { const Memes = () => {
const { colors } = useTheme(); const { colors } = useTheme();
const { navigate } = const { navigate } =
useNavigation<NativeStackNavigationProp<ParamListBase>>(); useNavigation<NativeStackNavigationProp<ParamListBase>>();
const { height } = useWindowDimensions();
const orientation = useDeviceOrientation();
const sort = useSelector((state: RootState) => state.memes.sort); const sort = useSelector((state: RootState) => state.memes.sort);
const sortDirection = useSelector( const sortDirection = useSelector(
(state: RootState) => state.memes.sortDirection, (state: RootState) => state.memes.sortDirection,
@@ -135,7 +139,10 @@ const Memes = () => {
search={search} search={search}
setSearch={setSearch} setSearch={setSearch}
onLayout={event => { onLayout={event => {
setFlashListPadding(event.nativeEvent.layout.height); setFlashListPadding(
event.nativeEvent.layout.height +
height * (orientation === 'portrait' ? 0.02 : 0.04),
);
}} }}
/> />
</HideableHeader> </HideableHeader>

View File

@@ -13,6 +13,7 @@ import {
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 type {} from 'redux-thunk/extend-redux'; import type {} from 'redux-thunk/extend-redux';
import { useDeviceOrientation } from '@react-native-community/hooks';
import styles from '../styles'; import styles from '../styles';
import { import {
RootState, RootState,
@@ -21,7 +22,6 @@ import {
setNoMedia, setNoMedia,
setStorageUri, setStorageUri,
} from '../state'; } from '../state';
import { ORIENTATION, useDimensions } from '../contexts';
const settingsStyles = StyleSheet.create({ const settingsStyles = StyleSheet.create({
snackbar: { snackbar: {
@@ -34,7 +34,7 @@ const settingsStyles = StyleSheet.create({
const Settings = () => { const Settings = () => {
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const noMedia = useSelector((state: RootState) => state.settings.noMedia); const noMedia = useSelector((state: RootState) => state.settings.noMedia);
const masonryColumns = useSelector( const masonryColumns = useSelector(
(state: RootState) => state.settings.masonryColumns, (state: RootState) => state.settings.masonryColumns,
@@ -60,85 +60,80 @@ const Settings = () => {
<> <>
<ScrollView <ScrollView
contentContainerStyle={[ contentContainerStyle={[
orientation === ORIENTATION.PORTRAIT orientation === 'portrait'
? styles.paddingTop ? styles.paddingTop
: styles.smallPaddingTop, : styles.smallPaddingTop,
styles.paddingHorizontal, styles.paddingHorizontal,
{ backgroundColor: colors.background }, { backgroundColor: colors.background },
]}> ]}>
<View> <List.Section>
<List.Section> <List.Subheader>Views</List.Subheader>
<List.Subheader>Views</List.Subheader> <Text
<Text style={[
style={[ settingsStyles.marginBottom,
settingsStyles.marginBottom, styles.smallPaddingHorizontal,
styles.smallPaddingHorizontal, ]}>
]}> Masonry Columns
Masonry Columns </Text>
</Text> <SegmentedButtons
<SegmentedButtons value={masonryColumns.toString()}
value={masonryColumns.toString()} onValueChange={value => {
void dispatch(
setMasonryColumns(Number.parseInt(value) as 1 | 2 | 3 | 4),
);
}}
buttons={[
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '3', value: '3' },
{ label: '4', value: '4' },
]}
style={settingsStyles.marginBottom}
/>
<Text
style={[
settingsStyles.marginBottom,
styles.smallPaddingHorizontal,
]}>
Grid Columns
</Text>
<SegmentedButtons
value={gridColumns.toString()}
onValueChange={value => {
void dispatch(
setGridColumns(Number.parseInt(value) as 1 | 2 | 3 | 4),
);
}}
buttons={[
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '3', value: '3' },
{ label: '4', value: '4' },
]}
/>
</List.Section>
<List.Section>
<List.Subheader>Media Storage</List.Subheader>
<Button
mode="elevated"
style={settingsStyles.marginBottom}
onPress={async () => {
const { uri } = await openDocumentTree(true);
void dispatch(setStorageUri(uri));
}}>
Change External Storage Path
</Button>
<View
style={[styles.flexRowSpaceBetween, styles.smallPaddingHorizontal]}>
<Text>Hide media from gallery</Text>
<Switch
value={noMedia}
onValueChange={value => { onValueChange={value => {
void dispatch( void dispatch(setNoMedia(value));
setMasonryColumns(Number.parseInt(value) as 1 | 2 | 3 | 4),
);
}} }}
buttons={[
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '3', value: '3' },
{ label: '4', value: '4' },
]}
style={settingsStyles.marginBottom}
/> />
<Text </View>
style={[ </List.Section>
settingsStyles.marginBottom,
styles.smallPaddingHorizontal,
]}>
Grid Columns
</Text>
<SegmentedButtons
value={gridColumns.toString()}
onValueChange={value => {
void dispatch(
setGridColumns(Number.parseInt(value) as 1 | 2 | 3 | 4),
);
}}
buttons={[
{ label: '1', value: '1' },
{ label: '2', value: '2' },
{ label: '3', value: '3' },
{ label: '4', value: '4' },
]}
/>
</List.Section>
<List.Section>
<List.Subheader>Media Storage</List.Subheader>
<Button
mode="elevated"
style={settingsStyles.marginBottom}
onPress={async () => {
const { uri } = await openDocumentTree(true);
void dispatch(setStorageUri(uri));
}}>
Change External Storage Path
</Button>
<View
style={[
styles.flexRowSpaceBetween,
styles.smallPaddingHorizontal,
]}>
<Text>Hide media from gallery</Text>
<Switch
value={noMedia}
onValueChange={value => {
void dispatch(setNoMedia(value));
}}
/>
</View>
</List.Section>
</View>
<List.Section> <List.Section>
<List.Subheader>Database</List.Subheader> <List.Subheader>Database</List.Subheader>
<Button <Button

View File

@@ -11,12 +11,13 @@ import { useQuery } from '@realm/react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { useDeviceOrientation } from '@react-native-community/hooks';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { HideableHeader, TagRow, TagsHeader } from '../components'; import { HideableHeader, TagRow, TagsHeader } from '../components';
import { Tag } from '../database'; import { Tag } from '../database';
import styles from '../styles'; import styles from '../styles';
import { RootState, setNavVisible } from '../state'; import { RootState, setNavVisible } from '../state';
import { SORT_DIRECTION, tagSortQuery } from '../types'; import { SORT_DIRECTION, tagSortQuery } from '../types';
import { ORIENTATION, useDimensions } from '../contexts';
const tagsStyles = StyleSheet.create({ const tagsStyles = StyleSheet.create({
flashList: { flashList: {
@@ -29,7 +30,8 @@ const tagsStyles = StyleSheet.create({
const Tags = () => { const Tags = () => {
const { colors } = useTheme(); const { colors } = useTheme();
const { dimensions, orientation } = useDimensions(); const { height, width } = useSafeAreaFrame();
const orientation = useDeviceOrientation();
const sort = useSelector((state: RootState) => state.tags.sort); const sort = useSelector((state: RootState) => state.tags.sort);
const sortDirection = useSelector( const sortDirection = useSelector(
(state: RootState) => state.tags.sortDirection, (state: RootState) => state.tags.sortDirection,
@@ -122,16 +124,15 @@ const Tags = () => {
data={tags} data={tags}
estimatedItemSize={50} estimatedItemSize={50}
estimatedListSize={{ estimatedListSize={{
height: dimensions.height, height,
width: dimensions.width * 0.92, width: width * 0.92,
}} }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
renderItem={({ item: tag }) => <TagRow tag={tag} />} renderItem={({ item: tag }) => <TagRow tag={tag} />}
contentContainerStyle={{ contentContainerStyle={{
paddingTop: paddingTop:
flashListPadding + flashListPadding +
dimensions.height * height * (orientation === 'portrait' ? 0.02 : 0.04),
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...tagsStyles.flashList, ...tagsStyles.flashList,
}} }}
ItemSeparatorComponent={() => <Divider />} ItemSeparatorComponent={() => <Divider />}
@@ -143,6 +144,7 @@ const Tags = () => {
</HelperText> </HelperText>
)} )}
onScroll={handleScroll} onScroll={handleScroll}
fadingEdgeLength={100}
/> />
</View> </View>
); );

View File

@@ -3,10 +3,10 @@ import { StyleSheet, View } from 'react-native';
import { Button, Text, useTheme } from 'react-native-paper'; import { Button, Text, useTheme } 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 { useDeviceOrientation } from '@react-native-community/hooks';
import styles from '../styles'; import styles from '../styles';
import { noOp } from '../utilities'; import { noOp } from '../utilities';
import { setStorageUri } from '../state'; import { setStorageUri } from '../state';
import { ORIENTATION, useDimensions } from '../contexts';
const welcomeStyles = StyleSheet.create({ const welcomeStyles = StyleSheet.create({
text: { text: {
@@ -19,7 +19,7 @@ const welcomeStyles = StyleSheet.create({
const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => { const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
const { colors } = useTheme(); const { colors } = useTheme();
const { orientation } = useDimensions(); const orientation = useDeviceOrientation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const selectStorageLocation = async () => { const selectStorageLocation = async () => {
@@ -32,11 +32,9 @@ const Welcome = ({ onWelcomeComplete }: { onWelcomeComplete: () => void }) => {
return ( return (
<View <View
style={[ style={[
orientation === ORIENTATION.PORTRAIT orientation === 'portrait' ? styles.paddingTop : styles.smallPaddingTop,
? styles.paddingTop
: styles.smallPaddingTop,
styles.paddingHorizontal, styles.paddingHorizontal,
styles.centered, styles.center,
styles.flex, styles.flex,
styles.fullSize, styles.fullSize,
{ backgroundColor: colors.background }, { backgroundColor: colors.background },

View File

@@ -25,7 +25,7 @@ const styles = StyleSheet.create({
paddingTop: { paddingTop: {
paddingTop: '4%', paddingTop: '4%',
}, },
centered: { center: {
justifyContent: 'center', justifyContent: 'center',
alignItems: 'center', alignItems: 'center',
}, },