Add meme-adding logic

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-21 09:46:13 +03:00
parent 1b2ce96c5e
commit 4b601872bc
40 changed files with 1037 additions and 324 deletions

141
src/screens/addMeme.tsx Normal file
View File

@@ -0,0 +1,141 @@
import React, { useState } from 'react';
import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import { useDimensions } from '../contexts';
import { ScrollView, View } from 'react-native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useRealm } from '@realm/react';
import { BSON } from 'realm';
import { AndroidScoped, FileSystem } from 'react-native-file-access';
import { useSelector } from 'react-redux';
import { extension } from 'react-native-mime-types';
import styles from '../styles';
import { ROUTE, RootStackParamList } from '../types';
import { MEME_TYPE, Meme, Tag } from '../database';
import { RootState } from '../state';
import { getMemeType } from '../utilities';
import { MemeEditor } from '../components';
const AddMeme = ({
route,
}: NativeStackScreenProps<RootStackParamList, ROUTE.ADD_MEME>) => {
const navigation = useNavigation();
const { colors } = useTheme();
const { orientation } = useDimensions();
const realm = useRealm();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const storageUri = useSelector(
(state: RootState) => state.settings.storageUri,
)!;
const { uri } = route.params;
const memeType =
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
uri.length > 1 ? MEME_TYPE.ALBUM : getMemeType(uri[0].type!);
const [memeTitle, setMemeTitle] = useState('New Meme');
const [memeDescription, setMemeDescription] = useState('');
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
const [memeTitleError, setMemeTitleError] = useState<string | undefined>();
const [isSaving, setIsSaving] = useState(false);
const handleSave = async () => {
setIsSaving(true);
const uuid = new BSON.UUID();
const savedUri: string[] = [];
const hash: string[] = [];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const fileExtension = extension(uri[0].type!);
if (!fileExtension) navigation.goBack();
savedUri.push(
AndroidScoped.appendPath(
storageUri,
`${uuid.toHexString()}.${fileExtension as string}`,
),
);
await FileSystem.cp(uri[0].uri, savedUri[0]);
const { size } = await FileSystem.stat(savedUri[0]);
hash.push(await FileSystem.hash(savedUri[0], 'MD5'));
realm.write(() => {
const meme: Meme | undefined = realm.create<Meme>(Meme.schema.name, {
id: uuid,
type: memeType,
uri: savedUri,
size,
hash,
title: memeTitle,
description: memeDescription,
isFavorite: memeIsFavorite,
tags: [...memeTags.values()],
tagsLength: memeTags.size,
});
meme.tags.forEach(tag => {
tag.dateModified = new Date();
const memes = tag.memes as Set<Meme>;
memes.add(meme);
tag.memesLength = memes.size;
});
});
setIsSaving(false);
navigation.goBack();
};
return (
<>
<Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title={'Add Meme'} />
<Appbar.Action
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
onPress={() => setMemeIsFavorite(!memeIsFavorite)}
/>
</Appbar.Header>
<ScrollView
contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical,
orientation == 'landscape' && styles.smallPaddingVertical,
styles.paddingHorizontal,
styles.flexGrow,
styles.flexColumnSpaceBetween,
{ backgroundColor: colors.background },
]}>
<View style={[styles.flex, styles.justifyStart]}>
<MemeEditor
imageUri={uri.map(uriIn => uriIn.uri)}
memeTitle={memeTitle}
setMemeTitle={setMemeTitle}
memeDescription={memeDescription}
setMemeDescription={setMemeDescription}
memeTags={memeTags}
setMemeTags={setMemeTags}
memeTitleError={memeTitleError}
setMemeTitleError={setMemeTitleError}
/>
</View>
<View style={[styles.flex, styles.justifyEnd]}>
<Button
mode="contained"
icon="floppy"
onPress={handleSave}
disabled={!!memeTitleError || isSaving}
loading={isSaving}>
Save
</Button>
</View>
</ScrollView>
</>
);
};
export default AddMeme;

79
src/screens/addTag.tsx Normal file
View File

