Professional Website Notes Manager

Professional notes manager with editable URLs, modern interface, shortcuts, and pin-functionality.

当前为 2025-01-26 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Professional Website Notes Manager
  3. // @namespace http://tampermonkey.net/
  4. // @version 0.6
  5. // @description Professional notes manager with editable URLs, modern interface, shortcuts, and pin-functionality.
  6. // @author Byakuran
  7. // @match https://*/*
  8. // @grant GM_setValue
  9. // @grant GM_getValue
  10. // @grant GM_registerMenuCommand
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. let scriptVersion = '0.6'
  17.  
  18. const defaultOptions = {
  19. version: scriptVersion,
  20. darkMode: window.matchMedia('(prefers-color-scheme: dark)').matches,
  21. addTimestampToTitle: false,
  22. showUrlLinksInNotesList: true,
  23. shortcuts: {
  24. newNote: { ctrlKey: true, shiftKey: true, key: 'S' },
  25. currentPageNotes: { ctrlKey: true, shiftKey: true, key: 'C' },
  26. allNotes: { ctrlKey: true, shiftKey: true, key: 'L' },
  27. showOptions: { ctrlKey: true, altKey: true, key: 'O' }
  28. }
  29. };
  30.  
  31. let options = checkAndUpdateOptions();
  32. GM_setValue('options', options);
  33.  
  34. function checkAndUpdateOptions() {
  35. let currentOptions = GM_getValue('options', defaultOptions);
  36.  
  37. // Check if the version has changed or if it doesn't exist
  38. if (!currentOptions.version || currentOptions.version !== defaultOptions.version) {
  39. // Version has changed, update options
  40. for (let key in defaultOptions) {
  41. if (!(key in currentOptions)) {
  42. currentOptions[key] = defaultOptions[key];
  43. }
  44. }
  45.  
  46. // Update nested objects (shortcuts, possibly more later)
  47. for (let key in defaultOptions.shortcuts) {
  48. if (!(key in currentOptions.shortcuts)) {
  49. currentOptions.shortcuts[key] = defaultOptions.shortcuts[key];
  50. }
  51. }
  52.  
  53. // Update the version
  54. currentOptions.version = defaultOptions.version;
  55.  
  56. // Save the updated options
  57. GM_setValue('options', currentOptions);
  58.  
  59. alert('Options updated to version ' + defaultOptions.version);
  60. console.log('Options updated to version ' + defaultOptions.version);
  61. }
  62.  
  63. return currentOptions;
  64. }
  65.  
  66. const isDarkMode = options.darkMode;
  67.  
  68. const darkModeStyles = {
  69. modal: {
  70. bg: '#1f2937',
  71. text: '#f3f4f6'
  72. },
  73. input: {
  74. bg: '#374151',
  75. border: '#4b5563',
  76. text: '#f3f4f6'
  77. },
  78. button: {
  79. primary: '#3b82f6',
  80. primaryHover: '#2563eb',
  81. secondary: '#4b5563',
  82. secondaryHover: '#374151',
  83. text: '#ffffff'
  84. },
  85. listItem: {
  86. bg: '#374151',
  87. bgHover: '#4b5563',
  88. text: '#f3f4f6'
  89. }
  90. };
  91.  
  92. const lightModeStyles = {
  93. modal: {
  94. bg: '#ffffff',
  95. text: '#111827'
  96. },
  97. input: {
  98. bg: '#f9fafb',
  99. border: '#e5e7eb',
  100. text: '#111827'
  101. },
  102. button: {
  103. primary: '#3b82f6',
  104. primaryHover: '#2563eb',
  105. secondary: '#f3f4f6',
  106. secondaryHover: '#e5e7eb',
  107. text: '#ffffff'
  108. },
  109. listItem: {
  110. bg: '#ffffff',
  111. bgHover: '#f9fafb',
  112. text: '#1f2937'
  113. }
  114. };
  115.  
  116. const currentTheme = isDarkMode ? darkModeStyles : lightModeStyles;
  117.  
  118. const styles = `
  119. .notes-modal {
  120. position: fixed;
  121. top: 50%;
  122. left: 50%;
  123. transform: translate(-50%, -50%);
  124. background: ${currentTheme.modal.bg};
  125. color: ${currentTheme.modal.text};
  126. padding: 32px;
  127. border-radius: 16px;
  128. box-shadow: 0 8px 32px rgba(0,0,0,0.25);
  129. z-index: 10000;
  130. max-width: 700px;
  131. width: 90%;
  132. font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  133. }
  134. .notes-overlay {
  135. position: fixed;
  136. top: 0;
  137. left: 0;
  138. right: 0;
  139. bottom: 0;
  140. background: rgba(0,0,0,${isDarkMode ? '0.8' : '0.7'});
  141. z-index: 9999;
  142. backdrop-filter: blur(4px);
  143. }
  144. .notes-input {
  145. width: 100%;
  146. margin: 12px 0;
  147. padding: 12px 16px;
  148. border: 2px solid ${currentTheme.input.border};
  149. border-radius: 8px;
  150. font-size: 15px;
  151. transition: all 0.2s ease;
  152. background: ${currentTheme.input.bg};
  153. color: ${currentTheme.input.text};
  154. box-sizing: border-box;
  155. }
  156. .notes-input:focus {
  157. outline: none;
  158. border-color: #3b82f6;
  159. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  160. }
  161. .notes-textarea {
  162. width: 100%;
  163. height: 200px;
  164. margin: 12px 0;
  165. padding: 16px;
  166. border: 2px solid ${currentTheme.input.border};
  167. border-radius: 8px;
  168. font-size: 15px;
  169. resize: vertical;
  170. transition: all 0.2s ease;
  171. background: ${currentTheme.input.bg};
  172. color: ${currentTheme.input.text};
  173. line-height: 1.5;
  174. box-sizing: border-box;
  175. }
  176. .notes-textarea:focus {
  177. outline: none;
  178. border-color: #3b82f6;
  179. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  180. }
  181. .notes-button {
  182. background: ${currentTheme.button.primary};
  183. color: ${currentTheme.button.text};
  184. border: none;
  185. padding: 12px 24px;
  186. border-radius: 8px;
  187. cursor: pointer;
  188. margin: 5px;
  189. font-size: 15px;
  190. font-weight: 500;
  191. transition: all 0.2s ease;
  192. }
  193. .notes-button:hover {
  194. background: ${currentTheme.button.primaryHover};
  195. transform: translateY(-1px);
  196. }
  197. .notes-button.secondary {
  198. background: ${currentTheme.button.secondary};
  199. color: ${isDarkMode ? '#f3f4f6' : '#4b5563'};
  200. }
  201. .notes-button.secondary:hover {
  202. background: ${currentTheme.button.secondaryHover};
  203. }
  204. .notes-button.delete {
  205. background: #ef4444;
  206. }
  207. .notes-button.delete:hover {
  208. background: #dc2626;
  209. }
  210. .notes-button.edit {
  211. background: #10b981;
  212. }
  213. .notes-button.edit:hover {
  214. background: #059669;
  215. }
  216. .notes-list-item {
  217. display: flex;
  218. justify-content: space-between;
  219. align-items: center;
  220. padding: 16px;
  221. border: 1px solid ${currentTheme.input.border};
  222. border-radius: 8px;
  223. margin: 8px 0;
  224. cursor: pointer;
  225. transition: all 0.2s ease;
  226. background: ${currentTheme.listItem.bg};
  227. color: ${currentTheme.listItem.text};
  228. }
  229. .notes-list-item:hover {
  230. background: ${currentTheme.listItem.bgHover};
  231. transform: translateY(-1px);
  232. box-shadow: 0 4px 12px rgba(0,0,0,${isDarkMode ? '0.3' : '0.05'});
  233. }
  234. .close-button {
  235. position: absolute;
  236. top: 16px;
  237. right: 16px;
  238. cursor: pointer;
  239. font-size: 24px;
  240. color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
  241. transition: all 0.2s;
  242. width: 32px;
  243. height: 32px;
  244. display: flex;
  245. align-items: center;
  246. justify-content: center;
  247. border-radius: 6px;
  248. }
  249. .close-button:hover {
  250. color: ${isDarkMode ? '#f3f4f6' : '#111827'};
  251. background: ${isDarkMode ? '#374151' : '#f3f4f6'};
  252. }
  253. .modal-title {
  254. font-size: 20px;
  255. font-weight: 600;
  256. margin-bottom: 24px;
  257. color: ${currentTheme.modal.text};
  258. }
  259. .url-text {
  260. font-size: 14px;
  261. color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
  262. word-break: break-all;
  263. margin-bottom: 16px;
  264. padding: 8px 12px;
  265. background: ${isDarkMode ? '#374151' : '#f3f4f6'};
  266. border-radius: 6px;
  267. }
  268. .timestamp {
  269. font-size: 12px;
  270. color: ${isDarkMode ? '#9ca3af' : '#6b7280'};
  271. margin-top: 4px;
  272. }
  273. .delete-note-button {
  274. background: none;
  275. border: none;
  276. color: #ef4444;
  277. font-size: 18px;
  278. cursor: pointer;
  279. padding: 4px 8px;
  280. border-radius: 4px;
  281. transition: all 0.2s ease;
  282. }
  283. .delete-note-button:hover {
  284. background: #ef4444;
  285. color: #ffffff;
  286. }
  287. .notes-options-input {
  288. width: 100%;
  289. margin: 8px 0;
  290. padding: 10px 14px;
  291. border: 2px solid ${currentTheme.input.border};
  292. border-radius: 8px;
  293. font-size: 15px;
  294. transition: all 0.2s ease;
  295. background: ${currentTheme.input.bg};
  296. color: ${currentTheme.input.text};
  297. box-sizing: border-box;
  298. }
  299.  
  300. .notes-options-input:focus {
  301. outline: none;
  302. border-color: #3b82f6;
  303. box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
  304. }
  305.  
  306. .notes-options-checkbox {
  307. margin-right: 8px;
  308. }
  309.  
  310. .notes-options-label {
  311. display: flex;
  312. align-items: center;
  313. margin: 10px 0;
  314. color: ${currentTheme.modal.text};
  315. }
  316. `;
  317.  
  318. const styleSheet = document.createElement("style");
  319. styleSheet.innerText = styles;
  320. document.head.appendChild(styleSheet);
  321.  
  322. function showOptionsMenu() {
  323. const container = document.createElement('div');
  324. container.innerHTML = `
  325. <h3 class="modal-title">Options</h3>
  326. <div class="notes-options-label">
  327. <label>
  328. <input type="checkbox" class="notes-options-checkbox" id="darkModeToggle" ${options.darkMode ? 'checked' : ''}>
  329. Dark Mode
  330. </label>
  331. </div>
  332. <div class="notes-options-label">
  333. <label>
  334. <input type="checkbox" class="notes-options-checkbox" id="timestampToggle" ${options.addTimestampToTitle ? 'checked' : ''}>
  335. Add timestamp to note titles
  336. </label>
  337. </div>
  338. <div class="notes-options-label">
  339. <label>
  340. <input type="checkbox" class="notes-options-checkbox" id="showUrlLinksToggle" ${options.showUrlLinksInNotesList ? 'checked' : ''}>
  341. Show URL links in notes list
  342. </label>
  343. </div>
  344. <h4 style="margin-top: 20px;">Keyboard Shortcuts</h4>
  345. <div>
  346. <label>New Note:
  347. <input type="text" class="notes-options-input" id="newNoteShortcut" value="${getShortcutString(options.shortcuts.newNote)}">
  348. </label>
  349. </div>
  350. <div>
  351. <label>Current Page Notes:
  352. <input type="text" class="notes-options-input" id="currentPageNotesShortcut" value="${getShortcutString(options.shortcuts.currentPageNotes)}">
  353. </label>
  354. </div>
  355. <div>
  356. <label>All Notes:
  357. <input type="text" class="notes-options-input" id="allNotesShortcut" value="${getShortcutString(options.shortcuts.allNotes)}">
  358. </label>
  359. </div>
  360. <div>
  361. <label>Show Options:
  362. <input type="text" class="notes-options-input" id="showOptionsWindow" value="${getShortcutString(options.shortcuts.showOptions)}">
  363. </label>
  364. </div>
  365. <button id="saveOptions" class="notes-button" style="margin-top: 20px;">Save Options</button>
  366. `;
  367.  
  368. createModal(container);
  369. document.getElementById('saveOptions').onclick = saveOptions;
  370. }
  371.  
  372. function getShortcutString(shortcut) {
  373. let str = '';
  374. if (shortcut.ctrlKey) str += 'Ctrl+';
  375. if (shortcut.shiftKey) str += 'Shift+';
  376. if (shortcut.altKey) str += 'Alt+';
  377. str += shortcut.key.toUpperCase();
  378. return str;
  379. }
  380.  
  381. function parseShortcutString(str) {
  382. const parts = str.toLowerCase().split('+');
  383. return {
  384. ctrlKey: parts.includes('ctrl'),
  385. shiftKey: parts.includes('shift'),
  386. altKey: parts.includes('alt'),
  387. key: parts[parts.length - 1]
  388. };
  389. }
  390.  
  391. function saveOptions() {
  392. options = {
  393. version: scriptVersion,
  394. darkMode: document.getElementById('darkModeToggle').checked,
  395. addTimestampToTitle: document.getElementById('timestampToggle').checked,
  396. showUrlLinksInNotesList: document.getElementById('showUrlLinksToggle').checked,
  397. shortcuts: {
  398. newNote: parseShortcutString(document.getElementById('newNoteShortcut').value),
  399. currentPageNotes: parseShortcutString(document.getElementById('currentPageNotesShortcut').value),
  400. allNotes: parseShortcutString(document.getElementById('allNotesShortcut').value),
  401. showOptions: parseShortcutString(document.getElementById('showOptionsWindow').value)
  402. }
  403. };
  404. GM_setValue('options', options);
  405. setupShortcutListener();
  406. alert('Options saved. Changes have been applied, some changes may require reloading.');
  407. }
  408.  
  409.  
  410. GM_registerMenuCommand('Toggle Dark Mode', () => {
  411. const newMode = !isDarkMode;
  412. GM_setValue('darkMode', newMode);
  413. location.reload();
  414. });
  415.  
  416. function createModal(content) {
  417. const overlay = document.createElement('div');
  418. overlay.className = 'notes-overlay';
  419.  
  420. const modal = document.createElement('div');
  421. modal.className = 'notes-modal';
  422.  
  423. const closeButton = document.createElement('span');
  424. closeButton.className = 'close-button';
  425. closeButton.textContent = '×';
  426. closeButton.onclick = () => overlay.remove();
  427.  
  428. modal.appendChild(closeButton);
  429. modal.appendChild(content);
  430. overlay.appendChild(modal);
  431. document.body.appendChild(overlay);
  432.  
  433. const escapeListener = (e) => {
  434. if (e.key === 'Escape') {
  435. overlay.remove();
  436. document.removeEventListener('keydown', escapeListener);
  437. }
  438. };
  439. document.addEventListener('keydown', escapeListener);
  440. }
  441.  
  442. function getAllNotes() {
  443. return GM_getValue('website-notes', {});
  444. }
  445.  
  446. function saveNote(title, url, content, timestamp = Date.now(), pinned = false) {
  447. const notes = getAllNotes();
  448. if (!notes[url]) notes[url] = [];
  449.  
  450. // Add timestamp to title if the option is enabled
  451. let finalTitle = title;
  452. if (options.addTimestampToTitle) {
  453. const date = new Date(timestamp);
  454. const formattedDate = date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
  455. finalTitle = `${title} [${formattedDate}]`;
  456. }
  457.  
  458. notes[url].push({ title: finalTitle, content, timestamp, pinned });
  459. GM_setValue('website-notes', notes);
  460. }
  461.  
  462. function updateNote(oldUrl, index, title, newUrl, content, pinned) {
  463. const notes = getAllNotes();
  464. deleteNote(oldUrl, index);
  465. saveNote(title, newUrl, content, notes[oldUrl][index].timestamp, pinned);
  466. }
  467.  
  468. function togglePinNote(url, index) {
  469. const notes = getAllNotes();
  470. if (notes[url] && notes[url][index]) {
  471. notes[url][index].pinned = !notes[url][index].pinned;
  472. GM_setValue('website-notes', notes);
  473. }
  474. }
  475.  
  476. function deleteNote(url, index) {
  477. const notes = getAllNotes();
  478. if (notes[url]) {
  479. notes[url].splice(index, 1);
  480. if (notes[url].length === 0) delete notes[url];
  481. GM_setValue('website-notes', notes);
  482. }
  483. }
  484.  
  485. function showNoteForm(editMode = false, existingNote = null, url = null, index = null) {
  486. const container = document.createElement('div');
  487. container.innerHTML = `<h3 class="modal-title">${editMode ? 'Edit Note' : 'Create New Note'}</h3>`;
  488.  
  489. const titleInput = document.createElement('input');
  490. titleInput.className = 'notes-input';
  491. titleInput.placeholder = 'Enter title';
  492. titleInput.value = editMode ? existingNote.title : '';
  493.  
  494. const urlInput = document.createElement('input');
  495. urlInput.className = 'notes-input';
  496. urlInput.placeholder = 'Enter URL(s) or URL pattern(s), separated by spaces (e.g., https://domain.com/*)';
  497. urlInput.value = editMode ? url : window.location.href;
  498.  
  499. const patternHelp = document.createElement('div');
  500. patternHelp.style.fontSize = '12px';
  501. patternHelp.style.color = isDarkMode ? '#9ca3af' : '#6b7280';
  502. patternHelp.style.marginTop = '-8px';
  503. patternHelp.style.marginBottom = '8px';
  504. patternHelp.innerHTML = 'Use * for wildcard matching. Multiple URLs: separate with spaces (e.g., https://domain1.com/* https://domain2.com/*)';
  505.  
  506. const contentArea = document.createElement('textarea');
  507. contentArea.className = 'notes-textarea';
  508. contentArea.placeholder = 'Enter your notes here';
  509. contentArea.value = editMode ? existingNote.content : '';
  510.  
  511. const buttonGroup = document.createElement('div');
  512. buttonGroup.className = 'button-group';
  513.  
  514. const saveButton = document.createElement('button');
  515. saveButton.className = 'notes-button';
  516. saveButton.textContent = editMode ? 'Update Note' : 'Save Note';
  517. saveButton.onclick = () => {
  518. if (titleInput.value && contentArea.value) {
  519. if (editMode) {
  520. updateNote(url, index, titleInput.value, urlInput.value, contentArea.value);
  521. } else {
  522. saveNote(titleInput.value, urlInput.value, contentArea.value);
  523. }
  524. container.parentElement.parentElement.remove();
  525. showCurrentPageNotes();
  526. }
  527. };
  528.  
  529. const cancelButton = document.createElement('button');
  530. cancelButton.className = 'notes-button secondary';
  531. cancelButton.textContent = 'Cancel';
  532. cancelButton.onclick = () => container.parentElement.parentElement.remove();
  533.  
  534. buttonGroup.appendChild(saveButton);
  535. buttonGroup.appendChild(cancelButton);
  536.  
  537. container.appendChild(titleInput);
  538. container.appendChild(urlInput);
  539. container.appendChild(patternHelp);
  540. container.appendChild(contentArea);
  541. container.appendChild(buttonGroup);
  542.  
  543. createModal(container);
  544. }
  545.  
  546. function formatDate(timestamp) {
  547. return new Date(timestamp).toLocaleString();
  548. }
  549.  
  550. function showNoteContent(note, url, index) {
  551. const container = document.createElement('div');
  552.  
  553. // Function to convert URLs to clickable links
  554. function linkify(text) {
  555. // URL pattern for matching
  556. const urlPattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gim;
  557.  
  558. // Replace URLs with anchor tags
  559. const linkedText = text.replace(urlPattern, function(url) {
  560. return `<a href="${url}" target="_blank" style="color: #3b82f6; text-decoration: underline; word-break: break-all;" onclick="event.stopPropagation();">${url}</a>`;
  561. });
  562.  
  563. // Convert line breaks to <br> tags and maintain whitespace
  564. return linkedText.replace(/\n/g, '<br>').replace(/\s{2,}/g, function(space) {
  565. return ' ' + '&nbsp;'.repeat(space.length - 1);
  566. });
  567. }
  568.  
  569. container.innerHTML = `
  570. <h3 class="modal-title">${note.title}</h3>
  571. <div class="url-text">${url}</div>
  572. <div class="timestamp">Created: ${formatDate(note.timestamp)}</div>
  573. <div style="margin: 16px 0; line-height: 1.6;">${linkify(note.content)}</div>
  574. `;
  575.  
  576. const buttonGroup = document.createElement('div');
  577. buttonGroup.className = 'button-group';
  578.  
  579. const editButton = document.createElement('button');
  580. editButton.className = 'notes-button edit';
  581. editButton.textContent = 'Edit';
  582. editButton.onclick = () => {
  583. container.parentElement.parentElement.remove();
  584. showNoteForm(true, note, url, index);
  585. };
  586.  
  587. const deleteButton = document.createElement('button');
  588. deleteButton.className = 'notes-button delete';
  589. deleteButton.textContent = 'Delete';
  590. deleteButton.onclick = () => {
  591. if (confirm('Are you sure you want to delete this note?')) {
  592. deleteNote(url, index);
  593. container.parentElement.parentElement.remove();
  594. showCurrentPageNotes();
  595. }
  596. };
  597.  
  598. const pinButton = document.createElement('button');
  599. pinButton.className = `notes-button ${note.pinned ? 'secondary' : ''}`;
  600. pinButton.textContent = note.pinned ? 'Unpin' : 'Pin';
  601. pinButton.onclick = () => {
  602. togglePinNote(url, index);
  603. pinButton.textContent = note.pinned ? 'Pin' : 'Unpin';
  604. pinButton.className = `notes-button ${!note.pinned ? 'secondary' : ''}`;
  605. displayPinnedNotes();
  606. };
  607.  
  608. buttonGroup.appendChild(editButton);
  609. buttonGroup.appendChild(deleteButton);
  610. buttonGroup.appendChild(pinButton);
  611. container.appendChild(buttonGroup);
  612.  
  613. createModal(container);
  614. }
  615.  
  616. function displayPinnedNotes() {
  617. const notes = getAllNotes();
  618. const currentUrl = window.location.href;
  619. let pinnedNotesContainer = document.getElementById('pinned-notes-container');
  620.  
  621. if (!pinnedNotesContainer) {
  622. pinnedNotesContainer = document.createElement('div');
  623. pinnedNotesContainer.id = 'pinned-notes-container';
  624. pinnedNotesContainer.style.position = 'fixed';
  625. pinnedNotesContainer.style.top = '10px';
  626. pinnedNotesContainer.style.right = '10px';
  627. pinnedNotesContainer.style.zIndex = '9999';
  628. pinnedNotesContainer.style.maxWidth = '300px';
  629. document.body.appendChild(pinnedNotesContainer);
  630. }
  631.  
  632. pinnedNotesContainer.innerHTML = '';
  633.  
  634. for (const url in notes) {
  635. if (doesUrlMatchPattern(url, currentUrl)) {
  636. notes[url].forEach((note, index) => {
  637. if (note.pinned) {
  638. const noteDiv = document.createElement('div');
  639. noteDiv.className = 'pinned-note';
  640. noteDiv.style.background = currentTheme.listItem.bg;
  641. noteDiv.style.color = currentTheme.listItem.text;
  642. noteDiv.style.padding = '10px';
  643. noteDiv.style.margin = '5px 0';
  644. noteDiv.style.borderRadius = '8px';
  645. noteDiv.style.boxShadow = '0 2px 4px rgba(0,0,0,0.1)';
  646. noteDiv.style.cursor = 'pointer';
  647. noteDiv.innerHTML = `<strong>${note.title}</strong>`;
  648. noteDiv.onclick = () => showNoteContent(note, url, index);
  649. pinnedNotesContainer.appendChild(noteDiv);
  650. }
  651. });
  652. }
  653. }
  654. }
  655.  
  656.  
  657. function doesUrlMatchPattern(urlPatterns, currentUrl) {
  658. // Split the pattern string into an array of patterns
  659. const patterns = urlPatterns.split(/\s+/).filter(pattern => pattern.trim() !== '');
  660.  
  661. // Check if any of the patterns match the current URL
  662. return patterns.some(pattern => {
  663. // Escape special characters for regex
  664. const escapeRegex = (string) => string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  665.  
  666. // Convert Tampermonkey-style pattern to regex
  667. const patternToRegex = (pattern) => {
  668. const parts = pattern.split('*');
  669. let regexString = '^';
  670. for (let i = 0; i < parts.length; i++) {
  671. regexString += escapeRegex(parts[i]);
  672. if (i < parts.length - 1) {
  673. if (parts[i + 1] === '') {
  674. // '**' matches any number of path segments
  675. regexString += '.*';
  676. i++; // Skip the next '*'
  677. } else {
  678. // Single '*' matches anything except '/'
  679. regexString += '[^/]*';
  680. }
  681. }
  682. }
  683. // If the pattern ends with '**', allow anything at the end
  684. if (pattern.endsWith('**')) {
  685. regexString += '.*';
  686. } else if (!pattern.endsWith('*')) {
  687. regexString += '$';
  688. }
  689. return new RegExp(regexString);
  690. };
  691.  
  692. try {
  693. const regex = patternToRegex(pattern);
  694. return regex.test(currentUrl);
  695. } catch (e) {
  696. console.error('Invalid URL pattern:', e);
  697. return false;
  698. }
  699. });
  700. }
  701.  
  702. function showCurrentPageNotes() {
  703. const notes = getAllNotes();
  704. const currentUrl = window.location.href;
  705. let matchingNotes = [];
  706.  
  707. // Collect all matching notes
  708. for (const urlPattern in notes) {
  709. if (doesUrlMatchPattern(urlPattern, currentUrl)) {
  710. matchingNotes.push({
  711. pattern: urlPattern,
  712. notes: notes[urlPattern]
  713. });
  714. }
  715. }
  716.  
  717. const container = document.createElement('div');
  718. container.innerHTML = `
  719. <h3 class="modal-title">Notes for Current Page</h3>
  720. <div class="url-text">${currentUrl}</div>
  721. `;
  722.  
  723. if (matchingNotes.length === 0) {
  724. container.innerHTML += '<p style="color: #6b7280;">No matching notes found for this page</p>';
  725. } else {
  726. matchingNotes.forEach(({pattern, notes: patternNotes}) => {
  727. const patternDiv = document.createElement('div');
  728.  
  729. if (options.showUrlLinksInNotesList) {
  730. patternDiv.innerHTML = `<div class="url-text">Pattern: ${pattern}</div>`;
  731. }
  732. patternNotes.forEach((note, index) => {
  733. const noteDiv = document.createElement('div');
  734. noteDiv.className = 'notes-list-item';
  735. noteDiv.innerHTML = `
  736. <span style="flex-grow: 1; display: flex; align-items: center;">${note.title}</span>
  737. <button class="delete-note-button" title="Delete note">×</button>
  738. `;
  739.  
  740. noteDiv.onclick = (e) => {
  741. if (!e.target.classList.contains('delete-note-button')) {
  742. container.parentElement.parentElement.remove();
  743. showNoteContent(note, pattern, index);
  744. }
  745. };
  746.  
  747. noteDiv.querySelector('.delete-note-button').onclick = (e) => {
  748. e.stopPropagation();
  749. if (confirm('Are you sure you want to delete this note?')) {
  750. deleteNote(pattern, index);
  751. noteDiv.remove();
  752. if (patternNotes.length === 1) {
  753. patternDiv.remove();
  754. }
  755. }
  756. };
  757.  
  758. patternDiv.appendChild(noteDiv);
  759. });
  760.  
  761. container.appendChild(patternDiv);
  762. });
  763. }
  764.  
  765. // Add help button and dropdown
  766. const helpButton = document.createElement('button');
  767. helpButton.textContent = '?';
  768. helpButton.style.position = 'absolute';
  769. helpButton.style.top = '16px';
  770. helpButton.style.right = '56px';
  771. helpButton.style.width = '32px';
  772. helpButton.style.height = '32px';
  773. helpButton.style.borderRadius = '50%';
  774. helpButton.style.border = 'none';
  775. helpButton.style.background = isDarkMode ? '#374151' : '#e5e7eb';
  776. helpButton.style.color = isDarkMode ? '#f3f4f6' : '#4b5563';
  777. helpButton.style.fontSize = '18px';
  778. helpButton.style.cursor = 'pointer';
  779. helpButton.style.display = 'flex';
  780. helpButton.style.alignItems = 'center';
  781. helpButton.style.justifyContent = 'center';
  782. helpButton.title = 'URL Pattern Help';
  783.  
  784. const helpDropdown = document.createElement('div');
  785. helpDropdown.style.position = 'absolute';
  786. helpDropdown.style.top = '52px';
  787. helpDropdown.style.right = '56px';
  788. helpDropdown.style.background = isDarkMode ? '#1f2937' : '#ffffff';
  789. helpDropdown.style.border = `1px solid ${isDarkMode ? '#4b5563' : '#e5e7eb'}`;
  790. helpDropdown.style.borderRadius = '8px';
  791. helpDropdown.style.padding = '16px';
  792. helpDropdown.style.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)';
  793. helpDropdown.style.zIndex = '10001';
  794. helpDropdown.style.display = 'none';
  795. helpDropdown.style.maxWidth = '300px';
  796. helpDropdown.style.color = isDarkMode ? '#f3f4f6' : '#4b5563';
  797. helpDropdown.innerHTML = `
  798. <strong>URL Pattern Examples:</strong><br>
  799. - https://domain.com/* (matches entire domain, one level deep)<br>
  800. - https://domain.com/** (matches entire domain, any number of levels)<br>
  801. - https://domain.com/specific/* (matches specific path and one level below)<br>
  802. - https://domain.com/specific/** (matches specific path and any levels below)<br>
  803. - https://domain.com/*/specific (matches specific ending, one level in between)<br>
  804. - https://domain.com/**/specific (matches specific ending, any number of levels in between)
  805. `;
  806.  
  807. let isDropdownOpen = false;
  808.  
  809. helpButton.onmouseenter = () => {
  810. if (!isDropdownOpen) {
  811. helpDropdown.style.display = 'block';
  812. }
  813. };
  814.  
  815. helpButton.onmouseleave = () => {
  816. if (!isDropdownOpen) {
  817. helpDropdown.style.display = 'none';
  818. }
  819. };
  820.  
  821. helpButton.onclick = () => {
  822. isDropdownOpen = !isDropdownOpen;
  823. helpDropdown.style.display = isDropdownOpen ? 'block' : 'none';
  824. };
  825.  
  826. document.addEventListener('click', (e) => {
  827. if (isDropdownOpen && e.target !== helpButton && !helpDropdown.contains(e.target)) {
  828. isDropdownOpen = false;
  829. helpDropdown.style.display = 'none';
  830. }
  831. });
  832.  
  833. container.appendChild(helpButton);
  834. container.appendChild(helpDropdown);
  835.  
  836. createModal(container);
  837. }
  838.  
  839. function showAllNotes() {
  840. const notes = getAllNotes();
  841. const container = document.createElement('div');
  842. container.innerHTML = '<h3 class="modal-title">All Notes</h3>';
  843.  
  844. if (Object.keys(notes).length === 0) {
  845. container.innerHTML += '<p style="color: #6b7280;">No notes found</p>';
  846. } else {
  847. for (const url in notes) {
  848. const urlDiv = document.createElement('div');
  849. urlDiv.innerHTML = `<div class="url-text">${url}</div>`;
  850.  
  851. notes[url].forEach((note, index) => {
  852. const noteDiv = document.createElement('div');
  853. noteDiv.className = 'notes-list-item';
  854. noteDiv.innerHTML = `
  855. <span style="flex-grow: 1; display: flex; align-items: center;">${note.title}</span>
  856. <button class="delete-note-button" title="Delete note">×</button>
  857. `;
  858.  
  859. noteDiv.onclick = (e) => {
  860. if (!e.target.classList.contains('delete-note-button')) {
  861. container.parentElement.parentElement.remove();
  862. showNoteContent(note, url, index);
  863. }
  864. };
  865.  
  866. noteDiv.querySelector('.delete-note-button').onclick = (e) => {
  867. e.stopPropagation();
  868. if (confirm('Are you sure you want to delete this note?')) {
  869. deleteNote(url, index);
  870. noteDiv.remove();
  871. if (notes[url].length === 1) {
  872. urlDiv.remove();
  873. }
  874. }
  875. };
  876.  
  877. urlDiv.appendChild(noteDiv);
  878. });
  879.  
  880. container.appendChild(urlDiv);
  881. }
  882. }
  883.  
  884. createModal(container);
  885. }
  886.  
  887. function setupShortcutListener() {
  888. document.removeEventListener('keydown', shortcutHandler);
  889. document.addEventListener('keydown', shortcutHandler);
  890. }
  891.  
  892. function shortcutHandler(e) {
  893. if (matchShortcut(e, options.shortcuts.newNote)) {
  894. e.preventDefault();
  895. showNoteForm();
  896. }
  897. if (matchShortcut(e, options.shortcuts.currentPageNotes)) {
  898. e.preventDefault();
  899. showCurrentPageNotes();
  900. }
  901. if (matchShortcut(e, options.shortcuts.allNotes)) {
  902. e.preventDefault();
  903. showAllNotes();
  904. }
  905. if (matchShortcut(e, options.shortcuts.showOptions)) {
  906. e.preventDefault();
  907. showOptionsMenu();
  908. }
  909. }
  910.  
  911. function matchShortcut(e, shortcut) {
  912. return e.ctrlKey === shortcut.ctrlKey &&
  913. e.shiftKey === shortcut.shiftKey &&
  914. e.altKey === shortcut.altKey &&
  915. e.key.toLowerCase() === shortcut.key.toLowerCase();
  916. }
  917.  
  918. displayPinnedNotes();
  919. setupShortcutListener();
  920.  
  921. // Register menu commands
  922. GM_registerMenuCommand('New Note', () => showNoteForm());
  923. GM_registerMenuCommand('View Notes (Current Page)', showCurrentPageNotes);
  924. GM_registerMenuCommand('View All Notes', showAllNotes);
  925. GM_registerMenuCommand('Options', showOptionsMenu);
  926.  
  927. })();