Skip to content

Markdown Notes App - Complete Setup Guide

1. Termux Setup and Installation

Install Required Packages

# Update package list
pkg update && pkg upgrade

# Install Node.js and npm
pkg install nodejs npm

# Install Git
pkg install git

# Install Android SDK tools (optional for debugging)
pkg install android-tools

# Give Termux storage permissions
termux-setup-storage

Install Expo CLI

npm install -g @expo/cli
npm install -g eas-cli

2. Project Initialization

Create Expo Project

# Create new Expo project
npx create-expo-app MarkdownNotesApp --template blank

# Navigate to project directory
cd MarkdownNotesApp

# Install required dependencies
npm install react-native-markdown-display
npm install react-native-mathjax-html-to-svg
npm install react-native-webview
npm install @react-native-async-storage/async-storage
npm install react-native-fs
npm install react-native-share
npm install expo-file-system
npm install expo-document-picker
npm install react-native-super-grid
npm install react-native-vector-icons
npm install react-native-modal
npm install react-native-keyboard-aware-scroll-view

# Install Mermaid support
npm install mermaid
npm install react-native-render-html

3. Project Structure

MarkdownNotesApp/
├── App.js
├── app.json
├── package.json
├── components/
│   ├── MarkdownEditor.js
│   ├── MarkdownPreview.js
│   ├── FileManager.js
│   ├── MathRenderer.js
│   ├── MermaidRenderer.js
│   └── TableOfContents.js
├── utils/
│   ├── storage.js
│   ├── markdownUtils.js
│   ├── exportUtils.js
│   └── keybindings.js
├── styles/
│   └── styles.js
└── assets/
    ├── fonts/
    └── icons/

4. Core Implementation Files

App.js (Main Application)

import React, { useState, useEffect, useRef } from 'react';
import {
  View,
  StyleSheet,
  StatusBar,
  KeyboardAvoidingView,
  Platform,
  Alert,
  BackHandler,
} from 'react-native';
import MarkdownEditor from './components/MarkdownEditor';
import MarkdownPreview from './components/MarkdownPreview';
import FileManager from './components/FileManager';
import TableOfContents from './components/TableOfContents';
import { loadNotes, saveNote, deleteNote } from './utils/storage';
import { extractTOC } from './utils/markdownUtils';

export default function App() {
  const [mode, setMode] = useState('edit'); // 'edit', 'preview', 'files', 'toc'
  const [currentNote, setCurrentNote] = useState({
    id: null,
    title: 'New Note',
    content: '# New Note\n\nStart writing your markdown here...',
    modified: new Date().toISOString(),
  });
  const [notes, setNotes] = useState([]);
  const [tocItems, setTocItems] = useState([]);
  const editorRef = useRef(null);

  useEffect(() => {
    loadAllNotes();
    setupKeyboardHandlers();
  }, []);

  useEffect(() => {
    if (mode === 'toc') {
      const toc = extractTOC(currentNote.content);
      setTocItems(toc);
    }
  }, [mode, currentNote.content]);

  const loadAllNotes = async () => {
    try {
      const loadedNotes = await loadNotes();
      setNotes(loadedNotes);
      if (loadedNotes.length > 0 && !currentNote.id) {
        setCurrentNote(loadedNotes[0]);
      }
    } catch (error) {
      Alert.alert('Error', 'Failed to load notes');
    }
  };

  const setupKeyboardHandlers = () => {
    const backAction = () => {
      handleKeyPress('Escape');
      return true;
    };

    const backHandler = BackHandler.addEventListener('hardwareBackPress', backAction);
    return () => backHandler.remove();
  };

  const handleKeyPress = (key) => {
    switch (key) {
      case 'i':
        if (mode !== 'edit') setMode('edit');
        break;
      case 'Escape':
        if (mode === 'edit') setMode('preview');
        else if (mode === 'preview') setMode('files');
        else if (mode === 'files' || mode === 'toc') setMode('preview');
        break;
      case 't':
        if (mode === 'preview') setMode('toc');
        break;
      case 'f':
        if (mode === 'preview') setMode('files');
        break;
    }
  };

  const handleSaveNote = async () => {
    try {
      const savedNote = await saveNote(currentNote);
      setCurrentNote(savedNote);
      await loadAllNotes();
    } catch (error) {
      Alert.alert('Error', 'Failed to save note');
    }
  };

  const handleContentChange = (content) => {
    setCurrentNote(prev => ({
      ...prev,
      content,
      modified: new Date().toISOString(),
    }));
  };

  const handleNoteSelect = (note) => {
    setCurrentNote(note);
    setMode('preview');
  };

  const handleTocItemPress = (item) => {
    // Scroll to heading in preview mode
    setMode('preview');
    // Implementation for scrolling to specific heading
  };

  const renderCurrentMode = () => {
    switch (mode) {
      case 'edit':
        return (
          <MarkdownEditor
            ref={editorRef}
            content={currentNote.content}
            onContentChange={handleContentChange}
            onSave={handleSaveNote}
            onKeyPress={handleKeyPress}
          />
        );
      case 'preview':
        return (
          <MarkdownPreview
            content={currentNote.content}
            onKeyPress={handleKeyPress}
          />
        );
      case 'files':
        return (
          <FileManager
            notes={notes}
            currentNote={currentNote}
            onNoteSelect={handleNoteSelect}
            onDeleteNote={deleteNote}
            onRefresh={loadAllNotes}
            onKeyPress={handleKeyPress}
          />
        );
      case 'toc':
        return (
          <TableOfContents
            items={tocItems}
            onItemPress={handleTocItemPress}
            onKeyPress={handleKeyPress}
          />
        );
      default:
        return null;
    }
  };

  return (
    <View style={styles.container}>
      <StatusBar barStyle="dark-content" backgroundColor="#ffffff" />
      <KeyboardAvoidingView
        style={styles.container}
        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      >
        {renderCurrentMode()}
      </KeyboardAvoidingView>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
});

