staging notes v1

暂存便签

  1. // ==UserScript==
  2. // @name staging notes v1
  3. // @namespace http://tampermonkey.net/
  4. // @version 1
  5. // @description 暂存便签
  6. // @author yeeel
  7. // @license MIT
  8. // @match *://*/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=greasyfork.org
  10. // @grant GM_setValue
  11. // @grant GM_getValue
  12. // @grant GM_addStyle
  13. // ==/UserScript==
  14.  
  15. (function() {
  16. 'use strict';
  17.  
  18. // --- CSS with Container/Icon Box-Shadow Removed ---
  19. GM_addStyle(`
  20. /* --- Base Wrapper --- */
  21. .goodnote-wrapper {
  22. position: fixed; top: 0; left: 0; width: 100%; height: 100%;
  23. pointer-events: none; z-index: 2000; /* Low z-index */
  24. }
  25.  
  26. /* --- Note Icon --- */
  27. .note-icon {
  28. display: flex; align-items: center; justify-content: center;
  29. position: fixed; z-index: 2002; /* Low z-index */
  30. pointer-events: auto; width: 24px; height: 24px; cursor: move;
  31. user-select: none; border-radius: 5px;
  32. background-color: rgba(255, 255, 255, 0.7);
  33. border: 1px solid rgba(200, 200, 200, 0.8);
  34. /* box-shadow: 0 2px 8px rgba(0,0,0,0.25); */ /* <-- REMOVED */
  35. opacity: 0.9;
  36. backdrop-filter: blur(10px);
  37. transition: transform 0.15s ease, background-color 0.2s ease, opacity 0.2s ease;
  38. transform: translate3d(0, 0, 0); will-change: transform;
  39. }
  40. .note-icon svg { width: 18px; height: 18px; fill: #333; }
  41. .note-icon:hover { opacity: 1; background-color: rgba(255, 255, 255, 0.9); transform: scale(1.1); }
  42. .note-icon:active { cursor: grabbing; transform: scale(0.95); }
  43.  
  44. /* --- Note Container --- */
  45. .note-container {
  46. display: none; position: fixed; z-index: 2001; /* Low z-index */
  47. pointer-events: auto; color: #333; min-width: 380px; padding: 12px;
  48. border-radius: 8px; background: rgba(255, 255, 255, 0.95);
  49. border: 1px solid #bbb;
  50. /* box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2); */ /* <-- REMOVED */
  51. backdrop-filter: blur(12px);
  52. /* NO Animation properties */
  53. }
  54. .note-container.active {
  55. /* Only display:block handled by JS */
  56. }
  57.  
  58. /* --- Note Header --- */
  59. .note-header { display: flex; align-items: center; justify-content: flex-start; margin-bottom: 10px; gap: 10px; user-select: none; padding-left: 2px; }
  60.  
  61. /* --- Action Buttons --- */
  62. .note-action-button, .pin-button { cursor: pointer; font-size: 20px; color: #555; padding: 3px; user-select: none; line-height: 1; transition: color 0.2s, transform 0.2s ease; }
  63. .note-action-button:hover, .pin-button:hover { color: #007aff; transform: scale(1.15); }
  64. .note-action-button:active, .pin-button:active { transform: scale(0.9); }
  65. .note-container.pinned .pin-button { color: #ff3b30; }
  66. .note-container.pinned .pin-button:hover { color: #ff6b6b; }
  67.  
  68. /* --- Text Area --- */
  69. .note-textarea { display: block; margin-bottom: 0 !important; background: #ffffff; color:#1c1c1e; min-height: 250px; min-width: 350px; height: 280px; width: 100%; border: 1px solid #d1d1d6; border-radius: 6px; padding: 12px; font-size: 15px; resize: both; overflow: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; word-break: break-word; text-align: left; outline: none; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
  70. .note-textarea:focus { outline: none; border-color: #007aff; box-shadow: 0 0 0 2px rgba(0, 122, 255, 0.2); } /* Keep textarea focus shadow */
  71. .note-textarea a, .note-textarea a:visited { color: #007aff; text-decoration: underline; cursor: pointer; }
  72. .note-textarea a:hover { opacity: 0.7; }
  73.  
  74. /* --- Selection Popup --- */
  75. #goodnote-selection-popup { position: absolute; background-color: #007aff; color: white; border: none; border-radius: 5px; padding: 5px 10px; font-size: 13px; cursor: pointer; z-index: 2003; box-shadow: 0 2px 5px rgba(0,0,0,0.2); opacity: 0; transform: translateY(5px); pointer-events: none; white-space: nowrap; transition: opacity 0.2s ease, transform 0.2s ease; display: none; /* Start hidden, JS will set display:block */ }
  76. #goodnote-selection-popup.visible { opacity: 1; transform: translateY(0); pointer-events: auto; display: block !important; } /* Use class again, JS sets this */
  77.  
  78.  
  79. /* --- Flash Message --- */
  80. #goodnote-message { position: fixed; bottom: 25px; left: 50%; transform: translateX(-50%); padding: 10px 18px; border-radius: 6px; color: white; font-size: 14px; z-index: 2003; opacity: 0; transition: opacity 0.4s ease; pointer-events: none; text-align: center; background-color: rgba(40, 167, 69, 0.85); box-shadow: 0 3px 10px rgba(0,0,0,0.2); }
  81. #goodnote-message.error { background-color: rgba(220, 53, 69, 0.85); }
  82. #goodnote-message.visible { opacity: 1; }
  83. `);
  84.  
  85. // --- Full JavaScript Code Below (Reverted Popup JS to use .visible class) ---
  86. // PASTE THE FULL JS CODE HERE (from Final v3/v4, make sure popup logic uses .visible class again)
  87. // ...
  88.  
  89. // --- DOM元素创建 (Create DOM Elements) ---
  90. const wrapper = document.createElement('div');
  91. wrapper.className = 'goodnote-wrapper';
  92. if (document.body) { document.body.appendChild(wrapper); } else { document.addEventListener('DOMContentLoaded', () => document.body.appendChild(wrapper)); }
  93. const noteIcon = document.createElement('div');
  94. noteIcon.className = 'note-icon';
  95. noteIcon.title = '打开/关闭笔记 (Ctrl+Shift+M)';
  96. noteIcon.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14,10H19.5L14,4.5V10M5,3H15L21,9V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3M5,12V14H19V12H5M5,16V18H14V16H5Z"/></svg>`;
  97. wrapper.appendChild(noteIcon);
  98. const noteContainer = document.createElement('div');
  99. noteContainer.className = 'note-container';
  100. wrapper.appendChild(noteContainer);
  101. const header = document.createElement('div');
  102. header.className = 'note-header';
  103. noteContainer.appendChild(header);
  104. const pinButton = document.createElement('span');
  105. pinButton.className = 'pin-button'; pinButton.textContent = '📌'; pinButton.title = '置顶/取消置顶笔记';
  106. header.appendChild(pinButton);
  107. const cutButton = document.createElement('span');
  108. cutButton.className = 'note-action-button cut-button'; cutButton.textContent = '✂️'; cutButton.title = '剪切全部笔记内容 (Ctrl+Alt+P)';
  109. header.appendChild(cutButton);
  110. const copyButton = document.createElement('span');
  111. copyButton.className = 'note-action-button copy-button'; copyButton.textContent = '📄'; copyButton.title = '复制全部笔记内容';
  112. header.appendChild(copyButton);
  113. const textarea = document.createElement('div');
  114. textarea.className = 'note-textarea'; textarea.contentEditable = true; textarea.spellcheck = false;
  115. textarea.setAttribute('placeholder', '在这里输入你的笔记...');
  116. noteContainer.appendChild(textarea);
  117. let selectionPopup = null;
  118.  
  119. // --- 存储键 (Storage Keys) ---
  120. const storageKey = "goodnote_global_note_v3_final_v6"; // Use a new key
  121. const positionKey = "goodnote_global_position_v3_final_v6";
  122.  
  123. // --- 核心功能函数 (Core Functions) ---
  124. function linkify(text) { const urlRegex = /(https?:\/\/[^\s<>"'`]+)/g; return text.replace(urlRegex, (url) => { if (url.includes('</a>')) return url; return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="note-link">${url}</a>`; }); }
  125. function saveNote() { const content = textarea.innerHTML; GM_setValue(storageKey, content); }
  126. function loadNote() { const savedNote = GM_getValue(storageKey, ''); if (textarea.innerHTML !== savedNote) textarea.innerHTML = savedNote; }
  127. function convertHtmlToPlainTextWithNewlines(html) { const tempDiv = document.createElement('div'); let processedHtml = html.replace(/<br\s*\/?>/gi, '\n'); tempDiv.innerHTML = processedHtml; let plainText = tempDiv.textContent || tempDiv.innerText || ""; return plainText.trim(); }
  128.  
  129. // --- 事件监听 (Event Listeners) ---
  130. let saveTimeout; const SAVE_DELAY = 150;
  131. textarea.addEventListener('input', () => { clearTimeout(saveTimeout); saveTimeout = setTimeout(saveNote, SAVE_DELAY); });
  132. textarea.addEventListener('paste', (e) => { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); if (!text) return; const textWithBreaks = text.replace(/\r\n|\n/g, '<br>'); const linkedText = linkify(textWithBreaks); document.execCommand('insertHTML', false, linkedText); textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); });
  133. textarea.addEventListener('click', (e) => { if (e.target.tagName === 'A' && e.target.classList.contains('note-link')) { e.preventDefault(); window.open(e.target.href, '_blank', 'noopener,noreferrer'); } });
  134.  
  135. // --- Selection Handling (Using .visible class again) ---
  136. function removeSelectionPopup() {
  137. if (selectionPopup && selectionPopup.parentNode) {
  138. selectionPopup.classList.remove('visible');
  139. // Allow animation to finish before removing
  140. setTimeout(() => {
  141. if (selectionPopup && selectionPopup.parentNode) {
  142. selectionPopup.parentNode.removeChild(selectionPopup);
  143. }
  144. selectionPopup = null;
  145. }, 250); // Match CSS transition duration
  146. }
  147. // Ensure variable is nulled even if removal is delayed
  148. if(selectionPopup && !selectionPopup.classList.contains('visible')) {
  149. selectionPopup = null;
  150. }
  151. }
  152. document.addEventListener('mouseup', (e) => {
  153. // console.log("Mouse Up Detected"); // Keep debug logs if needed
  154. if (noteContainer.contains(e.target) || noteIcon.contains(e.target) || (selectionPopup && selectionPopup.contains(e.target))) {
  155. // console.log("Mouse Up ignored (inside component)");
  156. return;
  157. }
  158. setTimeout(() => {
  159. const selection = window.getSelection();
  160. if (!selection || selection.rangeCount === 0 || selection.isCollapsed) {
  161. // console.log("No selection or collapsed, removing popup");
  162. removeSelectionPopup();
  163. return;
  164. }
  165. const selectedText = selection.toString().trim();
  166. // console.log("Selected Text:", selectedText);
  167. removeSelectionPopup(); // Remove any existing first
  168.  
  169. if (selectedText.length > 0) {
  170. const range = selection.getRangeAt(0);
  171. if (textarea.contains(range.commonAncestorContainer)) {
  172. // console.log("Selection inside textarea, ignored");
  173. return;
  174. }
  175. const rect = range.getBoundingClientRect();
  176. if (rect.width === 0 && rect.height === 0 && selectedText.length < 5) {
  177. // console.log("Selection too small or invisible, ignored");
  178. return;
  179. }
  180.  
  181. // console.log("Creating Selection Popup");
  182. selectionPopup = document.createElement('button');
  183. selectionPopup.id = 'goodnote-selection-popup';
  184. selectionPopup.textContent = '➕';
  185. // Ensure it starts hidden if CSS relies on opacity/transform
  186. selectionPopup.style.display = 'none'; // Explicitly hide initially
  187. document.body.appendChild(selectionPopup);
  188.  
  189. // Calculate position
  190. let popupTop = window.pageYOffset + rect.bottom + 8;
  191. let popupLeft = window.pageXOffset + rect.left + (rect.width / 2) - (selectionPopup.offsetWidth / 2);
  192. const popupWidth = selectionPopup.offsetWidth; const popupHeight = selectionPopup.offsetHeight;
  193. if (popupLeft + popupWidth > window.innerWidth - 10) popupLeft = window.innerWidth - popupWidth - 10;
  194. if (popupTop + popupHeight > window.innerHeight + window.pageYOffset - 10) popupTop = window.pageYOffset + rect.top - popupHeight - 8;
  195. popupLeft = Math.max(10 + window.pageXOffset, popupLeft);
  196. popupTop = Math.max(10 + window.pageYOffset, popupTop);
  197. // console.log("Popup Position:", {top: popupTop, left: popupLeft});
  198.  
  199. selectionPopup.style.top = `${popupTop}px`;
  200. selectionPopup.style.left = `${popupLeft}px`;
  201. selectionPopup.style.display = ''; // Clear display style override
  202.  
  203. // --- Use .visible class to trigger CSS animation ---
  204. // console.log("Adding .visible class to popup");
  205. requestAnimationFrame(() => { // Ensure element is ready for transition
  206. selectionPopup.classList.add('visible');
  207. });
  208. // --- END CHANGE ---
  209.  
  210. selectionPopup.addEventListener('click', function handleAddClick(event) {
  211. event.stopPropagation();
  212. // console.log("Add to Note Clicked");
  213. const currentContent = textarea.innerHTML.trim();
  214. const textToAdd = selectedText.replace(/\r\n|\n/g, '<br>');
  215. const linkedText = linkify(textToAdd);
  216. textarea.innerHTML += (currentContent ? '<br><br>' : '') + linkedText;
  217. saveNote();
  218. removeSelectionPopup();
  219. window.getSelection().removeAllRanges();
  220. flashMessage("已添加到笔记");
  221. });
  222. } else {
  223. // console.log("Selected text is empty after trim");
  224. }
  225. }, 100);
  226. });
  227. document.addEventListener('mousedown', (e) => { if (selectionPopup && !selectionPopup.contains(e.target)) { /* console.log("Mousedown outside popup, removing"); */ removeSelectionPopup(); } });
  228.  
  229.  
  230. // --- UI Action Buttons & Flash Message ---
  231. let messageTimeout;
  232. function flashMessage(message, isError = false) { let msgDiv = document.getElementById('goodnote-message'); if (!msgDiv) { msgDiv = document.createElement('div'); msgDiv.id = 'goodnote-message'; if (document.body) document.body.appendChild(msgDiv); else document.addEventListener('DOMContentLoaded', () => document.body.appendChild(msgDiv)); } msgDiv.textContent = message; msgDiv.classList.toggle('error', isError); msgDiv.classList.add('visible'); clearTimeout(messageTimeout); messageTimeout = setTimeout(() => { if (msgDiv) msgDiv.classList.remove('visible'); }, 2500); }
  233. copyButton.addEventListener('click', (e) => { e.stopPropagation(); const htmlContent = textarea.innerHTML; const textToCopy = convertHtmlToPlainTextWithNewlines(htmlContent); if (textToCopy) { navigator.clipboard.writeText(textToCopy).then(() => flashMessage("笔记已复制!")).catch(err => { console.error('GoodNote: 复制失败', err); flashMessage("复制失败", true); }); } else { flashMessage("笔记为空", true); } });
  234. cutButton.addEventListener('click', (e) => { e.stopPropagation(); performCutNoteAction(); });
  235.  
  236. // --- Drag Logic ---
  237. let isDragging = false; let dragOffsetX, dragOffsetY;
  238. function dragStart(e) { if (e.button === 0 && (e.target === noteIcon || noteIcon.contains(e.target))) { isDragging = true; const rect = noteIcon.getBoundingClientRect(); dragOffsetX = e.clientX - rect.left; dragOffsetY = e.clientY - rect.top; noteIcon.style.cursor = 'grabbing'; noteIcon.style.transition = 'none'; e.preventDefault(); } }
  239. function drag(e) { if (isDragging) { let newX = e.clientX - dragOffsetX; let newY = e.clientY - dragOffsetY; const iconWidth = noteIcon.offsetWidth; const iconHeight = noteIcon.offsetHeight; newX = Math.max(0, Math.min(newX, window.innerWidth - iconWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - iconHeight)); noteIcon.style.left = `${newX}px`; noteIcon.style.top = `${newY}px`; noteIcon.style.right = ''; noteIcon.style.bottom = ''; } }
  240. function dragEnd(e) { if (isDragging) { isDragging = false; noteIcon.style.cursor = 'move'; noteIcon.style.transition = 'transform 0.15s ease, background-color 0.2s ease, opacity 0.2s ease'; GM_setValue(positionKey, { top: noteIcon.style.top, left: noteIcon.style.left }); } }
  241. noteIcon.addEventListener('mousedown', dragStart);
  242. document.addEventListener('mousemove', drag);
  243. document.addEventListener('mouseup', dragEnd);
  244.  
  245. // --- Load Icon Position ---
  246. function loadIconPosition() { const savedPosition = GM_getValue(positionKey, null); if (savedPosition && typeof savedPosition.left === 'string' && typeof savedPosition.top === 'string') { noteIcon.style.left = savedPosition.left; noteIcon.style.top = savedPosition.top; noteIcon.style.right = ''; noteIcon.style.bottom = ''; } else { setDefaultIconPosition(); if (savedPosition) GM_setValue(positionKey, null); } }
  247. function setDefaultIconPosition() { noteIcon.style.top = '20px'; noteIcon.style.right = '20px'; noteIcon.style.left = ''; noteIcon.style.bottom = ''; }
  248.  
  249. // --- Note Visibility Logic ---
  250. let isVisible = false; let isPinned = false; let hoverTimeout;
  251. pinButton.addEventListener('click', (e) => { e.stopPropagation(); isPinned = !isPinned; noteContainer.classList.toggle('pinned', isPinned); pinButton.title = isPinned ? '取消置顶笔记' : '置顶笔记'; if (isPinned) { flashMessage("笔记已置顶"); if (!isVisible) toggleNote(true); clearTimeout(hoverTimeout); } else { flashMessage("笔记已取消置顶"); handleMouseLeave(); } });
  252.  
  253. // --- Action Functions (Cut/Paste) ---
  254. async function performCutNoteAction() { const htmlContent = textarea.innerHTML; const textToCopy = convertHtmlToPlainTextWithNewlines(htmlContent); if (textToCopy) { try { await navigator.clipboard.writeText(textToCopy); textarea.innerHTML = ''; saveNote(); flashMessage("笔记已剪切!"); return true; } catch (err) { console.error('GoodNote: 剪切失败', err); flashMessage("剪切失败", true); return false; } } else { flashMessage("笔记为空", true); return false; } }
  255. async function performPasteAction() { const activeElement = document.activeElement; const isEditable = activeElement && (activeElement.isContentEditable || activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA'); if (isEditable) { try { const text = await navigator.clipboard.readText(); if (!text) { flashMessage("剪贴板为空", true); return false; } if (activeElement.isContentEditable) { document.execCommand('insertText', false, text); if (activeElement === textarea) textarea.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } else { const start = activeElement.selectionStart; const end = activeElement.selectionEnd; const originalValue = activeElement.value; activeElement.value = originalValue.substring(0, start) + text + originalValue.substring(end); const newCursorPos = start + text.length; activeElement.selectionStart = newCursorPos; activeElement.selectionEnd = newCursorPos; activeElement.dispatchEvent(new Event('input', { bubbles: true, cancelable: true })); } return true; } catch (err) { if (err.name === 'NotAllowedError') flashMessage('需要剪贴板读取权限', true); else { console.error('GoodNote: 粘贴失败', err); flashMessage('粘贴失败', true); } return false; } } else { flashMessage("当前光标位置不可粘贴", true); return false; } }
  256.  
  257. // --- Keyboard Shortcuts ---
  258. document.addEventListener('keydown', async (e) => { if (e.ctrlKey && e.altKey && e.key && e.key.toLowerCase() === 'p') { e.preventDefault(); performCutNoteAction(); return; } if (e.ctrlKey && e.altKey && e.key && e.key.toLowerCase() === 'o') { e.preventDefault(); performPasteAction(); return; } if (e.ctrlKey && e.altKey && e.key && e.key.toLowerCase() === 'v') { e.preventDefault(); try { const cutSuccess = await performCutNoteAction(); if (cutSuccess) await performPasteAction(); } catch (error) { console.error("GoodNote: Ctrl+Alt+V 操作失败:", error); flashMessage("剪切粘贴操作失败", true); } return; } if (e.key === 'Escape' && isVisible && !isPinned) { if (textarea.contains(document.activeElement)) { toggleNote(); e.preventDefault(); return; } const targetTagName = e.target.tagName; const isInputFocused = e.target.isContentEditable || ['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTagName); if (!isInputFocused && !selectionPopup) { toggleNote(); e.preventDefault(); return; } } const targetTagNameGlobal = e.target.tagName; if (!e.target.isContentEditable && !['INPUT', 'TEXTAREA', 'SELECT'].includes(targetTagNameGlobal)) { const isMac = /Mac|iPod|iPhone|iPad/.test(navigator.platform); if ((isMac ? e.metaKey : e.ctrlKey) && e.shiftKey && e.key && e.key.toLowerCase() === 'm') { e.preventDefault(); toggleNote(); return; } } });
  259.  
  260. // --- Icon Click/Hover/Leave Logic ---
  261. noteIcon.addEventListener('click', (e) => { if (!isDragging && !isTouchDragging) toggleNote(); });
  262. noteIcon.addEventListener('mouseenter', () => { clearTimeout(hoverTimeout); if (!isDragging && !isTouchDragging && !isVisible && !isPinned) toggleNote(true); });
  263. function handleMouseLeave() { clearTimeout(hoverTimeout); if (isVisible && !isPinned) { hoverTimeout = setTimeout(() => { const activeElement = document.activeElement; if (!isVisible || isPinned || noteContainer.contains(activeElement)) return; if (!noteIcon.matches(':hover') && !noteContainer.matches(':hover')) toggleNote(); }, 600); } }
  264. noteIcon.addEventListener('mouseleave', handleMouseLeave);
  265. noteContainer.addEventListener('mouseleave', handleMouseLeave);
  266. noteContainer.addEventListener('mouseenter', () => clearTimeout(hoverTimeout));
  267. document.addEventListener('click', (e) => { if (isVisible && !isPinned && !noteContainer.contains(e.target) && !noteIcon.contains(e.target) && (!selectionPopup || !selectionPopup.contains(e.target))) { clearTimeout(hoverTimeout); toggleNote(); } }, true);
  268.  
  269. // --- Touch Drag Logic ---
  270. let touchStartX, touchStartY, touchInitialX, touchInitialY; let isTouchDragging = false; let touchStartTime = 0; let touchMoveDistance = 0; let touchHasDragged = false;
  271. noteIcon.addEventListener('touchstart', (e) => { if (e.touches.length === 1) { const touch = e.touches[0]; isTouchDragging = true; isDragging = false; touchHasDragged = false; const rect = noteIcon.getBoundingClientRect(); touchStartX = touch.clientX; touchStartY = touch.clientY; touchInitialX = rect.left; touchInitialY = rect.top; touchStartTime = Date.now(); touchMoveDistance = 0; noteIcon.style.transition = 'none'; } }, { passive: true });
  272. noteIcon.addEventListener('touchmove', (e) => { if (!isTouchDragging || e.touches.length !== 1) return; const touch = e.touches[0]; const dx = touch.clientX - touchStartX; const dy = touch.clientY - touchStartY; touchMoveDistance = Math.sqrt(dx * dx + dy * dy); if (touchMoveDistance > 10) { touchHasDragged = true; let newX = touchInitialX + dx; let newY = touchInitialY + dy; const iconWidth = noteIcon.offsetWidth; const iconHeight = noteIcon.offsetHeight; newX = Math.max(0, Math.min(newX, window.innerWidth - iconWidth)); newY = Math.max(0, Math.min(newY, window.innerHeight - iconHeight)); noteIcon.style.left = `${newX}px`; noteIcon.style.top = `${newY}px`; noteIcon.style.right = ''; noteIcon.style.bottom = ''; if (e.cancelable) e.preventDefault(); } }, { passive: false });
  273. noteIcon.addEventListener('touchend', (e) => { if (!isTouchDragging) return; noteIcon.style.transition = 'transform 0.15s ease, background-color 0.2s ease, opacity 0.2s ease'; const duration = Date.now() - touchStartTime; if (touchHasDragged) { GM_setValue(positionKey, { top: noteIcon.style.top, left: noteIcon.style.left }); } else if (duration < 300) toggleNote(); isTouchDragging = false; touchHasDragged = false; });
  274.  
  275. // --- Polling Sync Logic ---
  276. let pollingInterval = null; const POLLING_INTERVAL_MS = 800;
  277. function checkAndUpdateNoteViaPolling() { if (document.activeElement !== textarea) { const storedNote = GM_getValue(storageKey, ''); if (textarea.innerHTML !== storedNote) textarea.innerHTML = storedNote; } }
  278. function startPolling() { if (pollingInterval === null && !isVisible) { checkAndUpdateNoteViaPolling(); pollingInterval = setInterval(checkAndUpdateNoteViaPolling, POLLING_INTERVAL_MS); } }
  279. function stopPolling() { if (pollingInterval !== null) { clearInterval(pollingInterval); pollingInterval = null; } }
  280.  
  281. // --- Modified toggleNote (No container animation at all) ---
  282. function toggleNote(forceOpen = false) {
  283. const shouldBeVisible = forceOpen || !isVisible;
  284. if (shouldBeVisible) { // Opening
  285. if (isVisible && !forceOpen) return;
  286. stopPolling();
  287. const storedNoteBeforeShow = GM_getValue(storageKey, '');
  288. if (textarea.innerHTML !== storedNoteBeforeShow) textarea.innerHTML = storedNoteBeforeShow;
  289. const iconRect = noteIcon.getBoundingClientRect();
  290. const padding = 15;
  291. const containerStyle = getComputedStyle(noteContainer);
  292. const containerMinWidth = parseInt(containerStyle.minWidth) || 380;
  293. const containerMinHeight = 300;
  294.  
  295. let left = iconRect.right + padding;
  296. let top = Math.max(padding, iconRect.top);
  297. if (left + containerMinWidth > window.innerWidth - padding) left = iconRect.left - containerMinWidth - padding;
  298. left = Math.max(padding, left);
  299. const estimatedHeight = Math.max(containerMinHeight, noteContainer.offsetHeight);
  300. if (top + estimatedHeight > window.innerHeight - padding) top = window.innerHeight - estimatedHeight - padding;
  301. top = Math.max(padding, top);
  302. noteContainer.style.top = `${top}px`;
  303. noteContainer.style.left = `${left}px`;
  304.  
  305. noteContainer.style.display = 'block';
  306. noteContainer.classList.add('active');
  307.  
  308. setTimeout(() => {
  309. if (!selectionPopup && noteContainer.classList.contains('active')) {
  310. textarea.focus();
  311. try { const range = document.createRange(); const sel = window.getSelection(); range.selectNodeContents(textarea); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); }
  312. catch(e) { console.warn("GoodNote: Setting cursor position failed.", e); }
  313. }
  314. }, 50);
  315.  
  316. isVisible = true;
  317. } else { // Closing
  318. if (!isVisible || isPinned) return;
  319. noteContainer.style.display = 'none';
  320. noteContainer.classList.remove('active');
  321. isVisible = false;
  322. startPolling();
  323. }
  324. }
  325.  
  326. // --- Initialization ---
  327. console.log("GoodNote Final v6 Initializing...");
  328. loadNote();
  329. loadIconPosition();
  330. startPolling();
  331.  
  332. })();
  333. // --- END OF SCRIPT ---