5 Commits
v0.0.6 ... main

Author SHA1 Message Date
91bcc6072f Fix image resize mode
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2023-08-06 15:13:07 +03:00
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
13 changed files with 247 additions and 113 deletions

View File

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

66
package-lock.json generated
View File

@@ -12,6 +12,7 @@
"@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",
@@ -23,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",
@@ -34,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",
@@ -4114,6 +4118,15 @@
"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",
@@ -13434,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",
@@ -13470,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",
@@ -13582,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",
@@ -18809,6 +18851,12 @@
"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",
@@ -25861,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",
@@ -25887,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",
@@ -25973,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",

View File

@@ -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,7 @@
"@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",
@@ -28,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",
@@ -39,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",

View File

@@ -6,3 +6,4 @@ export { default as LoadingView } from './loadingView';
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

@@ -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,8 +1,12 @@
import React, { useEffect, useMemo, useState } from 'react';
import { HelperText, Text, TextInput, useTheme } from 'react-native-paper';
import { Image, LayoutAnimation, View } from 'react-native';
import { LayoutAnimation, View } from 'react-native';
import { useSafeAreaFrame } from 'react-native-safe-area-context';
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,
@@ -13,9 +17,6 @@ import { StagingMeme } from '../../../types';
import { useMemeDimensions } from '../../../hooks';
import { MEME_TYPE } from '../../../database';
import MemeTagSelector from './memeTagSelector/memeTagSelector';
import TextRecognition, {
TextRecognitionResult,
} from '@react-native-ml-kit/text-recognition';
const memeEditorStyles = {
media: {
@@ -95,7 +96,7 @@ const MemeEditor = ({
case MEME_TYPE.GIF: {
return (
<View>
<Image
<FastImage
source={{ uri }}
style={[memeEditorStyles.media, dimensionStyles]}
resizeMode="contain"

View File

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

View File

@@ -71,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))
@@ -83,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]);
@@ -94,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 }}
@@ -126,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!'),
);
}}
@@ -142,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!'));
}}
@@ -151,7 +151,7 @@ const MemeView = ({
<Appbar.Action
icon="pencil"
onPress={() => {
editMeme(navigation, memes[index.current]);
editMeme(navigation, memes[index]);
}}
disabled={isBlocked}
/>
@@ -159,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}

View File

@@ -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
/>
);
}
}

View File

@@ -1,52 +1,42 @@
import React, { useMemo } from 'react';
import { Image, TouchableHighlight } from 'react-native';
import { useSelector } from 'react-redux';
import { 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 '../../../components';
import { MemeFail, ThemedSkeletonPlaceholder } from '../../../components';
import { getFontAwesome5IconSize } from '../../../utilities';
import { useMemeDimensions } from '../../../hooks';
const MemesGridItem = ({
meme,
index,
focusMeme,
uri,
columns,
}: {
meme: Meme;
index: number;
focusMeme: (index: number) => void;
focusMeme: () => void;
uri: string;
columns: number;
}) => {
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 itemWidth = useMemo(
() => (width * 0.92 - 5) / columns,
[columns, width],
);
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
<FastImage
source={{ uri }}
style={[
{
width: itemWidth,
height: itemWidth,
},
]}
style={{ width: itemWidth, height: itemWidth }}
/>
);
}
@@ -56,17 +46,23 @@ const MemesGridItem = ({
}
}, [itemWidth, meme.memeType, uri]);
if (!error && (loading || !dimensions)) return <></>;
const skeletonComponent = useMemo(
() => (
<ThemedSkeletonPlaceholder>
<SkeletonPlaceholder.Item width={itemWidth} height={itemWidth} />
</ThemedSkeletonPlaceholder>
),
[itemWidth],
);
if (!error && (loading || !dimensions)) return skeletonComponent;
return (
<TouchableHighlight onPress={() => focusMeme(index)}>
<TouchableHighlight onPress={focusMeme}>
{error ? (
<MemeFail
style={{
width: (width * 0.92 - 5) / gridColumns,
height: (width * 0.92 - 5) / gridColumns,
}}
iconSize={getFontAwesome5IconSize(gridColumns)}
style={{ width: itemWidth, height: itemWidth }}
iconSize={getFontAwesome5IconSize(columns)}
/>
) : (
mediaComponent

View File

@@ -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"
/>
)}
</>

View File

@@ -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 '../../../components';
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>

View File

@@ -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 '../../../components';
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