components/MarkdownEditor.js

import React, { forwardRef, useImperativeHandle, useRef, useEffect } from 'react';
import {
  View,
  TextInput,
  StyleSheet,
  Text,
  TouchableOpacity,
  KeyboardAvoidingView,
  Platform,
} from 'react-native';
import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';

const MarkdownEditor = forwardRef(({ content, onContentChange, onSave, onKeyPress }, ref) => {
  const textInputRef = useRef(null);
  const [cursorPosition, setCursorPosition] = React.useState(0);

  useImperativeHandle(ref, () => ({
    focus: () => textInputRef.current?.focus(),
    blur: () => textInputRef.current?.blur(),
    insertText: (text) => {
      // Implementation for inserting text at cursor position
    },
  }));

  useEffect(() => {
    // Auto-focus when entering edit mode
    setTimeout(() => {
      textInputRef.current?.focus();
    }, 100);
  }, []);

  const handleKeyPress = ({ nativeEvent }) => {
    if (nativeEvent.key === 'Escape' || nativeEvent.key === 'Back') {
      onKeyPress('Escape');
    }
  };

  const insertMarkdown = (syntax) => {
    // Implementation for inserting markdown syntax
    const beforeCursor = content.substring(0, cursorPosition);
    const afterCursor = content.substring(cursorPosition);
    const newContent = beforeCursor + syntax + afterCursor;
    onContentChange(newContent);
    setCursorPosition(cursorPosition + syntax.length);
  };

  const toolbarItems = [
    { label: '**B**', syntax: '**bold**' },
    { label: '*I*', syntax: '*italic*' },
    { label: '`C`', syntax: '`code`' },
    { label: '[]', syntax: '[link](url)' },
    { label: '#', syntax: '# ' },
    { label: '$$', syntax: '$$math$$' },
    { label: '```', syntax: '```\ncode\n```' },
  ];

  return (
    <KeyboardAvoidingView style={styles.container} behavior={Platform.OS === 'ios' ? 'padding' : 'height'}>
      <View style={styles.toolbar}>
        <Text style={styles.title}>Edit Mode (ESC to preview)</Text>
        <TouchableOpacity style={styles.saveButton} onPress={onSave}>
          <Text style={styles.saveButtonText}>Save</Text>
        </TouchableOpacity>
      </View>

      <View style={styles.markdownToolbar}>
        {toolbarItems.map((item, index) => (
          <TouchableOpacity
            key={index}
            style={styles.toolbarButton}
            onPress={() => insertMarkdown(item.syntax)}
          >
            <Text style={styles.toolbarButtonText}>{item.label}</Text>
          </TouchableOpacity>
        ))}
      </View>

      <KeyboardAwareScrollView style={styles.editorContainer}>
        <TextInput
          ref={textInputRef}
          style={styles.textInput}
          value={content}
          onChangeText={onContentChange}
          multiline
          textAlignVertical="top"
          placeholder="Start writing your markdown here..."
          onKeyPress={handleKeyPress}
          onSelectionChange={(event) => setCursorPosition(event.nativeEvent.selection.start)}
          autoCapitalize="sentences"
          autoCorrect={false}
          spellCheck={false}
        />
      </KeyboardAwareScrollView>
    </KeyboardAvoidingView>
  );
});

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
  toolbar: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
  },
  saveButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 12,
    paddingVertical: 6,
    borderRadius: 4,
  },
  saveButtonText: {
    color: '#ffffff',
    fontWeight: 'bold',
  },
  markdownToolbar: {
    flexDirection: 'row',
    paddingHorizontal: 8,
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
    backgroundColor: '#fafafa',
  },
  toolbarButton: {
    paddingHorizontal: 8,
    paddingVertical: 4,
    marginHorizontal: 2,
    backgroundColor: '#e0e0e0',
    borderRadius: 4,
  },
  toolbarButtonText: {
    fontSize: 12,
    color: '#333',
  },
  editorContainer: {
    flex: 1,
  },
  textInput: {
    flex: 1,
    padding: 16,
    fontSize: 16,
    lineHeight: 24,
    fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
    color: '#333',
  },
});

export default MarkdownEditor;

components/MarkdownPreview.js

import React, { useRef } from 'react';
import {
  View,
  StyleSheet,
  Text,
  TouchableOpacity,
  ScrollView,
} from 'react-native';
import MarkdownDisplay from 'react-native-markdown-display';
import MathRenderer from './MathRenderer';
import MermaidRenderer from './MermaidRenderer';

