This repository has been archived on 2025-07-31. You can view files and clone it, but cannot push or open issues or pull requests.
Files
terminally-online/src/screens/editors/editMeme.tsx
2023-08-01 08:58:35 +03:00

233 lines
6.8 KiB
TypeScript

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { ScrollView, View } from 'react-native';
import { Appbar, Banner, Button, useTheme } from 'react-native-paper';
import { useNavigation } from '@react-navigation/native';
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 {
allowedMimeTypes,
deleteMeme,
favoriteMeme,
getMemeTypeFromMimeType,
guessMimeType,
noOp,
validateMemeTitle,
} from '../../utilities';
import { MemeEditor } from '../../components';
import editorStyles from './editorStyles';
import { RootState } from '../../state';
const EditMeme = ({
route,
}: NativeStackScreenProps<RootStackParamList, ROUTE.EDIT_MEME>) => {
const { goBack } = useNavigation();
const { colors } = useTheme();
const orientation = useDeviceOrientation();
const realm = useRealm();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const storageUri = useSelector(
(state: RootState) => state.settings.storageUri,
)!;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const meme = useObject<Meme>(
Meme.schema.name,
BSON.UUID.createFromHexString(route.params.id),
)!;
const [isSaving, setIsSaving] = useState(false);
const [uri, setUri] = useState<string>();
const [mimeType, setMimeType] = useState<string>();
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error>();
const [staging, setStaging] = useState<StagingMeme>();
const originalStaging = useRef<StagingMeme>();
const resetState = useCallback(
async (newUri: string) => {
setLoading(true);
// eslint-disable-next-line unicorn/no-useless-undefined
setError(undefined);
setUri(newUri);
const guessedMimeType = await guessMimeType(newUri);
if (!guessedMimeType) {
setError(
new Error('Could not determine MIME type or file is not supported.'),
);
return;
}
setMimeType(guessedMimeType);
const stagingMeme = {
title: validateMemeTitle(meme.title),
isFavorite: meme.isFavorite,
tags: new Map(meme.tags.map(tag => [tag.id.toHexString(), tag])),
};
setStaging(stagingMeme);
originalStaging.current = stagingMeme;
setLoading(false);
},
[meme.isFavorite, meme.tags, meme.title],
);
useEffect(
() => void resetState(AndroidScoped.appendPath(storageUri, meme.filename)),
[meme.filename, resetState, storageUri],
);
const handleSave = useCallback(() => {
if (!mimeType || !staging) return;
setIsSaving(true);
realm.write(() => {
meme.tags.forEach(tag => {
if (!staging.tags.has(tag.id.toHexString())) {
tag.memes.slice(tag.memes.indexOf(meme), 1);
tag.memesLength -= 1;
tag.dateModified = new Date();
}
});
staging.tags.forEach(tag => {
if (!meme.tags.some(memeTag => memeTag.id.equals(tag.id))) {
tag.memes.push(meme);
tag.memesLength = tag.memes.length;
tag.dateModified = new Date();
}
});
meme.title = staging.title.parsed;
// @ts-expect-error - Realm is a fuck
meme.tags = [...staging.tags.values()];
meme.tagsLength = staging.tags.size;
meme.dateModified = new Date();
});
goBack();
}, [goBack, meme, mimeType, realm, staging]);
const handleDelete = useCallback(async () => {
setIsSaving(true);
await deleteMeme(realm, storageUri, meme);
goBack();
}, [goBack, meme, realm, storageUri]);
const handleFixUri = useCallback(async () => {
const file = await pickSingle({ type: allowedMimeTypes }).catch(noOp);
if (!file) return;
const guessedMimeType = await guessMimeType(file.uri, file.type);
if (!guessedMimeType) {
setError(
new Error('Could not determine MIME type or file is not supported.'),
);
return;
}
const memeType = getMemeTypeFromMimeType(guessedMimeType);
if (!memeType) return;
const fileExtension = extension(guessedMimeType);
if (!fileExtension) return;
const filename = `${meme.id.toHexString()}-${
Date.now() / 1000
}.${fileExtension}`;
const newUri = AndroidScoped.appendPath(storageUri, filename);
await FileSystem.cp(file.uri, newUri);
const { size } = await FileSystem.stat(newUri);
realm.write(() => {
meme.filename = filename;
meme.memeType = memeType;
meme.mimeType = guessedMimeType;
meme.size = size;
});
void resetState(newUri);
}, [meme, realm, resetState, storageUri]);
return (
<>
<Appbar.Header>
<Appbar.BackAction onPress={() => goBack()} />
<Appbar.Content title={'Edit Meme'} />
<Appbar.Action
icon={meme.isFavorite ? 'heart' : 'heart-outline'}
onPress={() => favoriteMeme(realm, meme)}
/>
<Appbar.Action icon="delete" onPress={handleDelete} />
</Appbar.Header>
<Banner
visible={!!error}
actions={[
{
label: 'Fix URI',
onPress: handleFixUri,
},
{
label: 'Delete Meme',
onPress: handleDelete,
},
]}>
{error?.message}
</Banner>
<ScrollView
contentContainerStyle={[
editorStyles.scrollView,
orientation === 'portrait'
? editorStyles.scrollViewPortrait
: editorStyles.scrollViewLandscape,
{ backgroundColor: colors.background },
]}>
<View style={editorStyles.editorView}>
<MemeEditor
uri={uri}
mimeType={mimeType}
loading={loading}
setLoading={setLoading}
error={error}
setError={setError}
staging={staging}
setStaging={setStaging}
/>
</View>
<View style={editorStyles.saveButtonView}>
<Button
mode="contained"
icon="floppy"
onPress={handleSave}
disabled={
loading ||
!!error ||
isSaving ||
!staging?.title.valid ||
originalStaging.current === staging
}
loading={isSaving}
style={editorStyles.soloSaveButton}>
Save
</Button>
</View>
</ScrollView>
</>
);
};
export default EditMeme;