237 lines
8.8 KiB
TypeScript
237 lines
8.8 KiB
TypeScript
import Wrapper from '@/components/ui/Wrapper';
|
|
import { useFileSystem } from '@/hooks/useFilesystem';
|
|
import { CoreBridge, PlaceholderBridge, RichText, TenTapStartKit, Toolbar, useEditorBridge, useEditorContent } from '@10play/tentap-editor';
|
|
import { useRoute } from '@react-navigation/native';
|
|
import { format } from 'date-fns';
|
|
import { StorageAccessFramework } from 'expo-file-system';
|
|
import { useRouter } from 'expo-router';
|
|
import { marked } from 'marked';
|
|
import React, { useEffect, useState } from 'react';
|
|
import { Keyboard, View } from 'react-native';
|
|
import { Appbar, Button, Dialog, Menu, Portal, Text, useTheme } from 'react-native-paper';
|
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
import turndown from 'turndown-rn';
|
|
|
|
export default function EditorScreen() {
|
|
const [entryText, setEntryText] = useState<string>('');
|
|
const [stats, setStats] = useState<{ words: number; characters: number }>({
|
|
words: 0,
|
|
characters: 0,
|
|
});
|
|
|
|
useEffect(() => {
|
|
const words = entryText.trim().split(/\s+/).filter(Boolean).length;
|
|
const characters = entryText.length;
|
|
setStats({ words, characters });
|
|
}
|
|
, [entryText]);
|
|
|
|
const navigation = useRouter();
|
|
const route = useRoute();
|
|
const { writeFile, deleteFile } = useFileSystem();
|
|
const { fileUri, fileDatetime } = route.params as { fileUri: string | undefined, fileDatetime: Date } || { fileUri: undefined, fileDatetime: new Date() };
|
|
const [loadedContents, setLoadedContents] = useState<string>("");
|
|
const [menuVisible, setMenuVisible] = useState<boolean>(false);
|
|
const [dialogVisible, setDialogVisible] = useState<boolean>(false);
|
|
const openMenu = () => { setMenuVisible(true); };
|
|
const closeMenu = () => { setMenuVisible(false); };
|
|
const openDialog = () => { setDialogVisible(true); };
|
|
const closeDialog = () => { setDialogVisible(false); };
|
|
|
|
|
|
const loadFileContent = async (fileUri: string, forceReload = false) => {
|
|
try {
|
|
// Replace with your actual file reading logic
|
|
const content = await StorageAccessFramework.readAsStringAsync(fileUri);
|
|
setLoadedContents(await marked.parse(content));
|
|
return (await marked.parse(content));
|
|
} catch (error) {
|
|
console.error('Error loading file content:', error);
|
|
}
|
|
};
|
|
|
|
const saveFileContent = async () => {
|
|
console.log("Begin Save");
|
|
// try{
|
|
|
|
// }
|
|
// catch (error) {
|
|
// console.error("Error creating file:", error);
|
|
// return;
|
|
// }
|
|
try {
|
|
if (!content.html) return;
|
|
|
|
// Convert HTML back to markdown
|
|
const turndownService = new turndown({
|
|
headingStyle: 'atx',
|
|
hr: '---',
|
|
bulletListMarker: '-',
|
|
codeBlockStyle: 'fenced',
|
|
fence: '```',
|
|
emDelimiter: '*',
|
|
strongDelimiter: '**',
|
|
linkStyle: 'inlined',
|
|
linkReferenceStyle: 'full',
|
|
});
|
|
|
|
// Optional: Add custom rules for better conversion
|
|
turndownService.addRule('strikethrough', {
|
|
filter: ['del', 's'],
|
|
replacement: (content: string) => `~~${content}~~`
|
|
});
|
|
|
|
turndownService.addRule('underline', {
|
|
filter: ['u', 'ins'],
|
|
replacement: (content: string) => `<u>${content}</u>`
|
|
});
|
|
const markdownContent = turndownService.turndown(content.html);
|
|
if (markdownContent == turndownService.turndown(loadedContents)) { console.log("Skipping..."); return; }
|
|
|
|
// Save the markdown content
|
|
await writeFile(fileUri ?? format(fileDatetime, 't'), markdownContent);
|
|
console.log(`${fileDatetime} saved successfully`);
|
|
|
|
} catch (error) {
|
|
console.error(`Error saving ${fileUri}:`, error);
|
|
}
|
|
};
|
|
useEffect(() => {
|
|
if (fileUri) {
|
|
loadFileContent(fileUri);
|
|
}
|
|
}, [fileUri]);
|
|
useEffect(() => {
|
|
editor.setContent(loadedContents);
|
|
}, [loadedContents]);
|
|
|
|
const theme = useTheme();
|
|
const entryDate = format(fileDatetime, 'LLL d, h:mm aaa');
|
|
|
|
const insets = useSafeAreaInsets();
|
|
const [keyboardHeight, setKeyboardHeight] = useState(0);
|
|
useEffect(() => {
|
|
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
|
|
setKeyboardHeight(Keyboard.metrics()?.height || 0);
|
|
});
|
|
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
|
|
setKeyboardHeight(0);
|
|
});
|
|
|
|
return () => {
|
|
keyboardDidShowListener.remove();
|
|
keyboardDidHideListener.remove();
|
|
};
|
|
}, []);
|
|
const editor = useEditorBridge({
|
|
autofocus: true,
|
|
avoidIosKeyboard: true,
|
|
theme: {
|
|
webviewContainer: {
|
|
paddingLeft: 15,
|
|
backgroundColor: theme.colors.surface,
|
|
},
|
|
},
|
|
bridgeExtensions: [
|
|
...TenTapStartKit,
|
|
CoreBridge.configureCSS(`
|
|
* {
|
|
font-size: 14px;
|
|
}
|
|
`),
|
|
PlaceholderBridge.configureExtension({
|
|
placeholder: '',
|
|
}),
|
|
],
|
|
initialContent: loadedContents,
|
|
});
|
|
const content = {
|
|
html: useEditorContent(editor, { type: 'html' }),
|
|
text: useEditorContent(editor, { type: 'text' }),
|
|
};
|
|
useEffect(() => {
|
|
content.text && setEntryText(content.text ?? 'Nothing loaded.');
|
|
}, [content]);
|
|
return (
|
|
<Wrapper>
|
|
<Appbar.Header style={{
|
|
backgroundColor: theme.colors.primary,
|
|
borderBottomWidth: 2,
|
|
borderBottomColor: theme.colors.backdrop,
|
|
}}>
|
|
{
|
|
|
|
content.text && content.text != loadedContents ?
|
|
<Appbar.Action icon="check" onPress={() => {
|
|
saveFileContent();
|
|
navigation.back();
|
|
}} />
|
|
:
|
|
<Appbar.BackAction onPress={() => {
|
|
navigation.back();
|
|
}} />
|
|
|
|
}
|
|
<Appbar.Content title={entryDate}
|
|
titleStyle={{
|
|
fontSize: 16, fontWeight: 'bold',
|
|
}}
|
|
/>
|
|
<Appbar.Action icon="calendar" onPress={() => { }} />
|
|
<Appbar.Action icon="tag" onPress={() => { }} />
|
|
<Menu
|
|
visible={menuVisible}
|
|
onDismiss={closeMenu}
|
|
anchor={<Appbar.Action icon="dots-vertical" onPress={() => {
|
|
openMenu();
|
|
}} />}>
|
|
<Menu.Item onPress={() => {
|
|
navigation.back();
|
|
closeMenu();
|
|
}} title="Cancel" />
|
|
<Menu.Item onPress={() => {
|
|
openDialog();
|
|
closeMenu();
|
|
}} title="Delete" />
|
|
</Menu>
|
|
</Appbar.Header>
|
|
<Portal>
|
|
<Dialog visible={dialogVisible} onDismiss={closeDialog}>
|
|
<Dialog.Title>Delete file?</Dialog.Title>
|
|
<Dialog.Content>
|
|
<Text>Entry for
|
|
{' '}
|
|
<Text style={{ fontWeight: 'bold' }}>
|
|
{format(fileDatetime, "MMM dd, yyyy, HH:mm:ss")}</Text> will be permanently deleted.</Text>
|
|
</Dialog.Content>
|
|
<Dialog.Actions>
|
|
<Button onPress={() => {
|
|
closeDialog()
|
|
}}>Cancel</Button>
|
|
<Button onPress={() => {
|
|
if (fileUri) { deleteFile(fileUri); }
|
|
navigation.back();
|
|
closeDialog()
|
|
}}>Done</Button>
|
|
</Dialog.Actions>
|
|
</Dialog>
|
|
</Portal>
|
|
{/* <SafeAreaView style={{ flex: 1, backgroundColor: theme.colors.background }}> */}
|
|
<RichText editor={editor} style={{ marginTop: 10, }} />
|
|
<View
|
|
// horizontal={true}
|
|
// keyboardShouldPersistTaps="always"
|
|
style={{
|
|
position: 'absolute',
|
|
bottom: keyboardHeight + insets.bottom,
|
|
left: 10,
|
|
right: 0,
|
|
flexDirection: 'row',
|
|
}}>
|
|
<Toolbar editor={editor} hidden={false} />
|
|
</View>
|
|
{/* </SafeAreaView> */}
|
|
|
|
</Wrapper>
|
|
);
|
|
}; |