Professional notes manager with editable URLs, modern interface, and quick delete functionality
// ==UserScript== // @name Professional Website Notes Manager // @namespace http://tampermonkey.net/ // @version 0.9.1 // @description Professional notes manager with editable URLs, modern interface, and quick delete functionality // @author Byakuran // @match https://*/* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/markdown-it.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_listValues // @grant GM_deleteValue // ==/UserScript== (function() { 'use strict'; // Check if the script is running in the main document if (window.self !== window.top) { return; // Exit if inside an iframe } const md = markdownit({ breaks: true, linkify: true }); let changeLog = ` Changes from 0.8 → 0.9: - Script only in main document - Non-colour option re-created - Scrolling of complete note, or only text inside - Removed linkify and implemented proper markdown rendering - Added merging without duplicates - Improved updateNote function - Added caching for compiled regex patterns - Significantly improved search functionality with caching and max result limits - Improved CSS managing - Improved pattern matching for speed - Removed duplicate search - Better limit reached functionality for searching - Improved caching for patterns (limited to 10k entries) - Fixed escape key listener cleanup when closing modal - Added alert for duplicate shortcuts - Added limiting amount of notes shown per page Changes from 0.9.0 → 0.9.1: - Added Note IDs for importing and easier merging - Added non-intrusive notifications for saving/updating/deleting Planned for 0.10: - Markdown Preview with split-pane view or Preview button `; //changing allows for fixing settings keys let scriptVersion = '0.9.1' const defaultOptions = { version: scriptVersion, darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches, addTimestampToTitle: false, showUrlLinksInNotesList: true, autoBackup: true, scrollContentOnly: false, maxSearchResults: 50, maxPatternsPerPage: 50, shortcuts: { newNote: { ctrlKey: true, shiftKey: true, key: 'S' }, currentPageNotes: { ctrlKey: true, shiftKey: true, key: 'C' }, allNotes: { ctrlKey: true, shiftKey: true, key: 'L' }, showOptions: { ctrlKey: true, altKey: true, key: 'O' } } }; let options = checkAndUpdateOptions(); GM_setValue('options', options); function checkAndUpdateOptions() { let currentOptions; try { currentOptions = GM_getValue('options', defaultOptions); } catch (error) { console.error('Error loading options, resetting to defaults:', error); return defaultOptions; } // If options is not an object for some reason if (!currentOptions || typeof currentOptions !== 'object') { console.warn('Invalid options found, resetting to defaults'); return defaultOptions; } // Check if the version has changed or if it doesn't exist if (!currentOptions.version || currentOptions.version !== defaultOptions.version) { // Version has changed, update options for (let key in defaultOptions) { if (!(key in currentOptions)) { currentOptions[key] = defaultOptions[key]; } } // Update nested objects (shortcuts, possibly more later) if (!currentOptions.shortcuts || typeof currentOptions.shortcuts !== 'object') { currentOptions.shortcuts = defaultOptions.shortcuts; } else { for (let key in defaultOptions.shortcuts) { if (!(key in currentOptions.shortcuts)) { currentOptions.shortcuts[key] = defaultOptions.shortcuts[key]; } } } // Update the version currentOptions.version = defaultOptions.version; // Save the updated options GM_setValue('options', currentOptions); // Migrate existing notes to add IDs migrateNotesToAddIds(); alert('Options updated to version ' + defaultOptions.version + changeLog); console.log('Options updated to version ' + defaultOptions.version); } return currentOptions; } function migrateNotesToAddIds() { try { const notes = getAllNotes(); let migrated = false; for (const url in notes) { notes[url].forEach(note => { if (!note.id) { note.id = generateNoteId(); migrated = true; } }); } if (migrated) { GM_setValue('website-notes', notes); console.log('Migration completed: IDs added to existing notes'); } } catch (error) { console.error('Error during note migration:', error); } } const isDarkMode = options.darkMode; const darkModeStyles = { modal: { bg: '#1f2937', text: '#f3f4f6' }, input: { bg: '#374151', border: '#4b5563', text: '#f3f4f6' }, button: { primary: '#3b82f6', primaryHover: '#2563eb', secondary: '#4b5563', secondaryHover: '#374151', text: '#ffffff' }, listItem: { bg: '#374151', bgHover: '#4b5563', text: '#f3f4f6' } }; const lightModeStyles = { modal: { bg: '#ffffff', text: '#111827' }, input: { bg: '#f9fafb', border: '#e5e7eb', text: '#111827' }, button: { primary: '#3b82f6', primaryHover: '#2563eb', secondary: '#f3f4f6', secondaryHover: '#e5e7eb', text: '#ffffff' }, listItem: { bg: '#ffffff', bgHover: '#f9fafb', text: '#1f2937' } }; const currentTheme = isDarkMode ? darkModeStyles : lightModeStyles; const styles = ` .notes-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,${isDarkMode ? '0.8' : '0.7'}); z-index: 99998; backdrop-filter: blur(4px); contain: layout style paint; } .notes-overlay{ h1 { font-size: 1.5em; margin-top: 0.8em; margin-bottom: 0.5em; } h2 { font-size: 1.3em; margin-top: 0.7em; margin-bottom: 0.4em; } ul { list-style-type: disc; margin-left: 20px; } blockquote { border-left: 3px solid #9ca3af; padding-left: 10px; margin-left: 5px; color: #6b7280; } pre { background-color: #2a3441; padding: 10px; border-radius: 6px; overflow-x: auto; } code { background-color: #374151; padding: 2px 4px; border-radius: 4px; } table { border-collapse: collapse; width: 100%; } th, td { border: 1px solid #4b5563; padding: 8px; } hr { border: none; border-top: 1px solid ${isDarkMode ? '#4b5563' : '#e5e7eb'}; margin: 16px 0;} } .notes-overlay { .notes-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: ${currentTheme.modal.bg}; color: ${currentTheme.modal.text}; padding: 32px; border-radius: 16px; box-shadow: 0 8px 32px rgba(0,0,0,0.25); z-index: 10000; max-width: 700px; width: 90%; max-height: 90vh; overflow-y: auto; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .notes-input { width: 100%; margin: 12px 0; padding: 12px 16px; border: 2px solid ${currentTheme.input.border}; border-radius: 8px; font-size: 15px; transition: all 0.2s ease; background: ${currentTheme.input.bg}; color: ${currentTheme.input.text}; box-sizing: border-box; } .notes-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .notes-textarea { width: 100%; height: 200px; margin: 12px 0; padding: 16px; border: 2px solid ${currentTheme.input.border}; border-radius: 8px; font-size: 15px; resize: vertical; transition: all 0.2s ease; background: ${currentTheme.input.bg}; color: ${currentTheme.input.text}; line-height: 1.5; box-sizing: border-box; } .notes-textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .notes-button { background: ${currentTheme.button.primary}; color: ${currentTheme.button.text}; border: none; padding: 12px 24px; border-radius: 8px; cursor: pointer; margin: 5px; font-size: 15px; font-weight: 500; transition: all 0.2s ease; } .notes-button:hover { background: ${currentTheme.button.primaryHover}; transform: translateY(-1px); } .notes-button.secondary { background: ${currentTheme.button.secondary}; color: ${isDarkMode ? '#f3f4f6' : '#4b5563'}; } .notes-button.secondary:hover { background: ${currentTheme.button.secondaryHover}; } .notes-button.delete { background: #ef4444; } .notes-button.delete:hover { background: #dc2626; } .notes-button.edit { background: #10b981; } .notes-button.edit:hover { background: #059669; } .notes-list-item { display: flex; justify-content: space-between; align-items: center; padding: 16px; border: 1px solid ${currentTheme.input.border}; border-radius: 8px; margin: 8px 0; cursor: pointer; transition: all 0.2s ease; background: ${currentTheme.listItem.bg}; color: ${currentTheme.listItem.text}; contain: layout paint; } .notes-list-item:hover { background: ${currentTheme.listItem.bgHover}; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0,0,0,${isDarkMode ? '0.3' : '0.05'}); } .close-button { position: absolute; top: 16px; right: 16px; cursor: pointer; font-size: 24px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}; transition: all 0.2s; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center; border-radius: 6px; } .close-button:hover { color: ${isDarkMode ? '#f3f4f6' : '#111827'}; background: ${isDarkMode ? '#374151' : '#f3f4f6'}; } .modal-title { font-size: 20px; font-weight: 600; margin-bottom: 24px; color: ${currentTheme.modal.text}; } .url-text { font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}; word-break: break-all; margin-bottom: 16px; padding: 8px 12px; background: ${isDarkMode ? '#374151' : '#f3f4f6'}; border-radius: 6px; } .timestamp { font-size: 12px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}; margin-top: 4px; } .delete-note-button { background: none; border: none; color: #ef4444; font-size: 18px; cursor: pointer; padding: 4px 8px; border-radius: 4px; transition: all 0.2s ease; } .delete-note-button:hover { background: #ef4444; color: #ffffff; } .notes-options-input { width: 100%; margin: 8px 0; padding: 10px 14px; border: 2px solid ${currentTheme.input.border}; border-radius: 8px; font-size: 15px; transition: all 0.2s ease; background: ${currentTheme.input.bg}; color: ${currentTheme.input.text}; box-sizing: border-box; } .notes-options-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .notes-options-checkbox { margin-right: 8px; } .notes-options-label { display: flex; align-items: center; margin: 10px 0; color: ${currentTheme.modal.text}; } .notes-editor-toolbar { display: flex; gap: 8px; margin: 8px 0; padding: 8px; background: ${isDarkMode ? '#2a3441' : '#f3f4f6'}; border-radius: 6px; } .notes-tag { display: inline-block; padding: 4px 8px; margin: 0 4px 4px 0; border-radius: 4px; background: ${isDarkMode ? '#4b5563' : '#e5e7eb'}; color: ${isDarkMode ? '#f3f4f6' : '#374151'}; font-size: 12px; } } .notes-notification { position: fixed; top: 20px; right: 20px; z-index: 99999; padding: 12px 16px; color: white; border-radius: 8px; font-size: 13px; font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-weight: 500; box-shadow: 0 4px 12px rgba(0,0,0,0.15); max-width: 350px; animation: slideInRight 0.3s ease-out; cursor: pointer; display: flex; align-items: center; gap: 8px; .closing { animation: slideOutRight 0.3s ease-in; } } @keyframes slideInRight { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } @keyframes slideOutRight { to { transform: translateX(100%); opacity: 0; } } `; const mobileStyles = ` @media (max-width: 768px) { .notes-overlay .notes-modal { width: 95%; padding: 16px; max-height: 95vh; } .notes-overlay .notes-button { padding: 10px 16px; margin: 3px; font-size: 14px; } .notes-overlay .close-button { top: 8px; right: 8px; } .notes-overlay .button-group { display: flex; flex-direction: column; } .notes-overlay .notes-list-item { padding: 12px; } } `; const styleSheet = document.createElement("style"); styleSheet.innerText = styles + mobileStyles; document.head.appendChild(styleSheet); function showNotification(message, type = 'info', duration = 4000) { // Remove existing notifications document.querySelectorAll('.notes-notification').forEach(n => n.remove()); const notification = document.createElement('div'); notification.className = 'notes-notification'; const colors = { success: { bg: '#4CAF50', icon: '✅' }, warning: { bg: '#FF9800', icon: '⚠️' }, error: { bg: '#F44336', icon: '❌' }, info: { bg: '#2196F3', icon: 'ℹ️' }, deletion: { bg: '2196F3', icon: '🗑️' } }; const style = colors[type] || colors.info; notification.style.background = style.bg; notification.innerHTML = `${style.icon} ${message}`; // Click to dismiss notification.addEventListener('click', () => { notification.classList.add('closing'); setTimeout(() => notification.remove(), 300); }); document.body.appendChild(notification); // Auto-remove after duration setTimeout(() => { if (document.body.contains(notification)) { notification.classList.add('closing'); setTimeout(() => notification.remove(), 300); } }, duration); } function showOptionsMenu() { // Define shortcuts configuration const shortcuts = [ { key: 'newNote', label: 'New Note', id: 'newNoteShortcut' }, { key: 'currentPageNotes', label: 'Current Page Notes', id: 'currentPageNotesShortcut' }, { key: 'allNotes', label: 'All Notes', id: 'allNotesShortcut' }, { key: 'showOptions', label: 'Show Options', id: 'showOptionsWindow' } ]; // Generate shortcuts HTML const shortcutHTML = shortcuts.map(s => ` <div> <label>${s.label}: <input type="text" class="notes-options-input" id="${s.id}" value="${getShortcutString(options.shortcuts[s.key])}"> </label> </div> `).join(''); const container = document.createElement('div'); container.innerHTML = ` <h3 class="modal-title">Options</h3> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="darkModeToggle" ${options.darkMode ? 'checked' : ''}> Dark Mode </label> </div> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="timestampToggle" ${options.addTimestampToTitle ? 'checked' : ''}> Add timestamp to note titles </label> </div> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="showUrlLinksToggle" ${options.showUrlLinksInNotesList ? 'checked' : ''}> Show URL links in notes list </label> </div> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="autoBackupToggle" ${options.autoBackup ? 'checked' : ''}> Enable automatic backups </label> </div> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="scrollContentToggle" ${options.scrollContentOnly ? 'checked' : ''}> Scroll content only (instead of entire modal) </label> </div> <div class="notes-options-label"> <label> Max Search Results: <input type="number" class="notes-options-input" id="maxSearchResults" value="${options.maxSearchResults}" min="10" max="500" style="width: 80px; margin-left: 8px;"> </label> </div> <div class="notes-options-label"> <label> Max Patterns Per Page (When showing all notes): <input type="number" class="notes-options-input" id="maxPatternsPerPage" value="${options.maxPatternsPerPage}" min="10" max="500" style="width: 80px; margin-left: 8px;"> </label> </div> <h4 style="margin-top: 20px;">Keyboard Shortcuts</h4> ${shortcutHTML} <div style="margin-top: 20px; display: flex; gap: 10px;"> <button id="saveOptions" class="notes-button">Save Options</button> <button id="exportNotesBtn" class="notes-button secondary">Export Notes</button> <button id="importNotesBtn" class="notes-button secondary">Import Notes</button> </div> `; createModal(container); addRestoreBackupButton(); const buttonHandlers = { 'saveOptions': saveOptions, 'exportNotesBtn': exportNotes, 'importNotesBtn': importNotes }; // Add event listeners Object.entries(buttonHandlers).forEach(([id, handler]) => { document.getElementById(id).onclick = handler; }); } function getShortcutString(shortcut) { let str = ''; if (shortcut.ctrlKey) str += 'Ctrl+'; if (shortcut.shiftKey) str += 'Shift+'; if (shortcut.altKey) str += 'Alt+'; str += shortcut.key.toUpperCase(); return str; } function parseShortcutString(str) { if (!str || typeof str !== 'string') { console.warn('Invalid shortcut string:', str); // Return default values if string is invalid return { ctrlKey: true, shiftKey: true, altKey: false, key: 'S' }; } const parts = str.toLowerCase().split('+'); return { ctrlKey: parts.includes('ctrl'), shiftKey: parts.includes('shift'), altKey: parts.includes('alt'), key: parts[parts.length - 1] || 'S' }; } function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } function saveOptions() { try { const maxResults = parseInt(document.getElementById('maxSearchResults').value) || 50; const maxPerPage = parseInt(document.getElementById('maxPatternsPerPage').value) || 50; const shortcutMappings = { newNote: 'newNoteShortcut', currentPageNotes: 'currentPageNotesShortcut', allNotes: 'allNotesShortcut', showOptions: 'showOptionsWindow' }; const shortcuts = {}; const shortcutValues = new Set(); for (const [key, id] of Object.entries(shortcutMappings)) { const value = document.getElementById(id).value; const normalizedValue = value.toLowerCase(); if (shortcutValues.has(normalizedValue)) { alert(`Duplicate shortcut detected: ${value}. This will execute multiple functionalities at once.`); } shortcutValues.add(normalizedValue); shortcuts[key] = parseShortcutString(value); } options = { version: scriptVersion, darkMode: document.getElementById('darkModeToggle').checked, addTimestampToTitle: document.getElementById('timestampToggle').checked, showUrlLinksInNotesList: document.getElementById('showUrlLinksToggle').checked, autoBackup: document.getElementById('autoBackupToggle').checked, scrollContentOnly: document.getElementById('scrollContentToggle').checked, maxSearchResults: isNaN(maxResults) || maxResults < 10 ? 10 : Math.min(maxResults, 500), maxPatternsPerPage: isNaN(maxPerPage) || maxPerPage < 10 ? 10 : Math.min(maxPerPage, 500), shortcuts }; GM_setValue('options', options); updateShortcutListener(); alert('Options saved successfully. Some changes may require reloading the page.'); } catch (error) { console.error('Error saving options:', error); alert('Failed to save options. Please try again.'); } } function exportNotes() { try { const notes = getAllNotes(); // Ensure all notes have IDs before export let modified = false; for (const url in notes) { notes[url].forEach(note => { if (!note.id) { note.id = generateNoteId(); modified = true; } }); } // If we added IDs, save them back if (modified) { GM_setValue('website-notes', notes); } const dateInfo = getFormattedBackupDate(); const blob = new Blob([JSON.stringify(notes, null, 2)], {type: 'application/json'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `website-notes-backup-${dateInfo.formatted}.json`; a.click(); URL.revokeObjectURL(url); } catch (error) { console.error('Error exporting notes:', error); alert('Failed to export notes. Please try again.'); } } function importNotes() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { try { const importedNotes = JSON.parse(event.target.result); // Create custom modal for import options const overlay = document.createElement('div'); overlay.className = 'notes-overlay'; const modal = document.createElement('div'); modal.className = 'notes-modal'; modal.style.maxWidth = '500px'; const closeButton = document.createElement('span'); closeButton.className = 'close-button'; closeButton.textContent = '×'; closeButton.onclick = () => overlay.remove(); modal.innerHTML = ` <h3 class="modal-title">Import Notes</h3> <p>Choose how to import the notes:</p> <div class="notes-list-item" style="cursor: pointer; margin-bottom: 12px;"> <div> <strong>Merge</strong> <p style="margin: 5px 0; font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}"> Add imported notes to your existing notes. This will keep all your current notes and may create duplicates. </p> </div> </div> <div class="notes-list-item" style="cursor: pointer; margin-bottom: 12px;"> <div> <strong>Update</strong> <p style="margin: 5px 0; font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}"> Add imported notes, updating existing notes (based on ID). </p> </div> </div> <div class="notes-list-item" style="cursor: pointer;"> <div> <strong>Replace</strong> <p style="margin: 5px 0; font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}"> Replace all your current notes with the imported ones. This will delete all your existing notes. </p> </div> </div> <div style="display: flex; justify-content: space-between; margin-top: 20px;"> <button id="mergeBtn" class="notes-button">Merge</button> <button id="updateBtn" class="notes-button">Update</button> <button id="replaceBtn" class="notes-button delete">Replace</button> <button id="cancelBtn" class="notes-button secondary">Cancel</button> </div> `; modal.appendChild(closeButton); overlay.appendChild(modal); document.body.appendChild(overlay); // Add event listeners document.getElementById('mergeBtn').onclick = () => { mergeNotes(importedNotes); overlay.remove(); }; document.getElementById('updateBtn').onclick = () => { importUpdate(importedNotes); overlay.remove(); }; document.getElementById('replaceBtn').onclick = () => { if (confirm('This will permanently replace all your existing notes. Are you sure?')) { GM_setValue('website-notes', importedNotes); alert('Notes replaced successfully!'); overlay.remove(); } }; document.getElementById('cancelBtn').onclick = () => { overlay.remove(); }; } catch (error) { console.error('Error parsing imported notes:', error); alert('Error importing notes: Invalid format'); } }; reader.readAsText(file); }; input.click(); } function importUpdate(importedNotes) { try { // Get existing notes const existingNotes = getAllNotes(); // Build a Map of existing note IDs to their notes and locations for fast lookup const existingNotesMap = new Map(); for (const url in existingNotes) { existingNotes[url].forEach((note, index) => { if (note.id) { existingNotesMap.set(note.id, { note, url, index }); } }); } // Count imported, updated, and skipped notes for notification let importedCount = 0; let updatedCount = 0; let skippedCount = 0; // Merge notes by URL for (const url in importedNotes) { if (!existingNotes[url]) { // If URL is new, add all notes (but ensure they have IDs) existingNotes[url] = importedNotes[url].map(note => ({ ...note, id: note.id || generateNoteId() })); importedCount += importedNotes[url].length; } else { // Check for duplicates before adding importedNotes[url].forEach(importedNote => { // If imported note has an ID if (importedNote.id) { // Check if this ID already exists if (existingNotesMap.has(importedNote.id)) { const existing = existingNotesMap.get(importedNote.id); // Compare timestamps - keep the more recent version if (importedNote.timestamp > existing.note.timestamp) { // Imported note is newer, replace the existing one existingNotes[existing.url][existing.index] = importedNote; updatedCount++; // Update the map with the new note existingNotesMap.set(importedNote.id, { note: importedNote, url: existing.url, index: existing.index }); } else { // Existing note is newer or same age, skip skippedCount++; } } else { // New note with ID, add it existingNotes[url].push(importedNote); existingNotesMap.set(importedNote.id, { note: importedNote, url, index: existingNotes[url].length - 1 }); importedCount++; } } else { // Imported note has no ID - use old logic (content-based comparison) const isDuplicate = existingNotes[url].some(existingNote => { // First check timestamp for efficiency return existingNote.timestamp === importedNote.timestamp && existingNote.title === importedNote.title && existingNote.content === importedNote.content; }); if (!isDuplicate) { // Find if there's a matching note with an ID that we can reuse const matchingNoteWithId = existingNotes[url].find(existingNote => existingNote.id && existingNote.timestamp === importedNote.timestamp && existingNote.title === importedNote.title && existingNote.content === importedNote.content ); if (matchingNoteWithId) { // Use the existing note's ID importedNote.id = matchingNoteWithId.id; } else { // Generate a new ID importedNote.id = generateNoteId(); } existingNotes[url].push(importedNote); if (importedNote.id) { existingNotesMap.set(importedNote.id, { note: importedNote, url, index: existingNotes[url].length - 1 }); } importedCount++; } else { skippedCount++; } } }); } } // Save merged notes back to storage GM_setValue('website-notes', existingNotes); // Perform auto-backup if enabled if (options.autoBackup) { performAutoBackup(); } let message = `Notes merged successfully!\n${importedCount} new notes imported`; if (updatedCount > 0) { message += `, ${updatedCount} notes updated to newer versions`; } if (skippedCount > 0) { message += `, ${skippedCount} older/duplicate notes skipped`; } alert(message); } catch (error) { console.error('Error merging notes without duplicates:', error); alert('Error merging notes. Please try again.'); } } function mergeNotes(importedNotes) { try { // Get existing notes const existingNotes = getAllNotes(); // Count imported notes for notification let importedCount = 0; // Merge notes by URL for (const url in importedNotes) { if (existingNotes[url]) { // If URL exists, append notes to existing array existingNotes[url] = existingNotes[url].concat(importedNotes[url]); importedCount += importedNotes[url].length; } else { // If URL is new, add all notes existingNotes[url] = importedNotes[url]; importedCount += importedNotes[url].length; } } // Save merged notes back to storage GM_setValue('website-notes', existingNotes); // Perform auto-backup if enabled if (options.autoBackup) { performAutoBackup(); } alert(`Notes merged successfully! ${importedCount} notes were imported.`); } catch (error) { console.error('Error merging notes:', error); alert('Error merging notes. Please try again.'); } } function addRestoreBackupButton() { // Create a restore backup button const restoreBackupBtn = document.createElement('button'); restoreBackupBtn.id = 'restoreBackupBtn'; restoreBackupBtn.className = 'notes-button secondary'; restoreBackupBtn.textContent = 'Restore Backup'; // Add it to the export/import button group const buttonGroup = document.querySelector('[id="saveOptions"]').parentNode; buttonGroup.appendChild(restoreBackupBtn); // Add event listener document.getElementById('restoreBackupBtn').onclick = showBackupsList; } function showBackupsList() { // Create modal for backup list const overlay = document.createElement('div'); overlay.className = 'notes-overlay'; const modal = document.createElement('div'); modal.className = 'notes-modal'; modal.style.maxWidth = '500px'; const closeButton = document.createElement('span'); closeButton.className = 'close-button'; closeButton.textContent = '×'; closeButton.onclick = () => overlay.remove(); let backupKeys = []; try { backupKeys = GM_listValues().filter(key => key.startsWith('notes-backup-')).sort().reverse(); } catch (error) { console.warn('Could not retrieve list of backups:', error); } if (backupKeys.length === 0) { modal.innerHTML = ` <h3 class="modal-title">Restore Backup</h3> <p>No backups found. Automatic backups are ${options.autoBackup ? 'enabled' : 'disabled'} in your settings.</p> <button id="closeBackupsList" class="notes-button">Close</button> `; } else { modal.innerHTML = ` <h3 class="modal-title">Available Backups</h3> <p>Select a backup to restore:</p> <div id="backupsList" style="max-height: 300px; overflow-y: auto;"></div> <button id="closeBackupsList" class="notes-button secondary" style="margin-top: 16px;">Cancel</button> `; const backupsList = modal.querySelector('#backupsList'); backupKeys.forEach(key => { // Extract the timestamp from the key const timestampStr = key.replace('notes-backup-', ''); let timestamp; let readableDate = "Unknown date"; // Handle both timestamp formats if (/^\d+$/.test(timestampStr)) { // It's a numeric timestamp timestamp = parseInt(timestampStr, 10); } else if (timestampStr.includes('T')) { // It's an ISO date format try { timestamp = new Date(timestampStr.replace(/\-/g, ':')).getTime(); } catch (e) { console.error('Error parsing ISO date format:', e); } } // Format date in a more user-friendly way if (!isNaN(timestamp) && timestamp > 0) { const date = new Date(timestamp); // Format: "Feb 25, 2025 - 3:45 PM" (with day and time) const options = { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', hour12: true }; readableDate = date.toLocaleDateString(undefined, options); // Add relative time indication like "Today", "Yesterday", etc. const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); if (date.toDateString() === today.toDateString()) { readableDate = `Today, ${date.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit', hour12: true})}`; } else if (date.toDateString() === yesterday.toDateString()) { readableDate = `Yesterday, ${date.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit', hour12: true})}`; } } const backupItem = document.createElement('div'); backupItem.className = 'notes-list-item'; backupItem.innerHTML = `<span>${readableDate}</span>`; backupItem.onclick = () => confirmAndRestoreBackup(key); backupsList.appendChild(backupItem); }); } modal.appendChild(closeButton); overlay.appendChild(modal); document.body.appendChild(overlay); document.getElementById('closeBackupsList')?.addEventListener('click', () => overlay.remove()); } function confirmAndRestoreBackup(backupKey) { if (confirm('Are you sure you want to restore this backup? This will replace all your current notes.')) { try { const backupData = GM_getValue(backupKey); if (backupData) { GM_setValue('website-notes', backupData); alert('Backup restored successfully!'); location.reload(); // Reload the page to refresh notes display } else { alert('Error: Backup data is empty or corrupted.'); } } catch (error) { console.error('Error restoring backup:', error); alert('Failed to restore backup. Please try again.'); } } } // Add search functionality function addSearchButton() { // Add a search button to the top of the all notes view const searchButton = document.createElement('button'); searchButton.className = 'notes-button'; searchButton.textContent = '🔍 Search Notes'; searchButton.style.marginBottom = '16px'; searchButton.onclick = showSearchModal; // Find the appropriate container - the div after the modal title const titleElement = document.querySelector('.notes-modal .modal-title'); if (titleElement && titleElement.textContent === 'All Notes') { titleElement.parentNode.insertBefore(searchButton, titleElement.nextSibling); } } function showSearchModal() { const overlay = document.createElement('div'); overlay.className = 'notes-overlay'; const modal = document.createElement('div'); modal.className = 'notes-modal'; const closeButton = document.createElement('span'); closeButton.className = 'close-button'; closeButton.textContent = '×'; closeButton.onclick = () => overlay.remove(); modal.innerHTML = ` <h3 class="modal-title">Search Notes</h3> <input type="text" id="searchInput" class="notes-input" placeholder="Search by title, content, tags, or URL..."> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="searchTitle" checked> Search in titles </label> </div> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="searchContent" checked> Search in note content </label> </div> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="searchTags" checked> Search in tags </label> </div> <div class="notes-options-label"> <label> <input type="checkbox" class="notes-options-checkbox" id="searchUrls"> Search in URLs </label> </div> <div id="searchResults" style="margin-top: 16px; max-height: 400px; overflow-y: auto;"></div> <button id="closeSearchModal" class="notes-button secondary" style="margin-top: 16px;">Close</button> `; modal.appendChild(closeButton); overlay.appendChild(modal); document.body.appendChild(overlay); // Set up event listeners const searchInput = document.getElementById('searchInput'); searchInput.focus(); // Cache for search results let searchCache = { lastQuery: '', lastResults: [], lastSearchOptions: null }; const debouncedSearch = debounce(performSearch, 150); searchInput.addEventListener('input', debouncedSearch); function resetCacheAndSearch() { searchCache = { lastQuery: '', lastResults: [], lastSearchOptions: null, wasLimitReached: false }; performSearch(); } document.getElementById('searchTitle').addEventListener('change', resetCacheAndSearch); document.getElementById('searchContent').addEventListener('change', resetCacheAndSearch); document.getElementById('searchTags').addEventListener('change', resetCacheAndSearch); document.getElementById('searchUrls').addEventListener('change', resetCacheAndSearch); document.getElementById('closeSearchModal').addEventListener('click', () => overlay.remove()); // Perform search when input changes function performSearch() { const query = searchInput.value.toLowerCase().trim(); const searchTitle = document.getElementById('searchTitle').checked; const searchContent = document.getElementById('searchContent').checked; const searchTags = document.getElementById('searchTags').checked; const searchUrls = document.getElementById('searchUrls').checked; const searchResults = document.getElementById('searchResults'); const maxResults = options.maxSearchResults || 50; // Create search options object for comparison const currentSearchOptions = { searchTitle, searchContent, searchTags, searchUrls }; // Helper function to highlight matching text function highlightMatch(text, query) { if (!text) return ''; const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi'); return text.replace(regex, '<mark style="background-color: #fde68a; color: #1f2937;">$1</mark>'); } // Helper function to check if search options changed function searchOptionsChanged() { if (!searchCache.lastSearchOptions) return true; return JSON.stringify(currentSearchOptions) !== JSON.stringify(searchCache.lastSearchOptions); } // Helper function to add a note result to the results div function addNoteResult(container, note, url, index, query) { const noteDiv = document.createElement('div'); noteDiv.className = 'notes-list-item'; // Apply note color if available if (note.color) { noteDiv.style.borderLeft = `4px solid ${note.color}`; noteDiv.style.paddingLeft = '12px'; } // Create content with highlighted matches let titleHtml = note.title; let contentPreview = ''; if (query) { // Highlight matches if (searchTitle) { titleHtml = highlightMatch(note.title, query); } if (searchContent && note.content.toLowerCase().includes(query)) { // Find the context around the match const matchIndex = note.content.toLowerCase().indexOf(query); const startIndex = Math.max(0, matchIndex - 50); const endIndex = Math.min(note.content.length, matchIndex + query.length + 50); // Add ellipsis if we're not starting from the beginning let preview = (startIndex > 0 ? '...' : '') + note.content.substring(startIndex, endIndex) + (endIndex < note.content.length ? '...' : ''); contentPreview = `<div style="margin-top: 4px; font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'};"> ${highlightMatch(preview, query)} </div>`; } } // Add tags if available with highlighting let tagsHTML = ''; if (note.tags && note.tags.length > 0) { tagsHTML = '<div style="margin-top: 4px;">'; note.tags.forEach(tag => { if (searchTags && query && tag.toLowerCase().includes(query)) { tagsHTML += `<span class="notes-tag">${highlightMatch(tag, query)}</span>`; } else { tagsHTML += `<span class="notes-tag">${tag}</span>`; } }); tagsHTML += '</div>'; } noteDiv.innerHTML = ` <div style="flex-grow: 1;"> <div style="font-weight: 500;">${titleHtml}</div> ${contentPreview} ${tagsHTML} </div> `; noteDiv.onclick = () => { document.querySelector('.notes-overlay').remove(); showNoteContent(note, url, index); }; container.appendChild(noteDiv); } // Use DocumentFragment for efficient DOM updates const fragment = document.createDocumentFragment(); if (!query) { const emptyMessage = document.createElement('p'); emptyMessage.style.color = '#6b7280'; emptyMessage.textContent = 'Enter a search term to find notes'; fragment.appendChild(emptyMessage); searchResults.replaceChildren(fragment); // Reset cache when query is empty searchCache = { lastQuery: '', lastResults: [], lastSearchOptions: null, wasLimitReached: false }; return; } // Determine if we can use cached results const canUseCache = searchCache.lastQuery && query.startsWith(searchCache.lastQuery) && !searchOptionsChanged() && searchCache.lastResults.length > 0 && !searchCache.wasLimitReached; let searchPool = []; let useCache = false; if (canUseCache) { // Use cached results as the search pool for refinement searchPool = searchCache.lastResults.slice(); // Create a copy useCache = true; } else { // Need to search all notes - build the complete search pool const notes = getAllNotes(); for (const url in notes) { notes[url].forEach((note, index) => { searchPool.push({ note, url, index }); }); } useCache = false; } // Perform the actual search with early termination const results = []; let resultCount = 0; let limitReached = false; // Search through the pool and break early when limit is reached for (const item of searchPool) { if (resultCount >= maxResults) { limitReached = true; break; } const { note, url, index } = item; let matches = false; // Check URL match if (searchUrls && url.toLowerCase().includes(query)) { matches = true; } // Check note content matches if (!matches) { if (searchTitle && note.title.toLowerCase().includes(query)) matches = true; if (!matches && searchContent && note.content.toLowerCase().includes(query)) matches = true; if (!matches && searchTags && note.tags && note.tags.some(tag => tag.toLowerCase().includes(query))) matches = true; } if (matches) { results.push({ note, url, index }); resultCount++; } } // Update cache - always update when we have results if (results.length > 0 || !useCache) { searchCache = { lastQuery: query, lastResults: results, lastSearchOptions: { ...currentSearchOptions }, wasLimitReached: limitReached }; } // Create DOM elements if (resultCount === 0) { const noResults = document.createElement('p'); noResults.style.color = '#6b7280'; noResults.textContent = 'No matching notes found'; fragment.appendChild(noResults); } else { const countMessage = document.createElement('p'); countMessage.style.color = '#6b7280'; if (limitReached) { countMessage.textContent = `Search limit reached: showing first ${maxResults}`; } else { countMessage.textContent = `${resultCount} note${resultCount !== 1 ? 's' : ''} found`; } fragment.appendChild(countMessage); // Group results by URL const groupedResults = {}; results.forEach(result => { if (!groupedResults[result.url]) { groupedResults[result.url] = []; } groupedResults[result.url].push(result); }); Object.keys(groupedResults).forEach(url => { const urlDiv = document.createElement('div'); urlDiv.innerHTML = `<div class="url-text">${searchUrls && query ? highlightMatch(url, query) : url}</div>`; groupedResults[url].forEach(result => { addNoteResult(urlDiv, result.note, result.url, result.index, query); }); fragment.appendChild(urlDiv); }); } // Single DOM update searchResults.replaceChildren(fragment); } } GM_registerMenuCommand('Toggle Dark Mode', () => { const newMode = !isDarkMode; GM_setValue('darkMode', newMode); location.reload(); }); function createModal(content) { const overlay = document.createElement('div'); overlay.className = 'notes-overlay'; const modal = document.createElement('div'); modal.className = 'notes-modal'; const closeButton = document.createElement('span'); closeButton.className = 'close-button'; closeButton.textContent = '×'; closeButton.onclick = () => { overlay.remove(); document.removeEventListener('keydown', escapeListener); }; modal.appendChild(closeButton); modal.appendChild(content); overlay.appendChild(modal); document.body.appendChild(overlay); const escapeListener = (e) => { if (e.key === 'Escape') { overlay.remove(); document.removeEventListener('keydown', escapeListener); } }; document.addEventListener('keydown', escapeListener); } function getAllNotes() { return GM_getValue('website-notes', {}); } function generateNoteId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } function saveNote(title, url, content, timestamp = Date.now(), pinned = false, tags = [], color = null, id = null) { try { const notes = getAllNotes(); if (!notes[url]) notes[url] = []; // Add timestamp to title if the option is enabled let finalTitle = title.trim(); if (options.addTimestampToTitle) { const date = new Date(timestamp); const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); finalTitle = `${finalTitle} [${formattedDate}]`; } const noteData = { id: id || generateNoteId(), // Generate ID if not provided title: finalTitle, content: content.trim(), timestamp, pinned: Boolean(pinned), tags: Array.isArray(tags) ? tags.filter(tag => tag && tag.trim()) : [], color: color || null }; notes[url].push(noteData); GM_setValue('website-notes', notes); // Perform auto-backup if enabled if (options.autoBackup) { performAutoBackup(); } showNotification('✅ Note saved successfully', 'success'); return true; } catch (error) { console.error('Error saving note:', error); showNotification('Failed to save note: ' + error.message, 'error'); alert('Failed to save note: ' + error.message); return false; } } function performAutoBackup() { try { const notes = getAllNotes(); const dateInfo = getFormattedBackupDate(); // Use consistent format with numeric timestamp const backupKey = `notes-backup-${dateInfo.timestamp}`; // Create the new backup GM_setValue(backupKey, notes); console.log(`Auto-backup created successfully: ${dateInfo.formatted}`); // Now try to manage old backups try { // Try to get all backup keys const allBackupKeys = GM_listValues().filter(key => key.startsWith('notes-backup-')).sort(); // Keep only the last 5 backups if (allBackupKeys.length > 5) { // Delete oldest backups, keeping the 5 most recent for (let i = 0; i < allBackupKeys.length - 5; i++) { try { GM_deleteValue(allBackupKeys[i]); console.log(`Deleted old backup: ${allBackupKeys[i]}`); } catch (deleteError) { alert(`Could not delete backup ${allBackupKeys[i]}:`, deleteError); } } } } catch (listError) { console.warn('Could not retrieve list of backups to manage old backups:', listError); alert('Could not retrieve list of backups to manage old backups:', listError); // Alternative approach: Store the list of backup keys ourselves let storedBackupKeys = GM_getValue('backup-key-list', []); // Add the new backup key to our list storedBackupKeys.push(backupKey); // Only keep the most recent 5 backups if (storedBackupKeys.length > 5) { // Get keys to delete (all except the 5 most recent) const keysToDelete = storedBackupKeys.slice(0, storedBackupKeys.length - 5); // Delete old backups keysToDelete.forEach(keyToDelete => { try { GM_deleteValue(keyToDelete); console.log(`Deleted old backup (using fallback method): ${keyToDelete}`); } catch (deleteError) { console.warn(`Could not delete backup ${keyToDelete}:`, deleteError); } }); // Update our stored list to contain only the 5 most recent keys storedBackupKeys = storedBackupKeys.slice(storedBackupKeys.length - 5); } // Save the updated list of backup keys GM_setValue('backup-key-list', storedBackupKeys); } } catch (error) { console.error('Error during auto-backup:', error); } } function getFormattedBackupDate() { const now = new Date(); // Format: YYYY-MM-DD_HH-MM-SS (e.g., 2025-02-25_14-30-45) const year = now.getFullYear(); const month = String(now.getMonth() + 1).padStart(2, '0'); const day = String(now.getDate()).padStart(2, '0'); const hours = String(now.getHours()).padStart(2, '0'); const minutes = String(now.getMinutes()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0'); const dateString = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`; return { timestamp: now.getTime(), // Use numeric timestamp formatted: dateString }; } function updateNote(oldUrl, index, title, newUrl, content, pinned, tags = [], color = null) { const notes = getAllNotes(); const existingNote = notes[oldUrl][index]; if (!existingNote) { console.error('Note not found at specified index'); showNotification('Note not found', 'error'); return; } // Ensure the note has an ID (backfill for old notes) const noteId = existingNote.id || generateNoteId(); // If URL hasn't changed, update in place if (oldUrl === newUrl) { notes[oldUrl][index] = { id: noteId, title, content, timestamp: existingNote.timestamp, pinned, tags, color }; GM_setValue('website-notes', notes); } else { // URL changed: remove from old location, add to new location deleteNote(oldUrl, index); // Save with updated values but keep the original timestamp and ID saveNote( title, newUrl, content, existingNote.timestamp, pinned, tags, color, noteId // Pass the existing ID ); } // Perform auto-backup if enabled if (options.autoBackup) { performAutoBackup(); } showNotification('Note updated successfully', 'success'); displayPinnedNotes(); } function togglePinNote(url, index) { const notes = getAllNotes(); if (notes[url] && notes[url][index]) { notes[url][index].pinned = !notes[url][index].pinned; GM_setValue('website-notes', notes); } } function deleteNote(url, index) { const notes = getAllNotes(); if (notes[url]) { notes[url].splice(index, 1); if (notes[url].length === 0) delete notes[url]; GM_setValue('website-notes', notes); } } function showNoteForm(editMode = false, existingNote = null, url = null, index = null) { const container = document.createElement('div'); container.innerHTML = `<h3 class="modal-title">${editMode ? 'Edit Note' : 'Create New Note'}</h3>`; const titleInput = document.createElement('input'); titleInput.className = 'notes-input'; titleInput.placeholder = 'Enter title'; titleInput.value = editMode ? existingNote.title : ''; const urlInput = document.createElement('input'); urlInput.className = 'notes-input'; urlInput.placeholder = 'Enter URL(s) or URL pattern(s), separated by spaces (e.g., https://domain.com/*)'; urlInput.value = editMode ? url : window.location.href; const patternHelp = document.createElement('div'); patternHelp.style.fontSize = '12px'; patternHelp.style.color = isDarkMode ? '#9ca3af' : '#6b7280'; patternHelp.style.marginTop = '-8px'; patternHelp.style.marginBottom = '8px'; patternHelp.innerHTML = 'Use * for wildcard matching. Multiple URLs: separate with spaces (e.g., https://domain1.com/* https://domain2.com/*)'; // Add tags input const tagsInput = document.createElement('input'); tagsInput.className = 'notes-input'; tagsInput.placeholder = 'Tags (comma separated)'; tagsInput.value = editMode && existingNote.tags ? existingNote.tags.join(', ') : ''; const tagsHelp = document.createElement('div'); tagsHelp.style.fontSize = '12px'; tagsHelp.style.color = isDarkMode ? '#9ca3af' : '#6b7280'; tagsHelp.style.marginTop = '-8px'; tagsHelp.style.marginBottom = '8px'; tagsHelp.innerHTML = 'Add tags to organize notes (e.g., work, personal, important)'; // Add color picker const colorPicker = createColorPicker(editMode && existingNote.color); const colorPickerLabel = document.createElement('div'); colorPickerLabel.style.fontSize = '14px'; colorPickerLabel.style.marginBottom = '8px'; colorPickerLabel.innerHTML = 'Note Color:'; const contentArea = document.createElement('textarea'); contentArea.className = 'notes-textarea'; contentArea.placeholder = 'Enter your notes here'; contentArea.value = editMode ? existingNote.content : ''; // Add formatting toolbar const toolbar = enhanceTextEditor(contentArea); const buttonGroup = document.createElement('div'); buttonGroup.className = 'button-group'; buttonGroup.style.display = 'flex'; buttonGroup.style.justifyContent = 'space-between'; buttonGroup.style.marginTop = '16px'; const saveButton = document.createElement('button'); saveButton.className = 'notes-button'; saveButton.textContent = editMode ? 'Update Note' : 'Save Note'; saveButton.onclick = () => { if (titleInput.value && contentArea.value) { const tags = tagsInput.value.split(',').map(tag => tag.trim()).filter(tag => tag); const color = colorPicker.dataset.selectedColor; if (editMode) { updateNote(url, index, titleInput.value, urlInput.value, contentArea.value, existingNote.pinned, tags, color); } else { saveNote(titleInput.value, urlInput.value, contentArea.value, Date.now(), false, tags, color); } container.parentElement.parentElement.remove(); showCurrentPageNotes(); } else { alert('Title and content are required!'); } }; const cancelButton = document.createElement('button'); cancelButton.className = 'notes-button secondary'; cancelButton.textContent = 'Cancel'; cancelButton.onclick = () => container.parentElement.parentElement.remove(); buttonGroup.appendChild(saveButton); buttonGroup.appendChild(cancelButton); container.appendChild(titleInput); container.appendChild(urlInput); container.appendChild(patternHelp); container.appendChild(tagsInput); container.appendChild(tagsHelp); container.appendChild(colorPickerLabel); container.appendChild(colorPicker); container.appendChild(toolbar); container.appendChild(contentArea); container.appendChild(buttonGroup); createModal(container); } function createColorPicker(selectedColor = null) { const colorOptions = ['', '#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899']; const container = document.createElement('div'); container.style.display = 'flex'; container.style.gap = '8px'; container.style.margin = '8px 0'; container.style.flexWrap = 'wrap'; colorOptions.forEach(color => { const option = document.createElement('div'); option.style.width = '24px'; option.style.height = '24px'; option.style.borderRadius = '50%'; option.style.backgroundColor = color; option.style.cursor = 'pointer'; option.style.border = color === selectedColor ? '2px solid white' : '2px solid transparent'; option.style.boxShadow = '0 0 0 1px rgba(0,0,0,0.1)'; option.onclick = () => { container.querySelectorAll('div').forEach(div => { div.style.border = '2px solid transparent'; }); option.style.border = '2px solid white'; container.dataset.selectedColor = color; }; container.appendChild(option); }); container.dataset.selectedColor = selectedColor; return container; } function applyNoteColor(noteElement, color) { if (!color) return; // Apply color as a left border noteElement.style.borderLeft = `4px solid ${color}`; // Add subtle background tint const colorOpacity = isDarkMode ? '0.1' : '0.05'; noteElement.style.backgroundColor = `${color}${colorOpacity}`; } function enhanceTextEditor(textArea) { const toolbar = document.createElement('div'); toolbar.className = 'notes-editor-toolbar'; const addButton = (text, title, action) => { const btn = document.createElement('button'); btn.textContent = text; btn.title = title; btn.className = 'notes-button secondary'; btn.style.padding = '4px 8px'; btn.style.fontSize = '12px'; btn.onclick = (e) => { e.preventDefault(); action(textArea); textArea.focus(); // Keep focus on the textarea after button click }; return btn; }; // Add formatting buttons with icons or text toolbar.appendChild(addButton('B', 'Bold (Ctrl+B)', ta => { // If text is selected, wrap it in bold marks // Otherwise, just insert the marks and place cursor between them insertAround(ta, '**', '**'); })); toolbar.appendChild(addButton('I', 'Italic (Ctrl+I)', ta => { insertAround(ta, '*', '*'); })); toolbar.appendChild(addButton('hr', 'Horiztonal Line', ta => { insertAtCursor(ta, '___'); })); toolbar.appendChild(addButton('Link', 'Insert Link', ta => { const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd); if (selection) { insertAround(ta, '[', '](https://)'); // Position cursor after the opening bracket of the URL ta.selectionStart = ta.selectionEnd - 9; ta.selectionEnd = ta.selectionEnd - 1; } else { insertAtCursor(ta, '[Link text](https://)'); // Select "Link text" for easy replacement const cursorPos = ta.value.lastIndexOf('[Link text]'); ta.selectionStart = cursorPos + 1; ta.selectionEnd = cursorPos + 10; } })); toolbar.appendChild(addButton('List', 'Insert List', ta => { insertAtCursor(ta, '\n- Item 1\n- Item 2\n- Item 3\n'); })); toolbar.appendChild(addButton('H1', 'Heading 1', ta => { const start = ta.selectionStart; const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1; const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd); // Check if the line already starts with # to avoid duplicating const currentLine = ta.value.substring(lineStart, start); if (currentLine.trim().startsWith('# ')) { return; // Already has heading format } if (selection) { // Selected text becomes heading ta.value = ta.value.substring(0, ta.selectionStart) + '# ' + selection + ta.value.substring(ta.selectionEnd); ta.selectionStart = ta.selectionStart + 2; ta.selectionEnd = ta.selectionStart + selection.length; } else { // Insert at current line start ta.value = ta.value.substring(0, lineStart) + '# Heading' + ta.value.substring(lineStart); ta.selectionStart = lineStart + 2; ta.selectionEnd = lineStart + 9; } })); toolbar.appendChild(addButton('H2', 'Heading 2', ta => { const start = ta.selectionStart; const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1; const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd); // Check if the line already starts with ## to avoid duplicating const currentLine = ta.value.substring(lineStart, start); if (currentLine.trim().startsWith('## ')) { return; // Already has heading format } if (selection) { // Selected text becomes heading ta.value = ta.value.substring(0, ta.selectionStart) + '## ' + selection + ta.value.substring(ta.selectionEnd); ta.selectionStart = ta.selectionStart + 3; ta.selectionEnd = ta.selectionStart + selection.length; } else { // Insert at current line start ta.value = ta.value.substring(0, lineStart) + '## Subheading' + ta.value.substring(lineStart); ta.selectionStart = lineStart + 3; ta.selectionEnd = lineStart + 13; } })); toolbar.appendChild(addButton('Quote', 'Blockquote', ta => { const start = ta.selectionStart; const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1; const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd); if (selection) { // Add quote prefix to all selected lines const lines = selection.split('\n'); const quotedText = lines.map(line => `> ${line}`).join('\n'); ta.value = ta.value.substring(0, ta.selectionStart) + quotedText + ta.value.substring(ta.selectionEnd); ta.selectionStart = ta.selectionStart; ta.selectionEnd = ta.selectionStart + quotedText.length; } else { // Insert at current line start ta.value = ta.value.substring(0, lineStart) + '> ' + ta.value.substring(lineStart); ta.selectionStart = lineStart + 2; ta.selectionEnd = lineStart + 2; } })); // Add keyboard event listeners for common shortcuts textArea.addEventListener('keydown', (e) => { // Ctrl+B for bold if (e.ctrlKey && e.key === 'b') { e.preventDefault(); insertAround(textArea, '**', '**'); } // Ctrl+I for italic if (e.ctrlKey && e.key === 'i') { e.preventDefault(); insertAround(textArea, '_', '_'); } // Tab key handling for indentation if (e.key === 'Tab') { e.preventDefault(); insertAtCursor(textArea, ' '); } }); return toolbar; } function insertAround(textArea, before, after) { const start = textArea.selectionStart; const end = textArea.selectionEnd; const text = textArea.value; const selected = text.substring(start, end); textArea.value = text.substring(0, start) + before + selected + after + text.substring(end); textArea.focus(); textArea.setSelectionRange(start + before.length, start + before.length + selected.length); } function insertAtCursor(textArea, text) { const start = textArea.selectionStart; textArea.value = textArea.value.substring(0, start) + text + textArea.value.substring(start); textArea.focus(); textArea.setSelectionRange(start + text.length, start + text.length); } function formatDate(timestamp) { return new Date(timestamp).toLocaleString(); } function showNoteContent(note, url, index) { const container = document.createElement('div'); const contentContainer = document.createElement('div'); contentContainer.className = 'note-content-container'; if (options.scrollContentOnly) { contentContainer.style.maxHeight = '60vh'; contentContainer.style.overflowY = 'auto'; } contentContainer.style.padding = '16px'; contentContainer.style.borderRadius = '8px'; contentContainer.style.marginBottom = '16px'; if (note.color) { contentContainer.style.borderLeft = `4px solid ${note.color}`; contentContainer.style.backgroundColor = `${note.color}${isDarkMode ? '15' : '10'}`; } else { contentContainer.style.backgroundColor = isDarkMode ? '#2a3441' : '#f9fafb'; } let tagsHTML = ''; if (note.tags && note.tags.length > 0) { tagsHTML = '<div style="margin-top: 8px; margin-bottom: 8px;">'; note.tags.forEach(tag => { tagsHTML += `<span class="notes-tag">${tag}</span>`; }); tagsHTML += '</div>'; } container.innerHTML = ` <h3 class="modal-title">${note.title}</h3> <div class="url-text">${url}</div> <div class="timestamp">Created: ${formatDate(note.timestamp)}</div> ${tagsHTML} `; contentContainer.innerHTML = md.render(note.content); container.appendChild(contentContainer); contentContainer.addEventListener('copy', async (e) => { e.preventDefault(); const selection = window.getSelection(); let selectedText = selection.toString(); if (!selectedText) { // If no selection, copy the entire note content selectedText = note.content; } try { await navigator.clipboard.writeText(selectedText); console.log('Text copied to clipboard'); } catch (err) { console.error('Failed to copy text: ', err); alert('Failed to copy text. Please try again.'); } // Clear selection to prevent unwanted behavior selection.removeAllRanges(); }); const buttonGroup = document.createElement('div'); buttonGroup.className = 'button-group'; const editButton = document.createElement('button'); editButton.className = 'notes-button edit'; editButton.textContent = 'Edit'; editButton.onclick = () => { container.parentElement.parentElement.remove(); showNoteForm(true, note, url, index); }; const deleteButton = document.createElement('button'); deleteButton.className = 'notes-button delete'; deleteButton.textContent = 'Delete'; deleteButton.onclick = () => { if (confirm('Are you sure you want to delete this note?')) { deleteNote(url, index); container.parentElement.parentElement.remove(); showNotification('Note deleted', 'deletion'); showCurrentPageNotes(); } }; const pinButton = document.createElement('button'); pinButton.className = `notes-button ${note.pinned ? 'secondary' : ''}`; pinButton.textContent = note.pinned ? 'Unpin' : 'Pin'; pinButton.onclick = () => { togglePinNote(url, index); const notes = getAllNotes(); const isPinned = notes[url] && notes[url][index] ? notes[url][index].pinned : false; pinButton.textContent = isPinned ? 'Unpin' : 'Pin'; pinButton.className = `notes-button ${isPinned ? '' : 'secondary'}`; displayPinnedNotes(); }; buttonGroup.appendChild(editButton); buttonGroup.appendChild(deleteButton); buttonGroup.appendChild(pinButton); container.appendChild(buttonGroup); createModal(container); } function displayPinnedNotes() { const notes = getAllNotes(); const currentUrl = window.location.href; let pinnedNotesContainer = document.getElementById('pinned-notes-container'); if (!pinnedNotesContainer) { pinnedNotesContainer = document.createElement('div'); pinnedNotesContainer.id = 'pinned-notes-container'; pinnedNotesContainer.style.position = 'fixed'; pinnedNotesContainer.style.top = '10px'; pinnedNotesContainer.style.right = '10px'; pinnedNotesContainer.style.zIndex = '9999'; pinnedNotesContainer.style.maxWidth = '300px'; document.body.appendChild(pinnedNotesContainer); } pinnedNotesContainer.innerHTML = ''; for (const url in notes) { if (doesUrlMatchPattern(url, currentUrl)) { notes[url].forEach((note, index) => { if (note.pinned) { const noteDiv = document.createElement('div'); noteDiv.className = 'pinned-note'; noteDiv.style.background = currentTheme.listItem.bg; noteDiv.style.color = currentTheme.listItem.text; noteDiv.style.padding = '10px'; noteDiv.style.margin = '5px 0'; noteDiv.style.borderRadius = '8px'; noteDiv.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)'; noteDiv.style.cursor = 'pointer'; // Apply note color if available if (note.color) { noteDiv.style.borderLeft = `4px solid ${note.color}`; noteDiv.style.paddingLeft = '12px'; } noteDiv.innerHTML = `<strong>${note.title}</strong>`; noteDiv.onclick = () => showNoteContent(note, url, index); pinnedNotesContainer.appendChild(noteDiv); } }); } } } // Add caching for compiled regex patterns const patternCache = new Map(); const MAX_CACHE_SIZE = 10000; let cacheEntries = 0; function cacheResult(key, value) { if (cacheEntries < MAX_CACHE_SIZE) { patternCache.set(key, value); cacheEntries++; } // Don't cache beyond limit, but don't evict either } function doesUrlMatchPattern(urlPatterns, currentUrl) { // Check simple pattern cache first const cacheKey = `${urlPatterns}|${currentUrl}`; if (patternCache.has(cacheKey)) { return patternCache.get(cacheKey); } // Split the pattern string into an array of patterns const patterns = urlPatterns.split(/\s+/).filter(pattern => pattern.trim() !== ''); for (const pattern of patterns) { const result = matchSinglePattern(pattern, currentUrl); if (result) { // Cache positive results cacheResult(cacheKey, true); return true; } } // Cache negative results cacheResult(cacheKey, false); return false; } function matchSinglePattern(pattern, currentUrl) { // Handle exact match (no wildcards) if (!pattern.includes('*')) { return pattern === currentUrl; } const asteriskCount = countAsterisks(pattern) // Handle simple end double wildcard: "https://example.com/**" // Handle ** first, since * would catch **, also, ** more common if (pattern.endsWith('**') && asteriskCount === 2 ) { return currentUrl.startsWith(pattern.slice(0, -2)); } // Handle simple end single wildcard: "https://example.com/*" if (pattern.endsWith('*') && asteriskCount === 1) { const prefix = pattern.slice(0, -1); if (!currentUrl.startsWith(prefix)) { return false; } // Check that the remaining part doesn't contain '/' (single segment only) const remaining = currentUrl.slice(prefix.length); return !remaining.includes('/'); } // Handle simple start double wildcard: "**/path/to/page" if (pattern.startsWith('**') && asteriskCount === 2 ) { return currentUrl.endsWith(pattern.slice(2)); } if (pattern.startsWith('https://**') && asteriskCount === 2 ) { return currentUrl.endsWith(pattern.slice(10)); } /*removed, because starting with * doesn't make much sense, because https:// // Handle simple start wildcard: "* /path/to/page" ; space for multine comment if (pattern.startsWith('*') && !pattern.slice(1).includes('*')) { return currentUrl.endsWith(pattern.slice(1)); } */ // Handle patterns with only one * in the middle: "https://example.com/*/page" if (asteriskCount === 1 && !pattern.startsWith('*')) { const [prefix, suffix] = pattern.split('*'); if (currentUrl.startsWith(prefix) && currentUrl.endsWith(suffix) && currentUrl.length >= prefix.length + suffix.length) { // Extract the part that the * should match const matchedPart = currentUrl.slice(prefix.length, currentUrl.length - suffix.length); // Single * should not contain '/' (single level only) return !matchedPart.includes('/'); } return false; } // Handle patterns with only one ** in the middle: "https://example.com/**/page" if (asteriskCount === 2 && pattern.includes('**')) { const [prefix, suffix] = pattern.split('**'); return currentUrl.startsWith(prefix) && currentUrl.endsWith(suffix) && currentUrl.length >= prefix.length + suffix.length; } // For complex patterns, fall back to regex return matchComplexPattern(pattern, currentUrl); } function countAsterisks(str) { let count = 0; for (let i = 0; i < str.length; i++) { if (str[i] === '*') count++; } return count; } function matchComplexPattern(pattern, currentUrl) { try { const regex = patternToRegex(pattern); return regex.test(currentUrl); } catch (e) { console.error('Invalid URL pattern:', pattern, e); return false; } } function patternToRegex(pattern) { // Escape special characters for regex const escapeRegex = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); const parts = pattern.split('*'); let regexString = '^'; for (let i = 0; i < parts.length; i++) { regexString += escapeRegex(parts[i]); if (i < parts.length - 1) { if (i < parts.length - 2 && parts[i + 1] === '') { // '**' matches any number of path segments regexString += '.*'; i++; // Skip the next empty part } else { // Single '*' matches anything except '/' regexString += '[^/]*'; } } } regexString += '$'; return new RegExp(regexString); } function showCurrentPageNotes() { const notes = getAllNotes(); const currentUrl = window.location.href; let matchingNotes = []; // Collect all matching notes for (const urlPattern in notes) { if (doesUrlMatchPattern(urlPattern, currentUrl)) { matchingNotes.push({ pattern: urlPattern, notes: notes[urlPattern] }); } } const container = document.createElement('div'); container.innerHTML = ` <h3 class="modal-title">Notes for Current Page</h3> <div class="url-text">${currentUrl}</div> `; if (matchingNotes.length === 0) { container.innerHTML += '<p style="color: #6b7280;">No matching notes found for this page</p>'; } else { matchingNotes.forEach(({pattern, notes: patternNotes}) => { const patternDiv = document.createElement('div'); if (options.showUrlLinksInNotesList) { patternDiv.innerHTML = `<div class="url-text">Pattern: ${pattern}</div>`; } patternNotes.forEach((note, index) => { const noteDiv = document.createElement('div'); noteDiv.className = 'notes-list-item'; // Apply note color if available if (note.color) { noteDiv.style.borderLeft = `4px solid ${note.color}`; noteDiv.style.paddingLeft = '12px'; } // Add tags if available let tagsHTML = ''; if (note.tags && note.tags.length > 0) { tagsHTML = '<div style="margin-top: 4px;">'; note.tags.forEach(tag => { tagsHTML += `<span class="notes-tag">${tag}</span>`; }); tagsHTML += '</div>'; } noteDiv.innerHTML = ` <div style="flex-grow: 1; display: flex; flex-direction: column;"> <span style="font-weight: 500;">${note.title}</span> ${tagsHTML} </div> <button class="delete-note-button" title="Delete note">×</button> `; noteDiv.onclick = (e) => { if (!e.target.classList.contains('delete-note-button')) { container.parentElement.parentElement.remove(); showNoteContent(note, pattern, index); } }; noteDiv.querySelector('.delete-note-button').onclick = (e) => { e.stopPropagation(); if (confirm('Are you sure you want to delete this note?')) { deleteNote(pattern, index); noteDiv.remove(); if (patternNotes.length === 1) { patternDiv.remove(); } } }; patternDiv.appendChild(noteDiv); }); container.appendChild(patternDiv); }); } // Add help button and dropdown const helpButton = document.createElement('button'); helpButton.textContent = '?'; helpButton.style.position = 'absolute'; helpButton.style.top = '16px'; helpButton.style.right = '56px'; helpButton.style.width = '32px'; helpButton.style.height = '32px'; helpButton.style.borderRadius = '50%'; helpButton.style.border = 'none'; helpButton.style.background = isDarkMode ? '#374151' : '#e5e7eb'; helpButton.style.color = isDarkMode ? '#f3f4f6' : '#4b5563'; helpButton.style.fontSize = '18px'; helpButton.style.cursor = 'pointer'; helpButton.style.display = 'flex'; helpButton.style.alignItems = 'center'; helpButton.style.justifyContent = 'center'; helpButton.title = 'URL Pattern Help'; const helpDropdown = document.createElement('div'); helpDropdown.style.position = 'absolute'; helpDropdown.style.top = '52px'; helpDropdown.style.right = '56px'; helpDropdown.style.background = isDarkMode ? '#1f2937' : '#ffffff'; helpDropdown.style.border = `1px solid ${isDarkMode ? '#4b5563' : '#e5e7eb'}`; helpDropdown.style.borderRadius = '8px'; helpDropdown.style.padding = '16px'; helpDropdown.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)'; helpDropdown.style.zIndex = '10001'; helpDropdown.style.display = 'none'; helpDropdown.style.maxWidth = '300px'; helpDropdown.style.color = isDarkMode ? '#f3f4f6' : '#4b5563'; helpDropdown.innerHTML = ` <strong>URL Pattern Examples:</strong><br> - https://domain.com/* (matches entire domain, one level deep)<br> - https://domain.com/** (matches entire domain, any number of levels)<br> - https://domain.com/specific/* (matches specific path and one level below)<br> - https://domain.com/specific/** (matches specific path and any levels below)<br> - https://domain.com/*/specific (matches specific ending, one level in between)<br> - https://domain.com/**/specific (matches specific ending, any number of levels in between) `; let isDropdownOpen = false; helpButton.onmouseenter = () => { if (!isDropdownOpen) { helpDropdown.style.display = 'block'; } }; helpButton.onmouseleave = () => { if (!isDropdownOpen) { helpDropdown.style.display = 'none'; } }; helpButton.onclick = () => { isDropdownOpen = !isDropdownOpen; helpDropdown.style.display = isDropdownOpen ? 'block' : 'none'; }; document.addEventListener('click', (e) => { if (isDropdownOpen && e.target !== helpButton && !helpDropdown.contains(e.target)) { isDropdownOpen = false; helpDropdown.style.display = 'none'; } }); container.appendChild(helpButton); container.appendChild(helpDropdown); createModal(container); } function showAllNotes() { const notes = getAllNotes(); const container = document.createElement('div'); container.innerHTML = '<h3 class="modal-title">All Notes</h3>'; // Add search button const searchButton = document.createElement('button'); searchButton.className = 'notes-button'; searchButton.textContent = '🔍 Search Notes'; searchButton.style.marginBottom = '16px'; searchButton.onclick = showSearchModal; container.appendChild(searchButton); const notesPerPage = options.maxPatternsPerPage; let currentPage = 0; const urls = Object.keys(notes).sort(); // Create a scrollable container for notes const notesWrapper = document.createElement('div'); notesWrapper.id = 'notes-wrapper'; if (options.scrollContentOnly) { notesWrapper.style.maxHeight = '60vh'; // Limit height to keep pagination visible notesWrapper.style.overflowY = 'auto'; } notesWrapper.style.marginBottom = '8px'; container.appendChild(notesWrapper); // Create a dedicated container for pagination controls const paginationContainer = document.createElement('div'); paginationContainer.id = 'pagination-container'; paginationContainer.style.display = 'flex'; paginationContainer.style.gap = '8px'; paginationContainer.style.justifyContent = 'flex-end'; // Align buttons to the right container.appendChild(paginationContainer); function renderPage() { // Clear existing notes const existingNotesContainer = document.getElementById('notes-container'); if (existingNotesContainer) { existingNotesContainer.remove(); } const fragment = document.createDocumentFragment(); const start = currentPage * notesPerPage; const end = Math.min(start + notesPerPage, urls.length); const notesContainer = document.createElement('div'); notesContainer.id = 'notes-container'; for (let i = start; i < end; i++) { const url = urls[i]; const urlDiv = document.createElement('div'); urlDiv.innerHTML = `<div class="url-text">${url}</div>`; const notesFragment = document.createDocumentFragment(); notes[url].forEach((note, index) => { const noteDiv = document.createElement('div'); noteDiv.className = 'notes-list-item'; noteDiv.dataset.url = url; noteDiv.dataset.index = index; if (note.color) { noteDiv.style.borderLeft = `4px solid ${note.color}`; noteDiv.style.paddingLeft = '12px'; const colorOpacity = isDarkMode ? '0.1' : '0.05'; noteDiv.style.backgroundColor = `${note.color}${colorOpacity}`; } let tagsHTML = ''; if (note.tags && note.tags.length > 0) { tagsHTML = '<div style="margin-top: 4px;">'; note.tags.forEach(tag => { tagsHTML += `<span class="notes-tag">${tag}</span>`; }); tagsHTML += '</div>'; } const pinnedIndicator = note.pinned ? '<span title="Pinned" style="margin-right: 5px; color: #f59e0b;">📌</span>' : ''; noteDiv.innerHTML = ` <div style="flex-grow: 1; display: flex; flex-direction: column;"> <span style="font-weight: 500;">${pinnedIndicator}${note.title}</span> ${tagsHTML} </div> <button class="delete-note-button" title="Delete note">×</button> `; noteDiv.onclick = (e) => { if (!e.target.classList.contains('delete-note-button')) { container.parentElement.parentElement.remove(); showNoteContent(note, url, index); } }; noteDiv.querySelector('.delete-note-button').onclick = (e) => { e.stopPropagation(); if (confirm('Are you sure you want to delete this note?')) { deleteNote(url, index); noteDiv.remove(); if (notes[url].length === 1) { urlDiv.remove(); } // Update URLs and adjust page if needed const newUrls = Object.keys(getAllNotes()).sort(); if (currentPage * notesPerPage >= newUrls.length) { currentPage = Math.max(0, Math.floor((newUrls.length - 1) / notesPerPage)); } renderPage(); } }; notesFragment.appendChild(noteDiv); }); urlDiv.appendChild(notesFragment); fragment.appendChild(urlDiv); } notesContainer.appendChild(fragment); notesWrapper.appendChild(notesContainer); // Update pagination controls paginationContainer.innerHTML = ''; // Clear previous buttons // Only show Previous button if not on first page if (currentPage > 0) { const prevButton = document.createElement('button'); prevButton.className = 'notes-button secondary'; prevButton.textContent = 'Previous'; prevButton.onclick = () => { currentPage--; renderPage(); // Scroll to top of notes after page change notesWrapper.scrollTop = 0; }; paginationContainer.appendChild(prevButton); } // Only show Next button if not on last page if (end < urls.length) { const nextButton = document.createElement('button'); nextButton.className = 'notes-button secondary'; nextButton.textContent = 'Next'; nextButton.onclick = () => { currentPage++; renderPage(); // Scroll to top of notes after page change notesWrapper.scrollTop = 0; }; paginationContainer.appendChild(nextButton); } } if (urls.length === 0) { container.innerHTML += '<p style="color: #6b7280;">No notes found</p>'; } else { renderPage(); } createModal(container); } let shortcutListenerAdded = false; function setupShortcutListener() { if (!shortcutListenerAdded) { document.addEventListener('keydown', shortcutHandler); shortcutListenerAdded = true; } } function updateShortcutListener() { // Only call this when options actually change document.removeEventListener('keydown', shortcutHandler); shortcutListenerAdded = false; setupShortcutListener(); } function shortcutHandler(e) { if (matchShortcut(e, options.shortcuts.newNote)) { e.preventDefault(); showNoteForm(); } if (matchShortcut(e, options.shortcuts.currentPageNotes)) { e.preventDefault(); showCurrentPageNotes(); } if (matchShortcut(e, options.shortcuts.allNotes)) { e.preventDefault(); showAllNotes(); } if (matchShortcut(e, options.shortcuts.showOptions)) { e.preventDefault(); showOptionsMenu(); } } function matchShortcut(e, shortcut) { return e.ctrlKey === shortcut.ctrlKey && e.shiftKey === shortcut.shiftKey && e.altKey === shortcut.altKey && e.key.toLowerCase() === shortcut.key.toLowerCase(); } displayPinnedNotes(); setupShortcutListener(); // Register menu commands GM_registerMenuCommand('New Note', () => showNoteForm()); GM_registerMenuCommand('View Notes (Current Page)', showCurrentPageNotes); GM_registerMenuCommand('View All Notes', showAllNotes); GM_registerMenuCommand('Options', showOptionsMenu); })();