Add memes & meme-editing views
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -23,7 +23,7 @@ import { MemeEditor } from '../components';
|
||||
const AddMeme = ({
|
||||
route,
|
||||
}: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => {
|
||||
const navigation = useNavigation();
|
||||
const { goBack } = useNavigation();
|
||||
const { colors } = useTheme();
|
||||
const { orientation } = useDimensions();
|
||||
const realm = useRealm();
|
||||
@@ -32,9 +32,7 @@ const AddMeme = ({
|
||||
(state: RootState) => state.settings.storageUri,
|
||||
)!;
|
||||
|
||||
const uri = route.params.uri[0].uri;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const memeType = getMemeType(route.params.uri[0].type!);
|
||||
const { file } = route.params;
|
||||
|
||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||
const [memeDescription, setMemeDescription] = useState(
|
||||
@@ -49,31 +47,27 @@ const AddMeme = ({
|
||||
setIsSaving(true);
|
||||
|
||||
const uuid = new BSON.UUID();
|
||||
const savedUri: string[] = [];
|
||||
const hash: string[] = [];
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const fileExtension = extension(memeType!);
|
||||
if (!fileExtension) navigation.goBack();
|
||||
const mimeType = file.type!;
|
||||
const memeType = getMemeType(mimeType);
|
||||
|
||||
savedUri.push(
|
||||
AndroidScoped.appendPath(
|
||||
storageUri,
|
||||
`${uuid.toHexString()}.${fileExtension as string}`,
|
||||
),
|
||||
const fileExtension = extension(mimeType);
|
||||
if (!fileExtension) goBack();
|
||||
|
||||
const uri = AndroidScoped.appendPath(
|
||||
storageUri,
|
||||
`${uuid.toHexString()}.${fileExtension as string}`,
|
||||
);
|
||||
|
||||
await FileSystem.cp(uri, savedUri[0]);
|
||||
const { size } = await FileSystem.stat(savedUri[0]);
|
||||
hash.push(await FileSystem.hash(savedUri[0], 'MD5'));
|
||||
await FileSystem.cp(file.uri, uri);
|
||||
const { size } = await FileSystem.stat(uri);
|
||||
|
||||
realm.write(() => {
|
||||
const meme: Meme | undefined = realm.create<Meme>(Meme.schema.name, {
|
||||
id: uuid,
|
||||
type: memeType,
|
||||
uri: savedUri,
|
||||
uri,
|
||||
size,
|
||||
hash,
|
||||
title: memeTitle.parsed,
|
||||
description: memeDescription.parsed,
|
||||
isFavorite: memeIsFavorite,
|
||||
@@ -81,22 +75,21 @@ const AddMeme = ({
|
||||
tagsLength: memeTags.size,
|
||||
});
|
||||
|
||||
meme.tags.forEach(tag => {
|
||||
memeTags.forEach(tag => {
|
||||
tag.dateModified = new Date();
|
||||
const memes = tag.memes as Set<Meme>;
|
||||
const memes = tag.memes as Realm.Set<Meme>;
|
||||
memes.add(meme);
|
||||
tag.memesLength = memes.size;
|
||||
});
|
||||
});
|
||||
|
||||
setIsSaving(false);
|
||||
navigation.goBack();
|
||||
goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.BackAction onPress={() => goBack()} />
|
||||
<Appbar.Content title={'Add Meme'} />
|
||||
<Appbar.Action
|
||||
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
|
||||
@@ -115,7 +108,7 @@ const AddMeme = ({
|
||||
]}>
|
||||
<View style={[styles.flex, styles.justifyStart]}>
|
||||
<MemeEditor
|
||||
imageUri={uri}
|
||||
imageUri={file.uri}
|
||||
memeTitle={memeTitle}
|
||||
setMemeTitle={setMemeTitle}
|
||||
memeDescription={memeDescription}
|
||||
|
@@ -14,7 +14,7 @@ import { Tag } from '../database';
|
||||
import { TagEditor } from '../components';
|
||||
|
||||
const AddTag = () => {
|
||||
const navigation = useNavigation();
|
||||
const { goBack } = useNavigation();
|
||||
const { colors } = useTheme();
|
||||
const { orientation } = useDimensions();
|
||||
const realm = useRealm();
|
||||
@@ -32,13 +32,13 @@ const AddTag = () => {
|
||||
});
|
||||
});
|
||||
|
||||
navigation.goBack();
|
||||
goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.BackAction onPress={() => goBack()} />
|
||||
<Appbar.Content title={'Add Tag'} />
|
||||
</Appbar.Header>
|
||||
<ScrollView
|
||||
|
@@ -1,20 +1,112 @@
|
||||
import React from 'react';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { Appbar, useTheme } from 'react-native-paper';
|
||||
import React, { useState } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||
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 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 navigation = useNavigation();
|
||||
const EditMeme = ({
|
||||
route,
|
||||
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_MEME>) => {
|
||||
const { goBack } = useNavigation();
|
||||
const { colors } = useTheme();
|
||||
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 (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.BackAction onPress={() => goBack()} />
|
||||
<Appbar.Content title={'Edit Meme'} />
|
||||
<Appbar.Action
|
||||
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
|
||||
onPress={handleFavorite}
|
||||
/>
|
||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||
</Appbar.Header>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
@@ -25,7 +117,29 @@ const EditMeme = () => {
|
||||
styles.flexGrow,
|
||||
styles.flexColumnSpaceBetween,
|
||||
{ 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 { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { BSON } from 'realm';
|
||||
import { useRealm } from '@realm/react';
|
||||
import { useObject, useRealm } from '@realm/react';
|
||||
import { TagEditor } from '../components';
|
||||
import styles from '../styles';
|
||||
import { ORIENTATION, useDimensions } from '../contexts';
|
||||
@@ -15,14 +15,14 @@ import { validateColor, validateTagName } from '../utilities';
|
||||
const EditTag = ({
|
||||
route,
|
||||
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_TAG>) => {
|
||||
const navigation = useNavigation();
|
||||
const { goBack } = useNavigation();
|
||||
const { colors } = useTheme();
|
||||
const { orientation } = useDimensions();
|
||||
const realm = useRealm();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const tag = realm.objectForPrimaryKey(
|
||||
Tag,
|
||||
const tag = useObject<Tag>(
|
||||
Tag.schema.name,
|
||||
BSON.UUID.createFromHexString(route.params.id),
|
||||
)!;
|
||||
|
||||
@@ -36,14 +36,14 @@ const EditTag = ({
|
||||
tag.dateModified = new Date();
|
||||
});
|
||||
|
||||
navigation.goBack();
|
||||
goBack();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
realm.write(() => {
|
||||
for (const meme of tag.memes) {
|
||||
meme.dateModified = new Date();
|
||||
const tags = meme.tags as Set<Tag>;
|
||||
const tags = meme.tags as Realm.Set<Tag>;
|
||||
tags.delete(tag);
|
||||
meme.tagsLength = tags.size;
|
||||
}
|
||||
@@ -51,13 +51,13 @@ const EditTag = ({
|
||||
realm.delete(tag);
|
||||
});
|
||||
|
||||
navigation.goBack();
|
||||
goBack();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.BackAction onPress={() => goBack()} />
|
||||
<Appbar.Content title={'Edit Tag'} />
|
||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||
</Appbar.Header>
|
||||
|
@@ -9,29 +9,34 @@ import {
|
||||
import { useQuery } from '@realm/react';
|
||||
import { useTheme, HelperText } from 'react-native-paper';
|
||||
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 styles from '../styles';
|
||||
import { SORT_DIRECTION, memesSortQuery } from '../types';
|
||||
import { RootState, setNavVisible } from '../state';
|
||||
import { Meme, Tag } from '../database';
|
||||
import { useDimensions } from '../contexts';
|
||||
import { Meme } from '../database';
|
||||
import { ORIENTATION, useDimensions } from '../contexts';
|
||||
import { HideableHeader, MemesHeader } from '../components';
|
||||
import MemeCard from '../components/memes/memeCard';
|
||||
|
||||
const memesStyles = StyleSheet.create({
|
||||
helperText: {
|
||||
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 { colors } = useTheme();
|
||||
const { orientation } = useDimensions();
|
||||
const { dimensions, orientation } = useDimensions();
|
||||
const sort = useSelector((state: RootState) => state.memes.sort);
|
||||
const sortDirection = useSelector(
|
||||
(state: RootState) => state.memes.sortDirection,
|
||||
);
|
||||
const view = useSelector((state: RootState) => state.memes.view);
|
||||
const favoritesOnly = useSelector(
|
||||
(state: RootState) => state.memes.favoritesOnly,
|
||||
);
|
||||
@@ -80,10 +85,11 @@ const Memes = () => {
|
||||
setScrollOffset(currentOffset);
|
||||
};
|
||||
|
||||
const flashListRef = useRef<FlashList<Tag>>(null);
|
||||
const flashListRef = useRef<FlashList<Meme>>(null);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
dispatch(setNavVisible(true));
|
||||
const handleBackPress = () => {
|
||||
if (scrollOffset > 0) {
|
||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
@@ -96,7 +102,7 @@ const Memes = () => {
|
||||
|
||||
return () =>
|
||||
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
||||
}, [flashListRef, scrollOffset]),
|
||||
}, [dispatch, scrollOffset]),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -115,6 +121,33 @@ const Memes = () => {
|
||||
}}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
@@ -80,6 +80,7 @@ const Tags = () => {
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
dispatch(setNavVisible(true));
|
||||
const handleBackPress = () => {
|
||||
if (scrollOffset > 0) {
|
||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
@@ -92,7 +93,7 @@ const Tags = () => {
|
||||
|
||||
return () =>
|
||||
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
||||
}, [flashListRef, scrollOffset]),
|
||||
}, [dispatch, scrollOffset]),
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -111,31 +112,33 @@ const Tags = () => {
|
||||
}}
|
||||
/>
|
||||
</HideableHeader>
|
||||
{flashListPadding > 0 && (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={tags}
|
||||
estimatedItemSize={52}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: tag }) => <TagRow tag={tag} />}
|
||||
contentContainerStyle={{
|
||||
paddingTop:
|
||||
flashListPadding +
|
||||
dimensions.height *
|
||||
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||
...tagsStyles.flashList,
|
||||
}}
|
||||
ItemSeparatorComponent={() => <Divider />}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[tagsStyles.helperText, styles.centerText]}>
|
||||
No tags found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
)}
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={tags}
|
||||
estimatedItemSize={50}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width * 0.92,
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: tag }) => <TagRow tag={tag} />}
|
||||
contentContainerStyle={{
|
||||
paddingTop:
|
||||
flashListPadding +
|
||||
dimensions.height *
|
||||
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||
...tagsStyles.flashList,
|
||||
}}
|
||||
ItemSeparatorComponent={() => <Divider />}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[tagsStyles.helperText, styles.centerText]}>
|
||||
No tags found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
Reference in New Issue
Block a user