Add video support
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -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>
|
||||
|
@@ -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"
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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={[
|
||||
|
@@ -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
1
src/hooks/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { default as useMemeDimensions } from './useMemeDimensions';
|
56
src/hooks/useMemeDimensions.ts
Normal file
56
src/hooks/useMemeDimensions.ts
Normal 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;
|
@@ -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}
|
||||
|
@@ -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(
|
||||
|
@@ -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(
|
||||
|
@@ -1,6 +1,7 @@
|
||||
interface Dimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
aspectRatio: number;
|
||||
}
|
||||
|
||||
export { type Dimensions };
|
||||
|
@@ -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 {
|
||||
|
Reference in New Issue
Block a user