diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index 65eb93d..d02439a 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -3,7 +3,7 @@ import { Keyboard, StyleSheet } from 'react-native'; import { FAB } from 'react-native-paper'; import { ParamListBase, useNavigation } from '@react-navigation/native'; 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 { ROUTE } from '../types'; import { allowedMimeTypes, noOp } from '../utilities'; @@ -65,9 +65,9 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { onStateChange={({ open }) => setState(open)} onPress={async () => { if (!state) return; - const res = await pick({ type: allowedMimeTypes }).catch(noOp); - if (!res) return; - navigate(ROUTE.ADD_MEME, { uri: res }); + const file = await pickSingle({ type: allowedMimeTypes }).catch(noOp); + if (!file) return; + navigate(ROUTE.ADD_MEME, { file }); }} style={ orientation === ORIENTATION.PORTRAIT diff --git a/src/components/memes/memeCard.tsx b/src/components/memes/memeCard.tsx new file mode 100644 index 0000000..6cb1022 --- /dev/null +++ b/src/components/memes/memeCard.tsx @@ -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>(); + const { dimensions } = useDimensions(); + + const [imageWidth, setImageWidth] = useState(); + const [imageHeight, setImageHeight] = useState(); + + Image.getSize(meme.uri, (width, height) => { + const paddedWidth = (dimensions.width * 0.92) / 2 - 10; + setImageWidth(paddedWidth); + setImageHeight((paddedWidth / width) * height); + }); + + return ( + <> + {imageWidth && imageHeight && ( + + navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() }) + } + style={memeCardStyles.card}> + + + + )} + + ); +}; + +export default MemeCard; diff --git a/src/components/memes/memeEditor.tsx b/src/components/memes/memeEditor.tsx index b3c63e3..355f957 100644 --- a/src/components/memes/memeEditor.tsx +++ b/src/components/memes/memeEditor.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { HelperText, TextInput } from 'react-native-paper'; import { Image } from 'react-native'; import { useDimensions } from '../../contexts'; @@ -46,13 +46,11 @@ const MemeEditor = ({ const [imageWidth, setImageWidth] = useState(); const [imageHeight, setImageHeight] = useState(); - useEffect(() => { - Image.getSize(imageUri, (width, height) => { - const paddedWidth = dimensions.width * 0.92; - setImageWidth(paddedWidth); - setImageHeight((paddedWidth / width) * height); - }); - }, [dimensions.width, imageUri]); + Image.getSize(imageUri, (width, height) => { + const paddedWidth = dimensions.width * 0.92; + setImageWidth(paddedWidth); + setImageHeight((paddedWidth / width) * height); + }); if (!imageWidth || !imageHeight) return ; diff --git a/src/components/memes/memeTagSearchModal.tsx b/src/components/memes/memeTagSearchModal.tsx index 7958508..acf14cb 100644 --- a/src/components/memes/memeTagSearchModal.tsx +++ b/src/components/memes/memeTagSearchModal.tsx @@ -59,7 +59,7 @@ const MemeTagSearchModal = ({ let collection = collectionIn; if (search) { - collection = collection.filtered('name CONTAINS[c] $0', search); + collection = collection.filtered('name CONTAINS[c] $0', tagName.parsed); } collection = collection.sorted( @@ -88,7 +88,6 @@ const MemeTagSearchModal = ({ if (!tag) return; memeTags.set(tag.id.toHexString(), tag); setMemeTags(new Map(memeTags)); - setSearch(tag.name); }; return ( diff --git a/src/components/tags/tagEditor.tsx b/src/components/tags/tagEditor.tsx index c430ada..148fe36 100644 --- a/src/components/tags/tagEditor.tsx +++ b/src/components/tags/tagEditor.tsx @@ -22,8 +22,9 @@ const TagEditor = ({ const lastValidTagColor = useRef(tagColor.parsed); const handleTagColorChange = (color: string) => { - setTagColor(validateColor(color)); - if (tagColor.valid) lastValidTagColor.current = tagColor.parsed; + const result = validateColor(color); + setTagColor(result); + if (result.valid) lastValidTagColor.current = result.parsed; }; return ( diff --git a/src/database/meme.ts b/src/database/meme.ts index c2df2e5..504fcc4 100644 --- a/src/database/meme.ts +++ b/src/database/meme.ts @@ -21,13 +21,12 @@ const memeTypePlural = { class Meme extends Object { id!: BSON.UUID; type!: MEME_TYPE; - uri!: string[]; - hash!: string[]; + uri!: string; size!: number; title!: string; - description?: string; + description!: string; isFavorite!: boolean; - tags!: Tag[] | Set; + tags!: Tag[] | Realm.Set; tagsLength!: number; dateCreated!: Date; dateModified!: Date; @@ -40,11 +39,10 @@ class Meme extends Object { properties: { id: { type: 'uuid', default: () => new BSON.UUID() }, type: { type: 'string', indexed: true }, - uri: 'string[]', - hash: 'string[]', + uri: 'string', size: 'int', title: 'string', - description: 'string?', + description: { type: 'string', default: '' }, isFavorite: { type: 'bool', indexed: true, default: false }, tags: { type: 'set', objectType: 'Tag', default: [] }, tagsLength: { type: 'int', default: 0 }, diff --git a/src/database/tag.ts b/src/database/tag.ts index c778f6a..f54f46f 100644 --- a/src/database/tag.ts +++ b/src/database/tag.ts @@ -7,7 +7,7 @@ class Tag extends Object { id!: BSON.UUID; name!: string; color!: string; - memes!: Meme[] | Set; + memes!: Meme[] | Realm.Set; memesLength!: number; dateCreated!: Date; dateModified!: Date; diff --git a/src/screens/addMeme.tsx b/src/screens/addMeme.tsx index 900d38a..004328e 100644 --- a/src/screens/addMeme.tsx +++ b/src/screens/addMeme.tsx @@ -23,7 +23,7 @@ import { MemeEditor } from '../components'; const AddMeme = ({ route, }: NativeStackScreenProps) => { - 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.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; + const memes = tag.memes as Realm.Set; memes.add(meme); tag.memesLength = memes.size; }); }); - setIsSaving(false); - navigation.goBack(); + goBack(); }; return ( <> - navigation.goBack()} /> + goBack()} /> { - 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 ( <> - navigation.goBack()} /> + goBack()} /> { - const navigation = useNavigation(); +const EditMeme = ({ + route, +}: NativeStackScreenProps) => { + 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.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( + (meme.tags as Realm.Set).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; + memes.delete(meme); + tag.memesLength = memes.size; + tag.dateModified = new Date(); + } + }); + + memeTags.forEach(tag => { + if (!(meme.tags as Realm.Set).has(tag)) { + const memes = tag.memes as Realm.Set; + 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; + memes.delete(meme); + tag.memesLength = memes.size; + } + + realm.delete(meme); + }); + + goBack(); + }; return ( <> - navigation.goBack()} /> + goBack()} /> + + { styles.flexGrow, styles.flexColumnSpaceBetween, { backgroundColor: colors.background }, - ]}> + ]}> + + + + + + + ); }; diff --git a/src/screens/editTag.tsx b/src/screens/editTag.tsx index 4f09f8c..539b759 100644 --- a/src/screens/editTag.tsx +++ b/src/screens/editTag.tsx @@ -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) => { - 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.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; + const tags = meme.tags as Realm.Set; tags.delete(tag); meme.tagsLength = tags.size; } @@ -51,13 +51,13 @@ const EditTag = ({ realm.delete(tag); }); - navigation.goBack(); + goBack(); }; return ( <> - navigation.goBack()} /> + goBack()} /> diff --git a/src/screens/memes.tsx b/src/screens/memes.tsx index 7265372..12df1f8 100644 --- a/src/screens/memes.tsx +++ b/src/screens/memes.tsx @@ -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>(null); + const flashListRef = useRef>(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 = () => { }} /> + } + contentContainerStyle={{ + paddingTop: + flashListPadding + + dimensions.height * + (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), + ...memesStyles.flashList, + }} + ListEmptyComponent={() => ( + + No memes found + + )} + onScroll={handleScroll} + /> ); }; diff --git a/src/screens/tags.tsx b/src/screens/tags.tsx index cdc7074..9a6a20a 100644 --- a/src/screens/tags.tsx +++ b/src/screens/tags.tsx @@ -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 = () => { }} /> - {flashListPadding > 0 && ( - } - contentContainerStyle={{ - paddingTop: - flashListPadding + - dimensions.height * - (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), - ...tagsStyles.flashList, - }} - ItemSeparatorComponent={() => } - ListEmptyComponent={() => ( - - No tags found - - )} - onScroll={handleScroll} - /> - )} + } + contentContainerStyle={{ + paddingTop: + flashListPadding + + dimensions.height * + (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04), + ...tagsStyles.flashList, + }} + ItemSeparatorComponent={() => } + ListEmptyComponent={() => ( + + No tags found + + )} + onScroll={handleScroll} + /> ); }; diff --git a/src/types/dimensions.ts b/src/types/dimensions.ts new file mode 100644 index 0000000..1992449 --- /dev/null +++ b/src/types/dimensions.ts @@ -0,0 +1,6 @@ +interface Dimensions { + width: number; + height: number; +} + +export { type Dimensions }; diff --git a/src/types/index.ts b/src/types/index.ts index 2e6193e..b256712 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,3 +1,4 @@ +export { type Dimensions } from './dimensions'; export { ROUTE, type RootStackParamList } from './route'; export { MEME_SORT, diff --git a/src/types/route.ts b/src/types/route.ts index c1ee8c1..36bba4d 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -12,7 +12,7 @@ enum ROUTE { } interface AddMemeRouteParamsFromFiles { - uri: DocumentPickerResponse[]; + file: DocumentPickerResponse; } interface EditMemeRouteParams {