Add memes views & searching
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
45
src/components/memes/gridView/memesGridItem.tsx
Normal file
45
src/components/memes/gridView/memesGridItem.tsx
Normal 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;
|
74
src/components/memes/gridView/memesGridView.tsx
Normal file
74
src/components/memes/gridView/memesGridView.tsx
Normal 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;
|
@@ -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';
|
||||
|
98
src/components/memes/listView/memesListItem.tsx
Normal file
98
src/components/memes/listView/memesListItem.tsx
Normal 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;
|
68
src/components/memes/listView/memesListView.tsx
Normal file
68
src/components/memes/listView/memesListView.tsx
Normal 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;
|
57
src/components/memes/masonryView/memesMasonryItem.tsx
Normal file
57
src/components/memes/masonryView/memesMasonryItem.tsx
Normal 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;
|
75
src/components/memes/masonryView/memesMasonryView.tsx
Normal file
75
src/components/memes/masonryView/memesMasonryView.tsx
Normal 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;
|
@@ -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;
|
@@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user