Add broken URI handling
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
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 { ScrollView, View } from 'react-native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
@@ -39,6 +39,7 @@ const AddMeme = ({
|
||||
const file = useRef(route.params.file);
|
||||
|
||||
const [memeUri, setMemeUri] = useState(file.current.uri);
|
||||
const [memeUriError, setMemeUriError] = useState<Error>();
|
||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
|
||||
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
|
||||
@@ -51,14 +52,15 @@ const AddMeme = ({
|
||||
const uuid = new BSON.UUID();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
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;
|
||||
if (!fileExtension) goBack();
|
||||
|
||||
const uri = AndroidScoped.appendPath(
|
||||
storageUri,
|
||||
`${uuid.toHexString()}.${fileExtension}`,
|
||||
`${uuid.toHexString()}-${Math.round(Date.now() / 1000)}.${fileExtension}`,
|
||||
);
|
||||
|
||||
await FileSystem.cp(file.current.uri, uri);
|
||||
@@ -95,6 +97,17 @@ const AddMeme = ({
|
||||
onPress={() => setMemeIsFavorite(!memeIsFavorite)}
|
||||
/>
|
||||
</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
|
||||
contentContainerStyle={[
|
||||
editorStyles.scrollView,
|
||||
@@ -106,6 +119,8 @@ const AddMeme = ({
|
||||
<View style={editorStyles.editorView}>
|
||||
<MemeEditor
|
||||
memeUri={memeUri}
|
||||
memeUriError={memeUriError}
|
||||
setMemeUriError={setMemeUriError}
|
||||
memeTitle={memeTitle}
|
||||
setMemeTitle={setMemeTitle}
|
||||
memeTags={memeTags}
|
||||
@@ -128,7 +143,12 @@ const AddMeme = ({
|
||||
setMemeIsFavorite(false);
|
||||
setMemeTags(new Map<string, Tag>());
|
||||
}}
|
||||
disabled={!memeTitle.valid || isSaving || isSavingAndAddingAnother}
|
||||
disabled={
|
||||
!memeTitle.valid ||
|
||||
isSaving ||
|
||||
isSavingAndAddingAnother ||
|
||||
!!memeUriError
|
||||
}
|
||||
loading={isSavingAndAddingAnother}
|
||||
style={editorStyles.saveAndAddButton}>
|
||||
Save & Add
|
||||
@@ -142,7 +162,12 @@ const AddMeme = ({
|
||||
setIsSaving(false);
|
||||
goBack();
|
||||
}}
|
||||
disabled={!memeTitle.valid || isSaving || isSavingAndAddingAnother}
|
||||
disabled={
|
||||
!memeTitle.valid ||
|
||||
isSaving ||
|
||||
isSavingAndAddingAnother ||
|
||||
!!memeUriError
|
||||
}
|
||||
loading={isSaving}
|
||||
style={editorStyles.saveButton}>
|
||||
Save
|
||||
|
@@ -1,16 +1,29 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
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 { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { useObject, useRealm } from '@realm/react';
|
||||
import { useDeviceOrientation } from '@react-native-community/hooks';
|
||||
import { BSON } from 'realm';
|
||||
import { RootStackParamList, ROUTE } from '../../types';
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
import { AndroidScoped, FileSystem } from 'react-native-file-access';
|
||||
import { 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 editorStyles from './editorStyles';
|
||||
import { extension } from 'react-native-mime-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../state';
|
||||
|
||||
const EditMeme = ({
|
||||
route,
|
||||
@@ -19,6 +32,10 @@ const EditMeme = ({
|
||||
const { colors } = useTheme();
|
||||
const orientation = useDeviceOrientation();
|
||||
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
|
||||
const meme = useObject<Meme>(
|
||||
@@ -26,11 +43,24 @@ const EditMeme = ({
|
||||
BSON.UUID.createFromHexString(route.params.id),
|
||||
)!;
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const [memeUriError, setMemeUriError] = useState<Error>();
|
||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title));
|
||||
const [memeTags, setMemeTags] = useState(
|
||||
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 handleSave = useCallback(() => {
|
||||
@@ -59,6 +89,34 @@ const EditMeme = ({
|
||||
});
|
||||
}, [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 (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
@@ -78,6 +136,26 @@ const EditMeme = ({
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
contentContainerStyle={[
|
||||
editorStyles.scrollView,
|
||||
@@ -89,10 +167,12 @@ const EditMeme = ({
|
||||
<View style={editorStyles.editorView}>
|
||||
<MemeEditor
|
||||
memeUri={meme.uri}
|
||||
memeUriError={memeUriError}
|
||||
setMemeUriError={setMemeUriError}
|
||||
memeTitle={memeTitle}
|
||||
setMemeTitle={setMemeTitle}
|
||||
setMemeTitle={handleMemeTitleChange}
|
||||
memeTags={memeTags}
|
||||
setMemeTags={setMemeTags}
|
||||
setMemeTags={handleMemeTagsChange}
|
||||
/>
|
||||
</View>
|
||||
<View style={editorStyles.saveButtonView}>
|
||||
@@ -105,8 +185,11 @@ const EditMeme = ({
|
||||
setIsSaving(false);
|
||||
goBack();
|
||||
}}
|
||||
disabled={!memeTitle.valid || isSaving}
|
||||
loading={isSaving}>
|
||||
disabled={
|
||||
!memeTitle.valid || !hasChanges || isSaving || !!memeUriError
|
||||
}
|
||||
loading={isSaving}
|
||||
style={editorStyles.soloSaveButton}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
|
@@ -9,7 +9,12 @@ import { useDeviceOrientation } from '@react-native-community/hooks';
|
||||
import { TagEditor } from '../../components';
|
||||
import { ROUTE, RootStackParamList } from '../../types';
|
||||
import { Tag } from '../../database';
|
||||
import { deleteTag, validateColor, validateTagName } from '../../utilities';
|
||||
import {
|
||||
StringValidationResult,
|
||||
deleteTag,
|
||||
validateColor,
|
||||
validateTagName,
|
||||
} from '../../utilities';
|
||||
import editorStyles from './editorStyles';
|
||||
|
||||
const EditTag = ({
|
||||
@@ -26,9 +31,20 @@ const EditTag = ({
|
||||
BSON.UUID.createFromHexString(route.params.id),
|
||||
)!;
|
||||
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [tagName, setTagName] = useState(validateTagName(tag.name));
|
||||
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(() => {
|
||||
realm.write(() => {
|
||||
tag.name = tagName.parsed;
|
||||
@@ -62,9 +78,9 @@ const EditTag = ({
|
||||
<View style={editorStyles.editorView}>
|
||||
<TagEditor
|
||||
tagName={tagName}
|
||||
setTagName={setTagName}
|
||||
setTagName={handleTagNameChange}
|
||||
tagColor={tagColor}
|
||||
setTagColor={setTagColor}
|
||||
setTagColor={handleTagColorChange}
|
||||
/>
|
||||
</View>
|
||||
<View style={editorStyles.saveButtonView}>
|
||||
@@ -75,7 +91,8 @@ const EditTag = ({
|
||||
handleSave();
|
||||
goBack();
|
||||
}}
|
||||
disabled={!tagName.valid || !tagColor.valid}>
|
||||
disabled={!tagName.valid || !tagColor.valid || !hasChanges}
|
||||
style={editorStyles.soloSaveButton}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
|
@@ -29,6 +29,9 @@ const editorStyles = StyleSheet.create({
|
||||
flex: 1,
|
||||
marginLeft: 5,
|
||||
},
|
||||
soloSaveButton: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
export default editorStyles;
|
||||
|
@@ -5,6 +5,7 @@ import { useQuery, useRealm } from '@realm/react';
|
||||
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 { Meme } from '../database';
|
||||
import { MemeViewItem } from '../components';
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
multipleIdQuery,
|
||||
shareMeme,
|
||||
} from '../utilities';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
|
||||
const memeViewStyles = StyleSheet.create({
|
||||
// eslint-disable-next-line react-native/no-color-literals
|
||||
@@ -94,13 +94,27 @@ const MemeView = ({
|
||||
icon={memes[index].isFavorite ? 'heart' : 'heart-outline'}
|
||||
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
|
||||
icon="content-copy"
|
||||
onPress={() => {
|
||||
copyMeme(memes[index]);
|
||||
setSnackbarMessage('Meme copied!');
|
||||
setSnackbarVisible(true);
|
||||
onPress={async () => {
|
||||
await copyMeme(memes[index])
|
||||
.then(() => {
|
||||
setSnackbarMessage('Meme copied!');
|
||||
setSnackbarVisible(true);
|
||||
})
|
||||
.catch(() => {
|
||||
setSnackbarMessage('Failed to copy meme!');
|
||||
setSnackbarVisible(true);
|
||||
});
|
||||
}}
|
||||
/>
|
||||
<Appbar.Action
|
||||
|
Reference in New Issue
Block a user