Marumori.io Notes Sidebar with Rich Text Editor

Add a notes sidebar with a rich text editor to Marumori.io lesson pages

// ==UserScript==
// @name         Marumori.io Notes Sidebar with Rich Text Editor
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Add a notes sidebar with a rich text editor to Marumori.io lesson pages
// @author       Matskye
// @icon         https://www.google.com/s2/favicons?sz=64&domain=marumori.io
// @match        https://marumori.io/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Initialize IndexedDB
    const dbName = 'MarumoriNotesDB';
    const storeName = 'notes';
    let db;
    let currentLesson = null;
    let isSidebarOpen = false;
    let originalStyles = {};

    const request = indexedDB.open(dbName);

    request.onerror = function(event) {
        console.error('Database error:', event.target.error);
    };

    request.onsuccess = function(event) {
        db = event.target.result;
        setupMutationObserver();
    };

    request.onupgradeneeded = function(event) {
        const db = event.target.result;
        if (!db.objectStoreNames.contains(storeName)) {
            db.createObjectStore(storeName, { keyPath: 'lesson' });
        }
    };

    function setupMutationObserver() {
        const targetNode = document.body;
        const config = { childList: true, subtree: true };

        let debounceTimer;
        const debounceDelay = 500;

        const callback = function(mutationsList, observer) {
            clearTimeout(debounceTimer);
            debounceTimer = setTimeout(() => {
                checkForLessonChange();
            }, debounceDelay);
        };

        const observer = new MutationObserver(callback);
        observer.observe(targetNode, config);
    }

    function checkForLessonChange() {
        const isLessonPage = document.querySelector('.lesson-background-wrapper') !== null;

        if (!isLessonPage) {
            const sidebar = document.getElementById('notes-sidebar');
            if (sidebar) {
                sidebar.remove();
            }
            const toggleButton = document.getElementById('toggle-sidebar');
            if (toggleButton) {
                toggleButton.remove();
            }
            return;
        }

        const lessonTag = document.querySelector('.tag.default');
        if (lessonTag) {
            const lessonInfo = lessonTag.textContent.trim();
            if (/#\d+/.test(lessonInfo)) {
                if (currentLesson !== lessonInfo) {
                    currentLesson = lessonInfo;
                    checkOrCreateNote(lessonInfo);
                }
            } else {
                const sidebar = document.getElementById('notes-sidebar');
                if (sidebar) {
                    sidebar.remove();
                }
                const toggleButton = document.getElementById('toggle-sidebar');
                if (toggleButton) {
                    toggleButton.remove();
                }
            }
        }

        const markWordsButton = document.querySelector('span.svelte-1mbo79u');
        if (markWordsButton && markWordsButton.textContent.includes('I want to mark words as known')) {
            const sidebar = document.getElementById('notes-sidebar');
            if (sidebar) {
                sidebar.remove();
            }
            const toggleButton = document.getElementById('toggle-sidebar');
            if (toggleButton) {
                toggleButton.remove();
            }
        }
    }

    function checkOrCreateNote(lesson) {
        const transaction = db.transaction(storeName, 'readwrite');
        const store = transaction.objectStore(storeName);
        const getRequest = store.get(lesson);

        getRequest.onsuccess = function(event) {
            const note = event.target.result || { lesson: lesson, content: '' };
            updateNotesSidebar(note);
        };
    }

    function updateNotesSidebar(note) {
        let sidebar = document.getElementById('notes-sidebar');
        let toggleButton = document.getElementById('toggle-sidebar');

        if (!sidebar) {
            sidebar = document.createElement('div');
            sidebar.id = 'notes-sidebar';
            sidebar.style.position = 'fixed';
            sidebar.style.right = '0';
            sidebar.style.top = '0';
            sidebar.style.width = '33%';
            sidebar.style.height = '100%';
            sidebar.style.backgroundColor = 'hsl(200, 4%, 14%)';
            sidebar.style.borderLeft = '1px solid #ddd';
            sidebar.style.padding = '10px';
            sidebar.style.boxSizing = 'border-box';
            sidebar.style.transform = 'translateX(100%)';
            sidebar.style.transition = 'transform 0.3s ease';
            sidebar.style.zIndex = '1000';
            sidebar.style.overflowY = 'auto';
            sidebar.style.color = '#fff';

            toggleButton = document.createElement('button');
            toggleButton.id = 'toggle-sidebar';
            toggleButton.style.position = 'fixed';
            toggleButton.style.right = '10px';
            toggleButton.style.top = '10px';
            toggleButton.style.zIndex = '1001';
            toggleButton.textContent = '📝';
            toggleButton.onclick = function() {
                toggleSidebar();
            };

            document.body.appendChild(toggleButton);
            document.body.appendChild(sidebar);
        }

        let noteContent = document.getElementById('note-content');
        if (!noteContent) {
            noteContent = document.createElement('div');
            noteContent.id = 'note-content';
            noteContent.style.padding = '10px';
            noteContent.style.minHeight = '200px';
            noteContent.contentEditable = true;
            noteContent.style.outline = 'none';
            sidebar.appendChild(noteContent);
        }

        // Set initial content
        noteContent.innerHTML = note.content;

        // Add formatting toolbar
        addFormattingToolbar(sidebar, noteContent);

        // Style hyperlinks
        const style = document.createElement('style');
        style.innerHTML = `
            #note-content a {
                color: #0078d7;
                text-decoration: underline;
            }
        `;
        document.head.appendChild(style);

        let exportButtonContainer = document.getElementById('export-button-container');
        if (!exportButtonContainer) {
            exportButtonContainer = document.createElement('div');
            exportButtonContainer.id = 'export-button-container';
            exportButtonContainer.style.position = 'relative';
            exportButtonContainer.style.display = 'inline-block';
            exportButtonContainer.style.margin = '10px 0';

            const exportButton = document.createElement('button');
            exportButton.id = 'export-button';
            exportButton.textContent = 'Export Note';
            exportButton.style.backgroundColor = 'hsl(200, 4%, 24%)';
            exportButton.style.color = '#fff';
            exportButton.style.border = 'none';
            exportButton.style.padding = '8px 12px';
            exportButton.style.borderRadius = '4px';
            exportButton.style.cursor = 'pointer';
            exportButton.style.marginRight = '10px';

            const exportDropdown = document.createElement('select');
            exportDropdown.id = 'export-dropdown';
            exportDropdown.style.backgroundColor = 'hsl(200, 4%, 24%)';
            exportDropdown.style.color = '#fff';
            exportDropdown.style.border = 'none';
            exportDropdown.style.padding = '8px';
            exportDropdown.style.borderRadius = '4px';
            exportDropdown.style.marginRight = '10px';

            const optionCurrent = document.createElement('option');
            optionCurrent.value = 'current';
            optionCurrent.textContent = 'Current Note';
            exportDropdown.appendChild(optionCurrent);

            const optionAll = document.createElement('option');
            optionAll.value = 'all';
            optionAll.textContent = 'All Notes';
            exportDropdown.appendChild(optionAll);

            exportButton.onclick = function() {
                const selectedOption = exportDropdown.value;
                if (selectedOption === 'current') {
                    exportNote({ lesson: currentLesson, content: noteContent.innerHTML });
                } else if (selectedOption === 'all') {
                    exportAllNotes();
                }
            };

            exportButtonContainer.appendChild(exportButton);
            exportButtonContainer.appendChild(exportDropdown);
            sidebar.appendChild(exportButtonContainer);
        }

        let importButton = document.getElementById('import-button');
        if (!importButton) {
            importButton = document.createElement('button');
            importButton.id = 'import-button';
            importButton.textContent = 'Import Note';
            importButton.style.backgroundColor = 'hsl(200, 4%, 24%)';
            importButton.style.color = '#fff';
            importButton.style.border = 'none';
            importButton.style.padding = '8px 12px';
            importButton.style.borderRadius = '4px';
            importButton.style.cursor = 'pointer';
            importButton.style.marginTop = '10px';
            importButton.onclick = function() {
                importNote();
            };
            sidebar.appendChild(importButton);
        }

        // Save note on content change
        noteContent.addEventListener('input', function() {
            saveNote(currentLesson, noteContent.innerHTML);
        });
    }

    function addFormattingToolbar(sidebar, noteContent) {
        const toolbar = document.createElement('div');
        toolbar.style.marginBottom = '10px';

        const buttons = [
            { name: 'Bold', command: 'bold' },
            { name: 'Italic', command: 'italic' },
            { name: 'Underline', command: 'underline' },
            { name: 'Strikethrough', command: 'strikeThrough' },
            { name: 'Link', action: () => createLink() }
        ];

        buttons.forEach(button => {
            const btn = document.createElement('button');
            btn.textContent = button.name;
            btn.style.backgroundColor = 'hsl(200, 4%, 24%)';
            btn.style.color = '#fff';
            btn.style.border = 'none';
            btn.style.padding = '8px 12px';
            btn.style.borderRadius = '4px';
            btn.style.cursor = 'pointer';
            btn.style.marginRight = '5px';

            if (button.command) {
                btn.onclick = () => document.execCommand(button.command, false, null);
            } else if (button.action) {
                btn.onclick = button.action;
            }

            toolbar.appendChild(btn);
        });

        sidebar.insertBefore(toolbar, sidebar.firstChild);

        function createLink() {
            const url = prompt('Enter the URL:');
            if (url) {
                document.execCommand('createLink', false, url);
            }
        }
    }

    function toggleSidebar() {
        const sidebar = document.getElementById('notes-sidebar');
        const lessonWrapper = document.querySelector('main.lesson-wrapper');

        if (!sidebar || !lessonWrapper) return;

        isSidebarOpen = !isSidebarOpen;
        sidebar.style.transform = isSidebarOpen ? 'translateX(0%)' : 'translateX(100%)';

        if (isSidebarOpen) {
            originalStyles.marginRight = lessonWrapper.style.marginRight;
            lessonWrapper.style.marginRight = '33%';
        } else {
            lessonWrapper.style.marginRight = originalStyles.marginRight || '';
        }
    }

    function saveNote(lesson, content) {
        const transaction = db.transaction(storeName, 'readwrite');
        const store = transaction.objectStore(storeName);
        const note = { lesson: lesson, content: content };
        store.put(note);
    }

    function exportNote(note) {
        const blob = new Blob([JSON.stringify(note, null, 2)], { type: 'application/json' });
        const url = URL.createObjectURL(blob);
        const a = document.createElement('a');
        a.href = url;
        a.download = `note_${note.lesson}.json`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    }

    function exportAllNotes() {
        const transaction = db.transaction(storeName, 'readonly');
        const store = transaction.objectStore(storeName);
        const getAllRequest = store.getAll();

        getAllRequest.onsuccess = function(event) {
            const allNotes = event.target.result;
            const blob = new Blob([JSON.stringify(allNotes, null, 2)], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `all_notes.json`;
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        };
    }

    function importNote() {
        const input = document.createElement('input');
        input.type = 'file';
        input.accept = '.json';
        input.onchange = function(event) {
            const file = event.target.files[0];
            const reader = new FileReader();
            reader.onload = function(event) {
                const note = JSON.parse(event.target.result);
                const transaction = db.transaction(storeName, 'readwrite');
                const store = transaction.objectStore(storeName);
                store.put(note);
                location.reload();
            };
            reader.readAsText(file);
        };
        input.click();
    }
})();