const MarkdownPreview = ({ content, onKeyPress }) => {
  const scrollViewRef = useRef(null);

  const processContent = (content) => {
    // Process math blocks
    content = content.replace(/\$\$(.*?)\$\$/gs, (match, math) => {
      return `<Math>${math}</Math>`;
    });

    // Process inline math
    content = content.replace(/\$(.*?)\$/g, (match, math) => {
      return `<InlineMath>${math}</InlineMath>`;
    });

    // Process Mermaid blocks
    content = content.replace(/```mermaid\n(.*?)\n```/gs, (match, diagram) => {
      return `<Mermaid>${diagram}</Mermaid>`;
    });

    return content;
  };

  const customRenderers = {
    Math: ({ children }) => <MathRenderer content={children} inline={false} />,
    InlineMath: ({ children }) => <MathRenderer content={children} inline={true} />,
    Mermaid: ({ children }) => <MermaidRenderer content={children} />,
  };

  const markdownRules = {
    heading1: (node, children, parent, styles) => (
      <Text key={node.key} style={[styles.heading1, { marginTop: 20, marginBottom: 10 }]}>
        {children}
      </Text>
    ),
    heading2: (node, children, parent, styles) => (
      <Text key={node.key} style={[styles.heading2, { marginTop: 16, marginBottom: 8 }]}>
        {children}
      </Text>
    ),
    code_inline: (node, children, parent, styles) => (
      <Text key={node.key} style={styles.code_inline}>
        {children}
      </Text>
    ),
    code_block: (node, children, parent, styles) => (
      <View key={node.key} style={styles.code_block}>
        <Text style={styles.code_block_text}>{node.content}</Text>
      </View>
    ),
  };

  const markdownStyles = {
    body: {
      fontSize: 16,
      lineHeight: 24,
      color: '#333',
    },
    heading1: {
      fontSize: 28,
      fontWeight: 'bold',
      color: '#000',
    },
    heading2: {
      fontSize: 24,
      fontWeight: 'bold',
      color: '#000',
    },
    heading3: {
      fontSize: 20,
      fontWeight: 'bold',
      color: '#000',
    },
    paragraph: {
      marginBottom: 12,
    },
    code_inline: {
      backgroundColor: '#f0f0f0',
      paddingHorizontal: 4,
      paddingVertical: 2,
      borderRadius: 3,
      fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
      fontSize: 14,
    },
    code_block: {
      backgroundColor: '#f8f8f8',
      padding: 12,
      borderRadius: 6,
      marginVertical: 8,
      borderLeftWidth: 4,
      borderLeftColor: '#007AFF',
    },
    code_block_text: {
      fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
      fontSize: 14,
      color: '#333',
    },
    blockquote: {
      backgroundColor: '#f9f9f9',
      borderLeftWidth: 4,
      borderLeftColor: '#ccc',
      paddingLeft: 12,
      paddingVertical: 8,
      marginVertical: 8,
    },
    table: {
      borderWidth: 1,
      borderColor: '#ddd',
      marginVertical: 8,
    },
    table_head_cell: {
      backgroundColor: '#f5f5f5',
      padding: 8,
      borderWidth: 1,
      borderColor: '#ddd',
      fontWeight: 'bold',
    },
    table_cell: {
      padding: 8,
      borderWidth: 1,
      borderColor: '#ddd',
    },
  };

  return (
    <View style={styles.container}>
      <View style={styles.toolbar}>
        <Text style={styles.title}>Preview Mode</Text>
        <View style={styles.toolbarButtons}>
          <TouchableOpacity style={styles.toolbarButton} onPress={() => onKeyPress('t')}>
            <Text style={styles.toolbarButtonText}>TOC</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.toolbarButton} onPress={() => onKeyPress('i')}>
            <Text style={styles.toolbarButtonText}>Edit</Text>
          </TouchableOpacity>
          <TouchableOpacity style={styles.toolbarButton} onPress={() => onKeyPress('f')}>
            <Text style={styles.toolbarButtonText}>Files</Text>
          </TouchableOpacity>
        </View>
      </View>

      <ScrollView
        ref={scrollViewRef}
        style={styles.content}
        contentContainerStyle={styles.contentContainer}
      >
        <MarkdownDisplay
          style={markdownStyles}
          rules={markdownRules}
        >
          {processContent(content)}
        </MarkdownDisplay>
      </ScrollView>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
  toolbar: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
  },
  toolbarButtons: {
    flexDirection: 'row',
  },
  toolbarButton: {
    backgroundColor: '#007AFF',
    paddingHorizontal: 8,
    paddingVertical: 4,
    borderRadius: 4,
    marginLeft: 8,
  },
  toolbarButtonText: {
    color: '#ffffff',
    fontSize: 12,
    fontWeight: 'bold',
  },
  content: {
    flex: 1,
  },
  contentContainer: {
    padding: 16,
  },
});

export default MarkdownPreview;

utils/storage.js

import AsyncStorage from '@react-native-async-storage/async-storage';
import * as FileSystem from 'expo-file-system';

const NOTES_KEY = 'markdown_notes';
const NOTES_DIR = FileSystem.documentDirectory + 'notes/';

// Ensure notes directory exists
const ensureNotesDirectory = async () => {
  const dirInfo = await FileSystem.getInfoAsync(NOTES_DIR);
  if (!dirInfo.exists) {
    await FileSystem.makeDirectoryAsync(NOTES_DIR, { intermediates: true });
  }
};

