- // ==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.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;">
- <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="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();
-
- 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 searchResults = document.getElementById('searchResults');
- searchResults.innerHTML = '';
-
- if (!query) {
- searchResults.innerHTML = '<p style="color: #6b7280;">Enter a search term to find notes</p>';
- return;
- }
-
- const notes = getAllNotes();
- let resultCount = 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) => {
- addNoteResult(urlDiv, note, url, index);
- });
-
- searchResults.appendChild(urlDiv);
- resultCount += 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);
- addNoteResult(urlDiv, note, url, index, query);
- });
-
- searchResults.appendChild(urlDiv);
- resultCount += matchingNotes.length;
- }
- }
-
- if (resultCount === 0) {
- searchResults.innerHTML = '<p style="color: #6b7280;">No matching notes found</p>';
- } else {
- searchResults.insertAdjacentHTML('afterbegin', `<p style="color: #6b7280;">${resultCount} note${resultCount !== 1 ? 's' : ''} found</p>`);
- }
-
- // 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);
- }
- }
- }
-
-
- 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 (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);
- }
- });
- }
- }
- }
-
-
- function doesUrlMatchPattern(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 (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 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);
-
- })();