Add tag-adding logic

Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
2023-07-12 23:47:39 +03:00
parent 5bf066ac98
commit 703155232d
15 changed files with 322 additions and 28 deletions

View File

@@ -1,5 +1,6 @@
import { AppRegistry } from 'react-native';
import App from './src/app';
import { name as appName } from './app.json';
import 'react-native-get-random-values';
AppRegistry.registerComponent(appName, () => App);

52
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"react-native-fast-image": "^8.6.3",
"react-native-file-access": "^3.0.4",
"react-native-gesture-handler": "^2.12.0",
"react-native-get-random-values": "^1.9.0",
"react-native-paper": "^5.9.1",
"react-native-reanimated": "^3.3.0",
"react-native-safe-area-context": "^4.6.4",
@@ -68,7 +69,7 @@
"typescript": "^4.8.4"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@@ -4572,6 +4573,16 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz",
"integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==",
"optional": true,
"peer": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/react-native": {
"version": "0.70.14",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.14.tgz",
@@ -7414,6 +7425,11 @@
"node >=0.6.0"
]
},
"node_modules/fast-base64-decode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz",
"integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q=="
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -13389,6 +13405,17 @@
"react-native": "*"
}
},
"node_modules/react-native-get-random-values": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.9.0.tgz",
"integrity": "sha512-+29IR2oxzxNVeaRwCqGZ9ABadzMI8SLTBidrIDXPOkKnm5+kEmLt34QKM4JV+d2usPErvKyS85le0OmGTHnyWQ==",
"dependencies": {
"fast-base64-decode": "^1.0.0"
},
"peerDependencies": {
"react-native": ">=0.56"
}
},
"node_modules/react-native-paper": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",
@@ -19111,6 +19138,16 @@
"csstype": "^3.0.2"
}
},
"@types/react-dom": {
"version": "18.2.7",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.7.tgz",
"integrity": "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==",
"optional": true,
"peer": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-native": {
"version": "0.70.14",
"resolved": "https://registry.npmjs.org/@types/react-native/-/react-native-0.70.14.tgz",
@@ -21192,6 +21229,11 @@
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="
},
"fast-base64-decode": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fast-base64-decode/-/fast-base64-decode-1.0.0.tgz",
"integrity": "sha512-qwaScUgUGBYeDNRnbc/KyllVU88Jk1pRHPStuF/lO7B0/RTRLj7U0lkdTAutlBblY08rwZDff6tNU9cjv6j//Q=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -25734,6 +25776,14 @@
"prop-types": "^15.7.2"
}
},
"react-native-get-random-values": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/react-native-get-random-values/-/react-native-get-random-values-1.9.0.tgz",
"integrity": "sha512-+29IR2oxzxNVeaRwCqGZ9ABadzMI8SLTBidrIDXPOkKnm5+kEmLt34QKM4JV+d2usPErvKyS85le0OmGTHnyWQ==",
"requires": {
"fast-base64-decode": "^1.0.0"
}
},
"react-native-paper": {
"version": "5.9.1",
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",

View File

@@ -28,6 +28,7 @@
"react-native-fast-image": "^8.6.3",
"react-native-file-access": "^3.0.4",
"react-native-gesture-handler": "^2.12.0",
"react-native-get-random-values": "^1.9.0",
"react-native-paper": "^5.9.1",
"react-native-reanimated": "^3.3.0",
"react-native-safe-area-context": "^4.6.4",
@@ -73,6 +74,6 @@
"typescript": "^4.8.4"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
}

View File

@@ -46,22 +46,22 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
{
icon: 'tag',
label: 'Tag',
onPress: () => navigate('Add Item'),
onPress: () => navigate('Add Tag'),
},
{
icon: 'note-text',
label: 'Text',
onPress: () => navigate('Add Item'),
onPress: () => navigate('Add Meme'),
},
{
icon: 'image-album',
label: 'Album',
onPress: () => navigate('Add Item'),
onPress: () => navigate('Add Meme'),
},
]}
onStateChange={({ open }) => setState(open)}
onPress={() => {
if (state) navigate('Add Item');
if (state) navigate('Add Meme');
}}
style={styles.fab}
/>

