Add meme-adding logic
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -118,6 +118,10 @@ dependencies {
|
||||
} else {
|
||||
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)
|
||||
|
51
package-lock.json
generated
51
package-lock.json
generated
@@ -20,10 +20,10 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.2",
|
||||
"react-native-document-picker": "^9.0.1",
|
||||
"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-mime-types": "^2.4.0",
|
||||
"react-native-paper": "^5.9.1",
|
||||
"react-native-reanimated": "^3.3.0",
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "5.9.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",
|
||||
@@ -25752,12 +25762,6 @@
|
||||
"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": {
|
||||
"version": "3.0.4",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "5.9.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-paper/-/react-native-paper-5.9.1.tgz",
|
||||
|
@@ -25,10 +25,10 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.2",
|
||||
"react-native-document-picker": "^9.0.1",
|
||||
"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-mime-types": "^2.4.0",
|
||||
"react-native-paper": "^5.9.1",
|
||||
"react-native-reanimated": "^3.3.0",
|
||||
"react-native-safe-area-context": "^4.6.4",
|
||||
|
@@ -3,8 +3,10 @@ import { Keyboard } from 'react-native';
|
||||
import { FAB } from 'react-native-paper';
|
||||
import { ParamListBase, useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { pick } from 'react-native-document-picker';
|
||||
import { useDimensions } from '../contexts';
|
||||
import { ROUTE } from '../types';
|
||||
import { allowedMimeTypes, noOp } from '../utilities';
|
||||
|
||||
const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
|
||||
const { navigate } =
|
||||
@@ -39,22 +41,34 @@ const FloatingActionButton = ({ visible = true }: { visible?: boolean }) => {
|
||||
{
|
||||
icon: 'tag',
|
||||
label: 'Tag',
|
||||
onPress: () => navigate(ROUTE.EDIT_TAG),
|
||||
onPress: () => navigate(ROUTE.ADD_TAG),
|
||||
},
|
||||
{
|
||||
icon: 'note-text',
|
||||
label: 'Text',
|
||||
onPress: () => navigate(ROUTE.EDIT_MEME),
|
||||
onPress: () => {
|
||||
throw new Error('Not yet implemented');
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: 'image-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)}
|
||||
onPress={() => {
|
||||
if (state) navigate(ROUTE.EDIT_MEME);
|
||||
onPress={async () => {
|
||||
if (!state) return;
|
||||
const res = await pick({ type: allowedMimeTypes }).catch(noOp);
|
||||
if (!res) return;
|
||||
navigate(ROUTE.ADD_MEME, { uri: res });
|
||||
}}
|
||||
style={{
|
||||
paddingBottom: responsive.verticalScale(75),
|
||||
|
@@ -10,7 +10,7 @@ const hideableHeaderStyles = StyleSheet.create({
|
||||
top: 0,
|
||||
left: 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 HideableBottomNavigationBar } from './hideableBottomNavigationBar';
|
||||
export { default as HideableHeader } from './hideableHeader';
|
||||
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 { Chip } from 'react-native-paper';
|
||||
import { Chip, useTheme } from 'react-native-paper';
|
||||
import { Tag } from '../../database';
|
||||
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);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
{...props}
|
||||
icon={() => {
|
||||
return <FontAwesome5 name="tag" color={contrastColor} />;
|
||||
return (
|
||||
<FontAwesome5 name="tag" color={active ? contrastColor : tag.color} />
|
||||
);
|
||||
}}
|
||||
compact
|
||||
style={[
|
||||
{
|
||||
backgroundColor: tag.color,
|
||||
},
|
||||
]}
|
||||
textStyle={{ color: contrastColor }}>
|
||||
theme={chipTheme}
|
||||
mode={active ? 'flat' : 'outlined'}
|
||||
style={[tagChipStyles.chip, props.style]}
|
||||
textStyle={{
|
||||
color: active ? contrastColor : theme.colors.onBackground,
|
||||
}}
|
||||
onPress={onPress}>
|
||||
{'#' + tag.name}
|
||||
</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 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 { useDimensions } from '../../contexts';
|
||||
import { getContrastColor } from '../../utilities';
|
||||
@@ -16,7 +16,19 @@ const tagPreviewStyles = StyleSheet.create({
|
||||
});
|
||||
|
||||
const TagPreview = ({ name, color }: { name: string; color: string }) => {
|
||||
const theme = useTheme();
|
||||
const { responsive } = useDimensions();
|
||||
|
||||
const chipTheme = useMemo(() => {
|
||||
return {
|
||||
...theme,
|
||||
colors: {
|
||||
...theme.colors,
|
||||
secondaryContainer: color,
|
||||
},
|
||||
};
|
||||
}, [theme, color]);
|
||||
|
||||
const contrastColor = getContrastColor(color);
|
||||
|
||||
return (
|
||||
@@ -33,12 +45,8 @@ const TagPreview = ({ name, color }: { name: string; color: string }) => {
|
||||
return <FontAwesome5 name="tag" size={14} color={contrastColor} />;
|
||||
}}
|
||||
elevated
|
||||
style={[
|
||||
tagPreviewStyles.chip,
|
||||
{
|
||||
backgroundColor: color,
|
||||
},
|
||||
]}
|
||||
style={tagPreviewStyles.chip}
|
||||
theme={chipTheme}
|
||||
textStyle={[tagPreviewStyles.text, { color: contrastColor }]}>
|
||||
{'#' + name}
|
||||
</Chip>
|
||||
|
@@ -21,7 +21,7 @@ interface DimensionsContext {
|
||||
orientation: 'portrait' | 'landscape';
|
||||
dimensions: ScaledSize;
|
||||
responsive: ScaleFunctions;
|
||||
static: ScaleFunctions;
|
||||
fixed: ScaleFunctions;
|
||||
}
|
||||
|
||||
const createScaleFunctions = (dimensionsIn: ScaledSize) => {
|
||||
@@ -56,7 +56,7 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => {
|
||||
}, []);
|
||||
|
||||
const responsiveScale = createScaleFunctions(dimensions);
|
||||
const staticScale = createScaleFunctions(initialDimensions);
|
||||
const fixedScale = createScaleFunctions(initialDimensions);
|
||||
|
||||
useEffect(() => {
|
||||
const onChange = ({ window }: { window: ScaledSize }) => {
|
||||
@@ -76,7 +76,7 @@ const DimensionsProvider = ({ children }: { children: ReactNode }) => {
|
||||
orientation,
|
||||
dimensions,
|
||||
responsive: responsiveScale,
|
||||
static: staticScale,
|
||||
fixed: fixedScale,
|
||||
}}>
|
||||
{children}
|
||||
</DimensionsContext.Provider>
|
||||
|
@@ -1,2 +1,2 @@
|
||||
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 } from 'realm';
|
||||
import { BSON, Object, ObjectSchema } from 'realm';
|
||||
import { Tag } from './tag';
|
||||
|
||||
enum MEME_TYPE {
|
||||
@@ -20,33 +19,36 @@ const memeTypePlural = {
|
||||
[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;
|
||||
type!: MEME_TYPE;
|
||||
uri!: Realm.List<string>;
|
||||
uri!: string[];
|
||||
hash!: string[];
|
||||
size!: number;
|
||||
title!: string;
|
||||
description?: string;
|
||||
isFavorite!: boolean;
|
||||
tags!: Realm.List<Tag>;
|
||||
tags!: Tag[] | Set<Tag>;
|
||||
tagsLength!: number;
|
||||
dateCreated!: Date;
|
||||
dateModified!: Date;
|
||||
dateUsed?: Date;
|
||||
timesUsed!: number;
|
||||
|
||||
static schema: Realm.ObjectSchema = {
|
||||
static schema: ObjectSchema = {
|
||||
name: 'Meme',
|
||||
primaryKey: 'id',
|
||||
properties: {
|
||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||
type: { type: 'string', indexed: true },
|
||||
uri: 'string[]',
|
||||
hash: 'string[]',
|
||||
size: 'int',
|
||||
title: 'string',
|
||||
description: 'string?',
|
||||
isFavorite: { type: 'bool', indexed: true, default: false },
|
||||
tags: { type: 'list', objectType: 'Tag', default: [] },
|
||||
tags: { type: 'set', objectType: 'Tag', default: [] },
|
||||
tagsLength: { type: 'int', default: 0 },
|
||||
dateCreated: { type: 'date', default: () => new Date() },
|
||||
dateModified: { type: 'date', default: () => new Date() },
|
||||
|
@@ -1,43 +1,34 @@
|
||||
import { Realm } from '@realm/react';
|
||||
import { BSON } from 'realm';
|
||||
import { BSON, Object, ObjectSchema } from 'realm';
|
||||
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;
|
||||
name!: string;
|
||||
color!: string;
|
||||
memes!: Realm.List<Meme>;
|
||||
memes!: Meme[] | Set<Meme>;
|
||||
memesLength!: number;
|
||||
dateCreated!: Date;
|
||||
dateModified!: Date;
|
||||
dateUsed?: Date;
|
||||
timesUsed!: number;
|
||||
|
||||
static schema: Realm.ObjectSchema = {
|
||||
static schema: ObjectSchema = {
|
||||
name: 'Tag',
|
||||
primaryKey: 'id',
|
||||
properties: {
|
||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||
name: { type: 'string', indexed: true },
|
||||
color: 'string',
|
||||
memes: { type: 'list', objectType: 'Meme', default: [] },
|
||||
color: { type: 'string', default: () => generateRandomColor() },
|
||||
memes: { type: 'set', objectType: 'Meme', default: [] },
|
||||
memesLength: { type: 'int', default: 0 },
|
||||
dateCreated: { type: 'date', default: () => new Date() },
|
||||
dateModified: { type: 'date', default: () => new Date() },
|
||||
dateUsed: 'date?',
|
||||
timesUsed: { type: 'int', default: 0 },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const deleteTag = (realm: Realm, tag: Tag) => {
|
||||
realm.write(() => {
|
||||
realm.delete(tag);
|
||||
});
|
||||
};
|
||||
|
||||
const deleteAllTags = (realm: Realm) => {
|
||||
realm.write(() => {
|
||||
realm.delete(realm.objects<Tag>('Tag'));
|
||||
});
|
||||
};
|
||||
|
||||
export { Tag, deleteTag, deleteAllTags };
|
||||
export { Tag };
|
||||
|
@@ -5,7 +5,15 @@ import { createBottomTabNavigator } from '@react-navigation/bottom-tabs';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
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 {
|
||||
FloatingActionButton,
|
||||
@@ -19,7 +27,7 @@ const TabNavigator = () => {
|
||||
(state: RootState) => state.navigation.navVisible,
|
||||
);
|
||||
|
||||
const [route, setRoute] = React.useState(ROUTE.HOME);
|
||||
const [route, setRoute] = React.useState(ROUTE.MEMES);
|
||||
const TabNavigatorBase = createBottomTabNavigator();
|
||||
|
||||
return (
|
||||
@@ -27,6 +35,7 @@ const TabNavigator = () => {
|
||||
<TabNavigatorBase.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
lazy: false,
|
||||
}}
|
||||
tabBar={({ navigation, state, descriptors, insets }) => (
|
||||
<HideableBottomNavigationBar
|
||||
@@ -39,11 +48,11 @@ const TabNavigator = () => {
|
||||
/>
|
||||
)}>
|
||||
<TabNavigatorBase.Screen
|
||||
name={ROUTE.HOME}
|
||||
component={Home}
|
||||
name={ROUTE.MEMES}
|
||||
component={Memes}
|
||||
options={{
|
||||
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',
|
||||
}}>
|
||||
<StackNavigatorBase.Screen name={ROUTE.MAIN} component={TabNavigator} />
|
||||
<StackNavigatorBase.Screen name={ROUTE.ADD_MEME} component={AddMeme} />
|
||||
<StackNavigatorBase.Screen
|
||||
name={ROUTE.EDIT_MEME}
|
||||
component={EditMeme}
|
||||
/>
|
||||
<StackNavigatorBase.Screen name={ROUTE.ADD_TAG} component={AddTag} />
|
||||
<StackNavigatorBase.Screen name={ROUTE.EDIT_TAG} component={EditTag} />
|
||||
</StackNavigatorBase.Navigator>
|
||||
</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 { Appbar, Text, useTheme } from 'react-native-paper';
|
||||
import { ScrollView } from 'react-native';
|
||||
import { Appbar, useTheme } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useDimensions } from '../contexts';
|
||||
import { ScrollView } from 'react-native';
|
||||
import styles from '../styles';
|
||||
|
||||
const EditMeme = () => {
|
||||
@@ -14,20 +14,17 @@ const EditMeme = () => {
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.Content title="Add Meme" />
|
||||
<Appbar.Content title={'Edit Meme'} />
|
||||
</Appbar.Header>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
orientation == 'portrait' && styles.paddingVertical,
|
||||
orientation == 'landscape' && styles.smallPaddingVertical,
|
||||
styles.paddingHorizontal,
|
||||
[styles.centered, styles.flex],
|
||||
styles.fullSize,
|
||||
styles.flexGrow,
|
||||
styles.flexColumnSpaceBetween,
|
||||
{ backgroundColor: colors.background },
|
||||
]}
|
||||
nestedScrollEnabled>
|
||||
<Text>Add Meme</Text>
|
||||
</ScrollView>
|
||||
]}></ScrollView>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@@ -1,19 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import {
|
||||
TextInput,
|
||||
Appbar,
|
||||
HelperText,
|
||||
Button,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { BSON, UpdateMode } from 'realm';
|
||||
import { BSON } from 'realm';
|
||||
import { useRealm } from '@realm/react';
|
||||
import { TagPreview } from '../components';
|
||||
import { TagEditor } from '../components';
|
||||
import styles from '../styles';
|
||||
import { generateRandomColor, isValidColor } from '../utilities';
|
||||
import { useDimensions } from '../contexts';
|
||||
import { ROUTE, RootStackParamList } from '../types';
|
||||
import { Tag } from '../database';
|
||||
@@ -26,62 +19,41 @@ const EditTag = ({
|
||||
const { orientation } = useDimensions();
|
||||
const realm = useRealm();
|
||||
|
||||
const tagId = route.params?.id;
|
||||
const tag = tagId
|
||||
? realm.objectForPrimaryKey(Tag, BSON.UUID.createFromHexString(tagId))
|
||||
: undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const tag = realm.objectForPrimaryKey(
|
||||
Tag,
|
||||
BSON.UUID.createFromHexString(route.params.id),
|
||||
)!;
|
||||
|
||||
const [tagName, setTagName] = useState(tag?.name ?? 'newTag');
|
||||
const [tagColor, setTagColor] = useState(tag?.color ?? generateRandomColor());
|
||||
const [tagName, setTagName] = useState(tag.name);
|
||||
const [tagColor, setTagColor] = useState(tag.color);
|
||||
const [validatedTagColor, setValidatedTagColor] = useState(tagColor);
|
||||
|
||||
const [tagNameError, setTagNameError] = useState<string | undefined>();
|
||||
const [tagColorError, setTagColorError] = useState<string | undefined>();
|
||||
|
||||
const handleTagNameChange = (name: string) => {
|
||||
setTagName(name);
|
||||
|
||||
if (name.length === 0) {
|
||||
setTagNameError('Tag name cannot be empty');
|
||||
} else if (name.includes(' ')) {
|
||||
setTagNameError('Tag name cannot contain spaces');
|
||||
} else {
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setTagNameError(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTagColorChange = (color: string) => {
|
||||
setTagColor(color);
|
||||
|
||||
if (isValidColor(color)) {
|
||||
setValidatedTagColor(color);
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setTagColorError(undefined);
|
||||
} else {
|
||||
setTagColorError('Color must be a valid hex or rgb value');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
realm.write(() => {
|
||||
realm.create(
|
||||
Tag,
|
||||
{
|
||||
id: tag?.id,
|
||||
name: tagName,
|
||||
color: tagColor,
|
||||
},
|
||||
UpdateMode.Modified,
|
||||
);
|
||||
tag.name = tagName;
|
||||
tag.color = tagColor;
|
||||
tag.dateModified = new Date();
|
||||
});
|
||||
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
realm.write(() => {
|
||||
for (const meme of tag.memes) {
|
||||
meme.dateModified = new Date();
|
||||
const tags = meme.tags as Set<Tag>;
|
||||
tags.delete(tag);
|
||||
meme.tagsLength = tags.size;
|
||||
}
|
||||
|
||||
realm.delete(tag);
|
||||
});
|
||||
|
||||
navigation.goBack();
|
||||
};
|
||||
|
||||
@@ -89,50 +61,32 @@ const EditTag = ({
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.Content title={tag ? 'Edit Tag' : 'Add Tag'} />
|
||||
{tag && <Appbar.Action icon="delete" onPress={handleDelete} />}
|
||||
<Appbar.Content title={'Edit Tag'} />
|
||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||
</Appbar.Header>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
orientation == 'portrait' && styles.paddingVertical,
|
||||
orientation == 'landscape' && styles.smallPaddingVertical,
|
||||
styles.paddingHorizontal,
|
||||
styles.fullSize,
|
||||
styles.flexGrow,
|
||||
styles.flexColumnSpaceBetween,
|
||||
{ backgroundColor: colors.background },
|
||||
]}>
|
||||
]}
|
||||
nestedScrollEnabled>
|
||||
<View style={[styles.flex, styles.justifyStart]}>
|
||||
<TagPreview name={tagName} color={validatedTagColor} />
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Tag Name"
|
||||
value={tagName}
|
||||
onChangeText={handleTagNameChange}
|
||||
error={!!tagNameError}
|
||||
autoCapitalize="none"
|
||||
selectTextOnFocus
|
||||
<TagEditor
|
||||
tagName={tagName}
|
||||
setTagName={setTagName}
|
||||
tagColor={tagColor}
|
||||
setTagColor={setTagColor}
|
||||
validatedTagColor={validatedTagColor}
|
||||
setValidatedTagColor={setValidatedTagColor}
|
||||
tagNameError={tagNameError}
|
||||
setTagNameError={setTagNameError}
|
||||
tagColorError={tagColorError}
|
||||
setTagColorError={setTagColorError}
|
||||
/>
|
||||
<HelperText type="error" visible={!!tagNameError}>
|
||||
{tagNameError}
|
||||
</HelperText>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Tag Color"
|
||||
value={tagColor}
|
||||
onChangeText={handleTagColorChange}
|
||||
error={!!tagColorError}
|
||||
autoCorrect={false}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon="palette"
|
||||
onPress={() => handleTagColorChange(generateRandomColor())}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<HelperText type="error" visible={!!tagColorError}>
|
||||
{tagColorError}
|
||||
</HelperText>
|
||||
</View>
|
||||
<View style={[styles.flex, styles.justifyEnd]}>
|
||||
<Button
|
||||
|
@@ -1,6 +1,8 @@
|
||||
export { default as AddMeme } from './addMeme';
|
||||
export { default as AddTag } from './addTag';
|
||||
export { default as EditMeme } from './editMeme';
|
||||
export { default as EditTag } from './editTag';
|
||||
export { default as Home } from './home';
|
||||
export { default as Memes } from './memes';
|
||||
export { default as Settings } from './settings';
|
||||
export { default as Tags } from './tags';
|
||||
export { default as Welcome } from './welcome';
|
||||
|
@@ -16,17 +16,17 @@ import { MEME_SORT, SORT_DIRECTION } from '../types';
|
||||
import { getSortIcon, getViewIcon } from '../utilities';
|
||||
import {
|
||||
RootState,
|
||||
cycleHomeView,
|
||||
toggleHomeSortDirection,
|
||||
setHomeSortDirection,
|
||||
toggleHomeFavoritesOnly,
|
||||
setHomeSort,
|
||||
setHomeFilter,
|
||||
cycleMemesView,
|
||||
toggleMemesSortDirection,
|
||||
setMemesSortDirection,
|
||||
toggleMemesFavoritesOnly,
|
||||
setMemesSort,
|
||||
setMemesFilter,
|
||||
} from '../state';
|
||||
import { MEME_TYPE, Meme, memeTypePlural } from '../database';
|
||||
import { useDimensions } from '../contexts';
|
||||
|
||||
const homeStyles = StyleSheet.create({
|
||||
const memesStyles = StyleSheet.create({
|
||||
headerButtonView: {
|
||||
height: 50,
|
||||
},
|
||||
@@ -35,18 +35,18 @@ const homeStyles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const Home = () => {
|
||||
const Memes = () => {
|
||||
const { colors } = useTheme();
|
||||
const { orientation } = useDimensions();
|
||||
const sort = useSelector((state: RootState) => state.home.sort);
|
||||
const sort = useSelector((state: RootState) => state.memes.sort);
|
||||
const sortDirection = useSelector(
|
||||
(state: RootState) => state.home.sortDirection,
|
||||
(state: RootState) => state.memes.sortDirection,
|
||||
);
|
||||
const view = useSelector((state: RootState) => state.home.view);
|
||||
const view = useSelector((state: RootState) => state.memes.view);
|
||||
const favoritesOnly = useSelector(
|
||||
(state: RootState) => state.home.favoritesOnly,
|
||||
(state: RootState) => state.memes.favoritesOnly,
|
||||
);
|
||||
const filter = useSelector((state: RootState) => state.home.filter);
|
||||
const filter = useSelector((state: RootState) => state.memes.filter);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [sortMenuVisible, setSortMenuVisible] = useState(false);
|
||||
@@ -54,20 +54,20 @@ const Home = () => {
|
||||
|
||||
const handleSortModeChange = (newSort: MEME_SORT) => {
|
||||
if (newSort === sort) {
|
||||
dispatch(toggleHomeSortDirection());
|
||||
dispatch(toggleMemesSortDirection());
|
||||
} else {
|
||||
dispatch(setHomeSort(newSort));
|
||||
dispatch(setMemesSort(newSort));
|
||||
if (newSort === MEME_SORT.TITLE) {
|
||||
dispatch(setHomeSortDirection(SORT_DIRECTION.ASCENDING));
|
||||
dispatch(setMemesSortDirection(SORT_DIRECTION.ASCENDING));
|
||||
} else {
|
||||
dispatch(setHomeSortDirection(SORT_DIRECTION.DESCENDING));
|
||||
dispatch(setMemesSortDirection(SORT_DIRECTION.DESCENDING));
|
||||
}
|
||||
}
|
||||
setSortMenuVisible(false);
|
||||
};
|
||||
|
||||
const handleFilterChange = (newFilter: MEME_TYPE | undefined) => {
|
||||
dispatch(setHomeFilter(newFilter));
|
||||
dispatch(setMemesFilter(newFilter));
|
||||
setFilterMenuVisible(false);
|
||||
};
|
||||
|
||||
@@ -92,7 +92,7 @@ const Home = () => {
|
||||
style={[
|
||||
styles.flexRowSpaceBetween,
|
||||
styles.alignCenter,
|
||||
homeStyles.headerButtonView,
|
||||
memesStyles.headerButtonView,
|
||||
]}>
|
||||
<View style={[styles.flexRow, styles.alignCenter]}>
|
||||
<Menu
|
||||
@@ -127,13 +127,15 @@ const Home = () => {
|
||||
icon={getViewIcon(view)}
|
||||
iconColor={colors.primary}
|
||||
size={16}
|
||||
onPress={() => dispatch(cycleHomeView())}
|
||||
animated
|
||||
onPress={() => dispatch(cycleMemesView())}
|
||||
/>
|
||||
<IconButton
|
||||
icon={favoritesOnly ? 'heart' : 'heart-outline'}
|
||||
iconColor={colors.primary}
|
||||
size={16}
|
||||
onPress={() => dispatch(toggleHomeFavoritesOnly())}
|
||||
animated
|
||||
onPress={() => dispatch(toggleMemesFavoritesOnly())}
|
||||
/>
|
||||
<Menu
|
||||
visible={filterMenuVisible}
|
||||
@@ -172,7 +174,7 @@ const Home = () => {
|
||||
{memes.length === 0 && (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[homeStyles.helperText, styles.centerText]}>
|
||||
style={[memesStyles.helperText, styles.centerText]}>
|
||||
No memes found
|
||||
</HelperText>
|
||||
)}
|
||||
@@ -180,4 +182,4 @@ const Home = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
export default Memes;
|
@@ -28,16 +28,16 @@ const SettingsScreen = () => {
|
||||
const noMedia = useSelector((state: RootState) => state.settings.noMedia);
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const [optimizingDatabase, setOptimizingDatabase] = useState(false);
|
||||
const [isOptimizingDatabase, setIsOptimizingDatabase] = useState(false);
|
||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
|
||||
const optimizeDatabase = () => {
|
||||
setOptimizingDatabase(true);
|
||||
setIsOptimizingDatabase(true);
|
||||
// TODO: clean up missing / extra files
|
||||
setSnackbarMessage('Database optimized!');
|
||||
setSnackbarVisible(true);
|
||||
setOptimizingDatabase(false);
|
||||
setIsOptimizingDatabase(false);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -47,7 +47,6 @@ const SettingsScreen = () => {
|
||||
orientation == 'portrait' && styles.paddingTop,
|
||||
orientation == 'landscape' && styles.smallPaddingTop,
|
||||
styles.paddingHorizontal,
|
||||
styles.fullSize,
|
||||
{ backgroundColor: colors.background },
|
||||
]}>
|
||||
<View>
|
||||
@@ -55,10 +54,7 @@ const SettingsScreen = () => {
|
||||
<List.Subheader>Database</List.Subheader>
|
||||
<Button
|
||||
mode="elevated"
|
||||
style={{
|
||||
marginBottom: responsive.verticalScale(15),
|
||||
}}
|
||||
loading={optimizingDatabase}
|
||||
loading={isOptimizingDatabase}
|
||||
onPress={optimizeDatabase}>
|
||||
Optimize Database Now
|
||||
</Button>
|
||||
|
@@ -2,7 +2,6 @@ import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
Text,
|
||||
NativeSyntheticEvent,
|
||||
NativeScrollEvent,
|
||||
BackHandler,
|
||||
@@ -13,6 +12,7 @@ import {
|
||||
HelperText,
|
||||
Menu,
|
||||
Searchbar,
|
||||
Text,
|
||||
TouchableRipple,
|
||||
useTheme,
|
||||
} from 'react-native-paper';
|
||||
@@ -48,14 +48,13 @@ const tagsStyles = StyleSheet.create({
|
||||
height: 50,
|
||||
},
|
||||
tagRow: {
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-between',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10,
|
||||
paddingHorizontal: 15,
|
||||
},
|
||||
tagView: {
|
||||
tagChip: {
|
||||
flexShrink: 1,
|
||||
maxWidth: '80%',
|
||||
},
|
||||
@@ -195,6 +194,7 @@ const Tags = () => {
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={tags}
|
||||
keyExtractor={tag => tag.id.toHexString()}
|
||||
estimatedItemSize={52}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: tag }) => (
|
||||
@@ -203,9 +203,7 @@ const Tags = () => {
|
||||
navigate(ROUTE.EDIT_TAG, { id: tag.id.toHexString() })
|
||||
}>
|
||||
<View style={tagsStyles.tagRow}>
|
||||
<View style={tagsStyles.tagView}>
|
||||
<TagChip tag={tag} />
|
||||
</View>
|
||||
<TagChip tag={tag} style={tagsStyles.tagChip} />
|
||||
<Text>{tag.memesLength}</Text>
|
||||
</View>
|
||||
</TouchableRipple>
|
||||
|
@@ -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';
|
||||
import { createRealmPersistStorage } from '@bankify/redux-persist-realm';
|
||||
import settingsReducer from './settings';
|
||||
import homeReducer from './home';
|
||||
import memesReducer from './memes';
|
||||
import tagsReducer from './tags';
|
||||
import navigationReducer from './navigation';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
settings: settingsReducer,
|
||||
home: homeReducer,
|
||||
memes: memesReducer,
|
||||
tags: tagsReducer,
|
||||
navigation: navigationReducer,
|
||||
});
|
||||
|
||||
interface RootState {
|
||||
settings: ReturnType<typeof settingsReducer>;
|
||||
home: ReturnType<typeof homeReducer>;
|
||||
memes: ReturnType<typeof memesReducer>;
|
||||
tags: ReturnType<typeof tagsReducer>;
|
||||
navigation: ReturnType<typeof navigationReducer>;
|
||||
}
|
||||
@@ -57,16 +57,16 @@ export {
|
||||
validateSettings,
|
||||
} from './settings';
|
||||
export {
|
||||
type HomeState,
|
||||
setHomeSort,
|
||||
setHomeSortDirection,
|
||||
toggleHomeSortDirection,
|
||||
setHomeView,
|
||||
cycleHomeView,
|
||||
setHomeFavoritesOnly,
|
||||
toggleHomeFavoritesOnly,
|
||||
setHomeFilter,
|
||||
} from './home';
|
||||
type MemesState,
|
||||
setMemesSort,
|
||||
setMemesSortDirection,
|
||||
toggleMemesSortDirection,
|
||||
setMemesView,
|
||||
cycleMemesView,
|
||||
setMemesFavoritesOnly,
|
||||
toggleMemesFavoritesOnly,
|
||||
setMemesFilter,
|
||||
} from './memes';
|
||||
export {
|
||||
type TagsState,
|
||||
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: {
|
||||
textAlign: 'center',
|
||||
},
|
||||
selfCenter: {
|
||||
alignSelf: 'center',
|
||||
},
|
||||
flex: {
|
||||
flex: 1,
|
||||
},
|
||||
flexGrow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
flexShrink: {
|
||||
flexShrink: 1,
|
||||
},
|
||||
flexRow: {
|
||||
flexDirection: 'row',
|
||||
},
|
||||
@@ -61,6 +67,9 @@ const styles = StyleSheet.create({
|
||||
flexRowReverse: {
|
||||
flexDirection: 'row-reverse',
|
||||
},
|
||||
flexWrap: {
|
||||
flexWrap: 'wrap',
|
||||
},
|
||||
justifyStart: {
|
||||
justifyContent: 'flex-start',
|
||||
},
|
||||
|
@@ -1,7 +1,7 @@
|
||||
export { ROUTE, type RootStackParamList } from './route';
|
||||
export {
|
||||
MEME_SORT,
|
||||
homeSortQuery,
|
||||
memesSortQuery,
|
||||
TAG_SORT,
|
||||
tagSortQuery,
|
||||
SORT_DIRECTION,
|
||||
|
@@ -1,21 +1,39 @@
|
||||
import { DocumentPickerResponse } from 'react-native-document-picker';
|
||||
|
||||
enum ROUTE {
|
||||
MAIN = 'Main',
|
||||
HOME = 'Home',
|
||||
MEMES = 'Memes',
|
||||
TAGS = 'Tags',
|
||||
SETTINGS = 'Settings',
|
||||
ADD_MEME = 'Add Meme',
|
||||
EDIT_MEME = 'Edit Meme',
|
||||
ADD_TAG = 'Add Tag',
|
||||
EDIT_TAG = 'Edit Tag',
|
||||
}
|
||||
|
||||
interface AddMemeRouteParamsFromFiles {
|
||||
uri: DocumentPickerResponse[];
|
||||
}
|
||||
|
||||
interface EditMemeRouteParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface EditTagRouteParams {
|
||||
id: string;
|
||||
}
|
||||
|
||||
interface RootStackParamList {
|
||||
[key: string]: undefined | EditTagRouteParams;
|
||||
[key: string]:
|
||||
| undefined
|
||||
| AddMemeRouteParamsFromFiles
|
||||
| EditMemeRouteParams
|
||||
| EditTagRouteParams;
|
||||
[ROUTE.MAIN]: undefined;
|
||||
[ROUTE.EDIT_MEME]: undefined;
|
||||
[ROUTE.EDIT_TAG]: EditTagRouteParams | undefined;
|
||||
[ROUTE.ADD_MEME]: AddMemeRouteParamsFromFiles;
|
||||
[ROUTE.EDIT_MEME]: EditMemeRouteParams;
|
||||
[ROUTE.ADD_TAG]: undefined;
|
||||
[ROUTE.EDIT_TAG]: EditTagRouteParams;
|
||||
}
|
||||
|
||||
export { ROUTE, type RootStackParamList };
|
||||
|
@@ -7,7 +7,7 @@ enum MEME_SORT {
|
||||
SIZE = 'Size',
|
||||
}
|
||||
|
||||
const homeSortQuery = (sort: MEME_SORT) => {
|
||||
const memesSortQuery = (sort: MEME_SORT) => {
|
||||
switch (sort) {
|
||||
case MEME_SORT.TITLE: {
|
||||
return 'title';
|
||||
@@ -36,6 +36,7 @@ enum TAG_SORT {
|
||||
MEMES_LENGTH = 'Items',
|
||||
DATE_CREATED = 'Date Created',
|
||||
DATE_MODIFIED = 'Date Modified',
|
||||
DATE_USED = 'Last Used',
|
||||
TIMES_USED = 'Times Used',
|
||||
}
|
||||
|
||||
@@ -59,6 +60,9 @@ const tagSortQuery = (sort: TAG_SORT) => {
|
||||
case TAG_SORT.TIMES_USED: {
|
||||
return 'timesUsed';
|
||||
}
|
||||
case TAG_SORT.DATE_USED: {
|
||||
return 'dateUsed';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -67,4 +71,4 @@ enum SORT_DIRECTION {
|
||||
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 {
|
||||
MASONRY = 'Masonry',
|
||||
GRID = 'Grid',
|
||||
LIST = 'List',
|
||||
}
|
||||
|
||||
|
@@ -11,8 +11,8 @@ const getContrastColor = (hexColor: string) => {
|
||||
const brightness = (r * 299 + g * 587 + b * 114) / 1000;
|
||||
|
||||
return brightness > 128
|
||||
? lightTheme.colors.onSurface
|
||||
: darkTheme.colors.onSurface;
|
||||
? lightTheme.colors.onBackground
|
||||
: darkTheme.colors.onBackground;
|
||||
};
|
||||
|
||||
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_USED:
|
||||
case TAG_SORT.DATE_CREATED:
|
||||
case TAG_SORT.DATE_MODIFIED: {
|
||||
case TAG_SORT.DATE_MODIFIED:
|
||||
case TAG_SORT.DATE_USED: {
|
||||
sortIcon = 'sort-calendar';
|
||||
break;
|
||||
}
|
||||
@@ -49,9 +50,6 @@ const getViewIcon = (view: VIEW) => {
|
||||
case VIEW.MASONRY: {
|
||||
return 'view-dashboard';
|
||||
}
|
||||
case VIEW.GRID: {
|
||||
return 'view-grid';
|
||||
}
|
||||
case VIEW.LIST: {
|
||||
return 'view-list';
|
||||
}
|
||||
|
@@ -4,8 +4,15 @@ export {
|
||||
isRgbColor,
|
||||
isValidColor,
|
||||
rgbToHex,
|
||||
generateRandomColor
|
||||
generateRandomColor,
|
||||
} from './color';
|
||||
export { packageName, appName, fileProvider, noOp } from './constants';
|
||||
export { multipleIdQuery } from './database';
|
||||
export {
|
||||
allowedImageMimeTypes,
|
||||
allowedGifMimeTypes,
|
||||
allowedMimeTypes,
|
||||
getMemeType,
|
||||
} from './filesystem';
|
||||
export { isPermissionForPath, clearPermissions } from './permissions';
|
||||
export { getSortIcon, getViewIcon } from './icon';
|
||||
|
Reference in New Issue
Block a user