Add video support

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-08-01 14:35:10 +03:00
parent f1f969c8ea
commit b83407f1f4
13 changed files with 358 additions and 158 deletions

View File

@@ -28,6 +28,21 @@
<data android:mimeType="image/png" />
<data android:mimeType="image/webp" />
<data android:mimeType="image/gif" />
<data android:mimeType="video/av01" />
<data android:mimeType="video/3gpp" />
<data android:mimeType="video/avc" />
<data android:mimeType="video/hevc" />
<data android:mimeType="video/x-matroska" />
<data android:mimeType="video/mp2t" />
<data android:mimeType="video/mp4" />
<data android:mimeType="video/mp42" />
<data android:mimeType="video/mp43" />
<data android:mimeType="video/mp4v-es" />
<data android:mimeType="video/mpeg" />
<data android:mimeType="video/mpeg2" />
<data android:mimeType="video/x-vnd.on2.vp8" />
<data android:mimeType="video/x-vnd.on2.vp9" />
<data android:mimeType="video/webm" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
@@ -37,6 +52,21 @@
<data android:mimeType="image/png" />
<data android:mimeType="image/webp" />
<data android:mimeType="image/gif" />
<data android:mimeType="video/av01" />
<data android:mimeType="video/3gpp" />
<data android:mimeType="video/avc" />
<data android:mimeType="video/hevc" />
<data android:mimeType="video/x-matroska" />
<data android:mimeType="video/mp2t" />
<data android:mimeType="video/mp4" />
<data android:mimeType="video/mp42" />
<data android:mimeType="video/mp43" />
<data android:mimeType="video/mp4v-es" />
<data android:mimeType="video/mpeg" />
<data android:mimeType="video/mpeg2" />
<data android:mimeType="video/x-vnd.on2.vp8" />
<data android:mimeType="video/x-vnd.on2.vp9" />
<data android:mimeType="video/webm" />
</intent-filter>
</activity>
</application>

View File

