From 4b601872bc01f7ee358812263595b7593b51a9a4 Mon Sep 17 00:00:00 2001 From: Nikolaos Karaolidis Date: Fri, 21 Jul 2023 09:46:13 +0300 Subject: [PATCH] Add meme-adding logic Signed-off-by: Nikolaos Karaolidis --- android/app/build.gradle | 4 + package-lock.json | 51 ++++--- package.json | 2 +- src/components/floatingActionButton.tsx | 24 +++- src/components/hideableHeader.tsx | 2 +- src/components/index.ts | 4 +- src/components/memes/index.ts | 3 + src/components/memes/memeEditor.tsx | 99 ++++++++++++++ src/components/memes/memeTagSearchModal.tsx | 128 ++++++++++++++++++ src/components/memes/memeTagSelector.tsx | 70 ++++++++++ src/components/tags/index.ts | 3 + src/components/tags/tagChip.tsx | 53 ++++++-- src/components/tags/tagEditor.tsx | 90 +++++++++++++ src/components/tags/tagPreview.tsx | 24 ++-- src/contexts/dimensions.tsx | 6 +- src/database/index.ts | 2 +- src/database/meme.ts | 16 ++- src/database/tag.ts | 31 ++--- src/navigation.tsx | 21 ++- src/screens/addMeme.tsx | 141 ++++++++++++++++++++ src/screens/addTag.tsx | 79 +++++++++++ src/screens/editMeme.tsx | 15 +-- src/screens/editTag.tsx | 120 +++++------------ src/screens/index.ts | 4 +- src/screens/{home.tsx => memes.tsx} | 48 +++---- src/screens/settings.tsx | 12 +- src/screens/tags.tsx | 10 +- src/state/home.ts | 87 ------------ src/state/index.ts | 26 ++-- src/state/memes.ts | 83 ++++++++++++ src/styles.tsx | 9 ++ src/types/index.ts | 2 +- src/types/route.ts | 26 +++- src/types/sort.ts | 8 +- src/types/view.ts | 1 - src/utilities/color.ts | 4 +- src/utilities/database.ts | 5 + src/utilities/filesystem.ts | 33 +++++ src/utilities/icon.ts | 6 +- src/utilities/index.ts | 9 +- 40 files changed, 1037 insertions(+), 324 deletions(-) create mode 100644 src/components/memes/index.ts create mode 100644 src/components/memes/memeEditor.tsx create mode 100644 src/components/memes/memeTagSearchModal.tsx create mode 100644 src/components/memes/memeTagSelector.tsx create mode 100644 src/components/tags/index.ts create mode 100644 src/components/tags/tagEditor.tsx create mode 100644 src/screens/addMeme.tsx create mode 100644 src/screens/addTag.tsx rename src/screens/{home.tsx => memes.tsx} (80%) delete mode 100644 src/state/home.ts create mode 100644 src/state/memes.ts create mode 100644 src/utilities/database.ts create mode 100644 src/utilities/filesystem.ts diff --git a/android/app/build.gradle b/android/app/build.gradle index d19b498..70803aa 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -118,6 +118,10 @@ dependencies { } else { implementation jscFlavor } + + implementation 'com.facebook.fresco:animated-gif:2.5.0' + implementation 'com.facebook.fresco:animated-webp:2.5.0' + implementation 'com.facebook.fresco:webpsupport:2.5.0' } apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project) diff --git a/package-lock.json b/package-lock.json index 9d582b0..cb69163 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,10 +20,10 @@ "react": "18.2.0", "react-native": "0.72.2", "react-native-document-picker": "^9.0.1", - "react-native-fast-image": "^8.6.3", "react-native-file-access": "^3.0.4", "react-native-gesture-handler": "^2.12.0", "react-native-get-random-values": "^1.9.0", + "react-native-mime-types": "^2.4.0", "react-native-paper": "^5.9.1", "react-native-reanimated": "^3.3.0", "react-native-safe-area-context": "^4.6.4", @@ -13371,15 +13371,6 @@ } } }, - "node_modules/react-native-fast-image": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz", - "integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==", - "peerDependencies": { - "react": "^17 || ^18", - "react-native": ">=0.60.0" - } - }, "node_modules/react-native-file-access": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz", @@ -13416,6 +13407,25 @@ "react-native": ">=0.56" } }, + "node_modules/react-native-mime-types": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz", + "integrity": "sha512-a7LymNr7yQzrDEhSMPNAy9aIs1OckBpo6G8OkjVQTzaCe0XaSXCXu6KJsu/a4c3HVF9t0FiFSnxsRVEctpPI0g==", + "dependencies": { + "mime-db": "~1.37.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/react-native-mime-types/node_modules/mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/react-native-paper": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz", @@ -25752,12 +25762,6 @@ "invariant": "^2.2.4" } }, - "react-native-fast-image": { - "version": "8.6.3", - "resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz", - "integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==", - "requires": {} - }, "react-native-file-access": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz", @@ -25784,6 +25788,21 @@ "fast-base64-decode": "^1.0.0" } }, + "react-native-mime-types": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz", + "integrity": "sha512-a7LymNr7yQzrDEhSMPNAy9aIs1OckBpo6G8OkjVQTzaCe0XaSXCXu6KJsu/a4c3HVF9t0FiFSnxsRVEctpPI0g==", + "requires": { + "mime-db": "~1.37.0" + }, + "dependencies": { + "mime-db": { + "version": "1.37.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz", + "integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==" + } + } + }, "react-native-paper": { "version": "5.9.1", "resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz", diff --git a/package.json b/package.json index ba9fb18..c24a9f7 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,10 @@ "react": "18.2.0", "react-native": "0.72.2", "react-native-document-picker": "^9.0.1", - "react-native-fast-image": "^8.6.3", "react-native-file-access": "^3.0.4", "react-native-gesture-handler": "^2.12.0", "react-native-get-random-values": "^1.9.0", + "react-native-mime-types": "^2.4.0", "react-native-paper": "^5.9.1", "react-native-reanimated": "^3.3.0", "react-native-safe-area-context": "^4.6.4", diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index 92dfc4e..9825b96 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -3,8 +3,10 @@ import { Keyboard } 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 { useDimensions } from '../contexts'; import { ROUTE } from '../types'; +import { allowedMimeTypes, noOp } from '../utilities'; const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { const { navigate } = @@ -39,22 +41,34 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { { icon: 'tag', label: 'Tag', - onPress: () => navigate(ROUTE.EDIT_TAG), + onPress: () => navigate(ROUTE.ADD_TAG), }, { icon: 'note-text', label: 'Text', - onPress: () => navigate(ROUTE.EDIT_MEME), + onPress: () => { + throw new Error('Not yet implemented'); + }, }, { icon: 'image-album', label: 'Album', - onPress: () => navigate(ROUTE.EDIT_MEME), + onPress: async () => { + const res = await pick({ + allowMultiSelection: true, + type: allowedMimeTypes, + }).catch(noOp); + if (!res) return; + navigate(ROUTE.ADD_MEME, { uri: res }); + }, }, ]} onStateChange={({ open }) => setState(open)} - onPress={() => { - if (state) navigate(ROUTE.EDIT_MEME); + onPress={async () => { + if (!state) return; + const res = await pick({ type: allowedMimeTypes }).catch(noOp); + if (!res) return; + navigate(ROUTE.ADD_MEME, { uri: res }); }} style={{ paddingBottom: responsive.verticalScale(75), diff --git a/src/components/hideableHeader.tsx b/src/components/hideableHeader.tsx index fe341f9..fa64ca2 100644 --- a/src/components/hideableHeader.tsx +++ b/src/components/hideableHeader.tsx @@ -10,7 +10,7 @@ const hideableHeaderStyles = StyleSheet.create({ top: 0, left: 0, right: 0, - zIndex: 100, + zIndex: 1, }, }); diff --git a/src/components/index.ts b/src/components/index.ts index 455a959..334674c 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,6 +1,6 @@ +export { MemeEditor } from './memes'; +export { TagChip, TagEditor, TagPreview } from './tags'; export { default as FloatingActionButton } from './floatingActionButton'; export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar'; export { default as HideableHeader } from './hideableHeader'; export { default as LoadingView } from './loadingView'; -export { default as TagChip } from './tags/tagChip'; -export { default as TagPreview } from './tags/tagPreview'; diff --git a/src/components/memes/index.ts b/src/components/memes/index.ts new file mode 100644 index 0000000..3c001b1 --- /dev/null +++ b/src/components/memes/index.ts @@ -0,0 +1,3 @@ +export { default as MemeEditor } from './memeEditor'; +export { default as MemeTagSearchModal } from './memeTagSearchModal'; +export { default as MemeTagSelector } from './memeTagSelector'; diff --git a/src/components/memes/memeEditor.tsx b/src/components/memes/memeEditor.tsx new file mode 100644 index 0000000..9069dc9 --- /dev/null +++ b/src/components/memes/memeEditor.tsx @@ -0,0 +1,99 @@ +import React, { useEffect, useState } from 'react'; +import { HelperText, TextInput } from 'react-native-paper'; +import { Image } from 'react-native'; +import { useDimensions } from '../../contexts'; +import LoadingView from '../loadingView'; +import { MemeTagSelector } from '.'; +import { Tag } from '../../database'; + +const MemeEditor = ({ + imageUri, + memeTitle, + setMemeTitle, + memeDescription, + setMemeDescription, + memeTags, + setMemeTags, + memeTitleError, + setMemeTitleError, +}: { + imageUri: string[]; + memeTitle: string; + setMemeTitle: (name: string) => void; + memeDescription: string; + setMemeDescription: (description: string) => void; + memeTags: Map; + setMemeTags: (tags: Map) => void; + memeTitleError: string | undefined; + setMemeTitleError: (error: string | undefined) => void; +}) => { + const { dimensions, fixed, responsive } = useDimensions(); + + const [imageWidth, setImageWidth] = useState(); + const [imageHeight, setImageHeight] = useState(); + + useEffect(() => { + Image.getSize(imageUri[0], (width, height) => { + const paddedWidth = dimensions.width - dimensions.width * 0.08; + setImageWidth(paddedWidth); + setImageHeight((paddedWidth / width) * height); + }); + }, [dimensions.width, imageUri]); + + const handleMemeTitleChange = (name: string) => { + setMemeTitle(name); + if (name.length === 0) { + setMemeTitleError('Meme title cannot be empty'); + } else { + // eslint-disable-next-line unicorn/no-useless-undefined + setMemeTitleError(undefined); + } + }; + + if (!imageWidth || !imageHeight) return ; + + return ( + <> + + + {memeTitleError} + + + + + + ); +}; + +export default MemeEditor; diff --git a/src/components/memes/memeTagSearchModal.tsx b/src/components/memes/memeTagSearchModal.tsx new file mode 100644 index 0000000..31b38b1 --- /dev/null +++ b/src/components/memes/memeTagSearchModal.tsx @@ -0,0 +1,128 @@ +import React, { useRef, useState } from 'react'; +import { TagChip } from '../tags'; +import { Tag } from '../../database'; +import { useQuery, useRealm } from '@realm/react'; +import { TAG_SORT, tagSortQuery } from '../../types'; +import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper'; +import { StyleSheet } from 'react-native'; +import { useDimensions } from '../../contexts'; +import styles from '../../styles'; +import { FlashList } from '@shopify/flash-list'; + +const memeTagSearchModalStyles = StyleSheet.create({ + modal: { + position: 'absolute', + bottom: 0, + }, +}); + +const MemeTagSearchModal = ({ + visible, + setVisible, + memeTags, + setMemeTags, +}: { + visible: boolean; + setVisible: (visible: boolean) => void; + memeTags: Map; + setMemeTags: (tags: Map) => void; +}) => { + const { colors } = useTheme(); + const { fixed, responsive } = useDimensions(); + const realm = useRealm(); + + const flashListRef = useRef>(null); + + const [search, setSearch] = useState(''); + + const handleSearch = (newSearch: string) => { + flashListRef.current?.scrollToOffset({ offset: 0 }); + setSearch(newSearch); + }; + + const tags = useQuery( + Tag.schema.name, + collection => + collection + .filtered(`name CONTAINS[c] "${search}"`) + .sorted(tagSortQuery(TAG_SORT.DATE_MODIFIED), true), + [search], + ); + + const handleTagPress = (tag: Tag) => { + const id = tag.id.toHexString(); + memeTags.delete(id) || memeTags.set(id, tag); + setMemeTags(new Map(memeTags)); + }; + + const handleCreateTag = (name: string) => { + let tag: Tag | undefined; + realm.write(() => { + tag = realm.create(Tag.schema.name, { + name, + }); + }); + if (!tag) return; + memeTags.set(tag.id.toHexString(), tag); + setMemeTags(new Map(memeTags)); + setSearch(tag.name); + }; + + return ( + + setVisible(false)}> + + tag.id.toHexString()} + horizontal + estimatedItemSize={120} + showsHorizontalScrollIndicator={false} + keyboardShouldPersistTaps={'always'} + renderItem={({ item: tag }) => ( + handleTagPress(tag)} + active={memeTags.has(tag.id.toHexString())} + /> + )} + ListEmptyComponent={() => ( + handleCreateTag(search.replaceAll(/\s+/g, ''))}> + Create Tag #{search.replaceAll(/\s+/g, '')} + + )} + /> + + + ); +}; + +export default MemeTagSearchModal; diff --git a/src/components/memes/memeTagSelector.tsx b/src/components/memes/memeTagSelector.tsx new file mode 100644 index 0000000..06c743b --- /dev/null +++ b/src/components/memes/memeTagSelector.tsx @@ -0,0 +1,70 @@ +import React, { ComponentProps, useState } from 'react'; +import { View } from 'react-native'; +import { Chip } from 'react-native-paper'; +import { TagChip } from '../tags'; +import { Tag } from '../../database'; +import { useDimensions } from '../../contexts'; +import { MemeTagSearchModal } from '.'; +import { FlashList } from '@shopify/flash-list'; + +const MemeTagSelector = ({ + memeTags, + setMemeTags, + ...props +}: { + memeTags: Map; + setMemeTags: (tags: Map) => void; +} & ComponentProps) => { + const { fixed, dimensions } = useDimensions(); + + const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false); + + const handleTagPress = (tag: Tag) => { + const id = tag.id.toHexString(); + memeTags.delete(id) || memeTags.set(id, tag); + setMemeTags(new Map(memeTags)); + }; + + return ( + <> + + tag.id.toHexString()} + horizontal + estimatedItemSize={120} + showsHorizontalScrollIndicator={false} + renderItem={({ item: tag }) => ( + handleTagPress(tag)} + style={{ + marginRight: fixed.horizontalScale(4), + }} + /> + )} + ListFooterComponent={() => ( + setTagSearchModalVisible(true)} + style={{ + marginRight: dimensions.width * 0.92 - 105.8, + }}> + Add Tag + + )} + /> + + + + ); +}; + +export default MemeTagSelector; diff --git a/src/components/tags/index.ts b/src/components/tags/index.ts new file mode 100644 index 0000000..1cba957 --- /dev/null +++ b/src/components/tags/index.ts @@ -0,0 +1,3 @@ +export { default as TagChip } from './tagChip'; +export { default as TagEditor } from './tagEditor'; +export { default as TagPreview } from './tagPreview'; diff --git a/src/components/tags/tagChip.tsx b/src/components/tags/tagChip.tsx index dfb801a..0597faf 100644 --- a/src/components/tags/tagChip.tsx +++ b/src/components/tags/tagChip.tsx @@ -1,24 +1,57 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { getContrastColor } from '../../utilities'; -import { Chip } from 'react-native-paper'; +import { Chip, useTheme } from 'react-native-paper'; import { Tag } from '../../database'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; +import { StyleSheet } from 'react-native'; + +const tagChipStyles = StyleSheet.create({ + chip: { + borderWidth: 1, + }, +}); + +const TagChip = ({ + tag, + active = true, + onPress, + ...props +}: { + tag: Tag; + active?: boolean; + onPress?: () => void; +} & Omit, 'children'>) => { + const theme = useTheme(); + + const chipTheme = useMemo(() => { + return { + ...theme, + colors: { + ...theme.colors, + secondaryContainer: tag.color, + outline: tag.color, + }, + }; + }, [tag.color, theme]); -const TagChip = ({ tag }: { tag: Tag }) => { const contrastColor = getContrastColor(tag.color); return ( { - return ; + return ( + + ); }} compact - style={[ - { - backgroundColor: tag.color, - }, - ]} - textStyle={{ color: contrastColor }}> + theme={chipTheme} + mode={active ? 'flat' : 'outlined'} + style={[tagChipStyles.chip, props.style]} + textStyle={{ + color: active ? contrastColor : theme.colors.onBackground, + }} + onPress={onPress}> {'#' + tag.name} ); diff --git a/src/components/tags/tagEditor.tsx b/src/components/tags/tagEditor.tsx new file mode 100644 index 0000000..ea5717c --- /dev/null +++ b/src/components/tags/tagEditor.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { HelperText, TextInput } from 'react-native-paper'; +import TagPreview from './tagPreview'; +import { generateRandomColor, isValidColor } from '../../utilities'; + +const TagEditor = ({ + tagName, + setTagName, + tagColor, + setTagColor, + validatedTagColor, + setValidatedTagColor, + tagNameError, + setTagNameError, + tagColorError, + setTagColorError, +}: { + tagName: string; + setTagName: (name: string) => void; + tagColor: string; + setTagColor: (color: string) => void; + validatedTagColor: string; + setValidatedTagColor: (color: string) => void; + tagNameError: string | undefined; + setTagNameError: (error: string | undefined) => void; + tagColorError: string | undefined; + setTagColorError: (error: string | undefined) => void; +}) => { + const handleTagNameChange = (name: string) => { + setTagName(name); + + if (name.length === 0) { + setTagNameError('Tag name cannot be empty'); + } else if (name.includes(' ')) { + setTagNameError('Tag name cannot contain spaces'); + } else { + // eslint-disable-next-line unicorn/no-useless-undefined + setTagNameError(undefined); + } + }; + + const handleTagColorChange = (color: string) => { + setTagColor(color); + + if (isValidColor(color)) { + setValidatedTagColor(color); + // eslint-disable-next-line unicorn/no-useless-undefined + setTagColorError(undefined); + } else { + setTagColorError('Color must be a valid hex or rgb value'); + } + }; + + return ( + <> + + + + {tagNameError} + + handleTagColorChange(generateRandomColor())} + /> + } + /> + + {tagColorError} + + + ); +}; + +export default TagEditor; diff --git a/src/components/tags/tagPreview.tsx b/src/components/tags/tagPreview.tsx index 8e9e329..9b2ecd4 100644 --- a/src/components/tags/tagPreview.tsx +++ b/src/components/tags/tagPreview.tsx @@ -1,7 +1,7 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { StyleSheet, View } from 'react-native'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; -import { Chip } from 'react-native-paper'; +import { Chip, useTheme } from 'react-native-paper'; import styles from '../../styles'; import { useDimensions } from '../../contexts'; import { getContrastColor } from '../../utilities'; @@ -16,7 +16,19 @@ const tagPreviewStyles = StyleSheet.create({ }); const TagPreview = ({ name, color }: { name: string; color: string }) => { + const theme = useTheme(); const { responsive } = useDimensions(); + + const chipTheme = useMemo(() => { + return { + ...theme, + colors: { + ...theme.colors, + secondaryContainer: color, + }, + }; + }, [theme, color]); + const contrastColor = getContrastColor(color); return ( @@ -33,12 +45,8 @@ const TagPreview = ({ name, color }: { name: string; color: string }) => { return ; }} elevated - style={[ - tagPreviewStyles.chip, - { - backgroundColor: color, - }, - ]} + style={tagPreviewStyles.chip} + theme={chipTheme} textStyle={[tagPreviewStyles.text, { color: contrastColor }]}> {'#' + name} diff --git a/src/contexts/dimensions.tsx b/src/contexts/dimensions.tsx index b935e7f..63d5c47 100644 --- a/src/contexts/dimensions.tsx +++ b/src/contexts/dimensions.tsx @@ -21,7 +21,7 @@ interface DimensionsContext { orientation: 'portrait' | 'landscape'; dimensions: ScaledSize; responsive: ScaleFunctions; - static: ScaleFunctions; + fixed: ScaleFunctions; } const createScaleFunctions = (dimensionsIn: ScaledSize) => { @@ -56,7 +56,7 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => { }, []); const responsiveScale = createScaleFunctions(dimensions); - const staticScale = createScaleFunctions(initialDimensions); + const fixedScale = createScaleFunctions(initialDimensions); useEffect(() => { const onChange = ({ window }: { window: ScaledSize }) => { @@ -76,7 +76,7 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => { orientation, dimensions, responsive: responsiveScale, - static: staticScale, + fixed: fixedScale, }}> {children} diff --git a/src/database/index.ts b/src/database/index.ts index cf73766..e47d695 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -1,2 +1,2 @@ export { MEME_TYPE, memeTypePlural, Meme } from './meme'; -export { Tag, deleteTag, deleteAllTags } from './tag'; +export { Tag } from './tag'; diff --git a/src/database/meme.ts b/src/database/meme.ts index e58e604..e55ae18 100644 --- a/src/database/meme.ts +++ b/src/database/meme.ts @@ -1,5 +1,4 @@ -import { Realm } from '@realm/react'; -import { BSON } from 'realm'; +import { BSON, Object, ObjectSchema } from 'realm'; import { Tag } from './tag'; enum MEME_TYPE { @@ -20,33 +19,36 @@ const memeTypePlural = { [MEME_TYPE.TEXT]: 'Text', }; -class Meme extends Realm.Object { +// eslint-disable-next-line @typescript-eslint/naming-convention +class Meme extends Object { id!: BSON.UUID; type!: MEME_TYPE; - uri!: Realm.List; + uri!: string[]; + hash!: string[]; size!: number; title!: string; description?: string; isFavorite!: boolean; - tags!: Realm.List; + tags!: Tag[] | Set; tagsLength!: number; dateCreated!: Date; dateModified!: Date; dateUsed?: Date; timesUsed!: number; - static schema: Realm.ObjectSchema = { + static schema: ObjectSchema = { name: 'Meme', primaryKey: 'id', properties: { id: { type: 'uuid', default: () => new BSON.UUID() }, type: { type: 'string', indexed: true }, uri: 'string[]', + hash: 'string[]', size: 'int', title: 'string', description: 'string?', isFavorite: { type: 'bool', indexed: true, default: false }, - tags: { type: 'list', objectType: 'Tag', default: [] }, + tags: { type: 'set', objectType: 'Tag', default: [] }, tagsLength: { type: 'int', default: 0 }, dateCreated: { type: 'date', default: () => new Date() }, dateModified: { type: 'date', default: () => new Date() }, diff --git a/src/database/tag.ts b/src/database/tag.ts index 7612a7f..c778f6a 100644 --- a/src/database/tag.ts +++ b/src/database/tag.ts @@ -1,43 +1,34 @@ -import { Realm } from '@realm/react'; -import { BSON } from 'realm'; +import { BSON, Object, ObjectSchema } from 'realm'; import { Meme } from './meme'; +import { generateRandomColor } from '../utilities'; -class Tag extends Realm.Object { +// eslint-disable-next-line @typescript-eslint/naming-convention +class Tag extends Object { id!: BSON.UUID; name!: string; color!: string; - memes!: Realm.List; + memes!: Meme[] | Set; memesLength!: number; dateCreated!: Date; dateModified!: Date; + dateUsed?: Date; timesUsed!: number; - static schema: Realm.ObjectSchema = { + static schema: ObjectSchema = { name: 'Tag', primaryKey: 'id', properties: { id: { type: 'uuid', default: () => new BSON.UUID() }, name: { type: 'string', indexed: true }, - color: 'string', - memes: { type: 'list', objectType: 'Meme', default: [] }, + color: { type: 'string', default: () => generateRandomColor() }, + memes: { type: 'set', objectType: 'Meme', default: [] }, memesLength: { type: 'int', default: 0 }, dateCreated: { type: 'date', default: () => new Date() }, dateModified: { type: 'date', default: () => new Date() }, + dateUsed: 'date?', timesUsed: { type: 'int', default: 0 }, }, }; } -const deleteTag = (realm: Realm, tag: Tag) => { - realm.write(() => { - realm.delete(tag); - }); -}; - -const deleteAllTags = (realm: Realm) => { - realm.write(() => { - realm.delete(realm.objects('Tag')); - }); -}; - -export { Tag, deleteTag, deleteAllTags }; +export { Tag }; diff --git a/src/navigation.tsx b/src/navigation.tsx index 23e5bfe..3febe72 100644 --- a/src/navigation.tsx +++ b/src/navigation.tsx @@ -5,7 +5,15 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; import { createNativeStackNavigator } from '@react-navigation/native-stack'; import { useTheme } from 'react-native-paper'; import { useSelector } from 'react-redux'; -import { Home, Tags, Settings, EditMeme, EditTag } from './screens'; +import { + Memes, + Tags, + Settings, + EditMeme, + EditTag, + AddMeme, + AddTag, +} from './screens'; import { darkNavigationTheme, lightNavigationTheme } from './theme'; import { FloatingActionButton, @@ -19,7 +27,7 @@ const TabNavigator = () => { (state: RootState) => state.navigation.navVisible, ); - const [route, setRoute] = React.useState(ROUTE.HOME); + const [route, setRoute] = React.useState(ROUTE.MEMES); const TabNavigatorBase = createBottomTabNavigator(); return ( @@ -27,6 +35,7 @@ const TabNavigator = () => { ( { /> )}> ( - + ), }} /> @@ -85,10 +94,12 @@ const NavigationContainer = () => { animation: 'slide_from_bottom', }}> + + diff --git a/src/screens/addMeme.tsx b/src/screens/addMeme.tsx new file mode 100644 index 0000000..ec12651 --- /dev/null +++ b/src/screens/addMeme.tsx @@ -0,0 +1,141 @@ +import React, { useState } from 'react'; +import { Appbar, Button, useTheme } from 'react-native-paper'; +import { useNavigation } from '@react-navigation/native'; +import { useDimensions } from '../contexts'; +import { ScrollView, View } from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { useRealm } from '@realm/react'; +import { BSON } from 'realm'; +import { AndroidScoped, FileSystem } from 'react-native-file-access'; +import { useSelector } from 'react-redux'; +import { extension } from 'react-native-mime-types'; +import styles from '../styles'; +import { ROUTE, RootStackParamList } from '../types'; +import { MEME_TYPE, Meme, Tag } from '../database'; +import { RootState } from '../state'; +import { getMemeType } from '../utilities'; +import { MemeEditor } from '../components'; + +const AddMeme = ({ + route, +}: NativeStackScreenProps) => { + const navigation = useNavigation(); + const { colors } = useTheme(); + const { orientation } = useDimensions(); + const realm = useRealm(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storageUri = useSelector( + (state: RootState) => state.settings.storageUri, + )!; + + const { uri } = route.params; + + const memeType = + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(uri[0].type!); + + const [memeTitle, setMemeTitle] = useState('New Meme'); + const [memeDescription, setMemeDescription] = useState(''); + const [memeIsFavorite, setMemeIsFavorite] = useState(false); + const [memeTags, setMemeTags] = useState(new Map()); + + const [memeTitleError, setMemeTitleError] = useState(); + + const [isSaving, setIsSaving] = useState(false); + + const handleSave = async () => { + 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(uri[0].type!); + if (!fileExtension) navigation.goBack(); + + savedUri.push( + AndroidScoped.appendPath( + storageUri, + `${uuid.toHexString()}.${fileExtension as string}`, + ), + ); + + await FileSystem.cp(uri[0].uri, savedUri[0]); + const { size } = await FileSystem.stat(savedUri[0]); + hash.push(await FileSystem.hash(savedUri[0], 'MD5')); + + realm.write(() => { + const meme: Meme | undefined = realm.create(Meme.schema.name, { + id: uuid, + type: memeType, + uri: savedUri, + size, + hash, + title: memeTitle, + description: memeDescription, + isFavorite: memeIsFavorite, + tags: [...memeTags.values()], + tagsLength: memeTags.size, + }); + + meme.tags.forEach(tag => { + tag.dateModified = new Date(); + const memes = tag.memes as Set; + memes.add(meme); + tag.memesLength = memes.size; + }); + }); + + setIsSaving(false); + navigation.goBack(); + }; + + return ( + <> + + navigation.goBack()} /> + + setMemeIsFavorite(!memeIsFavorite)} + /> + + + + uriIn.uri)} + memeTitle={memeTitle} + setMemeTitle={setMemeTitle} + memeDescription={memeDescription} + setMemeDescription={setMemeDescription} + memeTags={memeTags} + setMemeTags={setMemeTags} + memeTitleError={memeTitleError} + setMemeTitleError={setMemeTitleError} + /> + + + + + + + ); +}; + +export default AddMeme; diff --git a/src/screens/addTag.tsx b/src/screens/addTag.tsx new file mode 100644 index 0000000..bef0222 --- /dev/null +++ b/src/screens/addTag.tsx @@ -0,0 +1,79 @@ +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 { useRealm } from '@realm/react'; +import styles from '../styles'; +import { generateRandomColor } from '../utilities'; +import { useDimensions } from '../contexts'; +import { Tag } from '../database'; +import { TagEditor } from '../components'; + +const AddTag = () => { + const navigation = useNavigation(); + const { colors } = useTheme(); + const { orientation } = useDimensions(); + const realm = useRealm(); + + const [tagName, setTagName] = useState('newTag'); + const [tagColor, setTagColor] = useState(generateRandomColor()); + const [validatedTagColor, setValidatedTagColor] = useState(tagColor); + + const [tagNameError, setTagNameError] = useState(); + const [tagColorError, setTagColorError] = useState(); + + const handleSave = () => { + realm.write(() => { + realm.create(Tag.schema.name, { + name: tagName, + color: tagColor, + }); + }); + + navigation.goBack(); + }; + + return ( + <> + + navigation.goBack()} /> + + + + + + + + + + + + ); +}; + +export default AddTag; diff --git a/src/screens/editMeme.tsx b/src/screens/editMeme.tsx index 0df1f83..97a0449 100644 --- a/src/screens/editMeme.tsx +++ b/src/screens/editMeme.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { Appbar, Text, useTheme } from 'react-native-paper'; +import { ScrollView } from 'react-native'; +import { Appbar, useTheme } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import { useDimensions } from '../contexts'; -import { ScrollView } from 'react-native'; import styles from '../styles'; const EditMeme = () => { @@ -14,20 +14,17 @@ const EditMeme = () => { <> navigation.goBack()} /> - + - Add Meme - + ]}> ); }; diff --git a/src/screens/editTag.tsx b/src/screens/editTag.tsx index bf152f3..74efc7f 100644 --- a/src/screens/editTag.tsx +++ b/src/screens/editTag.tsx @@ -1,19 +1,12 @@ import React, { useState } from 'react'; import { ScrollView, View } from 'react-native'; -import { - TextInput, - Appbar, - HelperText, - Button, - useTheme, -} from 'react-native-paper'; +import { Appbar, Button, useTheme } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; -import { BSON, UpdateMode } from 'realm'; +import { BSON } from 'realm'; import { useRealm } from '@realm/react'; -import { TagPreview } from '../components'; +import { TagEditor } from '../components'; import styles from '../styles'; -import { generateRandomColor, isValidColor } from '../utilities'; import { useDimensions } from '../contexts'; import { ROUTE, RootStackParamList } from '../types'; import { Tag } from '../database'; @@ -26,62 +19,41 @@ const EditTag = ({ const { orientation } = useDimensions(); const realm = useRealm(); - const tagId = route.params?.id; - const tag = tagId - ? realm.objectForPrimaryKey(Tag, BSON.UUID.createFromHexString(tagId)) - : undefined; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const tag = realm.objectForPrimaryKey( + Tag, + BSON.UUID.createFromHexString(route.params.id), + )!; - const [tagName, setTagName] = useState(tag?.name ?? 'newTag'); - const [tagColor, setTagColor] = useState(tag?.color ?? generateRandomColor()); + const [tagName, setTagName] = useState(tag.name); + const [tagColor, setTagColor] = useState(tag.color); const [validatedTagColor, setValidatedTagColor] = useState(tagColor); const [tagNameError, setTagNameError] = useState(); const [tagColorError, setTagColorError] = useState(); - const handleTagNameChange = (name: string) => { - setTagName(name); - - if (name.length === 0) { - setTagNameError('Tag name cannot be empty'); - } else if (name.includes(' ')) { - setTagNameError('Tag name cannot contain spaces'); - } else { - // eslint-disable-next-line unicorn/no-useless-undefined - setTagNameError(undefined); - } - }; - - const handleTagColorChange = (color: string) => { - setTagColor(color); - - if (isValidColor(color)) { - setValidatedTagColor(color); - // eslint-disable-next-line unicorn/no-useless-undefined - setTagColorError(undefined); - } else { - setTagColorError('Color must be a valid hex or rgb value'); - } - }; - const handleSave = () => { realm.write(() => { - realm.create( - Tag, - { - id: tag?.id, - name: tagName, - color: tagColor, - }, - UpdateMode.Modified, - ); + tag.name = tagName; + tag.color = tagColor; + tag.dateModified = new Date(); }); + navigation.goBack(); }; const handleDelete = () => { realm.write(() => { + for (const meme of tag.memes) { + meme.dateModified = new Date(); + const tags = meme.tags as Set; + tags.delete(tag); + meme.tagsLength = tags.size; + } + realm.delete(tag); }); + navigation.goBack(); }; @@ -89,50 +61,32 @@ const EditTag = ({ <> navigation.goBack()} /> - - {tag && } + + + ]} + nestedScrollEnabled> - - - - {tagNameError} - - handleTagColorChange(generateRandomColor())} - /> - } - /> - - {tagColorError} - diff --git a/src/screens/tags.tsx b/src/screens/tags.tsx index af56bbc..72f07b1 100644 --- a/src/screens/tags.tsx +++ b/src/screens/tags.tsx @@ -2,7 +2,6 @@ import React, { useCallback, useRef, useState } from 'react'; import { StyleSheet, View, - Text, NativeSyntheticEvent, NativeScrollEvent, BackHandler, @@ -13,6 +12,7 @@ import { HelperText, Menu, Searchbar, + Text, TouchableRipple, useTheme, } from 'react-native-paper'; @@ -48,14 +48,13 @@ const tagsStyles = StyleSheet.create({ height: 50, }, tagRow: { - flexWrap: 'wrap', justifyContent: 'space-between', flexDirection: 'row', alignItems: 'center', paddingVertical: 10, paddingHorizontal: 15, }, - tagView: { + tagChip: { flexShrink: 1, maxWidth: '80%', }, @@ -195,6 +194,7 @@ const Tags = () => { tag.id.toHexString()} estimatedItemSize={52} showsVerticalScrollIndicator={false} renderItem={({ item: tag }) => ( @@ -203,9 +203,7 @@ const Tags = () => { navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() }) }> - - - + {tag.memesLength} diff --git a/src/state/home.ts b/src/state/home.ts deleted file mode 100644 index e237670..0000000 --- a/src/state/home.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { MEME_SORT, SORT_DIRECTION, VIEW } from '../types'; -import { MEME_TYPE } from '../database'; - -interface HomeState { - sort: MEME_SORT; - sortDirection: SORT_DIRECTION; - view: VIEW; - favoritesOnly: boolean; - filter: MEME_TYPE | undefined; -} - -const initialState: HomeState = { - sort: MEME_SORT.TITLE, - sortDirection: SORT_DIRECTION.ASCENDING, - view: VIEW.MASONRY, - favoritesOnly: false, - filter: undefined, -}; - -const homeSlice = createSlice({ - name: 'home', - initialState, - reducers: { - setHomeSort: (state, action: PayloadAction) => { - state.sort = action.payload; - }, - setHomeSortDirection: (state, action: PayloadAction) => { - state.sortDirection = action.payload; - }, - toggleHomeSortDirection: state => { - state.sortDirection ^= 1; - }, - setHomeView: (state, action: PayloadAction) => { - state.view = action.payload; - }, - cycleHomeView: state => { - switch (state.view) { - case VIEW.MASONRY: { - state.view = VIEW.GRID; - break; - } - case VIEW.GRID: { - state.view = VIEW.LIST; - break; - } - case VIEW.LIST: { - state.view = VIEW.MASONRY; - break; - } - } - }, - setHomeFavoritesOnly: (state, action: PayloadAction) => { - state.favoritesOnly = action.payload; - }, - toggleHomeFavoritesOnly: state => { - state.favoritesOnly = !state.favoritesOnly; - }, - setHomeFilter: (state, action: PayloadAction) => { - state.filter = action.payload; - }, - }, -}); - -const { - setHomeSort, - setHomeSortDirection, - toggleHomeSortDirection, - setHomeView, - cycleHomeView, - setHomeFavoritesOnly, - toggleHomeFavoritesOnly, - setHomeFilter, -} = homeSlice.actions; - -export { - type HomeState, - setHomeSort, - setHomeSortDirection, - toggleHomeSortDirection, - setHomeView, - cycleHomeView, - setHomeFavoritesOnly, - toggleHomeFavoritesOnly, - setHomeFilter, -}; -export default homeSlice.reducer; diff --git a/src/state/index.ts b/src/state/index.ts index 576c603..66ae9f6 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -11,20 +11,20 @@ import { } from 'redux-persist'; import { createRealmPersistStorage } from '@bankify/redux-persist-realm'; import settingsReducer from './settings'; -import homeReducer from './home'; +import memesReducer from './memes'; import tagsReducer from './tags'; import navigationReducer from './navigation'; const rootReducer = combineReducers({ settings: settingsReducer, - home: homeReducer, + memes: memesReducer, tags: tagsReducer, navigation: navigationReducer, }); interface RootState { settings: ReturnType; - home: ReturnType; + memes: ReturnType; tags: ReturnType; navigation: ReturnType; } @@ -57,16 +57,16 @@ export { validateSettings, } from './settings'; export { - type HomeState, - setHomeSort, - setHomeSortDirection, - toggleHomeSortDirection, - setHomeView, - cycleHomeView, - setHomeFavoritesOnly, - toggleHomeFavoritesOnly, - setHomeFilter, -} from './home'; + type MemesState, + setMemesSort, + setMemesSortDirection, + toggleMemesSortDirection, + setMemesView, + cycleMemesView, + setMemesFavoritesOnly, + toggleMemesFavoritesOnly, + setMemesFilter, +} from './memes'; export { type TagsState, setTagsSort, diff --git a/src/state/memes.ts b/src/state/memes.ts new file mode 100644 index 0000000..d2b5c9a --- /dev/null +++ b/src/state/memes.ts @@ -0,0 +1,83 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { MEME_SORT, SORT_DIRECTION, VIEW } from '../types'; +import { MEME_TYPE } from '../database'; + +interface MemesState { + sort: MEME_SORT; + sortDirection: SORT_DIRECTION; + view: VIEW; + favoritesOnly: boolean; + filter: MEME_TYPE | undefined; +} + +const initialState: MemesState = { + sort: MEME_SORT.TITLE, + sortDirection: SORT_DIRECTION.ASCENDING, + view: VIEW.MASONRY, + favoritesOnly: false, + filter: undefined, +}; + +const memesSlice = createSlice({ + name: 'memes', + initialState, + reducers: { + setMemesSort: (state, action: PayloadAction) => { + state.sort = action.payload; + }, + setMemesSortDirection: (state, action: PayloadAction) => { + state.sortDirection = action.payload; + }, + toggleMemesSortDirection: state => { + state.sortDirection ^= 1; + }, + setMemesView: (state, action: PayloadAction) => { + state.view = action.payload; + }, + cycleMemesView: state => { + switch (state.view) { + case VIEW.MASONRY: { + state.view = VIEW.LIST; + break; + } + case VIEW.LIST: { + state.view = VIEW.MASONRY; + break; + } + } + }, + setMemesFavoritesOnly: (state, action: PayloadAction) => { + state.favoritesOnly = action.payload; + }, + toggleMemesFavoritesOnly: state => { + state.favoritesOnly = !state.favoritesOnly; + }, + setMemesFilter: (state, action: PayloadAction) => { + state.filter = action.payload; + }, + }, +}); + +const { + setMemesSort, + setMemesSortDirection, + toggleMemesSortDirection, + setMemesView, + cycleMemesView, + setMemesFavoritesOnly, + toggleMemesFavoritesOnly, + setMemesFilter, +} = memesSlice.actions; + +export { + type MemesState, + setMemesSort, + setMemesSortDirection, + toggleMemesSortDirection, + setMemesView, + cycleMemesView, + setMemesFavoritesOnly, + toggleMemesFavoritesOnly, + setMemesFilter, +}; +export default memesSlice.reducer; diff --git a/src/styles.tsx b/src/styles.tsx index 82a33bc..37cc5e2 100644 --- a/src/styles.tsx +++ b/src/styles.tsx @@ -38,12 +38,18 @@ const styles = StyleSheet.create({ centerText: { textAlign: 'center', }, + selfCenter: { + alignSelf: 'center', + }, flex: { flex: 1, }, flexGrow: { flexGrow: 1, }, + flexShrink: { + flexShrink: 1, + }, flexRow: { flexDirection: 'row', }, @@ -61,6 +67,9 @@ const styles = StyleSheet.create({ flexRowReverse: { flexDirection: 'row-reverse', }, + flexWrap: { + flexWrap: 'wrap', + }, justifyStart: { justifyContent: 'flex-start', }, diff --git a/src/types/index.ts b/src/types/index.ts index 45cfb75..2e6193e 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,7 +1,7 @@ export { ROUTE, type RootStackParamList } from './route'; export { MEME_SORT, - homeSortQuery, + memesSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION, diff --git a/src/types/route.ts b/src/types/route.ts index 67c6277..c1ee8c1 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -1,21 +1,39 @@ +import { DocumentPickerResponse } from 'react-native-document-picker'; + enum ROUTE { MAIN = 'Main', - HOME = 'Home', + MEMES = 'Memes', TAGS = 'Tags', SETTINGS = 'Settings', + ADD_MEME = 'Add Meme', EDIT_MEME = 'Edit Meme', + ADD_TAG = 'Add Tag', EDIT_TAG = 'Edit Tag', } +interface AddMemeRouteParamsFromFiles { + uri: DocumentPickerResponse[]; +} + +interface EditMemeRouteParams { + id: string; +} + interface EditTagRouteParams { id: string; } interface RootStackParamList { - [key: string]: undefined | EditTagRouteParams; + [key: string]: + | undefined + | AddMemeRouteParamsFromFiles + | EditMemeRouteParams + | EditTagRouteParams; [ROUTE.MAIN]: undefined; - [ROUTE.EDIT_MEME]: undefined; - [ROUTE.EDIT_TAG]: EditTagRouteParams | undefined; + [ROUTE.ADD_MEME]: AddMemeRouteParamsFromFiles; + [ROUTE.EDIT_MEME]: EditMemeRouteParams; + [ROUTE.ADD_TAG]: undefined; + [ROUTE.EDIT_TAG]: EditTagRouteParams; } export { ROUTE, type RootStackParamList }; diff --git a/src/types/sort.ts b/src/types/sort.ts index 1159911..79199bb 100644 --- a/src/types/sort.ts +++ b/src/types/sort.ts @@ -7,7 +7,7 @@ enum MEME_SORT { SIZE = 'Size', } -const homeSortQuery = (sort: MEME_SORT) => { +const memesSortQuery = (sort: MEME_SORT) => { switch (sort) { case MEME_SORT.TITLE: { return 'title'; @@ -36,6 +36,7 @@ enum TAG_SORT { MEMES_LENGTH = 'Items', DATE_CREATED = 'Date Created', DATE_MODIFIED = 'Date Modified', + DATE_USED = 'Last Used', TIMES_USED = 'Times Used', } @@ -59,6 +60,9 @@ const tagSortQuery = (sort: TAG_SORT) => { case TAG_SORT.TIMES_USED: { return 'timesUsed'; } + case TAG_SORT.DATE_USED: { + return 'dateUsed'; + } } }; @@ -67,4 +71,4 @@ enum SORT_DIRECTION { DESCENDING = 1, } -export { MEME_SORT, homeSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION }; +export { MEME_SORT, memesSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION }; diff --git a/src/types/view.ts b/src/types/view.ts index 82051a1..af21a2b 100644 --- a/src/types/view.ts +++ b/src/types/view.ts @@ -1,6 +1,5 @@ enum VIEW { MASONRY = 'Masonry', - GRID = 'Grid', LIST = 'List', } diff --git a/src/utilities/color.ts b/src/utilities/color.ts index 88c0ce7..e97cf5d 100644 --- a/src/utilities/color.ts +++ b/src/utilities/color.ts @@ -11,8 +11,8 @@ const getContrastColor = (hexColor: string) => { const brightness = (r * 299 + g * 587 + b * 114) / 1000; return brightness > 128 - ? lightTheme.colors.onSurface - : darkTheme.colors.onSurface; + ? lightTheme.colors.onBackground + : darkTheme.colors.onBackground; }; const isHexColor = (color: string) => { diff --git a/src/utilities/database.ts b/src/utilities/database.ts new file mode 100644 index 0000000..11b1abf --- /dev/null +++ b/src/utilities/database.ts @@ -0,0 +1,5 @@ +const multipleIdQuery = (ids: string[]) => { + return `id in {${ids.map(id => `uuid(${id})`).join(',')}}`; +}; + +export { multipleIdQuery }; diff --git a/src/utilities/filesystem.ts b/src/utilities/filesystem.ts new file mode 100644 index 0000000..e619627 --- /dev/null +++ b/src/utilities/filesystem.ts @@ -0,0 +1,33 @@ +import { MEME_TYPE } from '../database'; + +const allowedImageMimeTypes = [ + 'image/bmp', + 'image/jpeg', + 'image/png', + 'image/webp', +]; + +const allowedGifMimeTypes = ['image/gif']; + +const allowedMimeTypes = [...allowedImageMimeTypes, ...allowedGifMimeTypes]; + +const getMemeType = (mimeType: string) => { + switch (mimeType) { + case 'image/bmp': + case 'image/jpeg': + case 'image/png': + case 'image/webp': { + return MEME_TYPE.IMAGE; + } + case 'image/gif': { + return MEME_TYPE.GIF; + } + } +}; + +export { + allowedImageMimeTypes, + allowedGifMimeTypes, + allowedMimeTypes, + getMemeType, +}; diff --git a/src/utilities/icon.ts b/src/utilities/icon.ts index 5edfcc1..e55f868 100644 --- a/src/utilities/icon.ts +++ b/src/utilities/icon.ts @@ -17,7 +17,8 @@ const getSortIcon = ( case MEME_SORT.DATE_MODIFIED: case MEME_SORT.DATE_USED: case TAG_SORT.DATE_CREATED: - case TAG_SORT.DATE_MODIFIED: { + case TAG_SORT.DATE_MODIFIED: + case TAG_SORT.DATE_USED: { sortIcon = 'sort-calendar'; break; } @@ -49,9 +50,6 @@ const getViewIcon = (view: VIEW) => { case VIEW.MASONRY: { return 'view-dashboard'; } - case VIEW.GRID: { - return 'view-grid'; - } case VIEW.LIST: { return 'view-list'; } diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 1437e90..3f57914 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -4,8 +4,15 @@ export { isRgbColor, isValidColor, rgbToHex, - generateRandomColor + generateRandomColor, } from './color'; export { packageName, appName, fileProvider, noOp } from './constants'; +export { multipleIdQuery } from './database'; +export { + allowedImageMimeTypes, + allowedGifMimeTypes, + allowedMimeTypes, + getMemeType, +} from './filesystem'; export { isPermissionForPath, clearPermissions } from './permissions'; export { getSortIcon, getViewIcon } from './icon';