@@ -0,0 +1,79 @@
import React, { useState } from 'react';
import { ScrollView, View } from 'react-native';
import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import { useRealm } from '@realm/react';
import styles from '../styles';
import { generateRandomColor } from '../utilities';
import { useDimensions } from '../contexts';
import { Tag } from '../database';
import { TagEditor } from '../components';
const AddTag = () => {
const navigation = useNavigation();
const { colors } = useTheme();
const { orientation } = useDimensions();
const realm = useRealm();
const [tagName, setTagName] = useState('newTag');
const [tagColor, setTagColor] = useState(generateRandomColor());
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
const [tagNameError, setTagNameError] = useState<string | undefined>();
const [tagColorError, setTagColorError] = useState<string | undefined>();
const handleSave = () => {
realm.write(() => {
realm.create(Tag.schema.name, {
name: tagName,
color: tagColor,
});
});
navigation.goBack();
};
return (
<>
<Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title={'Add Tag'} />
</Appbar.Header>
<ScrollView
contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical,
orientation == 'landscape' && styles.smallPaddingVertical,
styles.paddingHorizontal,
styles.flexGrow,
styles.flexColumnSpaceBetween,
{ backgroundColor: colors.background },
]}>
<View style={[styles.flex, styles.justifyStart]}>
<TagEditor
tagName={tagName}
setTagName={setTagName}
tagColor={tagColor}
setTagColor={setTagColor}
validatedTagColor={validatedTagColor}
setValidatedTagColor={setValidatedTagColor}
tagNameError={tagNameError}
setTagNameError={setTagNameError}
tagColorError={tagColorError}
setTagColorError={setTagColorError}
/>
</View>
<View style={[styles.flex, styles.justifyEnd]}>
<Button
mode="contained"
icon="floppy"
onPress={handleSave}
disabled={!!tagNameError || !!tagColorError}>
Save
</Button>
</View>
</ScrollView>
</>
);
};
export default AddTag;

View File

@@ -1,8 +1,8 @@
import React from 'react';
import { Appbar, Text, useTheme } from 'react-native-paper';
import { ScrollView } from 'react-native';
import { Appbar, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import { useDimensions } from '../contexts';
import { ScrollView } from 'react-native';
import styles from '../styles';
const EditMeme = () => {
@@ -14,20 +14,17 @@ const EditMeme = () => {
<>
<Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title="Add Meme" />
<Appbar.Content title={'Edit Meme'} />
</Appbar.Header>
<ScrollView
contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical,
orientation == 'landscape' && styles.smallPaddingVertical,
styles.paddingHorizontal,
[styles.centered, styles.flex],
styles.fullSize,
styles.flexGrow,
styles.flexColumnSpaceBetween,
{ backgroundColor: colors.background },
]}
nestedScrollEnabled>
<Text>Add Meme</Text>
</ScrollView>
]}></ScrollView>
</>
);
};

View File

