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

@@ -7,8 +7,6 @@ const loadingViewStyles = StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
flex: 1, flex: 1,
width: '100%',
height: '100%',
}, },
}); });

View File

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

View File

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

View 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;

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
}; };

View File

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

View File

@@ -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>
); );
}; };

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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