Add tag-adding logic
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
1
index.js
1
index.js
@@ -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
52
package-lock.json
generated
@@ -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",
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -1,2 +1,2 @@
|
||||
export { MEME_TYPE, memeTypePlural, Meme } from './meme';
|
||||
export { Tag } from './tag';
|
||||
export { Tag, deleteAllTags } from './tag';
|
||||
|
@@ -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 };
|
||||
|
@@ -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>
|
||||
|
@@ -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
22
src/screens/addMeme.tsx
Normal 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
151
src/screens/addTag.tsx
Normal 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;
|
@@ -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';
|
||||
|
@@ -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>
|
||||
);
|
||||
};
|
||||
|
@@ -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
46
src/utilities/color.ts
Normal 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 };
|
@@ -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';
|
||||
|
Reference in New Issue
Block a user