Add memes views & searching
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
This commit is contained in:
104
package-lock.json
generated
104
package-lock.json
generated
@@ -23,6 +23,7 @@
|
|||||||
"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",
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
"react-native-screens": "^3.22.1",
|
"react-native-screens": "^3.22.1",
|
||||||
"react-native-share": "^9.2.3",
|
"react-native-share": "^9.2.3",
|
||||||
"react-native-vector-icons": "^9.2.0",
|
"react-native-vector-icons": "^9.2.0",
|
||||||
"react-native-video": "^5.2.1",
|
"react-native-video": "^6.0.0-alpha.6",
|
||||||
"react-redux": "^8.1.1",
|
"react-redux": "^8.1.1",
|
||||||
"realm": "^11.10.1",
|
"realm": "^11.10.1",
|
||||||
"redux-persist": "^6.0.0"
|
"redux-persist": "^6.0.0"
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
"@types/metro-config": "^0.76.3",
|
"@types/metro-config": "^0.76.3",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
"@types/react-native-vector-icons": "^6.4.13",
|
"@types/react-native-vector-icons": "^6.4.13",
|
||||||
|
"@types/react-native-video": "^5.0.15",
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
@@ -4602,6 +4604,16 @@
|
|||||||
"@types/react-native": "^0.70"
|
"@types/react-native": "^0.70"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react-native-video": {
|
||||||
|
"version": "5.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-native-video/-/react-native-video-5.0.15.tgz",
|
||||||
|
"integrity": "sha512-li3yBYQ+D5GqZl0Y+M/vCTPfZwVyUU67CtSjEg+/ERkgEpvHDH+gQaoc9O00ttXr8kvqEzpiC6Ca9juIfeIlMA==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react-test-renderer": {
|
"node_modules/@types/react-test-renderer": {
|
||||||
"version": "18.0.0",
|
"version": "18.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz",
|
||||||
@@ -6541,11 +6553,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
|
||||||
"integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
|
"integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
|
||||||
},
|
},
|
||||||
"node_modules/eme-encryption-scheme-polyfill": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-njD17wcUrbqCj0ArpLu5zWXtaiupHb/2fIUQGdInf83GlI+Q6mmqaPGLdrke4savKAu15J/z1Tg/ivDgl14g0g=="
|
|
||||||
},
|
|
||||||
"node_modules/emittery": {
|
"node_modules/emittery": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
||||||
@@ -13407,6 +13414,27 @@
|
|||||||
"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",
|
||||||
@@ -13528,14 +13556,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-video": {
|
"node_modules/react-native-video": {
|
||||||
"version": "5.2.1",
|
"version": "6.0.0-alpha.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.0.0-alpha.6.tgz",
|
||||||
"integrity": "sha512-aJlr9MeTuQ0LpZ4n+EC9RvhoKeiPbLtI2Rxy8u7zo/wzGevbRpWHSBj9xZ5YDBXnAVXzuqyNIkGhdw7bfdIBZw==",
|
"integrity": "sha512-MCqHfPGuqVokvJOkvidhD5/eGYkWZrDEcSDtlkwVo36V2157L6lZyt3mqb5tPR+e5jSz+c/ht2JpEhP1bCm/Dw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deprecated-react-native-prop-types": "^2.2.0",
|
"deprecated-react-native-prop-types": "^2.2.0",
|
||||||
"keymirror": "^0.1.1",
|
"keymirror": "^0.1.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2"
|
||||||
"shaka-player": "^2.5.9"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-native-video/node_modules/deprecated-react-native-prop-types": {
|
"node_modules/react-native-video/node_modules/deprecated-react-native-prop-types": {
|
||||||
@@ -14500,15 +14527,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||||
},
|
},
|
||||||
"node_modules/shaka-player": {
|
|
||||||
"version": "2.5.23",
|
|
||||||
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.23.tgz",
|
|
||||||
"integrity": "sha512-3MC9k0OXJGw8AZ4n/ZNCZS2yDxx+3as5KgH6Tx4Q5TRboTBBCu6dYPI5vp1DxKeyU12MBN1Zcbs7AKzXv2EnCg==",
|
|
||||||
"deprecated": "Shaka Player < v3.2 is no longer supported.",
|
|
||||||
"dependencies": {
|
|
||||||
"eme-encryption-scheme-polyfill": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/shallow-clone": {
|
"node_modules/shallow-clone": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
||||||
@@ -19177,6 +19195,16 @@
|
|||||||
"@types/react-native": "^0.70"
|
"@types/react-native": "^0.70"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/react-native-video": {
|
||||||
|
"version": "5.0.15",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-native-video/-/react-native-video-5.0.15.tgz",
|
||||||
|
"integrity": "sha512-li3yBYQ+D5GqZl0Y+M/vCTPfZwVyUU67CtSjEg+/ERkgEpvHDH+gQaoc9O00ttXr8kvqEzpiC6Ca9juIfeIlMA==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/react-test-renderer": {
|
"@types/react-test-renderer": {
|
||||||
"version": "18.0.0",
|
"version": "18.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-18.0.0.tgz",
|
||||||
@@ -20617,11 +20645,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.455.tgz",
|
||||||
"integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
|
"integrity": "sha512-8tgdX0Odl24LtmLwxotpJCVjIndN559AvaOtd67u+2mo+IDsgsTF580NB+uuDCqsHw8yFg53l5+imFV9Fw3cbA=="
|
||||||
},
|
},
|
||||||
"eme-encryption-scheme-polyfill": {
|
|
||||||
"version": "2.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/eme-encryption-scheme-polyfill/-/eme-encryption-scheme-polyfill-2.1.1.tgz",
|
|
||||||
"integrity": "sha512-njD17wcUrbqCj0ArpLu5zWXtaiupHb/2fIUQGdInf83GlI+Q6mmqaPGLdrke4savKAu15J/z1Tg/ivDgl14g0g=="
|
|
||||||
},
|
|
||||||
"emittery": {
|
"emittery": {
|
||||||
"version": "0.13.1",
|
"version": "0.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz",
|
||||||
@@ -25788,6 +25811,20 @@
|
|||||||
"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",
|
||||||
@@ -25879,14 +25916,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-native-video": {
|
"react-native-video": {
|
||||||
"version": "5.2.1",
|
"version": "6.0.0-alpha.6",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-native-video/-/react-native-video-6.0.0-alpha.6.tgz",
|
||||||
"integrity": "sha512-aJlr9MeTuQ0LpZ4n+EC9RvhoKeiPbLtI2Rxy8u7zo/wzGevbRpWHSBj9xZ5YDBXnAVXzuqyNIkGhdw7bfdIBZw==",
|
"integrity": "sha512-MCqHfPGuqVokvJOkvidhD5/eGYkWZrDEcSDtlkwVo36V2157L6lZyt3mqb5tPR+e5jSz+c/ht2JpEhP1bCm/Dw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"deprecated-react-native-prop-types": "^2.2.0",
|
"deprecated-react-native-prop-types": "^2.2.0",
|
||||||
"keymirror": "^0.1.1",
|
"keymirror": "^0.1.1",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2"
|
||||||
"shaka-player": "^2.5.9"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"deprecated-react-native-prop-types": {
|
"deprecated-react-native-prop-types": {
|
||||||
@@ -26511,14 +26547,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
|
||||||
},
|
},
|
||||||
"shaka-player": {
|
|
||||||
"version": "2.5.23",
|
|
||||||
"resolved": "https://registry.npmjs.org/shaka-player/-/shaka-player-2.5.23.tgz",
|
|
||||||
"integrity": "sha512-3MC9k0OXJGw8AZ4n/ZNCZS2yDxx+3as5KgH6Tx4Q5TRboTBBCu6dYPI5vp1DxKeyU12MBN1Zcbs7AKzXv2EnCg==",
|
|
||||||
"requires": {
|
|
||||||
"eme-encryption-scheme-polyfill": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"shallow-clone": {
|
"shallow-clone": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz",
|
||||||
|
@@ -28,6 +28,7 @@
|
|||||||
"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",
|
||||||
@@ -36,7 +37,7 @@
|
|||||||
"react-native-screens": "^3.22.1",
|
"react-native-screens": "^3.22.1",
|
||||||
"react-native-share": "^9.2.3",
|
"react-native-share": "^9.2.3",
|
||||||
"react-native-vector-icons": "^9.2.0",
|
"react-native-vector-icons": "^9.2.0",
|
||||||
"react-native-video": "^5.2.1",
|
"react-native-video": "^6.0.0-alpha.6",
|
||||||
"react-redux": "^8.1.1",
|
"react-redux": "^8.1.1",
|
||||||
"realm": "^11.10.1",
|
"realm": "^11.10.1",
|
||||||
"redux-persist": "^6.0.0"
|
"redux-persist": "^6.0.0"
|
||||||
@@ -53,6 +54,7 @@
|
|||||||
"@types/metro-config": "^0.76.3",
|
"@types/metro-config": "^0.76.3",
|
||||||
"@types/react": "^18.2.14",
|
"@types/react": "^18.2.14",
|
||||||
"@types/react-native-vector-icons": "^6.4.13",
|
"@types/react-native-vector-icons": "^6.4.13",
|
||||||
|
"@types/react-native-video": "^5.0.15",
|
||||||
"@types/react-test-renderer": "^18.0.0",
|
"@types/react-test-renderer": "^18.0.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
"@typescript-eslint/eslint-plugin": "^5.61.0",
|
||||||
"@typescript-eslint/parser": "^5.61.0",
|
"@typescript-eslint/parser": "^5.61.0",
|
||||||
|
@@ -1,4 +1,7 @@
|
|||||||
export {
|
export {
|
||||||
|
MemesGridView,
|
||||||
|
MemesListView,
|
||||||
|
MemesMasonryView,
|
||||||
MemeEditor,
|
MemeEditor,
|
||||||
MemesHeader,
|
MemesHeader,
|
||||||
MemeTagSearchModal,
|
MemeTagSearchModal,
|
||||||
|
45
src/components/memes/gridView/memesGridItem.tsx
Normal file
45
src/components/memes/gridView/memesGridItem.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
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 { dimensions } = useDimensions();
|
||||||
|
const gridColumns = useSelector(
|
||||||
|
(state: RootState) => state.settings.gridColumns,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [imageWidth, setImageWidth] = useState<number>();
|
||||||
|
const [imageHeight, setImageHeight] = useState<number>();
|
||||||
|
|
||||||
|
Image.getSize(meme.uri, () => {
|
||||||
|
const paddedWidth = (dimensions.width * 0.92 - 5) / gridColumns;
|
||||||
|
setImageWidth(paddedWidth);
|
||||||
|
setImageHeight(paddedWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{imageWidth && imageHeight && (
|
||||||
|
<View>
|
||||||
|
<TouchableHighlight
|
||||||
|
onPress={() =>
|
||||||
|
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
||||||
|
}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: meme.uri }}
|
||||||
|
style={[{ width: imageWidth, height: imageHeight }]}
|
||||||
|
/>
|
||||||
|
</TouchableHighlight>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemesGridItem;
|
74
src/components/memes/gridView/memesGridView.tsx
Normal file
74
src/components/memes/gridView/memesGridView.tsx
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
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,3 +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 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';
|
||||||
|
98
src/components/memes/listView/memesListItem.tsx
Normal file
98
src/components/memes/listView/memesListItem.tsx
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const memesListItemStyles = StyleSheet.create({
|
||||||
|
view: {
|
||||||
|
paddingVertical: 10,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
detailsView: {
|
||||||
|
marginLeft: 10,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
marginRight: 5,
|
||||||
|
marginBottom: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MemesListItem = ({ meme }: { meme: Meme }) => {
|
||||||
|
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
const { dimensions } = useDimensions();
|
||||||
|
|
||||||
|
const [imageWidth, setImageWidth] = useState<number>();
|
||||||
|
const [imageHeight, setImageHeight] = useState<number>();
|
||||||
|
|
||||||
|
Image.getSize(meme.uri, () => {
|
||||||
|
const paddedWidth = 75;
|
||||||
|
setImageWidth(paddedWidth);
|
||||||
|
setImageHeight(paddedWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{imageWidth && imageHeight && (
|
||||||
|
<TouchableRipple
|
||||||
|
onPress={() =>
|
||||||
|
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
||||||
|
}
|
||||||
|
style={[memesListItemStyles.view, styles.flexRow]}>
|
||||||
|
<>
|
||||||
|
<View style={{ width: imageWidth, height: imageHeight }}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: meme.uri }}
|
||||||
|
style={[
|
||||||
|
{ width: imageWidth, height: imageHeight },
|
||||||
|
memesListItemStyles.image,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
memesListItemStyles.detailsView,
|
||||||
|
styles.flexColumn,
|
||||||
|
{
|
||||||
|
width: dimensions.width * 0.92 - imageWidth - 10,
|
||||||
|
},
|
||||||
|
]}>
|
||||||
|
<Text variant="titleMedium" style={memesListItemStyles.text}>
|
||||||
|
{meme.title}
|
||||||
|
</Text>
|
||||||
|
<View style={styles.flexRow}>
|
||||||
|
<Text variant="labelSmall" style={memesListItemStyles.text}>
|
||||||
|
{meme.dateModified.toLocaleDateString()} • {meme.size / 1000}
|
||||||
|
KB
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<View style={[styles.flexRow, styles.flexWrap]}>
|
||||||
|
{meme.tags.map(tag => (
|
||||||
|
<Text
|
||||||
|
variant="labelMedium"
|
||||||
|
key={tag.id.toHexString()}
|
||||||
|
style={[
|
||||||
|
{
|
||||||
|
color: tag.color,
|
||||||
|
},
|
||||||
|
memesListItemStyles.text,
|
||||||
|
]}
|
||||||
|
numberOfLines={1}>
|
||||||
|
#{tag.name}
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</>
|
||||||
|
</TouchableRipple>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemesListItem;
|
68
src/components/memes/listView/memesListView.tsx
Normal file
68
src/components/memes/listView/memesListView.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
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;
|
57
src/components/memes/masonryView/memesMasonryItem.tsx
Normal file
57
src/components/memes/masonryView/memesMasonryItem.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
const memeMasonryItemStyles = StyleSheet.create({
|
||||||
|
view: {
|
||||||
|
margin: 2.5,
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
borderRadius: 5,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const MemesMasonryItem = ({ meme }: { meme: Meme }) => {
|
||||||
|
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
||||||
|
const { dimensions } = useDimensions();
|
||||||
|
const masonryColumns = useSelector(
|
||||||
|
(state: RootState) => state.settings.masonryColumns,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [imageWidth, setImageWidth] = useState<number>();
|
||||||
|
const [imageHeight, setImageHeight] = useState<number>();
|
||||||
|
|
||||||
|
Image.getSize(meme.uri, (width, height) => {
|
||||||
|
const paddedWidth = (dimensions.width * 0.92) / masonryColumns - 5;
|
||||||
|
setImageWidth(paddedWidth);
|
||||||
|
setImageHeight((paddedWidth / width) * height);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{imageWidth && imageHeight && (
|
||||||
|
<TouchableHighlight
|
||||||
|
onPress={() =>
|
||||||
|
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
||||||
|
}
|
||||||
|
style={memeMasonryItemStyles.view}>
|
||||||
|
<Image
|
||||||
|
source={{ uri: meme.uri }}
|
||||||
|
style={[
|
||||||
|
memeMasonryItemStyles.image,
|
||||||
|
{ width: imageWidth, height: imageHeight },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</TouchableHighlight>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MemesMasonryItem;
|
75
src/components/memes/masonryView/memesMasonryView.tsx
Normal file
75
src/components/memes/masonryView/memesMasonryView.tsx
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
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,51 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
|
||||||
import { Meme } from '../../database';
|
|
||||||
import { ROUTE, RootStackParamList } from '../../types';
|
|
||||||
import { Card } from 'react-native-paper';
|
|
||||||
import { Image, StyleSheet } from 'react-native';
|
|
||||||
import { useDimensions } from '../../contexts';
|
|
||||||
|
|
||||||
const memeCardStyles = StyleSheet.create({
|
|
||||||
card: {
|
|
||||||
margin: 5,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const MemeCard = ({ meme }: { meme: Meme }) => {
|
|
||||||
const { navigate } = useNavigation<NavigationProp<RootStackParamList>>();
|
|
||||||
const { dimensions } = useDimensions();
|
|
||||||
|
|
||||||
const [imageWidth, setImageWidth] = useState<number>();
|
|
||||||
const [imageHeight, setImageHeight] = useState<number>();
|
|
||||||
|
|
||||||
Image.getSize(meme.uri, (width, height) => {
|
|
||||||
const paddedWidth = (dimensions.width * 0.92) / 2 - 10;
|
|
||||||
setImageWidth(paddedWidth);
|
|
||||||
setImageHeight((paddedWidth / width) * height);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{imageWidth && imageHeight && (
|
|
||||||
<Card
|
|
||||||
onPress={() =>
|
|
||||||
navigate(ROUTE.EDIT_MEME, { id: meme.id.toHexString() })
|
|
||||||
}
|
|
||||||
style={memeCardStyles.card}>
|
|
||||||
<Card.Cover
|
|
||||||
source={{ uri: meme.uri }}
|
|
||||||
style={{ width: imageWidth, height: imageHeight }}
|
|
||||||
/>
|
|
||||||
<Card.Title
|
|
||||||
title={meme.title}
|
|
||||||
titleVariant="titleSmall"
|
|
||||||
titleNumberOfLines={3}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MemeCard;
|
|
@@ -5,11 +5,7 @@ import { useDimensions } from '../../contexts';
|
|||||||
import LoadingView from '../loadingView';
|
import LoadingView from '../loadingView';
|
||||||
import { MemeTagSelector } from '.';
|
import { MemeTagSelector } from '.';
|
||||||
import { Tag } from '../../database';
|
import { Tag } from '../../database';
|
||||||
import {
|
import { StringValidationResult, validateMemeTitle } from '../../utilities';
|
||||||
StringValidationResult,
|
|
||||||
validateMemeDescription,
|
|
||||||
validateMemeTitle,
|
|
||||||
} from '../../utilities';
|
|
||||||
|
|
||||||
const memeEditorStyles = {
|
const memeEditorStyles = {
|
||||||
image: {
|
image: {
|
||||||
@@ -28,16 +24,12 @@ const MemeEditor = ({
|
|||||||
imageUri,
|
imageUri,
|
||||||
memeTitle,
|
memeTitle,
|
||||||
setMemeTitle,
|
setMemeTitle,
|
||||||
memeDescription,
|
|
||||||
setMemeDescription,
|
|
||||||
memeTags,
|
memeTags,
|
||||||
setMemeTags,
|
setMemeTags,
|
||||||
}: {
|
}: {
|
||||||
imageUri: string;
|
imageUri: string;
|
||||||
memeTitle: StringValidationResult;
|
memeTitle: StringValidationResult;
|
||||||
setMemeTitle: (name: StringValidationResult) => void;
|
setMemeTitle: (name: StringValidationResult) => void;
|
||||||
memeDescription: StringValidationResult;
|
|
||||||
setMemeDescription: (description: StringValidationResult) => void;
|
|
||||||
memeTags: Map<string, Tag>;
|
memeTags: Map<string, Tag>;
|
||||||
setMemeTags: (tags: Map<string, Tag>) => void;
|
setMemeTags: (tags: Map<string, Tag>) => void;
|
||||||
}) => {
|
}) => {
|
||||||
@@ -48,8 +40,12 @@ const MemeEditor = ({
|
|||||||
|
|
||||||
Image.getSize(imageUri, (width, height) => {
|
Image.getSize(imageUri, (width, height) => {
|
||||||
const paddedWidth = dimensions.width * 0.92;
|
const paddedWidth = dimensions.width * 0.92;
|
||||||
|
const paddedHeight = Math.max(
|
||||||
|
Math.min((paddedWidth / width) * height, 500),
|
||||||
|
100,
|
||||||
|
);
|
||||||
setImageWidth(paddedWidth);
|
setImageWidth(paddedWidth);
|
||||||
setImageHeight((paddedWidth / width) * height);
|
setImageHeight(paddedHeight);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!imageWidth || !imageHeight) return <LoadingView />;
|
if (!imageWidth || !imageHeight) return <LoadingView />;
|
||||||
@@ -76,24 +72,13 @@ const MemeEditor = ({
|
|||||||
},
|
},
|
||||||
memeEditorStyles.image,
|
memeEditorStyles.image,
|
||||||
]}
|
]}
|
||||||
|
resizeMode="contain"
|
||||||
/>
|
/>
|
||||||
<MemeTagSelector
|
<MemeTagSelector
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
setMemeTags={setMemeTags}
|
setMemeTags={setMemeTags}
|
||||||
style={memeEditorStyles.memeTagSelector}
|
style={memeEditorStyles.memeTagSelector}
|
||||||
/>
|
/>
|
||||||
<TextInput
|
|
||||||
mode="outlined"
|
|
||||||
label="Description"
|
|
||||||
multiline
|
|
||||||
numberOfLines={6}
|
|
||||||
value={memeDescription.raw}
|
|
||||||
style={memeEditorStyles.description}
|
|
||||||
onChangeText={description =>
|
|
||||||
setMemeDescription(validateMemeDescription(description))
|
|
||||||
}
|
|
||||||
error={!memeDescription.valid}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@@ -24,9 +24,8 @@ class Meme extends Object<Meme> {
|
|||||||
uri!: string;
|
uri!: string;
|
||||||
size!: number;
|
size!: number;
|
||||||
title!: string;
|
title!: string;
|
||||||
description!: string;
|
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
tags!: Tag[] | Realm.Set<Tag>;
|
tags!: Realm.List<Tag>;
|
||||||
tagsLength!: number;
|
tagsLength!: number;
|
||||||
dateCreated!: Date;
|
dateCreated!: Date;
|
||||||
dateModified!: Date;
|
dateModified!: Date;
|
||||||
@@ -42,9 +41,8 @@ class Meme extends Object<Meme> {
|
|||||||
uri: 'string',
|
uri: 'string',
|
||||||
size: 'int',
|
size: 'int',
|
||||||
title: 'string',
|
title: 'string',
|
||||||
description: { type: 'string', default: '' },
|
|
||||||
isFavorite: { type: 'bool', indexed: true, default: false },
|
isFavorite: { type: 'bool', indexed: true, default: false },
|
||||||
tags: { type: 'set', objectType: 'Tag', default: [] },
|
tags: { type: 'list', objectType: 'Tag', default: [] },
|
||||||
tagsLength: { type: 'int', default: 0 },
|
tagsLength: { type: 'int', default: 0 },
|
||||||
dateCreated: { type: 'date', default: () => new Date() },
|
dateCreated: { type: 'date', default: () => new Date() },
|
||||||
dateModified: { type: 'date', default: () => new Date() },
|
dateModified: { type: 'date', default: () => new Date() },
|
||||||
|
@@ -7,7 +7,7 @@ class Tag extends Object<Tag> {
|
|||||||
id!: BSON.UUID;
|
id!: BSON.UUID;
|
||||||
name!: string;
|
name!: string;
|
||||||
color!: string;
|
color!: string;
|
||||||
memes!: Meme[] | Realm.Set<Meme>;
|
memes!: Realm.List<Meme>;
|
||||||
memesLength!: number;
|
memesLength!: number;
|
||||||
dateCreated!: Date;
|
dateCreated!: Date;
|
||||||
dateModified!: Date;
|
dateModified!: Date;
|
||||||
@@ -21,7 +21,7 @@ class Tag extends Object<Tag> {
|
|||||||
id: { type: 'uuid', default: () => new BSON.UUID() },
|
id: { type: 'uuid', default: () => new BSON.UUID() },
|
||||||
name: { type: 'string', indexed: true },
|
name: { type: 'string', indexed: true },
|
||||||
color: { type: 'string', default: () => generateRandomColor() },
|
color: { type: 'string', default: () => generateRandomColor() },
|
||||||
memes: { type: 'set', objectType: 'Meme', default: [] },
|
memes: { type: 'list', objectType: 'Meme', default: [] },
|
||||||
memesLength: { type: 'int', default: 0 },
|
memesLength: { type: 'int', default: 0 },
|
||||||
dateCreated: { type: 'date', default: () => new Date() },
|
dateCreated: { type: 'date', default: () => new Date() },
|
||||||
dateModified: { type: 'date', default: () => new Date() },
|
dateModified: { type: 'date', default: () => new Date() },
|
||||||
|
@@ -13,11 +13,7 @@ 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 {
|
import { getMemeType, validateMemeTitle } from '../utilities';
|
||||||
getMemeType,
|
|
||||||
validateMemeDescription,
|
|
||||||
validateMemeTitle,
|
|
||||||
} from '../utilities';
|
|
||||||
import { MemeEditor } from '../components';
|
import { MemeEditor } from '../components';
|
||||||
|
|
||||||
const AddMeme = ({
|
const AddMeme = ({
|
||||||
@@ -35,9 +31,6 @@ const AddMeme = ({
|
|||||||
const { file } = route.params;
|
const { file } = route.params;
|
||||||
|
|
||||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
const [memeTitle, setMemeTitle] = useState(validateMemeTitle('New Meme'));
|
||||||
const [memeDescription, setMemeDescription] = useState(
|
|
||||||
validateMemeDescription(''),
|
|
||||||
);
|
|
||||||
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>());
|
||||||
|
|
||||||
@@ -69,7 +62,6 @@ const AddMeme = ({
|
|||||||
uri,
|
uri,
|
||||||
size,
|
size,
|
||||||
title: memeTitle.parsed,
|
title: memeTitle.parsed,
|
||||||
description: memeDescription.parsed,
|
|
||||||
isFavorite: memeIsFavorite,
|
isFavorite: memeIsFavorite,
|
||||||
tags: [...memeTags.values()],
|
tags: [...memeTags.values()],
|
||||||
tagsLength: memeTags.size,
|
tagsLength: memeTags.size,
|
||||||
@@ -77,9 +69,8 @@ const AddMeme = ({
|
|||||||
|
|
||||||
memeTags.forEach(tag => {
|
memeTags.forEach(tag => {
|
||||||
tag.dateModified = new Date();
|
tag.dateModified = new Date();
|
||||||
const memes = tag.memes as Realm.Set<Meme>;
|
tag.memes.push(meme);
|
||||||
memes.add(meme);
|
tag.memesLength = tag.memes.length;
|
||||||
tag.memesLength = memes.size;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -111,8 +102,6 @@ const AddMeme = ({
|
|||||||
imageUri={file.uri}
|
imageUri={file.uri}
|
||||||
memeTitle={memeTitle}
|
memeTitle={memeTitle}
|
||||||
setMemeTitle={setMemeTitle}
|
setMemeTitle={setMemeTitle}
|
||||||
memeDescription={memeDescription}
|
|
||||||
setMemeDescription={setMemeDescription}
|
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
setMemeTags={setMemeTags}
|
setMemeTags={setMemeTags}
|
||||||
/>
|
/>
|
||||||
@@ -122,7 +111,7 @@ const AddMeme = ({
|
|||||||
mode="contained"
|
mode="contained"
|
||||||
icon="floppy"
|
icon="floppy"
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
disabled={!memeTitle.valid || !memeDescription.valid || isSaving}
|
disabled={!memeTitle.valid || isSaving}
|
||||||
loading={isSaving}>
|
loading={isSaving}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -10,7 +10,7 @@ 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, validateMemeDescription } from '../utilities';
|
import { validateMemeTitle } from '../utilities';
|
||||||
import { MemeEditor } from '../components';
|
import { MemeEditor } from '../components';
|
||||||
|
|
||||||
const EditMeme = ({
|
const EditMeme = ({
|
||||||
@@ -28,14 +28,8 @@ const EditMeme = ({
|
|||||||
)!;
|
)!;
|
||||||
|
|
||||||
const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title));
|
const [memeTitle, setMemeTitle] = useState(validateMemeTitle(meme.title));
|
||||||
const [memeDescription, setMemeDescription] = useState(
|
|
||||||
validateMemeDescription(meme.description),
|
|
||||||
);
|
|
||||||
const [memeIsFavorite, setMemeIsFavorite] = useState(meme.isFavorite);
|
|
||||||
const [memeTags, setMemeTags] = useState(
|
const [memeTags, setMemeTags] = useState(
|
||||||
new Map<string, Tag>(
|
new Map<string, Tag>(meme.tags.map(tag => [tag.id.toHexString(), tag])),
|
||||||
(meme.tags as Realm.Set<Tag>).map(tag => [tag.id.toHexString(), tag]),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
@@ -46,24 +40,22 @@ const EditMeme = ({
|
|||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
meme.tags.forEach(tag => {
|
meme.tags.forEach(tag => {
|
||||||
if (!memeTags.has(tag.id.toHexString())) {
|
if (!memeTags.has(tag.id.toHexString())) {
|
||||||
const memes = tag.memes as Realm.Set<Meme>;
|
tag.memes.slice(tag.memes.indexOf(meme), 1);
|
||||||
memes.delete(meme);
|
tag.memesLength -= 1;
|
||||||
tag.memesLength = memes.size;
|
|
||||||
tag.dateModified = new Date();
|
tag.dateModified = new Date();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
memeTags.forEach(tag => {
|
memeTags.forEach(tag => {
|
||||||
if (!(meme.tags as Realm.Set<Tag>).has(tag)) {
|
if (!meme.tags.some(memeTag => memeTag.id.equals(tag.id))) {
|
||||||
const memes = tag.memes as Realm.Set<Meme>;
|
tag.memes.push(meme);
|
||||||
memes.add(meme);
|
tag.memesLength = tag.memes.length;
|
||||||
tag.memesLength = memes.size;
|
|
||||||
tag.dateModified = new Date();
|
tag.dateModified = new Date();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
meme.title = memeTitle.parsed;
|
meme.title = memeTitle.parsed;
|
||||||
meme.description = memeDescription.parsed;
|
// @ts-expect-error - Realm is a fuck
|
||||||
meme.tags = [...memeTags.values()];
|
meme.tags = [...memeTags.values()];
|
||||||
meme.tagsLength = memeTags.size;
|
meme.tagsLength = memeTags.size;
|
||||||
meme.dateModified = new Date();
|
meme.dateModified = new Date();
|
||||||
@@ -74,9 +66,8 @@ const EditMeme = ({
|
|||||||
|
|
||||||
const handleFavorite = () => {
|
const handleFavorite = () => {
|
||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
meme.isFavorite = !memeIsFavorite;
|
meme.isFavorite = !meme.isFavorite;
|
||||||
});
|
});
|
||||||
setMemeIsFavorite(!memeIsFavorite);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDelete = async () => {
|
const handleDelete = async () => {
|
||||||
@@ -86,9 +77,8 @@ const EditMeme = ({
|
|||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
for (const tag of meme.tags) {
|
for (const tag of meme.tags) {
|
||||||
tag.dateModified = new Date();
|
tag.dateModified = new Date();
|
||||||
const memes = tag.memes as Realm.Set<Meme>;
|
tag.memes.slice(tag.memes.indexOf(meme), 1);
|
||||||
memes.delete(meme);
|
tag.memesLength -= 1;
|
||||||
tag.memesLength = memes.size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
realm.delete(meme);
|
realm.delete(meme);
|
||||||
@@ -103,7 +93,7 @@ const EditMeme = ({
|
|||||||
<Appbar.BackAction onPress={() => goBack()} />
|
<Appbar.BackAction onPress={() => goBack()} />
|
||||||
<Appbar.Content title={'Edit Meme'} />
|
<Appbar.Content title={'Edit Meme'} />
|
||||||
<Appbar.Action
|
<Appbar.Action
|
||||||
icon={memeIsFavorite ? 'heart' : 'heart-outline'}
|
icon={meme.isFavorite ? 'heart' : 'heart-outline'}
|
||||||
onPress={handleFavorite}
|
onPress={handleFavorite}
|
||||||
/>
|
/>
|
||||||
<Appbar.Action icon="delete" onPress={handleDelete} />
|
<Appbar.Action icon="delete" onPress={handleDelete} />
|
||||||
@@ -123,8 +113,6 @@ const EditMeme = ({
|
|||||||
imageUri={meme.uri}
|
imageUri={meme.uri}
|
||||||
memeTitle={memeTitle}
|
memeTitle={memeTitle}
|
||||||
setMemeTitle={setMemeTitle}
|
setMemeTitle={setMemeTitle}
|
||||||
memeDescription={memeDescription}
|
|
||||||
setMemeDescription={setMemeDescription}
|
|
||||||
memeTags={memeTags}
|
memeTags={memeTags}
|
||||||
setMemeTags={setMemeTags}
|
setMemeTags={setMemeTags}
|
||||||
/>
|
/>
|
||||||
@@ -134,7 +122,7 @@ const EditMeme = ({
|
|||||||
mode="contained"
|
mode="contained"
|
||||||
icon="floppy"
|
icon="floppy"
|
||||||
onPress={handleSave}
|
onPress={handleSave}
|
||||||
disabled={!memeTitle.valid || !memeDescription.valid || isSaving}
|
disabled={!memeTitle.valid || isSaving}
|
||||||
loading={isSaving}>
|
loading={isSaving}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
|
@@ -43,9 +43,8 @@ const EditTag = ({
|
|||||||
realm.write(() => {
|
realm.write(() => {
|
||||||
for (const meme of tag.memes) {
|
for (const meme of tag.memes) {
|
||||||
meme.dateModified = new Date();
|
meme.dateModified = new Date();
|
||||||
const tags = meme.tags as Realm.Set<Tag>;
|
meme.tags.slice(meme.tags.indexOf(tag), 1);
|
||||||
tags.delete(tag);
|
meme.tagsLength -= 1;
|
||||||
meme.tagsLength = tags.size;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
realm.delete(tag);
|
realm.delete(tag);
|
||||||
|
@@ -1,38 +1,79 @@
|
|||||||
import React, { useCallback, useRef, useState } from 'react';
|
import React, { RefObject, useCallback, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
BackHandler,
|
BackHandler,
|
||||||
NativeScrollEvent,
|
NativeScrollEvent,
|
||||||
NativeSyntheticEvent,
|
NativeSyntheticEvent,
|
||||||
StyleSheet,
|
|
||||||
View,
|
View,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import { useQuery } from '@realm/react';
|
import { useQuery } from '@realm/react';
|
||||||
import { useTheme, HelperText } from 'react-native-paper';
|
import { useTheme } from 'react-native-paper';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { FlashList, MasonryFlashList } from '@shopify/flash-list';
|
import { FlashList } from '@shopify/flash-list';
|
||||||
import { useFocusEffect } from '@react-navigation/native';
|
import { useFocusEffect } from '@react-navigation/native';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import { SORT_DIRECTION, memesSortQuery } from '../types';
|
import { SORT_DIRECTION, VIEW, memesSortQuery } from '../types';
|
||||||
import { RootState, setNavVisible } from '../state';
|
import { RootState, setNavVisible } from '../state';
|
||||||
import { Meme } from '../database';
|
import { Meme } from '../database';
|
||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import {
|
||||||
import { HideableHeader, MemesHeader } from '../components';
|
HideableHeader,
|
||||||
import MemeCard from '../components/memes/memeCard';
|
MemesHeader,
|
||||||
|
MemesMasonryView,
|
||||||
|
MemesGridView,
|
||||||
|
MemesListView,
|
||||||
|
} from '../components';
|
||||||
|
|
||||||
const memesStyles = StyleSheet.create({
|
const MemesView = ({
|
||||||
helperText: {
|
memes,
|
||||||
marginVertical: 10,
|
flashListRef,
|
||||||
},
|
flashListPadding,
|
||||||
flashList: {
|
handleScroll,
|
||||||
paddingBottom: 100,
|
}: {
|
||||||
// Needed to prevent fucky MasonryFlashList, see https://github.com/Shopify/flash-list/issues/876
|
memes: Realm.Results<Meme & Realm.Object<Meme>>;
|
||||||
paddingHorizontal: 0.01,
|
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 { dimensions, orientation } = useDimensions();
|
|
||||||
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,
|
||||||
@@ -54,10 +95,31 @@ const Memes = () => {
|
|||||||
collectionIn => {
|
collectionIn => {
|
||||||
let collection = collectionIn;
|
let collection = collectionIn;
|
||||||
|
|
||||||
|
const tokens = search
|
||||||
|
.match(/"[^"]+"|\S+/gi)
|
||||||
|
?.map(token => token.replaceAll(/["']/g, ''));
|
||||||
|
|
||||||
|
const tags = tokens
|
||||||
|
?.filter(token => token.startsWith('#'))
|
||||||
|
.map(tag => tag.slice(1));
|
||||||
|
|
||||||
|
const words = tokens?.filter(token => !token.startsWith('#'));
|
||||||
|
|
||||||
|
const tagsQuery = tags
|
||||||
|
?.map((tag, index) => `ANY tags.name CONTAINS[c] $${index}`)
|
||||||
|
.join(' OR ');
|
||||||
|
|
||||||
|
const wordsQuery = words
|
||||||
|
?.map((word, index) => `title CONTAINS[c] $${index}`)
|
||||||
|
.join(' OR ');
|
||||||
|
|
||||||
if (favoritesOnly) collection = collection.filtered('isFavorite == true');
|
if (favoritesOnly) collection = collection.filtered('isFavorite == true');
|
||||||
if (filter) collection = collection.filtered('type == $0', filter);
|
if (filter) collection = collection.filtered('type == $0', filter);
|
||||||
if (search) {
|
if (tags && tagsQuery) {
|
||||||
collection = collection.filtered('title CONTAINS[c] $0', search);
|
collection = collection.filtered(tagsQuery, ...tags);
|
||||||
|
}
|
||||||
|
if (words && wordsQuery) {
|
||||||
|
collection = collection.filtered(wordsQuery, ...words);
|
||||||
}
|
}
|
||||||
|
|
||||||
collection = collection.sorted(
|
collection = collection.sorted(
|
||||||
@@ -79,7 +141,9 @@ const Memes = () => {
|
|||||||
dispatch(setNavVisible(true));
|
dispatch(setNavVisible(true));
|
||||||
} else {
|
} else {
|
||||||
const diff = currentOffset - scrollOffset;
|
const diff = currentOffset - scrollOffset;
|
||||||
if (Math.abs(diff) > 50) dispatch(setNavVisible(diff < 0));
|
if (Math.abs(diff) > 50) {
|
||||||
|
dispatch(setNavVisible(diff < 0));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setScrollOffset(currentOffset);
|
setScrollOffset(currentOffset);
|
||||||
@@ -89,7 +153,6 @@ const Memes = () => {
|
|||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
dispatch(setNavVisible(true));
|
|
||||||
const handleBackPress = () => {
|
const handleBackPress = () => {
|
||||||
if (scrollOffset > 0) {
|
if (scrollOffset > 0) {
|
||||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||||
@@ -102,7 +165,13 @@ const Memes = () => {
|
|||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
||||||
}, [dispatch, scrollOffset]),
|
}, [scrollOffset]),
|
||||||
|
);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
dispatch(setNavVisible(true));
|
||||||
|
}, [dispatch]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -121,32 +190,11 @@ const Memes = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</HideableHeader>
|
</HideableHeader>
|
||||||
<MasonryFlashList
|
<MemesView
|
||||||
ref={flashListRef}
|
memes={memes}
|
||||||
data={memes}
|
flashListRef={flashListRef}
|
||||||
estimatedItemSize={200}
|
flashListPadding={flashListPadding}
|
||||||
estimatedListSize={{
|
handleScroll={handleScroll}
|
||||||
height: dimensions.height,
|
|
||||||
width: dimensions.width * 0.92,
|
|
||||||
}}
|
|
||||||
numColumns={2}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
renderItem={({ item: meme }) => <MemeCard meme={meme} />}
|
|
||||||
contentContainerStyle={{
|
|
||||||
paddingTop:
|
|
||||||
flashListPadding +
|
|
||||||
dimensions.height *
|
|
||||||
(orientation === ORIENTATION.PORTRAIT ? 0.02 : 0.04),
|
|
||||||
...memesStyles.flashList,
|
|
||||||
}}
|
|
||||||
ListEmptyComponent={() => (
|
|
||||||
<HelperText
|
|
||||||
type={'info'}
|
|
||||||
style={[memesStyles.helperText, styles.centerText]}>
|
|
||||||
No memes found
|
|
||||||
</HelperText>
|
|
||||||
)}
|
|
||||||
onScroll={handleScroll}
|
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
@@ -4,6 +4,7 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
List,
|
List,
|
||||||
Portal,
|
Portal,
|
||||||
|
SegmentedButtons,
|
||||||
Snackbar,
|
Snackbar,
|
||||||
Switch,
|
Switch,
|
||||||
Text,
|
Text,
|
||||||
@@ -13,19 +14,34 @@ import { openDocumentTree } from 'react-native-scoped-storage';
|
|||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import type {} from 'redux-thunk/extend-redux';
|
import type {} from 'redux-thunk/extend-redux';
|
||||||
import styles from '../styles';
|
import styles from '../styles';
|
||||||
import { RootState, setNoMedia, setStorageUri } from '../state';
|
import {
|
||||||
|
RootState,
|
||||||
|
setGridColumns,
|
||||||
|
setMasonryColumns,
|
||||||
|
setNoMedia,
|
||||||
|
setStorageUri,
|
||||||
|
} from '../state';
|
||||||
import { ORIENTATION, useDimensions } from '../contexts';
|
import { ORIENTATION, useDimensions } from '../contexts';
|
||||||
|
|
||||||
const settingsScreenStyles = StyleSheet.create({
|
const settingsStyles = StyleSheet.create({
|
||||||
snackbar: {
|
snackbar: {
|
||||||
marginBottom: 90,
|
marginBottom: 90,
|
||||||
},
|
},
|
||||||
|
marginBottom: {
|
||||||
|
marginBottom: 15,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const SettingsScreen = () => {
|
const Settings = () => {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { orientation, responsive } = useDimensions();
|
const { orientation } = useDimensions();
|
||||||
const noMedia = useSelector((state: RootState) => state.settings.noMedia);
|
const noMedia = useSelector((state: RootState) => state.settings.noMedia);
|
||||||
|
const masonryColumns = useSelector(
|
||||||
|
(state: RootState) => state.settings.masonryColumns,
|
||||||
|
);
|
||||||
|
const gridColumns = useSelector(
|
||||||
|
(state: RootState) => state.settings.gridColumns,
|
||||||
|
);
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const [isOptimizingDatabase, setIsOptimizingDatabase] = useState(false);
|
const [isOptimizingDatabase, setIsOptimizingDatabase] = useState(false);
|
||||||
@@ -52,21 +68,56 @@ const SettingsScreen = () => {
|
|||||||
]}>
|
]}>
|
||||||
<View>
|
<View>
|
||||||
<List.Section>
|
<List.Section>
|
||||||
<List.Subheader>Database</List.Subheader>
|
<List.Subheader>Views</List.Subheader>
|
||||||
<Button
|
<Text
|
||||||
mode="elevated"
|
style={[
|
||||||
loading={isOptimizingDatabase}
|
settingsStyles.marginBottom,
|
||||||
onPress={optimizeDatabase}>
|
styles.smallPaddingHorizontal,
|
||||||
Optimize Database Now
|
]}>
|
||||||
</Button>
|
Masonry Columns
|
||||||
|
</Text>
|
||||||
|
<SegmentedButtons
|
||||||
|
value={masonryColumns.toString()}
|
||||||
|
onValueChange={value => {
|
||||||
|
void dispatch(
|
||||||
|
setMasonryColumns(Number.parseInt(value) as 1 | 2 | 3 | 4),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
buttons={[
|
||||||
|
{ label: '1', value: '1' },
|
||||||
|
{ label: '2', value: '2' },
|
||||||
|
{ label: '3', value: '3' },
|
||||||
|
{ label: '4', value: '4' },
|
||||||
|
]}
|
||||||
|
style={settingsStyles.marginBottom}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
settingsStyles.marginBottom,
|
||||||
|
styles.smallPaddingHorizontal,
|
||||||
|
]}>
|
||||||
|
Grid Columns
|
||||||
|
</Text>
|
||||||
|
<SegmentedButtons
|
||||||
|
value={gridColumns.toString()}
|
||||||
|
onValueChange={value => {
|
||||||
|
void dispatch(
|
||||||
|
setGridColumns(Number.parseInt(value) as 1 | 2 | 3 | 4),
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
buttons={[
|
||||||
|
{ label: '1', value: '1' },
|
||||||
|
{ label: '2', value: '2' },
|
||||||
|
{ label: '3', value: '3' },
|
||||||
|
{ label: '4', value: '4' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</List.Section>
|
</List.Section>
|
||||||
<List.Section>
|
<List.Section>
|
||||||
<List.Subheader>Media Storage</List.Subheader>
|
<List.Subheader>Media Storage</List.Subheader>
|
||||||
<Button
|
<Button
|
||||||
mode="elevated"
|
mode="elevated"
|
||||||
style={{
|
style={settingsStyles.marginBottom}
|
||||||
marginBottom: responsive.verticalScale(15),
|
|
||||||
}}
|
|
||||||
onPress={async () => {
|
onPress={async () => {
|
||||||
const { uri } = await openDocumentTree(true);
|
const { uri } = await openDocumentTree(true);
|
||||||
void dispatch(setStorageUri(uri));
|
void dispatch(setStorageUri(uri));
|
||||||
@@ -77,9 +128,6 @@ const SettingsScreen = () => {
|
|||||||
style={[
|
style={[
|
||||||
styles.flexRowSpaceBetween,
|
styles.flexRowSpaceBetween,
|
||||||
styles.smallPaddingHorizontal,
|
styles.smallPaddingHorizontal,
|
||||||
{
|
|
||||||
marginBottom: responsive.verticalScale(15),
|
|
||||||
},
|
|
||||||
]}>
|
]}>
|
||||||
<Text>Hide media from gallery</Text>
|
<Text>Hide media from gallery</Text>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -91,12 +139,21 @@ const SettingsScreen = () => {
|
|||||||
</View>
|
</View>
|
||||||
</List.Section>
|
</List.Section>
|
||||||
</View>
|
</View>
|
||||||
|
<List.Section>
|
||||||
|
<List.Subheader>Database</List.Subheader>
|
||||||
|
<Button
|
||||||
|
mode="elevated"
|
||||||
|
loading={isOptimizingDatabase}
|
||||||
|
onPress={optimizeDatabase}>
|
||||||
|
Optimize Database Now
|
||||||
|
</Button>
|
||||||
|
</List.Section>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<Portal>
|
<Portal>
|
||||||
<Snackbar
|
<Snackbar
|
||||||
visible={snackbarVisible}
|
visible={snackbarVisible}
|
||||||
onDismiss={() => setSnackbarVisible(false)}
|
onDismiss={() => setSnackbarVisible(false)}
|
||||||
style={settingsScreenStyles.snackbar}
|
style={settingsStyles.snackbar}
|
||||||
action={{
|
action={{
|
||||||
label: 'Dismiss',
|
label: 'Dismiss',
|
||||||
onPress: () => setSnackbarVisible(false),
|
onPress: () => setSnackbarVisible(false),
|
||||||
@@ -108,4 +165,4 @@ const SettingsScreen = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SettingsScreen;
|
export default Settings;
|
||||||
|
@@ -80,7 +80,6 @@ const Tags = () => {
|
|||||||
|
|
||||||
useFocusEffect(
|
useFocusEffect(
|
||||||
useCallback(() => {
|
useCallback(() => {
|
||||||
dispatch(setNavVisible(true));
|
|
||||||
const handleBackPress = () => {
|
const handleBackPress = () => {
|
||||||
if (scrollOffset > 0) {
|
if (scrollOffset > 0) {
|
||||||
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
flashListRef.current?.scrollToOffset({ offset: 0, animated: true });
|
||||||
@@ -93,7 +92,13 @@ const Tags = () => {
|
|||||||
|
|
||||||
return () =>
|
return () =>
|
||||||
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
BackHandler.removeEventListener('hardwareBackPress', handleBackPress);
|
||||||
}, [dispatch, scrollOffset]),
|
}, [scrollOffset]),
|
||||||
|
);
|
||||||
|
|
||||||
|
useFocusEffect(
|
||||||
|
useCallback(() => {
|
||||||
|
dispatch(setNavVisible(true));
|
||||||
|
}, [dispatch]),
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@@ -54,6 +54,8 @@ export {
|
|||||||
type SettingsState,
|
type SettingsState,
|
||||||
setStorageUri,
|
setStorageUri,
|
||||||
setNoMedia,
|
setNoMedia,
|
||||||
|
setMasonryColumns,
|
||||||
|
setGridColumns,
|
||||||
validateSettings,
|
validateSettings,
|
||||||
} from './settings';
|
} from './settings';
|
||||||
export {
|
export {
|
||||||
|
@@ -37,6 +37,10 @@ const memesSlice = createSlice({
|
|||||||
cycleMemesView: state => {
|
cycleMemesView: state => {
|
||||||
switch (state.view) {
|
switch (state.view) {
|
||||||
case VIEW.MASONRY: {
|
case VIEW.MASONRY: {
|
||||||
|
state.view = VIEW.GRID;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case VIEW.GRID: {
|
||||||
state.view = VIEW.LIST;
|
state.view = VIEW.LIST;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
@@ -11,11 +11,15 @@ import { RootState } from '.';
|
|||||||
interface SettingsState {
|
interface SettingsState {
|
||||||
storageUri: string | undefined;
|
storageUri: string | undefined;
|
||||||
noMedia: boolean;
|
noMedia: boolean;
|
||||||
|
masonryColumns: 1 | 2 | 3 | 4;
|
||||||
|
gridColumns: 1 | 2 | 3 | 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
const initialState: SettingsState = {
|
||||||
storageUri: undefined,
|
storageUri: undefined,
|
||||||
noMedia: false,
|
noMedia: false,
|
||||||
|
masonryColumns: 2,
|
||||||
|
gridColumns: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
const settingsSlice = createSlice({
|
const settingsSlice = createSlice({
|
||||||
@@ -28,10 +32,17 @@ const settingsSlice = createSlice({
|
|||||||
setNoMedia: (state, action: PayloadAction<boolean>) => {
|
setNoMedia: (state, action: PayloadAction<boolean>) => {
|
||||||
state.noMedia = action.payload;
|
state.noMedia = action.payload;
|
||||||
},
|
},
|
||||||
|
setMasonryColumns: (state, action: PayloadAction<1 | 2 | 3 | 4>) => {
|
||||||
|
state.masonryColumns = action.payload;
|
||||||
|
},
|
||||||
|
setGridColumns: (state, action: PayloadAction<1 | 2 | 3 | 4>) => {
|
||||||
|
state.gridColumns = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const { setStorageUri, setNoMedia } = settingsSlice.actions;
|
const { setStorageUri, setNoMedia, setMasonryColumns, setGridColumns } =
|
||||||
|
settingsSlice.actions;
|
||||||
|
|
||||||
const updateStorageUri = createAsyncThunk(
|
const updateStorageUri = createAsyncThunk(
|
||||||
'settings/updateStorageUri',
|
'settings/updateStorageUri',
|
||||||
@@ -111,6 +122,8 @@ export {
|
|||||||
type SettingsState,
|
type SettingsState,
|
||||||
updateStorageUri as setStorageUri,
|
updateStorageUri as setStorageUri,
|
||||||
updateNoMedia as setNoMedia,
|
updateNoMedia as setNoMedia,
|
||||||
|
setMasonryColumns,
|
||||||
|
setGridColumns,
|
||||||
validateSettings,
|
validateSettings,
|
||||||
};
|
};
|
||||||
export default settingsSlice.reducer;
|
export default settingsSlice.reducer;
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
enum VIEW {
|
enum VIEW {
|
||||||
MASONRY = 'Masonry',
|
MASONRY = 'Masonry',
|
||||||
|
GRID = 'Grid',
|
||||||
LIST = 'List',
|
LIST = 'List',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
9
src/utilities/dimensions.ts
Normal file
9
src/utilities/dimensions.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const getFlashListItemHeight = (numColumns: number) => {
|
||||||
|
const A = 500;
|
||||||
|
const B = 300;
|
||||||
|
const C = 1;
|
||||||
|
const height = A - B * Math.log(numColumns + C);
|
||||||
|
return Math.max(Math.round(height), 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getFlashListItemHeight };
|
@@ -50,6 +50,9 @@ const getViewIcon = (view: VIEW) => {
|
|||||||
case VIEW.MASONRY: {
|
case VIEW.MASONRY: {
|
||||||
return 'view-dashboard';
|
return 'view-dashboard';
|
||||||
}
|
}
|
||||||
|
case VIEW.GRID: {
|
||||||
|
return 'view-grid';
|
||||||
|
}
|
||||||
case VIEW.LIST: {
|
case VIEW.LIST: {
|
||||||
return 'view-list';
|
return 'view-list';
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ export {
|
|||||||
} from './color';
|
} from './color';
|
||||||
export { packageName, appName, fileProvider, noOp } from './constants';
|
export { packageName, appName, fileProvider, noOp } from './constants';
|
||||||
export { multipleIdQuery } from './database';
|
export { multipleIdQuery } from './database';
|
||||||
|
export { getFlashListItemHeight } from './dimensions';
|
||||||
export {
|
export {
|
||||||
allowedImageMimeTypes,
|
allowedImageMimeTypes,
|
||||||
allowedGifMimeTypes,
|
allowedGifMimeTypes,
|
||||||
@@ -18,7 +19,6 @@ export { getSortIcon, getViewIcon } from './icon';
|
|||||||
export {
|
export {
|
||||||
type StringValidationResult,
|
type StringValidationResult,
|
||||||
validateMemeTitle,
|
validateMemeTitle,
|
||||||
validateMemeDescription,
|
|
||||||
validateTagName,
|
validateTagName,
|
||||||
validateColor,
|
validateColor,
|
||||||
} from './validation';
|
} from './validation';
|
||||||
|
@@ -26,18 +26,6 @@ const validateMemeTitle = (title: string): StringValidationResult => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateMemeDescription = (
|
|
||||||
description: string,
|
|
||||||
): StringValidationResult => {
|
|
||||||
const parsedDescription = description.trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
valid: true,
|
|
||||||
raw: description,
|
|
||||||
parsed: parsedDescription,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateTagName = (name: string): StringValidationResult => {
|
const validateTagName = (name: string): StringValidationResult => {
|
||||||
const parsedName = name.trim();
|
const parsedName = name.trim();
|
||||||
|
|
||||||
@@ -79,7 +67,6 @@ const validateColor = (color: string): StringValidationResult => {
|
|||||||
export {
|
export {
|
||||||
type StringValidationResult,
|
type StringValidationResult,
|
||||||
validateMemeTitle,
|
validateMemeTitle,
|
||||||
validateMemeDescription,
|
|
||||||
validateTagName,
|
validateTagName,
|
||||||
validateColor,
|
validateColor,
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user