@@ -1,19 +1,12 @@
import React, { useState } from 'react';
import { ScrollView, View } from 'react-native';
import {
TextInput,
Appbar,
HelperText,
Button,
useTheme,
} from 'react-native-paper';
import { Appbar, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BSON, UpdateMode } from 'realm';
import { BSON } from 'realm';
import { useRealm } from '@realm/react';
import { TagPreview } from '../components';
import { TagEditor } from '../components';
import styles from '../styles';
import { generateRandomColor, isValidColor } from '../utilities';
import { useDimensions } from '../contexts';
import { ROUTE, RootStackParamList } from '../types';
import { Tag } from '../database';
@@ -26,62 +19,41 @@ const EditTag = ({
const { orientation } = useDimensions();
const realm = useRealm();
const tagId = route.params?.id;
const tag = tagId
? realm.objectForPrimaryKey(Tag, BSON.UUID.createFromHexString(tagId))
: undefined;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const tag = realm.objectForPrimaryKey(
Tag,
BSON.UUID.createFromHexString(route.params.id),
)!;
const [tagName, setTagName] = useState(tag?.name ?? 'newTag');
const [tagColor, setTagColor] = useState(tag?.color ?? generateRandomColor());
const [tagName, setTagName] = useState(tag.name);
const [tagColor, setTagColor] = useState(tag.color);
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
const [tagNameError, setTagNameError] = useState<string | undefined>();
const [tagColorError, setTagColorError] = useState<string | undefined>();
const handleTagNameChange = (name: string) => {
setTagName(name);
if (name.length === 0) {
setTagNameError('Tag name cannot be empty');
} else if (name.includes(' ')) {
setTagNameError('Tag name cannot contain spaces');
} else {
// eslint-disable-next-line unicorn/no-useless-undefined
setTagNameError(undefined);
}
};
const handleTagColorChange = (color: string) => {
setTagColor(color);
if (isValidColor(color)) {
setValidatedTagColor(color);
// eslint-disable-next-line unicorn/no-useless-undefined
setTagColorError(undefined);
} else {
setTagColorError('Color must be a valid hex or rgb value');
}
};
const handleSave = () => {
realm.write(() => {
realm.create(
Tag,
{
id: tag?.id,
name: tagName,
color: tagColor,
},
UpdateMode.Modified,
);
tag.name = tagName;
tag.color = tagColor;
tag.dateModified = new Date();
});
navigation.goBack();
};
const handleDelete = () => {
realm.write(() => {
for (const meme of tag.memes) {
meme.dateModified = new Date();
const tags = meme.tags as Set<Tag>;
tags.delete(tag);
meme.tagsLength = tags.size;
}
realm.delete(tag);
});
navigation.goBack();
};
@@ -89,50 +61,32 @@ const EditTag = ({
<>
<Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title={tag ? 'Edit Tag' : 'Add Tag'} />
{tag && <Appbar.Action icon="delete" onPress={handleDelete} />}
<Appbar.Content title={'Edit Tag'} />
<Appbar.Action icon="delete" onPress={handleDelete} />
</Appbar.Header>
<ScrollView
contentContainerStyle={[
orientation == 'portrait' && styles.paddingVertical,
orientation == 'landscape' && styles.smallPaddingVertical,
styles.paddingHorizontal,
styles.fullSize,
styles.flexGrow,
styles.flexColumnSpaceBetween,
{ backgroundColor: colors.background },
]}>
]}
nestedScrollEnabled>
<View style={[styles.flex, styles.justifyStart]}>
<TagPreview name={tagName} color={validatedTagColor} />
<TextInput
mode="outlined"
label="Tag Name"
value={tagName}
onChangeText={handleTagNameChange}
error={!!tagNameError}
autoCapitalize="none"
selectTextOnFocus
<TagEditor
tagName={tagName}
setTagName={setTagName}
tagColor={tagColor}
setTagColor={setTagColor}
validatedTagColor={validatedTagColor}
setValidatedTagColor={setValidatedTagColor}
tagNameError={tagNameError}
setTagNameError={setTagNameError}
tagColorError={tagColorError}
setTagColorError={setTagColorError}
/>
<HelperText type="error" visible={!!tagNameError}>
{tagNameError}
</HelperText>
<TextInput
mode="outlined"
label="Tag Color"
value={tagColor}
onChangeText={handleTagColorChange}
error={!!tagColorError}
autoCorrect={false}
right={
<TextInput.Icon
icon="palette"
onPress={() => handleTagColorChange(generateRandomColor())}
/>
}
/>
<HelperText type="error" visible={!!tagColorError}>
{tagColorError}
</HelperText>
</View>
<View style={[styles.flex, styles.justifyEnd]}>
<Button

View File

@@ -1,6 +1,8 @@
export { default as AddMeme } from './addMeme';
export { default as AddTag } from './addTag';
export { default as EditMeme } from './editMeme';
export { default as EditTag } from './editTag';
export { default as Home } from './home';
export { default as Memes } from './memes';
export { default as Settings } from './settings';
export { default as Tags } from './tags';
export { default as Welcome } from './welcome';

View File

@@ -16,17 +16,17 @@ import { MEME_SORT, SORT_DIRECTION } from '../types';
import { getSortIcon, getViewIcon } from '../utilities';
import {
RootState,
cycleHomeView,
toggleHomeSortDirection,
setHomeSortDirection,
toggleHomeFavoritesOnly,
setHomeSort,
setHomeFilter,
cycleMemesView,
toggleMemesSortDirection,
setMemesSortDirection,
toggleMemesFavoritesOnly,
setMemesSort,
setMemesFilter,
} from '../state';
import { MEME_TYPE, Meme, memeTypePlural } from '../database';
import { useDimensions } from '../contexts';
const homeStyles = StyleSheet.create({
const memesStyles = StyleSheet.create({
headerButtonView: {
height: 50,
},
@@ -35,18 +35,18 @@ const homeStyles = StyleSheet.create({
},
});
const Home = () => {
const Memes = () => {
const { colors } = useTheme();
const { orientation } = useDimensions();
const sort = useSelector((state: RootState) => state.home.sort);
const sort = useSelector((state: RootState) => state.memes.sort);
const sortDirection = useSelector(
(state: RootState) => state.home.sortDirection,
(state: RootState) => state.memes.sortDirection,
);
const view = useSelector((state: RootState) => state.home.view);
const view = useSelector((state: RootState) => state.memes.view);
const favoritesOnly = useSelector(
(state: RootState) => state.home.favoritesOnly,
(state: RootState) => state.memes.favoritesOnly,
);
const filter = useSelector((state: RootState) => state.home.filter);
const filter = useSelector((state: RootState) => state.memes.filter);
const dispatch = useDispatch();
const [sortMenuVisible, setSortMenuVisible] = useState(false);
@@ -54,20 +54,20 @@ const Home = () => {
const handleSortModeChange = (newSort: MEME_SORT) => {
if (newSort === sort) {
dispatch(toggleHomeSortDirection());
dispatch(toggleMemesSortDirection());
} else {
dispatch(setHomeSort(newSort));
dispatch(setMemesSort(newSort));
if (newSort === MEME_SORT.TITLE) {
dispatch(setHomeSortDirection(SORT_DIRECTION.ASCENDING));
dispatch(setMemesSortDirection(SORT_DIRECTION.ASCENDING));
} else {
dispatch(setHomeSortDirection(SORT_DIRECTION.DESCENDING));
dispatch(setMemesSortDirection(SORT_DIRECTION.DESCENDING));
}
}
setSortMenuVisible(false);
};
const handleFilterChange = (newFilter: MEME_TYPE | undefined) => {
dispatch(setHomeFilter(newFilter));
dispatch(setMemesFilter(newFilter));
setFilterMenuVisible(false);
};
@@ -92,7 +92,7 @@ const Home = () => {
style={[
styles.flexRowSpaceBetween,
styles.alignCenter,
homeStyles.headerButtonView,
memesStyles.headerButtonView,
]}>
<View style={[styles.flexRow, styles.alignCenter]}>
<Menu
@@ -127,13 +127,15 @@ const Home = () => {
icon={getViewIcon(view)}
iconColor={colors.primary}
size={16}
onPress={() => dispatch(cycleHomeView())}
animated
onPress={() => dispatch(cycleMemesView())}
/>
<IconButton
icon={favoritesOnly ? 'heart' : 'heart-outline'}
iconColor={colors.primary}
size={16}
onPress={() => dispatch(toggleHomeFavoritesOnly())}
animated
onPress={() => dispatch(toggleMemesFavoritesOnly())}
/>
<Menu
visible={filterMenuVisible}
@@ -172,7 +174,7 @@ const Home = () => {
{memes.length === 0 && (
<HelperText
type={'info'}
style={[homeStyles.helperText, styles.centerText]}>
style={[memesStyles.helperText, styles.centerText]}>
No memes found
</HelperText>
)}
@@ -180,4 +182,4 @@ const Home = () => {
);
};
export default Home;
export default Memes;

View File

@@ -28,16 +28,16 @@ const SettingsScreen = () => {
const noMedia = useSelector((state: RootState) => state.settings.noMedia);
const dispatch = useDispatch();
const [optimizingDatabase, setOptimizingDatabase] = useState(false);
const [isOptimizingDatabase, setIsOptimizingDatabase] = useState(false);
const [snackbarVisible, setSnackbarVisible] = useState(false);
const [snackbarMessage, setSnackbarMessage] = useState('');
const optimizeDatabase = () => {
setOptimizingDatabase(true);
setIsOptimizingDatabase(true);
// TODO: clean up missing / extra files
setSnackbarMessage('Database optimized!');
setSnackbarVisible(true);
setOptimizingDatabase(false);
setIsOptimizingDatabase(false);
};
return (
@@ -47,7 +47,6 @@ const SettingsScreen = () => {
orientation == 'portrait' && styles.paddingTop,
orientation == 'landscape' && styles.smallPaddingTop,
styles.paddingHorizontal,
styles.fullSize,
{ backgroundColor: colors.background },
]}>
<View>
@@ -55,10 +54,7 @@ const SettingsScreen = () => {
<List.Subheader>Database</List.Subheader>
<Button
mode="elevated"
style={{
marginBottom: responsive.verticalScale(15),
}}
loading={optimizingDatabase}
loading={isOptimizingDatabase}
onPress={optimizeDatabase}>
Optimize Database Now
</Button>

View File

@@ -2,7 +2,6 @@ import React, { useCallback, useRef, useState } from 'react';
import {
StyleSheet,
View,
Text,
NativeSyntheticEvent,
NativeScrollEvent,
BackHandler,
@@ -13,6 +12,7 @@ import {
HelperText,
Menu,
Searchbar,
Text,
TouchableRipple,
useTheme,
} from 'react-native-paper';
@@ -48,14 +48,13 @@ const tagsStyles = StyleSheet.create({
height: 50,
},
tagRow: {
flexWrap: 'wrap',
justifyContent: 'space-between',
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 15,
},
tagView: {
tagChip: {
flexShrink: 1,
maxWidth: '80%',
},
@@ -195,6 +194,7 @@ const Tags = () => {
<FlashList
ref={flashListRef}
data={tags}
keyExtractor={tag => tag.id.toHexString()}
estimatedItemSize={52}
showsVerticalScrollIndicator={false}
renderItem={({ item: tag }) => (
@@ -203,9 +203,7 @@ const Tags = () => {
navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })
}>
<View style={tagsStyles.tagRow}>
<View style={tagsStyles.tagView}>
<TagChip tag={tag} />
</View>
<TagChip tag={tag} style={tagsStyles.tagChip} />
<Text>{tag.memesLength}</Text>
</View>
</TouchableRipple>