@@ -1,13 +1,20 @@
import React, { useState } from 'react';
import React, { useMemo } from 'react';
import { HelperText, Text, TextInput, useTheme } from 'react-native-paper';
import { Image, LayoutAnimation } from 'react-native';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { LoadingView, MemeFail, MemeTagSelector } from '..';
import { getFilenameFromUri, validateMemeTitle } from '../../utilities';
import { Dimensions, StagingMeme } from '../../types';
import {
getFilenameFromUri,
getMemeTypeFromMimeType,
validateMemeTitle,
} from '../../utilities';
import { StagingMeme } from '../../types';
import { useMemeDimensions } from '../../hooks';
import { MEME_TYPE } from '../../database';
import Video from 'react-native-video';
const memeEditorStyles = {
image: {
media: {
marginBottom: 15,
borderRadius: 5,
},
@@ -26,6 +33,7 @@ const memeEditorStyles = {
const MemeEditor = ({
uri,
mimeType,
loading,
setLoading,
error,
setError,
@@ -44,7 +52,59 @@ const MemeEditor = ({
const { width } = useSafeAreaFrame();
const { colors } = useTheme();
const [dimensions, setDimensions] = useState<Dimensions>();
const { dimensions } = useMemeDimensions(
uri,
mimeType,
useMemo(
() => () => {
setLoading(false);
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
},
[setLoading],
),
useMemo(() => (errorIn: Error) => setError(errorIn), [setError]),
);
const mediaComponent = useMemo(() => {
if (!mimeType || !dimensions) return <></>;
const dimensionStyles = {
width: width * 0.92,
height: Math.max(
Math.min((width * 0.92) / dimensions.aspectRatio, 500),
100,
),
};
const memeType = getMemeTypeFromMimeType(mimeType);
if (!memeType) return <></>;
switch (memeType) {
case MEME_TYPE.IMAGE:
case MEME_TYPE.GIF: {
return (
<Image
source={{ uri }}
style={[memeEditorStyles.media, dimensionStyles]}
resizeMode="contain"
/>
);
}
case MEME_TYPE.VIDEO: {
return (
<Video
source={{ uri }}
style={[memeEditorStyles.media, dimensionStyles]}
resizeMode="contain"
controls
/>
);
}
default: {
return <></>;
}
}
}, [dimensions, mimeType, uri, width]);
if (!uri || !mimeType || !staging) return <LoadingView />;
@@ -70,51 +130,15 @@ const MemeEditor = ({
width: width * 0.92,
height: width * 0.92,
},
memeEditorStyles.image,
memeEditorStyles.media,
]}
iconSize={50}
/>
) : // eslint-disable-next-line unicorn/no-nested-ternary
loading || !dimensions ? (
<></>
) : (
<Image
source={{ uri }}
style={[
dimensions
? {
width: width * 0.92,
height: Math.max(
Math.min(
((width * 0.92) / dimensions.width) * dimensions.height,
500,
),
100,
),
}
: // eslint-disable-next-line react-native/no-inline-styles
{
width: width * 0.92,
height: 1,
},
memeEditorStyles.image,
]}
resizeMode="contain"
onLoad={event => {
setDimensions({
width: event.nativeEvent.source.width,
height: event.nativeEvent.source.height,
});
setLoading(false);
LayoutAnimation.configureNext(
LayoutAnimation.Presets.easeInEaseOut,
);
}}
onError={() =>
setError(
new Error(
'The URI for this meme appears to be broken. This may have been caused by the file being moved or deleted.',
),
)
}
/>
mediaComponent
)}
<Text
variant="bodySmall"

View File

@@ -1,12 +1,13 @@
import React from 'react';
import React, { useMemo } from 'react';
import { StyleSheet, View } from 'react-native';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { useImageDimensions } from '@react-native-community/hooks';
import { AndroidScoped } from 'react-native-file-access';
import { useSelector } from 'react-redux';
import { Meme } from '../../database';
import { MEME_TYPE, Meme } from '../../database';
import { RootState } from '../../state';
import { AnimatedImage, LoadingView, MemeFail } from '..';
import { useMemeDimensions } from '../../hooks';
import Video from 'react-native-video';
const memeViewItemStyles = StyleSheet.create({
view: {
@@ -24,7 +25,34 @@ const MemeViewItem = ({ meme }: { meme: Meme }) => {
const uri = AndroidScoped.appendPath(storageUri, meme.filename);
const { dimensions, loading, error } = useImageDimensions({ uri });
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
const mediaComponent = useMemo(() => {
if (!dimensions) return <></>;
const dimensionStyles =
dimensions.aspectRatio > width / (height - 128)
? {
width,
height: width / (dimensions.width / dimensions.height),
}
: {
width: (height - 128) * (dimensions.width / dimensions.height),
height: height - 128,
};
switch (meme.memeType) {
case MEME_TYPE.IMAGE:
case MEME_TYPE.GIF: {
return <AnimatedImage source={{ uri }} style={dimensionStyles} />;
}
default: {
return (
<Video source={{ uri }} style={dimensionStyles} paused controls />
);
}
}
}, [dimensions, height, meme.memeType, uri, width]);
if (!error && (loading || !dimensions)) {
return <LoadingView style={{ width, height }} />;
@@ -41,21 +69,7 @@ const MemeViewItem = ({ meme }: { meme: Meme }) => {
iconSize={50}
/>
) : (
<AnimatedImage
source={{ uri }}
style={
dimensions.aspectRatio > width / (height - 128)
? {
width,
height: width / (dimensions.width / dimensions.height),
}
: {
width:
(height - 128) * (dimensions.width / dimensions.height),
height: height - 128,
}
}
/>
mediaComponent
)}
</View>
);

View File

@@ -1,13 +1,13 @@
import React from 'react';
import React, { useMemo } from 'react';
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 { MEME_TYPE, Meme } from '../../../database';
import { RootState } from '../../../state';
import { MemeFail } from '..';
import { getFontAwesome5IconSize } from '../../../utilities';
import { useMemeDimensions } from '../../../hooks';
const MemesGridItem = ({
meme,
@@ -29,7 +29,32 @@ const MemesGridItem = ({
const uri = AndroidScoped.appendPath(storageUri, meme.filename);
const { dimensions, loading, error } = useImageDimensions({ uri });
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
const itemWidth = (width * 0.92 - 5) / gridColumns;
const mediaComponent = useMemo(() => {
switch (meme.memeType) {
case MEME_TYPE.IMAGE:
case MEME_TYPE.GIF:
case MEME_TYPE.VIDEO: {
return (
<Image
source={{ uri }}
style={[
{
width: itemWidth,
height: itemWidth,
},
]}
/>
);
}
default: {
return <></>;
}
}
}, [itemWidth, meme.memeType, uri]);
if (!error && (loading || !dimensions)) return <></>;
@@ -44,15 +69,7 @@ const MemesGridItem = ({
iconSize={getFontAwesome5IconSize(gridColumns)}
/>
) : (
<Image
source={{ uri }}
style={[
{
width: (width * 0.92 - 5) / gridColumns,
height: (width * 0.92 - 5) / gridColumns,
},
]}
/>
mediaComponent
)}
</TouchableHighlight>
);

View File

@@ -1,13 +1,13 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Image, StyleSheet, View } from 'react-native';
import { Text, TouchableRipple } from 'react-native-paper';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { useImageDimensions } from '@react-native-community/hooks';
import { AndroidScoped } from 'react-native-file-access';
import { useSelector } from 'react-redux';
import { Meme } from '../../../database';
import { MEME_TYPE, Meme } from '../../../database';
import { MemeFail } from '..';
import { RootState } from '../../../state';
import { useMemeDimensions } from '../../../hooks';
const memesListItemStyles = StyleSheet.create({
view: {
@@ -50,7 +50,20 @@ const MemesListItem = ({
const uri = AndroidScoped.appendPath(storageUri, meme.filename);
const { dimensions, loading, error } = useImageDimensions({ uri });
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
const mediaComponent = useMemo(() => {
switch (meme.memeType) {
case MEME_TYPE.IMAGE:
case MEME_TYPE.GIF:
case MEME_TYPE.VIDEO: {
return <Image source={{ uri }} style={[memesListItemStyles.image]} />;
}
default: {
return <></>;
}
}
}, [meme.memeType, uri]);
if (!error && (loading || !dimensions)) return <></>;
@@ -62,7 +75,7 @@ const MemesListItem = ({
{error ? (
<MemeFail style={memesListItemStyles.image} />
) : (
<Image source={{ uri }} style={memesListItemStyles.image} />
mediaComponent
)}
<View
style={[

View File

@@ -1,13 +1,13 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Image, StyleSheet, TouchableHighlight } from 'react-native';
import { useSelector } from 'react-redux';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { AndroidScoped } from 'react-native-file-access';
import { useImageDimensions } from '@react-native-community/hooks';
import { Meme } from '../../../database';
import { MEME_TYPE, Meme } from '../../../database';
import { RootState } from '../../../state';
import { MemeFail } from '..';
import { getFontAwesome5IconSize } from '../../../utilities';
import { useMemeDimensions } from '../../../hooks';
const memeMasonryItemStyles = StyleSheet.create({
view: {
@@ -39,7 +39,35 @@ const MemesMasonryItem = ({
const uri = AndroidScoped.appendPath(storageUri, meme.filename);
const { dimensions, loading, error } = useImageDimensions({ uri });
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
const itemWidth = (width * 0.92 - 5) / masonryColumns - 5;
const itemHeight =
((width * 0.92) / masonryColumns - 5) / (dimensions?.aspectRatio ?? 1);
const mediaComponent = useMemo(() => {
switch (meme.memeType) {
case MEME_TYPE.IMAGE:
case MEME_TYPE.GIF:
case MEME_TYPE.VIDEO: {
return (
<Image
source={{ uri }}
style={[
memeMasonryItemStyles.image,
{
width: itemWidth,
height: itemHeight,
},
]}
/>
);
}
default: {
return <></>;
}
}
}, [itemHeight, itemWidth, meme.memeType, uri]);
if (!error && (loading || !dimensions)) return <></>;
@@ -51,25 +79,12 @@ const MemesMasonryItem = ({
<MemeFail
style={[
memeMasonryItemStyles.image,
{
width: (width * 0.92) / masonryColumns - 5,
height: (width * 0.92) / masonryColumns - 5,
},
{ width: itemWidth, height: itemHeight },
]}
iconSize={getFontAwesome5IconSize(masonryColumns)}
/>
) : (
<Image
source={{ uri }}
style={[
memeMasonryItemStyles.image,
{
width: (width * 0.92) / masonryColumns - 5,
height:
((width * 0.92) / masonryColumns - 5) / dimensions.aspectRatio,
},
]}
/>
mediaComponent
)}
</TouchableHighlight>
);

1
src/hooks/index.ts Normal file
View File

@@ -0,0 +1 @@
export { default as useMemeDimensions } from './useMemeDimensions';

View File

@@ -0,0 +1,56 @@
import { useEffect, useState } from 'react';
import { Image } from 'react-native';
import { Dimensions } from '../types';
const useMediaDimensions = (
uri?: string,
mimeType?: string,
onLoad?: (width: number, height: number, aspectRatio: number) => void,
onError?: (error: Error) => void,
) => {
const [dimensions, setDimensions] = useState<Dimensions | undefined>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | undefined>();
useEffect(() => {
const getDimensions = () => {
if (!uri || !mimeType) return;
const mimeStart = mimeType.split('/')[0];
switch (mimeStart) {
case 'image':
case 'video': {
Image.getSize(
uri,
(width, height) => {
const aspectRatio = width / height;
setDimensions({ width, height, aspectRatio });
setLoading(false);
onLoad?.(width, height, aspectRatio);
},
(errorIn: string) => {
const errorOut = new Error(errorIn);
setError(errorOut);
setLoading(false);
onError?.(errorOut);
},
);
break;
}
default: {
const errorOut = new Error(`Unknown mime type: ${mimeType}`);
setError(errorOut);
setLoading(false);
onError?.(errorOut);
}
}
};
getDimensions();
}, [mimeType, onError, onLoad, uri]);
return { dimensions, loading, error };
};
export default useMediaDimensions;

View File

@@ -6,7 +6,13 @@ import { FlashList } from '@shopify/flash-list';
import { Appbar, Portal, Snackbar } from 'react-native-paper';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { NavigationProp, useNavigation } from '@react-navigation/native';
import { RootStackParamList, ROUTE } from '../types';
import { useSelector } from 'react-redux';
import {
memesSortQuery,
RootStackParamList,
ROUTE,
SORT_DIRECTION,
} from '../types';
import { Meme } from '../database';
import { LoadingView, MemeViewItem } from '../components';
import {
@@ -17,7 +23,6 @@ import {
multipleIdQuery,
shareMeme,
} from '../utilities';
import { useSelector } from 'react-redux';
import { RootState } from '../state';
const memeViewStyles = StyleSheet.create({
@@ -50,6 +55,10 @@ const MemeView = ({
}: NativeStackScreenProps<RootStackParamList, ROUTE.MEME_VIEW>) => {
const { height, width } = useSafeAreaFrame();
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
const sort = useSelector((state: RootState) => state.memes.sort);
const sortDirection = useSelector(
(state: RootState) => state.memes.sortDirection,
);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const storageUri = useSelector(
(state: RootState) => state.settings.storageUri,
@@ -61,15 +70,20 @@ const MemeView = ({
const flashListRef = useRef<FlashList<Meme>>(null);
const [index, setIndex] = useState(route.params.index);
const index = useRef(route.params.index);
const memes = useQuery<Meme>(Meme.schema.name, collectionIn => {
return collectionIn.filtered(multipleIdQuery(route.params.ids));
return collectionIn
.filtered(multipleIdQuery(route.params.ids))
.sorted(
memesSortQuery(sort),
sortDirection === SORT_DIRECTION.DESCENDING,
);
});
useEffect(() => {
if (memes.length === 0) navigation.goBack();
if (index >= memes.length) {
setIndex(memes.length - 1);
if (index.current >= memes.length) {
index.current = memes.length - 1;
flashListRef.current?.scrollToIndex({ index: memes.length - 1 });
}
}, [index, memes.length, navigation]);
@@ -79,19 +93,19 @@ const MemeView = ({
<Appbar.Header style={memeViewStyles.header}>
<Appbar.BackAction onPress={() => navigation.goBack()} />
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
<Appbar.Content title={memes[index]?.title} />
<Appbar.Content title={memes[index.current]?.title} />
</Appbar.Header>
<FlashList
ref={flashListRef}
key={height}
data={memes}
initialScrollIndex={index}
initialScrollIndex={index.current}
onScroll={event => {
const newIndex = Math.round(
event.nativeEvent.contentOffset.x /
event.nativeEvent.layoutMeasurement.width,
);
if (newIndex !== index) setIndex(newIndex);
if (newIndex !== index.current) index.current = newIndex;
}}
estimatedItemSize={width}
estimatedListSize={{ height, width }}
@@ -111,14 +125,14 @@ const MemeView = ({
<Appbar style={memeViewStyles.footer}>
<Appbar.Action
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
icon={memes[index]?.isFavorite ? 'heart' : 'heart-outline'}
onPress={() => favoriteMeme(realm, memes[index])}
icon={memes[index.current]?.isFavorite ? 'heart' : 'heart-outline'}
onPress={() => favoriteMeme(realm, memes[index.current])}
disabled={isBlocked}
/>
<Appbar.Action
icon="share"
onPress={() => {
shareMeme(realm, storageUri, memes[index]).catch(() =>
shareMeme(realm, storageUri, memes[index.current]).catch(() =>
setSnackbarMessage('Failed to share meme!'),
);
}}
@@ -127,7 +141,7 @@ const MemeView = ({
<Appbar.Action
icon="content-copy"
onPress={async () => {
await copyMeme(realm, storageUri, memes[index])
await copyMeme(realm, storageUri, memes[index.current])
.then(() => setSnackbarMessage('Meme copied!'))
.catch(() => setSnackbarMessage('Failed to copy meme!'));
}}
@@ -136,7 +150,7 @@ const MemeView = ({
<Appbar.Action
icon="pencil"
onPress={() => {
editMeme(navigation, memes[index]);
editMeme(navigation, memes[index.current]);
}}
disabled={isBlocked}
/>
@@ -144,7 +158,7 @@ const MemeView = ({
icon="delete"
onPress={async () => {
setIsBlocked(true);
await deleteMeme(realm, storageUri, memes[index]);
await deleteMeme(realm, storageUri, memes[index.current]);
setIsBlocked(false);
}}
disabled={isBlocked}

View File

@@ -98,7 +98,7 @@ const Memes = () => {
[sort, sortDirection, favoritesOnly, filter, search],
);
const [scrollOffset, setScrollOffset] = useState(0);
const previousOffset = useRef(0);
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentOffset = event.nativeEvent.contentOffset.y;
@@ -106,13 +106,13 @@ const Memes = () => {
if (currentOffset <= 150) {
dispatch(setNavVisible(true));
} else {
const diff = currentOffset - scrollOffset;
if (Math.abs(diff) > 50) {
const diff = currentOffset - previousOffset.current;
if (Math.abs(diff) > 35) {
dispatch(setNavVisible(diff < 0));
}
}
setScrollOffset(currentOffset);
previousOffset.current = currentOffset;
};
const flashListRef = useRef<FlashList<Meme>>(null);
@@ -120,7 +120,7 @@ const Memes = () => {
useFocusEffect(
useCallback(() => {
const handleBackPress = () => {
if (scrollOffset <= 0) return false;
if (previousOffset.current <= 0) return false;
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
return true;
};
@@ -129,7 +129,7 @@ const Memes = () => {
return () =>
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
}, [scrollOffset]),
}, []),
);
useFocusEffect(

View File

@@ -71,7 +71,7 @@ const Tags = () => {
[search, sort, sortDirection],
);
const [scrollOffset, setScrollOffset] = useState(0);
const previousOffset = useRef(0);
const handleScroll = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
const currentOffset = event.nativeEvent.contentOffset.y;
@@ -79,11 +79,11 @@ const Tags = () => {
if (currentOffset <= 150) {
dispatch(setNavVisible(true));
} else {
const diff = currentOffset - scrollOffset;
const diff = currentOffset - previousOffset.current;
if (Math.abs(diff) > 50) dispatch(setNavVisible(diff < 0));
}
setScrollOffset(currentOffset);
previousOffset.current = currentOffset;
};
const flashListRef = useRef<FlashList<Tag>>(null);
@@ -91,7 +91,7 @@ const Tags = () => {
useFocusEffect(
useCallback(() => {
const handleBackPress = () => {
if (scrollOffset > 0) {
if (previousOffset.current > 0) {
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
return true;
}
@@ -102,7 +102,7 @@ const Tags = () => {
return () =>
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
}, [scrollOffset]),
}, []),
);
useFocusEffect(

View File

@@ -1,6 +1,7 @@
interface Dimensions {
width: number;
height: number;
aspectRatio: number;
}
export { type Dimensions };

View File

@@ -1,5 +1,6 @@
import { FileSystem } from 'react-native-file-access';
import filetypemime from 'magic-bytes.js';
import { lookup } from 'react-native-mime-types';
import { MEME_TYPE } from '../database';
const allowedImageMimeTypes = [
@@ -12,43 +13,52 @@ const allowedImageMimeTypes = [
const allowedGifMimeTypes = ['image/gif'];
const allowedMimeTypes = [...allowedImageMimeTypes, ...allowedGifMimeTypes];
const allowedVideoMimeTypes = [
'video/av01',
'video/3gpp',
'video/avc',
'video/hevc',
'video/x-matroska',
'video/mp2t',
'video/mp4',
'video/mp42',
'video/mp43',
'video/mp4v-es',
'video/mpeg',
'video/mpeg2',
'video/x-vnd.on2.vp8',
'video/x-vnd.on2.vp9',
'video/webm',
];
const allowedMimeTypes = [
...allowedImageMimeTypes,
...allowedGifMimeTypes,
...allowedVideoMimeTypes,
];
const getMemeTypeFromMimeType = (mimeType: string): MEME_TYPE | undefined => {
switch (mimeType) {
case 'image/bmp':
case 'image/jpg':
case 'image/jpeg':
case 'image/png':
case 'image/webp': {
if (!allowedMimeTypes.includes(mimeType)) return undefined;
const mimeStart = mimeType.split('/')[0];
switch (mimeStart) {
case 'image': {
if (mimeType === 'image/gif') return MEME_TYPE.GIF;
return MEME_TYPE.IMAGE;
}
case 'image/gif': {
return MEME_TYPE.GIF;
case 'video': {
return MEME_TYPE.VIDEO;
}
}
};
const guessMimeTypeFromExtension = (filename: string): string | undefined => {
const extension = filename.split('.').pop()?.toLowerCase();
switch (extension) {
case 'bmp': {
return 'image/bmp';
}
case 'jpg':
case 'jpeg': {
return 'image/jpeg';
}
case 'png': {
return 'image/png';
}
case 'webp': {
return 'image/webp';
}
case 'gif': {
return 'image/gif';
}
}
if (!extension) return undefined;
const guessedMimeType = lookup(extension);
if (!guessedMimeType) return undefined;
return guessedMimeType;
};
const guessMimeTypeFromMagicBytes = async (
@@ -66,10 +76,15 @@ const guessMimeType = async (
): Promise<string | undefined> => {
if (hint && allowedMimeTypes.includes(hint)) return hint;
const guessedMimeType = guessMimeTypeFromExtension(uri);
if (guessedMimeType) return guessedMimeType;
let guessedMimeType = guessMimeTypeFromExtension(uri);
if (guessedMimeType && allowedMimeTypes.includes(guessedMimeType)) {
return guessedMimeType;
}
return await guessMimeTypeFromMagicBytes(uri);
guessedMimeType = await guessMimeTypeFromMagicBytes(uri);
if (guessedMimeType && allowedMimeTypes.includes(guessedMimeType)) {
return guessedMimeType;
}
};
export {