233 lines
6.8 KiB
TypeScript
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;
|