export const loadNotes = async () => {
  try {
    await ensureNotesDirectory();
    const notesJson = await AsyncStorage.getItem(NOTES_KEY);
    if (notesJson) {
      return JSON.parse(notesJson);
    }
    return [];
  } catch (error) {
    console.error('Error loading notes:', error);
    return [];
  }
};

export const saveNote = async (note) => {
  try {
    await ensureNotesDirectory();

    const notes = await loadNotes();
    const noteId = note.id || Date.now().toString();
    const updatedNote = {
      ...note,
      id: noteId,
      modified: new Date().toISOString(),
    };

    // Save to file system
    const fileName = `${noteId}.md`;
    const filePath = NOTES_DIR + fileName;
    await FileSystem.writeAsStringAsync(filePath, updatedNote.content);

    // Update notes array
    const existingIndex = notes.findIndex(n => n.id === noteId);
    if (existingIndex >= 0) {
      notes[existingIndex] = updatedNote;
    } else {
      notes.push(updatedNote);
    }

    // Save to AsyncStorage
    await AsyncStorage.setItem(NOTES_KEY, JSON.stringify(notes));

    return updatedNote;
  } catch (error) {
    console.error('Error saving note:', error);
    throw error;
  }
};

export const deleteNote = async (noteId) => {
  try {
    const notes = await loadNotes();
    const filteredNotes = notes.filter(note => note.id !== noteId);

    // Delete file
    const fileName = `${noteId}.md`;
    const filePath = NOTES_DIR + fileName;
    const fileInfo = await FileSystem.getInfoAsync(filePath);
    if (fileInfo.exists) {
      await FileSystem.deleteAsync(filePath);
    }

    // Update AsyncStorage
    await AsyncStorage.setItem(NOTES_KEY, JSON.stringify(filteredNotes));

    return filteredNotes;
  } catch (error) {
    console.error('Error deleting note:', error);
    throw error;
  }
};

export const exportNotesAsHTML = async () => {
  try {
    const notes = await loadNotes();
    const exportDir = FileSystem.documentDirectory + 'export/';

    // Ensure export directory exists
    const dirInfo = await FileSystem.getInfoAsync(exportDir);
    if (!dirInfo.exists) {
      await FileSystem.makeDirectoryAsync(exportDir, { intermediates: true });
    }

    // Generate HTML for each note
    for (const note of notes) {
      const html = generateHTMLFromMarkdown(note.content, note.title);
      const htmlPath = exportDir + `${note.id}.html`;
      await FileSystem.writeAsStringAsync(htmlPath, html);
    }

    // Generate index file
    const indexHTML = generateIndexHTML(notes);
    const indexPath = exportDir + 'index.html';
    await FileSystem.writeAsStringAsync(indexPath, indexHTML);

    return exportDir;
  } catch (error) {
    console.error('Error exporting notes:', error);
    throw error;
  }
};