View File

@@ -1,2 +1,2 @@
export { MEME_TYPE, memeTypePlural, Meme } from './meme';
export { Tag } from './tag';
export { Tag, deleteAllTags } from './tag';

View File

@@ -19,4 +19,10 @@ class Tag extends Realm.Object<Tag> {
};
}
export { Tag };
const deleteAllTags = (realm: Realm) => {
realm.write(() => {
realm.delete(realm.objects<Tag>('Tag'));
});
};
export { Tag, deleteAllTags };

View File

@@ -7,7 +7,7 @@ import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { BottomNavigation, Portal, useTheme } from 'react-native-paper';
import { Home, Tags, Settings, AddItem } from './screens';
import { Home, Tags, Settings, AddMeme, AddTag } from './screens';
import { horizontalScale } from './styles';
import { FloatingActionButton } from './components';
import { darkNavigationTheme, lightNavigationTheme } from './theme';
@@ -105,8 +105,13 @@ const NavigationContainer = () => {
<StackNavigatorBase.Navigator screenOptions={{ headerShown: false }}>
<StackNavigatorBase.Screen name="Main" component={TabNavigator} />
<StackNavigatorBase.Screen
name="Add Item"
component={AddItem}
name="Add Meme"
component={AddMeme}
options={{ animation: 'slide_from_bottom' }}
/>
<StackNavigatorBase.Screen
name="Add Tag"
component={AddTag}
options={{ animation: 'slide_from_bottom' }}
/>
</StackNavigatorBase.Navigator>

View File

@@ -1,14 +0,0 @@
import React from 'react';
import { Text } from 'react-native-paper';
import { PaddedView } from '../components';
const AddItem = () => {
return (
<PaddedView centered>
<Text>Add Item</Text>
</PaddedView>
);
};
export default AddItem;

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

@@ -0,0 +1,22 @@
import React from 'react';
import { Appbar, Text } from 'react-native-paper';
import { PaddedView } from '../components';
import { useNavigation } from '@react-navigation/native';
const AddMeme = () => {
const navigation = useNavigation();
return (
<>
<Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title="Add Meme" />
</Appbar.Header>
<PaddedView centered>
<Text>Add Meme</Text>
</PaddedView>
</>
);
};
export default AddMeme;

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

@@ -0,0 +1,151 @@
import React, { useState } from 'react';
import { View, StyleSheet } from 'react-native';
import {
Chip,
TextInput,
Appbar,
HelperText,
Button,
} from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
import { BSON } from 'realm';
import { useRealm } from '@realm/react';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { PaddedView } from '../components';
import styles, { horizontalScale, verticalScale } from '../styles';
import {
generateRandomColor,
getContrastColor,
isValidColor,
} from '../utilities';
const tagStyles = StyleSheet.create({
preview: {
justifyContent: 'center',
flexDirection: 'row',
marginVertical: verticalScale(75),
},
chip: {
padding: horizontalScale(5),
},
chipText: {
fontSize: horizontalScale(15),
},
});
const AddTag = () => {
const navigation = useNavigation();
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 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: new BSON.UUID(),
name: tagName,
color: tagColor,
memes: [],
});
});
navigation.goBack();
};
return (
<>
<Appbar.Header>
<Appbar.BackAction onPress={() => navigation.goBack()} />
<Appbar.Content title="Add Tag" />
</Appbar.Header>
<PaddedView style={[styles.flex, styles.flexColumnSpaceBetween]}>
<View>
<View style={[tagStyles.preview]}>
<Chip
icon={() => {
return (
<FontAwesome5
name="tag"
size={horizontalScale(12)}
color={getContrastColor(validatedTagColor)}
/>
);
}}
elevated
style={[tagStyles.chip, { backgroundColor: validatedTagColor }]}
textStyle={[
tagStyles.chipText,
{ color: getContrastColor(validatedTagColor) },
]}>
{'#' + tagName}
</Chip>
</View>
<TextInput
mode="outlined"
label="Tag Name"
value={tagName}
onChangeText={handleTagNameChange}
error={!!tagNameError}
/>
<HelperText type="error" visible={!!tagNameError}>
{tagNameError}
</HelperText>
<TextInput
mode="outlined"
label="Tag Color"
value={tagColor}
onChangeText={handleTagColorChange}
error={!!tagColorError}
right={
<TextInput.Icon
icon="palette"
onPress={() => handleTagColorChange(generateRandomColor())}
/>
}
/>
<HelperText type="error" visible={!!tagColorError}>
{tagColorError}
</HelperText>
</View>
<Button
mode="contained"
icon="floppy"
onPress={handleSave}
disabled={!!tagNameError || !!tagColorError}>
Save
</Button>
</PaddedView>
</>
);
};
export default AddTag;

