Add broken URI handling

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-26 16:10:45 +03:00
parent 083e798fdf
commit acba17462f
17 changed files with 370 additions and 91 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -29,6 +29,9 @@ const editorStyles = StyleSheet.create({
flex: 1,
marginLeft: 5,
},
soloSaveButton: {
flex: 1,
},
});
export default editorStyles;

View File

@@ -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