6 Commits

Author SHA1 Message Date
231c9b0d79 Add skeleton placeholders
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2023-08-04 14:15:03 +03:00
1b09b058e4 Improve performance
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2023-08-04 13:39:55 +03:00
5958cf57ee Fix crash when creating tag inside meme editor
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2023-08-04 12:21:41 +03:00
e550fcd881 Fix a couple of bugs
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2023-08-03 21:01:25 +03:00
665931f7b9 Add text recognition
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2023-08-03 14:20:23 +03:00
d2054b028a Reorganize files
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2023-08-01 14:53:10 +03:00
38 changed files with 483 additions and 258 deletions

82
package-lock.json generated
View File

@@ -12,6 +12,8 @@
"@bankify/redux-persist-realm": "^0.1.3", "@bankify/redux-persist-realm": "^0.1.3",
"@react-native-clipboard/clipboard": "^1.11.2", "@react-native-clipboard/clipboard": "^1.11.2",
"@react-native-community/hooks": "^3.0.0", "@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/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7", "@react-navigation/native": "^6.1.7",
"@react-navigation/native-stack": "^6.9.13", "@react-navigation/native-stack": "^6.9.13",
@@ -22,9 +24,11 @@
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.72.2", "react-native": "0.72.2",
"react-native-document-picker": "^9.0.1", "react-native-document-picker": "^9.0.1",
"react-native-fast-image": "^8.6.3",
"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-linear-gradient": "^2.8.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",
@@ -33,6 +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-share-menu": "^6.0.0", "react-native-share-menu": "^6.0.0",
"react-native-skeleton-placeholder": "^5.2.4",
"react-native-vector-icons": "^9.2.0", "react-native-vector-icons": "^9.2.0",
"react-native-video": "^6.0.0-alpha.6", "react-native-video": "^6.0.0-alpha.6",
"react-redux": "^8.1.1", "react-redux": "^8.1.1",
@@ -4113,6 +4118,24 @@
"react-native": ">=0.65" "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": { "node_modules/@react-native/assets-registry": {
"version": "0.72.0", "version": "0.72.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz", "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": { "node_modules/react-native-file-access": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz", "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" "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": { "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",
@@ -13572,6 +13613,17 @@
"resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz", "resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz",
"integrity": "sha512-KdmRnqjI/B2MigSxGmhbYJ3WMJxKXj+0c47ANcVZ/PTzc2vtz6d1r4KQJgkBImXgNC+vowpuD2UGdPllxadr2A==" "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": { "node_modules/react-native-vector-icons": {
"version": "9.2.0", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz", "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==", "integrity": "sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==",
"requires": {} "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": { "@react-native/assets-registry": {
"version": "0.72.0", "version": "0.72.0",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.72.0.tgz",
@@ -25845,6 +25909,12 @@
"invariant": "^2.2.4" "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": { "react-native-file-access": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/react-native-file-access/-/react-native-file-access-3.0.4.tgz", "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" "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": { "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",
@@ -25957,6 +26033,12 @@
"resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz", "resolved": "https://registry.npmjs.org/react-native-share-menu/-/react-native-share-menu-6.0.0.tgz",
"integrity": "sha512-KdmRnqjI/B2MigSxGmhbYJ3WMJxKXj+0c47ANcVZ/PTzc2vtz6d1r4KQJgkBImXgNC+vowpuD2UGdPllxadr2A==" "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": { "react-native-vector-icons": {
"version": "9.2.0", "version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz", "resolved": "https://registry.npmjs.org/react-native-vector-icons/-/react-native-vector-icons-9.2.0.tgz",

View File

@@ -17,6 +17,8 @@
"@bankify/redux-persist-realm": "^0.1.3", "@bankify/redux-persist-realm": "^0.1.3",
"@react-native-clipboard/clipboard": "^1.11.2", "@react-native-clipboard/clipboard": "^1.11.2",
"@react-native-community/hooks": "^3.0.0", "@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/bottom-tabs": "^6.5.8",
"@react-navigation/native": "^6.1.7", "@react-navigation/native": "^6.1.7",
"@react-navigation/native-stack": "^6.9.13", "@react-navigation/native-stack": "^6.9.13",
@@ -27,9 +29,11 @@
"react": "18.2.0", "react": "18.2.0",
"react-native": "0.72.2", "react-native": "0.72.2",
"react-native-document-picker": "^9.0.1", "react-native-document-picker": "^9.0.1",
"react-native-fast-image": "^8.6.3",
"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-linear-gradient": "^2.8.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",
@@ -38,6 +42,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-share-menu": "^6.0.0", "react-native-share-menu": "^6.0.0",
"react-native-skeleton-placeholder": "^5.2.4",
"react-native-vector-icons": "^9.2.0", "react-native-vector-icons": "^9.2.0",
"react-native-video": "^6.0.0-alpha.6", "react-native-video": "^6.0.0-alpha.6",
"react-redux": "^8.1.1", "react-redux": "^8.1.1",

View File

@@ -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 AnimatedImage } from './animatedImage';
export { default as FloatingActionButton } from './floatingActionButton'; export { default as FloatingActionButton } from './floatingActionButton';
export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar'; export { default as HideableBottomNavigationBar } from './hideableBottomNavigationBar';
export { default as HideableHeader } from './hideableHeader'; export { default as HideableHeader } from './hideableHeader';
export { default as LoadingView } from './loadingView'; 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';

View File

@@ -2,7 +2,7 @@ import React, { ComponentProps } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { useTheme } from 'react-native-paper'; import { useTheme } from 'react-native-paper';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { rgbToRgba } from '../../utilities'; import { rgbToRgba } from '../utilities';
const memeFailStyles = StyleSheet.create({ const memeFailStyles = StyleSheet.create({
view: { view: {

View File

@@ -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';

View File

@@ -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;

View File

@@ -1,9 +1,9 @@
import React, { ComponentProps, useMemo } from 'react'; import React, { ComponentProps, useMemo } from 'react';
import { getContrastColor } from '../../utilities';
import { Chip, useTheme } from 'react-native-paper'; import { Chip, useTheme } from 'react-native-paper';
import { Tag } from '../../database';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import { getContrastColor } from '../utilities';
import { Tag } from '../database';
const tagChipStyles = StyleSheet.create({ const tagChipStyles = StyleSheet.create({
chip: { chip: {

View File

@@ -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';

View 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;

View 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;

View File

@@ -1,5 +1,5 @@
import { BSON, Object, ObjectSchema } from 'realm'; import { BSON, Object, ObjectSchema } from 'realm';
import { Tag } from './tag'; import { Tag } from '.';
enum MEME_TYPE { enum MEME_TYPE {
IMAGE = 'Image', IMAGE = 'Image',

View File

@@ -1,5 +1,5 @@
import { BSON, Object, ObjectSchema } from 'realm'; import { BSON, Object, ObjectSchema } from 'realm';
import { Meme } from './meme'; import { Meme } from '.';
import { generateRandomColor } from '../utilities'; import { generateRandomColor } from '../utilities';
class Tag extends Object<Tag> { class Tag extends Object<Tag> {

View File

@@ -15,17 +15,17 @@ import {
ROUTE, ROUTE,
RootStackParamList, RootStackParamList,
StagingMeme, StagingMeme,
} from '../../types'; } from '../../../types';
import { Meme, Tag } from '../../database'; import { Meme, Tag } from '../../../database';
import { RootState } from '../../state'; import { RootState } from '../../../state';
import { import {
allowedMimeTypes, allowedMimeTypes,
getMemeTypeFromMimeType, getMemeTypeFromMimeType,
guessMimeType, guessMimeType,
validateMemeTitle, validateMemeTitle,
} from '../../utilities'; } from '../../../utilities';
import { MemeEditor } from '../../components'; import MemeEditor from './memeEditor';
import editorStyles from './editorStyles'; import editorStyles from '../editorStyles';
const AddMeme = ({ const AddMeme = ({
route, route,

View File

@@ -6,12 +6,12 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { useObject, useRealm } from '@realm/react'; import { useObject, useRealm } from '@realm/react';
import { useDeviceOrientation } from '@react-native-community/hooks'; import { useDeviceOrientation } from '@react-native-community/hooks';
import { BSON } from 'realm'; import { BSON } from 'realm';
import { RootStackParamList, ROUTE, StagingMeme } from '../../types';
import { pickSingle } from 'react-native-document-picker'; import { pickSingle } from 'react-native-document-picker';
import { AndroidScoped, FileSystem } from 'react-native-file-access'; import { AndroidScoped, FileSystem } from 'react-native-file-access';
import { extension } from 'react-native-mime-types'; import { extension } from 'react-native-mime-types';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Meme } from '../../database'; import { RootStackParamList, ROUTE, StagingMeme } from '../../../types';
import { Meme } from '../../../database';
import { import {
allowedMimeTypes, allowedMimeTypes,
deleteMeme, deleteMeme,
@@ -20,10 +20,10 @@ import {
guessMimeType, guessMimeType,
noOp, noOp,
validateMemeTitle, validateMemeTitle,
} from '../../utilities'; } from '../../../utilities';
import { MemeEditor } from '../../components'; import { RootState } from '../../../state';
import editorStyles from './editorStyles'; import MemeEditor from './memeEditor';
import { RootState } from '../../state'; import editorStyles from '../editorStyles';
const EditMeme = ({ const EditMeme = ({
route, route,

View File

@@ -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 { 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 { 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 { import {
getFilenameFromUri, getFilenameFromUri,
getMemeTypeFromMimeType, getMemeTypeFromMimeType,
validateMemeTitle, validateMemeTitle,
} from '../../utilities'; } from '../../../utilities';
import { StagingMeme } from '../../types'; import { StagingMeme } from '../../../types';
import { useMemeDimensions } from '../../hooks'; import { useMemeDimensions } from '../../../hooks';
import { MEME_TYPE } from '../../database'; import { MEME_TYPE } from '../../../database';
import Video from 'react-native-video'; import MemeTagSelector from './memeTagSelector/memeTagSelector';
const memeEditorStyles = { const memeEditorStyles = {
media: { media: {
@@ -65,8 +70,15 @@ const MemeEditor = ({
useMemo(() => (errorIn: Error) => setError(errorIn), [setError]), 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(() => { const mediaComponent = useMemo(() => {
if (!mimeType || !dimensions) return <></>; if (!mimeType || !dimensions || !staging) return <></>;
const dimensionStyles = { const dimensionStyles = {
width: width * 0.92, width: width * 0.92,
@@ -83,11 +95,35 @@ const MemeEditor = ({
case MEME_TYPE.IMAGE: case MEME_TYPE.IMAGE:
case MEME_TYPE.GIF: { case MEME_TYPE.GIF: {
return ( return (
<Image <View>
<FastImage
source={{ uri }} source={{ uri }}
style={[memeEditorStyles.media, dimensionStyles]} style={[memeEditorStyles.media, dimensionStyles]}
resizeMode="contain" 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: { case MEME_TYPE.VIDEO: {
@@ -104,7 +140,7 @@ const MemeEditor = ({
return <></>; return <></>;
} }
} }
}, [dimensions, mimeType, uri, width]); }, [dimensions, mimeType, recognizedText, setStaging, staging, uri, width]);
if (!uri || !mimeType || !staging) return <LoadingView />; if (!uri || !mimeType || !staging) return <LoadingView />;

View File

@@ -4,10 +4,10 @@ import { Chip, Modal, Portal, Searchbar, useTheme } from 'react-native-paper';
import { LayoutAnimation, StyleSheet } from 'react-native'; import { LayoutAnimation, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { TAG_SORT, tagSortQuery } from '../../../types'; import { TAG_SORT, tagSortQuery } from '../../../../types';
import { TagChip } from '../../tags'; import { TagChip } from '../../../../components';
import { Tag } from '../../../database'; import { Tag } from '../../../../database';
import { validateTagName } from '../../../utilities'; import { validateTagName } from '../../../../utilities';
const memeTagSearchModalStyles = StyleSheet.create({ const memeTagSearchModalStyles = StyleSheet.create({
modal: { modal: {
@@ -87,6 +87,8 @@ const MemeTagSearchModal = ({
[search], [search],
); );
const [refreshKey, setRefreshKey] = useState(0);
const handleTagPress = (tag: Tag) => { const handleTagPress = (tag: Tag) => {
const id = tag.id.toHexString(); const id = tag.id.toHexString();
memeTags.delete(id) || memeTags.set(id, tag); memeTags.delete(id) || memeTags.set(id, tag);
@@ -105,6 +107,7 @@ const MemeTagSearchModal = ({
if (!tag) return; if (!tag) return;
memeTags.set(tag.id.toHexString(), tag); memeTags.set(tag.id.toHexString(), tag);
setMemeTags(new Map(memeTags)); setMemeTags(new Map(memeTags));
setRefreshKey(refreshKey + 1);
flashListRef.current?.prepareForLayoutAnimationRender(); flashListRef.current?.prepareForLayoutAnimationRender();
LayoutAnimation.configureNext(tagLayoutAnimation); LayoutAnimation.configureNext(tagLayoutAnimation);
}; };
@@ -129,6 +132,7 @@ const MemeTagSearchModal = ({
/> />
<FlashList <FlashList
ref={flashListRef} ref={flashListRef}
key={refreshKey}
data={tags} data={tags}
extraData={memeTags} extraData={memeTags}
keyExtractor={tag => tag.id.toHexString()} keyExtractor={tag => tag.id.toHexString()}
@@ -148,7 +152,7 @@ const MemeTagSearchModal = ({
active={memeTags.has(tag.id.toHexString())} active={memeTags.has(tag.id.toHexString())}
/> />
)} )}
ListEmptyComponent={() => ( ListFooterComponent={() => (
<Chip <Chip
icon="plus" icon="plus"
mode="outlined" mode="outlined"

View File

@@ -3,8 +3,8 @@ import { LayoutAnimation, StyleSheet, View } from 'react-native';
import { Chip } from 'react-native-paper'; import { Chip } from 'react-native-paper';
import { FlashList } from '@shopify/flash-list'; import { FlashList } from '@shopify/flash-list';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { TagChip } from '../../tags'; import { TagChip } from '../../../../components';
import { Tag } from '../../../database'; import { Tag } from '../../../../database';
import MemeTagSearchModal from './memeTagSearchModal'; import MemeTagSearchModal from './memeTagSearchModal';
const memeTagSelectorStyles = StyleSheet.create({ const memeTagSelectorStyles = StyleSheet.create({

View File

@@ -8,11 +8,11 @@ import {
generateRandomColor, generateRandomColor,
validateColor, validateColor,
validateTagName, validateTagName,
} from '../../utilities'; } from '../../../utilities';
import { Tag } from '../../database'; import { Tag } from '../../../database';
import { TagEditor } from '../../components'; import { StagingTag } from '../../../types';
import editorStyles from './editorStyles'; import TagEditor from './tagEditor';
import { StagingTag } from '../../types'; import editorStyles from '../editorStyles';
const AddTag = () => { const AddTag = () => {
const { goBack } = useNavigation(); const { goBack } = useNavigation();

View File

@@ -6,11 +6,11 @@ import { NativeStackScreenProps } from '@react-navigation/native-stack';
import { BSON } from 'realm'; import { BSON } from 'realm';
import { useObject, useRealm } from '@realm/react'; import { useObject, useRealm } from '@realm/react';
import { useDeviceOrientation } from '@react-native-community/hooks'; import { useDeviceOrientation } from '@react-native-community/hooks';
import { TagEditor } from '../../components'; import { ROUTE, RootStackParamList, StagingTag } from '../../../types';
import { ROUTE, RootStackParamList, StagingTag } from '../../types'; import { Tag } from '../../../database';
import { Tag } from '../../database'; import { deleteTag, validateColor, validateTagName } from '../../../utilities';
import { deleteTag, validateColor, validateTagName } from '../../utilities'; import TagEditor from './tagEditor';
import editorStyles from './editorStyles'; import editorStyles from '../editorStyles';
const EditTag = ({ const EditTag = ({
route, route,

View File

@@ -1,12 +1,12 @@
import React, { useEffect, useRef } from 'react'; import React, { useEffect, useRef } from 'react';
import { HelperText, TextInput } from 'react-native-paper'; import { HelperText, TextInput } from 'react-native-paper';
import TagPreview from './tagPreview';
import { import {
generateRandomColor, generateRandomColor,
validateColor, validateColor,
validateTagName, validateTagName,
} from '../../utilities'; } from '../../../utilities';
import { StagingTag } from '../../types'; import { StagingTag } from '../../../types';
import TagPreview from './tagPreview';
const TagEditor = ({ const TagEditor = ({
staging, staging,

View File

@@ -2,7 +2,7 @@ import React, { useMemo } from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5'; import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import { Chip, useTheme } from 'react-native-paper'; import { Chip, useTheme } from 'react-native-paper';
import { getContrastColor } from '../../utilities'; import { getContrastColor } from '../../../utilities';
const tagPreviewStyles = StyleSheet.create({ const tagPreviewStyles = StyleSheet.create({
view: { view: {

View File

@@ -1,9 +1,9 @@
export { default as AddMeme } from './editors/addMeme'; export { default as AddMeme } from './editors/meme/addMeme';
export { default as AddTag } from './editors/addTag'; export { default as AddTag } from './editors/tag/addTag';
export { default as EditMeme } from './editors/editMeme'; export { default as EditMeme } from './editors/meme/editMeme';
export { default as EditTag } from './editors/editTag'; export { default as EditTag } from './editors/tag/editTag';
export { default as Memes } from './memes'; export { default as Memes } from './memes/memes';
export { default as MemeView } from './memeView'; export { default as MemeView } from './memeView/memeView';
export { default as Settings } from './settings'; export { default as Settings } from './settings/settings';
export { default as Tags } from './tags'; export { default as Tags } from './tags/tags';
export { default as Welcome } from './welcome'; export { default as Welcome } from './welcome';

View File

@@ -12,9 +12,9 @@ import {
RootStackParamList, RootStackParamList,
ROUTE, ROUTE,
SORT_DIRECTION, SORT_DIRECTION,
} from '../types'; } from '../../types';
import { Meme } from '../database'; import { Meme } from '../../database';
import { LoadingView, MemeViewItem } from '../components'; import { LoadingView } from '../../components';
import { import {
copyMeme, copyMeme,
deleteMeme, deleteMeme,
@@ -22,8 +22,9 @@ import {
favoriteMeme, favoriteMeme,
multipleIdQuery, multipleIdQuery,
shareMeme, shareMeme,
} from '../utilities'; } from '../../utilities';
import { RootState } from '../state'; import { RootState } from '../../state';
import MemeViewItem from './memeViewItem';
const memeViewStyles = StyleSheet.create({ const memeViewStyles = StyleSheet.create({
// eslint-disable-next-line react-native/no-color-literals // eslint-disable-next-line react-native/no-color-literals
@@ -70,7 +71,7 @@ const MemeView = ({
const flashListRef = useRef<FlashList<Meme>>(null); 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 => { const memes = useQuery<Meme>(Meme.schema.name, collectionIn => {
return collectionIn return collectionIn
.filtered(multipleIdQuery(route.params.ids)) .filtered(multipleIdQuery(route.params.ids))
@@ -82,8 +83,8 @@ const MemeView = ({
useEffect(() => { useEffect(() => {
if (memes.length === 0) navigation.goBack(); if (memes.length === 0) navigation.goBack();
if (index.current >= memes.length) { if (index >= memes.length) {
index.current = memes.length - 1; setIndex(memes.length - 1);
flashListRef.current?.scrollToIndex({ index: memes.length - 1 }); flashListRef.current?.scrollToIndex({ index: memes.length - 1 });
} }
}, [index, memes.length, navigation]); }, [index, memes.length, navigation]);
@@ -93,19 +94,19 @@ const MemeView = ({
<Appbar.Header style={memeViewStyles.header}> <Appbar.Header style={memeViewStyles.header}>
<Appbar.BackAction onPress={() => navigation.goBack()} /> <Appbar.BackAction onPress={() => navigation.goBack()} />
{/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */} {/* eslint-disable-next-line @typescript-eslint/no-unnecessary-condition */}
<Appbar.Content title={memes[index.current]?.title} /> <Appbar.Content title={memes[index]?.title} />
</Appbar.Header> </Appbar.Header>
<FlashList <FlashList
ref={flashListRef} ref={flashListRef}
key={height} key={height}
data={memes} data={memes}
initialScrollIndex={index.current} initialScrollIndex={index}
onScroll={event => { onScroll={event => {
const newIndex = Math.round( const newIndex = Math.round(
event.nativeEvent.contentOffset.x / event.nativeEvent.contentOffset.x /
event.nativeEvent.layoutMeasurement.width, event.nativeEvent.layoutMeasurement.width,
); );
if (newIndex !== index.current) index.current = newIndex; if (newIndex !== index) setIndex(newIndex);
}} }}
estimatedItemSize={width} estimatedItemSize={width}
estimatedListSize={{ height, width }} estimatedListSize={{ height, width }}
@@ -125,14 +126,14 @@ const MemeView = ({
<Appbar style={memeViewStyles.footer}> <Appbar style={memeViewStyles.footer}>
<Appbar.Action <Appbar.Action
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
icon={memes[index.current]?.isFavorite ? 'heart' : 'heart-outline'} icon={memes[index]?.isFavorite ? 'heart' : 'heart-outline'}
onPress={() => favoriteMeme(realm, memes[index.current])} onPress={() => favoriteMeme(realm, memes[index])}
disabled={isBlocked} disabled={isBlocked}
/> />
<Appbar.Action <Appbar.Action
icon="share" icon="share"
onPress={() => { onPress={() => {
shareMeme(realm, storageUri, memes[index.current]).catch(() => shareMeme(realm, storageUri, memes[index]).catch(() =>
setSnackbarMessage('Failed to share meme!'), setSnackbarMessage('Failed to share meme!'),
); );
}} }}
@@ -141,7 +142,7 @@ const MemeView = ({
<Appbar.Action <Appbar.Action
icon="content-copy" icon="content-copy"
onPress={async () => { onPress={async () => {
await copyMeme(realm, storageUri, memes[index.current]) await copyMeme(realm, storageUri, memes[index])
.then(() => setSnackbarMessage('Meme copied!')) .then(() => setSnackbarMessage('Meme copied!'))
.catch(() => setSnackbarMessage('Failed to copy meme!')); .catch(() => setSnackbarMessage('Failed to copy meme!'));
}} }}
@@ -150,7 +151,7 @@ const MemeView = ({
<Appbar.Action <Appbar.Action
icon="pencil" icon="pencil"
onPress={() => { onPress={() => {
editMeme(navigation, memes[index.current]); editMeme(navigation, memes[index]);
}} }}
disabled={isBlocked} disabled={isBlocked}
/> />
@@ -158,7 +159,7 @@ const MemeView = ({
icon="delete" icon="delete"
onPress={async () => { onPress={async () => {
setIsBlocked(true); setIsBlocked(true);
await deleteMeme(realm, storageUri, memes[index.current]); await deleteMeme(realm, storageUri, memes[index]);
setIsBlocked(false); setIsBlocked(false);
}} }}
disabled={isBlocked} disabled={isBlocked}

View File

@@ -3,11 +3,11 @@ import { StyleSheet, View } from 'react-native';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { AndroidScoped } from 'react-native-file-access'; import { AndroidScoped } from 'react-native-file-access';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import Video from 'react-native-video';
import { MEME_TYPE, Meme } from '../../database'; import { MEME_TYPE, Meme } from '../../database';
import { RootState } from '../../state'; import { RootState } from '../../state';
import { AnimatedImage, LoadingView, MemeFail } from '..'; import { AnimatedImage, LoadingView, MemeFail } from '../../components';
import { useMemeDimensions } from '../../hooks'; import { useMemeDimensions } from '../../hooks';
import Video from 'react-native-video';
const memeViewItemStyles = StyleSheet.create({ const memeViewItemStyles = StyleSheet.create({
view: { view: {

View File

@@ -17,11 +17,13 @@ import {
useNavigation, useNavigation,
} from '@react-navigation/native'; } from '@react-navigation/native';
import { NativeStackNavigationProp } from '@react-navigation/native-stack'; 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 { 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({ const memesStyles = StyleSheet.create({
listView: { listView: {

View 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;

View File

@@ -15,6 +15,7 @@ import { getFlashListItemHeight } from '../../../utilities';
import MemesMasonryItem from './memesMasonryItem'; import MemesMasonryItem from './memesMasonryItem';
import MemesGridItem from './memesGridItem'; import MemesGridItem from './memesGridItem';
import MemesListItem from './memesListItem'; import MemesListItem from './memesListItem';
import { AndroidScoped } from 'react-native-file-access';
const sharedMemesListStyles = StyleSheet.create({ const sharedMemesListStyles = StyleSheet.create({
flashList: { flashList: {
@@ -66,6 +67,10 @@ const MemesList = ({
const gridColumns = useSelector( const gridColumns = useSelector(
(state: RootState) => state.settings.gridColumns, (state: RootState) => state.settings.gridColumns,
); );
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const storageUri = useSelector(
(state: RootState) => state.settings.storageUri,
)!;
return ( return (
<> <>
@@ -82,7 +87,12 @@ const MemesList = ({
numColumns={masonryColumns} numColumns={masonryColumns}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
renderItem={({ item: meme, index }) => ( 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={{ contentContainerStyle={{
paddingTop: flashListPadding, paddingTop: flashListPadding,
@@ -96,6 +106,7 @@ const MemesList = ({
)} )}
onScroll={handleScroll} onScroll={handleScroll}
fadingEdgeLength={100} fadingEdgeLength={100}
overScrollMode="never"
/> />
)} )}
{view === VIEW.GRID && ( {view === VIEW.GRID && (
@@ -111,7 +122,12 @@ const MemesList = ({
numColumns={gridColumns} numColumns={gridColumns}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
renderItem={({ item: meme, index }) => ( 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={{ contentContainerStyle={{
paddingTop: flashListPadding, paddingTop: flashListPadding,
@@ -125,20 +141,25 @@ const MemesList = ({
)} )}
onScroll={handleScroll} onScroll={handleScroll}
fadingEdgeLength={100} fadingEdgeLength={100}
overScrollMode="never"
/> />
)} )}
{view === VIEW.LIST && ( {view === VIEW.LIST && (
<FlashList <FlashList
ref={flashListRef} ref={flashListRef}
data={memes} data={memes}
estimatedItemSize={50} estimatedItemSize={90}
estimatedListSize={{ estimatedListSize={{
height: height, height: height,
width: width * 0.92, width: width * 0.92,
}} }}
showsVerticalScrollIndicator={false} showsVerticalScrollIndicator={false}
renderItem={({ item: meme, index }) => ( 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 />} ItemSeparatorComponent={() => <Divider />}
contentContainerStyle={{ contentContainerStyle={{
@@ -153,6 +174,7 @@ const MemesList = ({
)} )}
onScroll={handleScroll} onScroll={handleScroll}
fadingEdgeLength={100} fadingEdgeLength={100}
overScrollMode="never"
/> />
)} )}
</> </>

View File

@@ -1,12 +1,11 @@
import React, { useMemo } from 'react'; 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 { Text, TouchableRipple } from 'react-native-paper';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { AndroidScoped } from 'react-native-file-access'; import FastImage from 'react-native-fast-image';
import { useSelector } from 'react-redux'; import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
import { MEME_TYPE, Meme } from '../../../database'; import { MEME_TYPE, Meme } from '../../../database';
import { MemeFail } from '..'; import { MemeFail, ThemedSkeletonPlaceholder } from '../../../components';
import { RootState } from '../../../state';
import { useMemeDimensions } from '../../../hooks'; import { useMemeDimensions } from '../../../hooks';
const memesListItemStyles = StyleSheet.create({ const memesListItemStyles = StyleSheet.create({
@@ -35,20 +34,19 @@ const memesListItemStyles = StyleSheet.create({
const MemesListItem = ({ const MemesListItem = ({
meme, meme,
index,
focusMeme, focusMeme,
uri,
}: { }: {
meme: Meme; meme: Meme;
index: number; focusMeme: () => void;
focusMeme: (index: number) => void; uri: string;
}) => { }) => {
const { width } = useSafeAreaFrame(); 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); const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
@@ -57,7 +55,7 @@ const MemesListItem = ({
case MEME_TYPE.IMAGE: case MEME_TYPE.IMAGE:
case MEME_TYPE.GIF: case MEME_TYPE.GIF:
case MEME_TYPE.VIDEO: { case MEME_TYPE.VIDEO: {
return <Image source={{ uri }} style={[memesListItemStyles.image]} />; return <FastImage source={{ uri }} style={memesListItemStyles.image} />;
} }
default: { default: {
return <></>; return <></>;
@@ -65,25 +63,29 @@ const MemesListItem = ({
} }
}, [meme.memeType, uri]); }, [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 ( return (
<TouchableRipple <TouchableRipple onPress={focusMeme} style={memesListItemStyles.view}>
onPress={() => focusMeme(index)}
style={memesListItemStyles.view}>
<> <>
{error ? ( {error ? (
<MemeFail style={memesListItemStyles.image} /> <MemeFail style={memesListItemStyles.image} />
) : ( ) : (
mediaComponent mediaComponent
)} )}
<View <View style={[memesListItemStyles.detailsView, listItemWidth]}>
style={[
memesListItemStyles.detailsView,
{
width: width * 0.92 - 75 - 10,
},
]}>
<Text variant="titleMedium" style={memesListItemStyles.text}> <Text variant="titleMedium" style={memesListItemStyles.text}>
{meme.title} {meme.title}
</Text> </Text>

View File

@@ -1,11 +1,10 @@
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { Image, StyleSheet, TouchableHighlight } from 'react-native'; import { StyleSheet, TouchableHighlight } from 'react-native';
import { useSelector } from 'react-redux';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; 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 { MEME_TYPE, Meme } from '../../../database';
import { RootState } from '../../../state'; import { MemeFail, ThemedSkeletonPlaceholder } from '../../../components';
import { MemeFail } from '..';
import { getFontAwesome5IconSize } from '../../../utilities'; import { getFontAwesome5IconSize } from '../../../utilities';
import { useMemeDimensions } from '../../../hooks'; import { useMemeDimensions } from '../../../hooks';
@@ -21,29 +20,28 @@ const memeMasonryItemStyles = StyleSheet.create({
const MemesMasonryItem = ({ const MemesMasonryItem = ({
meme, meme,
index,
focusMeme, focusMeme,
uri,
columns,
}: { }: {
meme: Meme; meme: Meme;
index: number; focusMeme: () => void;
focusMeme: (index: number) => void; uri: string;
columns: number;
}) => { }) => {
const { width } = useSafeAreaFrame(); 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 itemWidth = useMemo(
() => (width * 0.92 - 5) / columns - 5,
[columns, width],
);
const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType); const { dimensions, loading, error } = useMemeDimensions(uri, meme.mimeType);
const itemWidth = (width * 0.92 - 5) / masonryColumns - 5; const itemHeight = useMemo(
const itemHeight = () => ((width * 0.92) / columns - 5) / (dimensions?.aspectRatio ?? 1),
((width * 0.92) / masonryColumns - 5) / (dimensions?.aspectRatio ?? 1); [columns, dimensions?.aspectRatio, width],
);
const mediaComponent = useMemo(() => { const mediaComponent = useMemo(() => {
switch (meme.memeType) { switch (meme.memeType) {
@@ -51,14 +49,11 @@ const MemesMasonryItem = ({
case MEME_TYPE.GIF: case MEME_TYPE.GIF:
case MEME_TYPE.VIDEO: { case MEME_TYPE.VIDEO: {
return ( return (
<Image <FastImage
source={{ uri }} source={{ uri }}
style={[ style={[
memeMasonryItemStyles.image, memeMasonryItemStyles.image,
{ { width: itemWidth, height: itemHeight },
width: itemWidth,
height: itemHeight,
},
]} ]}
/> />
); );
@@ -69,19 +64,30 @@ const MemesMasonryItem = ({
} }
}, [itemHeight, itemWidth, meme.memeType, uri]); }, [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 ( return (
<TouchableHighlight <TouchableHighlight onPress={focusMeme} style={memeMasonryItemStyles.view}>
onPress={() => focusMeme(index)}
style={memeMasonryItemStyles.view}>
{error || !dimensions ? ( {error || !dimensions ? (
<MemeFail <MemeFail
style={[ style={[
memeMasonryItemStyles.image, memeMasonryItemStyles.image,
{ width: itemWidth, height: itemHeight }, { width: itemWidth, height: itemHeight },
]} ]}
iconSize={getFontAwesome5IconSize(masonryColumns)} iconSize={getFontAwesome5IconSize(columns)}
/> />
) : ( ) : (
mediaComponent mediaComponent

View File

@@ -11,6 +11,8 @@ import {
} from 'react-native-paper'; } from 'react-native-paper';
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 { useRealm } from '@realm/react';
import { FileSystem, FileStat } from 'react-native-file-access';
import { import {
RootState, RootState,
setAutofocusMemesSearch, setAutofocusMemesSearch,
@@ -19,11 +21,9 @@ import {
setMasonryColumns, setMasonryColumns,
setNoMedia, setNoMedia,
setSnackbarMessage, setSnackbarMessage,
} from '../state'; } from '../../state';
import StorageLocationChangeDialog from '../components/storageLocationChangeDialog'; import { Meme } from '../../database';
import { useRealm } from '@realm/react'; import StorageLocationChangeDialog from './storageLocationChangeDialog';
import { FileSystem, FileStat } from 'react-native-file-access';
import { Meme } from '../database';
const settingsStyles = StyleSheet.create({ const settingsStyles = StyleSheet.create({
scrollView: { scrollView: {

View File

@@ -4,8 +4,8 @@ import { Dialog, ProgressBar, Text } from 'react-native-paper';
import { openDocumentTree } from 'react-native-scoped-storage'; import { openDocumentTree } from 'react-native-scoped-storage';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { AndroidScoped, FileSystem } from 'react-native-file-access'; import { AndroidScoped, FileSystem } from 'react-native-file-access';
import { RootState, setStorageUri } from '../state'; import { RootState, setStorageUri } from '../../state';
import { clearPermissions, isPermissionForPath, noOp } from '../utilities'; import { clearPermissions, isPermissionForPath, noOp } from '../../utilities';
const storageLocationChangeDialogStyles = StyleSheet.create({ const storageLocationChangeDialogStyles = StyleSheet.create({
progressBar: { progressBar: {

View File

@@ -13,10 +13,12 @@ import { FlashList } from '@shopify/flash-list';
import { useFocusEffect } from '@react-navigation/native'; import { useFocusEffect } from '@react-navigation/native';
import { useDeviceOrientation } from '@react-native-community/hooks'; import { useDeviceOrientation } from '@react-native-community/hooks';
import { useSafeAreaFrame } from 'react-native-safe-area-context'; import { useSafeAreaFrame } from 'react-native-safe-area-context';
import { HideableHeader, TagRow, TagsHeader } from '../components'; import { HideableHeader } from '../../components';
import { Tag } from '../database'; import { Tag } from '../../database';
import { RootState, setNavVisible } from '../state'; import { RootState, setNavVisible } from '../../state';
import { SORT_DIRECTION, tagSortQuery } from '../types'; import { SORT_DIRECTION, tagSortQuery } from '../../types';
import TagsHeader from './tagsHeader';
import TagRow from './tagsList/tagRow';
const tagsStyles = StyleSheet.create({ const tagsStyles = StyleSheet.create({
listView: { listView: {

View File

@@ -2,9 +2,9 @@ import React from 'react';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import { TouchableRipple, Text } from 'react-native-paper'; import { TouchableRipple, Text } from 'react-native-paper';
import { useNavigation, NavigationProp } from '@react-navigation/native'; import { useNavigation, NavigationProp } from '@react-navigation/native';
import { Tag } from '../../database'; import { Tag } from '../../../database';
import { ROUTE, RootStackParamList } from '../../types'; import { ROUTE, RootStackParamList } from '../../../types';
import { TagChip } from '.'; import { TagChip } from '../../../components';
const tagRowStyles = StyleSheet.create({ const tagRowStyles = StyleSheet.create({
view: { view: {

View File

@@ -1,6 +1,6 @@
import { DocumentPickerResponse } from 'react-native-document-picker'; import { DocumentPickerResponse } from 'react-native-document-picker';
import { getFilenameFromUri } from '../utilities'; import { getFilenameFromUri } from '../utilities';
import { SharedItem } from './share'; import { SharedItem } from '.';
enum ROUTE { enum ROUTE {
MAIN = 'Main', MAIN = 'Main',

View File

@@ -4,7 +4,7 @@ import Share from 'react-native-share';
import Clipboard from '@react-native-clipboard/clipboard'; import Clipboard from '@react-native-clipboard/clipboard';
import { Meme } from '../database'; import { Meme } from '../database';
import { ROUTE, RootStackParamList } from '../types'; import { ROUTE, RootStackParamList } from '../types';
import { noOp } from './constants'; import { noOp } from '.';
const favoriteMeme = (realm: Realm, meme: Meme) => { const favoriteMeme = (realm: Realm, meme: Meme) => {
realm.write(() => { realm.write(() => {

View File

@@ -1,4 +1,4 @@
import { isHexColor, isRgbColor } from './color'; import { isHexColor, isRgbColor } from '.';
interface StringValidationResult { interface StringValidationResult {
valid: boolean; valid: boolean;