myjourney/app/Editor.tsx
2025-06-25 06:34:07 +00:00

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