Add meme-adding logic
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -118,6 +118,10 @@ dependencies {
|
|||||||
} else {
|
} else {
|
||||||
implementation jscFlavor
|
implementation jscFlavor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
implementation 'com.facebook.fresco:animated-gif:2.5.0'
|
||||||
|
implementation 'com.facebook.fresco:animated-webp:2.5.0'
|
||||||
|
implementation 'com.facebook.fresco:webpsupport:2.5.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
|
||||||
|
51
package-lock.json
generated
51
package-lock.json
generated
@@ -20,10 +20,10 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.72.2",
|
"react-native": "0.72.2",
|
||||||
"react-native-document-picker": "^9.0.1",
|
"react-native-document-picker": "^9.0.1",
|
||||||
"react-native-fast-image": "^8.6.3",
|
|
||||||
"react-native-file-access": "^3.0.4",
|
"react-native-file-access": "^3.0.4",
|
||||||
"react-native-gesture-handler": "^2.12.0",
|
"react-native-gesture-handler": "^2.12.0",
|
||||||
"react-native-get-random-values": "^1.9.0",
|
"react-native-get-random-values": "^1.9.0",
|
||||||
|
"react-native-mime-types": "^2.4.0",
|
||||||
"react-native-paper": "^5.9.1",
|
"react-native-paper": "^5.9.1",
|
||||||
"react-native-reanimated": "^3.3.0",
|
"react-native-reanimated": "^3.3.0",
|
||||||
"react-native-safe-area-context": "^4.6.4",
|
"react-native-safe-area-context": "^4.6.4",
|
||||||
@@ -13371,15 +13371,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-fast-image": {
|
|
||||||
"version": "8.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz",
|
|
||||||
"integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^17 || ^18",
|
|
||||||
"react-native": ">=0.60.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-native-file-access": {
|
"node_modules/react-native-file-access": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz",
|
||||||
@@ -13416,6 +13407,25 @@
|
|||||||
"react-native": ">=0.56"
|
"react-native": ">=0.56"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-mime-types": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-a7LymNr7yQzrDEhSMPNAy9aIs1OckBpo6G8OkjVQTzaCe0XaSXCXu6KJsu/a4c3HVF9t0FiFSnxsRVEctpPI0g==",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "~1.37.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-native-mime-types/node_modules/mime-db": {
|
||||||
|
"version": "1.37.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
|
||||||
|
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native-paper": {
|
"node_modules/react-native-paper": {
|
||||||
"version": "5.9.1",
|
"version": "5.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",
|
||||||
@@ -25752,12 +25762,6 @@
|
|||||||
"invariant": "^2.2.4"
|
"invariant": "^2.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-native-fast-image": {
|
|
||||||
"version": "8.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz",
|
|
||||||
"integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==",
|
|
||||||
"requires": {}
|
|
||||||
},
|
|
||||||
"react-native-file-access": {
|
"react-native-file-access": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz",
|
||||||
@@ -25784,6 +25788,21 @@
|
|||||||
"fast-base64-decode": "^1.0.0"
|
"fast-base64-decode": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-native-mime-types": {
|
||||||
|
"version": "2.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz",
|
||||||
|
"integrity": "sha512-a7LymNr7yQzrDEhSMPNAy9aIs1OckBpo6G8OkjVQTzaCe0XaSXCXu6KJsu/a4c3HVF9t0FiFSnxsRVEctpPI0g==",
|
||||||
|
"requires": {
|
||||||
|
"mime-db": "~1.37.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": {
|
||||||
|
"version": "1.37.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.37.0.tgz",
|
||||||
|
"integrity": "sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg=="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-native-paper": {
|
"react-native-paper": {
|
||||||
"version": "5.9.1",
|
"version": "5.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",
|
||||||
|
@@ -25,10 +25,10 @@
|
|||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-native": "0.72.2",
|
"react-native": "0.72.2",
|
||||||
"react-native-document-picker": "^9.0.1",
|
"react-native-document-picker": "^9.0.1",
|
||||||
"react-native-fast-image": "^8.6.3",
|
|
||||||
"react-native-file-access": "^3.0.4",
|
"react-native-file-access": "^3.0.4",
|
||||||
"react-native-gesture-handler": "^2.12.0",
|
"react-native-gesture-handler": "^2.12.0",
|
||||||
"react-native-get-random-values": "^1.9.0",
|
"react-native-get-random-values": "^1.9.0",
|
||||||
|
"react-native-mime-types": "^2.4.0",
|
||||||
"react-native-paper": "^5.9.1",
|
"react-native-paper": "^5.9.1",
|
||||||
"react-native-reanimated": "^3.3.0",
|
"react-native-reanimated": "^3.3.0",
|
||||||
"react-native-safe-area-context": "^4.6.4",
|
"react-native-safe-area-context": "^4.6.4",
|
||||||
|
@@ -3,8 +3,10 @@ import { Keyboard } from 'react-native';
|
|||||||
import { FAB } from 'react-native-paper';
|
import { FAB } from 'react-native-paper';
|
||||||
import { ParamListBase, useNavigation } from '@react-navigation/native';
|
import { ParamListBase, useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||||
|
import { pick } from 'react-native-document-picker';
|
||||||
import { useDimensions } from '../contexts';
|
import { useDimensions } from '../contexts';
|
||||||
import { ROUTE } from '../types';
|
import { ROUTE } from '../types';
|
||||||
|
import { allowedMimeTypes, noOp } from '../utilities';
|
||||||
|
|
||||||
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
|
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
|
||||||
const { navigate } =
|
const { navigate } =
|
||||||
@@ -39,22 +41,34 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
|
|||||||
{
|
{
|
||||||
icon: 'tag',
|
icon: 'tag',
|
||||||
label: 'Tag',
|
label: 'Tag',
|
||||||
onPress: () => navigate(ROUTE.EDIT_TAG),
|
onPress: () => navigate(ROUTE.ADD_TAG),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'note-text',
|
icon: 'note-text',
|
||||||
label: 'Text',
|
label: 'Text',
|
||||||
onPress: () => navigate(ROUTE.EDIT_MEME),
|
onPress: () => {
|
||||||
|
throw new Error('Not yet implemented');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: 'image-album',
|
icon: 'image-album',
|
||||||
label: 'Album',
|
label: 'Album',
|
||||||
onPress: () => navigate(ROUTE.EDIT_MEME),
|
onPress: async () => {
|
||||||
|
const res = await pick({
|
||||||
|
allowMultiSelection: true,
|
||||||
|
type: allowedMimeTypes,
|
||||||
|
}).catch(noOp);
|
||||||
|
if (!res) return;
|
||||||
|
navigate(ROUTE.ADD_MEME, { uri: res });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onStateChange={({ open }) => setState(open)}
|
onStateChange={({ open }) => setState(open)}
|
||||||
onPress={() => {
|
onPress={async () => {
|
||||||
if (state) navigate(ROUTE.EDIT_MEME);
|
if (!state) return;
|
||||||
|
const res = await pick({ type: allowedMimeTypes }).catch(noOp);
|
||||||
|
if (!res) return;
|
||||||
|
navigate(ROUTE.ADD_MEME, { uri: res });
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
paddingBottom: responsive.verticalScale(75),
|
paddingBottom: responsive.verticalScale(75),
|
||||||
|
@@ -10,7 +10,7 @@ const hideableHeaderStyles = StyleSheet.create({
|
|||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
zIndex: 100,
|
zIndex: 1,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
|
export { MemeEditor } from './memes';
|
||||||
|
export { TagChip, TagEditor, TagPreview } from './tags';
|
||||||
export { default as FloatingActionButton } from './floatingActionButton';
|
export { default as FloatingActionButton } from './floatingActionButton';
|
||||||
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
|
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
|
||||||
export { default as HideableHeader } from './hideableHeader';
|
export { default as HideableHeader } from './hideableHeader';
|
||||||
export { default as LoadingView } from './loadingView';
|
export { default as LoadingView } from './loadingView';
|
||||||
export { default as TagChip } from './tags/tagChip';
|
|
||||||
export { default as TagPreview } from './tags/tagPreview';
|
|
||||||
|
3
src/components/memes/index.ts
Normal file
3
src/components/memes/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as MemeEditor } from './memeEditor';
|
||||||
|
export { default as MemeTagSearchModal } from './memeTagSearchModal';
|
||||||
|
export { default as MemeTagSelector } from './memeTagSelector';
|
99
src/components/memes/memeEditor.tsx
Normal file
99
src/components/memes/memeEditor.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { HelperText, TextInput } from 'react-native-paper';
|
||||||
|
import { Image } from 'react-native';
|
||||||
|
import { useDimensions } from '../../contexts';
|
||||||
|
import LoadingView from '../loadingView';
|
||||||
|
import { MemeTagSelector } from '.';
|
||||||
|
import { Tag } from '../../database';
|
||||||
|
|
||||||
|
const MemeEditor = ({
|
||||||
|
imageUri,
|
||||||
|
memeTitle,
|
||||||
|
setMemeTitle,
|
||||||
|
memeDescription,
|
||||||
|
setMemeDescription,
|
||||||
|
memeTags,
|
||||||
|
setMemeTags,
|
||||||
|
memeTitleError,
|
||||||
|
setMemeTitleError,
|
||||||
|
}: {
|
||||||
|
imageUri: string[];
|
||||||
|
memeTitle: string;
|
||||||
|
setMemeTitle: (name: string) => void;
|
||||||
|
memeDescription: string;
|
||||||
|
setMemeDescription: (description: string) => void;
|
||||||
|
memeTags: Map<string, Tag>;
|
||||||
|
setMemeTags: (tags: Map<string, Tag>) => void;
|
||||||
|
memeTitleError: string | undefined;
|
||||||
|
setMemeTitleError: (error: string | undefined) => void;
|
||||||
|
}) => {
|
||||||
|
const { dimensions, fixed, responsive } = useDimensions();
|
||||||
|
|
||||||
|
const [imageWidth, setImageWidth] = useState<number>();
|
||||||
|
const [imageHeight, setImageHeight] = useState<number>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Image.getSize(imageUri[0], (width, height) => {
|
||||||
|
const paddedWidth = dimensions.width - dimensions.width * 0.08;
|
||||||
|
setImageWidth(paddedWidth);
|
||||||
|
setImageHeight((paddedWidth / width) * height);
|
||||||
|
});
|
||||||
|
}, [dimensions.width, imageUri]);
|
||||||
|
|
||||||
|
const handleMemeTitleChange = (name: string) => {
|
||||||
|
setMemeTitle(name);
|
||||||
|
if (name.length === 0) {
|
||||||
|
setMemeTitleError('Meme title cannot be empty');
|
||||||
|
} else {
|
||||||
|
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||||
|
setMemeTitleError(undefined);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!imageWidth || !imageHeight) return <LoadingView />;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TextInput
|
||||||
|
mode="outlined"
|
||||||
|
label="Title"
|
||||||
|
value={memeTitle}
|
||||||
|
onChangeText={handleMemeTitleChange}
|
||||||
|
error={!!memeTitleError}
|
||||||
|
selectTextOnFocus
|
||||||
|
/>
|
||||||
|
<HelperText type="error" visible={!!memeTitleError}>
|
||||||
|
{memeTitleError}
|
||||||
|
</HelperText>
|
||||||
|
<Image
|
||||||
|
source={{ uri: imageUri[0] }}
|
||||||
|
style={{
|
||||||
|
width: imageWidth,
|
||||||
|
height: imageHeight,
|
||||||
|
marginBottom: fixed.verticalScale(10),
|
||||||
|
borderRadius: fixed.verticalScale(5),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<MemeTagSelector
|
||||||
|
memeTags={memeTags}
|
||||||
|
setMemeTags={setMemeTags}
|
||||||
|
style={{
|
||||||
|
marginBottom: responsive.verticalScale(10),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
mode="outlined"
|
||||||
|
label="Description"
|
||||||
|
multiline
|
||||||
|
numberOfLines={6}
|
||||||
|
style={{
|
||||||
|
marginBottom: responsive.verticalScale(15),
|
||||||
|
}}
|
||||||
|
value={memeDescription}
|
||||||
|
onChangeText={setMemeDescription}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemeEditor;
|
128
src/components/memes/memeTagSearchModal.tsx
Normal file
128
src/components/memes/memeTagSearchModal.tsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import { TagChip } from '../tags';
|
||||||
|
import { Tag } from '../../database';
|
||||||
|
import { useQuery, useRealm } from '@realm/react';
|
||||||
|
import { TAG_SORT, tagSortQuery } from '../../types';
|
||||||
|
import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
import { useDimensions } from '../../contexts';
|
||||||
|
import styles from '../../styles';
|
||||||
|
import { FlashList } from '@shopify/flash-list';
|
||||||
|
|
||||||
|
const memeTagSearchModalStyles = StyleSheet.create({
|
||||||
|
modal: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MemeTagSearchModal = ({
|
||||||
|
visible,
|
||||||
|
setVisible,
|
||||||
|
memeTags,
|
||||||
|
setMemeTags,
|
||||||
|
}: {
|
||||||
|
visible: boolean;
|
||||||
|
setVisible: (visible: boolean) => void;
|
||||||
|
memeTags: Map<string, Tag>;
|
||||||
|
setMemeTags: (tags: Map<string, Tag>) => void;
|
||||||
|
}) => {
|
||||||
|
const { colors } = useTheme();
|
||||||
|
const { fixed, responsive } = useDimensions();
|
||||||
|
const realm = useRealm();
|
||||||
|
|
||||||
|
const flashListRef = useRef<FlashList<Tag>>(null);
|
||||||
|
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const handleSearch = (newSearch: string) => {
|
||||||
|
flashListRef.current?.scrollToOffset({ offset: 0 });
|
||||||
|
setSearch(newSearch);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tags = useQuery<Tag>(
|
||||||
|
Tag.schema.name,
|
||||||
|
collection =>
|
||||||
|
collection
|
||||||
|
.filtered(`name CONTAINS[c] "${search}"`)
|
||||||
|
.sorted(tagSortQuery(TAG_SORT.DATE_MODIFIED), true),
|
||||||
|
[search],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleTagPress = (tag: Tag) => {
|
||||||
|
const id = tag.id.toHexString();
|
||||||
|
memeTags.delete(id) || memeTags.set(id, tag);
|
||||||
|
setMemeTags(new Map(memeTags));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCreateTag = (name: string) => {
|
||||||
|
let tag: Tag | undefined;
|
||||||
|
realm.write(() => {
|
||||||
|
tag = realm.create<Tag>(Tag.schema.name, {
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (!tag) return;
|
||||||
|
memeTags.set(tag.id.toHexString(), tag);
|
||||||
|
setMemeTags(new Map(memeTags));
|
||||||
|
setSearch(tag.name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Portal>
|
||||||
|
<Modal
|
||||||
|
visible={visible}
|
||||||
|
contentContainerStyle={[
|
||||||
|
{
|
||||||
|
padding: fixed.horizontalScale(10),
|
||||||
|
borderTopLeftRadius: fixed.verticalScale(20),
|
||||||
|
borderTopRightRadius: fixed.verticalScale(20),
|
||||||
|
backgroundColor: colors.surface,
|
||||||
|
},
|
||||||
|
styles.fullWidth,
|
||||||
|
memeTagSearchModalStyles.modal,
|
||||||
|
]}
|
||||||
|
onDismiss={() => setVisible(false)}>
|
||||||
|
<Searchbar
|
||||||
|
placeholder="Search or Create Tags"
|
||||||
|
onChangeText={handleSearch}
|
||||||
|
value={search}
|
||||||
|
style={{
|
||||||
|
marginBottom: responsive.verticalScale(10),
|
||||||
|
}}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
<FlashList
|
||||||
|
ref={flashListRef}
|
||||||
|
data={tags}
|
||||||
|
extraData={memeTags}
|
||||||
|
keyExtractor={tag => tag.id.toHexString()}
|
||||||
|
horizontal
|
||||||
|
estimatedItemSize={120}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
keyboardShouldPersistTaps={'always'}
|
||||||
|
renderItem={({ item: tag }) => (
|
||||||
|
<TagChip
|
||||||
|
tag={tag}
|
||||||
|
style={{
|
||||||
|
marginRight: fixed.horizontalScale(8),
|
||||||
|
}}
|
||||||
|
onPress={() => handleTagPress(tag)}
|
||||||
|
active={memeTags.has(tag.id.toHexString())}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
ListEmptyComponent={() => (
|
||||||
|
<Chip
|
||||||
|
icon="plus"
|
||||||
|
mode="outlined"
|
||||||
|
onPress={() => handleCreateTag(search.replaceAll(/\s+/g, ''))}>
|
||||||
|
Create Tag #{search.replaceAll(/\s+/g, '')}
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
</Portal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemeTagSearchModal;
|
70
src/components/memes/memeTagSelector.tsx
Normal file
70
src/components/memes/memeTagSelector.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import React, { ComponentProps, useState } from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
|
import { Chip } from 'react-native-paper';
|
||||||
|
import { TagChip } from '../tags';
|
||||||
|
import { Tag } from '../../database';
|
||||||
|
import { useDimensions } from '../../contexts';
|
||||||
|
import { MemeTagSearchModal } from '.';
|
||||||
|
import { FlashList } from '@shopify/flash-list';
|
||||||
|
|
||||||
|
const MemeTagSelector = ({
|
||||||
|
memeTags,
|
||||||
|
setMemeTags,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
memeTags: Map<string, Tag>;
|
||||||
|
setMemeTags: (tags: Map<string, Tag>) => void;
|
||||||
|
} & ComponentProps<typeof View>) => {
|
||||||
|
const { fixed, dimensions } = useDimensions();
|
||||||
|
|
||||||
|
const [tagSearchModalVisible, setTagSearchModalVisible] = useState(false);
|
||||||
|
|
||||||
|
const handleTagPress = (tag: Tag) => {
|
||||||
|
const id = tag.id.toHexString();
|
||||||
|
memeTags.delete(id) || memeTags.set(id, tag);
|
||||||
|
setMemeTags(new Map(memeTags));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<View {...props}>
|
||||||
|
<FlashList
|
||||||
|
data={[...memeTags.values()]}
|
||||||
|
extraData={memeTags}
|
||||||
|
keyExtractor={tag => tag.id.toHexString()}
|
||||||
|
horizontal
|
||||||
|
estimatedItemSize={120}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
renderItem={({ item: tag }) => (
|
||||||
|
<TagChip
|
||||||
|
tag={tag}
|
||||||
|
onPress={() => handleTagPress(tag)}
|
||||||
|
style={{
|
||||||
|
marginRight: fixed.horizontalScale(4),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
ListFooterComponent={() => (
|
||||||
|
<Chip
|
||||||
|
icon="plus"
|
||||||
|
mode="outlined"
|
||||||
|
onPress={() => setTagSearchModalVisible(true)}
|
||||||
|
style={{
|
||||||
|
marginRight: dimensions.width * 0.92 - 105.8,
|
||||||
|
}}>
|
||||||
|
Add Tag
|
||||||
|
</Chip>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<MemeTagSearchModal
|
||||||
|
visible={tagSearchModalVisible}
|
||||||
|
setVisible={setTagSearchModalVisible}
|
||||||
|
memeTags={memeTags}
|
||||||
|
setMemeTags={setMemeTags}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemeTagSelector;
|
3
src/components/tags/index.ts
Normal file
3
src/components/tags/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { default as TagChip } from './tagChip';
|
||||||
|
export { default as TagEditor } from './tagEditor';
|
||||||
|
export { default as TagPreview } from './tagPreview';
|
@@ -1,24 +1,57 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { getContrastColor } from '../../utilities';
|
import { getContrastColor } from '../../utilities';
|
||||||
import { Chip } from 'react-native-paper';
|
import { Chip, useTheme } from 'react-native-paper';
|
||||||
import { Tag } from '../../database';
|
import { Tag } from '../../database';
|
||||||
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||||
|
import { StyleSheet } from 'react-native';
|
||||||
|
|
||||||
|
const tagChipStyles = StyleSheet.create({
|
||||||
|
chip: {
|
||||||
|
borderWidth: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const TagChip = ({
|
||||||
|
tag,
|
||||||
|
active = true,
|
||||||
|
onPress,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
tag: Tag;
|
||||||
|
active?: boolean;
|
||||||
|
onPress?: () => void;
|
||||||
|
} & Omit<React.ComponentProps<typeof Chip>, 'children'>) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const chipTheme = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...theme,
|
||||||
|
colors: {
|
||||||
|
...theme.colors,
|
||||||
|
secondaryContainer: tag.color,
|
||||||
|
outline: tag.color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [tag.color, theme]);
|
||||||
|
|
||||||
const TagChip = ({ tag }: { tag: Tag }) => {
|
|
||||||
const contrastColor = getContrastColor(tag.color);
|
const contrastColor = getContrastColor(tag.color);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip
|
<Chip
|
||||||
|
{...props}
|
||||||
icon={() => {
|
icon={() => {
|
||||||
return <FontAwesome5 name="tag" color={contrastColor} />;
|
return (
|
||||||
|
<FontAwesome5 name="tag" color={active ? contrastColor : tag.color} />
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
compact
|
compact
|
||||||
style={[
|
theme={chipTheme}
|
||||||
{
|
mode={active ? 'flat' : 'outlined'}
|
||||||
backgroundColor: tag.color,
|
style={[tagChipStyles.chip, props.style]}
|
||||||
},
|
textStyle={{
|
||||||
]}
|
color: active ? contrastColor : theme.colors.onBackground,
|
||||||
textStyle={{ color: contrastColor }}>
|
}}
|
||||||
|
onPress={onPress}>
|
||||||
{'#' + tag.name}
|
{'#' + tag.name}
|
||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
|
90
src/components/tags/tagEditor.tsx
Normal file
90
src/components/tags/tagEditor.tsx
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { HelperText, TextInput } from 'react-native-paper';
|
||||||
|
import TagPreview from './tagPreview';
|
||||||
|
import { generateRandomColor, isValidColor } from '../../utilities';
|
||||||
|
|
||||||
|
const TagEditor = ({
|
||||||
|
tagName,
|
||||||
|
setTagName,
|
||||||
|
tagColor,
|
||||||
|
setTagColor,
|
||||||
|
validatedTagColor,
|
||||||
|
setValidatedTagColor,
|
||||||
|
tagNameError,
|
||||||
|
setTagNameError,
|
||||||
|
tagColorError,
|
||||||
|
setTagColorError,
|
||||||
|
}: {
|
||||||
|
tagName: string;
|
||||||
|
setTagName: (name: string) => void;
|
||||||
|
tagColor: string;
|
||||||
|
setTagColor: (color: string) => void;
|
||||||
|
validatedTagColor: string;
|
||||||
|
setValidatedTagColor: (color: string) => void;
|
||||||
|
tagNameError: string | undefined;
|
||||||
|
setTagNameError: (error: string | undefined) => void;
|
||||||
|
tagColorError: string | undefined;
|
||||||
|
setTagColorError: (error: string | undefined) => void;
|
||||||
|
}) => {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TagPreview name={tagName} color={validatedTagColor} />
|
||||||
|
<TextInput
|
||||||
|
mode="outlined"
|
||||||
|
label="Name"
|
||||||
|
value={tagName}
|
||||||
|
onChangeText={handleTagNameChange}
|
||||||
|
error={!!tagNameError}
|
||||||
|
autoCapitalize="none"
|
||||||
|
selectTextOnFocus
|
||||||
|
/>
|
||||||
|
<HelperText type="error" visible={!!tagNameError}>
|
||||||
|
{tagNameError}
|
||||||
|
</HelperText>
|
||||||
|
<TextInput
|
||||||
|
mode="outlined"
|
||||||
|
label="Color"
|
||||||
|
value={tagColor}
|
||||||
|
onChangeText={handleTagColorChange}
|
||||||
|
error={!!tagColorError}
|
||||||
|
autoCorrect={false}
|
||||||
|
right={
|
||||||
|
<TextInput.Icon
|
||||||
|
icon="palette"
|
||||||
|
onPress={() => handleTagColorChange(generateRandomColor())}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<HelperText type="error" visible={!!tagColorError}>
|
||||||
|
{tagColorError}
|
||||||
|
</HelperText>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagEditor;
|
@@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet, View } from 'react-native';
|
||||||
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||||
import { Chip } from 'react-native-paper';
|
import { Chip, useTheme } from 'react-native-paper';
|
||||||
import styles from '../../styles';
|
import styles from '../../styles';
|
||||||
import { useDimensions } from '../../contexts';
|
import { useDimensions } from '../../contexts';
|
||||||
import { getContrastColor } from '../../utilities';
|
import { getContrastColor } from '../../utilities';
|
||||||
@@ -16,7 +16,19 @@ const tagPreviewStyles = StyleSheet.create({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const TagPreview = ({ name, color }: { name: string; color: string }) => {
|
const TagPreview = ({ name, color }: { name: string; color: string }) => {
|
||||||
|
const theme = useTheme();
|
||||||
const { responsive } = useDimensions();
|
const { responsive } = useDimensions();
|
||||||
|
|
||||||
|
const chipTheme = useMemo(() => {
|
||||||
|
return {
|
||||||
|
...theme,
|
||||||
|
colors: {
|
||||||
|
...theme.colors,
|
||||||
|
secondaryContainer: color,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [theme, color]);
|
||||||
|
|
||||||
const contrastColor = getContrastColor(color);
|
const contrastColor = getContrastColor(color);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -33,12 +45,8 @@ const TagPreview = ({ name, color }: { name: string; color: string }) => {
|
|||||||
return <FontAwesome5 name="tag" size={14} color={contrastColor} />;
|
return <FontAwesome5 name="tag" size={14} color={contrastColor} />;
|
||||||
}}
|
}}
|
||||||
elevated
|
elevated
|
||||||
style={[
|
style={tagPreviewStyles.chip}
|
||||||
tagPreviewStyles.chip,
|
theme={chipTheme}
|
||||||
{
|
|
||||||
backgroundColor: color,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
textStyle={[tagPreviewStyles.text, { color: contrastColor }]}>
|
textStyle={[tagPreviewStyles.text, { color: contrastColor }]}>
|
||||||
{'#' + name}
|
{'#' + name}
|
||||||
</Chip>
|
</Chip>
|
||||||
|
@@ -21,7 +21,7 @@ interface DimensionsContext {
|
|||||||
orientation: 'portrait' | 'landscape';
|
orientation: 'portrait' | 'landscape';
|
||||||
dimensions: ScaledSize;
|
dimensions: ScaledSize;
|
||||||
responsive: ScaleFunctions;
|
responsive: ScaleFunctions;
|
||||||
static: ScaleFunctions;
|
fixed: ScaleFunctions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const createScaleFunctions = (dimensionsIn: ScaledSize) => {
|
const createScaleFunctions = (dimensionsIn: ScaledSize) => {
|
||||||
@@ -56,7 +56,7 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const responsiveScale = createScaleFunctions(dimensions);
|
const responsiveScale = createScaleFunctions(dimensions);
|
||||||
const staticScale = createScaleFunctions(initialDimensions);
|
const fixedScale = createScaleFunctions(initialDimensions);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onChange = ({ window }: { window: ScaledSize }) => {
|
const onChange = ({ window }: { window: ScaledSize }) => {
|
||||||
@@ -76,7 +76,7 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => {
|
|||||||
orientation,
|
orientation,
|
||||||
dimensions,
|
dimensions,
|
||||||
responsive: responsiveScale,
|
responsive: responsiveScale,
|
||||||
static: staticScale,
|
fixed: fixedScale,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</DimensionsContext.Provider>
|
</DimensionsContext.Provider>
|
||||||
|
@@ -1,2 +1,2 @@
|
|||||||
export { MEME_TYPE, memeTypePlural, Meme } from './meme';
|
export { MEME_TYPE, memeTypePlural, Meme } from './meme';
|
||||||
export { Tag, deleteTag, deleteAllTags } from './tag';
|
export { Tag } from './tag';
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { Realm } from '@realm/react';
|
import { BSON, Object, ObjectSchema } from 'realm';
|
||||||
import { BSON } from 'realm';
|
|
||||||
import { Tag } from './tag';
|
import { Tag } from './tag';
|
||||||
|
|
||||||
enum MEME_TYPE {
|
enum MEME_TYPE {
|
||||||
@@ -20,33 +19,36 @@ const memeTypePlural = {
|
|||||||
[MEME_TYPE.TEXT]: 'Text',
|
[MEME_TYPE.TEXT]: 'Text',
|
||||||
};
|
};
|
||||||
|
|
||||||
class Meme extends Realm.Object<Meme> {
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
class Meme extends Object<Meme> {
|
||||||
id!: BSON.UUID;
|
id!: BSON.UUID;
|
||||||
type!: MEME_TYPE;
|
type!: MEME_TYPE;
|
||||||
uri!: Realm.List<string>;
|
uri!: string[];
|
||||||
|
hash!: string[];
|
||||||
size!: number;
|
size!: number;
|
||||||
title!: string;
|
title!: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
tags!: Realm.List<Tag>;
|
tags!: Tag[] | Set<Tag>;
|
||||||
tagsLength!: number;
|
tagsLength!: number;
|
||||||
dateCreated!: Date;
|
dateCreated!: Date;
|
||||||
dateModified!: Date;
|
dateModified!: Date;
|
||||||
dateUsed?: Date;
|
dateUsed?: Date;
|
||||||
timesUsed!: number;
|
timesUsed!: number;
|
||||||
|
|
||||||
static schema: Realm.ObjectSchema = {
|
static schema: ObjectSchema = {
|
||||||
name: 'Meme',
|
name: 'Meme',
|
||||||
primaryKey: 'id',
|
primaryKey: 'id',
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||||
type: { type: 'string', indexed: true },
|
type: { type: 'string', indexed: true },
|
||||||
uri: 'string[]',
|
uri: 'string[]',
|
||||||
|
hash: 'string[]',
|
||||||
size: 'int',
|
size: 'int',
|
||||||
title: 'string',
|
title: 'string',
|
||||||
description: 'string?',
|
description: 'string?',
|
||||||
isFavorite: { type: 'bool', indexed: true, default: false },
|
isFavorite: { type: 'bool', indexed: true, default: false },
|
||||||
tags: { type: 'list', objectType: 'Tag', default: [] },
|
tags: { type: 'set', objectType: 'Tag', default: [] },
|
||||||
tagsLength: { type: 'int', default: 0 },
|
tagsLength: { type: 'int', default: 0 },
|
||||||
dateCreated: { type: 'date', default: () => new Date() },
|
dateCreated: { type: 'date', default: () => new Date() },
|
||||||
dateModified: { type: 'date', default: () => new Date() },
|
dateModified: { type: 'date', default: () => new Date() },
|
||||||
|
@@ -1,43 +1,34 @@
|
|||||||
import { Realm } from '@realm/react';
|
import { BSON, Object, ObjectSchema } from 'realm';
|
||||||
import { BSON } from 'realm';
|
|
||||||
import { Meme } from './meme';
|
import { Meme } from './meme';
|
||||||
|
import { generateRandomColor } from '../utilities';
|
||||||
|
|
||||||
class Tag extends Realm.Object<Tag> {
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||||
|
class Tag extends Object<Tag> {
|
||||||
id!: BSON.UUID;
|
id!: BSON.UUID;
|
||||||
name!: string;
|
name!: string;
|
||||||
color!: string;
|
color!: string;
|
||||||
memes!: Realm.List<Meme>;
|
memes!: Meme[] | Set<Meme>;
|
||||||
memesLength!: number;
|
memesLength!: number;
|
||||||
dateCreated!: Date;
|
dateCreated!: Date;
|
||||||
dateModified!: Date;
|
dateModified!: Date;
|
||||||
|
dateUsed?: Date;
|
||||||
timesUsed!: number;
|
timesUsed!: number;
|
||||||
|
|
||||||
static schema: Realm.ObjectSchema = {
|
static schema: ObjectSchema = {
|
||||||
name: 'Tag',
|
name: 'Tag',
|
||||||
primaryKey: 'id',
|
primaryKey: 'id',
|
||||||
properties: {
|
properties: {
|
||||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||||
name: { type: 'string', indexed: true },
|
name: { type: 'string', indexed: true },
|
||||||
color: 'string',
|
color: { type: 'string', default: () => generateRandomColor() },
|
||||||
memes: { type: 'list', objectType: 'Meme', default: [] },
|
memes: { type: 'set', objectType: 'Meme', default: [] },
|
||||||
memesLength: { type: 'int', default: 0 },
|
memesLength: { type: 'int', default: 0 },
|
||||||
dateCreated: { type: 'date', default: () => new Date() },
|
dateCreated: { type: 'date', default: () => new Date() },
|
||||||
dateModified: { type: 'date', default: () => new Date() },
|
dateModified: { type: 'date', default: () => new Date() },
|
||||||
|
dateUsed: 'date?',
|
||||||
timesUsed: { type: 'int', default: 0 },
|
timesUsed: { type: 'int', default: 0 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteTag = (realm: Realm, tag: Tag) => {
|
export { Tag };
|
||||||
realm.write(() => {
|
|
||||||
realm.delete(tag);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteAllTags = (realm: Realm) => {
|
|
||||||
realm.write(() => {
|
|
||||||
realm.delete(realm.objects<Tag>('Tag'));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export { Tag, deleteTag, deleteAllTags };
|
|
||||||
|
@@ -5,7 +5,15 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
|||||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||||
import { useTheme } from 'react-native-paper';
|
import { useTheme } from 'react-native-paper';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Home, Tags, Settings, EditMeme, EditTag } from './screens';
|
import {
|
||||||
|
Memes,
|
||||||
|
Tags,
|
||||||
|
Settings,
|
||||||
|
EditMeme,
|
||||||
|
EditTag,
|
||||||
|
AddMeme,
|
||||||
|
AddTag,
|
||||||
|
} from './screens';
|
||||||
import { darkNavigationTheme, lightNavigationTheme } from './theme';
|
import { darkNavigationTheme, lightNavigationTheme } from './theme';
|
||||||
import {
|
import {
|
||||||
FloatingActionButton,
|
FloatingActionButton,
|
||||||
@@ -19,7 +27,7 @@ const TabNavigator = () => {
|
|||||||
(state: RootState) => state.navigation.navVisible,
|
(state: RootState) => state.navigation.navVisible,
|
||||||
);
|
);
|
||||||
|
|
||||||
const [route, setRoute] = React.useState(ROUTE.HOME);
|
const [route, setRoute] = React.useState(ROUTE.MEMES);
|
||||||
const TabNavigatorBase = createBottomTabNavigator();
|
const TabNavigatorBase = createBottomTabNavigator();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -27,6 +35,7 @@ const TabNavigator = () => {
|
|||||||
<TabNavigatorBase.Navigator
|
<TabNavigatorBase.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
|
lazy: false,
|
||||||
}}
|
}}
|
||||||
tabBar={({ navigation, state, descriptors, insets }) => (
|
tabBar={({ navigation, state, descriptors, insets }) => (
|
||||||
<HideableBottomNavigationBar
|
<HideableBottomNavigationBar
|
||||||
@@ -39,11 +48,11 @@ const TabNavigator = () => {
|
|||||||
/>
|
/>
|
||||||
)}>
|
)}>
|
||||||
<TabNavigatorBase.Screen
|
<TabNavigatorBase.Screen
|
||||||
name={ROUTE.HOME}
|
name={ROUTE.MEMES}
|
||||||
component={Home}
|
component={Memes}
|
||||||
options={{
|
options={{
|
||||||
tabBarIcon: ({ color, size }) => (
|
tabBarIcon: ({ color, size }) => (
|
||||||
<FontAwesome5 name="home" color={color} size={size} />
|
<FontAwesome5 name="images" color={color} size={size} />
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -85,10 +94,12 @@ const NavigationContainer = () => {
|
|||||||
animation: 'slide_from_bottom',
|
animation: 'slide_from_bottom',
|
||||||
}}>
|
}}>
|
||||||
<StackNavigatorBase.Screen name={ROUTE.MAIN} component={TabNavigator} />
|
<StackNavigatorBase.Screen name={ROUTE.MAIN} component={TabNavigator} />
|
||||||
|
<StackNavigatorBase.Screen name={ROUTE.ADD_MEME} component={AddMeme} />
|
||||||
<StackNavigatorBase.Screen
|
<StackNavigatorBase.Screen
|
||||||
name={ROUTE.EDIT_MEME}
|
name={ROUTE.EDIT_MEME}
|
||||||
component={EditMeme}
|
component={EditMeme}
|
||||||
/>
|
/>
|
||||||
|
<StackNavigatorBase.Screen name={ROUTE.ADD_TAG} component={AddTag} />
|
||||||
<StackNavigatorBase.Screen name={ROUTE.EDIT_TAG} component={EditTag} />
|
<StackNavigatorBase.Screen name={ROUTE.EDIT_TAG} component={EditTag} />
|
||||||
</StackNavigatorBase.Navigator>
|
</StackNavigatorBase.Navigator>
|
||||||
</NavigationContainerBase>
|
</NavigationContainerBase>
|
||||||
|
141
src/screens/addMeme.tsx
Normal file
141
src/screens/addMeme.tsx
Normal 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
79
src/screens/addTag.tsx
Normal 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;
|
@@ -1,8 +1,8 @@
|
|||||||
import React from 'react';
|
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 { useNavigation } from '@react-navigation/native';
|
||||||
import { useDimensions } from '../contexts';
|
import { useDimensions } from '../contexts';
|
||||||
import { ScrollView } from 'react-native';
|
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
|
|
||||||
const EditMeme = () => {
|
const EditMeme = () => {
|
||||||
@@ -14,20 +14,17 @@ const EditMeme = () => {
|
|||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||||
<Appbar.Content title="Add Meme" />
|
<Appbar.Content title={'Edit Meme'} />
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
orientation == 'portrait' && styles.paddingVertical,
|
orientation == 'portrait' && styles.paddingVertical,
|
||||||
orientation == 'landscape' && styles.smallPaddingVertical,
|
orientation == 'landscape' && styles.smallPaddingVertical,
|
||||||
styles.paddingHorizontal,
|
styles.paddingHorizontal,
|
||||||
[styles.centered, styles.flex],
|
styles.flexGrow,
|
||||||
styles.fullSize,
|
styles.flexColumnSpaceBetween,
|
||||||
{ backgroundColor: colors.background },
|
{ backgroundColor: colors.background },
|
||||||
]}
|
]}></ScrollView>
|
||||||
nestedScrollEnabled>
|
|
||||||
<Text>Add Meme</Text>
|
|
||||||
</ScrollView>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -1,19 +1,12 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import {
|
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||||
TextInput,
|
|
||||||
Appbar,
|
|
||||||
HelperText,
|
|
||||||
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 { BSON, UpdateMode } from 'realm';
|
import { BSON } from 'realm';
|
||||||
import { useRealm } from '@realm/react';
|
import { useRealm } from '@realm/react';
|
||||||
import { TagPreview } from '../components';
|
import { TagEditor } from '../components';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import { generateRandomColor, isValidColor } from '../utilities';
|
|
||||||
import { useDimensions } from '../contexts';
|
import { useDimensions } from '../contexts';
|
||||||
import { ROUTE, RootStackParamList } from '../types';
|
import { ROUTE, RootStackParamList } from '../types';
|
||||||
import { Tag } from '../database';
|
import { Tag } from '../database';
|
||||||
@@ -26,62 +19,41 @@ const EditTag = ({
|
|||||||
const { orientation } = useDimensions();
|
const { orientation } = useDimensions();
|
||||||
const realm = useRealm();
|
const realm = useRealm();
|
||||||
|
|
||||||
const tagId = route.params?.id;
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const tag = tagId
|
const tag = realm.objectForPrimaryKey(
|
||||||
? realm.objectForPrimaryKey(Tag, BSON.UUID.createFromHexString(tagId))
|
Tag,
|
||||||
: undefined;
|
BSON.UUID.createFromHexString(route.params.id),
|
||||||
|
)!;
|
||||||
|
|
||||||
const [tagName, setTagName] = useState(tag?.name ?? 'newTag');
|
const [tagName, setTagName] = useState(tag.name);
|
||||||
const [tagColor, setTagColor] = useState(tag?.color ?? generateRandomColor());
|
const [tagColor, setTagColor] = useState(tag.color);
|
||||||
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
|
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
|
||||||
|
|
||||||
const [tagNameError, setTagNameError] = useState<string | undefined>();
|
const [tagNameError, setTagNameError] = useState<string | undefined>();
|
||||||
const [tagColorError, setTagColorError] = 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 = () => {
|
const handleSave = () => {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
realm.create(
|
tag.name = tagName;
|
||||||
Tag,
|
tag.color = tagColor;
|
||||||
{
|
tag.dateModified = new Date();
|
||||||
id: tag?.id,
|
|
||||||
name: tagName,
|
|
||||||
color: tagColor,
|
|
||||||
},
|
|
||||||
UpdateMode.Modified,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = () => {
|
const handleDelete = () => {
|
||||||
realm.write(() => {
|
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);
|
realm.delete(tag);
|
||||||
});
|
});
|
||||||
|
|
||||||
navigation.goBack();
|
navigation.goBack();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,50 +61,32 @@ const EditTag = ({
|
|||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||||
<Appbar.Content title={tag ? 'Edit Tag' : 'Add Tag'} />
|
<Appbar.Content title={'Edit Tag'} />
|
||||||
{tag && <Appbar.Action icon="delete" onPress={handleDelete} />}
|
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
orientation == 'portrait' && styles.paddingVertical,
|
orientation == 'portrait' && styles.paddingVertical,
|
||||||
orientation == 'landscape' && styles.smallPaddingVertical,
|
orientation == 'landscape' && styles.smallPaddingVertical,
|
||||||
styles.paddingHorizontal,
|
styles.paddingHorizontal,
|
||||||
styles.fullSize,
|
|
||||||
styles.flexGrow,
|
styles.flexGrow,
|
||||||
styles.flexColumnSpaceBetween,
|
styles.flexColumnSpaceBetween,
|
||||||
{ backgroundColor: colors.background },
|
{ backgroundColor: colors.background },
|
||||||
]}>
|
]}
|
||||||
|
nestedScrollEnabled>
|
||||||
<View style={[styles.flex, styles.justifyStart]}>
|
<View style={[styles.flex, styles.justifyStart]}>
|
||||||
<TagPreview name={tagName} color={validatedTagColor} />
|
<TagEditor
|
||||||
<TextInput
|
tagName={tagName}
|
||||||
mode="outlined"
|
setTagName={setTagName}
|
||||||
label="Tag Name"
|
tagColor={tagColor}
|
||||||
value={tagName}
|
setTagColor={setTagColor}
|
||||||
onChangeText={handleTagNameChange}
|
validatedTagColor={validatedTagColor}
|
||||||
error={!!tagNameError}
|
setValidatedTagColor={setValidatedTagColor}
|
||||||
autoCapitalize="none"
|
tagNameError={tagNameError}
|
||||||
selectTextOnFocus
|
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>
|
||||||
<View style={[styles.flex, styles.justifyEnd]}>
|
<View style={[styles.flex, styles.justifyEnd]}>
|
||||||
<Button
|
<Button
|
||||||
|
@@ -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 EditMeme } from './editMeme';
|
||||||
export { default as EditTag } from './editTag';
|
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 Settings } from './settings';
|
||||||
export { default as Tags } from './tags';
|
export { default as Tags } from './tags';
|
||||||
export { default as Welcome } from './welcome';
|
export { default as Welcome } from './welcome';
|
||||||
|
@@ -16,17 +16,17 @@ import { MEME_SORT, SORT_DIRECTION } from '../types';
|
|||||||
import { getSortIcon, getViewIcon } from '../utilities';
|
import { getSortIcon, getViewIcon } from '../utilities';
|
||||||
import {
|
import {
|
||||||
RootState,
|
RootState,
|
||||||
cycleHomeView,
|
cycleMemesView,
|
||||||
toggleHomeSortDirection,
|
toggleMemesSortDirection,
|
||||||
setHomeSortDirection,
|
setMemesSortDirection,
|
||||||
toggleHomeFavoritesOnly,
|
toggleMemesFavoritesOnly,
|
||||||
setHomeSort,
|
setMemesSort,
|
||||||
setHomeFilter,
|
setMemesFilter,
|
||||||
} from '../state';
|
} from '../state';
|
||||||
import { MEME_TYPE, Meme, memeTypePlural } from '../database';
|
import { MEME_TYPE, Meme, memeTypePlural } from '../database';
|
||||||
import { useDimensions } from '../contexts';
|
import { useDimensions } from '../contexts';
|
||||||
|
|
||||||
const homeStyles = StyleSheet.create({
|
const memesStyles = StyleSheet.create({
|
||||||
headerButtonView: {
|
headerButtonView: {
|
||||||
height: 50,
|
height: 50,
|
||||||
},
|
},
|
||||||
@@ -35,18 +35,18 @@ const homeStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const Home = () => {
|
const Memes = () => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation } = useDimensions();
|
const { orientation } = useDimensions();
|
||||||
const sort = useSelector((state: RootState) => state.home.sort);
|
const sort = useSelector((state: RootState) => state.memes.sort);
|
||||||
const sortDirection = useSelector(
|
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(
|
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 dispatch = useDispatch();
|
||||||
|
|
||||||
const [sortMenuVisible, setSortMenuVisible] = useState(false);
|
const [sortMenuVisible, setSortMenuVisible] = useState(false);
|
||||||
@@ -54,20 +54,20 @@ const Home = () => {
|
|||||||
|
|
||||||
const handleSortModeChange = (newSort: MEME_SORT) => {
|
const handleSortModeChange = (newSort: MEME_SORT) => {
|
||||||
if (newSort === sort) {
|
if (newSort === sort) {
|
||||||
dispatch(toggleHomeSortDirection());
|
dispatch(toggleMemesSortDirection());
|
||||||
} else {
|
} else {
|
||||||
dispatch(setHomeSort(newSort));
|
dispatch(setMemesSort(newSort));
|
||||||
if (newSort === MEME_SORT.TITLE) {
|
if (newSort === MEME_SORT.TITLE) {
|
||||||
dispatch(setHomeSortDirection(SORT_DIRECTION.ASCENDING));
|
dispatch(setMemesSortDirection(SORT_DIRECTION.ASCENDING));
|
||||||
} else {
|
} else {
|
||||||
dispatch(setHomeSortDirection(SORT_DIRECTION.DESCENDING));
|
dispatch(setMemesSortDirection(SORT_DIRECTION.DESCENDING));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setSortMenuVisible(false);
|
setSortMenuVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilterChange = (newFilter: MEME_TYPE | undefined) => {
|
const handleFilterChange = (newFilter: MEME_TYPE | undefined) => {
|
||||||
dispatch(setHomeFilter(newFilter));
|
dispatch(setMemesFilter(newFilter));
|
||||||
setFilterMenuVisible(false);
|
setFilterMenuVisible(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ const Home = () => {
|
|||||||
style={[
|
style={[
|
||||||
styles.flexRowSpaceBetween,
|
styles.flexRowSpaceBetween,
|
||||||
styles.alignCenter,
|
styles.alignCenter,
|
||||||
homeStyles.headerButtonView,
|
memesStyles.headerButtonView,
|
||||||
]}>
|
]}>
|
||||||
<View style={[styles.flexRow, styles.alignCenter]}>
|
<View style={[styles.flexRow, styles.alignCenter]}>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -127,13 +127,15 @@ const Home = () => {
|
|||||||
icon={getViewIcon(view)}
|
icon={getViewIcon(view)}
|
||||||
iconColor={colors.primary}
|
iconColor={colors.primary}
|
||||||
size={16}
|
size={16}
|
||||||
onPress={() => dispatch(cycleHomeView())}
|
animated
|
||||||
|
onPress={() => dispatch(cycleMemesView())}
|
||||||
/>
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
icon={favoritesOnly ? 'heart' : 'heart-outline'}
|
icon={favoritesOnly ? 'heart' : 'heart-outline'}
|
||||||
iconColor={colors.primary}
|
iconColor={colors.primary}
|
||||||
size={16}
|
size={16}
|
||||||
onPress={() => dispatch(toggleHomeFavoritesOnly())}
|
animated
|
||||||
|
onPress={() => dispatch(toggleMemesFavoritesOnly())}
|
||||||
/>
|
/>
|
||||||
<Menu
|
<Menu
|
||||||
visible={filterMenuVisible}
|
visible={filterMenuVisible}
|
||||||
@@ -172,7 +174,7 @@ const Home = () => {
|
|||||||
{memes.length === 0 && (
|
{memes.length === 0 && (
|
||||||
<HelperText
|
<HelperText
|
||||||
type={'info'}
|
type={'info'}
|
||||||
style={[homeStyles.helperText, styles.centerText]}>
|
style={[memesStyles.helperText, styles.centerText]}>
|
||||||
No memes found
|
No memes found
|
||||||
</HelperText>
|
</HelperText>
|
||||||
)}
|
)}
|
||||||
@@ -180,4 +182,4 @@ const Home = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Home;
|
export default Memes;
|
@@ -28,16 +28,16 @@ const SettingsScreen = () => {
|
|||||||
const noMedia = useSelector((state: RootState) => state.settings.noMedia);
|
const noMedia = useSelector((state: RootState) => state.settings.noMedia);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [optimizingDatabase, setOptimizingDatabase] = useState(false);
|
const [isOptimizingDatabase, setIsOptimizingDatabase] = useState(false);
|
||||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||||
|
|
||||||
const optimizeDatabase = () => {
|
const optimizeDatabase = () => {
|
||||||
setOptimizingDatabase(true);
|
setIsOptimizingDatabase(true);
|
||||||
// TODO: clean up missing / extra files
|
// TODO: clean up missing / extra files
|
||||||
setSnackbarMessage('Database optimized!');
|
setSnackbarMessage('Database optimized!');
|
||||||
setSnackbarVisible(true);
|
setSnackbarVisible(true);
|
||||||
setOptimizingDatabase(false);
|
setIsOptimizingDatabase(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -47,7 +47,6 @@ const SettingsScreen = () => {
|
|||||||
orientation == 'portrait' && styles.paddingTop,
|
orientation == 'portrait' && styles.paddingTop,
|
||||||
orientation == 'landscape' && styles.smallPaddingTop,
|
orientation == 'landscape' && styles.smallPaddingTop,
|
||||||
styles.paddingHorizontal,
|
styles.paddingHorizontal,
|
||||||
styles.fullSize,
|
|
||||||
{ backgroundColor: colors.background },
|
{ backgroundColor: colors.background },
|
||||||
]}>
|
]}>
|
||||||
<View>
|
<View>
|
||||||
@@ -55,10 +54,7 @@ const SettingsScreen = () => {
|
|||||||
<List.Subheader>Database</List.Subheader>
|
<List.Subheader>Database</List.Subheader>
|
||||||
<Button
|
<Button
|
||||||
mode="elevated"
|
mode="elevated"
|
||||||
style={{
|
loading={isOptimizingDatabase}
|
||||||
marginBottom: responsive.verticalScale(15),
|
|
||||||
}}
|
|
||||||
loading={optimizingDatabase}
|
|
||||||
onPress={optimizeDatabase}>
|
onPress={optimizeDatabase}>
|
||||||
Optimize Database Now
|
Optimize Database Now
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -2,7 +2,6 @@ import React, { useCallback, useRef, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
View,
|
View,
|
||||||
Text,
|
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
NativeScrollEvent,
|
NativeScrollEvent,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
@@ -13,6 +12,7 @@ import {
|
|||||||
HelperText,
|
HelperText,
|
||||||
Menu,
|
Menu,
|
||||||
Searchbar,
|
Searchbar,
|
||||||
|
Text,
|
||||||
TouchableRipple,
|
TouchableRipple,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from 'react-native-paper';
|
} from 'react-native-paper';
|
||||||
@@ -48,14 +48,13 @@ const tagsStyles = StyleSheet.create({
|
|||||||
height: 50,
|
height: 50,
|
||||||
},
|
},
|
||||||
tagRow: {
|
tagRow: {
|
||||||
flexWrap: 'wrap',
|
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
paddingVertical: 10,
|
paddingVertical: 10,
|
||||||
paddingHorizontal: 15,
|
paddingHorizontal: 15,
|
||||||
},
|
},
|
||||||
tagView: {
|
tagChip: {
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
maxWidth: '80%',
|
maxWidth: '80%',
|
||||||
},
|
},
|
||||||
@@ -195,6 +194,7 @@ const Tags = () => {
|
|||||||
<FlashList
|
<FlashList
|
||||||
ref={flashListRef}
|
ref={flashListRef}
|
||||||
data={tags}
|
data={tags}
|
||||||
|
keyExtractor={tag => tag.id.toHexString()}
|
||||||
estimatedItemSize={52}
|
estimatedItemSize={52}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
renderItem={({ item: tag }) => (
|
renderItem={({ item: tag }) => (
|
||||||
@@ -203,9 +203,7 @@ const Tags = () => {
|
|||||||
navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })
|
navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })
|
||||||
}>
|
}>
|
||||||
<View style={tagsStyles.tagRow}>
|
<View style={tagsStyles.tagRow}>
|
||||||
<View style={tagsStyles.tagView}>
|
<TagChip tag={tag} style={tagsStyles.tagChip} />
|
||||||
<TagChip tag={tag} />
|
|
||||||
</View>
|
|
||||||
<Text>{tag.memesLength}</Text>
|
<Text>{tag.memesLength}</Text>
|
||||||
</View>
|
</View>
|
||||||
</TouchableRipple>
|
</TouchableRipple>
|
||||||
|
@@ -1,87 +0,0 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { MEME_SORT, SORT_DIRECTION, VIEW } from '../types';
|
|
||||||
import { MEME_TYPE } from '../database';
|
|
||||||
|
|
||||||
interface HomeState {
|
|
||||||
sort: MEME_SORT;
|
|
||||||
sortDirection: SORT_DIRECTION;
|
|
||||||
view: VIEW;
|
|
||||||
favoritesOnly: boolean;
|
|
||||||
filter: MEME_TYPE | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: HomeState = {
|
|
||||||
sort: MEME_SORT.TITLE,
|
|
||||||
sortDirection: SORT_DIRECTION.ASCENDING,
|
|
||||||
view: VIEW.MASONRY,
|
|
||||||
favoritesOnly: false,
|
|
||||||
filter: undefined,
|
|
||||||
};
|
|
||||||
|
|
||||||
const homeSlice = createSlice({
|
|
||||||
name: 'home',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
setHomeSort: (state, action: PayloadAction<MEME_SORT>) => {
|
|
||||||
state.sort = action.payload;
|
|
||||||
},
|
|
||||||
setHomeSortDirection: (state, action: PayloadAction<SORT_DIRECTION>) => {
|
|
||||||
state.sortDirection = action.payload;
|
|
||||||
},
|
|
||||||
toggleHomeSortDirection: state => {
|
|
||||||
state.sortDirection ^= 1;
|
|
||||||
},
|
|
||||||
setHomeView: (state, action: PayloadAction<VIEW>) => {
|
|
||||||
state.view = action.payload;
|
|
||||||
},
|
|
||||||
cycleHomeView: state => {
|
|
||||||
switch (state.view) {
|
|
||||||
case VIEW.MASONRY: {
|
|
||||||
state.view = VIEW.GRID;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case VIEW.GRID: {
|
|
||||||
state.view = VIEW.LIST;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case VIEW.LIST: {
|
|
||||||
state.view = VIEW.MASONRY;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setHomeFavoritesOnly: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.favoritesOnly = action.payload;
|
|
||||||
},
|
|
||||||
toggleHomeFavoritesOnly: state => {
|
|
||||||
state.favoritesOnly = !state.favoritesOnly;
|
|
||||||
},
|
|
||||||
setHomeFilter: (state, action: PayloadAction<MEME_TYPE | undefined>) => {
|
|
||||||
state.filter = action.payload;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
setHomeSort,
|
|
||||||
setHomeSortDirection,
|
|
||||||
toggleHomeSortDirection,
|
|
||||||
setHomeView,
|
|
||||||
cycleHomeView,
|
|
||||||
setHomeFavoritesOnly,
|
|
||||||
toggleHomeFavoritesOnly,
|
|
||||||
setHomeFilter,
|
|
||||||
} = homeSlice.actions;
|
|
||||||
|
|
||||||
export {
|
|
||||||
type HomeState,
|
|
||||||
setHomeSort,
|
|
||||||
setHomeSortDirection,
|
|
||||||
toggleHomeSortDirection,
|
|
||||||
setHomeView,
|
|
||||||
cycleHomeView,
|
|
||||||
setHomeFavoritesOnly,
|
|
||||||
toggleHomeFavoritesOnly,
|
|
||||||
setHomeFilter,
|
|
||||||
};
|
|
||||||
export default homeSlice.reducer;
|
|
@@ -11,20 +11,20 @@ import {
|
|||||||
} from 'redux-persist';
|
} from 'redux-persist';
|
||||||
import { createRealmPersistStorage } from '@bankify/redux-persist-realm';
|
import { createRealmPersistStorage } from '@bankify/redux-persist-realm';
|
||||||
import settingsReducer from './settings';
|
import settingsReducer from './settings';
|
||||||
import homeReducer from './home';
|
import memesReducer from './memes';
|
||||||
import tagsReducer from './tags';
|
import tagsReducer from './tags';
|
||||||
import navigationReducer from './navigation';
|
import navigationReducer from './navigation';
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
settings: settingsReducer,
|
settings: settingsReducer,
|
||||||
home: homeReducer,
|
memes: memesReducer,
|
||||||
tags: tagsReducer,
|
tags: tagsReducer,
|
||||||
navigation: navigationReducer,
|
navigation: navigationReducer,
|
||||||
});
|
});
|
||||||
|
|
||||||
interface RootState {
|
interface RootState {
|
||||||
settings: ReturnType<typeof settingsReducer>;
|
settings: ReturnType<typeof settingsReducer>;
|
||||||
home: ReturnType<typeof homeReducer>;
|
memes: ReturnType<typeof memesReducer>;
|
||||||
tags: ReturnType<typeof tagsReducer>;
|
tags: ReturnType<typeof tagsReducer>;
|
||||||
navigation: ReturnType<typeof navigationReducer>;
|
navigation: ReturnType<typeof navigationReducer>;
|
||||||
}
|
}
|
||||||
@@ -57,16 +57,16 @@ export {
|
|||||||
validateSettings,
|
validateSettings,
|
||||||
} from './settings';
|
} from './settings';
|
||||||
export {
|
export {
|
||||||
type HomeState,
|
type MemesState,
|
||||||
setHomeSort,
|
setMemesSort,
|
||||||
setHomeSortDirection,
|
setMemesSortDirection,
|
||||||
toggleHomeSortDirection,
|
toggleMemesSortDirection,
|
||||||
setHomeView,
|
setMemesView,
|
||||||
cycleHomeView,
|
cycleMemesView,
|
||||||
setHomeFavoritesOnly,
|
setMemesFavoritesOnly,
|
||||||
toggleHomeFavoritesOnly,
|
toggleMemesFavoritesOnly,
|
||||||
setHomeFilter,
|
setMemesFilter,
|
||||||
} from './home';
|
} from './memes';
|
||||||
export {
|
export {
|
||||||
type TagsState,
|
type TagsState,
|
||||||
setTagsSort,
|
setTagsSort,
|
||||||
|
83
src/state/memes.ts
Normal file
83
src/state/memes.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { MEME_SORT, SORT_DIRECTION, VIEW } from '../types';
|
||||||
|
import { MEME_TYPE } from '../database';
|
||||||
|
|
||||||
|
interface MemesState {
|
||||||
|
sort: MEME_SORT;
|
||||||
|
sortDirection: SORT_DIRECTION;
|
||||||
|
view: VIEW;
|
||||||
|
favoritesOnly: boolean;
|
||||||
|
filter: MEME_TYPE | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: MemesState = {
|
||||||
|
sort: MEME_SORT.TITLE,
|
||||||
|
sortDirection: SORT_DIRECTION.ASCENDING,
|
||||||
|
view: VIEW.MASONRY,
|
||||||
|
favoritesOnly: false,
|
||||||
|
filter: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
const memesSlice = createSlice({
|
||||||
|
name: 'memes',
|
||||||
|
initialState,
|
||||||
|
reducers: {
|
||||||
|
setMemesSort: (state, action: PayloadAction<MEME_SORT>) => {
|
||||||
|
state.sort = action.payload;
|
||||||
|
},
|
||||||
|
setMemesSortDirection: (state, action: PayloadAction<SORT_DIRECTION>) => {
|
||||||
|
state.sortDirection = action.payload;
|
||||||
|
},
|
||||||
|
toggleMemesSortDirection: state => {
|
||||||
|
state.sortDirection ^= 1;
|
||||||
|
},
|
||||||
|
setMemesView: (state, action: PayloadAction<VIEW>) => {
|
||||||
|
state.view = action.payload;
|
||||||
|
},
|
||||||
|
cycleMemesView: state => {
|
||||||
|
switch (state.view) {
|
||||||
|
case VIEW.MASONRY: {
|
||||||
|
state.view = VIEW.LIST;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case VIEW.LIST: {
|
||||||
|
state.view = VIEW.MASONRY;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setMemesFavoritesOnly: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.favoritesOnly = action.payload;
|
||||||
|
},
|
||||||
|
toggleMemesFavoritesOnly: state => {
|
||||||
|
state.favoritesOnly = !state.favoritesOnly;
|
||||||
|
},
|
||||||
|
setMemesFilter: (state, action: PayloadAction<MEME_TYPE | undefined>) => {
|
||||||
|
state.filter = action.payload;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
setMemesSort,
|
||||||
|
setMemesSortDirection,
|
||||||
|
toggleMemesSortDirection,
|
||||||
|
setMemesView,
|
||||||
|
cycleMemesView,
|
||||||
|
setMemesFavoritesOnly,
|
||||||
|
toggleMemesFavoritesOnly,
|
||||||
|
setMemesFilter,
|
||||||
|
} = memesSlice.actions;
|
||||||
|
|
||||||
|
export {
|
||||||
|
type MemesState,
|
||||||
|
setMemesSort,
|
||||||
|
setMemesSortDirection,
|
||||||
|
toggleMemesSortDirection,
|
||||||
|
setMemesView,
|
||||||
|
cycleMemesView,
|
||||||
|
setMemesFavoritesOnly,
|
||||||
|
toggleMemesFavoritesOnly,
|
||||||
|
setMemesFilter,
|
||||||
|
};
|
||||||
|
export default memesSlice.reducer;
|
@@ -38,12 +38,18 @@ const styles = StyleSheet.create({
|
|||||||
centerText: {
|
centerText: {
|
||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
},
|
},
|
||||||
|
selfCenter: {
|
||||||
|
alignSelf: 'center',
|
||||||
|
},
|
||||||
flex: {
|
flex: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
},
|
},
|
||||||
flexGrow: {
|
flexGrow: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
},
|
},
|
||||||
|
flexShrink: {
|
||||||
|
flexShrink: 1,
|
||||||
|
},
|
||||||
flexRow: {
|
flexRow: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
},
|
},
|
||||||
@@ -61,6 +67,9 @@ const styles = StyleSheet.create({
|
|||||||
flexRowReverse: {
|
flexRowReverse: {
|
||||||
flexDirection: 'row-reverse',
|
flexDirection: 'row-reverse',
|
||||||
},
|
},
|
||||||
|
flexWrap: {
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
justifyStart: {
|
justifyStart: {
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
},
|
},
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
export { ROUTE, type RootStackParamList } from './route';
|
export { ROUTE, type RootStackParamList } from './route';
|
||||||
export {
|
export {
|
||||||
MEME_SORT,
|
MEME_SORT,
|
||||||
homeSortQuery,
|
memesSortQuery,
|
||||||
TAG_SORT,
|
TAG_SORT,
|
||||||
tagSortQuery,
|
tagSortQuery,
|
||||||
SORT_DIRECTION,
|
SORT_DIRECTION,
|
||||||
|
@@ -1,21 +1,39 @@
|
|||||||
|
import { DocumentPickerResponse } from 'react-native-document-picker';
|
||||||
|
|
||||||
enum ROUTE {
|
enum ROUTE {
|
||||||
MAIN = 'Main',
|
MAIN = 'Main',
|
||||||
HOME = 'Home',
|
MEMES = 'Memes',
|
||||||
TAGS = 'Tags',
|
TAGS = 'Tags',
|
||||||
SETTINGS = 'Settings',
|
SETTINGS = 'Settings',
|
||||||
|
ADD_MEME = 'Add Meme',
|
||||||
EDIT_MEME = 'Edit Meme',
|
EDIT_MEME = 'Edit Meme',
|
||||||
|
ADD_TAG = 'Add Tag',
|
||||||
EDIT_TAG = 'Edit Tag',
|
EDIT_TAG = 'Edit Tag',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface AddMemeRouteParamsFromFiles {
|
||||||
|
uri: DocumentPickerResponse[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EditMemeRouteParams {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface EditTagRouteParams {
|
interface EditTagRouteParams {
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RootStackParamList {
|
interface RootStackParamList {
|
||||||
[key: string]: undefined | EditTagRouteParams;
|
[key: string]:
|
||||||
|
| undefined
|
||||||
|
| AddMemeRouteParamsFromFiles
|
||||||
|
| EditMemeRouteParams
|
||||||
|
| EditTagRouteParams;
|
||||||
[ROUTE.MAIN]: undefined;
|
[ROUTE.MAIN]: undefined;
|
||||||
[ROUTE.EDIT_MEME]: undefined;
|
[ROUTE.ADD_MEME]: AddMemeRouteParamsFromFiles;
|
||||||
[ROUTE.EDIT_TAG]: EditTagRouteParams | undefined;
|
[ROUTE.EDIT_MEME]: EditMemeRouteParams;
|
||||||
|
[ROUTE.ADD_TAG]: undefined;
|
||||||
|
[ROUTE.EDIT_TAG]: EditTagRouteParams;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ROUTE, type RootStackParamList };
|
export { ROUTE, type RootStackParamList };
|
||||||
|
@@ -7,7 +7,7 @@ enum MEME_SORT {
|
|||||||
SIZE = 'Size',
|
SIZE = 'Size',
|
||||||
}
|
}
|
||||||
|
|
||||||
const homeSortQuery = (sort: MEME_SORT) => {
|
const memesSortQuery = (sort: MEME_SORT) => {
|
||||||
switch (sort) {
|
switch (sort) {
|
||||||
case MEME_SORT.TITLE: {
|
case MEME_SORT.TITLE: {
|
||||||
return 'title';
|
return 'title';
|
||||||
@@ -36,6 +36,7 @@ enum TAG_SORT {
|
|||||||
MEMES_LENGTH = 'Items',
|
MEMES_LENGTH = 'Items',
|
||||||
DATE_CREATED = 'Date Created',
|
DATE_CREATED = 'Date Created',
|
||||||
DATE_MODIFIED = 'Date Modified',
|
DATE_MODIFIED = 'Date Modified',
|
||||||
|
DATE_USED = 'Last Used',
|
||||||
TIMES_USED = 'Times Used',
|
TIMES_USED = 'Times Used',
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +60,9 @@ const tagSortQuery = (sort: TAG_SORT) => {
|
|||||||
case TAG_SORT.TIMES_USED: {
|
case TAG_SORT.TIMES_USED: {
|
||||||
return 'timesUsed';
|
return 'timesUsed';
|
||||||
}
|
}
|
||||||
|
case TAG_SORT.DATE_USED: {
|
||||||
|
return 'dateUsed';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,4 +71,4 @@ enum SORT_DIRECTION {
|
|||||||
DESCENDING = 1,
|
DESCENDING = 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
export { MEME_SORT, homeSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION };
|
export { MEME_SORT, memesSortQuery, TAG_SORT, tagSortQuery, SORT_DIRECTION };
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
enum VIEW {
|
enum VIEW {
|
||||||
MASONRY = 'Masonry',
|
MASONRY = 'Masonry',
|
||||||
GRID = 'Grid',
|
|
||||||
LIST = 'List',
|
LIST = 'List',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,8 +11,8 @@ const getContrastColor = (hexColor: string) => {
|
|||||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||||
|
|
||||||
return brightness > 128
|
return brightness > 128
|
||||||
? lightTheme.colors.onSurface
|
? lightTheme.colors.onBackground
|
||||||
: darkTheme.colors.onSurface;
|
: darkTheme.colors.onBackground;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isHexColor = (color: string) => {
|
const isHexColor = (color: string) => {
|
||||||
|
5
src/utilities/database.ts
Normal file
5
src/utilities/database.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const multipleIdQuery = (ids: string[]) => {
|
||||||
|
return `id in {${ids.map(id => `uuid(${id})`).join(',')}}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { multipleIdQuery };
|
33
src/utilities/filesystem.ts
Normal file
33
src/utilities/filesystem.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { MEME_TYPE } from '../database';
|
||||||
|
|
||||||
|
const allowedImageMimeTypes = [
|
||||||
|
'image/bmp',
|
||||||
|
'image/jpeg',
|
||||||
|
'image/png',
|
||||||
|
'image/webp',
|
||||||
|
];
|
||||||
|
|
||||||
|
const allowedGifMimeTypes = ['image/gif'];
|
||||||
|
|
||||||
|
const allowedMimeTypes = [...allowedImageMimeTypes, ...allowedGifMimeTypes];
|
||||||
|
|
||||||
|
const getMemeType = (mimeType: string) => {
|
||||||
|
switch (mimeType) {
|
||||||
|
case 'image/bmp':
|
||||||
|
case 'image/jpeg':
|
||||||
|
case 'image/png':
|
||||||
|
case 'image/webp': {
|
||||||
|
return MEME_TYPE.IMAGE;
|
||||||
|
}
|
||||||
|
case 'image/gif': {
|
||||||
|
return MEME_TYPE.GIF;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
allowedImageMimeTypes,
|
||||||
|
allowedGifMimeTypes,
|
||||||
|
allowedMimeTypes,
|
||||||
|
getMemeType,
|
||||||
|
};
|
@@ -17,7 +17,8 @@ const getSortIcon = (
|
|||||||
case MEME_SORT.DATE_MODIFIED:
|
case MEME_SORT.DATE_MODIFIED:
|
||||||
case MEME_SORT.DATE_USED:
|
case MEME_SORT.DATE_USED:
|
||||||
case TAG_SORT.DATE_CREATED:
|
case TAG_SORT.DATE_CREATED:
|
||||||
case TAG_SORT.DATE_MODIFIED: {
|
case TAG_SORT.DATE_MODIFIED:
|
||||||
|
case TAG_SORT.DATE_USED: {
|
||||||
sortIcon = 'sort-calendar';
|
sortIcon = 'sort-calendar';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -49,9 +50,6 @@ const getViewIcon = (view: VIEW) => {
|
|||||||
case VIEW.MASONRY: {
|
case VIEW.MASONRY: {
|
||||||
return 'view-dashboard';
|
return 'view-dashboard';
|
||||||
}
|
}
|
||||||
case VIEW.GRID: {
|
|
||||||
return 'view-grid';
|
|
||||||
}
|
|
||||||
case VIEW.LIST: {
|
case VIEW.LIST: {
|
||||||
return 'view-list';
|
return 'view-list';
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,15 @@ export {
|
|||||||
isRgbColor,
|
isRgbColor,
|
||||||
isValidColor,
|
isValidColor,
|
||||||
rgbToHex,
|
rgbToHex,
|
||||||
generateRandomColor
|
generateRandomColor,
|
||||||
} from './color';
|
} from './color';
|
||||||
export { packageName, appName, fileProvider, noOp } from './constants';
|
export { packageName, appName, fileProvider, noOp } from './constants';
|
||||||
|
export { multipleIdQuery } from './database';
|
||||||
|
export {
|
||||||
|
allowedImageMimeTypes,
|
||||||
|
allowedGifMimeTypes,
|
||||||
|
allowedMimeTypes,
|
||||||
|
getMemeType,
|
||||||
|
} from './filesystem';
|
||||||
export { isPermissionForPath, clearPermissions } from './permissions';
|
export { isPermissionForPath, clearPermissions } from './permissions';
|
||||||
export { getSortIcon, getViewIcon } from './icon';
|
export { getSortIcon, getViewIcon } from './icon';
|
||||||
|
Reference in New Issue
Block a user