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
|
- package-lock.json
|
||||||
paths:
|
paths:
|
||||||
- node_modules
|
- node_modules
|
||||||
policy: pull
|
|
||||||
before_script:
|
before_script:
|
||||||
- npm install
|
- npm install
|
||||||
- npm run postinstall
|
|
||||||
script:
|
script:
|
||||||
- npm run build
|
- npm run build
|
||||||
rules:
|
rules:
|
||||||
@@ -30,10 +28,8 @@ compile:
|
|||||||
- package-lock.json
|
- package-lock.json
|
||||||
paths:
|
paths:
|
||||||
- node_modules
|
- node_modules
|
||||||
policy: pull
|
|
||||||
before_script:
|
before_script:
|
||||||
- npm install
|
- npm install
|
||||||
- npm run postinstall
|
|
||||||
script:
|
script:
|
||||||
- npm run compile
|
- npm run compile
|
||||||
after_script:
|
after_script:
|
||||||
@@ -56,10 +52,8 @@ test:
|
|||||||
- package-lock.json
|
- package-lock.json
|
||||||
paths:
|
paths:
|
||||||
- node_modules
|
- node_modules
|
||||||
policy: pull
|
|
||||||
before_script:
|
before_script:
|
||||||
- npm install
|
- npm install
|
||||||
- npm run postinstall
|
|
||||||
script:
|
script:
|
||||||
- npm run test
|
- npm run test
|
||||||
rules:
|
rules:
|
||||||
@@ -77,7 +71,6 @@ lint:
|
|||||||
policy: pull
|
policy: pull
|
||||||
before_script:
|
before_script:
|
||||||
- npm install
|
- npm install
|
||||||
- npm run postinstall
|
|
||||||
script:
|
script:
|
||||||
- npm run lint
|
- npm run lint
|
||||||
rules:
|
rules:
|
||||||
@@ -90,12 +83,12 @@ release:
|
|||||||
- job: compile
|
- job: compile
|
||||||
artifacts: true
|
artifacts: true
|
||||||
script:
|
script:
|
||||||
- echo "Create Release $CI_COMMIT_SHA"
|
- echo "Create Release $CI_COMMIT_TAG"
|
||||||
release:
|
release:
|
||||||
name: "Release $CI_COMMIT_SHORT_SHA"
|
name: "Release $CI_COMMIT_TAG"
|
||||||
tag_name: "$CI_COMMIT_SHORT_SHA"
|
tag_name: "$CI_COMMIT_TAG"
|
||||||
ref: "$CI_COMMIT_SHORT_SHA"
|
ref: "$CI_COMMIT_TAG"
|
||||||
description: "Release $CI_COMMIT_SHORT_SHA"
|
description: "Release $CI_COMMIT_SHA"
|
||||||
assets:
|
assets:
|
||||||
links:
|
links:
|
||||||
- name: "app-release.apk"
|
- name: "app-release.apk"
|
||||||
|
3
index.js
3
index.js
@@ -1,9 +1,6 @@
|
|||||||
import { AppRegistry } from 'react-native';
|
import { AppRegistry } from 'react-native';
|
||||||
import 'react-native-get-random-values';
|
import 'react-native-get-random-values';
|
||||||
import { enableFreeze } from 'react-native-screens';
|
|
||||||
import App from './src/app';
|
import App from './src/app';
|
||||||
import { name as appName } from './app.json';
|
import { name as appName } from './app.json';
|
||||||
|
|
||||||
enableFreeze(true);
|
|
||||||
|
|
||||||
AppRegistry.registerComponent(appName, () => App);
|
AppRegistry.registerComponent(appName, () => App);
|
||||||
|
54
package-lock.json
generated
54
package-lock.json
generated
@@ -10,6 +10,7 @@
|
|||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bankify/redux-persist-realm": "^0.1.3",
|
"@bankify/redux-persist-realm": "^0.1.3",
|
||||||
|
"@likashefqet/react-native-image-zoom": "^1.3.0",
|
||||||
"@react-native-clipboard/clipboard": "^1.11.2",
|
"@react-native-clipboard/clipboard": "^1.11.2",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||||
"@react-navigation/native": "^6.1.7",
|
"@react-navigation/native": "^6.1.7",
|
||||||
@@ -23,7 +24,6 @@
|
|||||||
"react-native-file-access": "^3.0.4",
|
"react-native-file-access": "^3.0.4",
|
||||||
"react-native-gesture-handler": "^2.12.0",
|
"react-native-gesture-handler": "^2.12.0",
|
||||||
"react-native-get-random-values": "^1.9.0",
|
"react-native-get-random-values": "^1.9.0",
|
||||||
"react-native-image-zoom-viewer": "^3.0.1",
|
|
||||||
"react-native-mime-types": "^2.4.0",
|
"react-native-mime-types": "^2.4.0",
|
||||||
"react-native-paper": "^5.9.1",
|
"react-native-paper": "^5.9.1",
|
||||||
"react-native-reanimated": "^3.3.0",
|
"react-native-reanimated": "^3.3.0",
|
||||||
@@ -2988,6 +2988,17 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz",
|
||||||
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw=="
|
"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": {
|
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||||
"version": "5.1.1-v1",
|
"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",
|
"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"
|
"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": {
|
"node_modules/react-native-mime-types": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz",
|
"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": {
|
"@nicolo-ribaudo/eslint-scope-5-internals": {
|
||||||
"version": "5.1.1-v1",
|
"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",
|
"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"
|
"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": {
|
"react-native-mime-types": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz",
|
||||||
|
@@ -15,6 +15,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bankify/redux-persist-realm": "^0.1.3",
|
"@bankify/redux-persist-realm": "^0.1.3",
|
||||||
|
"@likashefqet/react-native-image-zoom": "^1.3.0",
|
||||||
"@react-native-clipboard/clipboard": "^1.11.2",
|
"@react-native-clipboard/clipboard": "^1.11.2",
|
||||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||||
"@react-navigation/native": "^6.1.7",
|
"@react-navigation/native": "^6.1.7",
|
||||||
@@ -28,7 +29,6 @@
|
|||||||
"react-native-file-access": "^3.0.4",
|
"react-native-file-access": "^3.0.4",
|
||||||
"react-native-gesture-handler": "^2.12.0",
|
"react-native-gesture-handler": "^2.12.0",
|
||||||
"react-native-get-random-values": "^1.9.0",
|
"react-native-get-random-values": "^1.9.0",
|
||||||
"react-native-image-zoom-viewer": "^3.0.1",
|
|
||||||
"react-native-mime-types": "^2.4.0",
|
"react-native-mime-types": "^2.4.0",
|
||||||
"react-native-paper": "^5.9.1",
|
"react-native-paper": "^5.9.1",
|
||||||
"react-native-reanimated": "^3.3.0",
|
"react-native-reanimated": "^3.3.0",
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
export {
|
export {
|
||||||
MemesGridView,
|
MemesList,
|
||||||
MemesListView,
|
|
||||||
MemesMasonryView,
|
|
||||||
MemeEditor,
|
MemeEditor,
|
||||||
MemesHeader,
|
MemesHeader,
|
||||||
MemeTagSearchModal,
|
MemeTagSearchModal,
|
||||||
MemeTagSelector,
|
MemeTagSelector,
|
||||||
|
MemeViewItem,
|
||||||
} from './memes';
|
} from './memes';
|
||||||
export { TagChip, TagEditor, TagPreview, TagRow, TagsHeader } from './tags';
|
export { TagChip, TagEditor, TagPreview, TagRow, TagsHeader } from './tags';
|
||||||
export { default as FloatingActionButton } from './floatingActionButton';
|
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 MemesList } from './memesList/memesList';
|
||||||
export { default as MemesListView } from './listView/memesListView';
|
|
||||||
export { default as MemesMasonryView } from './masonryView/memesMasonryView';
|
|
||||||
export { default as MemeEditor } from './memeEditor';
|
export { default as MemeEditor } from './memeEditor';
|
||||||
export { default as MemesHeader } from './memesHeader';
|
export { default as MemesHeader } from './memesHeader';
|
||||||
export { default as MemeTagSearchModal } from './memeTagSearchModal';
|
export { default as MemeTagSearchModal } from './memeTagSearchModal';
|
||||||
export { default as MemeTagSelector } from './memeTagSelector';
|
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 { HelperText, TextInput } from 'react-native-paper';
|
||||||
import { Image } from 'react-native';
|
import { Image } from 'react-native';
|
||||||
import { useDimensions } from '../../contexts';
|
import { useDimensions } from '../../contexts';
|
||||||
@@ -21,13 +21,13 @@ const memeEditorStyles = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MemeEditor = ({
|
const MemeEditor = ({
|
||||||
imageUri,
|
memeUri,
|
||||||
memeTitle,
|
memeTitle,
|
||||||
setMemeTitle,
|
setMemeTitle,
|
||||||
memeTags,
|
memeTags,
|
||||||
setMemeTags,
|
setMemeTags,
|
||||||
}: {
|
}: {
|
||||||
imageUri: string;
|
memeUri: string;
|
||||||
memeTitle: StringValidationResult;
|
memeTitle: StringValidationResult;
|
||||||
setMemeTitle: (name: StringValidationResult) => void;
|
setMemeTitle: (name: StringValidationResult) => void;
|
||||||
memeTags: Map<string, Tag>;
|
memeTags: Map<string, Tag>;
|
||||||
@@ -38,15 +38,23 @@ const MemeEditor = ({
|
|||||||
const [imageWidth, setImageWidth] = useState<number>();
|
const [imageWidth, setImageWidth] = useState<number>();
|
||||||
const [imageHeight, setImageHeight] = useState<number>();
|
const [imageHeight, setImageHeight] = useState<number>();
|
||||||
|
|
||||||
Image.getSize(imageUri, (width, height) => {
|
useEffect(() => {
|
||||||
const paddedWidth = dimensions.width * 0.92;
|
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||||
const paddedHeight = Math.max(
|
setImageWidth(undefined);
|
||||||
Math.min((paddedWidth / width) * height, 500),
|
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||||
100,
|
setImageHeight(undefined);
|
||||||
);
|
|
||||||
setImageWidth(paddedWidth);
|
Image.getSize(memeUri, (width, height) => {
|
||||||
setImageHeight(paddedHeight);
|
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 />;
|
if (!imageWidth || !imageHeight) return <LoadingView />;
|
||||||
|
|
||||||
@@ -64,7 +72,7 @@ const MemeEditor = ({
|
|||||||
{memeTitle.error}
|
{memeTitle.error}
|
||||||
</HelperText>
|
</HelperText>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: imageUri }}
|
source={{ uri: memeUri }}
|
||||||
style={[
|
style={[
|
||||||
{
|
{
|
||||||
width: imageWidth,
|
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 React, { useState } from 'react';
|
||||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
|
||||||
import { Image, TouchableHighlight, View } from 'react-native';
|
import { Image, TouchableHighlight, View } from 'react-native';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Meme } from '../../../database';
|
import { Meme } from '../../../database';
|
||||||
import { ROUTE, RootStackParamList } from '../../../types';
|
|
||||||
import { useDimensions } from '../../../contexts';
|
import { useDimensions } from '../../../contexts';
|
||||||
import { RootState } from '../../../state';
|
import { RootState } from '../../../state';
|
||||||
|
|
||||||
const MemesGridItem = ({ meme }: { meme: Meme }) => {
|
const MemesGridItem = ({
|
||||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
meme,
|
||||||
|
index,
|
||||||
|
focusMeme,
|
||||||
|
}: {
|
||||||
|
meme: Meme;
|
||||||
|
index: number;
|
||||||
|
focusMeme: (index: number) => void;
|
||||||
|
}) => {
|
||||||
const { dimensions } = useDimensions();
|
const { dimensions } = useDimensions();
|
||||||
const gridColumns = useSelector(
|
const gridColumns = useSelector(
|
||||||
(state: RootState) => state.settings.gridColumns,
|
(state: RootState) => state.settings.gridColumns,
|
||||||
@@ -27,10 +32,7 @@ const MemesGridItem = ({ meme }: { meme: Meme }) => {
|
|||||||
<>
|
<>
|
||||||
{imageWidth && imageHeight && (
|
{imageWidth && imageHeight && (
|
||||||
<View>
|
<View>
|
||||||
<TouchableHighlight
|
<TouchableHighlight onPress={() => focusMeme(index)}>
|
||||||
onPress={() =>
|
|
||||||
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
|
||||||
}>
|
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: meme.uri }}
|
source={{ uri: meme.uri }}
|
||||||
style={[{ width: imageWidth, height: imageHeight }]}
|
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 React, { useState } from 'react';
|
||||||
import { Image, StyleSheet, View } from 'react-native';
|
import { Image, StyleSheet, View } from 'react-native';
|
||||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
|
||||||
import { Text, TouchableRipple } from 'react-native-paper';
|
import { Text, TouchableRipple } from 'react-native-paper';
|
||||||
import { Meme } from '../../../database';
|
import { Meme } from '../../../database';
|
||||||
import { ROUTE, RootStackParamList } from '../../../types';
|
|
||||||
import styles from '../../../styles';
|
import styles from '../../../styles';
|
||||||
import { useDimensions } from '../../../contexts';
|
import { useDimensions } from '../../../contexts';
|
||||||
|
|
||||||
@@ -23,8 +21,15 @@ const memesListItemStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const MemesListItem = ({ meme }: { meme: Meme }) => {
|
const MemesListItem = ({
|
||||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
meme,
|
||||||
|
index,
|
||||||
|
focusMeme,
|
||||||
|
}: {
|
||||||
|
meme: Meme;
|
||||||
|
index: number;
|
||||||
|
focusMeme: (index: number) => void;
|
||||||
|
}) => {
|
||||||
const { dimensions } = useDimensions();
|
const { dimensions } = useDimensions();
|
||||||
|
|
||||||
const [imageWidth, setImageWidth] = useState<number>();
|
const [imageWidth, setImageWidth] = useState<number>();
|
||||||
@@ -40,9 +45,7 @@ const MemesListItem = ({ meme }: { meme: Meme }) => {
|
|||||||
<>
|
<>
|
||||||
{imageWidth && imageHeight && (
|
{imageWidth && imageHeight && (
|
||||||
<TouchableRipple
|
<TouchableRipple
|
||||||
onPress={() =>
|
onPress={() => focusMeme(index)}
|
||||||
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
|
||||||
}
|
|
||||||
style={[memesListItemStyles.view, styles.flexRow]}>
|
style={[memesListItemStyles.view, styles.flexRow]}>
|
||||||
<>
|
<>
|
||||||
<View style={{ width: imageWidth, height: imageHeight }}>
|
<View style={{ width: imageWidth, height: imageHeight }}>
|
@@ -1,9 +1,7 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Image, StyleSheet, TouchableHighlight } from 'react-native';
|
import { Image, StyleSheet, TouchableHighlight } from 'react-native';
|
||||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import { Meme } from '../../../database';
|
import { Meme } from '../../../database';
|
||||||
import { ROUTE, RootStackParamList } from '../../../types';
|
|
||||||
import { useDimensions } from '../../../contexts';
|
import { useDimensions } from '../../../contexts';
|
||||||
import { RootState } from '../../../state';
|
import { RootState } from '../../../state';
|
||||||
|
|
||||||
@@ -17,8 +15,15 @@ const memeMasonryItemStyles = StyleSheet.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const MemesMasonryItem = ({ meme }: { meme: Meme }) => {
|
const MemesMasonryItem = ({
|
||||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
meme,
|
||||||
|
index,
|
||||||
|
focusMeme,
|
||||||
|
}: {
|
||||||
|
meme: Meme;
|
||||||
|
index: number;
|
||||||
|
focusMeme: (index: number) => void;
|
||||||
|
}) => {
|
||||||
const { dimensions } = useDimensions();
|
const { dimensions } = useDimensions();
|
||||||
const masonryColumns = useSelector(
|
const masonryColumns = useSelector(
|
||||||
(state: RootState) => state.settings.masonryColumns,
|
(state: RootState) => state.settings.masonryColumns,
|
||||||
@@ -37,9 +42,7 @@ const MemesMasonryItem = ({ meme }: { meme: Meme }) => {
|
|||||||
<>
|
<>
|
||||||
{imageWidth && imageHeight && (
|
{imageWidth && imageHeight && (
|
||||||
<TouchableHighlight
|
<TouchableHighlight
|
||||||
onPress={() =>
|
onPress={() => focusMeme(index)}
|
||||||
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
|
||||||
}
|
|
||||||
style={memeMasonryItemStyles.view}>
|
style={memeMasonryItemStyles.view}>
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: meme.uri }}
|
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 { HelperText, TextInput } from 'react-native-paper';
|
||||||
import TagPreview from './tagPreview';
|
import TagPreview from './tagPreview';
|
||||||
import {
|
import {
|
||||||
@@ -21,15 +21,16 @@ const TagEditor = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const lastValidTagColor = useRef(tagColor.parsed);
|
const lastValidTagColor = useRef(tagColor.parsed);
|
||||||
|
|
||||||
const handleTagColorChange = (color: string) => {
|
useEffect(() => {
|
||||||
const result = validateColor(color);
|
if (tagColor.valid) lastValidTagColor.current = tagColor.parsed;
|
||||||
setTagColor(result);
|
}, [tagColor]);
|
||||||
if (result.valid) lastValidTagColor.current = result.parsed;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<TagPreview name={tagName.parsed} color={lastValidTagColor.current} />
|
<TagPreview
|
||||||
|
name={tagName.parsed}
|
||||||
|
color={tagColor.valid ? tagColor.parsed : lastValidTagColor.current}
|
||||||
|
/>
|
||||||
<TextInput
|
<TextInput
|
||||||
mode="outlined"
|
mode="outlined"
|
||||||
label="Name"
|
label="Name"
|
||||||
@@ -45,13 +46,13 @@ const TagEditor = ({
|
|||||||
mode="outlined"
|
mode="outlined"
|
||||||
label="Color"
|
label="Color"
|
||||||
value={tagColor.raw}
|
value={tagColor.raw}
|
||||||
onChangeText={handleTagColorChange}
|
onChangeText={color => setTagColor(validateColor(color))}
|
||||||
error={!tagColor.valid}
|
error={!tagColor.valid}
|
||||||
autoCorrect={false}
|
autoCorrect={false}
|
||||||
right={
|
right={
|
||||||
<TextInput.Icon
|
<TextInput.Icon
|
||||||
icon="palette"
|
icon="palette"
|
||||||
onPress={() => handleTagColorChange(generateRandomColor())}
|
onPress={() => setTagColor(validateColor(generateRandomColor()))}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
@@ -22,6 +22,7 @@ class Meme extends Object<Meme> {
|
|||||||
id!: BSON.UUID;
|
id!: BSON.UUID;
|
||||||
type!: MEME_TYPE;
|
type!: MEME_TYPE;
|
||||||
uri!: string;
|
uri!: string;
|
||||||
|
mimeType!: string;
|
||||||
size!: number;
|
size!: number;
|
||||||
title!: string;
|
title!: string;
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
@@ -39,6 +40,7 @@ class Meme extends Object<Meme> {
|
|||||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||||
type: { type: 'string', indexed: true },
|
type: { type: 'string', indexed: true },
|
||||||
uri: 'string',
|
uri: 'string',
|
||||||
|
mimeType: 'string',
|
||||||
size: 'int',
|
size: 'int',
|
||||||
title: 'string',
|
title: 'string',
|
||||||
isFavorite: { type: 'bool', indexed: true, default: false },
|
isFavorite: { type: 'bool', indexed: true, default: false },
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
EditTag,
|
EditTag,
|
||||||
AddMeme,
|
AddMeme,
|
||||||
AddTag,
|
AddTag,
|
||||||
|
MemeView,
|
||||||
} from './screens';
|
} from './screens';
|
||||||
import { darkNavigationTheme, lightNavigationTheme } from './theme';
|
import { darkNavigationTheme, lightNavigationTheme } from './theme';
|
||||||
import {
|
import {
|
||||||
@@ -91,10 +92,14 @@ const NavigationContainer = () => {
|
|||||||
<StackNavigatorBase.Navigator
|
<StackNavigatorBase.Navigator
|
||||||
screenOptions={{
|
screenOptions={{
|
||||||
headerShown: false,
|
headerShown: false,
|
||||||
freezeOnBlur: true,
|
|
||||||
animation: 'slide_from_bottom',
|
animation: 'slide_from_bottom',
|
||||||
}}>
|
}}>
|
||||||
<StackNavigatorBase.Screen name={ROUTE.MAIN} component={TabNavigator} />
|
<StackNavigatorBase.Screen name={ROUTE.MAIN} component={TabNavigator} />
|
||||||
|
<StackNavigatorBase.Screen
|
||||||
|
name={ROUTE.MEME_VIEW}
|
||||||
|
component={MemeView}
|
||||||
|
options={{ presentation: 'modal', animation: 'default' }}
|
||||||
|
/>
|
||||||
<StackNavigatorBase.Screen name={ROUTE.ADD_MEME} component={AddMeme} />
|
<StackNavigatorBase.Screen name={ROUTE.ADD_MEME} component={AddMeme} />
|
||||||
<StackNavigatorBase.Screen
|
<StackNavigatorBase.Screen
|
||||||
name={ROUTE.EDIT_MEME}
|
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 { Appbar, Button, useTheme } from 'react-native-paper';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
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 { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
import { useRealm } from '@realm/react';
|
import { useRealm } from '@realm/react';
|
||||||
import { BSON } from 'realm';
|
import { BSON } from 'realm';
|
||||||
@@ -13,8 +13,23 @@ import styles from '../styles';
|
|||||||
import { ROUTE, RootStackParamList } from '../types';
|
import { ROUTE, RootStackParamList } from '../types';
|
||||||
import { Meme, Tag } from '../database';
|
import { Meme, Tag } from '../database';
|
||||||
import { RootState } from '../state';
|
import { RootState } from '../state';
|
||||||
import { getMemeType, validateMemeTitle } from '../utilities';
|
import { allowedMimeTypes, getMemeType, validateMemeTitle } from '../utilities';
|
||||||
import { MemeEditor } from '../components';
|
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 = ({
|
const AddMeme = ({
|
||||||
route,
|
route,
|
||||||
@@ -28,31 +43,32 @@ const AddMeme = ({
|
|||||||
(state: RootState) => state.settings.storageUri,
|
(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 [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||||
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
|
const [memeIsFavorite, setMemeIsFavorite] = useState(false);
|
||||||
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
|
const [memeTags, setMemeTags] = useState(new Map<string, Tag>());
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
const [isSavingAndAddingAnother, setIsSavingAndAddingAnother] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = useCallback(async () => {
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
const uuid = new BSON.UUID();
|
const uuid = new BSON.UUID();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const mimeType = file.type!;
|
const mimeType = file.current.type!;
|
||||||
const memeType = getMemeType(mimeType);
|
const memeType = getMemeType(mimeType);
|
||||||
|
|
||||||
const fileExtension = extension(mimeType);
|
const fileExtension = extension(mimeType) as string;
|
||||||
if (!fileExtension) goBack();
|
if (!fileExtension) goBack();
|
||||||
|
|
||||||
const uri = AndroidScoped.appendPath(
|
const uri = AndroidScoped.appendPath(
|
||||||
storageUri,
|
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);
|
const { size } = await FileSystem.stat(uri);
|
||||||
|
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
@@ -60,6 +76,7 @@ const AddMeme = ({
|
|||||||
id: uuid,
|
id: uuid,
|
||||||
type: memeType,
|
type: memeType,
|
||||||
uri,
|
uri,
|
||||||
|
mimeType,
|
||||||
size,
|
size,
|
||||||
title: memeTitle.parsed,
|
title: memeTitle.parsed,
|
||||||
isFavorite: memeIsFavorite,
|
isFavorite: memeIsFavorite,
|
||||||
@@ -73,9 +90,7 @@ const AddMeme = ({
|
|||||||
tag.memesLength = tag.memes.length;
|
tag.memesLength = tag.memes.length;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}, [goBack, memeIsFavorite, memeTags, memeTitle.parsed, realm, storageUri]);
|
||||||
goBack();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,20 +114,46 @@ const AddMeme = ({
|
|||||||
]}>
|
]}>
|
||||||
<View style={[styles.flex, styles.justifyStart]}>
|
<View style={[styles.flex, styles.justifyStart]}>
|
||||||
<MemeEditor
|
<MemeEditor
|
||||||
imageUri={file.uri}
|
memeUri={memeUri}
|
||||||
memeTitle={memeTitle}
|
memeTitle={memeTitle}
|
||||||
setMemeTitle={setMemeTitle}
|
setMemeTitle={setMemeTitle}
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
setMemeTags={setMemeTags}
|
setMemeTags={setMemeTags}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
icon="floppy"
|
icon="floppy"
|
||||||
onPress={handleSave}
|
onPress={async () => {
|
||||||
disabled={!memeTitle.valid || isSaving}
|
setIsSaving(true);
|
||||||
loading={isSaving}>
|
await handleSave();
|
||||||
|
setIsSaving(false);
|
||||||
|
goBack();
|
||||||
|
}}
|
||||||
|
disabled={!memeTitle.valid || isSaving || isSavingAndAddingAnother}
|
||||||
|
loading={isSaving}
|
||||||
|
style={addMemeStyles.saveButton}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, StyleSheet, View } from 'react-native';
|
||||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { useRealm } from '@realm/react';
|
import { useRealm } from '@realm/react';
|
||||||
@@ -13,6 +13,17 @@ import { ORIENTATION, useDimensions } from '../contexts';
|
|||||||
import { Tag } from '../database';
|
import { Tag } from '../database';
|
||||||
import { TagEditor } from '../components';
|
import { TagEditor } from '../components';
|
||||||
|
|
||||||
|
const addTagStyles = StyleSheet.create({
|
||||||
|
saveAndAddButton: {
|
||||||
|
flex: 1,
|
||||||
|
marginRight: 5,
|
||||||
|
},
|
||||||
|
saveButton: {
|
||||||
|
flex: 1,
|
||||||
|
marginLeft: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const AddTag = () => {
|
const AddTag = () => {
|
||||||
const { goBack } = useNavigation();
|
const { goBack } = useNavigation();
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
@@ -24,16 +35,19 @@ const AddTag = () => {
|
|||||||
validateColor(generateRandomColor()),
|
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.write(() => {
|
||||||
realm.create(Tag.schema.name, {
|
realm.create(Tag.schema.name, {
|
||||||
name: tagName.parsed,
|
name: tagName.parsed,
|
||||||
color: tagColor.parsed,
|
color: tagColor.parsed,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}, [realm, tagColor.parsed, tagName.parsed]);
|
||||||
goBack();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -59,12 +73,31 @@ const AddTag = () => {
|
|||||||
setTagColor={setTagColor}
|
setTagColor={setTagColor}
|
||||||
/>
|
/>
|
||||||
</View>
|
</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
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
icon="floppy"
|
icon="floppy"
|
||||||
onPress={handleSave}
|
onPress={() => {
|
||||||
disabled={!tagName.valid || !tagColor.valid}>
|
handleSave();
|
||||||
|
goBack();
|
||||||
|
}}
|
||||||
|
disabled={!tagName.valid || isSavingAndAddingAnother}
|
||||||
|
style={addTagStyles.saveButton}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
|
@@ -1,16 +1,15 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||||
import { useObject, useRealm } from '@realm/react';
|
import { useObject, useRealm } from '@realm/react';
|
||||||
import { FileSystem } from 'react-native-file-access';
|
|
||||||
import { BSON } from 'realm';
|
import { BSON } from 'realm';
|
||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import { RootStackParamList, ROUTE } from '../types';
|
import { RootStackParamList, ROUTE } from '../types';
|
||||||
import { Tag, Meme } from '../database';
|
import { Tag, Meme } from '../database';
|
||||||
import { validateMemeTitle } from '../utilities';
|
import { deleteMeme, favoriteMeme, validateMemeTitle } from '../utilities';
|
||||||
import { MemeEditor } from '../components';
|
import { MemeEditor } from '../components';
|
||||||
|
|
||||||
const EditMeme = ({
|
const EditMeme = ({
|
||||||
@@ -34,9 +33,7 @@ const EditMeme = ({
|
|||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = useCallback(() => {
|
||||||
setIsSaving(true);
|
|
||||||
|
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
meme.tags.forEach(tag => {
|
meme.tags.forEach(tag => {
|
||||||
if (!memeTags.has(tag.id.toHexString())) {
|
if (!memeTags.has(tag.id.toHexString())) {
|
||||||
@@ -60,32 +57,7 @@ const EditMeme = ({
|
|||||||
meme.tagsLength = memeTags.size;
|
meme.tagsLength = memeTags.size;
|
||||||
meme.dateModified = new Date();
|
meme.dateModified = new Date();
|
||||||
});
|
});
|
||||||
|
}, [meme, memeTags, memeTitle.parsed, realm]);
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -94,9 +66,17 @@ const EditMeme = ({
|
|||||||
<Appbar.Content title={'Edit Meme'} />
|
<Appbar.Content title={'Edit Meme'} />
|
||||||
<Appbar.Action
|
<Appbar.Action
|
||||||
icon={meme.isFavorite ? 'heart' : 'heart-outline'}
|
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>
|
</Appbar.Header>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
@@ -110,7 +90,7 @@ const EditMeme = ({
|
|||||||
]}>
|
]}>
|
||||||
<View style={[styles.flex, styles.justifyStart]}>
|
<View style={[styles.flex, styles.justifyStart]}>
|
||||||
<MemeEditor
|
<MemeEditor
|
||||||
imageUri={meme.uri}
|
memeUri={meme.uri}
|
||||||
memeTitle={memeTitle}
|
memeTitle={memeTitle}
|
||||||
setMemeTitle={setMemeTitle}
|
setMemeTitle={setMemeTitle}
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
@@ -121,7 +101,12 @@ const EditMeme = ({
|
|||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
icon="floppy"
|
icon="floppy"
|
||||||
onPress={handleSave}
|
onPress={() => {
|
||||||
|
setIsSaving(true);
|
||||||
|
handleSave();
|
||||||
|
setIsSaving(false);
|
||||||
|
goBack();
|
||||||
|
}}
|
||||||
disabled={!memeTitle.valid || isSaving}
|
disabled={!memeTitle.valid || isSaving}
|
||||||
loading={isSaving}>
|
loading={isSaving}>
|
||||||
Save
|
Save
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { ScrollView, View } from 'react-native';
|
import { ScrollView, View } from 'react-native';
|
||||||
import { Appbar, Button, useTheme } from 'react-native-paper';
|
import { Appbar, Button, useTheme } from 'react-native-paper';
|
||||||
import { useNavigation } from '@react-navigation/native';
|
import { useNavigation } from '@react-navigation/native';
|
||||||
@@ -10,7 +10,7 @@ import styles from '../styles';
|
|||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
import { ROUTE, RootStackParamList } from '../types';
|
import { ROUTE, RootStackParamList } from '../types';
|
||||||
import { Tag } from '../database';
|
import { Tag } from '../database';
|
||||||
import { validateColor, validateTagName } from '../utilities';
|
import { deleteTag, validateColor, validateTagName } from '../utilities';
|
||||||
|
|
||||||
const EditTag = ({
|
const EditTag = ({
|
||||||
route,
|
route,
|
||||||
@@ -29,36 +29,26 @@ const EditTag = ({
|
|||||||
const [tagName, setTagName] = useState(validateTagName(tag.name));
|
const [tagName, setTagName] = useState(validateTagName(tag.name));
|
||||||
const [tagColor, setTagColor] = useState(validateColor(tag.color));
|
const [tagColor, setTagColor] = useState(validateColor(tag.color));
|
||||||
|
|
||||||
const handleSave = () => {
|
const handleSave = useCallback(() => {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
tag.name = tagName.parsed;
|
tag.name = tagName.parsed;
|
||||||
tag.color = tagColor.parsed;
|
tag.color = tagColor.parsed;
|
||||||
tag.dateModified = new Date();
|
tag.dateModified = new Date();
|
||||||
});
|
});
|
||||||
|
}, [realm, tag, tagColor.parsed, tagName.parsed]);
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Appbar.Header>
|
<Appbar.Header>
|
||||||
<Appbar.BackAction onPress={() => goBack()} />
|
<Appbar.BackAction onPress={() => goBack()} />
|
||||||
<Appbar.Content title={'Edit Tag'} />
|
<Appbar.Content title={'Edit Tag'} />
|
||||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
<Appbar.Action
|
||||||
|
icon="delete"
|
||||||
|
onPress={() => {
|
||||||
|
deleteTag(realm, tag);
|
||||||
|
goBack();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Appbar.Header>
|
</Appbar.Header>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
contentContainerStyle={[
|
contentContainerStyle={[
|
||||||
@@ -83,7 +73,10 @@ const EditTag = ({
|
|||||||
<Button
|
<Button
|
||||||
mode="contained"
|
mode="contained"
|
||||||
icon="floppy"
|
icon="floppy"
|
||||||
onPress={handleSave}
|
onPress={() => {
|
||||||
|
handleSave();
|
||||||
|
goBack();
|
||||||
|
}}
|
||||||
disabled={!tagName.valid || !tagColor.valid}>
|
disabled={!tagName.valid || !tagColor.valid}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -3,6 +3,7 @@ export { default as AddTag } from './addTag';
|
|||||||
export { default as EditMeme } from './editMeme';
|
export { default as EditMeme } from './editMeme';
|
||||||
export { default as EditTag } from './editTag';
|
export { default as EditTag } from './editTag';
|
||||||
export { default as Memes } from './memes';
|
export { default as Memes } from './memes';
|
||||||
|
export { default as MemeView } from './memeView';
|
||||||
export { default as Settings } from './settings';
|
export { default as Settings } from './settings';
|
||||||
export { default as Tags } from './tags';
|
export { default as Tags } from './tags';
|
||||||
export { default as Welcome } from './welcome';
|
export { default as Welcome } from './welcome';
|
||||||
|
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 {
|
import {
|
||||||
BackHandler,
|
BackHandler,
|
||||||
NativeScrollEvent,
|
NativeScrollEvent,
|
||||||
@@ -9,71 +9,22 @@ import { useQuery } from '@realm/react';
|
|||||||
import { useTheme } from 'react-native-paper';
|
import { useTheme } from 'react-native-paper';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { FlashList } from '@shopify/flash-list';
|
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 styles from '../styles';
|
||||||
import { SORT_DIRECTION, VIEW, memesSortQuery } from '../types';
|
import { ROUTE, SORT_DIRECTION, memesSortQuery } from '../types';
|
||||||
import { RootState, setNavVisible } from '../state';
|
import { RootState, setNavVisible } from '../state';
|
||||||
import { Meme } from '../database';
|
import { Meme } from '../database';
|
||||||
import {
|
import { HideableHeader, MemesHeader, MemesList } from '../components';
|
||||||
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 <></>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const Memes = () => {
|
const Memes = () => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
const { navigate } =
|
||||||
|
useNavigation<NativeStackNavigationProp<ParamListBase>>();
|
||||||
const sort = useSelector((state: RootState) => state.memes.sort);
|
const sort = useSelector((state: RootState) => state.memes.sort);
|
||||||
const sortDirection = useSelector(
|
const sortDirection = useSelector(
|
||||||
(state: RootState) => state.memes.sortDirection,
|
(state: RootState) => state.memes.sortDirection,
|
||||||
@@ -154,11 +105,9 @@ const Memes = () => {
|
|||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
const handleBackPress = () => {
|
const handleBackPress = () => {
|
||||||
if (scrollOffset > 0) {
|
if (scrollOffset <= 0) return false;
|
||||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
BackHandler.addEventListener('hardwareBackPress', handleBackPress);
|
||||||
@@ -190,11 +139,17 @@ const Memes = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</HideableHeader>
|
</HideableHeader>
|
||||||
<MemesView
|
<MemesList
|
||||||
memes={memes}
|
memes={memes}
|
||||||
flashListRef={flashListRef}
|
flashListRef={flashListRef}
|
||||||
flashListPadding={flashListPadding}
|
flashListPadding={flashListPadding}
|
||||||
handleScroll={handleScroll}
|
handleScroll={handleScroll}
|
||||||
|
focusMeme={(index: number) => {
|
||||||
|
navigate(ROUTE.MEME_VIEW, {
|
||||||
|
ids: memes.map(meme => meme.id.toHexString()),
|
||||||
|
index,
|
||||||
|
});
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@@ -19,12 +19,12 @@ import { SORT_DIRECTION, tagSortQuery } from '../types';
|
|||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
|
|
||||||
const tagsStyles = StyleSheet.create({
|
const tagsStyles = StyleSheet.create({
|
||||||
helperText: {
|
|
||||||
marginVertical: 10,
|
|
||||||
},
|
|
||||||
flashList: {
|
flashList: {
|
||||||
paddingBottom: 100,
|
paddingBottom: 100,
|
||||||
},
|
},
|
||||||
|
helperText: {
|
||||||
|
marginVertical: 15,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const Tags = () => {
|
const Tags = () => {
|
||||||
|
@@ -57,6 +57,10 @@ const styles = StyleSheet.create({
|
|||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
},
|
},
|
||||||
|
flexRowSpaceEvenly: {
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-evenly',
|
||||||
|
},
|
||||||
flexColumn: {
|
flexColumn: {
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
},
|
},
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
import { DefaultTheme as DefaultNavigationTheme } from '@react-navigation/native';
|
import {
|
||||||
|
DarkTheme,
|
||||||
|
DefaultTheme as LightTheme,
|
||||||
|
} from '@react-navigation/native';
|
||||||
import {
|
import {
|
||||||
MD3LightTheme,
|
MD3LightTheme,
|
||||||
MD3DarkTheme,
|
MD3DarkTheme,
|
||||||
@@ -98,12 +101,12 @@ const darkTheme = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { LightTheme: lightNavigationTheme } = adaptNavigationTheme({
|
const { LightTheme: lightNavigationTheme } = adaptNavigationTheme({
|
||||||
reactNavigationLight: DefaultNavigationTheme,
|
reactNavigationLight: LightTheme,
|
||||||
materialLight: lightTheme,
|
materialLight: lightTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { DarkTheme: darkNavigationTheme } = adaptNavigationTheme({
|
const { DarkTheme: darkNavigationTheme } = adaptNavigationTheme({
|
||||||
reactNavigationDark: DefaultNavigationTheme,
|
reactNavigationDark: DarkTheme,
|
||||||
materialDark: darkTheme,
|
materialDark: darkTheme,
|
||||||
});
|
});
|
||||||
|
|
@@ -5,13 +5,19 @@ enum ROUTE {
|
|||||||
MEMES = 'Memes',
|
MEMES = 'Memes',
|
||||||
TAGS = 'Tags',
|
TAGS = 'Tags',
|
||||||
SETTINGS = 'Settings',
|
SETTINGS = 'Settings',
|
||||||
|
MEME_VIEW = 'Meme View',
|
||||||
ADD_MEME = 'Add Meme',
|
ADD_MEME = 'Add Meme',
|
||||||
EDIT_MEME = 'Edit Meme',
|
EDIT_MEME = 'Edit Meme',
|
||||||
ADD_TAG = 'Add Tag',
|
ADD_TAG = 'Add Tag',
|
||||||
EDIT_TAG = 'Edit Tag',
|
EDIT_TAG = 'Edit Tag',
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AddMemeRouteParamsFromFiles {
|
interface MemeViewRouteParams {
|
||||||
|
ids: string[];
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AddMemeRouteParams {
|
||||||
file: DocumentPickerResponse;
|
file: DocumentPickerResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,11 +32,13 @@ interface EditTagRouteParams {
|
|||||||
interface RootStackParamList {
|
interface RootStackParamList {
|
||||||
[key: string]:
|
[key: string]:
|
||||||
| undefined
|
| undefined
|
||||||
| AddMemeRouteParamsFromFiles
|
| MemeViewRouteParams
|
||||||
|
| AddMemeRouteParams
|
||||||
| EditMemeRouteParams
|
| EditMemeRouteParams
|
||||||
| EditTagRouteParams;
|
| EditTagRouteParams;
|
||||||
[ROUTE.MAIN]: undefined;
|
[ROUTE.MAIN]: undefined;
|
||||||
[ROUTE.ADD_MEME]: AddMemeRouteParamsFromFiles;
|
[ROUTE.MEME_VIEW]: MemeViewRouteParams;
|
||||||
|
[ROUTE.ADD_MEME]: AddMemeRouteParams;
|
||||||
[ROUTE.EDIT_MEME]: EditMemeRouteParams;
|
[ROUTE.EDIT_MEME]: EditMemeRouteParams;
|
||||||
[ROUTE.ADD_TAG]: undefined;
|
[ROUTE.ADD_TAG]: undefined;
|
||||||
[ROUTE.EDIT_TAG]: EditTagRouteParams;
|
[ROUTE.EDIT_TAG]: EditTagRouteParams;
|
||||||
|
@@ -32,6 +32,15 @@ const rgbToHex = (rgb: string) => {
|
|||||||
return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
|
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 generateRandomColor = () => {
|
||||||
const r = Math.floor(Math.random() * 256)
|
const r = Math.floor(Math.random() * 256)
|
||||||
.toString(16)
|
.toString(16)
|
||||||
@@ -50,5 +59,6 @@ export {
|
|||||||
isHexColor,
|
isHexColor,
|
||||||
isRgbColor,
|
isRgbColor,
|
||||||
rgbToHex,
|
rgbToHex,
|
||||||
|
rgbToRgba,
|
||||||
generateRandomColor,
|
generateRandomColor,
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,7 @@ export {
|
|||||||
isHexColor,
|
isHexColor,
|
||||||
isRgbColor,
|
isRgbColor,
|
||||||
rgbToHex,
|
rgbToHex,
|
||||||
|
rgbToRgba,
|
||||||
generateRandomColor,
|
generateRandomColor,
|
||||||
} from './color';
|
} from './color';
|
||||||
export { packageName, appName, fileProvider, noOp } from './constants';
|
export { packageName, appName, fileProvider, noOp } from './constants';
|
||||||
@@ -14,8 +15,16 @@ export {
|
|||||||
allowedMimeTypes,
|
allowedMimeTypes,
|
||||||
getMemeType,
|
getMemeType,
|
||||||
} from './filesystem';
|
} from './filesystem';
|
||||||
export { isPermissionForPath, clearPermissions } from './permissions';
|
|
||||||
export { getSortIcon, getViewIcon } from './icon';
|
export { getSortIcon, getViewIcon } from './icon';
|
||||||
|
export {
|
||||||
|
favoriteMeme,
|
||||||
|
shareMeme,
|
||||||
|
copyMeme,
|
||||||
|
editMeme,
|
||||||
|
deleteMeme,
|
||||||
|
} from './meme';
|
||||||
|
export { isPermissionForPath, clearPermissions } from './permissions';
|
||||||
|
export { deleteTag } from './tag';
|
||||||
export {
|
export {
|
||||||
type StringValidationResult,
|
type StringValidationResult,
|
||||||
validateMemeTitle,
|
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