diff --git a/android/app/build.gradle b/android/app/build.gradle index 70803aa..f0d8646 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -79,7 +79,7 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion versionCode 1 - versionName "1.0" + versionName "0.0.3" } signingConfigs { debug { diff --git a/package-lock.json b/package-lock.json index e997a8d..4ebc6e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@karaolidis/terminally-online", - "version": "0.0.2", + "version": "0.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@karaolidis/terminally-online", - "version": "0.0.2", + "version": "0.0.3", "hasInstallScript": true, "dependencies": { "@bankify/redux-persist-realm": "^0.1.3", diff --git a/package.json b/package.json index 2f86d06..d376534 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@karaolidis/terminally-online", - "version": "0.0.2", + "version": "0.0.3", "private": true, "scripts": { "postinstall": "patch-package", diff --git a/src/app.tsx b/src/app.tsx index 8a38997..a720ecf 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -54,13 +54,13 @@ const App = () => { }, []); return ( - - - } - persistor={persistor} - onBeforeLift={onBeforeLift}> - + + + + } + persistor={persistor} + onBeforeLift={onBeforeLift}> { )} - - - - + + + + ); }; diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index 8f2deed..9a5d3b1 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -3,10 +3,10 @@ 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 { pickSingle } from 'react-native-document-picker'; +import { pick } from 'react-native-document-picker'; +import { useDeviceOrientation } from '@react-native-community/hooks'; import { ROUTE } from '../types'; import { allowedMimeTypes, noOp } from '../utilities'; -import { useDeviceOrientation } from '@react-native-community/hooks'; const floatingActionButtonStyles = StyleSheet.create({ fab: { @@ -65,9 +65,12 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { onStateChange={({ open }) => setState(open)} onPress={async () => { if (!state) return; - const file = await pickSingle({ type: allowedMimeTypes }).catch(noOp); - if (!file) return; - navigate(ROUTE.ADD_MEME, { file }); + const files = await pick({ + type: allowedMimeTypes, + allowMultiSelection: true, + }).catch(noOp); + if (!files) return; + navigate(ROUTE.ADD_MEME, { files }); }} style={ orientation === 'portrait' diff --git a/src/components/index.ts b/src/components/index.ts index c6aa651..f4db135 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -10,3 +10,4 @@ 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 storageLocationChangeDialog } from './storageLocationChangeDialog'; diff --git a/src/components/memes/memeEditor.tsx b/src/components/memes/memeEditor.tsx index ca3b439..185927f 100644 --- a/src/components/memes/memeEditor.tsx +++ b/src/components/memes/memeEditor.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react'; -import { HelperText, TextInput } from 'react-native-paper'; +import { HelperText, Text, TextInput, useTheme } from 'react-native-paper'; import { Image } from 'react-native'; import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useImageDimensions } from '@react-native-community/hooks/lib/useImageDimensions'; @@ -13,6 +13,10 @@ const memeEditorStyles = { marginBottom: 15, borderRadius: 5, }, + uri: { + marginBottom: 15, + marginHorizontal: 5, + }, memeTagSelector: { marginBottom: 10, }, @@ -23,27 +27,30 @@ const memeEditorStyles = { const MemeEditor = ({ memeUri, - memeUriError, - setMemeUriError, + memeFilename, + memeError, + setMemeError, memeTitle, setMemeTitle, memeTags, setMemeTags, }: { memeUri: string; - memeUriError: Error | undefined; - setMemeUriError: (error: Error | undefined) => void; + memeFilename?: string; + memeError: Error | undefined; + setMemeError: (error: Error | undefined) => void; memeTitle: StringValidationResult; setMemeTitle: (name: StringValidationResult) => void; memeTags: Map; setMemeTags: (tags: Map) => void; }) => { const { width } = useSafeAreaFrame(); + const { colors } = useTheme(); const { dimensions, loading, error } = useImageDimensions({ uri: memeUri }); - useEffect(() => setMemeUriError(error), [error, setMemeUriError]); + useEffect(() => setMemeError(error), [error, setMemeError]); - if (!memeUriError && (loading || !dimensions)) return ; + if (!memeError && (loading || !dimensions)) return ; return ( <> @@ -58,7 +65,7 @@ const MemeEditor = ({ {memeTitle.error} - {memeUriError || !dimensions ? ( + {memeError || !dimensions ? ( )} + + {memeFilename} + { const { height, width } = useSafeAreaFrame(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storageUri = useSelector( + (state: RootState) => state.settings.storageUri, + )!; - const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri }); + const uri = AndroidScoped.appendPath(storageUri, meme.filename); + + const { dimensions, loading, error } = useImageDimensions({ uri }); if (!error && (loading || !dimensions)) { return ( @@ -39,7 +48,7 @@ const MemeViewItem = ({ meme }: { meme: Meme }) => { /> ) : ( width / (height - 128) ? { diff --git a/src/components/memes/memesList/memesGridItem.tsx b/src/components/memes/memesList/memesGridItem.tsx index 30516e6..4f6e1df 100644 --- a/src/components/memes/memesList/memesGridItem.tsx +++ b/src/components/memes/memesList/memesGridItem.tsx @@ -3,6 +3,7 @@ import { Image, TouchableHighlight } from 'react-native'; import { useSelector } from 'react-redux'; import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useImageDimensions } from '@react-native-community/hooks'; +import { AndroidScoped } from 'react-native-file-access'; import { Meme } from '../../../database'; import { RootState } from '../../../state'; import { MemeFail } from '..'; @@ -21,8 +22,14 @@ const MemesGridItem = ({ const gridColumns = useSelector( (state: RootState) => state.settings.gridColumns, ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storageUri = useSelector( + (state: RootState) => state.settings.storageUri, + )!; - const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri }); + const uri = AndroidScoped.appendPath(storageUri, meme.filename); + + const { dimensions, loading, error } = useImageDimensions({ uri }); if (!error && (loading || !dimensions)) return <>; @@ -38,7 +45,7 @@ const MemesGridItem = ({ /> ) : ( void; }) => { const { width } = useSafeAreaFrame(); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storageUri = useSelector( + (state: RootState) => state.settings.storageUri, + )!; - const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri }); + const uri = AndroidScoped.appendPath(storageUri, meme.filename); + + const { dimensions, loading, error } = useImageDimensions({ uri }); if (!error && (loading || !dimensions)) return <>; @@ -53,7 +62,7 @@ const MemesListItem = ({ {error ? ( ) : ( - + )} state.settings.masonryColumns, ); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storageUri = useSelector( + (state: RootState) => state.settings.storageUri, + )!; - const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri }); + const uri = AndroidScoped.appendPath(storageUri, meme.filename); + + const { dimensions, loading, error } = useImageDimensions({ uri }); if (!error && (loading || !dimensions)) return <>; @@ -53,7 +60,7 @@ const MemesMasonryItem = ({ /> ) : ( void; + setSnackbarVisible: (visible: boolean) => void; + setSnackbarMessage: (message: string) => void; +}) => { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const storageUri = useSelector( + (state: RootState) => state.settings.storageUri, + )!; + const dispatch = useDispatch(); + + const [progress, setProgress] = useState(0); + + useEffect(() => { + const selectNewStorageUri = async () => { + const uri = await openDocumentTree(true).catch(noOp); + + if (!uri) { + setVisible(false); + return; + } + + const newStorageUri = uri.uri; + + if (isPermissionForPath(storageUri, newStorageUri)) { + setSnackbarMessage('Folder already selected.'); + setSnackbarVisible(true); + setVisible(false); + return; + } + + const files = await FileSystem.ls(storageUri); + let filesCopied = 0; + + await Promise.all( + files.map(async file => { + const oldUri = AndroidScoped.appendPath(storageUri, file); + const newUri = AndroidScoped.appendPath(newStorageUri, file); + + // You may be wondering, why cp and unlink instead of mv? + // That's because Android is a fuck and does not allow moving across different scoped storage paths. + await FileSystem.cp(oldUri, newUri).catch(noOp); + await FileSystem.unlink(oldUri).catch(noOp); + + filesCopied++; + setProgress(filesCopied / files.length); + }), + ); + + await dispatch(setStorageUri(newStorageUri)); + await clearPermissions([newStorageUri]); + + setVisible(false); + setProgress(0); + }; + + if (visible) void selectNewStorageUri(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [visible]); + + return ( + setVisible(false)} + dismissable={false} + dismissableBackButton={false}> + Change Storage Location + + Copying files. Do not close the app. + + + + ); +}; + +export default StorageLocationChangeDialog; diff --git a/src/database/meme.ts b/src/database/meme.ts index 67df4e6..bca91c8 100644 --- a/src/database/meme.ts +++ b/src/database/meme.ts @@ -21,7 +21,7 @@ const memeTypePlural = { class Meme extends Object { id!: BSON.UUID; type!: MEME_TYPE; - uri!: string; + filename!: string; mimeType!: string; size!: number; title!: string; @@ -39,7 +39,7 @@ class Meme extends Object { properties: { id: { type: 'uuid', default: () => new BSON.UUID() }, type: { type: 'string', indexed: true }, - uri: 'string', + filename: 'string', mimeType: 'string', size: 'int', title: 'string', diff --git a/src/screens/editors/addMeme.tsx b/src/screens/editors/addMeme.tsx index 9682096..7ecce50 100644 --- a/src/screens/editors/addMeme.tsx +++ b/src/screens/editors/addMeme.tsx @@ -9,10 +9,7 @@ import { AndroidScoped, FileSystem } from 'react-native-file-access'; import { useSelector } from 'react-redux'; import { extension } from 'react-native-mime-types'; import { useDeviceOrientation } from '@react-native-community/hooks'; -import { - DocumentPickerResponse, - pickSingle, -} from 'react-native-document-picker'; +import { DocumentPickerResponse, pick } from 'react-native-document-picker'; import { ROUTE, RootStackParamList } from '../../types'; import { Meme, Tag } from '../../database'; import { RootState } from '../../state'; @@ -36,19 +33,24 @@ const AddMeme = ({ (state: RootState) => state.settings.storageUri, )!; - const file = useRef(route.params.file); + const [index, setIndex] = useState(0); + const files = useRef(route.params.files); + const file = useRef(files.current[index]); + const isLastFile = index === files.current.length - 1; const [memeUri, setMemeUri] = useState(file.current.uri); - const [memeUriError, setMemeUriError] = useState(); + const [memeFilename, setMemeFilename] = useState( + file.current.name ?? undefined, + ); + const [memeError, setMemeError] = useState(); const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme')); const [memeIsFavorite, setMemeIsFavorite] = useState(false); const [memeTags, setMemeTags] = useState(new Map()); const [isSaving, setIsSaving] = useState(false); - const [isSavingAndAddingAnother, setIsSavingAndAddingAnother] = - useState(false); + const [isSavingAndAddingMore, setIsSavingAndAddingMore] = useState(false); - const handleSave = useCallback(async () => { + const saveMeme = useCallback(async () => { const uuid = new BSON.UUID(); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const mimeType = file.current.type!; @@ -58,10 +60,10 @@ const AddMeme = ({ const fileExtension = extension(mimeType) as string; if (!fileExtension) goBack(); - const uri = AndroidScoped.appendPath( - storageUri, - `${uuid.toHexString()}-${Math.round(Date.now() / 1000)}.${fileExtension}`, - ); + const filename = `${uuid.toHexString()}-${Math.round( + Date.now() / 1000, + )}.${fileExtension}`; + const uri = AndroidScoped.appendPath(storageUri, filename); await FileSystem.cp(file.current.uri, uri); const { size } = await FileSystem.stat(uri); @@ -70,7 +72,7 @@ const AddMeme = ({ const meme: Meme | undefined = realm.create(Meme.schema.name, { id: uuid, type: memeType, - uri, + filename, mimeType, size, title: memeTitle.parsed, @@ -87,6 +89,47 @@ const AddMeme = ({ }); }, [goBack, memeIsFavorite, memeTags, memeTitle.parsed, realm, storageUri]); + const handleSave = useCallback(async () => { + setIsSaving(true); + await saveMeme(); + setIsSaving(false); + goBack(); + }, [goBack, saveMeme]); + + const handleSaveAndNext = useCallback(async () => { + setIsSaving(true); + await saveMeme(); + setIsSaving(false); + + setIndex(index + 1); + file.current = files.current[index + 1]; + + setMemeUri(file.current.uri); + setMemeFilename(file.current.name ?? undefined); + setMemeTitle(validateMemeTitle('New Meme')); + setMemeIsFavorite(false); + setMemeTags(new Map()); + }, [index, saveMeme]); + + const handleSaveAndAddMore = useCallback(async () => { + setIsSavingAndAddingMore(true); + await saveMeme(); + setIsSavingAndAddingMore(false); + + setIndex(0); + files.current = (await pick({ + type: allowedMimeTypes, + allowMultiSelection: true, + }).catch(goBack)) as DocumentPickerResponse[]; + file.current = files.current[0]; + + setMemeUri(file.current.uri); + setMemeFilename(file.current.name ?? undefined); + setMemeTitle(validateMemeTitle('New Meme')); + setMemeIsFavorite(false); + setMemeTags(new Map()); + }, [goBack, saveMeme]); + return ( <> @@ -98,7 +141,7 @@ const AddMeme = ({ /> { - setIsSavingAndAddingAnother(true); - await handleSave(); - setIsSavingAndAddingAnother(false); - file.current = (await pickSingle({ - type: allowedMimeTypes, - }).catch(goBack)) as DocumentPickerResponse; - setMemeUri(file.current.uri); - setMemeTitle(validateMemeTitle('New Meme')); - setMemeIsFavorite(false); - setMemeTags(new Map()); - }} + onPress={handleSaveAndAddMore} disabled={ !memeTitle.valid || isSaving || - isSavingAndAddingAnother || - !!memeUriError + isSavingAndAddingMore || + !!memeError || + !isLastFile } - loading={isSavingAndAddingAnother} + loading={isSavingAndAddingMore} style={editorStyles.saveAndAddButton}> - Save & Add + Save & Add More diff --git a/src/screens/editors/addTag.tsx b/src/screens/editors/addTag.tsx index 9716b23..6f8c16b 100644 --- a/src/screens/editors/addTag.tsx +++ b/src/screens/editors/addTag.tsx @@ -26,10 +26,9 @@ const AddTag = () => { // Although saving tags is instantaneous, we still want to show a loading // indicator to prevent the user from spamming the save button. - const [isSavingAndAddingAnother, setIsSavingAndAddingAnother] = - useState(false); + const [isSavingAndAddingMore, setIsSavingAndAddingMore] = useState(false); - const handleSave = useCallback(() => { + const saveTag = useCallback(() => { realm.write(() => { realm.create(Tag.schema.name, { name: tagName.parsed, @@ -38,6 +37,19 @@ const AddTag = () => { }); }, [realm, tagColor.parsed, tagName.parsed]); + const handleSave = useCallback(() => { + saveTag(); + goBack(); + }, [goBack, saveTag]); + + const handleSaveAndAddMore = useCallback(() => { + setIsSavingAndAddingMore(true); + saveTag(); + setTimeout(() => setIsSavingAndAddingMore(false), 250); + setTagName(validateTagName('newTag')); + setTagColor(validateColor(generateRandomColor())); + }, [saveTag]); + return ( <> @@ -64,26 +76,17 @@ const AddTag = () => { diff --git a/src/screens/editors/editMeme.tsx b/src/screens/editors/editMeme.tsx index 1ddfa1e..795c2b7 100644 --- a/src/screens/editors/editMeme.tsx +++ b/src/screens/editors/editMeme.tsx @@ -9,6 +9,8 @@ import { BSON } from 'realm'; import { RootStackParamList, ROUTE } from '../../types'; import { pickSingle } from 'react-native-document-picker'; import { AndroidScoped, FileSystem } from 'react-native-file-access'; +import { extension } from 'react-native-mime-types'; +import { useSelector } from 'react-redux'; import { Tag, Meme } from '../../database'; import { StringValidationResult, @@ -21,8 +23,6 @@ import { } from '../../utilities'; import { MemeEditor } from '../../components'; import editorStyles from './editorStyles'; -import { extension } from 'react-native-mime-types'; -import { useSelector } from 'react-redux'; import { RootState } from '../../state'; const EditMeme = ({ @@ -42,10 +42,11 @@ const EditMeme = ({ Meme.schema.name, BSON.UUID.createFromHexString(route.params.id), )!; + const uri = AndroidScoped.appendPath(storageUri, meme.filename); const [hasChanges, setHasChanges] = useState(false); - const [memeUriError, setMemeUriError] = useState(); + const [memeError, setMemeError] = useState(); const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title)); const [memeTags, setMemeTags] = useState( new Map(meme.tags.map(tag => [tag.id.toHexString(), tag])), @@ -64,6 +65,8 @@ const EditMeme = ({ const [isSaving, setIsSaving] = useState(false); const handleSave = useCallback(() => { + setIsSaving(true); + realm.write(() => { meme.tags.forEach(tag => { if (!memeTags.has(tag.id.toHexString())) { @@ -87,7 +90,17 @@ const EditMeme = ({ meme.tagsLength = memeTags.size; meme.dateModified = new Date(); }); - }, [meme, memeTags, memeTitle.parsed, realm]); + + setIsSaving(false); + goBack(); + }, [goBack, meme, memeTags, memeTitle.parsed, realm]); + + const handleDelete = useCallback(async () => { + setIsSaving(true); + await deleteMeme(realm, storageUri, meme); + setIsSaving(false); + goBack(); + }, [goBack, meme, realm, storageUri]); const handleFixUri = useCallback(async () => { const file = await pickSingle({ type: allowedMimeTypes }).catch(noOp); @@ -101,16 +114,16 @@ const EditMeme = ({ const fileExtension = extension(mimeType) as string; if (!fileExtension) return; - const uri = AndroidScoped.appendPath( - storageUri, - `${meme.id.toHexString()}-${Date.now() / 1000}.${fileExtension}`, - ); + const filename = `${meme.id.toHexString()}-${ + Date.now() / 1000 + }.${fileExtension}`; + const newUri = AndroidScoped.appendPath(storageUri, filename); - await FileSystem.cp(file.uri, uri); - const { size } = await FileSystem.stat(uri); + await FileSystem.cp(file.uri, newUri); + const { size } = await FileSystem.stat(newUri); realm.write(() => { - meme.uri = uri; + meme.filename = filename; meme.type = memeType; meme.mimeType = mimeType; meme.size = size; @@ -126,18 +139,10 @@ const EditMeme = ({ icon={meme.isFavorite ? 'heart' : 'heart-outline'} onPress={() => favoriteMeme(realm, meme)} /> - { - setIsSaving(true); - await deleteMeme(realm, meme); - setIsSaving(false); - goBack(); - }} - /> + { - setIsSaving(true); - await deleteMeme(realm, meme); - setIsSaving(false); - goBack(); - }, + onPress: handleDelete, }, ]}> The URI for this meme appears to be broken. This may have been caused by @@ -166,9 +166,10 @@ const EditMeme = ({ ]}> { - setIsSaving(true); - handleSave(); - setIsSaving(false); - goBack(); - }} + onPress={handleSave} disabled={ - !memeTitle.valid || !hasChanges || isSaving || !!memeUriError + !memeTitle.valid || !hasChanges || isSaving || !!memeError } loading={isSaving} style={editorStyles.soloSaveButton}> diff --git a/src/screens/editors/editTag.tsx b/src/screens/editors/editTag.tsx index 4a01055..b9a087c 100644 --- a/src/screens/editors/editTag.tsx +++ b/src/screens/editors/editTag.tsx @@ -51,7 +51,9 @@ const EditTag = ({ tag.color = tagColor.parsed; tag.dateModified = new Date(); }); - }, [realm, tag, tagColor.parsed, tagName.parsed]); + + goBack(); + }, [goBack, realm, tag, tagColor.parsed, tagName.parsed]); return ( <> @@ -87,10 +89,7 @@ const EditTag = ({ + Hide media from gallery @@ -131,17 +156,16 @@ const Settings = () => { /> - - Database - - + + + setSnackbarVisible(false)} diff --git a/src/types/route.ts b/src/types/route.ts index 6815214..46541f4 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -18,7 +18,7 @@ interface MemeViewRouteParams { } interface AddMemeRouteParams { - file: DocumentPickerResponse; + files: DocumentPickerResponse[]; } interface EditMemeRouteParams { diff --git a/src/utilities/index.ts b/src/utilities/index.ts index 5bc0843..65f5dc6 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -23,7 +23,10 @@ export { editMeme, deleteMeme, } from './meme'; -export { isPermissionForPath, clearPermissions } from './permissions'; +export { + isPermissionForPath, + clearPermissions, +} from './permissions'; export { deleteTag } from './tag'; export { type StringValidationResult, diff --git a/src/utilities/meme.ts b/src/utilities/meme.ts index 611b6e1..e0db3d9 100644 --- a/src/utilities/meme.ts +++ b/src/utilities/meme.ts @@ -1,6 +1,5 @@ import { NavigationProp } from '@react-navigation/native'; -import { Dirs, FileSystem } from 'react-native-file-access'; -import { extension } from 'react-native-mime-types'; +import { AndroidScoped, Dirs, FileSystem } from 'react-native-file-access'; import Share from 'react-native-share'; import Clipboard from '@react-native-clipboard/clipboard'; import { Meme } from '../database'; @@ -13,10 +12,20 @@ const favoriteMeme = (realm: Realm, meme: Meme) => { }); }; -const shareMeme = async (meme: Meme) => { - const fileExtension = extension(meme.mimeType) as string; - const cacheUri = `${Dirs.CacheDir}/${meme.id.toHexString()}.${fileExtension}`; - await FileSystem.cp(meme.uri, cacheUri); +const shareMeme = async (realm: Realm, storageUri: string, meme: Meme) => { + const uri = AndroidScoped.appendPath(storageUri, meme.filename); + const cacheUri = `${Dirs.CacheDir}/${meme.filename}`; + + realm.write(() => { + meme.dateUsed = new Date(); + meme.timesUsed += 1; + meme.tags.forEach(tag => { + tag.dateUsed = new Date(); + tag.timesUsed += 1; + }); + }); + + await FileSystem.cp(uri, cacheUri); await Share.open({ url: `file://${cacheUri}`, type: meme.mimeType, @@ -24,10 +33,22 @@ const shareMeme = async (meme: Meme) => { }); }; -const copyMeme = async (meme: Meme) => { - const exists = await FileSystem.exists(meme.uri); +const copyMeme = async (realm: Realm, storageUri: string, meme: Meme) => { + const uri = AndroidScoped.appendPath(storageUri, meme.filename); + + const exists = await FileSystem.exists(uri); if (!exists) throw new Error('File does not exist'); - Clipboard.setURI(meme.uri); + + realm.write(() => { + meme.dateUsed = new Date(); + meme.timesUsed += 1; + meme.tags.forEach(tag => { + tag.dateUsed = new Date(); + tag.timesUsed += 1; + }); + }); + + Clipboard.setURI(uri); }; const editMeme = ( @@ -37,8 +58,10 @@ const editMeme = ( navigation.navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() }); }; -const deleteMeme = async (realm: Realm, meme: Meme) => { - await FileSystem.unlink(meme.uri).catch(noOp); +const deleteMeme = async (realm: Realm, storageUri: string, meme: Meme) => { + const uri = AndroidScoped.appendPath(storageUri, meme.filename); + + await FileSystem.unlink(uri).catch(noOp); realm.write(() => { for (const tag of meme.tags) { diff --git a/src/utilities/permissions.ts b/src/utilities/permissions.ts index 3e30403..413fdd6 100644 --- a/src/utilities/permissions.ts +++ b/src/utilities/permissions.ts @@ -4,7 +4,7 @@ import { } from 'react-native-scoped-storage'; const isPermissionForPath = (permission: string, path: string) => { - return path.startsWith(permission + '/'); + return path.startsWith(permission + '/') || path === permission; }; const clearPermissions = async (excepts: string[] = []) => {