Add broken URI handling
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -7,8 +7,6 @@ const loadingViewStyles = StyleSheet.create({
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
flex: 1,
|
flex: 1,
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
export { default as MemesList } from './memesList/memesList';
|
export { default as MemesList } from './memesList/memesList';
|
||||||
export { default as MemeEditor } from './memeEditor';
|
export { default as MemeEditor } from './memeEditor';
|
||||||
|
export { default as MemeFail } from './memeFail';
|
||||||
export { default as MemesHeader } from './memesHeader';
|
export { default as MemesHeader } from './memesHeader';
|
||||||
export { default as MemeTagSearchModal } from './memeTagSearchModal';
|
export { default as MemeTagSearchModal } from './memeTagSearchModal';
|
||||||
export { default as MemeTagSelector } from './memeTagSelector';
|
export { default as MemeTagSelector } from './memeTagSelector';
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
import React from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { HelperText, TextInput } from 'react-native-paper';
|
import { HelperText, TextInput } from 'react-native-paper';
|
||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||||
|
import { useImageDimensions } from '@react-native-community/hooks/lib/useImageDimensions';
|
||||||
import LoadingView from '../loadingView';
|
import LoadingView from '../loadingView';
|
||||||
import { MemeTagSelector } from '.';
|
import { MemeFail, MemeTagSelector } from '.';
|
||||||
import { Tag } from '../../database';
|
import { Tag } from '../../database';
|
||||||
import { StringValidationResult, validateMemeTitle } from '../../utilities';
|
import { StringValidationResult, validateMemeTitle } from '../../utilities';
|
||||||
import { useImageDimensions } from '@react-native-community/hooks';
|
|
||||||
|
|
||||||
const memeEditorStyles = {
|
const memeEditorStyles = {
|
||||||
image: {
|
image: {
|
||||||
@@ -23,12 +23,16 @@ const memeEditorStyles = {
|
|||||||
|
|
||||||
const MemeEditor = ({
|
const MemeEditor = ({
|
||||||
memeUri,
|
memeUri,
|
||||||
|
memeUriError,
|
||||||
|
setMemeUriError,
|
||||||
memeTitle,
|
memeTitle,
|
||||||
setMemeTitle,
|
setMemeTitle,
|
||||||
memeTags,
|
memeTags,
|
||||||
setMemeTags,
|
setMemeTags,
|
||||||
}: {
|
}: {
|
||||||
memeUri: string;
|
memeUri: string;
|
||||||
|
memeUriError: Error | undefined;
|
||||||
|
setMemeUriError: (error: Error | undefined) => void;
|
||||||
memeTitle: StringValidationResult;
|
memeTitle: StringValidationResult;
|
||||||
setMemeTitle: (name: StringValidationResult) => void;
|
setMemeTitle: (name: StringValidationResult) => void;
|
||||||
memeTags: Map<string, Tag>;
|
memeTags: Map<string, Tag>;
|
||||||
@@ -37,8 +41,9 @@ const MemeEditor = ({
|
|||||||
const { width } = useSafeAreaFrame();
|
const { width } = useSafeAreaFrame();
|
||||||
|
|
||||||
const { dimensions, loading, error } = useImageDimensions({ uri: memeUri });
|
const { dimensions, loading, error } = useImageDimensions({ uri: memeUri });
|
||||||
|
setMemeUriError(error);
|
||||||
|
|
||||||
if (loading || error || !dimensions) return <LoadingView />;
|
if (!memeUriError && (loading || !dimensions)) return <LoadingView />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -53,23 +58,36 @@ const MemeEditor = ({
|
|||||||
<HelperText type="error" visible={!memeTitle.valid}>
|
<HelperText type="error" visible={!memeTitle.valid}>
|
||||||
{memeTitle.error}
|
{memeTitle.error}
|
||||||
</HelperText>
|
</HelperText>
|
||||||
<Image
|
{memeUriError || !dimensions ? (
|
||||||
source={{ uri: memeUri }}
|
<MemeFail
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
width: width * 0.92,
|
width: width * 0.92,
|
||||||
height: Math.max(
|
height: width * 0.92,
|
||||||
Math.min(
|
},
|
||||||
((width * 0.92) / dimensions.width) * dimensions.height,
|
memeEditorStyles.image,
|
||||||
500,
|
]}
|
||||||
|
iconSize={50}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
source={{ uri: memeUri }}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width: width * 0.92,
|
||||||
|
height: Math.max(
|
||||||
|
Math.min(
|
||||||
|
((width * 0.92) / dimensions.width) * dimensions.height,
|
||||||
|
500,
|
||||||
|
),
|
||||||
|
100,
|
||||||
),
|
),
|
||||||
100,
|
},
|
||||||
),
|
memeEditorStyles.image,
|
||||||
},
|
]}
|
||||||
memeEditorStyles.image,
|
resizeMode="contain"
|
||||||
]}
|
/>
|
||||||
resizeMode="contain"
|
)}
|
||||||
/>
|
|
||||||
<MemeTagSelector
|
<MemeTagSelector
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
setMemeTags={setMemeTags}
|
setMemeTags={setMemeTags}
|
||||||
|
41
src/components/memes/memeFail.tsx
Normal file
41
src/components/memes/memeFail.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import React, { ComponentProps } from 'react';
|
||||||
|
import { StyleSheet, View } from 'react-native';
|
||||||
|
import { useTheme } from 'react-native-paper';
|
||||||
|
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import { rgbToRgba } from '../../utilities';
|
||||||
|
|
||||||
|
const memeFailStyles = StyleSheet.create({
|
||||||
|
view: {
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MemeFail = ({
|
||||||
|
iconSize,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
iconSize?: number;
|
||||||
|
} & ComponentProps<typeof View>) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
{...props}
|
||||||
|
style={[
|
||||||
|
props.style,
|
||||||
|
memeFailStyles.view,
|
||||||
|
{
|
||||||
|
backgroundColor: rgbToRgba(colors.error, 0.2),
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<FontAwesome5
|
||||||
|
name="exclamation-triangle"
|
||||||
|
size={iconSize}
|
||||||
|
color={colors.error}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemeFail;
|
@@ -5,6 +5,7 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
|||||||
import { useImageDimensions } from '@react-native-community/hooks';
|
import { useImageDimensions } from '@react-native-community/hooks';
|
||||||
import LoadingView from '../loadingView';
|
import LoadingView from '../loadingView';
|
||||||
import { Meme } from '../../database';
|
import { Meme } from '../../database';
|
||||||
|
import MemeFail from './memeFail';
|
||||||
|
|
||||||
const memeViewItemStyles = StyleSheet.create({
|
const memeViewItemStyles = StyleSheet.create({
|
||||||
view: {
|
view: {
|
||||||
@@ -18,25 +19,42 @@ const MemeViewItem = ({ meme }: { meme: Meme }) => {
|
|||||||
|
|
||||||
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
||||||
|
|
||||||
if (loading || error || !dimensions) return <LoadingView />;
|
if (!error && (loading || !dimensions)) {
|
||||||
|
return (
|
||||||
|
<View style={{ width, height }}>
|
||||||
|
<LoadingView />
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[{ width, height }, memeViewItemStyles.view]}>
|
<View style={[{ width, height }, memeViewItemStyles.view]}>
|
||||||
<ImageZoom
|
{error || !dimensions ? (
|
||||||
source={{ uri: meme.uri }}
|
<MemeFail
|
||||||
style={
|
style={{
|
||||||
dimensions.aspectRatio > width / (height - 128)
|
width: Math.min(width, height - 128),
|
||||||
? {
|
height: Math.min(width, height - 128),
|
||||||
width,
|
}}
|
||||||
height: width / (dimensions.width / dimensions.height),
|
iconSize={50}
|
||||||
}
|
/>
|
||||||
: {
|
) : (
|
||||||
width: (height - 128) * (dimensions.width / dimensions.height),
|
<ImageZoom
|
||||||
height: height - 128,
|
source={{ uri: meme.uri }}
|
||||||
}
|
style={
|
||||||
}
|
dimensions.aspectRatio > width / (height - 128)
|
||||||
minScale={0.5}
|
? {
|
||||||
/>
|
width,
|
||||||
|
height: width / (dimensions.width / dimensions.height),
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
width:
|
||||||
|
(height - 128) * (dimensions.width / dimensions.height),
|
||||||
|
height: height - 128,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
minScale={0.5}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -5,6 +5,8 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
|||||||
import { useImageDimensions } from '@react-native-community/hooks';
|
import { useImageDimensions } from '@react-native-community/hooks';
|
||||||
import { Meme } from '../../../database';
|
import { Meme } from '../../../database';
|
||||||
import { RootState } from '../../../state';
|
import { RootState } from '../../../state';
|
||||||
|
import { MemeFail } from '..';
|
||||||
|
import { getFontAwesome5IconSize } from '../../../utilities';
|
||||||
|
|
||||||
const MemesGridItem = ({
|
const MemesGridItem = ({
|
||||||
meme,
|
meme,
|
||||||
@@ -22,19 +24,29 @@ const MemesGridItem = ({
|
|||||||
|
|
||||||
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
||||||
|
|
||||||
if (loading || error || !dimensions) return <></>;
|
if (!error && (loading || !dimensions)) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableHighlight onPress={() => focusMeme(index)}>
|
<TouchableHighlight onPress={() => focusMeme(index)}>
|
||||||
<Image
|
{error ? (
|
||||||
source={{ uri: meme.uri }}
|
<MemeFail
|
||||||
style={[
|
style={{
|
||||||
{
|
|
||||||
width: (width * 0.92 - 5) / gridColumns,
|
width: (width * 0.92 - 5) / gridColumns,
|
||||||
height: (width * 0.92 - 5) / gridColumns,
|
height: (width * 0.92 - 5) / gridColumns,
|
||||||
},
|
}}
|
||||||
]}
|
iconSize={getFontAwesome5IconSize(gridColumns)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
source={{ uri: meme.uri }}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
width: (width * 0.92 - 5) / gridColumns,
|
||||||
|
height: (width * 0.92 - 5) / gridColumns,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -4,6 +4,7 @@ import { Text, TouchableRipple } from 'react-native-paper';
|
|||||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||||
import { useImageDimensions } from '@react-native-community/hooks';
|
import { useImageDimensions } from '@react-native-community/hooks';
|
||||||
import { Meme } from '../../../database';
|
import { Meme } from '../../../database';
|
||||||
|
import { MemeFail } from '..';
|
||||||
|
|
||||||
const memesListItemStyles = StyleSheet.create({
|
const memesListItemStyles = StyleSheet.create({
|
||||||
view: {
|
view: {
|
||||||
@@ -42,14 +43,18 @@ const MemesListItem = ({
|
|||||||
|
|
||||||
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
||||||
|
|
||||||
if (loading || error || !dimensions) return <></>;
|
if (!error && (loading || !dimensions)) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
onPress={() => focusMeme(index)}
|
onPress={() => focusMeme(index)}
|
||||||
style={memesListItemStyles.view}>
|
style={memesListItemStyles.view}>
|
||||||
<>
|
<>
|
||||||
<Image source={{ uri: meme.uri }} style={memesListItemStyles.image} />
|
{error ? (
|
||||||
|
<MemeFail style={memesListItemStyles.image} />
|
||||||
|
) : (
|
||||||
|
<Image source={{ uri: meme.uri }} style={memesListItemStyles.image} />
|
||||||
|
)}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
memesListItemStyles.detailsView,
|
memesListItemStyles.detailsView,
|
||||||
|
@@ -5,6 +5,8 @@ import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
|||||||
import { Meme } from '../../../database';
|
import { Meme } from '../../../database';
|
||||||
import { RootState } from '../../../state';
|
import { RootState } from '../../../state';
|
||||||
import { useImageDimensions } from '@react-native-community/hooks';
|
import { useImageDimensions } from '@react-native-community/hooks';
|
||||||
|
import { MemeFail } from '..';
|
||||||
|
import { getFontAwesome5IconSize } from '../../../utilities';
|
||||||
|
|
||||||
const memeMasonryItemStyles = StyleSheet.create({
|
const memeMasonryItemStyles = StyleSheet.create({
|
||||||
view: {
|
view: {
|
||||||
@@ -32,23 +34,36 @@ const MemesMasonryItem = ({
|
|||||||
|
|
||||||
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
const { dimensions, loading, error } = useImageDimensions({ uri: meme.uri });
|
||||||
|
|
||||||
if (loading || error || !dimensions) return <></>;
|
if (!error && (loading || !dimensions)) return <></>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
onPress={() => focusMeme(index)}
|
onPress={() => focusMeme(index)}
|
||||||
style={memeMasonryItemStyles.view}>
|
style={memeMasonryItemStyles.view}>
|
||||||
<Image
|
{error || !dimensions ? (
|
||||||
source={{ uri: meme.uri }}
|
<MemeFail
|
||||||
style={[
|
style={[
|
||||||
memeMasonryItemStyles.image,
|
memeMasonryItemStyles.image,
|
||||||
{
|
{
|
||||||
width: (width * 0.92) / masonryColumns - 5,
|
width: (width * 0.92) / masonryColumns - 5,
|
||||||
height:
|
height: (width * 0.92) / masonryColumns - 5,
|
||||||
((width * 0.92) / masonryColumns - 5) / dimensions.aspectRatio,
|
},
|
||||||
},
|
]}
|
||||||
]}
|
iconSize={getFontAwesome5IconSize(masonryColumns)}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
source={{ uri: meme.uri }}
|
||||||
|
style={[
|
||||||
|
memeMasonryItemStyles.image,
|
||||||
|
{
|
||||||
|
width: (width * 0.92) / masonryColumns - 5,
|
||||||
|
height:
|
||||||
|
((width * 0.92) / masonryColumns - 5) / dimensions.aspectRatio,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</TouchableHighlight>
|
</TouchableHighlight>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
import { Appbar, Banner, Button, useTheme } from 'react-native-paper';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
@@ -39,6 +39,7 @@ const AddMeme = ({
|
|||||||
const file = useRef(route.params.file);
|
const file = useRef(route.params.file);
|
||||||
|
|
||||||
const [memeUri, setMemeUri] = useState(file.current.uri);
|
const [memeUri, setMemeUri] = useState(file.current.uri);
|
||||||
|
const [memeUriError, setMemeUriError] = useState<Error>();
|
||||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||||
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
|
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
|
||||||
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
|
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
|
||||||
@@ -51,14 +52,15 @@ const AddMeme = ({
|
|||||||
const uuid = new BSON.UUID();
|
const uuid = new BSON.UUID();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const mimeType = file.current.type!;
|
const mimeType = file.current.type!;
|
||||||
const memeType = getMemeType(mimeType);
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const memeType = getMemeType(mimeType)!;
|
||||||
|
|
||||||
const fileExtension = extension(mimeType) as string;
|
const fileExtension = extension(mimeType) as string;
|
||||||
if (!fileExtension) goBack();
|
if (!fileExtension) goBack();
|
||||||
|
|
||||||
const uri = AndroidScoped.appendPath(
|
const uri = AndroidScoped.appendPath(
|
||||||
storageUri,
|
storageUri,
|
||||||
`${uuid.toHexString()}.${fileExtension}`,
|
`${uuid.toHexString()}-${Math.round(Date.now() / 1000)}.${fileExtension}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await FileSystem.cp(file.current.uri, uri);
|
await FileSystem.cp(file.current.uri, uri);
|
||||||
@@ -95,6 +97,17 @@ const AddMeme = ({
|
|||||||
onPress={() => setMemeIsFavorite(!memeIsFavorite)}
|
onPress={() => setMemeIsFavorite(!memeIsFavorite)}
|
||||||
/>
|
/>
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
|
<Banner
|
||||||
|
visible={!!memeUriError}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Cancel',
|
||||||
|
onPress: goBack,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
The selected URI appears to be broken. This may have been caused by the
|
||||||
|
file being corrupted or unsupported.
|
||||||
|
</Banner>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
editorStyles.scrollView,
|
editorStyles.scrollView,
|
||||||
@@ -106,6 +119,8 @@ const AddMeme = ({
|
|||||||
<View style={editorStyles.editorView}>
|
<View style={editorStyles.editorView}>
|
||||||
<MemeEditor
|
<MemeEditor
|
||||||
memeUri={memeUri}
|
memeUri={memeUri}
|
||||||
|
memeUriError={memeUriError}
|
||||||
|
setMemeUriError={setMemeUriError}
|
||||||
memeTitle={memeTitle}
|
memeTitle={memeTitle}
|
||||||
setMemeTitle={setMemeTitle}
|
setMemeTitle={setMemeTitle}
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
@@ -128,7 +143,12 @@ const AddMeme = ({
|
|||||||
setMemeIsFavorite(false);
|
setMemeIsFavorite(false);
|
||||||
setMemeTags(new Map<string, Tag>());
|
setMemeTags(new Map<string, Tag>());
|
||||||
}}
|
}}
|
||||||
disabled={!memeTitle.valid || isSaving || isSavingAndAddingAnother}
|
disabled={
|
||||||
|
!memeTitle.valid ||
|
||||||
|
isSaving ||
|
||||||
|
isSavingAndAddingAnother ||
|
||||||
|
!!memeUriError
|
||||||
|
}
|
||||||
loading={isSavingAndAddingAnother}
|
loading={isSavingAndAddingAnother}
|
||||||
style={editorStyles.saveAndAddButton}>
|
style={editorStyles.saveAndAddButton}>
|
||||||
Save & Add
|
Save & Add
|
||||||
@@ -142,7 +162,12 @@ const AddMeme = ({
|
|||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
goBack();
|
goBack();
|
||||||
}}
|
}}
|
||||||
disabled={!memeTitle.valid || isSaving || isSavingAndAddingAnother}
|
disabled={
|
||||||
|
!memeTitle.valid ||
|
||||||
|
isSaving ||
|
||||||
|
isSavingAndAddingAnother ||
|
||||||
|
!!memeUriError
|
||||||
|
}
|
||||||
loading={isSaving}
|
loading={isSaving}
|
||||||
style={editorStyles.saveButton}>
|
style={editorStyles.saveButton}>
|
||||||
Save
|
Save
|
||||||
|
@@ -1,16 +1,29 @@
|
|||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
import { Appbar, Banner, Button, useTheme } from 'react-native-paper';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
import { useObject, useRealm } from '@realm/react';
|
import { useObject, useRealm } from '@realm/react';
|
||||||
import { useDeviceOrientation } from '@react-native-community/hooks';
|
import { useDeviceOrientation } from '@react-native-community/hooks';
|
||||||
import { BSON } from 'realm';
|
import { BSON } from 'realm';
|
||||||
import { RootStackParamList, ROUTE } from '../../types';
|
import { RootStackParamList, ROUTE } from '../../types';
|
||||||
|
import { pickSingle } from 'react-native-document-picker';
|
||||||
|
import { AndroidScoped, FileSystem } from 'react-native-file-access';
|
||||||
import { Tag, Meme } from '../../database';
|
import { Tag, Meme } from '../../database';
|
||||||
import { deleteMeme, favoriteMeme, validateMemeTitle } from '../../utilities';
|
import {
|
||||||
|
StringValidationResult,
|
||||||
|
allowedMimeTypes,
|
||||||
|
deleteMeme,
|
||||||
|
favoriteMeme,
|
||||||
|
getMemeType,
|
||||||
|
noOp,
|
||||||
|
validateMemeTitle,
|
||||||
|
} from '../../utilities';
|
||||||
import { MemeEditor } from '../../components';
|
import { MemeEditor } from '../../components';
|
||||||
import editorStyles from './editorStyles';
|
import editorStyles from './editorStyles';
|
||||||
|
import { extension } from 'react-native-mime-types';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { RootState } from '../../state';
|
||||||
|
|
||||||
const EditMeme = ({
|
const EditMeme = ({
|
||||||
route,
|
route,
|
||||||
@@ -19,6 +32,10 @@ const EditMeme = ({
|
|||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const orientation = useDeviceOrientation();
|
const orientation = useDeviceOrientation();
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const storageUri = useSelector(
|
||||||
|
(state: RootState) => state.settings.storageUri,
|
||||||
|
)!;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const meme = useObject<Meme>(
|
const meme = useObject<Meme>(
|
||||||
@@ -26,11 +43,24 @@ const EditMeme = ({
|
|||||||
BSON.UUID.createFromHexString(route.params.id),
|
BSON.UUID.createFromHexString(route.params.id),
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
|
|
||||||
|
const [memeUriError, setMemeUriError] = useState<Error>();
|
||||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title));
|
const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title));
|
||||||
const [memeTags, setMemeTags] = useState(
|
const [memeTags, setMemeTags] = useState(
|
||||||
new Map<string, Tag>(meme.tags.map(tag => [tag.id.toHexString(), tag])),
|
new Map<string, Tag>(meme.tags.map(tag => [tag.id.toHexString(), tag])),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleMemeTitleChange = useCallback((title: StringValidationResult) => {
|
||||||
|
setMemeTitle(title);
|
||||||
|
setHasChanges(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMemeTagsChange = useCallback((tags: Map<string, Tag>) => {
|
||||||
|
setMemeTags(tags);
|
||||||
|
setHasChanges(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
@@ -59,6 +89,34 @@ const EditMeme = ({
|
|||||||
});
|
});
|
||||||
}, [meme, memeTags, memeTitle.parsed, realm]);
|
}, [meme, memeTags, memeTitle.parsed, realm]);
|
||||||
|
|
||||||
|
const handleFixUri = useCallback(async () => {
|
||||||
|
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 fileExtension = extension(mimeType) as string;
|
||||||
|
if (!fileExtension) return;
|
||||||
|
|
||||||
|
const uri = AndroidScoped.appendPath(
|
||||||
|
storageUri,
|
||||||
|
`${meme.id.toHexString()}-${Date.now() / 1000}.${fileExtension}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
await FileSystem.cp(file.uri, uri);
|
||||||
|
const { size } = await FileSystem.stat(uri);
|
||||||
|
|
||||||
|
realm.write(() => {
|
||||||
|
meme.uri = uri;
|
||||||
|
meme.type = memeType;
|
||||||
|
meme.mimeType = mimeType;
|
||||||
|
meme.size = size;
|
||||||
|
});
|
||||||
|
}, [meme, realm, storageUri]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
@@ -78,6 +136,26 @@ const EditMeme = ({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
|
<Banner
|
||||||
|
visible={!!memeUriError}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
label: 'Fix URI',
|
||||||
|
onPress: handleFixUri,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Delete Meme',
|
||||||
|
onPress: async () => {
|
||||||
|
setIsSaving(true);
|
||||||
|
await deleteMeme(realm, meme);
|
||||||
|
setIsSaving(false);
|
||||||
|
goBack();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
The URI for this meme appears to be broken. This may have been caused by
|
||||||
|
the file being moved or deleted.
|
||||||
|
</Banner>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
editorStyles.scrollView,
|
editorStyles.scrollView,
|
||||||
@@ -89,10 +167,12 @@ const EditMeme = ({
|
|||||||
<View style={editorStyles.editorView}>
|
<View style={editorStyles.editorView}>
|
||||||
<MemeEditor
|
<MemeEditor
|
||||||
memeUri={meme.uri}
|
memeUri={meme.uri}
|
||||||
|
memeUriError={memeUriError}
|
||||||
|
setMemeUriError={setMemeUriError}
|
||||||
memeTitle={memeTitle}
|
memeTitle={memeTitle}
|
||||||
setMemeTitle={setMemeTitle}
|
setMemeTitle={handleMemeTitleChange}
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
setMemeTags={setMemeTags}
|
setMemeTags={handleMemeTagsChange}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={editorStyles.saveButtonView}>
|
<View style={editorStyles.saveButtonView}>
|
||||||
@@ -105,8 +185,11 @@ const EditMeme = ({
|
|||||||
setIsSaving(false);
|
setIsSaving(false);
|
||||||
goBack();
|
goBack();
|
||||||
}}
|
}}
|
||||||
disabled={!memeTitle.valid || isSaving}
|
disabled={
|
||||||
loading={isSaving}>
|
!memeTitle.valid || !hasChanges || isSaving || !!memeUriError
|
||||||
|
}
|
||||||
|
loading={isSaving}
|
||||||
|
style={editorStyles.soloSaveButton}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
@@ -9,7 +9,12 @@ import { useDeviceOrientation } from '@react-native-community/hooks';
|
|||||||
import { TagEditor } from '../../components';
|
import { TagEditor } from '../../components';
|
||||||
import { ROUTE, RootStackParamList } from '../../types';
|
import { ROUTE, RootStackParamList } from '../../types';
|
||||||
import { Tag } from '../../database';
|
import { Tag } from '../../database';
|
||||||
import { deleteTag, validateColor, validateTagName } from '../../utilities';
|
import {
|
||||||
|
StringValidationResult,
|
||||||
|
deleteTag,
|
||||||
|
validateColor,
|
||||||
|
validateTagName,
|
||||||
|
} from '../../utilities';
|
||||||
import editorStyles from './editorStyles';
|
import editorStyles from './editorStyles';
|
||||||
|
|
||||||
const EditTag = ({
|
const EditTag = ({
|
||||||
@@ -26,9 +31,20 @@ const EditTag = ({
|
|||||||
BSON.UUID.createFromHexString(route.params.id),
|
BSON.UUID.createFromHexString(route.params.id),
|
||||||
)!;
|
)!;
|
||||||
|
|
||||||
|
const [hasChanges, setHasChanges] = useState(false);
|
||||||
const [tagName, setTagName] = useState(validateTagName(tag.name));
|
const [tagName, setTagName] = useState(validateTagName(tag.name));
|
||||||
const [tagColor, setTagColor] = useState(validateColor(tag.color));
|
const [tagColor, setTagColor] = useState(validateColor(tag.color));
|
||||||
|
|
||||||
|
const handleTagNameChange = useCallback((name: StringValidationResult) => {
|
||||||
|
setTagName(name);
|
||||||
|
setHasChanges(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleTagColorChange = useCallback((color: StringValidationResult) => {
|
||||||
|
setTagColor(color);
|
||||||
|
setHasChanges(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
tag.name = tagName.parsed;
|
tag.name = tagName.parsed;
|
||||||
@@ -62,9 +78,9 @@ const EditTag = ({
|
|||||||
<View style={editorStyles.editorView}>
|
<View style={editorStyles.editorView}>
|
||||||
<TagEditor
|
<TagEditor
|
||||||
tagName={tagName}
|
tagName={tagName}
|
||||||
setTagName={setTagName}
|
setTagName={handleTagNameChange}
|
||||||
tagColor={tagColor}
|
tagColor={tagColor}
|
||||||
setTagColor={setTagColor}
|
setTagColor={handleTagColorChange}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
<View style={editorStyles.saveButtonView}>
|
<View style={editorStyles.saveButtonView}>
|
||||||
@@ -75,7 +91,8 @@ const EditTag = ({
|
|||||||
handleSave();
|
handleSave();
|
||||||
goBack();
|
goBack();
|
||||||
}}
|
}}
|
||||||
disabled={!tagName.valid || !tagColor.valid}>
|
disabled={!tagName.valid || !tagColor.valid || !hasChanges}
|
||||||
|
style={editorStyles.soloSaveButton}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
@@ -29,6 +29,9 @@ const editorStyles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
marginLeft: 5,
|
marginLeft: 5,
|
||||||
},
|
},
|
||||||
|
soloSaveButton: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default editorStyles;
|
export default editorStyles;
|
||||||
|
@@ -5,6 +5,7 @@ import { useQuery, useRealm } from '@realm/react';
|
|||||||
import { FlashList } from '@shopify/flash-list';
|
import { FlashList } from '@shopify/flash-list';
|
||||||
import { Appbar, Portal, Snackbar } from 'react-native-paper';
|
import { Appbar, Portal, Snackbar } from 'react-native-paper';
|
||||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||||
|
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||||
import { RootStackParamList, ROUTE } from '../types';
|
import { RootStackParamList, ROUTE } from '../types';
|
||||||
import { Meme } from '../database';
|
import { Meme } from '../database';
|
||||||
import { MemeViewItem } from '../components';
|
import { MemeViewItem } from '../components';
|
||||||
@@ -16,7 +17,6 @@ import {
|
|||||||
multipleIdQuery,
|
multipleIdQuery,
|
||||||
shareMeme,
|
shareMeme,
|
||||||
} from '../utilities';
|
} from '../utilities';
|
||||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
|
||||||
|
|
||||||
const memeViewStyles = StyleSheet.create({
|
const memeViewStyles = StyleSheet.create({
|
||||||
// eslint-disable-next-line react-native/no-color-literals
|
// eslint-disable-next-line react-native/no-color-literals
|
||||||
@@ -94,13 +94,27 @@ const MemeView = ({
|
|||||||
icon={memes[index].isFavorite ? 'heart' : 'heart-outline'}
|
icon={memes[index].isFavorite ? 'heart' : 'heart-outline'}
|
||||||
onPress={() => favoriteMeme(realm, memes[index])}
|
onPress={() => favoriteMeme(realm, memes[index])}
|
||||||
/>
|
/>
|
||||||
<Appbar.Action icon="share" onPress={() => shareMeme(memes[index])} />
|
<Appbar.Action
|
||||||
|
icon="share"
|
||||||
|
onPress={() => {
|
||||||
|
shareMeme(memes[index]).catch(() => {
|
||||||
|
setSnackbarMessage('Failed to share meme!');
|
||||||
|
setSnackbarVisible(true);
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<Appbar.Action
|
<Appbar.Action
|
||||||
icon="content-copy"
|
icon="content-copy"
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
copyMeme(memes[index]);
|
await copyMeme(memes[index])
|
||||||
setSnackbarMessage('Meme copied!');
|
.then(() => {
|
||||||
setSnackbarVisible(true);
|
setSnackbarMessage('Meme copied!');
|
||||||
|
setSnackbarVisible(true);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
setSnackbarMessage('Failed to copy meme!');
|
||||||
|
setSnackbarVisible(true);
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Appbar.Action
|
<Appbar.Action
|
||||||
|
@@ -1,9 +1,35 @@
|
|||||||
const getFlashListItemHeight = (numColumns: number) => {
|
const getFlashListItemHeight = (numColumns: number) => {
|
||||||
const A = 500;
|
switch (numColumns) {
|
||||||
const B = 300;
|
case 1: {
|
||||||
const C = 1;
|
return 350;
|
||||||
const height = A - B * Math.log(numColumns + C);
|
}
|
||||||
return Math.max(Math.round(height), 0);
|
case 2: {
|
||||||
|
return 180;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
return 120;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
return 90;
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { getFlashListItemHeight };
|
const getFontAwesome5IconSize = (numColumns: number) => {
|
||||||
|
switch (numColumns) {
|
||||||
|
case 1: {
|
||||||
|
return 40;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
return 30;
|
||||||
|
}
|
||||||
|
case 3: {
|
||||||
|
return 20;
|
||||||
|
}
|
||||||
|
case 4: {
|
||||||
|
return 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getFlashListItemHeight, getFontAwesome5IconSize };
|
||||||
|
@@ -11,7 +11,7 @@ const allowedGifMimeTypes = ['image/gif'];
|
|||||||
|
|
||||||
const allowedMimeTypes = [...allowedImageMimeTypes, ...allowedGifMimeTypes];
|
const allowedMimeTypes = [...allowedImageMimeTypes, ...allowedGifMimeTypes];
|
||||||
|
|
||||||
const getMemeType = (mimeType: string) => {
|
const getMemeType = (mimeType: string): MEME_TYPE | undefined => {
|
||||||
switch (mimeType) {
|
switch (mimeType) {
|
||||||
case 'image/bmp':
|
case 'image/bmp':
|
||||||
case 'image/jpeg':
|
case 'image/jpeg':
|
||||||
|
@@ -8,7 +8,7 @@ export {
|
|||||||
} from './color';
|
} from './color';
|
||||||
export { packageName, appName, fileProvider, noOp } from './constants';
|
export { packageName, appName, fileProvider, noOp } from './constants';
|
||||||
export { multipleIdQuery } from './database';
|
export { multipleIdQuery } from './database';
|
||||||
export { getFlashListItemHeight } from './dimensions';
|
export { getFlashListItemHeight, getFontAwesome5IconSize } from './dimensions';
|
||||||
export {
|
export {
|
||||||
allowedImageMimeTypes,
|
allowedImageMimeTypes,
|
||||||
allowedGifMimeTypes,
|
allowedGifMimeTypes,
|
||||||
|
@@ -5,6 +5,7 @@ import Share from 'react-native-share';
|
|||||||
import Clipboard from '@react-native-clipboard/clipboard';
|
import Clipboard from '@react-native-clipboard/clipboard';
|
||||||
import { Meme } from '../database';
|
import { Meme } from '../database';
|
||||||
import { ROUTE, RootStackParamList } from '../types';
|
import { ROUTE, RootStackParamList } from '../types';
|
||||||
|
import { noOp } from './constants';
|
||||||
|
|
||||||
const favoriteMeme = (realm: Realm, meme: Meme) => {
|
const favoriteMeme = (realm: Realm, meme: Meme) => {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
@@ -23,7 +24,9 @@ const shareMeme = async (meme: Meme) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyMeme = (meme: Meme) => {
|
const copyMeme = async (meme: Meme) => {
|
||||||
|
const exists = await FileSystem.exists(meme.uri);
|
||||||
|
if (!exists) throw new Error('File does not exist');
|
||||||
Clipboard.setURI(meme.uri);
|
Clipboard.setURI(meme.uri);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,7 +38,7 @@ const editMeme = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteMeme = async (realm: Realm, meme: Meme) => {
|
const deleteMeme = async (realm: Realm, meme: Meme) => {
|
||||||
await FileSystem.unlink(meme.uri);
|
await FileSystem.unlink(meme.uri).catch(noOp);
|
||||||
|
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
for (const tag of meme.tags) {
|
for (const tag of meme.tags) {
|
||||||
|
Reference in New Issue
Block a user