Greasy Fork 支持简体中文。

Professional Website Notes Manager

Professional notes manager with editable URLs, modern interface, and quick delete functionality

  1. // ==UserScript==
  2. // @name Professional Website Notes Manager
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.8
  5. // @description Professional notes manager with editable URLs, modern interface, and quick delete functionality
  6. // @author Byakuran
  7. // @match https://*/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_listValues
  12. // @grant GM_deleteValue
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. let scriptVersion = '0.8'
  19.  
  20. const defaultOptions = {
  21. version: scriptVersion,
  22. darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
  23. addTimestampToTitle: false,
  24. showUrlLinksInNotesList: true,
  25. autoBackup: true,
  26. shortcuts: {
  27. newNote: { ctrlKey: true, shiftKey: true, key: 'S' },
  28. currentPageNotes: { ctrlKey: true, shiftKey: true, key: 'C' },
  29. allNotes: { ctrlKey: true, shiftKey: true, key: 'L' },
  30. showOptions: { ctrlKey: true, altKey: true, key: 'O' }
  31. }
  32. };
  33.  
  34. let options = checkAndUpdateOptions();
  35. GM_setValue('options', options);
  36.  
  37. function checkAndUpdateOptions() {
  38. let currentOptions;
  39. try {
  40. currentOptions = GM_getValue('options', defaultOptions);
  41. } catch (error) {
  42. console.error('Error loading options, resetting to defaults:', error);
  43. return defaultOptions;
  44. }
  45.  
  46. // If options is not an object for some reason
  47. if (!currentOptions || typeof currentOptions !== 'object') {
  48. console.warn('Invalid options found, resetting to defaults');
  49. return defaultOptions;
  50. }
  51.  
  52. // Check if the version has changed or if it doesn't exist
  53. if (!currentOptions.version || currentOptions.version !== defaultOptions.version) {
  54. // Version has changed, update options
  55. for (let key in defaultOptions) {
  56. if (!(key in currentOptions)) {
  57. currentOptions[key] = defaultOptions[key];
  58. }
  59. }
  60.  
  61. // Update nested objects (shortcuts, possibly more later)
  62. if (!currentOptions.shortcuts || typeof currentOptions.shortcuts !== 'object') {
  63. currentOptions.shortcuts = defaultOptions.shortcuts;
  64. } else {
  65. for (let key in defaultOptions.shortcuts) {
  66. if (!(key in currentOptions.shortcuts)) {
  67. currentOptions.shortcuts[key] = defaultOptions.shortcuts[key];
  68. }
  69. }
  70. }
  71.  
  72. // Update the version
  73. currentOptions.version = defaultOptions.version;
  74.  
  75. // Save the updated options
  76. GM_setValue('options', currentOptions);
  77.  
  78. alert('Options updated to version ' + defaultOptions.version);
  79. console.log('Options updated to version ' + defaultOptions.version);
  80. }
  81.  
  82. return currentOptions;
  83. }
  84.  
  85. const isDarkMode = options.darkMode;
  86.  
  87. const darkModeStyles = {
  88. modal: {
  89. bg: '#1f2937',
  90. text: '#f3f4f6'
  91. },
  92. input: {
  93. bg: '#374151',
  94. border: '#4b5563',
  95. text: '#f3f4f6'
  96. },
  97. button: {
  98. primary: '#3b82f6',
  99. primaryHover: '#2563eb',
  100. secondary: '#4b5563',
  101. secondaryHover: '#374151',
  102. text: '#ffffff'
  103. },
  104. listItem: {
  105. bg: '#374151',
  106. bgHover: '#4b5563',
  107. text: '#f3f4f6'
  108. }
  109. };
  110.  
  111. const lightModeStyles = {
  112. modal: {
  113. bg: '#ffffff',
  114. text: '#111827'
  115. },
  116. input: {
  117. bg: '#f9fafb',
  118. border: '#e5e7eb',
  119. text: '#111827'
  120. },
  121. button: {
  122. primary: '#3b82f6',
  123. primaryHover: '#2563eb',
  124. secondary: '#f3f4f6',
  125. secondaryHover: '#e5e7eb',
  126. text: '#ffffff'
  127. },
  128. listItem: {
  129. bg: '#ffffff',
  130. bgHover: '#f9fafb',
  131. text: '#1f2937'
  132. }
  133. };
  134.  
  135. const currentTheme = isDarkMode ? darkModeStyles : lightModeStyles;
  136.  
  137. const styles = `
  138. .notes-overlay .notes-modal {
  139. position: fixed;
  140. top: 50%;
  141. left: 50%;
  142. transform: translate(-50%, -50%);
  143. background: ${currentTheme.modal.bg};
  144. color: ${currentTheme.modal.text};
  145. padding: 32px;
  146. border-radius: 16px;
  147. box-shadow: 0 8px 32px rgba(0,0,0,0.25);
  148. z-index: 10000;
  149. max-width: 700px;
  150. width: 90%;
  151. max-height: 90vh;
  152. overflow-y: auto;
  153. font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  154. }
  155. .notes-overlay {
  156. position: fixed;
  157. top: 0;
  158. left: 0;
  159. right: 0;
  160. bottom: 0;
  161. background: rgba(0,0,0,${isDarkMode ? '0.8' : '0.7'});
  162. z-index: 9999;
  163. backdrop-filter: blur(4px);
  164. }
  165. .notes-overlay .notes-input {
  166. width: 100%;
  167. margin: 12px 0;
  168. padding: 12px 16px;
  169. border: 2px solid ${currentTheme.input.border};
  170. border-radius: 8px;
  171. font-size: 15px;
  172. transition: all 0.2s ease;
  173. background: ${currentTheme.input.bg};
  174. color: ${currentTheme.input.text};
  175. box-sizing: border-box;
  176. }
  177. .notes-overlay .notes-input:focus {
  178. outline: none;
  179. border-color: #3b82f6;
  180. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  181. }
  182. .notes-overlay .notes-textarea {
  183. width: 100%;
  184. height: 200px;
  185. margin: 12px 0;
  186. padding: 16px;
  187. border: 2px solid ${currentTheme.input.border};
  188. border-radius: 8px;
  189. font-size: 15px;
  190. resize: vertical;
  191. transition: all 0.2s ease;
  192. background: ${currentTheme.input.bg};
  193. color: ${currentTheme.input.text};
  194. line-height: 1.5;
  195. box-sizing: border-box;
  196. }
  197. .notes-overlay .notes-textarea:focus {
  198. outline: none;
  199. border-color: #3b82f6;
  200. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  201. }
  202. .notes-overlay .notes-button {
  203. background: ${currentTheme.button.primary};
  204. color: ${currentTheme.button.text};
  205. border: none;
  206. padding: 12px 24px;
  207. border-radius: 8px;
  208. cursor: pointer;
  209. margin: 5px;
  210. font-size: 15px;
  211. font-weight: 500;
  212. transition: all 0.2s ease;
  213. }
  214. .notes-overlay .notes-button:hover {
  215. background: ${currentTheme.button.primaryHover};
  216. transform: translateY(-1px);
  217. }
  218. .notes-overlay .notes-button.secondary {
  219. background: ${currentTheme.button.secondary};
  220. color: ${isDarkMode ? '#f3f4f6' : '#4b5563'};
  221. }
  222. .notes-overlay .notes-button.secondary:hover {
  223. background: ${currentTheme.button.secondaryHover};
  224. }
  225. .notes-overlay .notes-button.delete {
  226. background: #ef4444;
  227. }
  228. .notes-overlayt .notes-button.delete:hover {
  229. background: #dc2626;
  230. }
  231. .notes-overlay .notes-button.edit {
  232. background: #10b981;
  233. }
  234. .notes-overlay .notes-button.edit:hover {
  235. background: #059669;
  236. }
  237. .notes-overlay .notes-list-item {
  238. display: flex;
  239. justify-content: space-between;
  240. align-items: center;
  241. padding: 16px;
  242. border: 1px solid ${currentTheme.input.border};
  243. border-radius: 8px;
  244. margin: 8px 0;
  245. cursor: pointer;
  246. transition: all 0.2s ease;
  247. background: ${currentTheme.listItem.bg};
  248. color: ${currentTheme.listItem.text};
  249. }
  250. .notes-overlay .notes-list-item:hover {
  251. background: ${currentTheme.listItem.bgHover};
  252. transform: translateY(-1px);
  253. box-shadow: 0 4px 12px rgba(0,0,0,${isDarkMode ? '0.3' : '0.05'});
  254. }
  255. .notes-overlay .close-button {
  256. position: absolute;
  257. top: 16px;
  258. right: 16px;
  259. cursor: pointer;
  260. font-size: 24px;
  261. color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
  262. transition: all 0.2s;
  263. width: 32px;
  264. height: 32px;
  265. display: flex;
  266. align-items: center;
  267. justify-content: center;
  268. border-radius: 6px;
  269. }
  270. .notes-overlay .close-button:hover {
  271. color: ${isDarkMode ? '#f3f4f6' : '#111827'};
  272. background: ${isDarkMode ? '#374151' : '#f3f4f6'};
  273. }
  274. .notes-overlay .modal-title {
  275. font-size: 20px;
  276. font-weight: 600;
  277. margin-bottom: 24px;
  278. color: ${currentTheme.modal.text};
  279. }
  280. .notes-overlay .url-text {
  281. font-size: 14px;
  282. color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
  283. word-break: break-all;
  284. margin-bottom: 16px;
  285. padding: 8px 12px;
  286. background: ${isDarkMode ? '#374151' : '#f3f4f6'};
  287. border-radius: 6px;
  288. }
  289. .notes-overlay .timestamp {
  290. font-size: 12px;
  291. color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
  292. margin-top: 4px;
  293. }
  294. .notes-overlay .delete-note-button {
  295. background: none;
  296. border: none;
  297. color: #ef4444;
  298. font-size: 18px;
  299. cursor: pointer;
  300. padding: 4px 8px;
  301. border-radius: 4px;
  302. transition: all 0.2s ease;
  303. }
  304. .notes-overlay .delete-note-button:hover {
  305. background: #ef4444;
  306. color: #ffffff;
  307. }
  308. .notes-overlay .notes-options-input {
  309. width: 100%;
  310. margin: 8px 0;
  311. padding: 10px 14px;
  312. border: 2px solid ${currentTheme.input.border};
  313. border-radius: 8px;
  314. font-size: 15px;
  315. transition: all 0.2s ease;
  316. background: ${currentTheme.input.bg};
  317. color: ${currentTheme.input.text};
  318. box-sizing: border-box;
  319. }
  320.  
  321. .notes-overlay .notes-options-input:focus {
  322. outline: none;
  323. border-color: #3b82f6;
  324. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  325. }
  326.  
  327. .notes-overlay .notes-options-checkbox {
  328. margin-right: 8px;
  329. }
  330.  
  331. .notes-overlay .notes-options-label {
  332. display: flex;
  333. align-items: center;
  334. margin: 10px 0;
  335. color: ${currentTheme.modal.text};
  336. }
  337.  
  338. .notes-overlay .notes-editor-toolbar {
  339. display: flex;
  340. gap: 8px;
  341. margin: 8px 0;
  342. padding: 8px;
  343. background: ${isDarkMode ? '#2a3441' : '#f3f4f6'};
  344. border-radius: 6px;
  345. }
  346.  
  347. .notes-overlay .notes-tag {
  348. display: inline-block;
  349. padding: 4px 8px;
  350. margin: 0 4px 4px 0;
  351. border-radius: 4px;
  352. background: ${isDarkMode ? '#4b5563' : '#e5e7eb'};
  353. color: ${isDarkMode ? '#f3f4f6' : '#374151'};
  354. font-size: 12px;
  355. }
  356. `;
  357.  
  358.  
  359. const mobileStyles = `
  360. @media (max-width: 768px) {
  361. .notes-overlay .notes-modal {
  362. width: 95%;
  363. padding: 16px;
  364. max-height: 95vh;
  365. }
  366.  
  367. .notes-overlay .notes-button {
  368. padding: 10px 16px;
  369. margin: 3px;
  370. font-size: 14px;
  371. }
  372.  
  373. .notes-overlay .close-button {
  374. top: 8px;
  375. right: 8px;
  376. }
  377.  
  378. .notes-overlay .button-group {
  379. display: flex;
  380. flex-direction: column;
  381. }
  382.  
  383. .notes-overlay .notes-list-item {
  384. padding: 12px;
  385. }
  386. }
  387. `;
  388.  
  389. const styleSheet = document.createElement("style");
  390. styleSheet.innerText = styles + mobileStyles;
  391. document.head.appendChild(styleSheet);
  392.  
  393. function showOptionsMenu() {
  394. const container = document.createElement('div');
  395. container.innerHTML = `
  396. <h3 class="modal-title">Options</h3>
  397. <div class="notes-options-label">
  398. <label>
  399. <input type="checkbox" class="notes-options-checkbox" id="darkModeToggle" ${options.darkMode ? 'checked' : ''}>
  400. Dark Mode
  401. </label>
  402. </div>
  403. <div class="notes-options-label">
  404. <label>
  405. <input type="checkbox" class="notes-options-checkbox" id="timestampToggle" ${options.addTimestampToTitle ? 'checked' : ''}>
  406. Add timestamp to note titles
  407. </label>
  408. </div>
  409. <div class="notes-options-label">
  410. <label>
  411. <input type="checkbox" class="notes-options-checkbox" id="showUrlLinksToggle" ${options.showUrlLinksInNotesList ? 'checked' : ''}>
  412. Show URL links in notes list
  413. </label>
  414. </div>
  415. <div class="notes-options-label">
  416. <label>
  417. <input type="checkbox" class="notes-options-checkbox" id="autoBackupToggle" ${options.autoBackup ? 'checked' : ''}>
  418. Enable automatic backups
  419. </label>
  420. </div>
  421. <h4 style="margin-top: 20px;">Keyboard Shortcuts</h4>
  422. <div>
  423. <label>New Note:
  424. <input type="text" class="notes-options-input" id="newNoteShortcut" value="${getShortcutString(options.shortcuts.newNote)}">
  425. </label>
  426. </div>
  427. <div>
  428. <label>Current Page Notes:
  429. <input type="text" class="notes-options-input" id="currentPageNotesShortcut" value="${getShortcutString(options.shortcuts.currentPageNotes)}">
  430. </label>
  431. </div>
  432. <div>
  433. <label>All Notes:
  434. <input type="text" class="notes-options-input" id="allNotesShortcut" value="${getShortcutString(options.shortcuts.allNotes)}">
  435. </label>
  436. </div>
  437. <div>
  438. <label>Show Options:
  439. <input type="text" class="notes-options-input" id="showOptionsWindow" value="${getShortcutString(options.shortcuts.showOptions)}">
  440. </label>
  441. </div>
  442. <div style="margin-top: 20px; display: flex; gap: 10px;">
  443. <button id="saveOptions" class="notes-button">Save Options</button>
  444. <button id="exportNotesBtn" class="notes-button secondary">Export Notes</button>
  445. <button id="importNotesBtn" class="notes-button secondary">Import Notes</button>
  446. </div>
  447. `;
  448.  
  449. createModal(container);
  450.  
  451. addRestoreBackupButton();
  452.  
  453. // Add event listeners
  454. document.getElementById('saveOptions').onclick = saveOptions;
  455. document.getElementById('exportNotesBtn').onclick = exportNotes;
  456. document.getElementById('importNotesBtn').onclick = importNotes;
  457. }
  458.  
  459. function getShortcutString(shortcut) {
  460. let str = '';
  461. if (shortcut.ctrlKey) str += 'Ctrl+';
  462. if (shortcut.shiftKey) str += 'Shift+';
  463. if (shortcut.altKey) str += 'Alt+';
  464. str += shortcut.key.toUpperCase();
  465. return str;
  466. }
  467.  
  468. function parseShortcutString(str) {
  469. if (!str || typeof str !== 'string') {
  470. console.warn('Invalid shortcut string:', str);
  471. // Return default values if string is invalid
  472. return {
  473. ctrlKey: true,
  474. shiftKey: true,
  475. altKey: false,
  476. key: 'S'
  477. };
  478. }
  479.  
  480. const parts = str.toLowerCase().split('+');
  481. return {
  482. ctrlKey: parts.includes('ctrl'),
  483. shiftKey: parts.includes('shift'),
  484. altKey: parts.includes('alt'),
  485. key: parts[parts.length - 1] || 'S'
  486. };
  487. }
  488.  
  489. // Replace the saveOptions function with this corrected version
  490. function saveOptions() {
  491. try {
  492. options = {
  493. version: scriptVersion,
  494. darkMode: document.getElementById('darkModeToggle').checked,
  495. addTimestampToTitle: document.getElementById('timestampToggle').checked,
  496. showUrlLinksInNotesList: document.getElementById('showUrlLinksToggle').checked,
  497. autoBackup: document.getElementById('autoBackupToggle').checked,
  498. shortcuts: {
  499. newNote: parseShortcutString(document.getElementById('newNoteShortcut').value),
  500. currentPageNotes: parseShortcutString(document.getElementById('currentPageNotesShortcut').value),
  501. allNotes: parseShortcutString(document.getElementById('allNotesShortcut').value),
  502. showOptions: parseShortcutString(document.getElementById('showOptionsWindow').value)
  503. }
  504. };
  505. GM_setValue('options', options);
  506. setupShortcutListener();
  507. alert('Options saved successfully. Some changes may require reloading the page.');
  508. } catch (error) {
  509. console.error('Error saving options:', error);
  510. alert('Failed to save options. Please try again.');
  511. }
  512. }
  513.  
  514. function exportNotes() {
  515. try {
  516. const notes = getAllNotes();
  517. const dateInfo = getFormattedBackupDate();
  518. const blob = new Blob([JSON.stringify(notes, null, 2)], {type: 'application/json'});
  519. const url = URL.createObjectURL(blob);
  520.  
  521. const a = document.createElement('a');
  522. a.href = url;
  523. a.download = `website-notes-backup-${dateInfo.formatted}.json`;
  524. a.click();
  525.  
  526. URL.revokeObjectURL(url);
  527. } catch (error) {
  528. console.error('Error exporting notes:', error);
  529. alert('Failed to export notes. Please try again.');
  530. }
  531. }
  532.  
  533. function importNotes() {
  534. const input = document.createElement('input');
  535. input.type = 'file';
  536. input.accept = '.json';
  537.  
  538. input.onchange = (e) => {
  539. const file = e.target.files[0];
  540. if (!file) return;
  541.  
  542. const reader = new FileReader();
  543.  
  544. reader.onload = (event) => {
  545. try {
  546. const importedNotes = JSON.parse(event.target.result);
  547.  
  548. // Create custom modal for import options
  549. const overlay = document.createElement('div');
  550. overlay.className = 'notes-overlay';
  551.  
  552. const modal = document.createElement('div');
  553. modal.className = 'notes-modal';
  554. modal.style.maxWidth = '500px';
  555.  
  556. const closeButton = document.createElement('span');
  557. closeButton.className = 'close-button';
  558. closeButton.textContent = '×';
  559. closeButton.onclick = () => overlay.remove();
  560.  
  561. modal.innerHTML = `
  562. <h3 class="modal-title">Import Notes</h3>
  563. <p>Choose how to import the notes:</p>
  564.  
  565. <div class="notes-list-item" style="cursor: pointer; margin-bottom: 12px;">
  566. <div>
  567. <strong>Merge</strong>
  568. <p style="margin: 5px 0; font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}">
  569. Add imported notes to your existing notes. This will keep all your current notes and may create duplicates.
  570. </p>
  571. </div>
  572. </div>
  573.  
  574. <div class="notes-list-item" style="cursor: pointer;">
  575. <div>
  576. <strong>Replace</strong>
  577. <p style="margin: 5px 0; font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'}">
  578. Replace all your current notes with the imported ones. This will delete all your existing notes.
  579. </p>
  580. </div>
  581. </div>
  582.  
  583. <div style="display: flex; justify-content: space-between; margin-top: 20px;">
  584. <button id="mergeBtn" class="notes-button">Merge</button>
  585. <button id="replaceBtn" class="notes-button delete">Replace</button>
  586. <button id="cancelBtn" class="notes-button secondary">Cancel</button>
  587. </div>
  588. `;
  589.  
  590. modal.appendChild(closeButton);
  591. overlay.appendChild(modal);
  592. document.body.appendChild(overlay);
  593.  
  594. // Add event listeners
  595. document.getElementById('mergeBtn').onclick = () => {
  596. mergeNotes(importedNotes);
  597. overlay.remove();
  598. };
  599.  
  600. document.getElementById('replaceBtn').onclick = () => {
  601. if (confirm('This will permanently replace all your existing notes. Are you sure?')) {
  602. GM_setValue('website-notes', importedNotes);
  603. alert('Notes replaced successfully!');
  604. overlay.remove();
  605. }
  606. };
  607.  
  608. document.getElementById('cancelBtn').onclick = () => {
  609. overlay.remove();
  610. };
  611.  
  612. } catch (error) {
  613. console.error('Error parsing imported notes:', error);
  614. alert('Error importing notes: Invalid format');
  615. }
  616. };
  617.  
  618. reader.readAsText(file);
  619. };
  620.  
  621. input.click();
  622. }
  623.  
  624. function mergeNotes(importedNotes) {
  625. try {
  626. // Get existing notes
  627. const existingNotes = getAllNotes();
  628.  
  629. // Count imported notes for notification
  630. let importedCount = 0;
  631.  
  632. // Merge notes by URL
  633. for (const url in importedNotes) {
  634. if (existingNotes[url]) {
  635. // If URL exists, append notes to existing array
  636. existingNotes[url] = existingNotes[url].concat(importedNotes[url]);
  637. importedCount += importedNotes[url].length;
  638. } else {
  639. // If URL is new, add all notes
  640. existingNotes[url] = importedNotes[url];
  641. importedCount += importedNotes[url].length;
  642. }
  643. }
  644.  
  645. // Save merged notes back to storage
  646. GM_setValue('website-notes', existingNotes);
  647.  
  648. // Perform auto-backup if enabled
  649. if (options.autoBackup) {
  650. performAutoBackup();
  651. }
  652.  
  653. alert(`Notes merged successfully! ${importedCount} notes were imported.`);
  654. } catch (error) {
  655. console.error('Error merging notes:', error);
  656. alert('Error merging notes. Please try again.');
  657. }
  658. }
  659.  
  660. function addRestoreBackupButton() {
  661. // Create a restore backup button
  662. const restoreBackupBtn = document.createElement('button');
  663. restoreBackupBtn.id = 'restoreBackupBtn';
  664. restoreBackupBtn.className = 'notes-button secondary';
  665. restoreBackupBtn.textContent = 'Restore Backup';
  666.  
  667. // Add it to the export/import button group
  668. const buttonGroup = document.querySelector('[id="saveOptions"]').parentNode;
  669. buttonGroup.appendChild(restoreBackupBtn);
  670.  
  671. // Add event listener
  672. document.getElementById('restoreBackupBtn').onclick = showBackupsList;
  673. }
  674.  
  675. function showBackupsList() {
  676. // Create modal for backup list
  677. const overlay = document.createElement('div');
  678. overlay.className = 'notes-overlay';
  679.  
  680. const modal = document.createElement('div');
  681. modal.className = 'notes-modal';
  682. modal.style.maxWidth = '500px';
  683.  
  684. const closeButton = document.createElement('span');
  685. closeButton.className = 'close-button';
  686. closeButton.textContent = '×';
  687. closeButton.onclick = () => overlay.remove();
  688.  
  689. let backupKeys = [];
  690. try {
  691. backupKeys = GM_listValues().filter(key => key.startsWith('notes-backup-')).sort().reverse();
  692. } catch (error) {
  693. console.warn('Could not retrieve list of backups:', error);
  694. }
  695.  
  696. if (backupKeys.length === 0) {
  697. modal.innerHTML = `
  698. <h3 class="modal-title">Restore Backup</h3>
  699. <p>No backups found. Automatic backups are ${options.autoBackup ? 'enabled' : 'disabled'} in your settings.</p>
  700. <button id="closeBackupsList" class="notes-button">Close</button>
  701. `;
  702. } else {
  703. modal.innerHTML = `
  704. <h3 class="modal-title">Available Backups</h3>
  705. <p>Select a backup to restore:</p>
  706. <div id="backupsList" style="max-height: 300px; overflow-y: auto;"></div>
  707. <button id="closeBackupsList" class="notes-button secondary" style="margin-top: 16px;">Cancel</button>
  708. `;
  709.  
  710. const backupsList = modal.querySelector('#backupsList');
  711.  
  712. backupKeys.forEach(key => {
  713. // Extract the timestamp from the key
  714. const timestampStr = key.replace('notes-backup-', '');
  715. let timestamp;
  716. let readableDate = "Unknown date";
  717.  
  718. // Handle both timestamp formats
  719. if (/^\d+$/.test(timestampStr)) {
  720. // It's a numeric timestamp
  721. timestamp = parseInt(timestampStr, 10);
  722. } else if (timestampStr.includes('T')) {
  723. // It's an ISO date format
  724. try {
  725. timestamp = new Date(timestampStr.replace(/\-/g, ':')).getTime();
  726. } catch (e) {
  727. console.error('Error parsing ISO date format:', e);
  728. }
  729. }
  730.  
  731. // Format date in a more user-friendly way
  732. if (!isNaN(timestamp) && timestamp > 0) {
  733. const date = new Date(timestamp);
  734.  
  735. // Format: "Feb 25, 2025 - 3:45 PM" (with day and time)
  736. const options = {
  737. year: 'numeric',
  738. month: 'short',
  739. day: 'numeric',
  740. hour: 'numeric',
  741. minute: '2-digit',
  742. hour12: true
  743. };
  744. readableDate = date.toLocaleDateString(undefined, options);
  745.  
  746. // Add relative time indication like "Today", "Yesterday", etc.
  747. const today = new Date();
  748. const yesterday = new Date(today);
  749. yesterday.setDate(yesterday.getDate() - 1);
  750.  
  751. if (date.toDateString() === today.toDateString()) {
  752. readableDate = `Today, ${date.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit', hour12: true})}`;
  753. } else if (date.toDateString() === yesterday.toDateString()) {
  754. readableDate = `Yesterday, ${date.toLocaleTimeString(undefined, {hour: 'numeric', minute: '2-digit', hour12: true})}`;
  755. }
  756. }
  757.  
  758. const backupItem = document.createElement('div');
  759. backupItem.className = 'notes-list-item';
  760. backupItem.innerHTML = `<span>${readableDate}</span>`;
  761. backupItem.onclick = () => confirmAndRestoreBackup(key);
  762.  
  763. backupsList.appendChild(backupItem);
  764. });
  765. }
  766.  
  767. modal.appendChild(closeButton);
  768. overlay.appendChild(modal);
  769. document.body.appendChild(overlay);
  770. document.getElementById('closeBackupsList')?.addEventListener('click', () => overlay.remove());
  771. }
  772.  
  773. function confirmAndRestoreBackup(backupKey) {
  774. if (confirm('Are you sure you want to restore this backup? This will replace all your current notes.')) {
  775. try {
  776. const backupData = GM_getValue(backupKey);
  777. if (backupData) {
  778. GM_setValue('website-notes', backupData);
  779. alert('Backup restored successfully!');
  780. location.reload(); // Reload the page to refresh notes display
  781. } else {
  782. alert('Error: Backup data is empty or corrupted.');
  783. }
  784. } catch (error) {
  785. console.error('Error restoring backup:', error);
  786. alert('Failed to restore backup. Please try again.');
  787. }
  788. }
  789. }
  790.  
  791. // Add search functionality
  792. function addSearchButton() {
  793. // Add a search button to the top of the all notes view
  794. const searchButton = document.createElement('button');
  795. searchButton.className = 'notes-button';
  796. searchButton.textContent = '🔍 Search Notes';
  797. searchButton.style.marginBottom = '16px';
  798. searchButton.onclick = showSearchModal;
  799.  
  800. // Find the appropriate container - the div after the modal title
  801. const titleElement = document.querySelector('.notes-modal .modal-title');
  802. if (titleElement && titleElement.textContent === 'All Notes') {
  803. titleElement.parentNode.insertBefore(searchButton, titleElement.nextSibling);
  804. }
  805. }
  806.  
  807. function showSearchModal() {
  808. const overlay = document.createElement('div');
  809. overlay.className = 'notes-overlay';
  810.  
  811. const modal = document.createElement('div');
  812. modal.className = 'notes-modal';
  813.  
  814. const closeButton = document.createElement('span');
  815. closeButton.className = 'close-button';
  816. closeButton.textContent = '×';
  817. closeButton.onclick = () => overlay.remove();
  818.  
  819. modal.innerHTML = `
  820. <h3 class="modal-title">Search Notes</h3>
  821. <input type="text" id="searchInput" class="notes-input" placeholder="Search by title, content, tags, or URL...">
  822. <div class="notes-options-label">
  823. <label>
  824. <input type="checkbox" class="notes-options-checkbox" id="searchTitle" checked>
  825. Search in titles
  826. </label>
  827. </div>
  828. <div class="notes-options-label">
  829. <label>
  830. <input type="checkbox" class="notes-options-checkbox" id="searchContent" checked>
  831. Search in note content
  832. </label>
  833. </div>
  834. <div class="notes-options-label">
  835. <label>
  836. <input type="checkbox" class="notes-options-checkbox" id="searchTags" checked>
  837. Search in tags
  838. </label>
  839. </div>
  840. <div class="notes-options-label">
  841. <label>
  842. <input type="checkbox" class="notes-options-checkbox" id="searchUrls">
  843. Search in URLs
  844. </label>
  845. </div>
  846. <div id="searchResults" style="margin-top: 16px; max-height: 400px; overflow-y: auto;"></div>
  847. <button id="closeSearchModal" class="notes-button secondary" style="margin-top: 16px;">Close</button>
  848. `;
  849.  
  850. modal.appendChild(closeButton);
  851. overlay.appendChild(modal);
  852. document.body.appendChild(overlay);
  853.  
  854. // Set up event listeners
  855. const searchInput = document.getElementById('searchInput');
  856. searchInput.focus();
  857.  
  858. searchInput.addEventListener('input', performSearch);
  859. document.getElementById('searchTitle').addEventListener('change', performSearch);
  860. document.getElementById('searchContent').addEventListener('change', performSearch);
  861. document.getElementById('searchTags').addEventListener('change', performSearch);
  862. document.getElementById('searchUrls').addEventListener('change', performSearch);
  863. document.getElementById('closeSearchModal').addEventListener('click', () => overlay.remove());
  864.  
  865. // Perform search when input changes
  866. function performSearch() {
  867. const query = searchInput.value.toLowerCase().trim();
  868. const searchTitle = document.getElementById('searchTitle').checked;
  869. const searchContent = document.getElementById('searchContent').checked;
  870. const searchTags = document.getElementById('searchTags').checked;
  871. const searchUrls = document.getElementById('searchUrls').checked;
  872.  
  873. const searchResults = document.getElementById('searchResults');
  874. searchResults.innerHTML = '';
  875.  
  876. if (!query) {
  877. searchResults.innerHTML = '<p style="color: #6b7280;">Enter a search term to find notes</p>';
  878. return;
  879. }
  880.  
  881. const notes = getAllNotes();
  882. let resultCount = 0;
  883.  
  884. // Function to highlight matching text
  885. function highlightMatch(text, query) {
  886. if (!text) return '';
  887. const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')})`, 'gi');
  888. return text.replace(regex, '<mark style="background-color: #fde68a; color: #1f2937;">$1</mark>');
  889. }
  890.  
  891. // Search through all notes
  892. for (const url in notes) {
  893. if (searchUrls && url.toLowerCase().includes(query)) {
  894. // The URL itself matches
  895. const urlDiv = document.createElement('div');
  896. urlDiv.innerHTML = `<div class="url-text">${highlightMatch(url, query)}</div>`;
  897.  
  898. // Add all notes under this URL
  899. notes[url].forEach((note, index) => {
  900. addNoteResult(urlDiv, note, url, index);
  901. });
  902.  
  903. searchResults.appendChild(urlDiv);
  904. resultCount += notes[url].length;
  905. continue;
  906. }
  907.  
  908. // Check if any notes match the search criteria
  909. const matchingNotes = notes[url].filter(note => {
  910. if (searchTitle && note.title.toLowerCase().includes(query)) return true;
  911. if (searchContent && note.content.toLowerCase().includes(query)) return true;
  912. if (searchTags && note.tags && note.tags.some(tag => tag.toLowerCase().includes(query))) return true;
  913. return false;
  914. });
  915.  
  916. if (matchingNotes.length > 0) {
  917. const urlDiv = document.createElement('div');
  918. urlDiv.innerHTML = `<div class="url-text">${url}</div>`;
  919.  
  920. matchingNotes.forEach(note => {
  921. const index = notes[url].indexOf(note);
  922. addNoteResult(urlDiv, note, url, index, query);
  923. });
  924.  
  925. searchResults.appendChild(urlDiv);
  926. resultCount += matchingNotes.length;
  927. }
  928. }
  929.  
  930. if (resultCount === 0) {
  931. searchResults.innerHTML = '<p style="color: #6b7280;">No matching notes found</p>';
  932. } else {
  933. searchResults.insertAdjacentHTML('afterbegin', `<p style="color: #6b7280;">${resultCount} note${resultCount !== 1 ? 's' : ''} found</p>`);
  934. }
  935.  
  936. // Helper function to add a note result to the results div
  937. function addNoteResult(container, note, url, index, query) {
  938. const noteDiv = document.createElement('div');
  939. noteDiv.className = 'notes-list-item';
  940.  
  941. // Apply note color if available
  942. if (note.color) {
  943. noteDiv.style.borderLeft = `4px solid ${note.color}`;
  944. noteDiv.style.paddingLeft = '12px';
  945. }
  946.  
  947. // Create content with highlighted matches
  948. let titleHtml = note.title;
  949. let contentPreview = '';
  950.  
  951. if (query) {
  952. // Highlight matches
  953. if (searchTitle) {
  954. titleHtml = highlightMatch(note.title, query);
  955. }
  956.  
  957. if (searchContent && note.content.toLowerCase().includes(query)) {
  958. // Find the context around the match
  959. const matchIndex = note.content.toLowerCase().indexOf(query);
  960. const startIndex = Math.max(0, matchIndex - 50);
  961. const endIndex = Math.min(note.content.length, matchIndex + query.length + 50);
  962.  
  963. // Add ellipsis if we're not starting from the beginning
  964. let preview = (startIndex > 0 ? '...' : '') +
  965. note.content.substring(startIndex, endIndex) +
  966. (endIndex < note.content.length ? '...' : '');
  967.  
  968. contentPreview = `<div style="margin-top: 4px; font-size: 14px; color: ${isDarkMode ? '#9ca3af' : '#6b7280'};">
  969. ${highlightMatch(preview, query)}
  970. </div>`;
  971. }
  972. }
  973.  
  974. // Add tags if available with highlighting
  975. let tagsHTML = '';
  976. if (note.tags && note.tags.length > 0) {
  977. tagsHTML = '<div style="margin-top: 4px;">';
  978. note.tags.forEach(tag => {
  979. if (searchTags && query && tag.toLowerCase().includes(query)) {
  980. tagsHTML += `<span class="notes-tag">${highlightMatch(tag, query)}</span>`;
  981. } else {
  982. tagsHTML += `<span class="notes-tag">${tag}</span>`;
  983. }
  984. });
  985. tagsHTML += '</div>';
  986. }
  987.  
  988. noteDiv.innerHTML = `
  989. <div style="flex-grow: 1;">
  990. <div style="font-weight: 500;">${titleHtml}</div>
  991. ${contentPreview}
  992. ${tagsHTML}
  993. </div>
  994. `;
  995.  
  996. noteDiv.onclick = () => {
  997. document.querySelector('.notes-overlay').remove();
  998. showNoteContent(note, url, index);
  999. };
  1000.  
  1001. container.appendChild(noteDiv);
  1002. }
  1003. }
  1004. }
  1005.  
  1006.  
  1007. GM_registerMenuCommand('Toggle Dark Mode', () => {
  1008. const newMode = !isDarkMode;
  1009. GM_setValue('darkMode', newMode);
  1010. location.reload();
  1011. });
  1012.  
  1013. function createModal(content) {
  1014. const overlay = document.createElement('div');
  1015. overlay.className = 'notes-overlay';
  1016.  
  1017. const modal = document.createElement('div');
  1018. modal.className = 'notes-modal';
  1019.  
  1020. const closeButton = document.createElement('span');
  1021. closeButton.className = 'close-button';
  1022. closeButton.textContent = '×';
  1023. closeButton.onclick = () => overlay.remove();
  1024.  
  1025. modal.appendChild(closeButton);
  1026. modal.appendChild(content);
  1027. overlay.appendChild(modal);
  1028. document.body.appendChild(overlay);
  1029.  
  1030. const escapeListener = (e) => {
  1031. if (e.key === 'Escape') {
  1032. overlay.remove();
  1033. document.removeEventListener('keydown', escapeListener);
  1034. }
  1035. };
  1036. document.addEventListener('keydown', escapeListener);
  1037. }
  1038.  
  1039. function getAllNotes() {
  1040. return GM_getValue('website-notes', {});
  1041. }
  1042.  
  1043. function saveNote(title, url, content, timestamp = Date.now(), pinned = false, tags = [], color = null) {
  1044. const notes = getAllNotes();
  1045. if (!notes[url]) notes[url] = [];
  1046.  
  1047. // Add timestamp to title if the option is enabled
  1048. let finalTitle = title;
  1049. if (options.addTimestampToTitle) {
  1050. const date = new Date(timestamp);
  1051. const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
  1052. finalTitle = `${title} [${formattedDate}]`;
  1053. }
  1054.  
  1055. notes[url].push({
  1056. title: finalTitle,
  1057. content,
  1058. timestamp,
  1059. pinned,
  1060. tags,
  1061. color
  1062. });
  1063.  
  1064. GM_setValue('website-notes', notes);
  1065.  
  1066. // Perform auto-backup if enabled
  1067. if (options.autoBackup) {
  1068. performAutoBackup();
  1069. }
  1070. }
  1071.  
  1072. function performAutoBackup() {
  1073. try {
  1074. const notes = getAllNotes();
  1075. const dateInfo = getFormattedBackupDate();
  1076. // Use consistent format with numeric timestamp
  1077. const backupKey = `notes-backup-${dateInfo.timestamp}`;
  1078.  
  1079. // Create the new backup
  1080. GM_setValue(backupKey, notes);
  1081. console.log(`Auto-backup created successfully: ${dateInfo.formatted}`);
  1082.  
  1083. // Now try to manage old backups
  1084. try {
  1085. // Try to get all backup keys
  1086. const allBackupKeys = GM_listValues().filter(key => key.startsWith('notes-backup-')).sort();
  1087.  
  1088. // Keep only the last 5 backups
  1089. if (allBackupKeys.length > 5) {
  1090. // Delete oldest backups, keeping the 5 most recent
  1091. for (let i = 0; i < allBackupKeys.length - 5; i++) {
  1092. try {
  1093. GM_deleteValue(allBackupKeys[i]);
  1094. console.log(`Deleted old backup: ${allBackupKeys[i]}`);
  1095. } catch (deleteError) {
  1096. alert(`Could not delete backup ${allBackupKeys[i]}:`, deleteError);
  1097. }
  1098. }
  1099. }
  1100. } catch (listError) {
  1101. console.warn('Could not retrieve list of backups to manage old backups:', listError);
  1102. alert('Could not retrieve list of backups to manage old backups:', listError);
  1103.  
  1104. // Alternative approach: Store the list of backup keys ourselves
  1105. let storedBackupKeys = GM_getValue('backup-key-list', []);
  1106.  
  1107. // Add the new backup key to our list
  1108. storedBackupKeys.push(backupKey);
  1109.  
  1110. // Only keep the most recent 5 backups
  1111. if (storedBackupKeys.length > 5) {
  1112. // Get keys to delete (all except the 5 most recent)
  1113. const keysToDelete = storedBackupKeys.slice(0, storedBackupKeys.length - 5);
  1114.  
  1115. // Delete old backups
  1116. keysToDelete.forEach(keyToDelete => {
  1117. try {
  1118. GM_deleteValue(keyToDelete);
  1119. console.log(`Deleted old backup (using fallback method): ${keyToDelete}`);
  1120. } catch (deleteError) {
  1121. console.warn(`Could not delete backup ${keyToDelete}:`, deleteError);
  1122. }
  1123. });
  1124.  
  1125. // Update our stored list to contain only the 5 most recent keys
  1126. storedBackupKeys = storedBackupKeys.slice(storedBackupKeys.length - 5);
  1127. }
  1128.  
  1129. // Save the updated list of backup keys
  1130. GM_setValue('backup-key-list', storedBackupKeys);
  1131. }
  1132. } catch (error) {
  1133. console.error('Error during auto-backup:', error);
  1134. }
  1135. }
  1136.  
  1137. function getFormattedBackupDate() {
  1138. const now = new Date();
  1139.  
  1140. // Format: YYYY-MM-DD_HH-MM-SS (e.g., 2025-02-25_14-30-45)
  1141. const year = now.getFullYear();
  1142. const month = String(now.getMonth() + 1).padStart(2, '0');
  1143. const day = String(now.getDate()).padStart(2, '0');
  1144. const hours = String(now.getHours()).padStart(2, '0');
  1145. const minutes = String(now.getMinutes()).padStart(2, '0');
  1146. const seconds = String(now.getSeconds()).padStart(2, '0');
  1147.  
  1148. const dateString = `${year}-${month}-${day}_${hours}-${minutes}-${seconds}`;
  1149.  
  1150. return {
  1151. timestamp: now.getTime(), // Use numeric timestamp
  1152. formatted: dateString
  1153. };
  1154. }
  1155.  
  1156. function updateNote(oldUrl, index, title, newUrl, content, pinned, tags = [], color = null) {
  1157. const notes = getAllNotes();
  1158. const existingNote = notes[oldUrl][index];
  1159.  
  1160. // Delete the old note
  1161. deleteNote(oldUrl, index);
  1162.  
  1163. // Save with updated values but keep the original timestamp
  1164. saveNote(
  1165. title,
  1166. newUrl,
  1167. content,
  1168. existingNote.timestamp,
  1169. pinned,
  1170. tags,
  1171. color
  1172. );
  1173. }
  1174.  
  1175. function togglePinNote(url, index) {
  1176. const notes = getAllNotes();
  1177. if (notes[url] && notes[url][index]) {
  1178. notes[url][index].pinned = !notes[url][index].pinned;
  1179. GM_setValue('website-notes', notes);
  1180. }
  1181. }
  1182.  
  1183. function deleteNote(url, index) {
  1184. const notes = getAllNotes();
  1185. if (notes[url]) {
  1186. notes[url].splice(index, 1);
  1187. if (notes[url].length === 0) delete notes[url];
  1188. GM_setValue('website-notes', notes);
  1189. }
  1190. }
  1191.  
  1192. function showNoteForm(editMode = false, existingNote = null, url = null, index = null) {
  1193. const container = document.createElement('div');
  1194. container.innerHTML = `<h3 class="modal-title">${editMode ? 'Edit Note' : 'Create New Note'}</h3>`;
  1195.  
  1196. const titleInput = document.createElement('input');
  1197. titleInput.className = 'notes-input';
  1198. titleInput.placeholder = 'Enter title';
  1199. titleInput.value = editMode ? existingNote.title : '';
  1200.  
  1201. const urlInput = document.createElement('input');
  1202. urlInput.className = 'notes-input';
  1203. urlInput.placeholder = 'Enter URL(s) or URL pattern(s), separated by spaces (e.g., https://domain.com/*)';
  1204. urlInput.value = editMode ? url : window.location.href;
  1205.  
  1206. const patternHelp = document.createElement('div');
  1207. patternHelp.style.fontSize = '12px';
  1208. patternHelp.style.color = isDarkMode ? '#9ca3af' : '#6b7280';
  1209. patternHelp.style.marginTop = '-8px';
  1210. patternHelp.style.marginBottom = '8px';
  1211. patternHelp.innerHTML = 'Use * for wildcard matching. Multiple URLs: separate with spaces (e.g., https://domain1.com/* https://domain2.com/*)';
  1212.  
  1213. // Add tags input
  1214. const tagsInput = document.createElement('input');
  1215. tagsInput.className = 'notes-input';
  1216. tagsInput.placeholder = 'Tags (comma separated)';
  1217. tagsInput.value = editMode && existingNote.tags ? existingNote.tags.join(', ') : '';
  1218.  
  1219. const tagsHelp = document.createElement('div');
  1220. tagsHelp.style.fontSize = '12px';
  1221. tagsHelp.style.color = isDarkMode ? '#9ca3af' : '#6b7280';
  1222. tagsHelp.style.marginTop = '-8px';
  1223. tagsHelp.style.marginBottom = '8px';
  1224. tagsHelp.innerHTML = 'Add tags to organize notes (e.g., work, personal, important)';
  1225.  
  1226. // Add color picker
  1227. const colorPicker = createColorPicker(editMode && existingNote.color ? existingNote.color : '#3b82f6');
  1228. const colorPickerLabel = document.createElement('div');
  1229. colorPickerLabel.style.fontSize = '14px';
  1230. colorPickerLabel.style.marginBottom = '8px';
  1231. colorPickerLabel.innerHTML = 'Note Color:';
  1232.  
  1233. const contentArea = document.createElement('textarea');
  1234. contentArea.className = 'notes-textarea';
  1235. contentArea.placeholder = 'Enter your notes here';
  1236. contentArea.value = editMode ? existingNote.content : '';
  1237.  
  1238. // Add formatting toolbar
  1239. const toolbar = enhanceTextEditor(contentArea);
  1240.  
  1241. const buttonGroup = document.createElement('div');
  1242. buttonGroup.className = 'button-group';
  1243. buttonGroup.style.display = 'flex';
  1244. buttonGroup.style.justifyContent = 'space-between';
  1245. buttonGroup.style.marginTop = '16px';
  1246.  
  1247. const saveButton = document.createElement('button');
  1248. saveButton.className = 'notes-button';
  1249. saveButton.textContent = editMode ? 'Update Note' : 'Save Note';
  1250. saveButton.onclick = () => {
  1251. if (titleInput.value && contentArea.value) {
  1252. const tags = tagsInput.value.split(',').map(tag => tag.trim()).filter(tag => tag);
  1253. const color = colorPicker.dataset.selectedColor;
  1254.  
  1255. if (editMode) {
  1256. updateNote(url, index, titleInput.value, urlInput.value, contentArea.value,
  1257. existingNote.pinned, tags, color);
  1258. } else {
  1259. saveNote(titleInput.value, urlInput.value, contentArea.value, Date.now(), false, tags, color);
  1260. }
  1261. container.parentElement.parentElement.remove();
  1262. showCurrentPageNotes();
  1263. } else {
  1264. alert('Title and content are required!');
  1265. }
  1266. };
  1267.  
  1268. const cancelButton = document.createElement('button');
  1269. cancelButton.className = 'notes-button secondary';
  1270. cancelButton.textContent = 'Cancel';
  1271. cancelButton.onclick = () => container.parentElement.parentElement.remove();
  1272.  
  1273. buttonGroup.appendChild(saveButton);
  1274. buttonGroup.appendChild(cancelButton);
  1275.  
  1276. container.appendChild(titleInput);
  1277. container.appendChild(urlInput);
  1278. container.appendChild(patternHelp);
  1279. container.appendChild(tagsInput);
  1280. container.appendChild(tagsHelp);
  1281. container.appendChild(colorPickerLabel);
  1282. container.appendChild(colorPicker);
  1283. container.appendChild(toolbar);
  1284. container.appendChild(contentArea);
  1285. container.appendChild(buttonGroup);
  1286.  
  1287. createModal(container);
  1288. }
  1289.  
  1290. function createColorPicker(selectedColor = '#3b82f6') {
  1291. const colorOptions = ['#3b82f6', '#ef4444', '#10b981', '#f59e0b', '#8b5cf6', '#ec4899'];
  1292.  
  1293. const container = document.createElement('div');
  1294. container.style.display = 'flex';
  1295. container.style.gap = '8px';
  1296. container.style.margin = '8px 0';
  1297. container.style.flexWrap = 'wrap';
  1298.  
  1299. colorOptions.forEach(color => {
  1300. const option = document.createElement('div');
  1301. option.style.width = '24px';
  1302. option.style.height = '24px';
  1303. option.style.borderRadius = '50%';
  1304. option.style.backgroundColor = color;
  1305. option.style.cursor = 'pointer';
  1306. option.style.border = color === selectedColor ? '2px solid white' : '2px solid transparent';
  1307. option.style.boxShadow = '0 0 0 1px rgba(0,0,0,0.1)';
  1308.  
  1309. option.onclick = () => {
  1310. container.querySelectorAll('div').forEach(div => {
  1311. div.style.border = '2px solid transparent';
  1312. });
  1313. option.style.border = '2px solid white';
  1314. container.dataset.selectedColor = color;
  1315. };
  1316.  
  1317. container.appendChild(option);
  1318. });
  1319.  
  1320. container.dataset.selectedColor = selectedColor;
  1321. return container;
  1322. }
  1323.  
  1324. function applyNoteColor(noteElement, color) {
  1325. if (!color) return;
  1326.  
  1327. // Apply color as a left border
  1328. noteElement.style.borderLeft = `4px solid ${color}`;
  1329. // Add subtle background tint
  1330. const colorOpacity = isDarkMode ? '0.1' : '0.05';
  1331. noteElement.style.backgroundColor = `${color}${colorOpacity}`;
  1332. }
  1333.  
  1334. function enhanceTextEditor(textArea) {
  1335. const toolbar = document.createElement('div');
  1336. toolbar.className = 'notes-editor-toolbar';
  1337.  
  1338. const addButton = (text, title, action) => {
  1339. const btn = document.createElement('button');
  1340. btn.textContent = text;
  1341. btn.title = title;
  1342. btn.className = 'notes-button secondary';
  1343. btn.style.padding = '4px 8px';
  1344. btn.style.fontSize = '12px';
  1345. btn.onclick = (e) => {
  1346. e.preventDefault();
  1347. action(textArea);
  1348. textArea.focus(); // Keep focus on the textarea after button click
  1349. };
  1350. return btn;
  1351. };
  1352.  
  1353. // Add formatting buttons with icons or text
  1354. toolbar.appendChild(addButton('B', 'Bold (Ctrl+B)', ta => {
  1355. // If text is selected, wrap it in bold marks
  1356. // Otherwise, just insert the marks and place cursor between them
  1357. insertAround(ta, '**', '**');
  1358. }));
  1359.  
  1360. toolbar.appendChild(addButton('I', 'Italic (Ctrl+I)', ta => {
  1361. insertAround(ta, '_', '_');
  1362. }));
  1363.  
  1364. toolbar.appendChild(addButton('Link', 'Insert Link', ta => {
  1365. const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd);
  1366. if (selection) {
  1367. insertAround(ta, '[', '](https://)');
  1368. // Position cursor after the opening bracket of the URL
  1369. ta.selectionStart = ta.selectionEnd - 9;
  1370. ta.selectionEnd = ta.selectionEnd - 1;
  1371. } else {
  1372. insertAtCursor(ta, '[Link text](https://)');
  1373. // Select "Link text" for easy replacement
  1374. const cursorPos = ta.value.lastIndexOf('[Link text]');
  1375. ta.selectionStart = cursorPos + 1;
  1376. ta.selectionEnd = cursorPos + 10;
  1377. }
  1378. }));
  1379.  
  1380. toolbar.appendChild(addButton('List', 'Insert List', ta => {
  1381. insertAtCursor(ta, '\n- Item 1\n- Item 2\n- Item 3\n');
  1382. }));
  1383.  
  1384. toolbar.appendChild(addButton('H1', 'Heading 1', ta => {
  1385. const start = ta.selectionStart;
  1386. const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
  1387. const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd);
  1388.  
  1389. // Check if the line already starts with # to avoid duplicating
  1390. const currentLine = ta.value.substring(lineStart, start);
  1391. if (currentLine.trim().startsWith('# ')) {
  1392. return; // Already has heading format
  1393. }
  1394.  
  1395. if (selection) {
  1396. // Selected text becomes heading
  1397. ta.value = ta.value.substring(0, ta.selectionStart) +
  1398. '# ' + selection +
  1399. ta.value.substring(ta.selectionEnd);
  1400. ta.selectionStart = ta.selectionStart + 2;
  1401. ta.selectionEnd = ta.selectionStart + selection.length;
  1402. } else {
  1403. // Insert at current line start
  1404. ta.value = ta.value.substring(0, lineStart) +
  1405. '# Heading' +
  1406. ta.value.substring(lineStart);
  1407. ta.selectionStart = lineStart + 2;
  1408. ta.selectionEnd = lineStart + 9;
  1409. }
  1410. }));
  1411.  
  1412. toolbar.appendChild(addButton('H2', 'Heading 2', ta => {
  1413. const start = ta.selectionStart;
  1414. const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
  1415. const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd);
  1416.  
  1417. // Check if the line already starts with ## to avoid duplicating
  1418. const currentLine = ta.value.substring(lineStart, start);
  1419. if (currentLine.trim().startsWith('## ')) {
  1420. return; // Already has heading format
  1421. }
  1422.  
  1423. if (selection) {
  1424. // Selected text becomes heading
  1425. ta.value = ta.value.substring(0, ta.selectionStart) +
  1426. '## ' + selection +
  1427. ta.value.substring(ta.selectionEnd);
  1428. ta.selectionStart = ta.selectionStart + 3;
  1429. ta.selectionEnd = ta.selectionStart + selection.length;
  1430. } else {
  1431. // Insert at current line start
  1432. ta.value = ta.value.substring(0, lineStart) +
  1433. '## Subheading' +
  1434. ta.value.substring(lineStart);
  1435. ta.selectionStart = lineStart + 3;
  1436. ta.selectionEnd = lineStart + 13;
  1437. }
  1438. }));
  1439.  
  1440. toolbar.appendChild(addButton('Quote', 'Blockquote', ta => {
  1441. const start = ta.selectionStart;
  1442. const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
  1443. const selection = ta.value.substring(ta.selectionStart, ta.selectionEnd);
  1444.  
  1445. if (selection) {
  1446. // Add quote prefix to all selected lines
  1447. const lines = selection.split('\n');
  1448. const quotedText = lines.map(line => `> ${line}`).join('\n');
  1449.  
  1450. ta.value = ta.value.substring(0, ta.selectionStart) +
  1451. quotedText +
  1452. ta.value.substring(ta.selectionEnd);
  1453.  
  1454. ta.selectionStart = ta.selectionStart;
  1455. ta.selectionEnd = ta.selectionStart + quotedText.length;
  1456. } else {
  1457. // Insert at current line start
  1458. ta.value = ta.value.substring(0, lineStart) +
  1459. '> ' + ta.value.substring(lineStart);
  1460. ta.selectionStart = lineStart + 2;
  1461. ta.selectionEnd = lineStart + 2;
  1462. }
  1463. }));
  1464.  
  1465. // Add keyboard event listeners for common shortcuts
  1466. textArea.addEventListener('keydown', (e) => {
  1467. // Ctrl+B for bold
  1468. if (e.ctrlKey && e.key === 'b') {
  1469. e.preventDefault();
  1470. insertAround(textArea, '**', '**');
  1471. }
  1472. // Ctrl+I for italic
  1473. if (e.ctrlKey && e.key === 'i') {
  1474. e.preventDefault();
  1475. insertAround(textArea, '_', '_');
  1476. }
  1477. // Tab key handling for indentation
  1478. if (e.key === 'Tab') {
  1479. e.preventDefault();
  1480. insertAtCursor(textArea, ' ');
  1481. }
  1482. });
  1483.  
  1484. return toolbar;
  1485. }
  1486.  
  1487. function insertAround(textArea, before, after) {
  1488. const start = textArea.selectionStart;
  1489. const end = textArea.selectionEnd;
  1490. const text = textArea.value;
  1491. const selected = text.substring(start, end);
  1492.  
  1493. textArea.value = text.substring(0, start) + before + selected + after + text.substring(end);
  1494. textArea.focus();
  1495. textArea.setSelectionRange(start + before.length, start + before.length + selected.length);
  1496. }
  1497.  
  1498. function insertAtCursor(textArea, text) {
  1499. const start = textArea.selectionStart;
  1500. textArea.value = textArea.value.substring(0, start) + text + textArea.value.substring(start);
  1501. textArea.focus();
  1502. textArea.setSelectionRange(start + text.length, start + text.length);
  1503. }
  1504.  
  1505. function formatDate(timestamp) {
  1506. return new Date(timestamp).toLocaleString();
  1507. }
  1508.  
  1509. function showNoteContent(note, url, index) {
  1510. const container = document.createElement('div');
  1511.  
  1512. // Function to convert URLs to clickable links and preserve line breaks
  1513. function linkify(text) {
  1514. // URL pattern for matching
  1515. const urlPattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
  1516.  
  1517. // Replace URLs with anchor tags
  1518. const linkedText = text.replace(urlPattern, function(url) {
  1519. return `<a href="${url}" target="_blank" style="color: #3b82f6; text-decoration: underline; word-break: break-all;" onclick="event.stopPropagation();">${url}</a>`;
  1520. });
  1521.  
  1522. // Process markdown formatting
  1523. let formattedText = linkedText
  1524. // Bold
  1525. .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
  1526. // Italic
  1527. .replace(/_(.*?)_/g, '<em>$1</em>')
  1528. // Headers
  1529. .replace(/^# (.*?)$/gm, '<h1 style="font-size: 1.5em; margin-top: 0.8em; margin-bottom: 0.5em;">$1</h1>')
  1530. .replace(/^## (.*?)$/gm, '<h2 style="font-size: 1.3em; margin-top: 0.7em; margin-bottom: 0.4em;">$1</h2>')
  1531. // Lists
  1532. .replace(/^- (.*?)$/gm, '• $1<br>')
  1533. // Blockquotes
  1534. .replace(/^> (.*?)$/gm, '<blockquote style="border-left: 3px solid #9ca3af; padding-left: 10px; margin-left: 5px; color: #6b7280;">$1</blockquote>');
  1535.  
  1536. // Convert line breaks to <br> tags and maintain whitespace
  1537. return formattedText.replace(/\n/g, '<br>').replace(/\s{2,}/g, function(space) {
  1538. return ' ' + '&nbsp;'.repeat(space.length - 1);
  1539. });
  1540. }
  1541.  
  1542. // Create a hidden textarea for proper copying
  1543. const hiddenTextarea = document.createElement('textarea');
  1544. hiddenTextarea.style.position = 'absolute';
  1545. hiddenTextarea.style.left = '-9999px';
  1546. hiddenTextarea.style.top = '-9999px';
  1547. document.body.appendChild(hiddenTextarea);
  1548.  
  1549. // Create note header with title and color
  1550. const noteHeader = document.createElement('div');
  1551. noteHeader.className = 'note-header';
  1552. noteHeader.style.display = 'flex';
  1553. noteHeader.style.alignItems = 'center';
  1554. noteHeader.style.marginBottom = '16px';
  1555.  
  1556. // Create color indicator
  1557. const colorIndicator = document.createElement('div');
  1558. colorIndicator.style.width = '16px';
  1559. colorIndicator.style.height = '16px';
  1560. colorIndicator.style.borderRadius = '50%';
  1561. colorIndicator.style.marginRight = '8px';
  1562. colorIndicator.style.backgroundColor = note.color || '#3b82f6';
  1563.  
  1564. // Create the actual content container
  1565. const contentContainer = document.createElement('div');
  1566. contentContainer.className = 'note-content-container';
  1567. contentContainer.style.padding = '16px';
  1568. contentContainer.style.borderRadius = '8px';
  1569. contentContainer.style.marginBottom = '16px';
  1570.  
  1571. // Apply the note color
  1572. if (note.color) {
  1573. contentContainer.style.borderLeft = `4px solid ${note.color}`;
  1574. contentContainer.style.backgroundColor = `${note.color}${isDarkMode ? '15' : '10'}`;
  1575. } else {
  1576. contentContainer.style.backgroundColor = isDarkMode ? '#2a3441' : '#f9fafb';
  1577. }
  1578.  
  1579. // Add tags display if the note has tags
  1580. let tagsHTML = '';
  1581. if (note.tags && note.tags.length > 0) {
  1582. tagsHTML = '<div style="margin-top: 8px; margin-bottom: 8px;">';
  1583. note.tags.forEach(tag => {
  1584. tagsHTML += `<span class="notes-tag">${tag}</span>`;
  1585. });
  1586. tagsHTML += '</div>';
  1587. }
  1588.  
  1589. container.innerHTML = `
  1590. <h3 class="modal-title">${note.title}</h3>
  1591. <div class="url-text">${url}</div>
  1592. <div class="timestamp">Created: ${formatDate(note.timestamp)}</div>
  1593. ${tagsHTML}
  1594. `;
  1595.  
  1596. // Add content to the content container
  1597. contentContainer.innerHTML = linkify(note.content);
  1598. container.appendChild(contentContainer);
  1599.  
  1600. // Add copy event listener to the content div
  1601. contentContainer.addEventListener('copy', (e) => {
  1602. e.preventDefault();
  1603. const selection = window.getSelection();
  1604. const selectedText = selection.toString();
  1605.  
  1606. // Replace <br> tags with actual newlines in the copied text
  1607. hiddenTextarea.value = selectedText.replace(/\s*\n\s*/g, '\n');
  1608. hiddenTextarea.select();
  1609. document.execCommand('copy');
  1610.  
  1611. // Clean up
  1612. selection.removeAllRanges();
  1613. selection.addRange(document.createRange());
  1614. });
  1615.  
  1616. const buttonGroup = document.createElement('div');
  1617. buttonGroup.className = 'button-group';
  1618.  
  1619. const editButton = document.createElement('button');
  1620. editButton.className = 'notes-button edit';
  1621. editButton.textContent = 'Edit';
  1622. editButton.onclick = () => {
  1623. container.parentElement.parentElement.remove();
  1624. showNoteForm(true, note, url, index);
  1625. };
  1626.  
  1627. const deleteButton = document.createElement('button');
  1628. deleteButton.className = 'notes-button delete';
  1629. deleteButton.textContent = 'Delete';
  1630. deleteButton.onclick = () => {
  1631. if (confirm('Are you sure you want to delete this note?')) {
  1632. deleteNote(url, index);
  1633. container.parentElement.parentElement.remove();
  1634. showCurrentPageNotes();
  1635. }
  1636. };
  1637.  
  1638. const pinButton = document.createElement('button');
  1639. pinButton.className = `notes-button ${note.pinned ? 'secondary' : ''}`;
  1640. pinButton.textContent = note.pinned ? 'Unpin' : 'Pin';
  1641. pinButton.onclick = () => {
  1642. togglePinNote(url, index);
  1643. // Get the updated notes data after toggling pin status
  1644. const notes = getAllNotes();
  1645. // Update the button text and class based on the updated pin status
  1646. const isPinned = notes[url] && notes[url][index] ? notes[url][index].pinned : false;
  1647. pinButton.textContent = isPinned ? 'Unpin' : 'Pin';
  1648. pinButton.className = `notes-button ${isPinned ? '' : 'secondary'}`;
  1649. // Update the pinned notes display
  1650. displayPinnedNotes();
  1651. };
  1652.  
  1653. buttonGroup.appendChild(editButton);
  1654. buttonGroup.appendChild(deleteButton);
  1655. buttonGroup.appendChild(pinButton);
  1656. container.appendChild(buttonGroup);
  1657.  
  1658. createModal(container);
  1659. }
  1660.  
  1661. function displayPinnedNotes() {
  1662. const notes = getAllNotes();
  1663. const currentUrl = window.location.href;
  1664. let pinnedNotesContainer = document.getElementById('pinned-notes-container');
  1665.  
  1666. if (!pinnedNotesContainer) {
  1667. pinnedNotesContainer = document.createElement('div');
  1668. pinnedNotesContainer.id = 'pinned-notes-container';
  1669. pinnedNotesContainer.style.position = 'fixed';
  1670. pinnedNotesContainer.style.top = '10px';
  1671. pinnedNotesContainer.style.right = '10px';
  1672. pinnedNotesContainer.style.zIndex = '9999';
  1673. pinnedNotesContainer.style.maxWidth = '300px';
  1674. document.body.appendChild(pinnedNotesContainer);
  1675. }
  1676.  
  1677. pinnedNotesContainer.innerHTML = '';
  1678.  
  1679. for (const url in notes) {
  1680. if (doesUrlMatchPattern(url, currentUrl)) {
  1681. notes[url].forEach((note, index) => {
  1682. if (note.pinned) {
  1683. const noteDiv = document.createElement('div');
  1684. noteDiv.className = 'pinned-note';
  1685. noteDiv.style.background = currentTheme.listItem.bg;
  1686. noteDiv.style.color = currentTheme.listItem.text;
  1687. noteDiv.style.padding = '10px';
  1688. noteDiv.style.margin = '5px 0';
  1689. noteDiv.style.borderRadius = '8px';
  1690. noteDiv.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
  1691. noteDiv.style.cursor = 'pointer';
  1692.  
  1693. // Apply note color if available
  1694. if (note.color) {
  1695. noteDiv.style.borderLeft = `4px solid ${note.color}`;
  1696. noteDiv.style.paddingLeft = '12px';
  1697. }
  1698.  
  1699. noteDiv.innerHTML = `<strong>${note.title}</strong>`;
  1700. noteDiv.onclick = () => showNoteContent(note, url, index);
  1701. pinnedNotesContainer.appendChild(noteDiv);
  1702. }
  1703. });
  1704. }
  1705. }
  1706. }
  1707.  
  1708.  
  1709. function doesUrlMatchPattern(urlPatterns, currentUrl) {
  1710. // Split the pattern string into an array of patterns
  1711. const patterns = urlPatterns.split(/\s+/).filter(pattern => pattern.trim() !== '');
  1712.  
  1713. // Check if any of the patterns match the current URL
  1714. return patterns.some(pattern => {
  1715. // Escape special characters for regex
  1716. const escapeRegex = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1717.  
  1718. // Convert Tampermonkey-style pattern to regex
  1719. const patternToRegex = (pattern) => {
  1720. const parts = pattern.split('*');
  1721. let regexString = '^';
  1722. for (let i = 0; i < parts.length; i++) {
  1723. regexString += escapeRegex(parts[i]);
  1724. if (i < parts.length - 1) {
  1725. if (parts[i + 1] === '') {
  1726. // '**' matches any number of path segments
  1727. regexString += '.*';
  1728. i++; // Skip the next '*'
  1729. } else {
  1730. // Single '*' matches anything except '/'
  1731. regexString += '[^/]*';
  1732. }
  1733. }
  1734. }
  1735. // If the pattern ends with '**', allow anything at the end
  1736. if (pattern.endsWith('**')) {
  1737. regexString += '.*';
  1738. } else if (!pattern.endsWith('*')) {
  1739. regexString += '$';
  1740. }
  1741. return new RegExp(regexString);
  1742. };
  1743.  
  1744. try {
  1745. const regex = patternToRegex(pattern);
  1746. return regex.test(currentUrl);
  1747. } catch (e) {
  1748. console.error('Invalid URL pattern:', e);
  1749. return false;
  1750. }
  1751. });
  1752. }
  1753.  
  1754. function showCurrentPageNotes() {
  1755. const notes = getAllNotes();
  1756. const currentUrl = window.location.href;
  1757. let matchingNotes = [];
  1758.  
  1759. // Collect all matching notes
  1760. for (const urlPattern in notes) {
  1761. if (doesUrlMatchPattern(urlPattern, currentUrl)) {
  1762. matchingNotes.push({
  1763. pattern: urlPattern,
  1764. notes: notes[urlPattern]
  1765. });
  1766. }
  1767. }
  1768.  
  1769. const container = document.createElement('div');
  1770. container.innerHTML = `
  1771. <h3 class="modal-title">Notes for Current Page</h3>
  1772. <div class="url-text">${currentUrl}</div>
  1773. `;
  1774.  
  1775. if (matchingNotes.length === 0) {
  1776. container.innerHTML += '<p style="color: #6b7280;">No matching notes found for this page</p>';
  1777. } else {
  1778. matchingNotes.forEach(({pattern, notes: patternNotes}) => {
  1779. const patternDiv = document.createElement('div');
  1780.  
  1781. if (options.showUrlLinksInNotesList) {
  1782. patternDiv.innerHTML = `<div class="url-text">Pattern: ${pattern}</div>`;
  1783. }
  1784. patternNotes.forEach((note, index) => {
  1785. const noteDiv = document.createElement('div');
  1786. noteDiv.className = 'notes-list-item';
  1787.  
  1788. // Apply note color if available
  1789. if (note.color) {
  1790. noteDiv.style.borderLeft = `4px solid ${note.color}`;
  1791. noteDiv.style.paddingLeft = '12px';
  1792. }
  1793.  
  1794. // Add tags if available
  1795. let tagsHTML = '';
  1796. if (note.tags && note.tags.length > 0) {
  1797. tagsHTML = '<div style="margin-top: 4px;">';
  1798. note.tags.forEach(tag => {
  1799. tagsHTML += `<span class="notes-tag">${tag}</span>`;
  1800. });
  1801. tagsHTML += '</div>';
  1802. }
  1803.  
  1804. noteDiv.innerHTML = `
  1805. <div style="flex-grow: 1; display: flex; flex-direction: column;">
  1806. <span style="font-weight: 500;">${note.title}</span>
  1807. ${tagsHTML}
  1808. </div>
  1809. <button class="delete-note-button" title="Delete note">×</button>
  1810. `;
  1811.  
  1812. noteDiv.onclick = (e) => {
  1813. if (!e.target.classList.contains('delete-note-button')) {
  1814. container.parentElement.parentElement.remove();
  1815. showNoteContent(note, pattern, index);
  1816. }
  1817. };
  1818.  
  1819. noteDiv.querySelector('.delete-note-button').onclick = (e) => {
  1820. e.stopPropagation();
  1821. if (confirm('Are you sure you want to delete this note?')) {
  1822. deleteNote(pattern, index);
  1823. noteDiv.remove();
  1824. if (patternNotes.length === 1) {
  1825. patternDiv.remove();
  1826. }
  1827. }
  1828. };
  1829.  
  1830. patternDiv.appendChild(noteDiv);
  1831. });
  1832.  
  1833. container.appendChild(patternDiv);
  1834. });
  1835. }
  1836.  
  1837. // Add help button and dropdown
  1838. const helpButton = document.createElement('button');
  1839. helpButton.textContent = '?';
  1840. helpButton.style.position = 'absolute';
  1841. helpButton.style.top = '16px';
  1842. helpButton.style.right = '56px';
  1843. helpButton.style.width = '32px';
  1844. helpButton.style.height = '32px';
  1845. helpButton.style.borderRadius = '50%';
  1846. helpButton.style.border = 'none';
  1847. helpButton.style.background = isDarkMode ? '#374151' : '#e5e7eb';
  1848. helpButton.style.color = isDarkMode ? '#f3f4f6' : '#4b5563';
  1849. helpButton.style.fontSize = '18px';
  1850. helpButton.style.cursor = 'pointer';
  1851. helpButton.style.display = 'flex';
  1852. helpButton.style.alignItems = 'center';
  1853. helpButton.style.justifyContent = 'center';
  1854. helpButton.title = 'URL Pattern Help';
  1855.  
  1856. const helpDropdown = document.createElement('div');
  1857. helpDropdown.style.position = 'absolute';
  1858. helpDropdown.style.top = '52px';
  1859. helpDropdown.style.right = '56px';
  1860. helpDropdown.style.background = isDarkMode ? '#1f2937' : '#ffffff';
  1861. helpDropdown.style.border = `1px solid ${isDarkMode ? '#4b5563' : '#e5e7eb'}`;
  1862. helpDropdown.style.borderRadius = '8px';
  1863. helpDropdown.style.padding = '16px';
  1864. helpDropdown.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
  1865. helpDropdown.style.zIndex = '10001';
  1866. helpDropdown.style.display = 'none';
  1867. helpDropdown.style.maxWidth = '300px';
  1868. helpDropdown.style.color = isDarkMode ? '#f3f4f6' : '#4b5563';
  1869. helpDropdown.innerHTML = `
  1870. <strong>URL Pattern Examples:</strong><br>
  1871. - https://domain.com/* (matches entire domain, one level deep)<br>
  1872. - https://domain.com/** (matches entire domain, any number of levels)<br>
  1873. - https://domain.com/specific/* (matches specific path and one level below)<br>
  1874. - https://domain.com/specific/** (matches specific path and any levels below)<br>
  1875. - https://domain.com/*/specific (matches specific ending, one level in between)<br>
  1876. - https://domain.com/**/specific (matches specific ending, any number of levels in between)
  1877. `;
  1878.  
  1879. let isDropdownOpen = false;
  1880.  
  1881. helpButton.onmouseenter = () => {
  1882. if (!isDropdownOpen) {
  1883. helpDropdown.style.display = 'block';
  1884. }
  1885. };
  1886.  
  1887. helpButton.onmouseleave = () => {
  1888. if (!isDropdownOpen) {
  1889. helpDropdown.style.display = 'none';
  1890. }
  1891. };
  1892.  
  1893. helpButton.onclick = () => {
  1894. isDropdownOpen = !isDropdownOpen;
  1895. helpDropdown.style.display = isDropdownOpen ? 'block' : 'none';
  1896. };
  1897.  
  1898. document.addEventListener('click', (e) => {
  1899. if (isDropdownOpen && e.target !== helpButton && !helpDropdown.contains(e.target)) {
  1900. isDropdownOpen = false;
  1901. helpDropdown.style.display = 'none';
  1902. }
  1903. });
  1904.  
  1905. container.appendChild(helpButton);
  1906. container.appendChild(helpDropdown);
  1907.  
  1908. createModal(container);
  1909. }
  1910.  
  1911. function showAllNotes() {
  1912. const notes = getAllNotes();
  1913. const container = document.createElement('div');
  1914. container.innerHTML = '<h3 class="modal-title">All Notes</h3>';
  1915.  
  1916. // Add a search button
  1917. const searchButton = document.createElement('button');
  1918. searchButton.className = 'notes-button';
  1919. searchButton.textContent = '🔍 Search Notes';
  1920. searchButton.style.marginBottom = '16px';
  1921. searchButton.onclick = showSearchModal;
  1922. container.appendChild(searchButton);
  1923.  
  1924. if (Object.keys(notes).length === 0) {
  1925. container.innerHTML += '<p style="color: #6b7280;">No notes found</p>';
  1926. } else {
  1927. for (const url in notes) {
  1928. const urlDiv = document.createElement('div');
  1929. urlDiv.innerHTML = `<div class="url-text">${url}</div>`;
  1930.  
  1931. notes[url].forEach((note, index) => {
  1932. const noteDiv = document.createElement('div');
  1933. noteDiv.className = 'notes-list-item';
  1934.  
  1935. // Apply note color if available
  1936. if (note.color) {
  1937. noteDiv.style.borderLeft = `4px solid ${note.color}`;
  1938. noteDiv.style.paddingLeft = '12px';
  1939.  
  1940. // Add subtle background tint based on the note color
  1941. const colorOpacity = isDarkMode ? '0.1' : '0.05';
  1942. noteDiv.style.backgroundColor = `${note.color}${colorOpacity}`;
  1943. }
  1944.  
  1945. // Add tags if available
  1946. let tagsHTML = '';
  1947. if (note.tags && note.tags.length > 0) {
  1948. tagsHTML = '<div style="margin-top: 4px;">';
  1949. note.tags.forEach(tag => {
  1950. tagsHTML += `<span class="notes-tag">${tag}</span>`;
  1951. });
  1952. tagsHTML += '</div>';
  1953. }
  1954.  
  1955. // Add pin indicator if note is pinned
  1956. const pinnedIndicator = note.pinned ?
  1957. '<span title="Pinned" style="margin-right: 5px; color: #f59e0b;">📌</span>' : '';
  1958.  
  1959. noteDiv.innerHTML = `
  1960. <div style="flex-grow: 1; display: flex; flex-direction: column;">
  1961. <span style="font-weight: 500;">${pinnedIndicator}${note.title}</span>
  1962. ${tagsHTML}
  1963. </div>
  1964. <button class="delete-note-button" title="Delete note">×</button>
  1965. `;
  1966.  
  1967. noteDiv.onclick = (e) => {
  1968. if (!e.target.classList.contains('delete-note-button')) {
  1969. container.parentElement.parentElement.remove();
  1970. showNoteContent(note, url, index);
  1971. }
  1972. };
  1973.  
  1974. noteDiv.querySelector('.delete-note-button').onclick = (e) => {
  1975. e.stopPropagation();
  1976. if (confirm('Are you sure you want to delete this note?')) {
  1977. deleteNote(url, index);
  1978. noteDiv.remove();
  1979. if (notes[url].length === 1) {
  1980. urlDiv.remove();
  1981. }
  1982. }
  1983. };
  1984.  
  1985. urlDiv.appendChild(noteDiv);
  1986. });
  1987.  
  1988. container.appendChild(urlDiv);
  1989. }
  1990. }
  1991.  
  1992. createModal(container);
  1993. }
  1994.  
  1995. function setupShortcutListener() {
  1996. document.removeEventListener('keydown', shortcutHandler);
  1997. document.addEventListener('keydown', shortcutHandler);
  1998. }
  1999.  
  2000. function shortcutHandler(e) {
  2001. if (matchShortcut(e, options.shortcuts.newNote)) {
  2002. e.preventDefault();
  2003. showNoteForm();
  2004. }
  2005. if (matchShortcut(e, options.shortcuts.currentPageNotes)) {
  2006. e.preventDefault();
  2007. showCurrentPageNotes();
  2008. }
  2009. if (matchShortcut(e, options.shortcuts.allNotes)) {
  2010. e.preventDefault();
  2011. showAllNotes();
  2012. }
  2013. if (matchShortcut(e, options.shortcuts.showOptions)) {
  2014. e.preventDefault();
  2015. showOptionsMenu();
  2016. }
  2017. }
  2018.  
  2019. function matchShortcut(e, shortcut) {
  2020. return e.ctrlKey === shortcut.ctrlKey &&
  2021. e.shiftKey === shortcut.shiftKey &&
  2022. e.altKey === shortcut.altKey &&
  2023. e.key.toLowerCase() === shortcut.key.toLowerCase();
  2024. }
  2025.  
  2026. displayPinnedNotes();
  2027. setupShortcutListener();
  2028.  
  2029. // Register menu commands
  2030. GM_registerMenuCommand('New Note', () => showNoteForm());
  2031. GM_registerMenuCommand('View Notes (Current Page)', showCurrentPageNotes);
  2032. GM_registerMenuCommand('View All Notes', showAllNotes);
  2033. GM_registerMenuCommand('Options', showOptionsMenu);
  2034.  
  2035. })();