Professional notes manager with editable URLs, modern interface, and quick delete functionality
// ==UserScript== // @name Professional Website Notes Manager // @namespace http://tampermonkey.net/ // @version 0.8 // @description Professional notes manager with editable URLs, modern interface, and quick delete functionality // @author Byakuran // @match https://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_listValues // @grant GM_deleteValue // ==/UserScript== (function() { 'use strict'; let scriptVersion = '0.8' const defaultOptions = { version: scriptVersion, darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches, addTimestampToTitle: false, showUrlLinksInNotesList: true, autoBackup: true, 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); alert('Options updated to version ' + defaultOptions.version); console.log('Options updated to version ' + defaultOptions.version); } return currentOptions; } 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 .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-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,${isDarkMode ? '0.8' : '0.7'}); z-index: 9999; backdrop-filter: blur(4px); } .notes-overlay .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-overlay .notes-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .notes-overlay .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-overlay .notes-textarea:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .notes-overlay .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-overlay .notes-button:hover { background: ${currentTheme.button.primaryHover}; transform: translateY(-1px); } .notes-overlay .notes-button.secondary { background: ${currentTheme.button.secondary}; color: ${isDarkMode ? '#f3f4f6' : '#4b5563'}; } .notes-overlay .notes-button.secondary:hover { background: ${currentTheme.button.secondaryHover}; } .notes-overlay .notes-button.delete { background: #ef4444; } .notes-overlayt .notes-button.delete:hover { background: #dc2626; } .notes-overlay .notes-button.edit { background: #10b981; } .notes-overlay .notes-button.edit:hover { background: #059669; } .notes-overlay .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}; } .notes-overlay .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'}); } .notes-overlay .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; } .notes-overlay .close-button:hover { color: ${isDarkMode ? '#f3f4f6' : '#111827'}; background: ${isDarkMode ? '#374151' : '#f3f4f6'}; } .notes-overlay .modal-title { font-size: 20px; font-weight: 600; margin-bottom: 24px; color: ${currentTheme.modal.text}; } .notes-overlay .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; } .notes-overlay .timestamp { font-size: 12px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}; margin-top: 4px; } .notes-overlay .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; } .notes-overlay .delete-note-button:hover { background: #ef4444; color: #ffffff; } .notes-overlay .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-overlay .notes-options-input:focus { outline: none; border-color: #3b82f6; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1); } .notes-overlay .notes-options-checkbox { margin-right: 8px; } .notes-overlay .notes-options-label { display: flex; align-items: center; margin: 10px 0; color: ${currentTheme.modal.text}; } .notes-overlay .notes-editor-toolbar { display: flex; gap: 8px; margin: 8px 0; padding: 8px; background: ${isDarkMode ? '#2a3441' : '#f3f4f6'}; border-radius: 6px; } .notes-overlay .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; } `; 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 showOptionsMenu() { 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> <h4 style="margin-top: 20px;">Keyboard Shortcuts</h4> <div> <label>New Note: <input type="text" class="notes-options-input" id="newNoteShortcut" value="${getShortcutString(options.shortcuts.newNote)}"> </label> </div> <div> <label>Current Page Notes: <input type="text" class="notes-options-input" id="currentPageNotesShortcut" value="${getShortcutString(options.shortcuts.currentPageNotes)}"> </label> </div> <div> <label>All Notes: <input type="text" class="notes-options-input" id="allNotesShortcut" value="${getShortcutString(options.shortcuts.allNotes)}"> </label> </div> <div> <label>Show Options: <input type="text" class="notes-options-input" id="showOptionsWindow" value="${getShortcutString(options.shortcuts.showOptions)}"> </label> </div> <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(); // Add event listeners document.getElementById('saveOptions').onclick = saveOptions; document.getElementById('exportNotesBtn').onclick = exportNotes; document.getElementById('importNotesBtn').onclick = importNotes; } 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' }; } // Replace the saveOptions function with this corrected version function saveOptions() { try { options = { version: scriptVersion, darkMode: document.getElementById('darkModeToggle').checked, addTimestampToTitle: document.getElementById('timestampToggle').checked, showUrlLinksInNotesList: document.getElementById('showUrlLinksToggle').checked, autoBackup: document.getElementById('autoBackupToggle').checked, shortcuts: { newNote: parseShortcutString(document.getElementById('newNoteShortcut').value), currentPageNotes: parseShortcutString(document.getElementById('currentPageNotesShortcut').value), allNotes: parseShortcutString(document.getElementById('allNotesShortcut').value), showOptions: parseShortcutString(document.getElementById('showOptionsWindow').value) } }; GM_setValue('options', options); setupShortcutListener(); 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(); 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.r###lt); // 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;"> <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="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('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 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="searchR###lts" 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(); searchInput.addEventListener('input', performSearch); document.getElementById('searchTitle').addEventListener('change', performSearch); document.getElementById('searchContent').addEventListener('change', performSearch); document.getElementById('searchTags').addEventListener('change', performSearch); document.getElementById('searchUrls').addEventListener('change', performSearch); 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 searchR###lts = document.getElementById('searchR###lts'); searchR###lts.innerHTML = ''; if (!query) { searchR###lts.innerHTML = '<p style="color: #6b7280;">Enter a search term to find notes</p>'; return; } const notes = getAllNotes(); let r###ltCount = 0; // 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>'); } // Search through all notes for (const url in notes) { if (searchUrls && url.toLowerCase().includes(query)) { // The URL itself matches const urlDiv = document.createElement('div'); urlDiv.innerHTML = `<div class="url-text">${highlightMatch(url, query)}</div>`; // Add all notes under this URL notes[url].forEach((note, index) => { addNoteR###lt(urlDiv, note, url, index); }); searchR###lts.appendChild(urlDiv); r###ltCount += notes[url].length; continue; } // Check if any notes match the search criteria const matchingNotes = notes[url].filter(note => { if (searchTitle && note.title.toLowerCase().includes(query)) return true; if (searchContent && note.content.toLowerCase().includes(query)) return true; if (searchTags && note.tags && note.tags.some(tag => tag.toLowerCase().includes(query))) return true; return false; }); if (matchingNotes.length > 0) { const urlDiv = document.createElement('div'); urlDiv.innerHTML = `<div class="url-text">${url}</div>`; matchingNotes.forEach(note => { const index = notes[url].indexOf(note); addNoteR###lt(urlDiv, note, url, index, query); }); searchR###lts.appendChild(urlDiv); r###ltCount += matchingNotes.length; } } if (r###ltCount === 0) { searchR###lts.innerHTML = '<p style="color: #6b7280;">No matching notes found</p>'; } else { searchR###lts.insertAdjacentHTML('afterbegin', `<p style="color: #6b7280;">${r###ltCount} note${r###ltCount !== 1 ? 's' : ''} found</p>`); } // Helper function to add a note r###lt to the r###lts div function addNoteR###lt(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); } } } 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(); 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 saveNote(title, url, content, timestamp = Date.now(), pinned = false, tags = [], color = null) { const notes = getAllNotes(); if (!notes[url]) notes[url] = []; // Add timestamp to title if the option is enabled let finalTitle = title; if (options.addTimestampToTitle) { const date = new Date(timestamp); const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString(); finalTitle = `${title} [${formattedDate}]`; } notes[url].push({ title: finalTitle, content, timestamp, pinned, tags, color }); GM_setValue('website-notes', notes); // Perform auto-backup if enabled if (options.autoBackup) { performAutoBackup(); } } 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]; // Delete the old note deleteNote(oldUrl, index); // Save with updated values but keep the original timestamp saveNote( title, newUrl, content, existingNote.timestamp, pinned, tags, color ); } 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 ? existingNote.color : '#3b82f6'); 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 = '#3b82f6') { 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('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'); // Function to convert URLs to clickable links and preserve line breaks function linkify(text) { // URL pattern for matching const urlPattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim; // Replace URLs with anchor tags const linkedText = text.replace(urlPattern, function(url) { return `<a href="${url}" target="_blank" style="color: #3b82f6; text-decoration: underline; word-break: break-all;" onclick="event.stopPropagation();">${url}</a>`; }); // Process markdown formatting let formattedText = linkedText // Bold .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>') // Italic .replace(/_(.*?)_/g, '<em>$1</em>') // Headers .replace(/^# (.*?)$/gm, '<h1 style="font-size: 1.5em; margin-top: 0.8em; margin-bottom: 0.5em;">$1</h1>') .replace(/^## (.*?)$/gm, '<h2 style="font-size: 1.3em; margin-top: 0.7em; margin-bottom: 0.4em;">$1</h2>') // Lists .replace(/^- (.*?)$/gm, '• $1<br>') // Blockquotes .replace(/^> (.*?)$/gm, '<blockquote style="border-left: 3px solid #9ca3af; padding-left: 10px; margin-left: 5px; color: #6b7280;">$1</blockquote>'); // Convert line breaks to <br> tags and maintain whitespace return formattedText.replace(/\n/g, '<br>').replace(/\s{2,}/g, function(space) { return ' ' + ' '.repeat(space.length - 1); }); } // Create a hidden textarea for proper copying const hiddenTextarea = document.createElement('textarea'); hiddenTextarea.style.position = 'absolute'; hiddenTextarea.style.left = '-9999px'; hiddenTextarea.style.top = '-9999px'; document.body.appendChild(hiddenTextarea); // Create note header with title and color const noteHeader = document.createElement('div'); noteHeader.className = 'note-header'; noteHeader.style.display = 'flex'; noteHeader.style.alignItems = 'center'; noteHeader.style.marginBottom = '16px'; // Create color indicator const colorIndicator = document.createElement('div'); colorIndicator.style.width = '16px'; colorIndicator.style.height = '16px'; colorIndicator.style.borderRadius = '50%'; colorIndicator.style.marginRight = '8px'; colorIndicator.style.backgroundColor = note.color || '#3b82f6'; // Create the actual content container const contentContainer = document.createElement('div'); contentContainer.className = 'note-content-container'; contentContainer.style.padding = '16px'; contentContainer.style.borderRadius = '8px'; contentContainer.style.marginBottom = '16px'; // Apply the note color 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'; } // Add tags display if the note has tags 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} `; // Add content to the content container contentContainer.innerHTML = linkify(note.content); container.appendChild(contentContainer); // Add copy event listener to the content div contentContainer.addEventListener('copy', (e) => { e.preventDefault(); const selection = window.getSelection(); const selectedText = selection.toString(); // Replace <br> tags with actual newlines in the copied text hiddenTextarea.value = selectedText.replace(/\s*\n\s*/g, '\n'); hiddenTextarea.select(); document.execCommand('copy'); // Clean up selection.removeAllRanges(); selection.addRange(document.createRange()); }); 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(); showCurrentPageNotes(); } }; const pinButton = document.createElement('button'); pinButton.className = `notes-button ${note.pinned ? 'secondary' : ''}`; pinButton.textContent = note.pinned ? 'Unpin' : 'Pin'; pinButton.onclick = () => { togglePinNote(url, index); // Get the updated notes data after toggling pin status const notes = getAllNotes(); // Update the button text and class based on the updated pin status const isPinned = notes[url] && notes[url][index] ? notes[url][index].pinned : false; pinButton.textContent = isPinned ? 'Unpin' : 'Pin'; pinButton.className = `notes-button ${isPinned ? '' : 'secondary'}`; // Update the pinned notes display 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 (do###rlMatchPattern(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); } }); } } } function do###rlMatchPattern(urlPatterns, currentUrl) { // Split the pattern string into an array of patterns const patterns = urlPatterns.split(/\s+/).filter(pattern => pattern.trim() !== ''); // Check if any of the patterns match the current URL return patterns.some(pattern => { // Escape special characters for regex const escapeRegex = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Convert Tampermonkey-style pattern to regex const patternToRegex = (pattern) => { const parts = pattern.split('*'); let regexString = '^'; for (let i = 0; i < parts.length; i++) { regexString += escapeRegex(parts[i]); if (i < parts.length - 1) { if (parts[i + 1] === '') { // '**' matches any number of path segments regexString += '.*'; i++; // Skip the next '*' } else { // Single '*' matches anything except '/' regexString += '[^/]*'; } } } // If the pattern ends with '**', allow anything at the end if (pattern.endsWith('**')) { regexString += '.*'; } else if (!pattern.endsWith('*')) { regexString += '$'; } return new RegExp(regexString); }; try { const regex = patternToRegex(pattern); return regex.test(currentUrl); } catch (e) { console.error('Invalid URL pattern:', e); return false; } }); } function showCurrentPageNotes() { const notes = getAllNotes(); const currentUrl = window.location.href; let matchingNotes = []; // Collect all matching notes for (const urlPattern in notes) { if (do###rlMatchPattern(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 a 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); if (Object.keys(notes).length === 0) { container.innerHTML += '<p style="color: #6b7280;">No notes found</p>'; } else { for (const url in notes) { const urlDiv = document.createElement('div'); urlDiv.innerHTML = `<div class="url-text">${url}</div>`; notes[url].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 subtle background tint based on the note color const colorOpacity = isDarkMode ? '0.1' : '0.05'; noteDiv.style.backgroundColor = `${note.color}${colorOpacity}`; } // 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>'; } // Add pin indicator if note is pinned 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(); } } }; urlDiv.appendChild(noteDiv); }); container.appendChild(urlDiv); } } createModal(container); } function setupShortcutListener() { document.removeEventListener('keydown', shortcutHandler); document.addEventListener('keydown', shortcutHandler); } 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); })();