const generateHTMLFromMarkdown = (content, title) => {
  // Basic markdown to HTML conversion
  // In a real app, you'd use a proper markdown parser
  return `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>${title}</title>
    <style>
        body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        code { background: #f0f0f0; padding: 2px 4px; border-radius: 3px; }
        pre { background: #f8f8f8; padding: 16px; border-radius: 6px; overflow-x: auto; }
        blockquote { border-left: 4px soli




# Complete Remaining Files for Markdown Notes App

## components/FileManager.js
```javascript
import React, { useState } from 'react';
import {
  View,
  Text,
  StyleSheet,
  FlatList,
  TouchableOpacity,
  Alert,
  TextInput,
  Modal,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import * as DocumentPicker from 'expo-document-picker';
import * as FileSystem from 'expo-file-system';
import { exportNotesAsHTML } from '../utils/storage';

const FileManager = ({ notes, currentNote, onNoteSelect, onDeleteNote, onRefresh, onKeyPress }) => {
  const [searchQuery, setSearchQuery] = useState('');
  const [showNewNoteModal, setShowNewNoteModal] = useState(false);
  const [newNoteTitle, setNewNoteTitle] = useState('');
  const [sortBy, setSortBy] = useState('modified'); // 'modified', 'title', 'created'

  const filteredNotes = notes
    .filter(note => 
      note.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
      note.content.toLowerCase().includes(searchQuery.toLowerCase())
    )
    .sort((a, b) => {
      switch (sortBy) {
        case 'title':
          return a.title.localeCompare(b.title);
        case 'created':
          return new Date(b.created) - new Date(a.created);
        case 'modified':
        default:
          return new Date(b.modified) - new Date(a.modified);
      }
    });

  const handleDeleteNote = (noteId) => {
    Alert.alert(
      'Delete Note',
      'Are you sure you want to delete this note?',
      [
        { text: 'Cancel', style: 'cancel' },
        {
          text: 'Delete',
          style: 'destructive',
          onPress: async () => {
            try {
              await onDeleteNote(noteId);
              onRefresh();
            } catch (error) {
              Alert.alert('Error', 'Failed to delete note');
            }
          },
        },
      ]
    );
  };

  const handleCreateNote = () => {
    if (newNoteTitle.trim()) {
      const newNote = {
        id: Date.now().toString(),
        title: newNoteTitle.trim(),
        content: `# ${newNoteTitle.trim()}\n\nStart writing your note here...`,
        created: new Date().toISOString(),
        modified: new Date().toISOString(),
      };
      onNoteSelect(newNote);
      setNewNoteTitle('');
      setShowNewNoteModal(false);
    }
  };

  const handleImportFile = async () => {
    try {
      const result = await DocumentPicker.getDocumentAsync({
        type: 'text/*',
        copyToCacheDirectory: true,
      });

      if (!result.canceled) {
        const content = await FileSystem.readAsStringAsync(result.assets[0].uri);
        const fileName = result.assets[0].name.replace(/\.[^/.]+$/, '');

        const importedNote = {
          id: Date.now().toString(),
          title: fileName,
          content: content,
          created: new Date().toISOString(),
          modified: new Date().toISOString(),
        };

        onNoteSelect(importedNote);
      }
    } catch (error) {
      Alert.alert('Error', 'Failed to import file');
    }
  };

  const handleExportNotes = async () => {
    try {
      const exportPath = await exportNotesAsHTML();
      Alert.alert(
        'Export Complete',
        `Notes exported to: ${exportPath}`,
        [
          { text: 'OK' },
          {
            text: 'Share',
            onPress: () => {
              // Implementation for sharing the export folder
            },
          },
        ]
      );
    } catch (error) {
      Alert.alert('Error', 'Failed to export notes');
    }
  };

  const formatDate = (dateString) => {
    const date = new Date(dateString);
    return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {
      hour: '2-digit',
      minute: '2-digit'
    });
  };

  const renderNoteItem = ({ item }) => (
    <TouchableOpacity
      style={[
        styles.noteItem,
        item.id === currentNote?.id && styles.selectedNoteItem
      ]}
      onPress={() => onNoteSelect(item)}
    >
      <View style={styles.noteContent}>
        <Text style={styles.noteTitle} numberOfLines={1}>
          {item.title}
        </Text>
        <Text style={styles.notePreview} numberOfLines={2}>
          {item.content.replace(/[#*`]/g, '').substring(0, 100)}
        </Text>
        <Text style={styles.noteDate}>
          Modified: {formatDate(item.modified)}
        </Text>
      </View>
      <TouchableOpacity
        style={styles.deleteButton}
        onPress={() => handleDeleteNote(item.id)}
      >
        <Ionicons name="trash-outline" size={20} color="#ff4444" />
      </TouchableOpacity>
    </TouchableOpacity>
  );

  return (
    <View style={styles.container}>
      <View style={styles.header}>
        <Text style={styles.title}>Notes ({notes.length})</Text>
        <View style={styles.headerButtons}>
          <TouchableOpacity style={styles.headerButton} onPress={() => setShowNewNoteModal(true)}>
            <Ionicons name="add" size={20} color="#ffffff" />
          </TouchableOpacity>
          <TouchableOpacity style={styles.headerButton} onPress={handleImportFile}>
            <Ionicons name="download-outline" size={20} color="#ffffff" />
          </TouchableOpacity>
          <TouchableOpacity style={styles.headerButton} onPress={handleExportNotes}>
            <Ionicons name="share-outline" size={20} color="#ffffff" />
          </TouchableOpacity>
        </View>
      </View>

      <View style={styles.searchContainer}>
        <TextInput
          style={styles.searchInput}
          placeholder="Search notes..."
          value={searchQuery}
          onChangeText={setSearchQuery}
        />
        <TouchableOpacity
          style={styles.sortButton}
          onPress={() => {
            const nextSort = sortBy === 'modified' ? 'title' : sortBy === 'title' ? 'created' : 'modified';
            setSortBy(nextSort);
          }}
        >
          <Text style={styles.sortButtonText}>
            Sort: {sortBy === 'modified' ? 'Date' : sortBy === 'title' ? 'Title' : 'Created'}
          </Text>
        </TouchableOpacity>
      </View>

      <FlatList
        data={filteredNotes}
        renderItem={renderNoteItem}
        keyExtractor={(item) => item.id}
        style={styles.notesList}
        contentContainerStyle={styles.notesListContent}
        showsVerticalScrollIndicator={false}
      />

      <View style={styles.footer}>
        <Text style={styles.footerText}>ESC to go back</Text>
      </View>

      <Modal
        visible={showNewNoteModal}
        animationType="slide"
        transparent={true}
        onRequestClose={() => setShowNewNoteModal(false)}
      >
        <View style={styles.modalOverlay}>
          <View style={styles.modalContent}>
            <Text style={styles.modalTitle}>Create New Note</Text>
            <TextInput
              style={styles.modalInput}
              placeholder="Note title"
              value={newNoteTitle}
              onChangeText={setNewNoteTitle}
              autoFocus
            />
            <View style={styles.modalButtons}>
              <TouchableOpacity
                style={[styles.modalButton, styles.cancelButton]}
                onPress={() => setShowNewNoteModal(false)}
              >
                <Text style={styles.cancelButtonText}>Cancel</Text>
              </TouchableOpacity>
              <TouchableOpacity
                style={[styles.modalButton, styles.createButton]}
                onPress={handleCreateNote}
              >
                <Text style={styles.createButtonText}>Create</Text>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </Modal>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#ffffff',
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    paddingHorizontal: 16,
    paddingVertical: 12,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
    backgroundColor: '#f5f5f5',
  },
  title: {
    fontSize: 18,
    fontWeight: 'bold',
    color: '#333',
  },
  headerButtons: {
    flexDirection: 'row',
  },
  headerButton: {
    backgroundColor: '#007AFF',
    padding: 8,
    borderRadius: 4,
    marginLeft: 8,
  },
  searchContainer: {
    flexDirection: 'row',
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderBottomWidth: 1,
    borderBottomColor: '#e0e0e0',
  },
  searchInput: {
    flex: 1,
    height: 36,
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 4,
    paddingHorizontal: 12,
    marginRight: 8,
  },
  sortButton: {
    backgroundColor: '#f0f0f0',
    paddingHorizontal: 12,
    paddingVertical: 8,
    borderRadius: 4,
    justifyContent: 'center',
  },
  sortButtonText: {
    fontSize: 12,
    color: '#666',
  },
  notesList: {
    flex: 1,
  },
  notesListContent: {
    padding: 16,
  },
  noteItem: {
    flexDirection: 'row',
    backgroundColor: '#f9f9f9',
    borderRadius: 8,
    padding: 12,
    marginBottom: 8,
    borderWidth: 1,
    borderColor: '#e0e0e0',
  },
  selectedNoteItem: {
    backgroundColor: '#e3f2fd',
    borderColor: '#007AFF',
  },
  noteContent: {
    flex: 1,
  },
  noteTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#333',
    marginBottom: 4,
  },
  notePreview: {
    fontSize: 14,
    color: '#666',
    lineHeight: 20,
    marginBottom: 4,
  },
  noteDate: {
    fontSize: 12,
    color: '#999',
  },
  deleteButton: {
    padding: 8,
    justifyContent: 'center',
    alignItems: 'center',
  },
  footer: {
    padding: 16,
    borderTopWidth: 1,
    borderTopColor: '#e0e0e0',
    backgroundColor: '#f5f5f5',
  },
  footerText: {
    textAlign: 'center',
    color: '#666',
    fontSize: 12,
  },
  modalOverlay: {
    flex: 1,
    backgroundColor: 'rgba(0, 0, 0, 0.5)',
    justifyContent: 'center',
    alignItems: 'center',
  },
  modalContent: {
    backgroundColor: 'white',
    padding: 20,
    borderRadius: 8,
    width: '80%',
    maxWidth: 300,
  },
  modalTitle: {
    fontSize: 18,
    fontWeight: 'bold',
    marginBottom: 16,
    textAlign: 'center',
  },
  modalInput: {
    borderWidth: 1,
    borderColor: '#ddd',
    borderRadius: 4,
    padding: 12,
    marginBottom: 16,
  },
  modalButtons: {
    flexDirection: 'row',
    justifyContent: 'space-between',
  },
  modalButton: {
    flex: 1,
    padding: 12,
    borderRadius: 4,
    marginHorizontal: 4,
  },
  cancelButton: {
    backgroundColor: '#f0f0f0',
  },
  createButton: {
    backgroundColor: '#007AFF',
  },
  cancelButtonText: {
    textAlign: 'center',
    color: '#333',
  },
  createButtonText: {
    textAlign: 'center',
    color: '#ffffff',
    fontWeight: 'bold',
  },
});

export default FileManager;

components/MathRenderer.js

import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';

const MathRenderer = ({ content, inline = false }) => {
  const mathHTML = `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-mml-chtml.js"></script>
    <style>
        body {
            margin: 0;
            padding: ${inline ? '0' : '8px'};
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            background: transparent;
            overflow: hidden;
        }
        .math-container {
            ${inline ? 'display: inline;' : 'display: block; text-align: center;'}
            font-size: ${inline ? '16px' : '18px'};
        }
        mjx-container {
            ${inline ? 'display: inline !important;' : ''}
        }
    </style>
    <script>
        window.MathJax = {
            tex: {
                inlineMath: [['$', '$'], ['\\\\(', '\\\\)']],
                displayMath: [['$$', '$$'], ['\\\\[', '\\\\]']],
                processEscapes: true,
                processEnvironments: true
            },
            options: {
                skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre']
            },
            startup: {
                ready: () => {
                    MathJax.startup.defaultReady();
                    MathJax.startup.promise.then(() => {
                        // Auto-resize the WebView
                        const height = document.body.scrollHeight;
                        window.ReactNativeWebView?.postMessage(JSON.stringify({
                            type: 'resize',
                            height: height
                        }));
                    });
                }
            }
        };
    </script>
</head>
<body>
    <div class="math-container">
        ${inline ? `$${content}$` : `$$${content}$$`}
    </div>
</body>
</html>`;

  const [webViewHeight, setWebViewHeight] = React.useState(inline ? 30 : 60);

  const handleMessage = (event) => {
    try {
      const data = JSON.parse(event.nativeEvent.data);
      if (data.type === 'resize') {
        setWebViewHeight(Math.max(data.height, inline ? 30 : 60));
      }
    } catch (error) {
      console.error('Error parsing WebView message:', error);
    }
  };

  return (
    <View style={[styles.container, inline && styles.inlineContainer]}>
      <WebView
        source={{ html: mathHTML }}
        style={[styles.webView, { height: webViewHeight }]}
        onMessage={handleMessage}
        scrollEnabled={false}
        showsVerticalScrollIndicator={false}
        showsHorizontalScrollIndicator={false}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        startInLoadingState={false}
        mixedContentMode="compatibility"
        allowsInlineMediaPlayback={true}
        mediaPlaybackRequiresUserAction={false}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    marginVertical: 8,
    backgroundColor: 'transparent',
  },
  inlineContainer: {
    marginVertical: 0,
    display: 'flex',
    flexDirection: 'row',
  },
  webView: {
    backgroundColor: 'transparent',
  },
});

export default MathRenderer;

components/MermaidRenderer.js

import React, { useState } from 'react';
import { View, StyleSheet, Text, TouchableOpacity } from 'react-native';
import { WebView } from 'react-native-webview';

const MermaidRenderer = ({ content }) => {
  const [webViewHeight, setWebViewHeight] = useState(200);
  const [error, setError] = useState(null);

  const mermaidHTML = `
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/9.4.3/mermaid.min.js"></script>
    <style>
        body {
            margin: 0;
            padding: 16px;
            font-family: -apple-system, BlinkMacSystemFont, sans-serif;
            background: #ffffff;
            overflow-x: auto;
        }
        .mermaid {
            text-align: center;
            background: #ffffff;
        }
        .error {
            color: #ff4444;
            padding: 16px;
            background: #ffebee;
            border-radius: 4px;
            border-left: 4px solid #ff4444;
        }
    </style>
</head>
<body>
    <div id="diagram">
        <div class="mermaid">
${content}
        </div>
    </div>

    <script>
        mermaid.initialize({
            startOnLoad: true,
            theme: 'default',
            themeVariables: {
                primaryColor: '#007AFF',
                primaryTextColor: '#333333',
                primaryBorderColor: '#007AFF',
                lineColor: '#333333',
                secondaryColor: '#f0f0f0',
                tertiaryColor: '#ffffff'
            },
            flowchart: {
                useMaxWidth: true,
                htmlLabels: true
            },
            sequence: {
                diagramMarginX: 50,
                diagramMarginY: 10,
                actorMargin: 50,
                width: 150,
                height: 65,
                boxMargin: 10,
                boxTextMargin: 5,
                noteMargin: 10,
                messageMargin: 35,
                mirrorActors: true,
                bottomMarginAdj: 1,
                useMaxWidth: true
            },
            gantt: {
                titleTopMargin: 25,
                barHeight: 20,
                fontFamily: '"Open-Sans", "sans-serif"',
                fontSize: 11,
                gridLineStartPadding: 35,
                bottomPadding: 25,
                bottomMarginAdj: 1
            }
        });

        // Function to resize WebView
        function resizeWebView() {
            const height = Math.max(document.body.scrollHeight, 200);
            if (window.ReactNativeWebView) {
                window.ReactNativeWebView.postMessage(JSON.stringify({
                    type: 'resize',
                    height: height
                }));
            }
        }

        // Wait for Mermaid to render
        setTimeout(() => {
            resizeWebView();
        }, 1000);

        // Listen for window resize
        window.addEventListener('resize', resizeWebView);

        // Error handling
        window.addEventListener('error', function(e) {
            if (window.ReactNativeWebView) {
                window.ReactNativeWebView.postMessage(JSON.stringify({
                    type: 'error',
                    message: e.message
                }));
            }
        });

        // Mermaid error handling
        mermaid.parseError = function(err, hash) {
            if (window.ReactNativeWebView) {
                window.ReactNativeWebView.postMessage(JSON.stringify({
                    type: 'error',
                    message: 'Mermaid parsing error: ' + err
                }));
            }
        };
    </script>
</body>
</html>`;

  const handleMessage = (event) => {
    try {
      const data = JSON.parse(event.nativeEvent.data);
      if (data.type === 'resize') {
        setWebViewHeight(Math.max(data.height, 200));
        setError(null);
      } else if (data.type === 'error') {
        setError(data.message);
      }
    } catch (error) {
      console.error('Error parsing WebView message:', error);
      setError('Failed to render diagram');
    }
  };

  const handleRetry = () => {
    setError(null);
    setWebViewHeight(200);
  };

  if (error) {
    return (
      <View style={styles.errorContainer}>
        <Text style={styles.errorTitle}>Mermaid Diagram Error</Text>
        <Text style={styles.errorMessage}>{error}</Text>
        <TouchableOpacity style={styles.retryButton} onPress={handleRetry}>
          <Text style={styles.retryButtonText}>Retry</Text>
        </TouchableOpacity>
        <View style={styles.codeContainer}>
          <Text style={styles.codeTitle}>Original Code:</Text>
          <Text style={styles.codeText}>{content}</Text>
        </View>
      </View>
    );
  }

  return (
    <View style={styles.container}>
      <WebView
        source={{ html: mermaidHTML }}
        style={[styles.webView, { height: webViewHeight }]}
        onMessage={handleMessage}
        scrollEnabled={false}
        showsVerticalScrollIndicator={false}
        showsHorizontalScrollIndicator={false}
        javaScriptEnabled={true}
        domStorageEnabled={true}
        startInLoadingState={false}
        mixedContentMode="compatibility"
        allowsInlineMediaPlaybook={true}
        mediaPlaybackRequiresUserAction={false}
        onError={(syntheticEvent) => {
          const { nativeEvent } = syntheticEvent;
          setError(`WebView error: ${nativeEvent.description}`);
        }}
      />
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    marginVertical: 12,
    backgroundColor: '#ffffff',
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#e0e0e0',
    overflow: 'hidden',
  },
  webView: {
    backgroundColor: '#ffffff',
  },
  errorContainer: {
    backgroundColor: '#ffebee',
    borderRadius: 8,
    borderWidth: 1,
    borderColor: '#ffcdd2',
    padding: 16,
    marginVertical: 12,
  },
  errorTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#d32f2f',
    marginBottom: 8,
  },
  errorMessage: {
    fontSize: 14,
    color: '#d32f2f',
    marginBottom: 16,
  },
  retryButton: {
    backgroundColor: '#d32f2f',
    paddingHorizontal: 16,
    paddingVertical: 8,
    borderRadius: 4,
    alignSelf: 'flex-start',
    marginBottom: 16,
  },
  retryButtonText: {
    color: '#ffffff',
    fontWeight: 'bold',
  },
  codeContainer: {
    backgroundColor: '#f5f5f5',
    borderRadius: 4,
    padding: 12,
  },
  codeTitle: {
    fontSize: 12,
    fontWeight: 'bold',
    color: '#666',
    marginBottom: 8,
  },
  codeText: {
    fontSize: 12,
    fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
    color: '#333',
  },
});

