Add memes views & searching

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-23 20:20:11 +03:00
parent e44ee7de34
commit 04661ca356
28 changed files with 737 additions and 247 deletions

View File

@@ -0,0 +1,45 @@
import React, { useState } from 'react';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { Image, TouchableHighlight, View } from 'react-native';
import { useSelector } from 'react-redux';
import { Meme } from '../../../database';
import { ROUTE, RootStackParamList } from '../../../types';
import { useDimensions } from '../../../contexts';
import { RootState } from '../../../state';
const MemesGridItem = ({ meme }: { meme: Meme }) => {
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
const { dimensions } = useDimensions();
const gridColumns = useSelector(
(state: RootState) => state.settings.gridColumns,
);
const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, () => {
const paddedWidth = (dimensions.width * 0.92 - 5) / gridColumns;
setImageWidth(paddedWidth);
setImageHeight(paddedWidth);
});
return (
<>
{imageWidth && imageHeight && (
<View>
<TouchableHighlight
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}>
<Image
source={{ uri: meme.uri }}
style={[{ width: imageWidth, height: imageHeight }]}
/>
</TouchableHighlight>
</View>
)}
</>
);
};
export default MemesGridItem;

View File

@@ -0,0 +1,74 @@
import React, { RefObject } from 'react';
import { Meme } from '../../../database';
import { FlashList } from '@shopify/flash-list';
import { HelperText } from 'react-native-paper';
import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
} from 'react-native';
import { useSelector } from 'react-redux';
import styles from '../../../styles';
import { RootState } from '../../../state';
import { ORIENTATION, useDimensions } from '../../../contexts';
import { getFlashListItemHeight } from '../../../utilities';
import MemesGridItem from './memesGridItem';
const gridViewStyles = StyleSheet.create({
helperText: {
marginVertical: 10,
},
flashList: {
paddingBottom: 100,
paddingHorizontal: 2.5,
},
});
const MemesGridView = ({
memes,
flashListRef,
flashListPadding,
handleScroll,
}: {
memes: Realm.Results<Meme & Realm.Object<Meme>>;
flashListRef: RefObject<FlashList<Meme>>;
flashListPadding: number;
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}) => {
const { orientation, dimensions } = useDimensions();
const gridColumns = useSelector(
(state: RootState) => state.settings.gridColumns,
);
return (
<FlashList
ref={flashListRef}
data={memes}
estimatedItemSize={getFlashListItemHeight(gridColumns)}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
numColumns={gridColumns}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme }) => <MemesGridItem meme={meme} />}
contentContainerStyle={{
paddingTop:
flashListPadding +
dimensions.height *
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...gridViewStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[gridViewStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
);
};
export default MemesGridView;

View File

@@ -1,3 +1,6 @@
export { default as MemesGridView } from './gridView/memesGridView';
export { default as MemesListView } from './listView/memesListView';
export { default as MemesMasonryView } from './masonryView/memesMasonryView';
export { default as MemeEditor } from './memeEditor';
export { default as MemesHeader } from './memesHeader';
export { default as MemeTagSearchModal } from './memeTagSearchModal';

View File

@@ -0,0 +1,98 @@
import React, { useState } from 'react';
import { Image, StyleSheet, View } from 'react-native';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { Text, TouchableRipple } from 'react-native-paper';
import { Meme } from '../../../database';
import { ROUTE, RootStackParamList } from '../../../types';
import styles from '../../../styles';
import { useDimensions } from '../../../contexts';
const memesListItemStyles = StyleSheet.create({
view: {
paddingVertical: 10,
},
image: {
borderRadius: 5,
},
detailsView: {
marginLeft: 10,
},
text: {
marginRight: 5,
marginBottom: 5,
},
});
const MemesListItem = ({ meme }: { meme: Meme }) => {
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
const { dimensions } = useDimensions();
const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, () => {
const paddedWidth = 75;
setImageWidth(paddedWidth);
setImageHeight(paddedWidth);
});
return (
<>
{imageWidth && imageHeight && (
<TouchableRipple
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}
style={[memesListItemStyles.view, styles.flexRow]}>
<>
<View style={{ width: imageWidth, height: imageHeight }}>
<Image
source={{ uri: meme.uri }}
style={[
{ width: imageWidth, height: imageHeight },
memesListItemStyles.image,
]}
/>
</View>
<View
style={[
memesListItemStyles.detailsView,
styles.flexColumn,
{
width: dimensions.width * 0.92 - imageWidth - 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={[
{
color: tag.color,
},
memesListItemStyles.text,
]}
numberOfLines={1}>
#{tag.name}
</Text>
))}
</View>
</View>
</>
</TouchableRipple>
)}
</>
);
};
export default MemesListItem;