View File

@@ -1,4 +1,5 @@
export { default as AddItem } from './addItem';
export { default as AddMeme } from './addMeme';
export { default as AddTag } from './addTag';
export { default as Home } from './home';
export { default as Settings } from './settings';
export { default as Tags } from './tags';

View File

@@ -1,12 +1,22 @@
import React from 'react';
import { Text } from 'react-native-paper';
import { Button, Text } from 'react-native-paper';
import { PaddedView } from '../components';
import { useQuery, useRealm } from '@realm/react';
import { Tag, deleteAllTags } from '../database';
const Tags = () => {
const realm = useRealm();
const tags = useQuery<Tag>('Tag');
return (
<PaddedView centered>
<Text>Tags</Text>
{tags.map(tag => (
<Text key={tag.id.toHexString()} style={{ color: tag.color }}>
{tag.name}
</Text>
))}
<Button onPress={() => deleteAllTags(realm)}>Delete All Tags</Button>
</PaddedView>
);
};

View File

@@ -47,6 +47,13 @@ const styles = StyleSheet.create({
flexDirection: 'row',
justifyContent: 'space-between',
},
flexColumn: {
flexDirection: 'column',
},
flexColumnSpaceBetween: {
flexDirection: 'column',
justifyContent: 'space-between',
},
flexRowReverse: {
flexDirection: 'row-reverse',
},

46
src/utilities/color.ts Normal file
View File

@@ -0,0 +1,46 @@
import { darkTheme, lightTheme } from '../theme';
const getContrastColor = (hexColor: string) => {
if (hexColor.startsWith('#')) {
hexColor = hexColor.slice(1);
}
const r = Number.parseInt(hexColor.slice(0, 2), 16);
const g = Number.parseInt(hexColor.slice(2, 4), 16);
const b = Number.parseInt(hexColor.slice(4, 6), 16);
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
return brightness > 128
? lightTheme.colors.onSurface
: darkTheme.colors.onSurface;
};
const isHexColor = (color: string) => {
return /^#([\da-f]{6})$/i.test(color);
};
const isRgbColor = (color: string) => {
return /^rgb\((\d{1,3}), ?(\d{1,3}), ?(\d{1,3})\)$/i.test(color);
};
const isValidColor = (color: string) => {
return isHexColor(color) || isRgbColor(color);
};
const rgbToHex = (rgb: string) => {
const [r, g, b] = rgb
.replaceAll(/[^\d,]/g, '')
.split(',')
.map(value => Number.parseInt(value, 10));
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
};
const generateRandomColor = () => {
const r = Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
const g = Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
const b = Math.floor(Math.random() * 256).toString(16).padStart(2, '0');
return `#${r}${g}${b}`;
};
export { getContrastColor, isHexColor, isRgbColor, isValidColor, rgbToHex, generateRandomColor };

View File

@@ -1,3 +1,11 @@
export {
getContrastColor,
isHexColor,
isRgbColor,
isValidColor,
rgbToHex,
generateRandomColor
} from './color';
export { packageName, appName, fileProvider, noOp } from './constants';
export { isPermissionForPath, clearPermissions } from './permissions';
export { getSortIcon, getViewIcon } from './icon';