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