View File

@@ -0,0 +1,68 @@
import React, { RefObject } from 'react';
import { Meme } from '../../../database';
import { FlashList } from '@shopify/flash-list';
import { Divider, HelperText } from 'react-native-paper';
import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
} from 'react-native';
import styles from '../../../styles';
import { ORIENTATION, useDimensions } from '../../../contexts';
import MemesListItem from './memesListItem';
const gridViewStyles = StyleSheet.create({
helperText: {
marginVertical: 10,
},
flashList: {
paddingBottom: 100,
paddingHorizontal: 5,
},
});
const MemesListView = ({
memes,
flashListRef,
flashListPadding,
handleScroll,
}: {
memes: Realm.Results<Meme & Realm.Object<Meme>>;
flashListRef: RefObject<FlashList<Meme>>;
flashListPadding: number;
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}) => {
const { orientation, dimensions } = useDimensions();
return (
<FlashList
ref={flashListRef}
data={memes}
estimatedItemSize={50}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme }) => <MemesListItem meme={meme} />}
ItemSeparatorComponent={() => <Divider />}
contentContainerStyle={{
paddingTop:
flashListPadding +
dimensions.height *
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...gridViewStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[gridViewStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
);
};
export default MemesListView;

View File

@@ -0,0 +1,57 @@
import React, { useState } from 'react';
import { Image, StyleSheet, TouchableHighlight } from 'react-native';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { useSelector } from 'react-redux';
import { Meme } from '../../../database';
import { ROUTE, RootStackParamList } from '../../../types';
import { useDimensions } from '../../../contexts';
import { RootState } from '../../../state';
const memeMasonryItemStyles = StyleSheet.create({
view: {
margin: 2.5,
borderRadius: 5,
},
image: {
borderRadius: 5,
},
});
const MemesMasonryItem = ({ meme }: { meme: Meme }) => {
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
const { dimensions } = useDimensions();
const masonryColumns = useSelector(
(state: RootState) => state.settings.masonryColumns,
);
const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, (width, height) => {
const paddedWidth = (dimensions.width * 0.92) / masonryColumns - 5;
setImageWidth(paddedWidth);
setImageHeight((paddedWidth / width) * height);
});
return (
<>
{imageWidth && imageHeight && (
<TouchableHighlight
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}
style={memeMasonryItemStyles.view}>
<Image
source={{ uri: meme.uri }}
style={[
memeMasonryItemStyles.image,
{ width: imageWidth, height: imageHeight },
]}
/>
</TouchableHighlight>
)}
</>
);
};
export default MemesMasonryItem;

View File

