// ==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();
}
})();