Add memes & meme-editing views
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { Keyboard, StyleSheet } from 'react-native';
|
|||||||
import { FAB } from 'react-native-paper';
|
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 { pick } from 'react-native-document-picker';
|
import { pickSingle } from 'react-native-document-picker';
|
||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
import { ROUTE } from '../types';
|
import { ROUTE } from '../types';
|
||||||
import { allowedMimeTypes, noOp } from '../utilities';
|
import { allowedMimeTypes, noOp } from '../utilities';
|
||||||
@@ -65,9 +65,9 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
|
|||||||
onStateChange={({ open }) => setState(open)}
|
onStateChange={({ open }) => setState(open)}
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
if (!state) return;
|
if (!state) return;
|
||||||
const res = await pick({ type: allowedMimeTypes }).catch(noOp);
|
const file = await pickSingle({ type: allowedMimeTypes }).catch(noOp);
|
||||||
if (!res) return;
|
if (!file) return;
|
||||||
navigate(ROUTE.ADD_MEME, { uri: res });
|
navigate(ROUTE.ADD_MEME, { file });
|
||||||
}}
|
}}
|
||||||
style={
|
style={
|
||||||
orientation === ORIENTATION.PORTRAIT
|
orientation === ORIENTATION.PORTRAIT
|
||||||
|
51
src/components/memes/memeCard.tsx
Normal file
51
src/components/memes/memeCard.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
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;
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useState } 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 { useDimensions } from '../../contexts';
|
||||||
@@ -46,13 +46,11 @@ const MemeEditor = ({
|
|||||||
const [imageWidth, setImageWidth] = useState<number>();
|
const [imageWidth, setImageWidth] = useState<number>();
|
||||||
const [imageHeight, setImageHeight] = useState<number>();
|
const [imageHeight, setImageHeight] = useState<number>();
|
||||||
|
|
||||||
useEffect(() => {
|
Image.getSize(imageUri, (width, height) => {
|
||||||
Image.getSize(imageUri, (width, height) => {
|
const paddedWidth = dimensions.width * 0.92;
|
||||||
const paddedWidth = dimensions.width * 0.92;
|
setImageWidth(paddedWidth);
|
||||||
setImageWidth(paddedWidth);
|
setImageHeight((paddedWidth / width) * height);
|
||||||
setImageHeight((paddedWidth / width) * height);
|
});
|
||||||
});
|
|
||||||
}, [dimensions.width, imageUri]);
|
|
||||||
|
|
||||||
if (!imageWidth || !imageHeight) return <LoadingView />;
|
if (!imageWidth || !imageHeight) return <LoadingView />;
|
||||||
|
|
||||||
|
@@ -59,7 +59,7 @@ const MemeTagSearchModal = ({
|
|||||||
let collection = collectionIn;
|
let collection = collectionIn;
|
||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
collection = collection.filtered('name CONTAINS[c] $0', search);
|
collection = collection.filtered('name CONTAINS[c] $0', tagName.parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
collection = collection.sorted(
|
collection = collection.sorted(
|
||||||
@@ -88,7 +88,6 @@ const MemeTagSearchModal = ({
|
|||||||
if (!tag) return;
|
if (!tag) return;
|
||||||
memeTags.set(tag.id.toHexString(), tag);
|
memeTags.set(tag.id.toHexString(), tag);
|
||||||
setMemeTags(new Map(memeTags));
|
setMemeTags(new Map(memeTags));
|
||||||
setSearch(tag.name);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -22,8 +22,9 @@ const TagEditor = ({
|
|||||||
const lastValidTagColor = useRef(tagColor.parsed);
|
const lastValidTagColor = useRef(tagColor.parsed);
|
||||||
|
|
||||||
const handleTagColorChange = (color: string) => {
|
const handleTagColorChange = (color: string) => {
|
||||||
setTagColor(validateColor(color));
|
const result = validateColor(color);
|
||||||
if (tagColor.valid) lastValidTagColor.current = tagColor.parsed;
|
setTagColor(result);
|
||||||
|
if (result.valid) lastValidTagColor.current = result.parsed;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -21,13 +21,12 @@ const memeTypePlural = {
|
|||||||
class Meme extends Object<Meme> {
|
class Meme extends Object<Meme> {
|
||||||
id!: BSON.UUID;
|
id!: BSON.UUID;
|
||||||
type!: MEME_TYPE;
|
type!: MEME_TYPE;
|
||||||
uri!: string[];
|
uri!: string;
|
||||||
hash!: string[];
|
|
||||||
size!: number;
|
size!: number;
|
||||||
title!: string;
|
title!: string;
|
||||||
description?: string;
|
description!: string;
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
tags!: Tag[] | Set<Tag>;
|
tags!: Tag[] | Realm.Set<Tag>;
|
||||||
tagsLength!: number;
|
tagsLength!: number;
|
||||||
dateCreated!: Date;
|
dateCreated!: Date;
|
||||||
dateModified!: Date;
|
dateModified!: Date;
|
||||||
@@ -40,11 +39,10 @@ class Meme extends Object<Meme> {
|
|||||||
properties: {
|
properties: {
|
||||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||||
type: { type: 'string', indexed: true },
|
type: { type: 'string', indexed: true },
|
||||||
uri: 'string[]',
|
uri: 'string',
|
||||||
hash: 'string[]',
|
|
||||||
size: 'int',
|
size: 'int',
|
||||||
title: 'string',
|
title: 'string',
|
||||||
description: 'string?',
|
description: { type: 'string', default: '' },
|
||||||
isFavorite: { type: 'bool', indexed: true, default: false },
|
isFavorite: { type: 'bool', indexed: true, default: false },
|
||||||
tags: { type: 'set', objectType: 'Tag', default: [] },
|
tags: { type: 'set', objectType: 'Tag', default: [] },
|
||||||
tagsLength: { type: 'int', default: 0 },
|
tagsLength: { type: 'int', default: 0 },
|
||||||
|
@@ -7,7 +7,7 @@ class Tag extends Object<Tag> {
|
|||||||
id!: BSON.UUID;
|
id!: BSON.UUID;
|
||||||
name!: string;
|
name!: string;
|
||||||
color!: string;
|
color!: string;
|
||||||
memes!: Meme[] | Set<Meme>;
|
memes!: Meme[] | Realm.Set<Meme>;
|
||||||
memesLength!: number;
|
memesLength!: number;
|
||||||
dateCreated!: Date;
|
dateCreated!: Date;
|
||||||
dateModified!: Date;
|
dateModified!: Date;
|
||||||
|
@@ -23,7 +23,7 @@ import { MemeEditor } from '../components';
|
|||||||
const AddMeme = ({
|
const AddMeme = ({
|
||||||
route,
|
route,
|
||||||
}: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => {
|
}: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => {
|
||||||
const navigation = useNavigation();
|
const { goBack } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation } = useDimensions();
|
const { orientation } = useDimensions();
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
@@ -32,9 +32,7 @@ const AddMeme = ({
|
|||||||
(state: RootState) => state.settings.storageUri,
|
(state: RootState) => state.settings.storageUri,
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
const uri = route.params.uri[0].uri;
|
const { file } = route.params;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
||||||
const memeType = getMemeType(route.params.uri[0].type!);
|
|
||||||
|
|
||||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||||
const [memeDescription, setMemeDescription] = useState(
|
const [memeDescription, setMemeDescription] = useState(
|
||||||
@@ -49,31 +47,27 @@ const AddMeme = ({
|
|||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
|
|
||||||
const uuid = new BSON.UUID();
|
const uuid = new BSON.UUID();
|
||||||
const savedUri: string[] = [];
|
|
||||||
const hash: string[] = [];
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const fileExtension = extension(memeType!);
|
const mimeType = file.type!;
|
||||||
if (!fileExtension) navigation.goBack();
|
const memeType = getMemeType(mimeType);
|
||||||
|
|
||||||
savedUri.push(
|
const fileExtension = extension(mimeType);
|
||||||
AndroidScoped.appendPath(
|
if (!fileExtension) goBack();
|
||||||
storageUri,
|
|
||||||
`${uuid.toHexString()}.${fileExtension as string}`,
|
const uri = AndroidScoped.appendPath(
|
||||||
),
|
storageUri,
|
||||||
|
`${uuid.toHexString()}.${fileExtension as string}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileSystem.cp(uri, savedUri[0]);
|
await FileSystem.cp(file.uri, uri);
|
||||||
const { size } = await FileSystem.stat(savedUri[0]);
|
const { size } = await FileSystem.stat(uri);
|
||||||
hash.push(await FileSystem.hash(savedUri[0], 'MD5'));
|
|
||||||
|
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
const meme: Meme | undefined = realm.create<Meme>(Meme.schema.name, {
|
const meme: Meme | undefined = realm.create<Meme>(Meme.schema.name, {
|
||||||
id: uuid,
|
id: uuid,
|
||||||
type: memeType,
|
type: memeType,
|
||||||
uri: savedUri,
|
uri,
|
||||||
size,
|
size,
|
||||||
hash,
|
|
||||||
title: memeTitle.parsed,
|
title: memeTitle.parsed,
|
||||||
description: memeDescription.parsed,
|
description: memeDescription.parsed,
|
||||||
isFavorite: memeIsFavorite,
|
isFavorite: memeIsFavorite,
|
||||||
@@ -81,22 +75,21 @@ const AddMeme = ({
|
|||||||
tagsLength: memeTags.size,
|
tagsLength: memeTags.size,
|
||||||
});
|
});
|
||||||
|
|
||||||
meme.tags.forEach(tag => {
|
memeTags.forEach(tag => {
|
||||||
tag.dateModified = new Date();
|
tag.dateModified = new Date();
|
||||||
const memes = tag.memes as Set<Meme>;
|
const memes = tag.memes as Realm.Set<Meme>;
|
||||||
memes.add(meme);
|
memes.add(meme);
|
||||||
tag.memesLength = memes.size;
|
tag.memesLength = memes.size;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
setIsSaving(false);
|
goBack();
|
||||||
navigation.goBack();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
<Appbar.BackAction onPress={() => goBack()} />
|
||||||
<Appbar.Content title={'Add Meme'} />
|
<Appbar.Content title={'Add Meme'} />
|
||||||
<Appbar.Action
|
<Appbar.Action
|
||||||
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
|
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
|
||||||
@@ -115,7 +108,7 @@ const AddMeme = ({
|
|||||||
]}>
|
]}>
|
||||||
<View style={[styles.flex, styles.justifyStart]}>
|
<View style={[styles.flex, styles.justifyStart]}>
|
||||||
<MemeEditor
|
<MemeEditor
|
||||||
imageUri={uri}
|
imageUri={file.uri}
|
||||||
memeTitle={memeTitle}
|
memeTitle={memeTitle}
|
||||||
setMemeTitle={setMemeTitle}
|
setMemeTitle={setMemeTitle}
|
||||||
memeDescription={memeDescription}
|
memeDescription={memeDescription}
|
||||||
|
@@ -14,7 +14,7 @@ import { Tag } from '../database';
|
|||||||
import { TagEditor } from '../components';
|
import { TagEditor } from '../components';
|
||||||
|
|
||||||
const AddTag = () => {
|
const AddTag = () => {
|
||||||
const navigation = useNavigation();
|
const { goBack } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation } = useDimensions();
|
const { orientation } = useDimensions();
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
@@ -32,13 +32,13 @@ const AddTag = () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
navigation.goBack();
|
goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
<Appbar.BackAction onPress={() => goBack()} />
|
||||||
<Appbar.Content title={'Add Tag'} />
|
<Appbar.Content title={'Add Tag'} />
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
|
@@ -1,20 +1,112 @@
|
|||||||
import React from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ScrollView } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { Appbar, 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 { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
|
import { useObject, useRealm } from '@realm/react';
|
||||||
|
import { FileSystem } from 'react-native-file-access';
|
||||||
|
import { BSON } from 'realm';
|
||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
|
import { RootStackParamList, ROUTE } from '../types';
|
||||||
|
import { Tag, Meme } from '../database';
|
||||||
|
import { validateMemeTitle, validateMemeDescription } from '../utilities';
|
||||||
|
import { MemeEditor } from '../components';
|
||||||
|
|
||||||
const EditMeme = () => {
|
const EditMeme = ({
|
||||||
const navigation = useNavigation();
|
route,
|
||||||
|
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_MEME>) => {
|
||||||
|
const { goBack } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation } = useDimensions();
|
const { orientation } = useDimensions();
|
||||||
|
const realm = useRealm();
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const meme = useObject<Meme>(
|
||||||
|
Meme.schema.name,
|
||||||
|
BSON.UUID.createFromHexString(route.params.id),
|
||||||
|
)!;
|
||||||
|
|
||||||
|
const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title));
|
||||||
|
const [memeDescription, setMemeDescription] = useState(
|
||||||
|
validateMemeDescription(meme.description),
|
||||||
|
);
|
||||||
|
const [memeIsFavorite, setMemeIsFavorite] = useState(meme.isFavorite);
|
||||||
|
const [memeTags, setMemeTags] = useState(
|
||||||
|
new Map<string, Tag>(
|
||||||
|
(meme.tags as Realm.Set<Tag>).map(tag => [tag.id.toHexString(), tag]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
|
const handleSave = () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
|
||||||
|
realm.write(() => {
|
||||||
|
meme.tags.forEach(tag => {
|
||||||
|
if (!memeTags.has(tag.id.toHexString())) {
|
||||||
|
const memes = tag.memes as Realm.Set<Meme>;
|
||||||
|
memes.delete(meme);
|
||||||
|
tag.memesLength = memes.size;
|
||||||
|
tag.dateModified = new Date();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
memeTags.forEach(tag => {
|
||||||
|
if (!(meme.tags as Realm.Set<Tag>).has(tag)) {
|
||||||
|
const memes = tag.memes as Realm.Set<Meme>;
|
||||||
|
memes.add(meme);
|
||||||
|
tag.memesLength = memes.size;
|
||||||
|
tag.dateModified = new Date();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
meme.title = memeTitle.parsed;
|
||||||
|
meme.description = memeDescription.parsed;
|
||||||
|
meme.tags = [...memeTags.values()];
|
||||||
|
meme.tagsLength = memeTags.size;
|
||||||
|
meme.dateModified = new Date();
|
||||||
|
});
|
||||||
|
|
||||||
|
goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFavorite = () => {
|
||||||
|
realm.write(() => {
|
||||||
|
meme.isFavorite = !memeIsFavorite;
|
||||||
|
});
|
||||||
|
setMemeIsFavorite(!memeIsFavorite);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
await FileSystem.unlink(meme.uri);
|
||||||
|
|
||||||
|
realm.write(() => {
|
||||||
|
for (const tag of meme.tags) {
|
||||||
|
tag.dateModified = new Date();
|
||||||
|
const memes = tag.memes as Realm.Set<Meme>;
|
||||||
|
memes.delete(meme);
|
||||||
|
tag.memesLength = memes.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
realm.delete(meme);
|
||||||
|
});
|
||||||
|
|
||||||
|
goBack();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
<Appbar.BackAction onPress={() => goBack()} />
|
||||||
<Appbar.Content title={'Edit Meme'} />
|
<Appbar.Content title={'Edit Meme'} />
|
||||||
|
<Appbar.Action
|
||||||
|
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
|
||||||
|
onPress={handleFavorite}
|
||||||
|
/>
|
||||||
|
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
@@ -25,7 +117,29 @@ const EditMeme = () => {
|
|||||||
styles.flexGrow,
|
styles.flexGrow,
|
||||||
styles.flexColumnSpaceBetween,
|
styles.flexColumnSpaceBetween,
|
||||||
{ backgroundColor: colors.background },
|
{ backgroundColor: colors.background },
|
||||||
]}></ScrollView>
|
]}>
|
||||||
|
<View style={[styles.flex, styles.justifyStart]}>
|
||||||
|
<MemeEditor
|
||||||
|
imageUri={meme.uri}
|
||||||
|
memeTitle={memeTitle}
|
||||||
|
setMemeTitle={setMemeTitle}
|
||||||
|
memeDescription={memeDescription}
|
||||||
|
setMemeDescription={setMemeDescription}
|
||||||
|
memeTags={memeTags}
|
||||||
|
setMemeTags={setMemeTags}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.flex, styles.justifyEnd]}>
|
||||||
|
<Button
|
||||||
|
mode="contained"
|
||||||
|
icon="floppy"
|
||||||
|
onPress={handleSave}
|
||||||
|
disabled={!memeTitle.valid || !memeDescription.valid || isSaving}
|
||||||
|
loading={isSaving}>
|
||||||
|
Save
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
</ScrollView>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -4,7 +4,7 @@ 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 { BSON } from 'realm';
|
import { BSON } from 'realm';
|
||||||
import { useRealm } from '@realm/react';
|
import { useObject, useRealm } from '@realm/react';
|
||||||
import { TagEditor } from '../components';
|
import { TagEditor } from '../components';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
@@ -15,14 +15,14 @@ import { validateColor, validateTagName } from '../utilities';
|
|||||||
const EditTag = ({
|
const EditTag = ({
|
||||||
route,
|
route,
|
||||||
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_TAG>) => {
|
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_TAG>) => {
|
||||||
const navigation = useNavigation();
|
const { goBack } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation } = useDimensions();
|
const { orientation } = useDimensions();
|
||||||
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 tag = realm.objectForPrimaryKey(
|
const tag = useObject<Tag>(
|
||||||
Tag,
|
Tag.schema.name,
|
||||||
BSON.UUID.createFromHexString(route.params.id),
|
BSON.UUID.createFromHexString(route.params.id),
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
@@ -36,14 +36,14 @@ const EditTag = ({
|
|||||||
tag.dateModified = new Date();
|
tag.dateModified = new Date();
|
||||||
});
|
});
|
||||||
|
|
||||||
navigation.goBack();
|
goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
for (const meme of tag.memes) {
|
for (const meme of tag.memes) {
|
||||||
meme.dateModified = new Date();
|
meme.dateModified = new Date();
|
||||||
const tags = meme.tags as Set<Tag>;
|
const tags = meme.tags as Realm.Set<Tag>;
|
||||||
tags.delete(tag);
|
tags.delete(tag);
|
||||||
meme.tagsLength = tags.size;
|
meme.tagsLength = tags.size;
|
||||||
}
|
}
|
||||||
@@ -51,13 +51,13 @@ const EditTag = ({
|
|||||||
realm.delete(tag);
|
realm.delete(tag);
|
||||||
});
|
});
|
||||||
|
|
||||||
navigation.goBack();
|
goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
<Appbar.BackAction onPress={() => goBack()} />
|
||||||
<Appbar.Content title={'Edit Tag'} />
|
<Appbar.Content title={'Edit Tag'} />
|
||||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
|
@@ -9,29 +9,34 @@ import {
|
|||||||
import { useQuery } from '@realm/react';
|
import { useQuery } from '@realm/react';
|
||||||
import { useTheme, HelperText } from 'react-native-paper';
|
import { useTheme, HelperText } from 'react-native-paper';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { FlashList } from '@shopify/flash-list';
|
import { FlashList, MasonryFlashList } from '@shopify/flash-list';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import { SORT_DIRECTION, memesSortQuery } from '../types';
|
import { SORT_DIRECTION, memesSortQuery } from '../types';
|
||||||
import { RootState, setNavVisible } from '../state';
|
import { RootState, setNavVisible } from '../state';
|
||||||
import { Meme, Tag } from '../database';
|
import { Meme } from '../database';
|
||||||
import { useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
import { HideableHeader, MemesHeader } from '../components';
|
import { HideableHeader, MemesHeader } from '../components';
|
||||||
|
import MemeCard from '../components/memes/memeCard';
|
||||||
|
|
||||||
const memesStyles = StyleSheet.create({
|
const memesStyles = StyleSheet.create({
|
||||||
helperText: {
|
helperText: {
|
||||||
marginVertical: 10,
|
marginVertical: 10,
|
||||||
},
|
},
|
||||||
|
flashList: {
|
||||||
|
paddingBottom: 100,
|
||||||
|
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
|
||||||
|
paddingHorizontal: 0.01,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const Memes = () => {
|
const Memes = () => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation } = useDimensions();
|
const { dimensions, orientation } = useDimensions();
|
||||||
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,
|
||||||
);
|
);
|
||||||
const view = useSelector((state: RootState) => state.memes.view);
|
|
||||||
const favoritesOnly = useSelector(
|
const favoritesOnly = useSelector(
|
||||||
(state: RootState) => state.memes.favoritesOnly,
|
(state: RootState) => state.memes.favoritesOnly,
|
||||||
);
|
);
|
||||||
@@ -80,10 +85,11 @@ const Memes = () => {
|
|||||||
setScrollOffset(currentOffset);
|
setScrollOffset(currentOffset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const flashListRef = useRef<FlashList<Tag>>(null);
|
const flashListRef = useRef<FlashList<Meme>>(null);
|
||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
dispatch(setNavVisible(true));
|
||||||
const handleBackPress = () => {
|
const handleBackPress = () => {
|
||||||
if (scrollOffset > 0) {
|
if (scrollOffset > 0) {
|
||||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||||
@@ -96,7 +102,7 @@ const Memes = () => {
|
|||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
||||||
}, [flashListRef, scrollOffset]),
|
}, [dispatch, scrollOffset]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,6 +121,33 @@ const Memes = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</HideableHeader>
|
</HideableHeader>
|
||||||
|
<MasonryFlashList
|
||||||
|
ref={flashListRef}
|
||||||
|
data={memes}
|
||||||
|
estimatedItemSize={200}
|
||||||
|
estimatedListSize={{
|
||||||
|
height: dimensions.height,
|
||||||
|
width: dimensions.width * 0.92,
|
||||||
|
}}
|
||||||
|
numColumns={2}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
renderItem={({ item: meme }) => <MemeCard meme={meme} />}
|
||||||
|
contentContainerStyle={{
|
||||||
|
paddingTop:
|
||||||
|
flashListPadding +
|
||||||
|
dimensions.height *
|
||||||
|
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||||
|
...memesStyles.flashList,
|
||||||
|
}}
|
||||||
|
ListEmptyComponent={() => (
|
||||||
|
<HelperText
|
||||||
|
type={'info'}
|
||||||
|
style={[memesStyles.helperText, styles.centerText]}>
|
||||||
|
No memes found
|
||||||
|
</HelperText>
|
||||||
|
)}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -80,6 +80,7 @@ const Tags = () => {
|
|||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
|
dispatch(setNavVisible(true));
|
||||||
const handleBackPress = () => {
|
const handleBackPress = () => {
|
||||||
if (scrollOffset > 0) {
|
if (scrollOffset > 0) {
|
||||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||||
@@ -92,7 +93,7 @@ const Tags = () => {
|
|||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
||||||
}, [flashListRef, scrollOffset]),
|
}, [dispatch, scrollOffset]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -111,31 +112,33 @@ const Tags = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</HideableHeader>
|
</HideableHeader>
|
||||||
{flashListPadding > 0 && (
|
<FlashList
|
||||||
<FlashList
|
ref={flashListRef}
|
||||||
ref={flashListRef}
|
data={tags}
|
||||||
data={tags}
|
estimatedItemSize={50}
|
||||||
estimatedItemSize={52}
|
estimatedListSize={{
|
||||||
showsVerticalScrollIndicator={false}
|
height: dimensions.height,
|
||||||
renderItem={({ item: tag }) => <TagRow tag={tag} />}
|
width: dimensions.width * 0.92,
|
||||||
contentContainerStyle={{
|
}}
|
||||||
paddingTop:
|
showsVerticalScrollIndicator={false}
|
||||||
flashListPadding +
|
renderItem={({ item: tag }) => <TagRow tag={tag} />}
|
||||||
dimensions.height *
|
contentContainerStyle={{
|
||||||
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
paddingTop:
|
||||||
...tagsStyles.flashList,
|
flashListPadding +
|
||||||
}}
|
dimensions.height *
|
||||||
ItemSeparatorComponent={() => <Divider />}
|
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||||
ListEmptyComponent={() => (
|
...tagsStyles.flashList,
|
||||||
<HelperText
|
}}
|
||||||
type={'info'}
|
ItemSeparatorComponent={() => <Divider />}
|
||||||
style={[tagsStyles.helperText, styles.centerText]}>
|
ListEmptyComponent={() => (
|
||||||
No tags found
|
<HelperText
|
||||||
</HelperText>
|
type={'info'}
|
||||||
)}
|
style={[tagsStyles.helperText, styles.centerText]}>
|
||||||
onScroll={handleScroll}
|
No tags found
|
||||||
/>
|
</HelperText>
|
||||||
)}
|
)}
|
||||||
|
onScroll={handleScroll}
|
||||||
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
6
src/types/dimensions.ts
Normal file
6
src/types/dimensions.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
interface Dimensions {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { type Dimensions };
|
@@ -1,3 +1,4 @@
|
|||||||
|
export { type Dimensions } from './dimensions';
|
||||||
export { ROUTE, type RootStackParamList } from './route';
|
export { ROUTE, type RootStackParamList } from './route';
|
||||||
export {
|
export {
|
||||||
MEME_SORT,
|
MEME_SORT,
|
||||||
|
@@ -12,7 +12,7 @@ enum ROUTE {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface AddMemeRouteParamsFromFiles {
|
interface AddMemeRouteParamsFromFiles {
|
||||||
uri: DocumentPickerResponse[];
|
file: DocumentPickerResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface EditMemeRouteParams {
|
interface EditMemeRouteParams {
|
||||||
|
Reference in New Issue
Block a user