@@ -0,0 +1,75 @@
import React, { RefObject } from 'react';
import { Meme } from '../../../database';
import { FlashList, MasonryFlashList } from '@shopify/flash-list';
import { HelperText } from 'react-native-paper';
import {
NativeScrollEvent,
NativeSyntheticEvent,
StyleSheet,
} from 'react-native';
import { useSelector } from 'react-redux';
import styles from '../../../styles';
import { RootState } from '../../../state';
import { ORIENTATION, useDimensions } from '../../../contexts';
import { getFlashListItemHeight } from '../../../utilities';
import MemesMasonryItem from './memesMasonryItem';
const memeMasonryViewStyles = StyleSheet.create({
helperText: {
marginVertical: 10,
},
flashList: {
paddingBottom: 100,
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
paddingHorizontal: 0.1,
},
});
const MemesMasonryView = ({
memes,
flashListRef,
flashListPadding,
handleScroll,
}: {
memes: Realm.Results<Meme & Realm.Object<Meme>>;
flashListRef: RefObject<FlashList<Meme>>;
flashListPadding: number;
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
}) => {
const { orientation, dimensions } = useDimensions();
const masonryColumns = useSelector(
(state: RootState) => state.settings.masonryColumns,
);
return (
<MasonryFlashList
ref={flashListRef}
data={memes}
estimatedItemSize={getFlashListItemHeight(masonryColumns)}
estimatedListSize={{
height: dimensions.height,
width: dimensions.width * 0.92,
}}
numColumns={masonryColumns}
showsVerticalScrollIndicator={false}
renderItem={({ item: meme }) => <MemesMasonryItem meme={meme} />}
contentContainerStyle={{
paddingTop:
flashListPadding +
dimensions.height *
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
...memeMasonryViewStyles.flashList,
}}
ListEmptyComponent={() => (
<HelperText
type={'info'}
style={[memeMasonryViewStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
onScroll={handleScroll}
/>
);
};
export default MemesMasonryView;

View File

@@ -1,51 +0,0 @@
import React, { useState } from 'react';
import { useNavigation, NavigationProp } from '@react-navigation/native';
import { Meme } from '../../database';
import { ROUTE, RootStackParamList } from '../../types';
import { Card } from 'react-native-paper';
import { Image, StyleSheet } from 'react-native';
import { useDimensions } from '../../contexts';
const memeCardStyles = StyleSheet.create({
card: {
margin: 5,
},
});
const MemeCard = ({ meme }: { meme: Meme }) => {
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
const { dimensions } = useDimensions();
const [imageWidth, setImageWidth] = useState<number>();
const [imageHeight, setImageHeight] = useState<number>();
Image.getSize(meme.uri, (width, height) => {
const paddedWidth = (dimensions.width * 0.92) / 2 - 10;
setImageWidth(paddedWidth);
setImageHeight((paddedWidth / width) * height);
});
return (
<>
{imageWidth && imageHeight && (
<Card
onPress={() =>
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
}
style={memeCardStyles.card}>
<Card.Cover
source={{ uri: meme.uri }}
style={{ width: imageWidth, height: imageHeight }}
/>
<Card.Title
title={meme.title}
titleVariant="titleSmall"
titleNumberOfLines={3}
/>
</Card>
)}
</>
);
};
export default MemeCard;

View File

@@ -5,11 +5,7 @@ import { useDimensions } from '../../contexts';
import LoadingView from '../loadingView';
import { MemeTagSelector } from '.';
import { Tag } from '../../database';
import {
StringValidationResult,
validateMemeDescription,
validateMemeTitle,
} from '../../utilities';
import { StringValidationResult, validateMemeTitle } from '../../utilities';
const memeEditorStyles = {
image: {
@@ -28,16 +24,12 @@ const MemeEditor = ({
imageUri,
memeTitle,
setMemeTitle,
memeDescription,
setMemeDescription,
memeTags,
setMemeTags,
}: {
imageUri: string;
memeTitle: StringValidationResult;
setMemeTitle: (name: StringValidationResult) => void;
memeDescription: StringValidationResult;
setMemeDescription: (description: StringValidationResult) => void;
memeTags: Map<string, Tag>;
setMemeTags: (tags: Map<string, Tag>) => void;
}) => {
@@ -48,8 +40,12 @@ const MemeEditor = ({
Image.getSize(imageUri, (width, height) => {
const paddedWidth = dimensions.width * 0.92;
const paddedHeight = Math.max(
Math.min((paddedWidth / width) * height, 500),
100,
);
setImageWidth(paddedWidth);
setImageHeight((paddedWidth / width) * height);
setImageHeight(paddedHeight);
});
if (!imageWidth || !imageHeight) return <LoadingView />;
@@ -76,24 +72,13 @@ const MemeEditor = ({
},
memeEditorStyles.image,
]}
resizeMode="contain"
/>
<MemeTagSelector
memeTags={memeTags}
setMemeTags={setMemeTags}
style={memeEditorStyles.memeTagSelector}
/>
<TextInput
mode="outlined"
label="Description"
multiline
numberOfLines={6}
value={memeDescription.raw}
style={memeEditorStyles.description}
onChangeText={description =>
setMemeDescription(validateMemeDescription(description))
}
error={!memeDescription.valid}
/>
</>
);
};