Compare commits
7 Commits
Author | SHA1 | Date | |
---|---|---|---|
91bcc6072f | |||
231c9b0d79 | |||
1b09b058e4 | |||
5958cf57ee | |||
e550fcd881 | |||
665931f7b9 | |||
d2054b028a |
@@ -79,7 +79,7 @@ android {
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "0.0.4"
|
||||
versionName "0.0.8"
|
||||
}
|
||||
signingConfigs {
|
||||
debug {
|
||||
|
82
package-lock.json
generated
82
package-lock.json
generated
@@ -12,6 +12,8 @@
|
||||
"@bankify/redux-persist-realm": "^0.1.3",
|
||||
"@react-native-clipboard/clipboard": "^1.11.2",
|
||||
"@react-native-community/hooks": "^3.0.0",
|
||||
"@react-native-masked-view/masked-view": "^0.2.9",
|
||||
"@react-native-ml-kit/text-recognition": "^1.2.1",
|
||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
"@react-navigation/native-stack": "^6.9.13",
|
||||
@@ -22,9 +24,11 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.2",
|
||||
"react-native-document-picker": "^9.0.1",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-file-access": "^3.0.4",
|
||||
"react-native-gesture-handler": "^2.12.0",
|
||||
"react-native-get-random-values": "^1.9.0",
|
||||
"react-native-linear-gradient": "^2.8.1",
|
||||
"react-native-mime-types": "^2.4.0",
|
||||
"react-native-paper": "^5.9.1",
|
||||
"react-native-reanimated": "^3.3.0",
|
||||
@@ -33,6 +37,7 @@
|
||||
"react-native-screens": "^3.22.1",
|
||||
"react-native-share": "^9.2.3",
|
||||
"react-native-share-menu": "^6.0.0",
|
||||
"react-native-skeleton-placeholder": "^5.2.4",
|
||||
"react-native-vector-icons": "^9.2.0",
|
||||
"react-native-video": "^6.0.0-alpha.6",
|
||||
"react-redux": "^8.1.1",
|
||||
@@ -4113,6 +4118,24 @@
|
||||
"react-native": ">=0.65"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-masked-view/masked-view": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.2.9.tgz",
|
||||
"integrity": "sha512-Hs4vKBKj+15VxHZHFtMaFWSBxXoOE5Ea8saoigWhahp8Mepssm0ezU+2pTl7DK9z8Y9s5uOl/aPb4QmBZ3R3Zw==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16",
|
||||
"react-native": ">=0.57"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native-ml-kit/text-recognition": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-ml-kit/text-recognition/-/text-recognition-1.2.1.tgz",
|
||||
"integrity": "sha512-pJrnf8AvihzYdPAZoZZEeKbOUOMjdsetDjHlleXOoVcoPo6qjfh6Il/Q0ey3boIQuO3HglvNjcMPGEPThF3sPA==",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.1",
|
||||
"react-native": ">=0.60.0-rc.0 <1.0.x"
|
||||
}
|
||||
},
|
||||
"node_modules/@react-native/assets-registry": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz",
|
||||
@@ -13424,6 +13447,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-fast-image": {
|
||||
"version": "8.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz",
|
||||
"integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==",
|
||||
"peerDependencies": {
|
||||
"react": "^17 || ^18",
|
||||
"react-native": ">=0.60.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-file-access": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz",
|
||||
@@ -13460,6 +13492,15 @@
|
||||
"react-native": ">=0.56"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-linear-gradient": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz",
|
||||
"integrity": "sha512-934R4Bnjo7mYT38W9ypS1Dq/YW6TgyGdkHg+w72HNxN0ZDKG1GqAnZ6XlicMUYJDh7ViiJAKN8eOF3Ho0N4J0Q==",
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-mime-types": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz",
|
||||
@@ -13572,6 +13613,17 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz",
|
||||
"integrity": "sha512-KdmRnqjI/B2MigSxGmhbYJ3WMJxKXj+0c47ANcVZ/PTzc2vtz6d1r4KQJgkBImXgNC+vowpuD2UGdPllxadr2A=="
|
||||
},
|
||||
"node_modules/react-native-skeleton-placeholder": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-skeleton-placeholder/-/react-native-skeleton-placeholder-5.2.4.tgz",
|
||||
"integrity": "sha512-OZntVq1hU1UX33FltxK2ezT2v9vHIhV8YnEbnMWUCvxT0N9OsgD1qxiHm6qb9YRJVgq2o5z3S7dNPsPnDF/jNg==",
|
||||
"peerDependencies": {
|
||||
"@react-native-masked-view/masked-view": "^0.2.8",
|
||||
"react": ">=0.14.8",
|
||||
"react-native": ">=0.50.1",
|
||||
"react-native-linear-gradient": "^2.5.6"
|
||||
}
|
||||
},
|
||||
"node_modules/react-native-vector-icons": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz",
|
||||
@@ -18799,6 +18851,18 @@
|
||||
"integrity": "sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-native-masked-view/masked-view": {
|
||||
"version": "0.2.9",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-masked-view/masked-view/-/masked-view-0.2.9.tgz",
|
||||
"integrity": "sha512-Hs4vKBKj+15VxHZHFtMaFWSBxXoOE5Ea8saoigWhahp8Mepssm0ezU+2pTl7DK9z8Y9s5uOl/aPb4QmBZ3R3Zw==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-native-ml-kit/text-recognition": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@react-native-ml-kit/text-recognition/-/text-recognition-1.2.1.tgz",
|
||||
"integrity": "sha512-pJrnf8AvihzYdPAZoZZEeKbOUOMjdsetDjHlleXOoVcoPo6qjfh6Il/Q0ey3boIQuO3HglvNjcMPGEPThF3sPA==",
|
||||
"requires": {}
|
||||
},
|
||||
"@react-native/assets-registry": {
|
||||
"version": "0.72.0",
|
||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz",
|
||||
@@ -25845,6 +25909,12 @@
|
||||
"invariant": "^2.2.4"
|
||||
}
|
||||
},
|
||||
"react-native-fast-image": {
|
||||
"version": "8.6.3",
|
||||
"resolved": "https://registry.npmjs.org/react-native-fast-image/-/react-native-fast-image-8.6.3.tgz",
|
||||
"integrity": "sha512-Sdw4ESidXCXOmQ9EcYguNY2swyoWmx53kym2zRsvi+VeFCHEdkO+WG1DK+6W81juot40bbfLNhkc63QnWtesNg==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-file-access": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz",
|
||||
@@ -25871,6 +25941,12 @@
|
||||
"fast-base64-decode": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"react-native-linear-gradient": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/react-native-linear-gradient/-/react-native-linear-gradient-2.8.1.tgz",
|
||||
"integrity": "sha512-934R4Bnjo7mYT38W9ypS1Dq/YW6TgyGdkHg+w72HNxN0ZDKG1GqAnZ6XlicMUYJDh7ViiJAKN8eOF3Ho0N4J0Q==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-mime-types": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-mime-types/-/react-native-mime-types-2.4.0.tgz",
|
||||
@@ -25957,6 +26033,12 @@
|
||||
"resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz",
|
||||
"integrity": "sha512-KdmRnqjI/B2MigSxGmhbYJ3WMJxKXj+0c47ANcVZ/PTzc2vtz6d1r4KQJgkBImXgNC+vowpuD2UGdPllxadr2A=="
|
||||
},
|
||||
"react-native-skeleton-placeholder": {
|
||||
"version": "5.2.4",
|
||||
"resolved": "https://registry.npmjs.org/react-native-skeleton-placeholder/-/react-native-skeleton-placeholder-5.2.4.tgz",
|
||||
"integrity": "sha512-OZntVq1hU1UX33FltxK2ezT2v9vHIhV8YnEbnMWUCvxT0N9OsgD1qxiHm6qb9YRJVgq2o5z3S7dNPsPnDF/jNg==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-native-vector-icons": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz",
|
||||
|
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@karaolidis/terminally-online",
|
||||
"version": "0.0.4",
|
||||
"version": "0.0.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"postinstall": "patch-package",
|
||||
@@ -17,6 +17,8 @@
|
||||
"@bankify/redux-persist-realm": "^0.1.3",
|
||||
"@react-native-clipboard/clipboard": "^1.11.2",
|
||||
"@react-native-community/hooks": "^3.0.0",
|
||||
"@react-native-masked-view/masked-view": "^0.2.9",
|
||||
"@react-native-ml-kit/text-recognition": "^1.2.1",
|
||||
"@react-navigation/bottom-tabs": "^6.5.8",
|
||||
"@react-navigation/native": "^6.1.7",
|
||||
"@react-navigation/native-stack": "^6.9.13",
|
||||
@@ -27,9 +29,11 @@
|
||||
"react": "18.2.0",
|
||||
"react-native": "0.72.2",
|
||||
"react-native-document-picker": "^9.0.1",
|
||||
"react-native-fast-image": "^8.6.3",
|
||||
"react-native-file-access": "^3.0.4",
|
||||
"react-native-gesture-handler": "^2.12.0",
|
||||
"react-native-get-random-values": "^1.9.0",
|
||||
"react-native-linear-gradient": "^2.8.1",
|
||||
"react-native-mime-types": "^2.4.0",
|
||||
"react-native-paper": "^5.9.1",
|
||||
"react-native-reanimated": "^3.3.0",
|
||||
@@ -38,6 +42,7 @@
|
||||
"react-native-screens": "^3.22.1",
|
||||
"react-native-share": "^9.2.3",
|
||||
"react-native-share-menu": "^6.0.0",
|
||||
"react-native-skeleton-placeholder": "^5.2.4",
|
||||
"react-native-vector-icons": "^9.2.0",
|
||||
"react-native-video": "^6.0.0-alpha.6",
|
||||
"react-redux": "^8.1.1",
|
||||
|
@@ -1,15 +1,9 @@
|
||||
export {
|
||||
MemesList,
|
||||
MemeEditor,
|
||||
MemeFail,
|
||||
MemesHeader,
|
||||
MemeTagSelector,
|
||||
MemeViewItem,
|
||||
} from './memes';
|
||||
export { TagChip, TagEditor, TagPreview, TagRow, TagsHeader } from './tags';
|
||||
export { default as AnimatedImage } from './animatedImage';
|
||||
export { default as FloatingActionButton } from './floatingActionButton';
|
||||
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
|
||||
export { default as HideableHeader } from './hideableHeader';
|
||||
export { default as LoadingView } from './loadingView';
|
||||
export { default as storageLocationChangeDialog } from './storageLocationChangeDialog';
|
||||
export { default as MemeFail } from './memeFail';
|
||||
export { default as TagChip } from './tagChip';
|
||||
export { default as TextOverlay } from './textOverlay';
|
||||
export { default as ThemedSkeletonPlaceholder } from './themedSkeletonPlaceholder';
|
||||
|
@@ -2,7 +2,7 @@ import React, { ComponentProps } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||
import { rgbToRgba } from '../../utilities';
|
||||
import { rgbToRgba } from '../utilities';
|
||||
|
||||
const memeFailStyles = StyleSheet.create({
|
||||
view: {
|
@@ -1,6 +0,0 @@
|
||||
export { default as MemesList } from './memesList/memesList';
|
||||
export { default as MemeEditor } from './memeEditor';
|
||||
export { default as MemeFail } from './memeFail';
|
||||
export { default as MemesHeader } from './memesHeader';
|
||||
export { default as MemeTagSelector } from './memeTagSelector/memeTagSelector';
|
||||
export { default as MemeViewItem } from './memeViewItem';
|
@@ -1,78 +0,0 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Image, TouchableHighlight } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { AndroidScoped } from 'react-native-file-access';
|
||||
import { MEME_TYPE, Meme } from '../../../database';
|
||||
import { RootState } from '../../../state';
|
||||
import { MemeFail } from '..';
|
||||
import { getFontAwesome5IconSize } from '../../../utilities';
|
||||
import { useMemeDimensions } from '../../../hooks';
|
||||
|
||||
const MemesGridItem = ({
|
||||
meme,
|
||||
index,
|
||||
focusMeme,
|
||||
}: {
|
||||
meme: Meme;
|
||||
index: number;
|
||||
focusMeme: (index: number) => void;
|
||||
}) => {
|
||||
const { width } = useSafeAreaFrame();
|
||||
const gridColumns = useSelector(
|
||||
(state: RootState) => state.settings.gridColumns,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const storageUri = useSelector(
|
||||
(state: RootState) => state.settings.storageUri,
|
||||
)!;
|
||||
|
||||
const uri = AndroidScoped.appendPath(storageUri, meme.filename);
|
||||
|
||||
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
|
||||
|
||||
const itemWidth = (width * 0.92 - 5) / gridColumns;
|
||||
|
||||
const mediaComponent = useMemo(() => {
|
||||
switch (meme.memeType) {
|
||||
case MEME_TYPE.IMAGE:
|
||||
case MEME_TYPE.GIF:
|
||||
case MEME_TYPE.VIDEO: {
|
||||
return (
|
||||
<Image
|
||||
source={{ uri }}
|
||||
style={[
|
||||
{
|
||||
width: itemWidth,
|
||||
height: itemWidth,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
}, [itemWidth, meme.memeType, uri]);
|
||||
|
||||
if (!error && (loading || !dimensions)) return <></>;
|
||||
|
||||
return (
|
||||
<TouchableHighlight onPress={() => focusMeme(index)}>
|
||||
{error ? (
|
||||
<MemeFail
|
||||
style={{
|
||||
width: (width * 0.92 - 5) / gridColumns,
|
||||
height: (width * 0.92 - 5) / gridColumns,
|
||||
}}
|
||||
iconSize={getFontAwesome5IconSize(gridColumns)}
|
||||
/>
|
||||
) : (
|
||||
mediaComponent
|
||||
)}
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemesGridItem;
|
@@ -1,9 +1,9 @@
|
||||
import React, { ComponentProps, useMemo } from 'react';
|
||||
import { getContrastColor } from '../../utilities';
|
||||
import { Chip, useTheme } from 'react-native-paper';
|
||||
import { Tag } from '../../database';
|
||||
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { getContrastColor } from '../utilities';
|
||||
import { Tag } from '../database';
|
||||
|
||||
const tagChipStyles = StyleSheet.create({
|
||||
chip: {
|
@@ -1,5 +0,0 @@
|
||||
export { default as TagChip } from './tagChip';
|
||||
export { default as TagEditor } from './tagEditor';
|
||||
export { default as TagPreview } from './tagPreview';
|
||||
export { default as TagRow } from './tagRow';
|
||||
export { default as TagsHeader } from './tagsHeader';
|
64
src/components/textOverlay.tsx
Normal file
64
src/components/textOverlay.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import { TextBlock } from '@react-native-ml-kit/text-recognition';
|
||||
import { TouchableRipple, useTheme } from 'react-native-paper';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import { Dimensions } from '../types';
|
||||
|
||||
const textOverlayStyles = StyleSheet.create({
|
||||
touchable: {
|
||||
position: 'absolute',
|
||||
borderWidth: 1,
|
||||
zIndex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const TextOverlay = ({
|
||||
blocks,
|
||||
onTextPress,
|
||||
onTextLongPress,
|
||||
imageDimensions,
|
||||
frameDimensions,
|
||||
}: {
|
||||
blocks: TextBlock[];
|
||||
onTextPress: (text: string) => void;
|
||||
onTextLongPress: (text: string) => void;
|
||||
imageDimensions: Dimensions;
|
||||
frameDimensions: Dimensions;
|
||||
}) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
const widthScale = frameDimensions.width / imageDimensions.width;
|
||||
const heightScale = frameDimensions.height / imageDimensions.height;
|
||||
|
||||
return (
|
||||
<>
|
||||
{blocks.map(
|
||||
(block, index) =>
|
||||
block.frame && (
|
||||
<TouchableRipple
|
||||
key={index}
|
||||
style={[
|
||||
textOverlayStyles.touchable,
|
||||
{
|
||||
top: block.frame.top * heightScale - 5,
|
||||
left: block.frame.left * widthScale - 5,
|
||||
width: block.frame.width * widthScale + 10,
|
||||
height: block.frame.height * heightScale + 10,
|
||||
borderColor: colors.error,
|
||||
},
|
||||
]}
|
||||
onPress={() =>
|
||||
onTextPress(block.text.replaceAll('\n', ' ').trim())
|
||||
}
|
||||
onLongPress={() =>
|
||||
onTextLongPress(block.text.replaceAll('\n', ' ').trim())
|
||||
}>
|
||||
<></>
|
||||
</TouchableRipple>
|
||||
),
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextOverlay;
|
20
src/components/themedSkeletonPlaceholder.tsx
Normal file
20
src/components/themedSkeletonPlaceholder.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React, { ComponentProps } from 'react';
|
||||
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
|
||||
import { useTheme } from 'react-native-paper';
|
||||
import { rgbToRgba } from '../utilities';
|
||||
|
||||
const ThemedSkeletonPlaceholder = ({
|
||||
...props
|
||||
}: ComponentProps<typeof SkeletonPlaceholder>) => {
|
||||
const { colors } = useTheme();
|
||||
|
||||
return (
|
||||
<SkeletonPlaceholder
|
||||
backgroundColor={rgbToRgba(colors.surfaceVariant, 0.2)}
|
||||
highlightColor={rgbToRgba(colors.surfaceVariant, 0.7)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemedSkeletonPlaceholder;
|
@@ -1,5 +1,5 @@
|
||||
import { BSON, Object, ObjectSchema } from 'realm';
|
||||
import { Tag } from './tag';
|
||||
import { Tag } from '.';
|
||||
|
||||
enum MEME_TYPE {
|
||||
IMAGE = 'Image',
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { BSON, Object, ObjectSchema } from 'realm';
|
||||
import { Meme } from './meme';
|
||||
import { Meme } from '.';
|
||||
import { generateRandomColor } from '../utilities';
|
||||
|
||||
class Tag extends Object<Tag> {
|
||||
|
@@ -15,17 +15,17 @@ import {
|
||||
ROUTE,
|
||||
RootStackParamList,
|
||||
StagingMeme,
|
||||
} from '../../types';
|
||||
import { Meme, Tag } from '../../database';
|
||||
import { RootState } from '../../state';
|
||||
} from '../../../types';
|
||||
import { Meme, Tag } from '../../../database';
|
||||
import { RootState } from '../../../state';
|
||||
import {
|
||||
allowedMimeTypes,
|
||||
getMemeTypeFromMimeType,
|
||||
guessMimeType,
|
||||
validateMemeTitle,
|
||||
} from '../../utilities';
|
||||
import { MemeEditor } from '../../components';
|
||||
import editorStyles from './editorStyles';
|
||||
} from '../../../utilities';
|
||||
import MemeEditor from './memeEditor';
|
||||
import editorStyles from '../editorStyles';
|
||||
|
||||
const AddMeme = ({
|
||||
route,
|
@@ -6,12 +6,12 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { useObject, useRealm } from '@realm/react';
|
||||
import { useDeviceOrientation } from '@react-native-community/hooks';
|
||||
import { BSON } from 'realm';
|
||||
import { RootStackParamList, ROUTE, StagingMeme } from '../../types';
|
||||
import { pickSingle } from 'react-native-document-picker';
|
||||
import { AndroidScoped, FileSystem } from 'react-native-file-access';
|
||||
import { extension } from 'react-native-mime-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Meme } from '../../database';
|
||||
import { RootStackParamList, ROUTE, StagingMeme } from '../../../types';
|
||||
import { Meme } from '../../../database';
|
||||
import {
|
||||
allowedMimeTypes,
|
||||
deleteMeme,
|
||||
@@ -20,10 +20,10 @@ import {
|
||||
guessMimeType,
|
||||
noOp,
|
||||
validateMemeTitle,
|
||||
} from '../../utilities';
|
||||
import { MemeEditor } from '../../components';
|
||||
import editorStyles from './editorStyles';
|
||||
import { RootState } from '../../state';
|
||||
} from '../../../utilities';
|
||||
import { RootState } from '../../../state';
|
||||
import MemeEditor from './memeEditor';
|
||||
import editorStyles from '../editorStyles';
|
||||
|
||||
const EditMeme = ({
|
||||
route,
|
@@ -1,17 +1,22 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { HelperText, Text, TextInput, useTheme } from 'react-native-paper';
|
||||
import { Image, LayoutAnimation } from 'react-native';
|
||||
import { LayoutAnimation, View } from 'react-native';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { LoadingView, MemeFail, MemeTagSelector } from '..';
|
||||
import Video from 'react-native-video';
|
||||
import TextRecognition, {
|
||||
TextRecognitionResult,
|
||||
} from '@react-native-ml-kit/text-recognition';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import { LoadingView, MemeFail, TextOverlay } from '../../../components';
|
||||
import {
|
||||
getFilenameFromUri,
|
||||
getMemeTypeFromMimeType,
|
||||
validateMemeTitle,
|
||||
} from '../../utilities';
|
||||
import { StagingMeme } from '../../types';
|
||||
import { useMemeDimensions } from '../../hooks';
|
||||
import { MEME_TYPE } from '../../database';
|
||||
import Video from 'react-native-video';
|
||||
} from '../../../utilities';
|
||||
import { StagingMeme } from '../../../types';
|
||||
import { useMemeDimensions } from '../../../hooks';
|
||||
import { MEME_TYPE } from '../../../database';
|
||||
import MemeTagSelector from './memeTagSelector/memeTagSelector';
|
||||
|
||||
const memeEditorStyles = {
|
||||
media: {
|
||||
@@ -65,8 +70,15 @@ const MemeEditor = ({
|
||||
useMemo(() => (errorIn: Error) => setError(errorIn), [setError]),
|
||||
);
|
||||
|
||||
const [recognizedText, setRecognizedText] = useState<TextRecognitionResult>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!uri || !mimeType || !mimeType.startsWith('image')) return;
|
||||
void TextRecognition.recognize(uri).then(setRecognizedText);
|
||||
}, [mimeType, uri]);
|
||||
|
||||
const mediaComponent = useMemo(() => {
|
||||
if (!mimeType || !dimensions) return <></>;
|
||||
if (!mimeType || !dimensions || !staging) return <></>;
|
||||
|
||||
const dimensionStyles = {
|
||||
width: width * 0.92,
|
||||
@@ -83,11 +95,35 @@ const MemeEditor = ({
|
||||
case MEME_TYPE.IMAGE:
|
||||
case MEME_TYPE.GIF: {
|
||||
return (
|
||||
<Image
|
||||
source={{ uri }}
|
||||
style={[memeEditorStyles.media, dimensionStyles]}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<View>
|
||||
<FastImage
|
||||
source={{ uri }}
|
||||
style={[memeEditorStyles.media, dimensionStyles]}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
{recognizedText && (
|
||||
<TextOverlay
|
||||
blocks={recognizedText.blocks}
|
||||
onTextPress={text =>
|
||||
setStaging({
|
||||
...staging,
|
||||
title: validateMemeTitle(text),
|
||||
})
|
||||
}
|
||||
onTextLongPress={text =>
|
||||
setStaging({
|
||||
...staging,
|
||||
title: validateMemeTitle(`${staging.title.parsed} ${text}`),
|
||||
})
|
||||
}
|
||||
imageDimensions={dimensions}
|
||||
frameDimensions={{
|
||||
...dimensionStyles,
|
||||
aspectRatio: dimensionStyles.width / dimensionStyles.height,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
case MEME_TYPE.VIDEO: {
|
||||
@@ -104,7 +140,7 @@ const MemeEditor = ({
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
}, [dimensions, mimeType, uri, width]);
|
||||
}, [dimensions, mimeType, recognizedText, setStaging, staging, uri, width]);
|
||||
|
||||
if (!uri || !mimeType || !staging) return <LoadingView />;
|
||||
|
@@ -4,10 +4,10 @@ import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper';
|
||||
import { LayoutAnimation, StyleSheet } from 'react-native';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { TAG_SORT, tagSortQuery } from '../../../types';
|
||||
import { TagChip } from '../../tags';
|
||||
import { Tag } from '../../../database';
|
||||
import { validateTagName } from '../../../utilities';
|
||||
import { TAG_SORT, tagSortQuery } from '../../../../types';
|
||||
import { TagChip } from '../../../../components';
|
||||
import { Tag } from '../../../../database';
|
||||
import { validateTagName } from '../../../../utilities';
|
||||
|
||||
const memeTagSearchModalStyles = StyleSheet.create({
|
||||
modal: {
|
||||
@@ -87,6 +87,8 @@ const MemeTagSearchModal = ({
|
||||
[search],
|
||||
);
|
||||
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
const handleTagPress = (tag: Tag) => {
|
||||
const id = tag.id.toHexString();
|
||||
memeTags.delete(id) || memeTags.set(id, tag);
|
||||
@@ -105,6 +107,7 @@ const MemeTagSearchModal = ({
|
||||
if (!tag) return;
|
||||
memeTags.set(tag.id.toHexString(), tag);
|
||||
setMemeTags(new Map(memeTags));
|
||||
setRefreshKey(refreshKey + 1);
|
||||
flashListRef.current?.prepareForLayoutAnimationRender();
|
||||
LayoutAnimation.configureNext(tagLayoutAnimation);
|
||||
};
|
||||
@@ -129,6 +132,7 @@ const MemeTagSearchModal = ({
|
||||
/>
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
key={refreshKey}
|
||||
data={tags}
|
||||
extraData={memeTags}
|
||||
keyExtractor={tag => tag.id.toHexString()}
|
||||
@@ -148,7 +152,7 @@ const MemeTagSearchModal = ({
|
||||
active={memeTags.has(tag.id.toHexString())}
|
||||
/>
|
||||
)}
|
||||
ListEmptyComponent={() => (
|
||||
ListFooterComponent={() => (
|
||||
<Chip
|
||||
icon="plus"
|
||||
mode="outlined"
|
@@ -3,8 +3,8 @@ import { LayoutAnimation, StyleSheet, View } from 'react-native';
|
||||
import { Chip } from 'react-native-paper';
|
||||
import { FlashList } from '@shopify/flash-list';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { TagChip } from '../../tags';
|
||||
import { Tag } from '../../../database';
|
||||
import { TagChip } from '../../../../components';
|
||||
import { Tag } from '../../../../database';
|
||||
import MemeTagSearchModal from './memeTagSearchModal';
|
||||
|
||||
const memeTagSelectorStyles = StyleSheet.create({
|
@@ -8,11 +8,11 @@ import {
|
||||
generateRandomColor,
|
||||
validateColor,
|
||||
validateTagName,
|
||||
} from '../../utilities';
|
||||
import { Tag } from '../../database';
|
||||
import { TagEditor } from '../../components';
|
||||
import editorStyles from './editorStyles';
|
||||
import { StagingTag } from '../../types';
|
||||
} from '../../../utilities';
|
||||
import { Tag } from '../../../database';
|
||||
import { StagingTag } from '../../../types';
|
||||
import TagEditor from './tagEditor';
|
||||
import editorStyles from '../editorStyles';
|
||||
|
||||
const AddTag = () => {
|
||||
const { goBack } = useNavigation();
|
@@ -6,11 +6,11 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { BSON } from 'realm';
|
||||
import { useObject, useRealm } from '@realm/react';
|
||||
import { useDeviceOrientation } from '@react-native-community/hooks';
|
||||
import { TagEditor } from '../../components';
|
||||
import { ROUTE, RootStackParamList, StagingTag } from '../../types';
|
||||
import { Tag } from '../../database';
|
||||
import { deleteTag, validateColor, validateTagName } from '../../utilities';
|
||||
import editorStyles from './editorStyles';
|
||||
import { ROUTE, RootStackParamList, StagingTag } from '../../../types';
|
||||
import { Tag } from '../../../database';
|
||||
import { deleteTag, validateColor, validateTagName } from '../../../utilities';
|
||||
import TagEditor from './tagEditor';
|
||||
import editorStyles from '../editorStyles';
|
||||
|
||||
const EditTag = ({
|
||||
route,
|
@@ -1,12 +1,12 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { HelperText, TextInput } from 'react-native-paper';
|
||||
import TagPreview from './tagPreview';
|
||||
import {
|
||||
generateRandomColor,
|
||||
validateColor,
|
||||
validateTagName,
|
||||
} from '../../utilities';
|
||||
import { StagingTag } from '../../types';
|
||||
} from '../../../utilities';
|
||||
import { StagingTag } from '../../../types';
|
||||
import TagPreview from './tagPreview';
|
||||
|
||||
const TagEditor = ({
|
||||
staging,
|
@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
|
||||
import { Chip, useTheme } from 'react-native-paper';
|
||||
import { getContrastColor } from '../../utilities';
|
||||
import { getContrastColor } from '../../../utilities';
|
||||
|
||||
const tagPreviewStyles = StyleSheet.create({
|
||||
view: {
|
@@ -1,9 +1,9 @@
|
||||
export { default as AddMeme } from './editors/addMeme';
|
||||
export { default as AddTag } from './editors/addTag';
|
||||
export { default as EditMeme } from './editors/editMeme';
|
||||
export { default as EditTag } from './editors/editTag';
|
||||
export { default as Memes } from './memes';
|
||||
export { default as MemeView } from './memeView';
|
||||
export { default as Settings } from './settings';
|
||||
export { default as Tags } from './tags';
|
||||
export { default as AddMeme } from './editors/meme/addMeme';
|
||||
export { default as AddTag } from './editors/tag/addTag';
|
||||
export { default as EditMeme } from './editors/meme/editMeme';
|
||||
export { default as EditTag } from './editors/tag/editTag';
|
||||
export { default as Memes } from './memes/memes';
|
||||
export { default as MemeView } from './memeView/memeView';
|
||||
export { default as Settings } from './settings/settings';
|
||||
export { default as Tags } from './tags/tags';
|
||||
export { default as Welcome } from './welcome';
|
||||
|
@@ -12,9 +12,9 @@ import {
|
||||
RootStackParamList,
|
||||
ROUTE,
|
||||
SORT_DIRECTION,
|
||||
} from '../types';
|
||||
import { Meme } from '../database';
|
||||
import { LoadingView, MemeViewItem } from '../components';
|
||||
} from '../../types';
|
||||
import { Meme } from '../../database';
|
||||
import { LoadingView } from '../../components';
|
||||
import {
|
||||
copyMeme,
|
||||
deleteMeme,
|
||||
@@ -22,8 +22,9 @@ import {
|
||||
favoriteMeme,
|
||||
multipleIdQuery,
|
||||
shareMeme,
|
||||
} from '../utilities';
|
||||
import { RootState } from '../state';
|
||||
} from '../../utilities';
|
||||
import { RootState } from '../../state';
|
||||
import MemeViewItem from './memeViewItem';
|
||||
|
||||
const memeViewStyles = StyleSheet.create({
|
||||
// eslint-disable-next-line react-native/no-color-literals
|
||||
@@ -70,7 +71,7 @@ const MemeView = ({
|
||||
|
||||
const flashListRef = useRef<FlashList<Meme>>(null);
|
||||
|
||||
const index = useRef(route.params.index);
|
||||
const [index, setIndex] = useState(route.params.index);
|
||||
const memes = useQuery<Meme>(Meme.schema.name, collectionIn => {
|
||||
return collectionIn
|
||||
.filtered(multipleIdQuery(route.params.ids))
|
||||
@@ -82,8 +83,8 @@ const MemeView = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (memes.length === 0) navigation.goBack();
|
||||
if (index.current >= memes.length) {
|
||||
index.current = memes.length - 1;
|
||||
if (index >= memes.length) {
|
||||
setIndex(memes.length - 1);
|
||||
flashListRef.current?.scrollToIndex({ index: memes.length - 1 });
|
||||
}
|
||||
}, [index, memes.length, navigation]);
|
||||
@@ -93,19 +94,19 @@ const MemeView = ({
|
||||
<Appbar.Header style={memeViewStyles.header}>
|
||||
<Appbar.BackAction onPress={() => navigation.goBack()} />
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
|
||||
<Appbar.Content title={memes[index.current]?.title} />
|
||||
<Appbar.Content title={memes[index]?.title} />
|
||||
</Appbar.Header>
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
key={height}
|
||||
data={memes}
|
||||
initialScrollIndex={index.current}
|
||||
initialScrollIndex={index}
|
||||
onScroll={event => {
|
||||
const newIndex = Math.round(
|
||||
event.nativeEvent.contentOffset.x /
|
||||
event.nativeEvent.layoutMeasurement.width,
|
||||
);
|
||||
if (newIndex !== index.current) index.current = newIndex;
|
||||
if (newIndex !== index) setIndex(newIndex);
|
||||
}}
|
||||
estimatedItemSize={width}
|
||||
estimatedListSize={{ height, width }}
|
||||
@@ -125,14 +126,14 @@ const MemeView = ({
|
||||
<Appbar style={memeViewStyles.footer}>
|
||||
<Appbar.Action
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
icon={memes[index.current]?.isFavorite ? 'heart' : 'heart-outline'}
|
||||
onPress={() => favoriteMeme(realm, memes[index.current])}
|
||||
icon={memes[index]?.isFavorite ? 'heart' : 'heart-outline'}
|
||||
onPress={() => favoriteMeme(realm, memes[index])}
|
||||
disabled={isBlocked}
|
||||
/>
|
||||
<Appbar.Action
|
||||
icon="share"
|
||||
onPress={() => {
|
||||
shareMeme(realm, storageUri, memes[index.current]).catch(() =>
|
||||
shareMeme(realm, storageUri, memes[index]).catch(() =>
|
||||
setSnackbarMessage('Failed to share meme!'),
|
||||
);
|
||||
}}
|
||||
@@ -141,7 +142,7 @@ const MemeView = ({
|
||||
<Appbar.Action
|
||||
icon="content-copy"
|
||||
onPress={async () => {
|
||||
await copyMeme(realm, storageUri, memes[index.current])
|
||||
await copyMeme(realm, storageUri, memes[index])
|
||||
.then(() => setSnackbarMessage('Meme copied!'))
|
||||
.catch(() => setSnackbarMessage('Failed to copy meme!'));
|
||||
}}
|
||||
@@ -150,7 +151,7 @@ const MemeView = ({
|
||||
<Appbar.Action
|
||||
icon="pencil"
|
||||
onPress={() => {
|
||||
editMeme(navigation, memes[index.current]);
|
||||
editMeme(navigation, memes[index]);
|
||||
}}
|
||||
disabled={isBlocked}
|
||||
/>
|
||||
@@ -158,7 +159,7 @@ const MemeView = ({
|
||||
icon="delete"
|
||||
onPress={async () => {
|
||||
setIsBlocked(true);
|
||||
await deleteMeme(realm, storageUri, memes[index.current]);
|
||||
await deleteMeme(realm, storageUri, memes[index]);
|
||||
setIsBlocked(false);
|
||||
}}
|
||||
disabled={isBlocked}
|
@@ -3,11 +3,11 @@ import { StyleSheet, View } from 'react-native';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { AndroidScoped } from 'react-native-file-access';
|
||||
import { useSelector } from 'react-redux';
|
||||
import Video from 'react-native-video';
|
||||
import { MEME_TYPE, Meme } from '../../database';
|
||||
import { RootState } from '../../state';
|
||||
import { AnimatedImage, LoadingView, MemeFail } from '..';
|
||||
import { AnimatedImage, LoadingView, MemeFail } from '../../components';
|
||||
import { useMemeDimensions } from '../../hooks';
|
||||
import Video from 'react-native-video';
|
||||
|
||||
const memeViewItemStyles = StyleSheet.create({
|
||||
view: {
|
||||
@@ -44,11 +44,23 @@ const MemeViewItem = ({ meme }: { meme: Meme }) => {
|
||||
switch (meme.memeType) {
|
||||
case MEME_TYPE.IMAGE:
|
||||
case MEME_TYPE.GIF: {
|
||||
return <AnimatedImage source={{ uri }} style={dimensionStyles} />;
|
||||
return (
|
||||
<AnimatedImage
|
||||
source={{ uri }}
|
||||
style={dimensionStyles}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return (
|
||||
<Video source={{ uri }} style={dimensionStyles} paused controls />
|
||||
<Video
|
||||
source={{ uri }}
|
||||
style={dimensionStyles}
|
||||
resizeMode="contain"
|
||||
paused
|
||||
controls
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
@@ -17,11 +17,13 @@ import {
|
||||
useNavigation,
|
||||
} from '@react-navigation/native';
|
||||
import { NativeStackNavigationProp } from '@react-navigation/native-stack';
|
||||
import { ROUTE, SORT_DIRECTION, memesSortQuery } from '../types';
|
||||
import { RootState, setNavVisible } from '../state';
|
||||
import { Meme } from '../database';
|
||||
import { HideableHeader, MemesHeader, MemesList } from '../components';
|
||||
import { useDeviceOrientation } from '@react-native-community/hooks';
|
||||
import { ROUTE, SORT_DIRECTION, memesSortQuery } from '../../types';
|
||||
import { RootState, setNavVisible } from '../../state';
|
||||
import { Meme } from '../../database';
|
||||
import { HideableHeader } from '../../components';
|
||||
import MemesHeader from './memesHeader';
|
||||
import MemesList from './memesList/memesList';
|
||||
|
||||
const memesStyles = StyleSheet.create({
|
||||
listView: {
|
74
src/screens/memes/memesList/memesGridItem.tsx
Normal file
74
src/screens/memes/memesList/memesGridItem.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { TouchableHighlight } from 'react-native';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
|
||||
import { MEME_TYPE, Meme } from '../../../database';
|
||||
import { MemeFail, ThemedSkeletonPlaceholder } from '../../../components';
|
||||
import { getFontAwesome5IconSize } from '../../../utilities';
|
||||
import { useMemeDimensions } from '../../../hooks';
|
||||
|
||||
const MemesGridItem = ({
|
||||
meme,
|
||||
focusMeme,
|
||||
uri,
|
||||
columns,
|
||||
}: {
|
||||
meme: Meme;
|
||||
focusMeme: () => void;
|
||||
uri: string;
|
||||
columns: number;
|
||||
}) => {
|
||||
const { width } = useSafeAreaFrame();
|
||||
|
||||
const itemWidth = useMemo(
|
||||
() => (width * 0.92 - 5) / columns,
|
||||
[columns, width],
|
||||
);
|
||||
|
||||
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
|
||||
|
||||
const mediaComponent = useMemo(() => {
|
||||
switch (meme.memeType) {
|
||||
case MEME_TYPE.IMAGE:
|
||||
case MEME_TYPE.GIF:
|
||||
case MEME_TYPE.VIDEO: {
|
||||
return (
|
||||
<FastImage
|
||||
source={{ uri }}
|
||||
style={{ width: itemWidth, height: itemWidth }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
default: {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
}, [itemWidth, meme.memeType, uri]);
|
||||
|
||||
const skeletonComponent = useMemo(
|
||||
() => (
|
||||
<ThemedSkeletonPlaceholder>
|
||||
<SkeletonPlaceholder.Item width={itemWidth} height={itemWidth} />
|
||||
</ThemedSkeletonPlaceholder>
|
||||
),
|
||||
[itemWidth],
|
||||
);
|
||||
|
||||
if (!error && (loading || !dimensions)) return skeletonComponent;
|
||||
|
||||
return (
|
||||
<TouchableHighlight onPress={focusMeme}>
|
||||
{error ? (
|
||||
<MemeFail
|
||||
style={{ width: itemWidth, height: itemWidth }}
|
||||
iconSize={getFontAwesome5IconSize(columns)}
|
||||
/>
|
||||
) : (
|
||||
mediaComponent
|
||||
)}
|
||||
</TouchableHighlight>
|
||||
);
|
||||
};
|
||||
|
||||
export default MemesGridItem;
|
@@ -15,6 +15,7 @@ import { getFlashListItemHeight } from '../../../utilities';
|
||||
import MemesMasonryItem from './memesMasonryItem';
|
||||
import MemesGridItem from './memesGridItem';
|
||||
import MemesListItem from './memesListItem';
|
||||
import { AndroidScoped } from 'react-native-file-access';
|
||||
|
||||
const sharedMemesListStyles = StyleSheet.create({
|
||||
flashList: {
|
||||
@@ -66,6 +67,10 @@ const MemesList = ({
|
||||
const gridColumns = useSelector(
|
||||
(state: RootState) => state.settings.gridColumns,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const storageUri = useSelector(
|
||||
(state: RootState) => state.settings.storageUri,
|
||||
)!;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -82,7 +87,12 @@ const MemesList = ({
|
||||
numColumns={masonryColumns}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme, index }) => (
|
||||
<MemesMasonryItem meme={meme} index={index} focusMeme={focusMeme} />
|
||||
<MemesMasonryItem
|
||||
meme={meme}
|
||||
focusMeme={() => focusMeme(index)}
|
||||
uri={AndroidScoped.appendPath(storageUri, meme.filename)}
|
||||
columns={masonryColumns}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={{
|
||||
paddingTop: flashListPadding,
|
||||
@@ -96,6 +106,7 @@ const MemesList = ({
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
fadingEdgeLength={100}
|
||||
overScrollMode="never"
|
||||
/>
|
||||
)}
|
||||
{view === VIEW.GRID && (
|
||||
@@ -111,7 +122,12 @@ const MemesList = ({
|
||||
numColumns={gridColumns}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme, index }) => (
|
||||
<MemesGridItem meme={meme} index={index} focusMeme={focusMeme} />
|
||||
<MemesGridItem
|
||||
meme={meme}
|
||||
focusMeme={() => focusMeme(index)}
|
||||
uri={AndroidScoped.appendPath(storageUri, meme.filename)}
|
||||
columns={gridColumns}
|
||||
/>
|
||||
)}
|
||||
contentContainerStyle={{
|
||||
paddingTop: flashListPadding,
|
||||
@@ -125,20 +141,25 @@ const MemesList = ({
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
fadingEdgeLength={100}
|
||||
overScrollMode="never"
|
||||
/>
|
||||
)}
|
||||
{view === VIEW.LIST && (
|
||||
<FlashList
|
||||
ref={flashListRef}
|
||||
data={memes}
|
||||
estimatedItemSize={50}
|
||||
estimatedItemSize={90}
|
||||
estimatedListSize={{
|
||||
height: height,
|
||||
width: width * 0.92,
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
renderItem={({ item: meme, index }) => (
|
||||
<MemesListItem meme={meme} index={index} focusMeme={focusMeme} />
|
||||
<MemesListItem
|
||||
meme={meme}
|
||||
focusMeme={() => focusMeme(index)}
|
||||
uri={AndroidScoped.appendPath(storageUri, meme.filename)}
|
||||
/>
|
||||
)}
|
||||
ItemSeparatorComponent={() => <Divider />}
|
||||
contentContainerStyle={{
|
||||
@@ -153,6 +174,7 @@ const MemesList = ({
|
||||
)}
|
||||
onScroll={handleScroll}
|
||||
fadingEdgeLength={100}
|
||||
overScrollMode="never"
|
||||
/>
|
||||
)}
|
||||
</>
|
@@ -1,12 +1,11 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Image, StyleSheet, View } from 'react-native';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { Text, TouchableRipple } from 'react-native-paper';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { AndroidScoped } from 'react-native-file-access';
|
||||
import { useSelector } from 'react-redux';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
|
||||
import { MEME_TYPE, Meme } from '../../../database';
|
||||
import { MemeFail } from '..';
|
||||
import { RootState } from '../../../state';
|
||||
import { MemeFail, ThemedSkeletonPlaceholder } from '../../../components';
|
||||
import { useMemeDimensions } from '../../../hooks';
|
||||
|
||||
const memesListItemStyles = StyleSheet.create({
|
||||
@@ -35,20 +34,19 @@ const memesListItemStyles = StyleSheet.create({
|
||||
|
||||
const MemesListItem = ({
|
||||
meme,
|
||||
index,
|
||||
focusMeme,
|
||||
uri,
|
||||
}: {
|
||||
meme: Meme;
|
||||
index: number;
|
||||
focusMeme: (index: number) => void;
|
||||
focusMeme: () => void;
|
||||
uri: string;
|
||||
}) => {
|
||||
const { width } = useSafeAreaFrame();
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const storageUri = useSelector(
|
||||
(state: RootState) => state.settings.storageUri,
|
||||
)!;
|
||||
|
||||
const uri = AndroidScoped.appendPath(storageUri, meme.filename);
|
||||
const listItemWidth = useMemo(
|
||||
() => ({ width: width * 0.92 - 75 - 10 }),
|
||||
[width],
|
||||
);
|
||||
|
||||
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
|
||||
|
||||
@@ -57,7 +55,7 @@ const MemesListItem = ({
|
||||
case MEME_TYPE.IMAGE:
|
||||
case MEME_TYPE.GIF:
|
||||
case MEME_TYPE.VIDEO: {
|
||||
return <Image source={{ uri }} style={[memesListItemStyles.image]} />;
|
||||
return <FastImage source={{ uri }} style={memesListItemStyles.image} />;
|
||||
}
|
||||
default: {
|
||||
return <></>;
|
||||
@@ -65,25 +63,29 @@ const MemesListItem = ({
|
||||
}
|
||||
}, [meme.memeType, uri]);
|
||||
|
||||
if (!error && (loading || !dimensions)) return <></>;
|
||||
const skeletonComponent = useMemo(
|
||||
() => (
|
||||
<ThemedSkeletonPlaceholder>
|
||||
<SkeletonPlaceholder.Item
|
||||
width={listItemWidth.width + 75}
|
||||
height={90}
|
||||
/>
|
||||
</ThemedSkeletonPlaceholder>
|
||||
),
|
||||
[listItemWidth.width],
|
||||
);
|
||||
|
||||
if (!error && (loading || !dimensions)) return skeletonComponent;
|
||||
|
||||
return (
|
||||
<TouchableRipple
|
||||
onPress={() => focusMeme(index)}
|
||||
style={memesListItemStyles.view}>
|
||||
<TouchableRipple onPress={focusMeme} style={memesListItemStyles.view}>
|
||||
<>
|
||||
{error ? (
|
||||
<MemeFail style={memesListItemStyles.image} />
|
||||
) : (
|
||||
mediaComponent
|
||||
)}
|
||||
<View
|
||||
style={[
|
||||
memesListItemStyles.detailsView,
|
||||
{
|
||||
width: width * 0.92 - 75 - 10,
|
||||
},
|
||||
]}>
|
||||
<View style={[memesListItemStyles.detailsView, listItemWidth]}>
|
||||
<Text variant="titleMedium" style={memesListItemStyles.text}>
|
||||
{meme.title}
|
||||
</Text>
|
@@ -1,11 +1,10 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { Image, StyleSheet, TouchableHighlight } from 'react-native';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { StyleSheet, TouchableHighlight } from 'react-native';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { AndroidScoped } from 'react-native-file-access';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
|
||||
import { MEME_TYPE, Meme } from '../../../database';
|
||||
import { RootState } from '../../../state';
|
||||
import { MemeFail } from '..';
|
||||
import { MemeFail, ThemedSkeletonPlaceholder } from '../../../components';
|
||||
import { getFontAwesome5IconSize } from '../../../utilities';
|
||||
import { useMemeDimensions } from '../../../hooks';
|
||||
|
||||
@@ -21,29 +20,28 @@ const memeMasonryItemStyles = StyleSheet.create({
|
||||
|
||||
const MemesMasonryItem = ({
|
||||
meme,
|
||||
index,
|
||||
focusMeme,
|
||||
uri,
|
||||
columns,
|
||||
}: {
|
||||
meme: Meme;
|
||||
index: number;
|
||||
focusMeme: (index: number) => void;
|
||||
focusMeme: () => void;
|
||||
uri: string;
|
||||
columns: number;
|
||||
}) => {
|
||||
const { width } = useSafeAreaFrame();
|
||||
const masonryColumns = useSelector(
|
||||
(state: RootState) => state.settings.masonryColumns,
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const storageUri = useSelector(
|
||||
(state: RootState) => state.settings.storageUri,
|
||||
)!;
|
||||
|
||||
const uri = AndroidScoped.appendPath(storageUri, meme.filename);
|
||||
|
||||
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
|
||||
|
||||
const itemWidth = (width * 0.92 - 5) / masonryColumns - 5;
|
||||
const itemHeight =
|
||||
((width * 0.92) / masonryColumns - 5) / (dimensions?.aspectRatio ?? 1);
|
||||
const itemWidth = useMemo(
|
||||
() => (width * 0.92 - 5) / columns - 5,
|
||||
[columns, width],
|
||||
);
|
||||
|
||||
const itemHeight = useMemo(
|
||||
() => itemWidth / (dimensions?.aspectRatio ?? 1),
|
||||
[dimensions?.aspectRatio, itemWidth],
|
||||
);
|
||||
|
||||
const mediaComponent = useMemo(() => {
|
||||
switch (meme.memeType) {
|
||||
@@ -51,14 +49,11 @@ const MemesMasonryItem = ({
|
||||
case MEME_TYPE.GIF:
|
||||
case MEME_TYPE.VIDEO: {
|
||||
return (
|
||||
<Image
|
||||
<FastImage
|
||||
source={{ uri }}
|
||||
style={[
|
||||
memeMasonryItemStyles.image,
|
||||
{
|
||||
width: itemWidth,
|
||||
height: itemHeight,
|
||||
},
|
||||
{ width: itemWidth, height: itemHeight },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
@@ -69,19 +64,30 @@ const MemesMasonryItem = ({
|
||||
}
|
||||
}, [itemHeight, itemWidth, meme.memeType, uri]);
|
||||
|
||||
if (!error && (loading || !dimensions)) return <></>;
|
||||
const skeletonComponent = useMemo(
|
||||
() => (
|
||||
<ThemedSkeletonPlaceholder borderRadius={5}>
|
||||
<SkeletonPlaceholder.Item
|
||||
width={itemWidth}
|
||||
height={itemWidth}
|
||||
style={memeMasonryItemStyles.view}
|
||||
/>
|
||||
</ThemedSkeletonPlaceholder>
|
||||
),
|
||||
[itemWidth],
|
||||
);
|
||||
|
||||
if (!error && (loading || !dimensions)) return skeletonComponent;
|
||||
|
||||
return (
|
||||
<TouchableHighlight
|
||||
onPress={() => focusMeme(index)}
|
||||
style={memeMasonryItemStyles.view}>
|
||||
<TouchableHighlight onPress={focusMeme} style={memeMasonryItemStyles.view}>
|
||||
{error || !dimensions ? (
|
||||
<MemeFail
|
||||
style={[
|
||||
memeMasonryItemStyles.image,
|
||||
{ width: itemWidth, height: itemHeight },
|
||||
]}
|
||||
iconSize={getFontAwesome5IconSize(masonryColumns)}
|
||||
iconSize={getFontAwesome5IconSize(columns)}
|
||||
/>
|
||||
) : (
|
||||
mediaComponent
|
@@ -11,6 +11,8 @@ import {
|
||||
} from 'react-native-paper';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import type {} from 'redux-thunk/extend-redux';
|
||||
import { useRealm } from '@realm/react';
|
||||
import { FileSystem, FileStat } from 'react-native-file-access';
|
||||
import {
|
||||
RootState,
|
||||
setAutofocusMemesSearch,
|
||||
@@ -19,11 +21,9 @@ import {
|
||||
setMasonryColumns,
|
||||
setNoMedia,
|
||||
setSnackbarMessage,
|
||||
} from '../state';
|
||||
import StorageLocationChangeDialog from '../components/storageLocationChangeDialog';
|
||||
import { useRealm } from '@realm/react';
|
||||
import { FileSystem, FileStat } from 'react-native-file-access';
|
||||
import { Meme } from '../database';
|
||||
} from '../../state';
|
||||
import { Meme } from '../../database';
|
||||
import StorageLocationChangeDialog from './storageLocationChangeDialog';
|
||||
|
||||
const settingsStyles = StyleSheet.create({
|
||||
scrollView: {
|
@@ -4,8 +4,8 @@ import { Dialog, ProgressBar, Text } from 'react-native-paper';
|
||||
import { openDocumentTree } from 'react-native-scoped-storage';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { AndroidScoped, FileSystem } from 'react-native-file-access';
|
||||
import { RootState, setStorageUri } from '../state';
|
||||
import { clearPermissions, isPermissionForPath, noOp } from '../utilities';
|
||||
import { RootState, setStorageUri } from '../../state';
|
||||
import { clearPermissions, isPermissionForPath, noOp } from '../../utilities';
|
||||
|
||||
const storageLocationChangeDialogStyles = StyleSheet.create({
|
||||
progressBar: {
|
@@ -13,10 +13,12 @@ import { FlashList } from '@shopify/flash-list';
|
||||
import { useFocusEffect } from '@react-navigation/native';
|
||||
import { useDeviceOrientation } from '@react-native-community/hooks';
|
||||
import { useSafeAreaFrame } from 'react-native-safe-area-context';
|
||||
import { HideableHeader, TagRow, TagsHeader } from '../components';
|
||||
import { Tag } from '../database';
|
||||
import { RootState, setNavVisible } from '../state';
|
||||
import { SORT_DIRECTION, tagSortQuery } from '../types';
|
||||
import { HideableHeader } from '../../components';
|
||||
import { Tag } from '../../database';
|
||||
import { RootState, setNavVisible } from '../../state';
|
||||
import { SORT_DIRECTION, tagSortQuery } from '../../types';
|
||||
import TagsHeader from './tagsHeader';
|
||||
import TagRow from './tagsList/tagRow';
|
||||
|
||||
const tagsStyles = StyleSheet.create({
|
||||
listView: {
|
@@ -2,9 +2,9 @@ import React from 'react';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import { TouchableRipple, Text } from 'react-native-paper';
|
||||
import { useNavigation, NavigationProp } from '@react-navigation/native';
|
||||
import { Tag } from '../../database';
|
||||
import { ROUTE, RootStackParamList } from '../../types';
|
||||
import { TagChip } from '.';
|
||||
import { Tag } from '../../../database';
|
||||
import { ROUTE, RootStackParamList } from '../../../types';
|
||||
import { TagChip } from '../../../components';
|
||||
|
||||
const tagRowStyles = StyleSheet.create({
|
||||
view: {
|
@@ -1,6 +1,6 @@
|
||||
import { DocumentPickerResponse } from 'react-native-document-picker';
|
||||
import { getFilenameFromUri } from '../utilities';
|
||||
import { SharedItem } from './share';
|
||||
import { SharedItem } from '.';
|
||||
|
||||
enum ROUTE {
|
||||
MAIN = 'Main',
|
||||
|
@@ -4,7 +4,7 @@ import Share from 'react-native-share';
|
||||
import Clipboard from '@react-native-clipboard/clipboard';
|
||||
import { Meme } from '../database';
|
||||
import { ROUTE, RootStackParamList } from '../types';
|
||||
import { noOp } from './constants';
|
||||
import { noOp } from '.';
|
||||
|
||||
const favoriteMeme = (realm: Realm, meme: Meme) => {
|
||||
realm.write(() => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { isHexColor, isRgbColor } from './color';
|
||||
import { isHexColor, isRgbColor } from '.';
|
||||
|
||||
interface StringValidationResult {
|
||||
valid: boolean;
|
||||
|
Reference in New Issue
Block a user