diff --git a/package-lock.json b/package-lock.json index 3241385..590a1a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@realm/react": "^0.5.1", "@reduxjs/toolkit": "^1.9.5", "@shopify/flash-list": "^1.4.3", + "magic-bytes.js": "^1.0.15", "react": "18.2.0", "react-native": "0.72.2", "react-native-document-picker": "^9.0.1", @@ -5617,6 +5618,29 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -5687,7 +5711,7 @@ "node": ">=6.9.0" } }, - "node_modules/buffer": { + "node_modules/bson/node_modules/buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", @@ -11095,6 +11119,11 @@ "yallist": "^3.0.2" } }, + "node_modules/magic-bytes.js": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.0.15.tgz", + "integrity": "sha512-bpRmwbRHqongRhA+mXzbLWjVy7ylqmfMBYaQkSs6pac0z6hBTvsgrH0r4FBYd/UYVJBmS6Rp/O+oCCQVLzKV1g==" + }, "node_modules/magic-string": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", @@ -19947,6 +19976,17 @@ "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } } }, "brace-expansion": { @@ -19991,15 +20031,17 @@ "integrity": "sha512-Uu4OCZa0jouQJCKOk1EmmyqtdWAP5HVLru4lQxTwzJzxT+sJ13lVpEZU/MATDxtHiekWMAL84oQY3Xn1LpJVSg==", "requires": { "buffer": "^5.6.0" - } - }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } } }, "buffer-from": { @@ -23979,6 +24021,11 @@ "yallist": "^3.0.2" } }, + "magic-bytes.js": { + "version": "1.0.15", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.0.15.tgz", + "integrity": "sha512-bpRmwbRHqongRhA+mXzbLWjVy7ylqmfMBYaQkSs6pac0z6hBTvsgrH0r4FBYd/UYVJBmS6Rp/O+oCCQVLzKV1g==" + }, "magic-string": { "version": "0.30.1", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", diff --git a/package.json b/package.json index ae3b2ca..3581801 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@realm/react": "^0.5.1", "@reduxjs/toolkit": "^1.9.5", "@shopify/flash-list": "^1.4.3", + "magic-bytes.js": "^1.0.15", "react": "18.2.0", "react-native": "0.72.2", "react-native-document-picker": "^9.0.1", diff --git a/patches/react-native-file-access+3.0.4.patch b/patches/react-native-file-access+3.0.4.patch new file mode 100644 index 0000000..fbe936b --- /dev/null +++ b/patches/react-native-file-access+3.0.4.patch @@ -0,0 +1,169 @@ +diff --git a/node_modules/react-native-file-access/README.md b/node_modules/react-native-file-access/README.md +index e0540e0..9eb3295 100644 +--- a/node_modules/react-native-file-access/README.md ++++ b/node_modules/react-native-file-access/README.md +@@ -155,6 +155,12 @@ type ManagedFetchResult = { + - Read the content of a file. + - Default encoding of returned string is utf8. + ++`FileSystem.read(path: string, length?: number, position?: number): Promise` ++ ++- Read a file as a byte array. ++ - `length` - Optional number of bytes to read. ++ - `position` - Optional position to start reading from. ++ + ``` + FileSystem.stat(path: string): Promise + +diff --git a/node_modules/react-native-file-access/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt b/node_modules/react-native-file-access/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt +index 248a938..cbd4fe7 100644 +--- a/node_modules/react-native-file-access/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt ++++ b/node_modules/react-native-file-access/android/src/main/java/com/alpha0010/fs/FileAccessModule.kt +@@ -13,6 +13,8 @@ import com.facebook.react.bridge.Promise + import com.facebook.react.bridge.ReactApplicationContext + import com.facebook.react.bridge.ReactMethod + import com.facebook.react.bridge.ReadableMap ++import com.facebook.react.bridge.WritableArray ++import com.facebook.react.bridge.WritableNativeArray + import kotlinx.coroutines.CoroutineScope + import kotlinx.coroutines.Dispatchers + import kotlinx.coroutines.launch +@@ -400,6 +402,27 @@ class FileAccessModule internal constructor(context: ReactApplicationContext) : + } + } + ++ @ReactMethod ++ override fun read(path: String, length: Double, position: Double, promise: Promise) { ++ ioScope.launch { ++ try { ++ val data = openForReading(path).use { ++ it.skip(position.toLong()) ++ val byteArray = ByteArray(length.toInt()) ++ it.read(byteArray) ++ byteArray ++ } ++ val writableArray: WritableArray = WritableNativeArray() ++ for (byte in data) { ++ writableArray.pushInt(byte.toInt() and 0xFF) ++ } ++ promise.resolve(writableArray) ++ } catch (e: Throwable) { ++ promise.reject(e) ++ } ++ } ++ } ++ + @ReactMethod + override fun stat(path: String, promise: Promise) { + ioScope.launch { +diff --git a/node_modules/react-native-file-access/android/src/oldarch/FileAccessSpec.kt b/node_modules/react-native-file-access/android/src/oldarch/FileAccessSpec.kt +index 736324b..c223276 100644 +--- a/node_modules/react-native-file-access/android/src/oldarch/FileAccessSpec.kt ++++ b/node_modules/react-native-file-access/android/src/oldarch/FileAccessSpec.kt +@@ -30,6 +30,7 @@ abstract class FileAccessSpec internal constructor(context: ReactApplicationCont + abstract fun mkdir(path: String, promise: Promise) + abstract fun mv(source: String, target: String, promise: Promise) + abstract fun readFile(path: String, encoding: String, promise: Promise) ++ abstract fun read(path: String, length: Double, position: Double, promise: Promise) + abstract fun stat(path: String, promise: Promise) + abstract fun statDir(path: String, promise: Promise) + abstract fun unlink(path: String, promise: Promise) +diff --git a/node_modules/react-native-file-access/lib/commonjs/index.js b/node_modules/react-native-file-access/lib/commonjs/index.js +index 88f1c2c..20eb70b 100644 +--- a/node_modules/react-native-file-access/lib/commonjs/index.js ++++ b/node_modules/react-native-file-access/lib/commonjs/index.js +@@ -209,6 +209,12 @@ const FileSystem = { + let encoding = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'utf8'; + return FileAccessNative.readFile(path, encoding); + }, ++ /** ++ * Read the content of a file as a byte array. ++ */ ++ read(path, length, position) { ++ return FileAccessNative.read(path, length, position); ++ }, + /** + * Read file metadata. + */ +diff --git a/node_modules/react-native-file-access/lib/module/index.js b/node_modules/react-native-file-access/lib/module/index.js +index 0581920..22c37d3 100644 +--- a/node_modules/react-native-file-access/lib/module/index.js ++++ b/node_modules/react-native-file-access/lib/module/index.js +@@ -198,6 +198,12 @@ export const FileSystem = { + let encoding = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'utf8'; + return FileAccessNative.readFile(path, encoding); + }, ++ /** ++ * Read the content of a file as a byte array. ++ */ ++ read(path, length, position) { ++ return FileAccessNative.read(path, length, position); ++ }, + /** + * Read file metadata. + */ +diff --git a/node_modules/react-native-file-access/lib/typescript/NativeFileAccess.d.ts b/node_modules/react-native-file-access/lib/typescript/NativeFileAccess.d.ts +index 0c58874..ed84081 100644 +--- a/node_modules/react-native-file-access/lib/typescript/NativeFileAccess.d.ts ++++ b/node_modules/react-native-file-access/lib/typescript/NativeFileAccess.d.ts +@@ -63,6 +63,7 @@ export interface Spec extends TurboModule { + mkdir(path: string): Promise; + mv(source: string, target: string): Promise; + readFile(path: string, encoding: string): Promise; ++ read(path: string, length: number, position: number): Promise; + stat(path: string): Promise; + statDir(path: string): Promise; + unlink(path: string): Promise; +diff --git a/node_modules/react-native-file-access/lib/typescript/index.d.ts b/node_modules/react-native-file-access/lib/typescript/index.d.ts +index 5433d53..12854ca 100644 +--- a/node_modules/react-native-file-access/lib/typescript/index.d.ts ++++ b/node_modules/react-native-file-access/lib/typescript/index.d.ts +@@ -84,6 +84,10 @@ export declare const FileSystem: { + * Read the content of a file. + */ + readFile(path: string, encoding?: Encoding): Promise; ++ /** ++ * Read the content of a file as a byte array. ++ */ ++ read(path: string, length?: number, position?: number): Promise; + /** + * Read file metadata. + */ +diff --git a/node_modules/react-native-file-access/src/NativeFileAccess.ts b/node_modules/react-native-file-access/src/NativeFileAccess.ts +index b3a7baa..affd76c 100644 +--- a/node_modules/react-native-file-access/src/NativeFileAccess.ts ++++ b/node_modules/react-native-file-access/src/NativeFileAccess.ts +@@ -71,6 +71,7 @@ export interface Spec extends TurboModule { + mkdir(path: string): Promise; + mv(source: string, target: string): Promise; + readFile(path: string, encoding: string): Promise; ++ read(path: string, length: number, position: number): Promise; + stat(path: string): Promise; + statDir(path: string): Promise; + unlink(path: string): Promise; +diff --git a/node_modules/react-native-file-access/src/index.ts b/node_modules/react-native-file-access/src/index.ts +index 1b38d45..5c9fd5e 100644 +--- a/node_modules/react-native-file-access/src/index.ts ++++ b/node_modules/react-native-file-access/src/index.ts +@@ -31,7 +31,6 @@ const LINKING_ERROR = + '- You rebuilt the app after installing the package\n' + + '- You are not using Expo Go\n'; + +-// @ts-expect-error + const isTurboModuleEnabled = global.__turboModuleProxy != null; + + const FileAccessModule = isTurboModuleEnabled +@@ -275,6 +274,13 @@ export const FileSystem = { + return FileAccessNative.readFile(path, encoding); + }, + ++ /** ++ * Read the content of a file as a byte array. ++ */ ++ read(path: string, length: number = 1000, position: number = 0) { ++ return FileAccessNative.read(path, length, position); ++ }, ++ + /** + * Read file metadata. + */ diff --git a/src/components/floatingActionButton.tsx b/src/components/floatingActionButton.tsx index 0e0d748..5627a86 100644 --- a/src/components/floatingActionButton.tsx +++ b/src/components/floatingActionButton.tsx @@ -9,14 +9,9 @@ import { useKeyboard, } from '@react-native-community/hooks'; import Clipboard from '@react-native-clipboard/clipboard'; -import { documentPickerResponseToAddMemeFile, ROUTE } from '../types'; -import { - allowedMimeTypes, - getFilenameFromUri, - guessMimeType, - noOp, -} from '../utilities'; import { useDispatch } from 'react-redux'; +import { documentPickerResponseToAddMemeFile, ROUTE } from '../types'; +import { allowedMimeTypes, getFilenameFromUri, noOp } from '../utilities'; import { setSnackbarMessage } from '../state'; const floatingActionButtonStyles = StyleSheet.create({ @@ -59,17 +54,11 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => { dispatch(setSnackbarMessage('Clipboard does not contain a URI.')); return; } - const mimeType = guessMimeType(uri); - if (!mimeType) { - dispatch(setSnackbarMessage('Unsupported MIME type.')); - return; - } navigate(ROUTE.ADD_MEME, { files: [ { uri: uri, filename: getFilenameFromUri(uri), - type: mimeType, }, ], }); diff --git a/src/components/memes/memeEditor.tsx b/src/components/memes/memeEditor.tsx index 0d14879..a3cff6b 100644 --- a/src/components/memes/memeEditor.tsx +++ b/src/components/memes/memeEditor.tsx @@ -1,11 +1,11 @@ -import React, { useEffect } from 'react'; +import React, { useState } from 'react'; 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'; -import { MemeFail, MemeTagSelector, LoadingView } from '..'; +import { MemeFail, MemeTagSelector } from '..'; import { Tag } from '../../database'; import { StringValidationResult, validateMemeTitle } from '../../utilities'; +import { Dimensions } from '../../types'; const memeEditorStyles = { image: { @@ -46,10 +46,7 @@ const MemeEditor = ({ const { width } = useSafeAreaFrame(); const { colors } = useTheme(); - const { dimensions, loading, error } = useImageDimensions({ uri: memeUri }); - useEffect(() => setMemeError(error), [error, setMemeError]); - - if (!memeError && (loading || !dimensions)) return ; + const [dimensions, setDimensions] = useState(); return ( <> @@ -64,7 +61,7 @@ const MemeEditor = ({ {memeTitle.error} - {memeError || !dimensions ? ( + {memeError ? ( { + setDimensions({ + width: event.nativeEvent.source.width, + height: event.nativeEvent.source.height, + }); + }} + onError={event => setMemeError(event.nativeEvent.error as Error)} /> )} { id!: BSON.UUID; - type!: MEME_TYPE; + memeType!: MEME_TYPE; filename!: string; mimeType!: string; size!: number; @@ -38,7 +38,7 @@ class Meme extends Object { primaryKey: 'id', properties: { id: { type: 'uuid', default: () => new BSON.UUID() }, - type: { type: 'string', indexed: true }, + memeType: { type: 'string', indexed: true }, filename: 'string', mimeType: 'string', size: 'int', diff --git a/src/screens/editors/addMeme.tsx b/src/screens/editors/addMeme.tsx index 9697c66..c5201ff 100644 --- a/src/screens/editors/addMeme.tsx +++ b/src/screens/editors/addMeme.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { Appbar, Banner, Button, useTheme } from 'react-native-paper'; import { useNavigation } from '@react-navigation/native'; import { ScrollView, View } from 'react-native'; @@ -19,7 +19,8 @@ import { Meme, Tag } from '../../database'; import { RootState } from '../../state'; import { allowedMimeTypes, - getMemeType, + getMemeTypeFromMimeType, + guessMimeType, validateMemeTitle, } from '../../utilities'; import { MemeEditor } from '../../components'; @@ -42,25 +43,46 @@ const AddMeme = ({ const file = useRef(files.current[index]); const isLastFile = index === files.current.length - 1; - const [memeUri, setMemeUri] = useState(file.current.uri); - const [memeFilename, setMemeFilename] = useState(file.current.filename); + const [memeLoading, setMemeLoading] = useState(true); 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 [isSavingAndAddingMore, setIsSavingAndAddingMore] = useState(false); - const saveMeme = useCallback(async () => { - const uuid = new BSON.UUID(); - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mimeType = file.current.type!; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const memeType = getMemeType(mimeType)!; + const [memeUri, setMemeUri] = useState(file.current.uri); + const [memeFilename, setMemeFilename] = useState(file.current.filename); + const [memeMimeType, setMemeMimeType] = useState(); + const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme')); + const [memeIsFavorite, setMemeIsFavorite] = useState(false); + const [memeTags, setMemeTags] = useState(new Map()); - const fileExtension = extension(mimeType) as string; - if (!fileExtension) goBack(); + useEffect(() => { + const loadMeme = async () => { + const mimeType = await guessMimeType(file.current.uri); + if (!mimeType) { + setMemeError( + new Error('Could not determine MIME type or file is not supported.'), + ); + return; + } + + setMemeMimeType(mimeType); + }; + + setMemeLoading(true); + void loadMeme(); + setMemeLoading(false); + }, [file]); + + const saveMeme = useCallback(async () => { + if (!memeMimeType) return; + const uuid = new BSON.UUID(); + + const memeType = getMemeTypeFromMimeType(memeMimeType); + if (!memeType) return; + + const fileExtension = extension(memeMimeType); + if (!fileExtension) return; const filename = `${uuid.toHexString()}-${Math.round( Date.now() / 1000, @@ -73,9 +95,9 @@ const AddMeme = ({ realm.write(() => { const meme: Meme | undefined = realm.create(Meme.schema.name, { id: uuid, - type: memeType, + memeType, filename, - mimeType, + mimeType: memeMimeType, size, title: memeTitle.parsed, isFavorite: memeIsFavorite, @@ -89,7 +111,14 @@ const AddMeme = ({ tag.memesLength = tag.memes.length; }); }); - }, [goBack, memeIsFavorite, memeTags, memeTitle.parsed, realm, storageUri]); + }, [ + memeIsFavorite, + memeMimeType, + memeTags, + memeTitle.parsed, + realm, + storageUri, + ]); const handleSave = useCallback(async () => { setIsSaving(true); @@ -97,6 +126,14 @@ const AddMeme = ({ goBack(); }, [goBack, saveMeme]); + const resetState = useCallback(() => { + setMemeUri(file.current.uri); + setMemeFilename(file.current.filename); + setMemeTitle(validateMemeTitle('New Meme')); + setMemeIsFavorite(false); + setMemeTags(new Map()); + }, []); + const handleSaveAndNext = useCallback(async () => { setIsSaving(true); await saveMeme(); @@ -105,12 +142,8 @@ const AddMeme = ({ setIndex(index + 1); file.current = files.current[index + 1]; - setMemeUri(file.current.uri); - setMemeFilename(file.current.filename); - setMemeTitle(validateMemeTitle('New Meme')); - setMemeIsFavorite(false); - setMemeTags(new Map()); - }, [index, saveMeme]); + resetState(); + }, [index, resetState, saveMeme]); const handleSaveAndAddMore = useCallback(async () => { setIsSavingAndAddingMore(true); @@ -126,12 +159,8 @@ const AddMeme = ({ files.current = documentPickerResponseToAddMemeFile(response); file.current = files.current[0]; - setMemeUri(file.current.uri); - setMemeFilename(file.current.filename); - setMemeTitle(validateMemeTitle('New Meme')); - setMemeIsFavorite(false); - setMemeTags(new Map()); - }, [goBack, saveMeme]); + resetState(); + }, [goBack, resetState, saveMeme]); return ( <> @@ -180,10 +209,11 @@ const AddMeme = ({ icon="plus" onPress={handleSaveAndAddMore} disabled={ - !memeTitle.valid || + memeLoading || + !!memeError || isSaving || isSavingAndAddingMore || - !!memeError || + !memeTitle.valid || !isLastFile } loading={isSavingAndAddingMore} @@ -195,10 +225,11 @@ const AddMeme = ({ icon="floppy" onPress={isLastFile ? handleSave : handleSaveAndNext} disabled={ - !memeTitle.valid || + memeLoading || + !!memeError || isSaving || isSavingAndAddingMore || - !!memeError + !memeTitle.valid } loading={isSaving} style={editorStyles.saveButton}> diff --git a/src/screens/editors/editMeme.tsx b/src/screens/editors/editMeme.tsx index a8def35..2628b28 100644 --- a/src/screens/editors/editMeme.tsx +++ b/src/screens/editors/editMeme.tsx @@ -17,7 +17,8 @@ import { allowedMimeTypes, deleteMeme, favoriteMeme, - getMemeType, + getMemeTypeFromMimeType, + guessMimeType, noOp, validateMemeTitle, } from '../../utilities'; @@ -47,6 +48,7 @@ const EditMeme = ({ const [hasChanges, setHasChanges] = useState(false); 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])), @@ -104,10 +106,16 @@ const EditMeme = ({ const file = await pickSingle({ type: allowedMimeTypes }).catch(noOp); if (!file) return; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const mimeType = file.type!; - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const memeType = getMemeType(mimeType)!; + const mimeType = await guessMimeType(file.uri, file.type); + if (!mimeType) { + setMemeError( + new Error('Could not determine MIME type or file is not supported.'), + ); + return; + } + + const memeType = getMemeTypeFromMimeType(mimeType); + if (!memeType) return; const fileExtension = extension(mimeType) as string; if (!fileExtension) return; @@ -122,7 +130,7 @@ const EditMeme = ({ realm.write(() => { meme.filename = filename; - meme.type = memeType; + meme.memeType = memeType; meme.mimeType = mimeType; meme.size = size; }); diff --git a/src/screens/memes.tsx b/src/screens/memes.tsx index f574bf2..73c3ae8 100644 --- a/src/screens/memes.tsx +++ b/src/screens/memes.tsx @@ -80,7 +80,7 @@ const Memes = () => { .join(' OR '); if (favoritesOnly) collection = collection.filtered('isFavorite == true'); - if (filter) collection = collection.filtered('type == $0', filter); + if (filter) collection = collection.filtered('memeType == $0', filter); if (tags && tagsQuery) { collection = collection.filtered(tagsQuery, ...tags); } diff --git a/src/types/route.ts b/src/types/route.ts index fcb64e6..5db0819 100644 --- a/src/types/route.ts +++ b/src/types/route.ts @@ -1,5 +1,5 @@ import { DocumentPickerResponse } from 'react-native-document-picker'; -import { getFilenameFromUri, guessMimeType } from '../utilities'; +import { getFilenameFromUri } from '../utilities'; import { SharedItem } from './share'; enum ROUTE { @@ -34,7 +34,7 @@ const documentPickerResponseToAddMemeFile = ( return { uri, filename: name ?? getFilenameFromUri(uri), - type: type ?? guessMimeType(uri), + type: type ?? undefined, }; }); }; @@ -55,7 +55,7 @@ const sharedItemToAddMemeFile = (item: SharedItem): AddMemeFile[] => { return data.map(uri => ({ uri, filename: getFilenameFromUri(uri), - type: guessMimeType(uri), + type: mimeType, })); }; diff --git a/src/utilities/filesystem.ts b/src/utilities/filesystem.ts index e62ee33..24e2937 100644 --- a/src/utilities/filesystem.ts +++ b/src/utilities/filesystem.ts @@ -1,3 +1,5 @@ +import { FileSystem } from 'react-native-file-access'; +import filetypemime from 'magic-bytes.js'; import { MEME_TYPE } from '../database'; const allowedImageMimeTypes = [ @@ -12,7 +14,7 @@ const allowedGifMimeTypes = ['image/gif']; const allowedMimeTypes = [...allowedImageMimeTypes, ...allowedGifMimeTypes]; -const getMemeType = (mimeType: string): MEME_TYPE | undefined => { +const getMemeTypeFromMimeType = (mimeType: string): MEME_TYPE | undefined => { switch (mimeType) { case 'image/bmp': case 'image/jpg': @@ -27,7 +29,7 @@ const getMemeType = (mimeType: string): MEME_TYPE | undefined => { } }; -const guessMimeType = (filename: string): string | undefined => { +const guessMimeTypeFromExtension = (filename: string): string | undefined => { const extension = filename.split('.').pop()?.toLowerCase(); switch (extension) { case 'bmp': { @@ -49,10 +51,34 @@ const guessMimeType = (filename: string): string | undefined => { } }; +const guessMimeTypeFromMagicBytes = async ( + uri: string, +): Promise => { + const fileContent = await FileSystem.read(uri, 100); + const possibleMimeTypes = filetypemime(fileContent); + if (possibleMimeTypes.length === 0) return undefined; + return possibleMimeTypes[0].mime; +}; + +const guessMimeType = async ( + uri: string, + hint?: string | null, +): Promise => { + if (hint) { + if (allowedMimeTypes.includes(hint)) return hint; + if (!hint.startsWith('image/')) return undefined; + } + + const guessedMimeType = guessMimeTypeFromExtension(uri); + if (guessedMimeType) return guessedMimeType; + + return await guessMimeTypeFromMagicBytes(uri); +}; + export { allowedImageMimeTypes, allowedGifMimeTypes, allowedMimeTypes, - getMemeType, + getMemeTypeFromMimeType, guessMimeType, }; diff --git a/src/utilities/index.ts b/src/utilities/index.ts index c02b2c4..26ad6a2 100644 --- a/src/utilities/index.ts +++ b/src/utilities/index.ts @@ -14,7 +14,7 @@ export { allowedImageMimeTypes, allowedGifMimeTypes, allowedMimeTypes, - getMemeType, + getMemeTypeFromMimeType, guessMimeType, } from './filesystem'; export { getSortIcon, getViewIcon } from './icon';