您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();