export default MermaidRenderer;

components/TableOfContents.js

```javascript import React from 'react'; import { View, Text, StyleSheet, FlatList, TouchableOpacity, } from 'react-native'; import { Ionicons } from 'expo/vector-icons';

const TableOfContents = ({ items, onItemPress, onKeyPress }) => { const renderTocItem = ({ item, index }) => { const indentLevel = item.level - 1; const indentStyle = { marginLeft: indentLevel * 20, };

return (
  <TouchableOpacity
    style={[styles.tocItem, indentStyle]}
    onPress={() => onItemPress(item)}
  >
    <View style={styles.tocItemContent}>
      <View style={[styles.levelIndicator, { backgroundColor: getLevelColor(item.level) }]} />
      <Text style={[styles.tocText, { fontSize: getTocFontSize(item.level) }]}>
        {item.text}
      </Text>
    </View>
    <Text style={styles.tocLevel}>H{item.level}</Text>
  </TouchableOpacity>
);

};

const getLevelColor = (level) => { const colors = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE', '#00C7BE']; return colors[(level - 1) % colors.length]; };

const getTocFontSize = (level) => { switch (level) { case 1: return 18; case 2: return 16; case 3: return 15; case 4: return 14; case 5: return 13; case 6: return 12; default: return 14; } };

const getTocStats = () => { const stats = {}; items.forEach(item => { stats[item.level] = (stats[item.level] || 0) + 1; }); return stats; };

const stats = getTocStats();

return ( Table of Contents onKeyPress('Escape')}>

  {items.length > 0 && (
    <View style={styles.statsContainer}>
      <Text style={styles.statsTitle}>Document Structure:</Text>
      <View style={styles.statsRow}>
        {Object.entries(stats).map(([level, count]) => (
          <View key={level} style={styles.statItem}>
            <View style={[styles.statIndicator, { backgroundColor: getLevelColor(parseInt(level)) }]} />
            <Text style={styles.statText}>H{level}: {count}</Text>
          </View>
        ))}
      </View>
    </View>
  )}

  {items.length === 0 ? (
    <View style={styles.emptyContainer}>
      <Ionicons name="document-text-outline" size={48} color="#ccc" />
      <Text style={styles.emptyTitle}>No Headings Found</Text>
      <Text style={styles.emptyText}>
        Add headings to your document using # symbols to generate a table of contents.
      </Text>
      <View style={styles.exampleContainer}>
        <Text style={styles.exampleTitle}>Examples:</Text>
        <Text style={styles.exampleText}># Main Heading</Text>
        <Text style={styles.exampleText}>## Sub Heading</Text>
        <Text style={styles.exampleText}>### Sub-sub Heading</Text>
      </View>
    </View>
  ) : (
    <FlatList
      data={items}
      renderItem={renderTocItem}
      keyExtractor={(item, index) => `${item.level}-${index}`}
      style={styles.tocList}
      contentContainerStyle={styles.tocListContent}
      showsVerticalScrollIndicator={false}
    />
  )}

  <View style={styles.footer}>
    <Text style={styles.footerText}>
      {items.length > 0 ? 'Tap a heading to navigate • ' : ''}ESC to go back
    </Text>
  </View>
</View>

); };

const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#ffffff', }, header: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 12, borderBottomWidth: 1, borderBottomColor: '#e0e0e0', backgroundColor: '#f5f5f5', }, title: { fontSize: 18, fontWeight: 'bold', color: '#333', }, closeButton: { padding: 4, }, statsContainer: { paddingHorizontal: 16, paddingVertical: 12, backgroundColor: '#f9f9f9', borderBottomWidth: 1, borderBottomColor: '#e0e0e0', }, statsTitle: { fontSize: 14, fontWeight: '600', color: '#666', marginBottom: 8, }, statsRow: { flexDirection: 'row', flexWrap: 'wrap', }, statItem: { flexDirection: 'row', alignItems: 'center', marginRight: 16, marginBottom: 4, }, statIndicator: { width: 8, height: 8, borderRadius: 4, marginRight: 6, }, statText: { fontSize: 12, color: '#666', }, tocList: { flex: 1, }, tocListContent: { paddingVertical: 8, },