Add meme view & sharing
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
@@ -12,10 +12,8 @@ build:
|
||||
- package-lock.json
|
||||
paths:
|
||||
- node_modules
|
||||
policy: pull
|
||||
before_script:
|
||||
- npm install
|
||||
- npm run postinstall
|
||||
script:
|
||||
- npm run build
|
||||
rules:
|
||||
@@ -30,10 +28,8 @@ compile:
|
||||
- package-lock.json
|
||||
paths:
|
||||
- node_modules
|
||||
policy: pull
|
||||
before_script:
|
||||
- npm install
|
||||
- npm run postinstall
|
||||
script:
|
||||
- npm run compile
|
||||
after_script:
|
||||
@@ -56,10 +52,8 @@ test:
|
||||
- package-lock.json
|
||||
paths:
|
||||
- node_modules
|
||||
policy: pull
|
||||
before_script:
|
||||
- npm install
|
||||
- npm run postinstall
|
||||
script:
|
||||
- npm run test
|
||||
rules:
|
||||
@@ -77,7 +71,6 @@ lint:
|
||||
policy: pull
|
||||
before_script:
|
||||
- npm install
|
||||
- npm run postinstall
|
||||
script:
|
||||
- npm run lint
|
||||
rules:
|
||||
@@ -90,12 +83,12 @@ release:
|
||||
- job: compile
|
||||
artifacts: true
|
||||
script:
|
||||
- echo "Create Release $CI_COMMIT_SHA"
|
||||
- echo "Create Release $CI_COMMIT_TAG"
|
||||
release:
|
||||
name: "Release $CI_COMMIT_SHORT_SHA"
|
||||
tag_name: "$CI_COMMIT_SHORT_SHA"
|
||||
ref: "$CI_COMMIT_SHORT_SHA"
|
||||
description: "Release $CI_COMMIT_SHORT_SHA"
|
||||
name: "Release $CI_COMMIT_TAG"
|
||||
tag_name: "$CI_COMMIT_TAG"
|
||||
ref: "$CI_COMMIT_TAG"
|
||||
description: "Release $CI_COMMIT_SHA"
|
||||
assets:
|
||||
links:
|
||||
- name: "app-release.apk"
|
||||
|
3
index.js
3
index.js
@@ -1,9 +1,6 @@
|
||||
import { AppRegistry } from 'react-native';
|
||||
import 'react-native-get-random-values';
|
||||
import { enableFreeze } from 'react-native-screens';
|
||||
import App from './src/app';
|
||||
import { name as appName } from './app.json';
|
||||
|
||||
enableFreeze(true);
|
||||
|
||||
AppRegistry.registerComponent(appName, () => App);
|
||||
|
54
package-lock.json
generated
54
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@bankify/redux-persist-realm": "^0.1.3",
|
||||
"@likashefqet/react-native-image-zoom": "^1.3.0",
|
||||
"@react-native-clipboard/clipboard": "^1.11.2",
|
||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
@@ -23,7 +24,6 @@
|
||||
"react-native-file-access": "^3.0.4",
|
||||
"react-native-gesture-handler": "^2.12.0",
|
||||
"react-native-get-random-values": "^1.9.0",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
"react-native-mime-types": "^2.4.0",
|
||||
"react-native-paper": "^5.9.1",
|
||||
"react-native-reanimated": "^3.3.0",
|
||||
@@ -2988,6 +2988,17 @@
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
||||
},
|
||||
"node_modules/@likashefqet/react-native-image-zoom": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@likashefqet/react-native-image-zoom/-/react-native-image-zoom-1.3.0.tgz",
|
||||
"integrity": "sha512-PLRd1hNMHe9LUn8b4rmLt86282geuaqP4Qd2rFWIloxMS2ePNTIaNlEUu3T3LaO8Pg9vhVV97TxfFeU8F+tcYQ==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.x.x",
|
||||
"react-native": ">=0.62.x",
|
||||
"react-native-gesture-handler": ">=2.x.x",
|
||||
"react-native-reanimated": ">=2.x.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
@@ -13414,27 +13425,6 @@
|
||||
"react-native": ">=0.56"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-pan-zoom": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz",
|
||||
"integrity": "sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q==",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-image-zoom-viewer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-zoom-viewer/-/react-native-image-zoom-viewer-3.0.1.tgz",
|
||||
"integrity": "sha512-la6s5DNSuq4GCRLsi5CZ29FPjgTpdCuGIRdO5T9rUrAtxrlpBPhhSnHrbmPVxsdtOUvxHacTh2Gfa9+RraMZQA==",
|
||||
"dependencies": {
|
||||
"react-native-image-pan-zoom": "^2.1.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -17927,6 +17917,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@likashefqet/react-native-image-zoom": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@likashefqet/react-native-image-zoom/-/react-native-image-zoom-1.3.0.tgz",
|
||||
"integrity": "sha512-PLRd1hNMHe9LUn8b4rmLt86282geuaqP4Qd2rFWIloxMS2ePNTIaNlEUu3T3LaO8Pg9vhVV97TxfFeU8F+tcYQ==",
|
||||
"requires": {}
|
||||
},
|
||||
"@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||
"version": "5.1.1-v1",
|
||||
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
|
||||
@@ -25811,20 +25807,6 @@
|
||||
"fast-base64-decode": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-native-image-pan-zoom": {
|
||||
"version": "2.1.12",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-pan-zoom/-/react-native-image-pan-zoom-2.1.12.tgz",
|
||||
"integrity": "sha512-BF66XeP6dzuANsPmmFsJshM2Jyh/Mo1t8FsGc1L9Q9/sVP8MJULDabB1hms+eAoqgtyhMr5BuXV3E1hJ5U5H6Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-image-zoom-viewer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-image-zoom-viewer/-/react-native-image-zoom-viewer-3.0.1.tgz",
|
||||
"integrity": "sha512-la6s5DNSuq4GCRLsi5CZ29FPjgTpdCuGIRdO5T9rUrAtxrlpBPhhSnHrbmPVxsdtOUvxHacTh2Gfa9+RraMZQA==",
|
||||
"requires": {
|
||||
"react-native-image-pan-zoom": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"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",
|
||||
|
@@ -15,6 +15,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@bankify/redux-persist-realm": "^0.1.3",
|
||||
"@likashefqet/react-native-image-zoom": "^1.3.0",
|
||||
"@react-native-clipboard/clipboard": "^1.11.2",
|
||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
@@ -28,7 +29,6 @@
|
||||
"react-native-file-access": "^3.0.4",
|
||||
"react-native-gesture-handler": "^2.12.0",
|
||||
"react-native-get-random-values": "^1.9.0",
|
||||
"react-native-image-zoom-viewer": "^3.0.1",
|
||||
"react-native-mime-types": "^2.4.0",
|
||||
"react-native-paper": "^5.9.1",
|
||||
"react-native-reanimated": "^3.3.0",
|
||||
|
@@ -1,11 +1,10 @@
|
||||
export {
|
||||
MemesGridView,
|
||||
MemesListView,
|
||||
MemesMasonryView,
|
||||
MemesList,
|
||||
MemeEditor,
|
||||
MemesHeader,
|
||||
MemeTagSearchModal,
|
||||
MemeTagSelector,
|
||||
MemeViewItem,
|
||||
} from './memes';
|
||||
export { TagChip, TagEditor, TagPreview, TagRow, TagsHeader } from './tags';
|
||||
export { default as FloatingActionButton } from './floatingActionButton';
|
||||
|
@@ -1,74 +0,0 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { Meme } from '../../../database';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { HelperText } from 'react-native-paper';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styles from '../../../styles';
|
||||
import { RootState } from '../../../state';
|
||||
import { ORIENTATION, useDimensions } from '../../../contexts';
|
||||
import { getFlashListItemHeight } from '../../../utilities';
|
||||
import MemesGridItem from './memesGridItem';
|
||||
|
||||
const gridViewStyles = StyleSheet.create({
|
||||
helperText: {
|
||||
marginVertical: 10,
|
||||
},
|
||||
flashList: {
|
||||
paddingBottom: 100,
|
||||
paddingHorizontal: 2.5,
|
||||
},
|
||||
});
|
||||
|
||||
const MemesGridView = ({
|
||||
memes,
|
||||
flashListRef,
|
||||
flashListPadding,
|
||||
handleScroll,
|
||||
}: {
|
||||
memes: Realm.Results<Meme & Realm.Object<Meme>>;
|
||||
flashListRef: RefObject<FlashList<Meme>>;
|
||||
flashListPadding: number;
|
||||
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
}) => {
|
||||
const { orientation, dimensions } = useDimensions();
|
||||
const gridColumns = useSelector(
|
||||
(state: RootState) => state.settings.gridColumns,
|
||||
);
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={memes}
|
||||
estimatedItemSize={getFlashListItemHeight(gridColumns)}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width * 0.92,
|
||||
}}
|
||||
numColumns={gridColumns}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme }) => <MemesGridItem meme={meme} />}
|
||||
contentContainerStyle={{
|
||||
paddingTop:
|
||||
flashListPadding +
|
||||
dimensions.height *
|
||||
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||
...gridViewStyles.flashList,
|
||||
}}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[gridViewStyles.helperText, styles.centerText]}>
|
||||
No memes found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemesGridView;
|
@@ -1,7 +1,6 @@
|
||||
export { default as MemesGridView } from './gridView/memesGridView';
|
||||
export { default as MemesListView } from './listView/memesListView';
|
||||
export { default as MemesMasonryView } from './masonryView/memesMasonryView';
|
||||
export { default as MemesList } from './memesList/memesList';
|
||||
export { default as MemeEditor } from './memeEditor';
|
||||
export { default as MemesHeader } from './memesHeader';
|
||||
export { default as MemeTagSearchModal } from './memeTagSearchModal';
|
||||
export { default as MemeTagSelector } from './memeTagSelector';
|
||||
export { default as MemeViewItem } from './memeViewItem';
|
||||
|
@@ -1,68 +0,0 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { Meme } from '../../../database';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { Divider, HelperText } from 'react-native-paper';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import styles from '../../../styles';
|
||||
import { ORIENTATION, useDimensions } from '../../../contexts';
|
||||
import MemesListItem from './memesListItem';
|
||||
|
||||
const gridViewStyles = StyleSheet.create({
|
||||
helperText: {
|
||||
marginVertical: 10,
|
||||
},
|
||||
flashList: {
|
||||
paddingBottom: 100,
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const MemesListView = ({
|
||||
memes,
|
||||
flashListRef,
|
||||
flashListPadding,
|
||||
handleScroll,
|
||||
}: {
|
||||
memes: Realm.Results<Meme & Realm.Object<Meme>>;
|
||||
flashListRef: RefObject<FlashList<Meme>>;
|
||||
flashListPadding: number;
|
||||
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
}) => {
|
||||
const { orientation, dimensions } = useDimensions();
|
||||
|
||||
return (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={memes}
|
||||
estimatedItemSize={50}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width * 0.92,
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme }) => <MemesListItem meme={meme} />}
|
||||
ItemSeparatorComponent={() => <Divider />}
|
||||
contentContainerStyle={{
|
||||
paddingTop:
|
||||
flashListPadding +
|
||||
dimensions.height *
|
||||
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||
...gridViewStyles.flashList,
|
||||
}}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[gridViewStyles.helperText, styles.centerText]}>
|
||||
No memes found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemesListView;
|
@@ -1,75 +0,0 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { Meme } from '../../../database';
|
||||
import { FlashList, MasonryFlashList } from '@shopify/flash-list';
|
||||
import { HelperText } from 'react-native-paper';
|
||||
import {
|
||||
NativeScrollEvent,
|
||||
NativeSyntheticEvent,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import styles from '../../../styles';
|
||||
import { RootState } from '../../../state';
|
||||
import { ORIENTATION, useDimensions } from '../../../contexts';
|
||||
import { getFlashListItemHeight } from '../../../utilities';
|
||||
import MemesMasonryItem from './memesMasonryItem';
|
||||
|
||||
const memeMasonryViewStyles = StyleSheet.create({
|
||||
helperText: {
|
||||
marginVertical: 10,
|
||||
},
|
||||
flashList: {
|
||||
paddingBottom: 100,
|
||||
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
|
||||
paddingHorizontal: 0.1,
|
||||
},
|
||||
});
|
||||
|
||||
const MemesMasonryView = ({
|
||||
memes,
|
||||
flashListRef,
|
||||
flashListPadding,
|
||||
handleScroll,
|
||||
}: {
|
||||
memes: Realm.Results<Meme & Realm.Object<Meme>>;
|
||||
flashListRef: RefObject<FlashList<Meme>>;
|
||||
flashListPadding: number;
|
||||
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
}) => {
|
||||
const { orientation, dimensions } = useDimensions();
|
||||
const masonryColumns = useSelector(
|
||||
(state: RootState) => state.settings.masonryColumns,
|
||||
);
|
||||
|
||||
return (
|
||||
<MasonryFlashList
|
||||
ref={flashListRef}
|
||||
data={memes}
|
||||
estimatedItemSize={getFlashListItemHeight(masonryColumns)}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width * 0.92,
|
||||
}}
|
||||
numColumns={masonryColumns}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme }) => <MemesMasonryItem meme={meme} />}
|
||||
contentContainerStyle={{
|
||||
paddingTop:
|
||||
flashListPadding +
|
||||
dimensions.height *
|
||||
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
||||
...memeMasonryViewStyles.flashList,
|
||||
}}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[memeMasonryViewStyles.helperText, styles.centerText]}>
|
||||
No memes found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemesMasonryView;
|
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { HelperText, TextInput } from 'react-native-paper';
|
||||
import { Image } from 'react-native';
|
||||
import { useDimensions } from '../../contexts';
|
||||
@@ -21,13 +21,13 @@ const memeEditorStyles = {
|
||||
};
|
||||
|
||||
const MemeEditor = ({
|
||||
imageUri,
|
||||
memeUri,
|
||||
memeTitle,
|
||||
setMemeTitle,
|
||||
memeTags,
|
||||
setMemeTags,
|
||||
}: {
|
||||
imageUri: string;
|
||||
memeUri: string;
|
||||
memeTitle: StringValidationResult;
|
||||
setMemeTitle: (name: StringValidationResult) => void;
|
||||
memeTags: Map<string, Tag>;
|
||||
@@ -38,15 +38,23 @@ const MemeEditor = ({
|
||||
const [imageWidth, setImageWidth] = useState<number>();
|
||||
const [imageHeight, setImageHeight] = useState<number>();
|
||||
|
||||
Image.getSize(imageUri, (width, height) => {
|
||||
const paddedWidth = dimensions.width * 0.92;
|
||||
const paddedHeight = Math.max(
|
||||
Math.min((paddedWidth / width) * height, 500),
|
||||
100,
|
||||
);
|
||||
setImageWidth(paddedWidth);
|
||||
setImageHeight(paddedHeight);
|
||||
});
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setImageWidth(undefined);
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
setImageHeight(undefined);
|
||||
|
||||
Image.getSize(memeUri, (width, height) => {
|
||||
const paddedWidth = dimensions.width * 0.92;
|
||||
const paddedHeight = Math.max(
|
||||
Math.min((paddedWidth / width) * height, 500),
|
||||
100,
|
||||
);
|
||||
|
||||
setImageWidth(paddedWidth);
|
||||
setImageHeight(paddedHeight);
|
||||
});
|
||||
}, [memeUri, dimensions.width]);
|
||||
|
||||
if (!imageWidth || !imageHeight) return <LoadingView />;
|
||||
|
||||
@@ -64,7 +72,7 @@ const MemeEditor = ({
|
||||
{memeTitle.error}
|
||||
</HelperText>
|
||||
<Image
|
||||
source={{ uri: imageUri }}
|
||||
source={{ uri: memeUri }}
|
||||
style={[
|
||||
{
|
||||
width: imageWidth,
|
||||
|
53
src/components/memes/memeViewItem.tsx
Normal file
53
src/components/memes/memeViewItem.tsx
Normal file
@@ -0,0 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ActivityIndicator, useTheme } from 'react-native-paper';
|
||||
import { ImageZoom } from '@likashefqet/react-native-image-zoom';
|
||||
import { Meme } from '../../database';
|
||||
import { useDimensions } from '../../contexts';
|
||||
import { Image, View } from 'react-native';
|
||||
import styles from '../../styles';
|
||||
|
||||
const MemeViewItem = ({ meme }: { meme: Meme }) => {
|
||||
const { dimensions } = useDimensions();
|
||||
const { colors } = useTheme();
|
||||
|
||||
const [imageWidth, setImageWidth] = useState<number>();
|
||||
const [imageHeight, setImageHeight] = useState<number>();
|
||||
|
||||
useEffect(() => {
|
||||
Image.getSize(meme.uri, (width, height) => {
|
||||
const ratio = width / height;
|
||||
const screenRatio = dimensions.width / dimensions.height - 160;
|
||||
|
||||
if (ratio > screenRatio) {
|
||||
setImageWidth(dimensions.width);
|
||||
setImageHeight(dimensions.width / ratio);
|
||||
} else {
|
||||
setImageWidth(dimensions.height * ratio);
|
||||
setImageHeight(dimensions.height);
|
||||
}
|
||||
});
|
||||
}, [meme.uri, dimensions.width, dimensions.height]);
|
||||
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
{
|
||||
width: dimensions.width,
|
||||
height: dimensions.height - 160,
|
||||
backgroundColor: colors.background,
|
||||
},
|
||||
styles.centered,
|
||||
]}>
|
||||
{imageWidth && imageHeight ? (
|
||||
<ImageZoom
|
||||
source={{ uri: meme.uri }}
|
||||
style={{ width: imageWidth, height: imageHeight }}
|
||||
/>
|
||||
) : (
|
||||
<ActivityIndicator size="large" color={colors.primary} />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemeViewItem;
|
@@ -1,14 +1,19 @@
|
||||
import React, { useState } from 'react';
|
||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
||||
import { Image, TouchableHighlight, View } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Meme } from '../../../database';
|
||||
import { ROUTE, RootStackParamList } from '../../../types';
|
||||
import { useDimensions } from '../../../contexts';
|
||||
import { RootState } from '../../../state';
|
||||
|
||||
const MemesGridItem = ({ meme }: { meme: Meme }) => {
|
||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const MemesGridItem = ({
|
||||
meme,
|
||||
index,
|
||||
focusMeme,
|
||||
}: {
|
||||
meme: Meme;
|
||||
index: number;
|
||||
focusMeme: (index: number) => void;
|
||||
}) => {
|
||||
const { dimensions } = useDimensions();
|
||||
const gridColumns = useSelector(
|
||||
(state: RootState) => state.settings.gridColumns,
|
||||
@@ -27,10 +32,7 @@ const MemesGridItem = ({ meme }: { meme: Meme }) => {
|
||||
<>
|
||||
{imageWidth && imageHeight && (
|
||||
<View>
|
||||
<TouchableHighlight
|
||||
onPress={() =>
|
||||
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
||||
}>
|
||||
<TouchableHighlight onPress={() => focusMeme(index)}>
|
||||
<Image
|
||||
source={{ uri: meme.uri }}
|
||||
style={[{ width: imageWidth, height: imageHeight }]}
|
167
src/components/memes/memesList/memesList.tsx
Normal file
167
src/components/memes/memesList/memesList.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import React, { RefObject } from 'react';
|
||||
import { FlashList, MasonryFlashList } from '@shopify/flash-list';
|
||||
import {
|
||||
NativeSyntheticEvent,
|
||||
NativeScrollEvent,
|
||||
StyleSheet,
|
||||
} from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Divider, HelperText } from 'react-native-paper';
|
||||
import { useDimensions, ORIENTATION } from '../../../contexts';
|
||||
import { Meme } from '../../../database';
|
||||
import { RootState } from '../../../state';
|
||||
import { VIEW } from '../../../types';
|
||||
import { getFlashListItemHeight } from '../../../utilities';
|
||||
import styles from '../../../styles';
|
||||
import MemesMasonryItem from './memesMasonryItem';
|
||||
import MemesGridItem from './memesGridItem';
|
||||
import MemesListItem from './memesListItem';
|
||||
|
||||
const memesMasonryListStyles = StyleSheet.create({
|
||||
flashList: {
|
||||
paddingBottom: 100,
|
||||
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
|
||||
paddingHorizontal: 0.1,
|
||||
},
|
||||
helperText: {
|
||||
marginVertical: 15,
|
||||
},
|
||||
});
|
||||
|
||||
const memesGridListStyles = StyleSheet.create({
|
||||
flashList: {
|
||||
paddingBottom: 100,
|
||||
paddingHorizontal: 2.5,
|
||||
},
|
||||
helperText: {
|
||||
marginVertical: 12.5,
|
||||
},
|
||||
});
|
||||
|
||||
const memesListListStyles = StyleSheet.create({
|
||||
flashList: {
|
||||
paddingBottom: 100,
|
||||
paddingHorizontal: 5,
|
||||
},
|
||||
helperText: {
|
||||
marginVertical: 15,
|
||||
},
|
||||
});
|
||||
|
||||
const MemesList = ({
|
||||
memes,
|
||||
flashListRef,
|
||||
flashListPadding,
|
||||
handleScroll,
|
||||
focusMeme,
|
||||
}: {
|
||||
memes: Realm.Results<Meme & Realm.Object<Meme>>;
|
||||
flashListRef: RefObject<FlashList<Meme>>;
|
||||
flashListPadding: number;
|
||||
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
focusMeme: (index: number) => void;
|
||||
}) => {
|
||||
const { dimensions, orientation } = useDimensions();
|
||||
const view = useSelector((state: RootState) => state.memes.view);
|
||||
const masonryColumns = useSelector(
|
||||
(state: RootState) => state.settings.masonryColumns,
|
||||
);
|
||||
const gridColumns = useSelector(
|
||||
(state: RootState) => state.settings.gridColumns,
|
||||
);
|
||||
|
||||
const extraFlashListPadding =
|
||||
flashListPadding +
|
||||
dimensions.height * (orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04);
|
||||
|
||||
return (
|
||||
<>
|
||||
{view === VIEW.MASONRY && (
|
||||
<MasonryFlashList
|
||||
ref={flashListRef}
|
||||
data={memes}
|
||||
estimatedItemSize={getFlashListItemHeight(masonryColumns)}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width * 0.92,
|
||||
}}
|
||||
numColumns={masonryColumns}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme, index }) => (
|
||||
<MemesMasonryItem meme={meme} index={index} focusMeme={focusMeme} />
|
||||
)}
|
||||
contentContainerStyle={{
|
||||
paddingTop: extraFlashListPadding,
|
||||
...memesMasonryListStyles.flashList,
|
||||
}}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[memesMasonryListStyles.helperText, styles.centerText]}>
|
||||
No memes found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
)}
|
||||
{view === VIEW.GRID && (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={memes}
|
||||
estimatedItemSize={getFlashListItemHeight(gridColumns)}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width * 0.92,
|
||||
}}
|
||||
numColumns={gridColumns}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme, index }) => (
|
||||
<MemesGridItem meme={meme} index={index} focusMeme={focusMeme} />
|
||||
)}
|
||||
contentContainerStyle={{
|
||||
paddingTop: extraFlashListPadding + 2.5,
|
||||
...memesGridListStyles.flashList,
|
||||
}}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[memesGridListStyles.helperText, styles.centerText]}>
|
||||
No memes found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
)}
|
||||
{view === VIEW.LIST && (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={memes}
|
||||
estimatedItemSize={50}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height,
|
||||
width: dimensions.width * 0.92,
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme, index }) => (
|
||||
<MemesListItem meme={meme} index={index} focusMeme={focusMeme} />
|
||||
)}
|
||||
ItemSeparatorComponent={() => <Divider />}
|
||||
contentContainerStyle={{
|
||||
paddingTop: extraFlashListPadding,
|
||||
...memesListListStyles.flashList,
|
||||
}}
|
||||
ListEmptyComponent={() => (
|
||||
<HelperText
|
||||
type={'info'}
|
||||
style={[memesListListStyles.helperText, styles.centerText]}>
|
||||
No memes found
|
||||
</HelperText>
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemesList;
|
@@ -1,9 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Image, StyleSheet, View } from 'react-native';
|
||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
||||
import { Text, TouchableRipple } from 'react-native-paper';
|
||||
import { Meme } from '../../../database';
|
||||
import { ROUTE, RootStackParamList } from '../../../types';
|
||||
import styles from '../../../styles';
|
||||
import { useDimensions } from '../../../contexts';
|
||||
|
||||
@@ -23,8 +21,15 @@ const memesListItemStyles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const MemesListItem = ({ meme }: { meme: Meme }) => {
|
||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const MemesListItem = ({
|
||||
meme,
|
||||
index,
|
||||
focusMeme,
|
||||
}: {
|
||||
meme: Meme;
|
||||
index: number;
|
||||
focusMeme: (index: number) => void;
|
||||
}) => {
|
||||
const { dimensions } = useDimensions();
|
||||
|
||||
const [imageWidth, setImageWidth] = useState<number>();
|
||||
@@ -40,9 +45,7 @@ const MemesListItem = ({ meme }: { meme: Meme }) => {
|
||||
<>
|
||||
{imageWidth && imageHeight && (
|
||||
<TouchableRipple
|
||||
onPress={() =>
|
||||
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
||||
}
|
||||
onPress={() => focusMeme(index)}
|
||||
style={[memesListItemStyles.view, styles.flexRow]}>
|
||||
<>
|
||||
<View style={{ width: imageWidth, height: imageHeight }}>
|
@@ -1,9 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Image, StyleSheet, TouchableHighlight } from 'react-native';
|
||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Meme } from '../../../database';
|
||||
import { ROUTE, RootStackParamList } from '../../../types';
|
||||
import { useDimensions } from '../../../contexts';
|
||||
import { RootState } from '../../../state';
|
||||
|
||||
@@ -17,8 +15,15 @@ const memeMasonryItemStyles = StyleSheet.create({
|
||||
},
|
||||
});
|
||||
|
||||
const MemesMasonryItem = ({ meme }: { meme: Meme }) => {
|
||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const MemesMasonryItem = ({
|
||||
meme,
|
||||
index,
|
||||
focusMeme,
|
||||
}: {
|
||||
meme: Meme;
|
||||
index: number;
|
||||
focusMeme: (index: number) => void;
|
||||
}) => {
|
||||
const { dimensions } = useDimensions();
|
||||
const masonryColumns = useSelector(
|
||||
(state: RootState) => state.settings.masonryColumns,
|
||||
@@ -37,9 +42,7 @@ const MemesMasonryItem = ({ meme }: { meme: Meme }) => {
|
||||
<>
|
||||
{imageWidth && imageHeight && (
|
||||
<TouchableHighlight
|
||||
onPress={() =>
|
||||
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
||||
}
|
||||
onPress={() => focusMeme(index)}
|
||||
style={memeMasonryItemStyles.view}>
|
||||
<Image
|
||||
source={{ uri: meme.uri }}
|
@@ -1,4 +1,4 @@
|
||||
import React, { useRef } from 'react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { HelperText, TextInput } from 'react-native-paper';
|
||||
import TagPreview from './tagPreview';
|
||||
import {
|
||||
@@ -21,15 +21,16 @@ const TagEditor = ({
|
||||
}) => {
|
||||
const lastValidTagColor = useRef(tagColor.parsed);
|
||||
|
||||
const handleTagColorChange = (color: string) => {
|
||||
const result = validateColor(color);
|
||||
setTagColor(result);
|
||||
if (result.valid) lastValidTagColor.current = result.parsed;
|
||||
};
|
||||
useEffect(() => {
|
||||
if (tagColor.valid) lastValidTagColor.current = tagColor.parsed;
|
||||
}, [tagColor]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<TagPreview name={tagName.parsed} color={lastValidTagColor.current} />
|
||||
<TagPreview
|
||||
name={tagName.parsed}
|
||||
color={tagColor.valid ? tagColor.parsed : lastValidTagColor.current}
|
||||
/>
|
||||
<TextInput
|
||||
mode="outlined"
|
||||
label="Name"
|
||||
@@ -45,13 +46,13 @@ const TagEditor = ({
|
||||
mode="outlined"
|
||||
label="Color"
|
||||
value={tagColor.raw}
|
||||
onChangeText={handleTagColorChange}
|
||||
onChangeText={color => setTagColor(validateColor(color))}
|
||||
error={!tagColor.valid}
|
||||
autoCorrect={false}
|
||||
right={
|
||||
<TextInput.Icon
|
||||
icon="palette"
|
||||
onPress={() => handleTagColorChange(generateRandomColor())}
|
||||
onPress={() => setTagColor(validateColor(generateRandomColor()))}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
@@ -22,6 +22,7 @@ class Meme extends Object<Meme> {
|
||||
id!: BSON.UUID;
|
||||
type!: MEME_TYPE;
|
||||
uri!: string;
|
||||
mimeType!: string;
|
||||
size!: number;
|
||||
title!: string;
|
||||
isFavorite!: boolean;
|
||||
@@ -39,6 +40,7 @@ class Meme extends Object<Meme> {
|
||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||
type: { type: 'string', indexed: true },
|
||||
uri: 'string',
|
||||
mimeType: 'string',
|
||||
size: 'int',
|
||||
title: 'string',
|
||||
isFavorite: { type: 'bool', indexed: true, default: false },
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
EditTag,
|
||||
AddMeme,
|
||||
AddTag,
|
||||
MemeView,
|
||||
} from './screens';
|
||||
import { darkNavigationTheme, lightNavigationTheme } from './theme';
|
||||
import {
|
||||
@@ -91,10 +92,14 @@ const NavigationContainer = () => {
|
||||
<StackNavigatorBase.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
freezeOnBlur: true,
|
||||
animation: 'slide_from_bottom',
|
||||
}}>
|
||||
<StackNavigatorBase.Screen name={ROUTE.MAIN} component={TabNavigator} />
|
||||
<StackNavigatorBase.Screen
|
||||
name={ROUTE.MEME_VIEW}
|
||||
component={MemeView}
|
||||
options={{ presentation: 'modal', animation: 'default' }}
|
||||
/>
|
||||
<StackNavigatorBase.Screen name={ROUTE.ADD_MEME} component={AddMeme} />
|
||||
<StackNavigatorBase.Screen
|
||||
name={ROUTE.EDIT_MEME}
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { ORIENTATION, useDimensions } from '../contexts';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { useRealm } from '@realm/react';
|
||||
import { BSON } from 'realm';
|
||||
@@ -13,8 +13,23 @@ import styles from '../styles';
|
||||
import { ROUTE, RootStackParamList } from '../types';
|
||||
import { Meme, Tag } from '../database';
|
||||
import { RootState } from '../state';
|
||||
import { getMemeType, validateMemeTitle } from '../utilities';
|
||||
import { allowedMimeTypes, getMemeType, validateMemeTitle } from '../utilities';
|
||||
import { MemeEditor } from '../components';
|
||||
import {
|
||||
DocumentPickerResponse,
|
||||
pickSingle,
|
||||
} from 'react-native-document-picker';
|
||||
|
||||
const addMemeStyles = StyleSheet.create({
|
||||
saveAndAddButton: {
|
||||
flex: 1,
|
||||
marginRight: 5,
|
||||
},
|
||||
saveButton: {
|
||||
flex: 1,
|
||||
marginLeft: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const AddMeme = ({
|
||||
route,
|
||||
@@ -28,31 +43,32 @@ const AddMeme = ({
|
||||
(state: RootState) => state.settings.storageUri,
|
||||
)!;
|
||||
|
||||
const { file } = route.params;
|
||||
const file = useRef(route.params.file);
|
||||
|
||||
const [memeUri, setMemeUri] = useState(file.current.uri);
|
||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
|
||||
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSavingAndAddingAnother, setIsSavingAndAddingAnother] =
|
||||
useState(false);
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
const uuid = new BSON.UUID();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const mimeType = file.type!;
|
||||
const mimeType = file.current.type!;
|
||||
const memeType = getMemeType(mimeType);
|
||||
|
||||
const fileExtension = extension(mimeType);
|
||||
const fileExtension = extension(mimeType) as string;
|
||||
if (!fileExtension) goBack();
|
||||
|
||||
const uri = AndroidScoped.appendPath(
|
||||
storageUri,
|
||||
`${uuid.toHexString()}.${fileExtension as string}`,
|
||||
`${uuid.toHexString()}.${fileExtension}`,
|
||||
);
|
||||
|
||||
await FileSystem.cp(file.uri, uri);
|
||||
await FileSystem.cp(file.current.uri, uri);
|
||||
const { size } = await FileSystem.stat(uri);
|
||||
|
||||
realm.write(() => {
|
||||
@@ -60,6 +76,7 @@ const AddMeme = ({
|
||||
id: uuid,
|
||||
type: memeType,
|
||||
uri,
|
||||
mimeType,
|
||||
size,
|
||||
title: memeTitle.parsed,
|
||||
isFavorite: memeIsFavorite,
|
||||
@@ -73,9 +90,7 @@ const AddMeme = ({
|
||||
tag.memesLength = tag.memes.length;
|
||||
});
|
||||
});
|
||||
|
||||
goBack();
|
||||
};
|
||||
}, [goBack, memeIsFavorite, memeTags, memeTitle.parsed, realm, storageUri]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -99,20 +114,46 @@ const AddMeme = ({
|
||||
]}>
|
||||
<View style={[styles.flex, styles.justifyStart]}>
|
||||
<MemeEditor
|
||||
imageUri={file.uri}
|
||||
memeUri={memeUri}
|
||||
memeTitle={memeTitle}
|
||||
setMemeTitle={setMemeTitle}
|
||||
memeTags={memeTags}
|
||||
setMemeTags={setMemeTags}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex, styles.justifyEnd]}>
|
||||
<View style={[styles.flexRow, styles.fullWidth]}>
|
||||
<Button
|
||||
mode="contained-tonal"
|
||||
icon="plus"
|
||||
onPress={async () => {
|
||||
setIsSavingAndAddingAnother(true);
|
||||
await handleSave();
|
||||
setIsSavingAndAddingAnother(false);
|
||||
file.current = (await pickSingle({
|
||||
type: allowedMimeTypes,
|
||||
}).catch(goBack)) as DocumentPickerResponse;
|
||||
setMemeUri(file.current.uri);
|
||||
setMemeTitle(validateMemeTitle('New Meme'));
|
||||
setMemeIsFavorite(false);
|
||||
setMemeTags(new Map<string, Tag>());
|
||||
}}
|
||||
disabled={!memeTitle.valid || isSaving || isSavingAndAddingAnother}
|
||||
loading={isSavingAndAddingAnother}
|
||||
style={addMemeStyles.saveAndAddButton}>
|
||||
Save & Add
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
disabled={!memeTitle.valid || isSaving}
|
||||
loading={isSaving}>
|
||||
onPress={async () => {
|
||||
setIsSaving(true);
|
||||
await handleSave();
|
||||
setIsSaving(false);
|
||||
goBack();
|
||||
}}
|
||||
disabled={!memeTitle.valid || isSaving || isSavingAndAddingAnother}
|
||||
loading={isSaving}
|
||||
style={addMemeStyles.saveButton}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { useRealm } from '@realm/react';
|
||||
@@ -13,6 +13,17 @@ import { ORIENTATION, useDimensions } from '../contexts';
|
||||
import { Tag } from '../database';
|
||||
import { TagEditor } from '../components';
|
||||
|
||||
const addTagStyles = StyleSheet.create({
|
||||
saveAndAddButton: {
|
||||
flex: 1,
|
||||
marginRight: 5,
|
||||
},
|
||||
saveButton: {
|
||||
flex: 1,
|
||||
marginLeft: 5,
|
||||
},
|
||||
});
|
||||
|
||||
const AddTag = () => {
|
||||
const { goBack } = useNavigation();
|
||||
const { colors } = useTheme();
|
||||
@@ -24,16 +35,19 @@ const AddTag = () => {
|
||||
validateColor(generateRandomColor()),
|
||||
);
|
||||
|
||||
const handleSave = () => {
|
||||
// Although saving tags is instantaneous, we still want to show a loading
|
||||
// indicator to prevent the user from spamming the save button.
|
||||
const [isSavingAndAddingAnother, setIsSavingAndAddingAnother] =
|
||||
useState(false);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
realm.write(() => {
|
||||
realm.create(Tag.schema.name, {
|
||||
name: tagName.parsed,
|
||||
color: tagColor.parsed,
|
||||
});
|
||||
});
|
||||
|
||||
goBack();
|
||||
};
|
||||
}, [realm, tagColor.parsed, tagName.parsed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -59,12 +73,31 @@ const AddTag = () => {
|
||||
setTagColor={setTagColor}
|
||||
/>
|
||||
</View>
|
||||
<View style={[styles.flex, styles.justifyEnd]}>
|
||||
<View style={[styles.flexRow, styles.fullWidth]}>
|
||||
<Button
|
||||
mode="contained-tonal"
|
||||
icon="plus"
|
||||
onPress={() => {
|
||||
setIsSavingAndAddingAnother(true);
|
||||
handleSave();
|
||||
setTimeout(() => setIsSavingAndAddingAnother(false), 250);
|
||||
setTagName(validateTagName('newTag'));
|
||||
setTagColor(validateColor(generateRandomColor()));
|
||||
}}
|
||||
disabled={!tagName.valid || isSavingAndAddingAnother}
|
||||
loading={isSavingAndAddingAnother}
|
||||
style={addTagStyles.saveAndAddButton}>
|
||||
Save & Add
|
||||
</Button>
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
disabled={!tagName.valid || !tagColor.valid}>
|
||||
onPress={() => {
|
||||
handleSave();
|
||||
goBack();
|
||||
}}
|
||||
disabled={!tagName.valid || isSavingAndAddingAnother}
|
||||
style={addTagStyles.saveButton}>
|
||||
Save
|
||||
</Button>
|
||||
</View>
|
||||
|
@@ -1,16 +1,15 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { useObject, useRealm } from '@realm/react';
|
||||
import { FileSystem } from 'react-native-file-access';
|
||||
import { BSON } from 'realm';
|
||||
import { ORIENTATION, useDimensions } from '../contexts';
|
||||
import styles from '../styles';
|
||||
import { RootStackParamList, ROUTE } from '../types';
|
||||
import { Tag, Meme } from '../database';
|
||||
import { validateMemeTitle } from '../utilities';
|
||||
import { deleteMeme, favoriteMeme, validateMemeTitle } from '../utilities';
|
||||
import { MemeEditor } from '../components';
|
||||
|
||||
const EditMeme = ({
|
||||
@@ -34,9 +33,7 @@ const EditMeme = ({
|
||||
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
|
||||
const handleSave = () => {
|
||||
setIsSaving(true);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
realm.write(() => {
|
||||
meme.tags.forEach(tag => {
|
||||
if (!memeTags.has(tag.id.toHexString())) {
|
||||
@@ -60,32 +57,7 @@ const EditMeme = ({
|
||||
meme.tagsLength = memeTags.size;
|
||||
meme.dateModified = new Date();
|
||||
});
|
||||
|
||||
goBack();
|
||||
};
|
||||
|
||||
const handleFavorite = () => {
|
||||
realm.write(() => {
|
||||
meme.isFavorite = !meme.isFavorite;
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsSaving(true);
|
||||
await FileSystem.unlink(meme.uri);
|
||||
|
||||
realm.write(() => {
|
||||
for (const tag of meme.tags) {
|
||||
tag.dateModified = new Date();
|
||||
tag.memes.slice(tag.memes.indexOf(meme), 1);
|
||||
tag.memesLength -= 1;
|
||||
}
|
||||
|
||||
realm.delete(meme);
|
||||
});
|
||||
|
||||
goBack();
|
||||
};
|
||||
}, [meme, memeTags, memeTitle.parsed, realm]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -94,9 +66,17 @@ const EditMeme = ({
|
||||
<Appbar.Content title={'Edit Meme'} />
|
||||
<Appbar.Action
|
||||
icon={meme.isFavorite ? 'heart' : 'heart-outline'}
|
||||
onPress={handleFavorite}
|
||||
onPress={() => favoriteMeme(realm, meme)}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="delete"
|
||||
onPress={async () => {
|
||||
setIsSaving(true);
|
||||
await deleteMeme(realm, meme);
|
||||
setIsSaving(false);
|
||||
goBack();
|
||||
}}
|
||||
/>
|
||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||
</Appbar.Header>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
@@ -110,7 +90,7 @@ const EditMeme = ({
|
||||
]}>
|
||||
<View style={[styles.flex, styles.justifyStart]}>
|
||||
<MemeEditor
|
||||
imageUri={meme.uri}
|
||||
memeUri={meme.uri}
|
||||
memeTitle={memeTitle}
|
||||
setMemeTitle={setMemeTitle}
|
||||
memeTags={memeTags}
|
||||
@@ -121,7 +101,12 @@ const EditMeme = ({
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
onPress={() => {
|
||||
setIsSaving(true);
|
||||
handleSave();
|
||||
setIsSaving(false);
|
||||
goBack();
|
||||
}}
|
||||
disabled={!memeTitle.valid || isSaving}
|
||||
loading={isSaving}>
|
||||
Save
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { ScrollView, View } from 'react-native';
|
||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||
import { useNavigation } from '@react-navigation/native';
|
||||
@@ -10,7 +10,7 @@ import styles from '../styles';
|
||||
import { ORIENTATION, useDimensions } from '../contexts';
|
||||
import { ROUTE, RootStackParamList } from '../types';
|
||||
import { Tag } from '../database';
|
||||
import { validateColor, validateTagName } from '../utilities';
|
||||
import { deleteTag, validateColor, validateTagName } from '../utilities';
|
||||
|
||||
const EditTag = ({
|
||||
route,
|
||||
@@ -29,36 +29,26 @@ const EditTag = ({
|
||||
const [tagName, setTagName] = useState(validateTagName(tag.name));
|
||||
const [tagColor, setTagColor] = useState(validateColor(tag.color));
|
||||
|
||||
const handleSave = () => {
|
||||
const handleSave = useCallback(() => {
|
||||
realm.write(() => {
|
||||
tag.name = tagName.parsed;
|
||||
tag.color = tagColor.parsed;
|
||||
tag.dateModified = new Date();
|
||||
});
|
||||
|
||||
goBack();
|
||||
};
|
||||
|
||||
const handleDelete = () => {
|
||||
realm.write(() => {
|
||||
for (const meme of tag.memes) {
|
||||
meme.dateModified = new Date();
|
||||
meme.tags.slice(meme.tags.indexOf(tag), 1);
|
||||
meme.tagsLength -= 1;
|
||||
}
|
||||
|
||||
realm.delete(tag);
|
||||
});
|
||||
|
||||
goBack();
|
||||
};
|
||||
}, [realm, tag, tagColor.parsed, tagName.parsed]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => goBack()} />
|
||||
<Appbar.Content title={'Edit Tag'} />
|
||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||
<Appbar.Action
|
||||
icon="delete"
|
||||
onPress={() => {
|
||||
deleteTag(realm, tag);
|
||||
goBack();
|
||||
}}
|
||||
/>
|
||||
</Appbar.Header>
|
||||
<ScrollView
|
||||
contentContainerStyle={[
|
||||
@@ -83,7 +73,10 @@ const EditTag = ({
|
||||
<Button
|
||||
mode="contained"
|
||||
icon="floppy"
|
||||
onPress={handleSave}
|
||||
onPress={() => {
|
||||
handleSave();
|
||||
goBack();
|
||||
}}
|
||||
disabled={!tagName.valid || !tagColor.valid}>
|
||||
Save
|
||||
</Button>
|
||||
|
@@ -3,6 +3,7 @@ export { default as AddTag } from './addTag';
|
||||
export { default as EditMeme } from './editMeme';
|
||||
export { default as EditTag } from './editTag';
|
||||
export { default as Memes } from './memes';
|
||||
export { default as MemeView } from './memeView';
|
||||
export { default as Settings } from './settings';
|
||||
export { default as Tags } from './tags';
|
||||
export { default as Welcome } from './welcome';
|
||||
|
136
src/screens/memeView.tsx
Normal file
136
src/screens/memeView.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { useQuery, useRealm } from '@realm/react';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { Appbar, Portal, Snackbar } from 'react-native-paper';
|
||||
import { RootStackParamList, ROUTE } from '../types';
|
||||
import { Meme } from '../database';
|
||||
import { useDimensions } from '../contexts';
|
||||
import { MemeViewItem } from '../components';
|
||||
import {
|
||||
copyMeme,
|
||||
deleteMeme,
|
||||
editMeme,
|
||||
favoriteMeme,
|
||||
multipleIdQuery,
|
||||
shareMeme,
|
||||
} from '../utilities';
|
||||
import { NavigationProp, useNavigation } from '@react-navigation/native';
|
||||
import styles from '../styles';
|
||||
|
||||
const memeViewStyles = StyleSheet.create({
|
||||
footer: {
|
||||
position: 'absolute',
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 80,
|
||||
},
|
||||
snackbar: {
|
||||
marginBottom: 90,
|
||||
},
|
||||
});
|
||||
|
||||
const MemeView = ({
|
||||
route,
|
||||
}: NativeStackScreenProps<RootStackParamList, ROUTE.MEME_VIEW>) => {
|
||||
const { orientation, dimensions } = useDimensions();
|
||||
const navigation = useNavigation<NavigationProp<RootStackParamList>>();
|
||||
const realm = useRealm();
|
||||
|
||||
const { ids } = route.params;
|
||||
const [index, setIndex] = useState(route.params.index);
|
||||
|
||||
const [snackbarVisible, setSnackbarVisible] = useState(false);
|
||||
const [snackbarMessage, setSnackbarMessage] = useState('');
|
||||
|
||||
const flashListRef = useRef<FlashList<Meme>>(null);
|
||||
|
||||
const memes = useQuery<Meme>(Meme.schema.name, collectionIn => {
|
||||
return collectionIn.filtered(multipleIdQuery(ids));
|
||||
});
|
||||
|
||||
if (memes.length === 0) return <></>;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Appbar.Header>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
<Appbar.Content title={memes[index].title} />
|
||||
</Appbar.Header>
|
||||
<View>
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
key={orientation}
|
||||
data={memes}
|
||||
initialScrollIndex={index}
|
||||
onScroll={event => {
|
||||
const newIndex = Math.round(
|
||||
event.nativeEvent.contentOffset.x /
|
||||
event.nativeEvent.layoutMeasurement.width,
|
||||
);
|
||||
if (newIndex !== index) setIndex(newIndex);
|
||||
}}
|
||||
estimatedItemSize={dimensions.width}
|
||||
pagingEnabled
|
||||
horizontal
|
||||
showsHorizontalScrollIndicator={false}
|
||||
estimatedListSize={{
|
||||
height: dimensions.height - 160,
|
||||
width: dimensions.width,
|
||||
}}
|
||||
renderItem={({ item: meme }) => <MemeViewItem meme={meme} />}
|
||||
/>
|
||||
</View>
|
||||
<Appbar style={[memeViewStyles.footer, styles.flexRowSpaceEvenly]}>
|
||||
<Appbar.Action
|
||||
icon={memes[index].isFavorite ? 'heart' : 'heart-outline'}
|
||||
onPress={() => favoriteMeme(realm, memes[index])}
|
||||
/>
|
||||
<Appbar.Action icon="share" onPress={() => shareMeme(memes[index])} />
|
||||
<Appbar.Action
|
||||
icon="content-copy"
|
||||
onPress={() => {
|
||||
copyMeme(memes[index]);
|
||||
setSnackbarMessage('Meme copied!');
|
||||
setSnackbarVisible(true);
|
||||
}}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="pencil"
|
||||
onPress={() => {
|
||||
editMeme(navigation, memes[index]);
|
||||
}}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="delete"
|
||||
onPress={() => {
|
||||
if (index === memes.length - 1) {
|
||||
setIndex(index - 1);
|
||||
flashListRef.current?.scrollToIndex({
|
||||
index: index - 1,
|
||||
});
|
||||
}
|
||||
void deleteMeme(realm, memes[index]);
|
||||
if (memes.length === 1) navigation.goBack();
|
||||
}}
|
||||
/>
|
||||
</Appbar>
|
||||
<Portal>
|
||||
<Snackbar
|
||||
visible={snackbarVisible}
|
||||
onDismiss={() => setSnackbarVisible(false)}
|
||||
style={memeViewStyles.snackbar}
|
||||
action={{
|
||||
label: 'Dismiss',
|
||||
onPress: () => setSnackbarVisible(false),
|
||||
}}>
|
||||
{snackbarMessage}
|
||||
</Snackbar>
|
||||
</Portal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemeView;
|
@@ -1,4 +1,4 @@
|
||||
import React, { RefObject, useCallback, useRef, useState } from 'react';
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import {
|
||||
BackHandler,
|
||||
NativeScrollEvent,
|
||||
@@ -9,71 +9,22 @@ import { useQuery } from '@realm/react';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import {
|
||||
ParamListBase,
|
||||
useFocusEffect,
|
||||
useNavigation,
|
||||
} from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import styles from '../styles';
|
||||
import { SORT_DIRECTION, VIEW, memesSortQuery } from '../types';
|
||||
import { ROUTE, SORT_DIRECTION, memesSortQuery } from '../types';
|
||||
import { RootState, setNavVisible } from '../state';
|
||||
import { Meme } from '../database';
|
||||
import {
|
||||
HideableHeader,
|
||||
MemesHeader,
|
||||
MemesMasonryView,
|
||||
MemesGridView,
|
||||
MemesListView,
|
||||
} from '../components';
|
||||
|
||||
const MemesView = ({
|
||||
memes,
|
||||
flashListRef,
|
||||
flashListPadding,
|
||||
handleScroll,
|
||||
}: {
|
||||
memes: Realm.Results<Meme & Realm.Object<Meme>>;
|
||||
flashListRef: RefObject<FlashList<Meme>>;
|
||||
flashListPadding: number;
|
||||
handleScroll: (event: NativeSyntheticEvent<NativeScrollEvent>) => void;
|
||||
}) => {
|
||||
const view = useSelector((state: RootState) => state.memes.view);
|
||||
|
||||
switch (view) {
|
||||
case VIEW.MASONRY: {
|
||||
return (
|
||||
<MemesMasonryView
|
||||
memes={memes}
|
||||
flashListRef={flashListRef}
|
||||
flashListPadding={flashListPadding}
|
||||
handleScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case VIEW.GRID: {
|
||||
return (
|
||||
<MemesGridView
|
||||
memes={memes}
|
||||
flashListRef={flashListRef}
|
||||
flashListPadding={flashListPadding}
|
||||
handleScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
case VIEW.LIST: {
|
||||
return (
|
||||
<MemesListView
|
||||
memes={memes}
|
||||
flashListRef={flashListRef}
|
||||
flashListPadding={flashListPadding}
|
||||
handleScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
};
|
||||
import { HideableHeader, MemesHeader, MemesList } from '../components';
|
||||
|
||||
const Memes = () => {
|
||||
const { colors } = useTheme();
|
||||
const { navigate } =
|
||||
useNavigation<NativeStackNavigationProp<ParamListBase>>();
|
||||
const sort = useSelector((state: RootState) => state.memes.sort);
|
||||
const sortDirection = useSelector(
|
||||
(state: RootState) => state.memes.sortDirection,
|
||||
@@ -154,11 +105,9 @@ const Memes = () => {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
const handleBackPress = () => {
|
||||
if (scrollOffset > 0) {
|
||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
if (scrollOffset <= 0) return false;
|
||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||
return true;
|
||||
};
|
||||
|
||||
BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
||||
@@ -190,11 +139,17 @@ const Memes = () => {
|
||||
}}
|
||||
/>
|
||||
</HideableHeader>
|
||||
<MemesView
|
||||
<MemesList
|
||||
memes={memes}
|
||||
flashListRef={flashListRef}
|
||||
flashListPadding={flashListPadding}
|
||||
handleScroll={handleScroll}
|
||||
focusMeme={(index: number) => {
|
||||
navigate(ROUTE.MEME_VIEW, {
|
||||
ids: memes.map(meme => meme.id.toHexString()),
|
||||
index,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
@@ -19,12 +19,12 @@ import { SORT_DIRECTION, tagSortQuery } from '../types';
|
||||
import { ORIENTATION, useDimensions } from '../contexts';
|
||||
|
||||
const tagsStyles = StyleSheet.create({
|
||||
helperText: {
|
||||
marginVertical: 10,
|
||||
},
|
||||
flashList: {
|
||||
paddingBottom: 100,
|
||||
},
|
||||
helperText: {
|
||||
marginVertical: 15,
|
||||
},
|
||||
});
|
||||
|
||||
const Tags = () => {
|
||||
|
@@ -57,6 +57,10 @@ const styles = StyleSheet.create({
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-between',
|
||||
},
|
||||
flexRowSpaceEvenly: {
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'space-evenly',
|
||||
},
|
||||
flexColumn: {
|
||||
flexDirection: 'column',
|
||||
},
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { DefaultTheme as DefaultNavigationTheme } from '@react-navigation/native';
|
||||
import {
|
||||
DarkTheme,
|
||||
DefaultTheme as LightTheme,
|
||||
} from '@react-navigation/native';
|
||||
import {
|
||||
MD3LightTheme,
|
||||
MD3DarkTheme,
|
||||
@@ -98,12 +101,12 @@ const darkTheme = {
|
||||
};
|
||||
|
||||
const { LightTheme: lightNavigationTheme } = adaptNavigationTheme({
|
||||
reactNavigationLight: DefaultNavigationTheme,
|
||||
reactNavigationLight: LightTheme,
|
||||
materialLight: lightTheme,
|
||||
});
|
||||
|
||||
const { DarkTheme: darkNavigationTheme } = adaptNavigationTheme({
|
||||
reactNavigationDark: DefaultNavigationTheme,
|
||||
reactNavigationDark: DarkTheme,
|
||||
materialDark: darkTheme,
|
||||
});
|
||||
|
@@ -5,13 +5,19 @@ enum ROUTE {
|
||||
MEMES = 'Memes',
|
||||
TAGS = 'Tags',
|
||||
SETTINGS = 'Settings',
|
||||
MEME_VIEW = 'Meme View',
|
||||
ADD_MEME = 'Add Meme',
|
||||
EDIT_MEME = 'Edit Meme',
|
||||
ADD_TAG = 'Add Tag',
|
||||
EDIT_TAG = 'Edit Tag',
|
||||
}
|
||||
|
||||
interface AddMemeRouteParamsFromFiles {
|
||||
interface MemeViewRouteParams {
|
||||
ids: string[];
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface AddMemeRouteParams {
|
||||
file: DocumentPickerResponse;
|
||||
}
|
||||
|
||||
@@ -26,11 +32,13 @@ interface EditTagRouteParams {
|
||||
interface RootStackParamList {
|
||||
[key: string]:
|
||||
| undefined
|
||||
| AddMemeRouteParamsFromFiles
|
||||
| MemeViewRouteParams
|
||||
| AddMemeRouteParams
|
||||
| EditMemeRouteParams
|
||||
| EditTagRouteParams;
|
||||
[ROUTE.MAIN]: undefined;
|
||||
[ROUTE.ADD_MEME]: AddMemeRouteParamsFromFiles;
|
||||
[ROUTE.MEME_VIEW]: MemeViewRouteParams;
|
||||
[ROUTE.ADD_MEME]: AddMemeRouteParams;
|
||||
[ROUTE.EDIT_MEME]: EditMemeRouteParams;
|
||||
[ROUTE.ADD_TAG]: undefined;
|
||||
[ROUTE.EDIT_TAG]: EditTagRouteParams;
|
||||
|
@@ -32,6 +32,15 @@ const rgbToHex = (rgb: string) => {
|
||||
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
|
||||
};
|
||||
|
||||
const rgbToRgba = (rgb: string, alpha: number) => {
|
||||
const [r, g, b] = rgb
|
||||
.replaceAll(/[^\d,]/g, '')
|
||||
.split(',')
|
||||
.map(value => Number.parseInt(value, 10));
|
||||
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
};
|
||||
|
||||
const generateRandomColor = () => {
|
||||
const r = Math.floor(Math.random() * 256)
|
||||
.toString(16)
|
||||
@@ -50,5 +59,6 @@ export {
|
||||
isHexColor,
|
||||
isRgbColor,
|
||||
rgbToHex,
|
||||
rgbToRgba,
|
||||
generateRandomColor,
|
||||
};
|
||||
|
@@ -3,6 +3,7 @@ export {
|
||||
isHexColor,
|
||||
isRgbColor,
|
||||
rgbToHex,
|
||||
rgbToRgba,
|
||||
generateRandomColor,
|
||||
} from './color';
|
||||
export { packageName, appName, fileProvider, noOp } from './constants';
|
||||
@@ -14,8 +15,16 @@ export {
|
||||
allowedMimeTypes,
|
||||
getMemeType,
|
||||
} from './filesystem';
|
||||
export { isPermissionForPath, clearPermissions } from './permissions';
|
||||
export { getSortIcon, getViewIcon } from './icon';
|
||||
export {
|
||||
favoriteMeme,
|
||||
shareMeme,
|
||||
copyMeme,
|
||||
editMeme,
|
||||
deleteMeme,
|
||||
} from './meme';
|
||||
export { isPermissionForPath, clearPermissions } from './permissions';
|
||||
export { deleteTag } from './tag';
|
||||
export {
|
||||
type StringValidationResult,
|
||||
validateMemeTitle,
|
||||
|
51
src/utilities/meme.ts
Normal file
51
src/utilities/meme.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NavigationProp } from '@react-navigation/native';
|
||||
import { Dirs, FileSystem } from 'react-native-file-access';
|
||||
import { extension } from 'react-native-mime-types';
|
||||
import Share from 'react-native-share';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Meme } from '../database';
|
||||
import { ROUTE, RootStackParamList } from '../types';
|
||||
|
||||
const favoriteMeme = (realm: Realm, meme: Meme) => {
|
||||
realm.write(() => {
|
||||
meme.isFavorite = !meme.isFavorite;
|
||||
});
|
||||
};
|
||||
|
||||
const shareMeme = async (meme: Meme) => {
|
||||
const fileExtension = extension(meme.mimeType) as string;
|
||||
const cacheUri = `${Dirs.CacheDir}/${meme.id.toHexString()}.${fileExtension}`;
|
||||
await FileSystem.cp(meme.uri, cacheUri);
|
||||
await Share.open({
|
||||
url: `file://${cacheUri}`,
|
||||
type: meme.mimeType,
|
||||
failOnCancel: false,
|
||||
});
|
||||
};
|
||||
|
||||
const copyMeme = (meme: Meme) => {
|
||||
Clipboard.setURI(meme.uri);
|
||||
};
|
||||
|
||||
const editMeme = (
|
||||
navigation: NavigationProp<RootStackParamList>,
|
||||
meme: Meme,
|
||||
) => {
|
||||
navigation.navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() });
|
||||
};
|
||||
|
||||
const deleteMeme = async (realm: Realm, meme: Meme) => {
|
||||
await FileSystem.unlink(meme.uri);
|
||||
|
||||
realm.write(() => {
|
||||
for (const tag of meme.tags) {
|
||||
tag.dateModified = new Date();
|
||||
tag.memes.slice(tag.memes.indexOf(meme), 1);
|
||||
tag.memesLength -= 1;
|
||||
}
|
||||
|
||||
realm.delete(meme);
|
||||
});
|
||||
};
|
||||
|
||||
export { favoriteMeme, shareMeme, copyMeme, editMeme, deleteMeme };
|
16
src/utilities/tag.ts
Normal file
16
src/utilities/tag.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Realm } from '@realm/react';
|
||||
import { Tag } from '../database';
|
||||
|
||||
const deleteTag = (realm: Realm, tag: Tag) => {
|
||||
realm.write(() => {
|
||||
for (const meme of tag.memes) {
|
||||
meme.dateModified = new Date();
|
||||
meme.tags.slice(meme.tags.indexOf(tag), 1);
|
||||
meme.tagsLength -= 1;
|
||||
}
|
||||
|
||||
realm.delete(tag);
|
||||
});
|
||||
};
|
||||
|
||||
export { deleteTag };
|
Reference in New Issue
Block a user