// ==UserScript== // @name 磁吸笔记 // @namespace https://bbs.tampermonkey.net.cn/ // @version 0.4.0 // @description 右侧侧吸笔记卡片,可新建和切换,内容自动保存;初始收起,标签可删除。 // @author huanxue // @match *://*/* // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @grant GM_deleteValue // ==/UserScript== (() => { 'use strict'; const STORAGE_META = 'notes_meta'; const NOTE_PREFIX = 'note_'; const getMeta = () => JSON.parse(GM_getValue(STORAGE_META, '[]')); const saveMeta = (m) => GM_setValue(STORAGE_META, JSON.stringify(m)); const getContent = (id) => GM_getValue(NOTE_PREFIX + id, ''); const saveContent = (id, c) => GM_setValue(NOTE_PREFIX + id, c); const delContent = (id) => GM_deleteValue(NOTE_PREFIX + id); const debounce = (fn, d = 300) => { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), d); }; }; let sidebar, btnToggle, btnNew, noteTabs, noteEditor, headerBar; let currentId = null; let isDragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0; let initialized = false; const injectStyle = () => GM_addStyle(` #noteSidebar { position: fixed; top: 100px; right: 0; width: 300px; background: #fafafa; border-left: 1px solid #d0d0d0; box-shadow: -2px 0 6px rgba(0,0,0,.12); font-family: system-ui, sans-serif; z-index: 2147483647; transition: width .2s ease, opacity .2s ease; user-select: none; } #noteSidebar.minimized { width: 40px; } #noteSidebar header { display: flex; align-items: center; gap: 6px; padding: 6px 8px; background: #f0f0f0; border-bottom: 1px solid #d0d0d0; cursor: move; } #noteSidebar header button { padding: 4px 6px; font-size: 12px; cursor: pointer; } #noteSidebar.minimized header span, #noteSidebar.minimized #btnNew, #noteSidebar.minimized #noteTabs, #noteSidebar.minimized #noteEditor { display: none !important; } #noteTabs { display: flex; flex-wrap: wrap; gap: 4px; padding: 6px; max-height: 120px; overflow-y: auto; } .note-tab { position: relative; cursor: pointer; background: #e0e0e0; border-radius: 4px; padding: 4px 18px 4px 6px; font-size: 12px; } .note-tab.active { background: #c0c0c0; font-weight: 600; } .note-close { position: absolute; right: 4px; top: 1px; cursor: pointer; font-weight: 700; line-height: 12px; } .note-close:hover { color: #d00; } #noteEditor { width: 100%; height: 200px; border: none; resize: vertical; padding: 8px; box-sizing: border-box; font-size: 13px; line-height: 1.4; border-top: 1px solid #d0d0d0; background: #fff; outline: none; } `); const initSidebar = () => { if (initialized) return; initialized = true; injectStyle(); sidebar = document.createElement('div'); sidebar.id = 'noteSidebar'; sidebar.className = 'minimized'; sidebar.innerHTML = `
Notes
`; document.body.appendChild(sidebar); btnToggle = sidebar.querySelector('#btnToggle'); btnNew = sidebar.querySelector('#btnNew'); noteTabs = sidebar.querySelector('#noteTabs'); noteEditor = sidebar.querySelector('#noteEditor'); headerBar = sidebar.querySelector('#noteHeader'); const onMouseDown = (e) => { if (e.button !== 0 || e.target.tagName === 'BUTTON') return; isDragging = true; startX = e.clientX; startY = e.clientY; const r = sidebar.getBoundingClientRect(); startLeft = r.left; startTop = r.top; document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; const onMouseMove = (e) => { if (!isDragging) return; const dx = e.clientX - startX, dy = e.clientY - startY; sidebar.style.left = startLeft + dx + 'px'; sidebar.style.top = Math.max(0, startTop + dy) + 'px'; sidebar.style.right = 'auto'; }; const onMouseUp = () => { if (!isDragging) return; isDragging = false; document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); const r = sidebar.getBoundingClientRect(); const vw = window.innerWidth, margin = 20; if (r.left <= margin) { sidebar.style.left = '0px'; sidebar.style.right = 'auto'; ensureMinimized(true); } else if (vw - r.right <= margin) { sidebar.style.left = 'auto'; sidebar.style.right = '0px'; ensureMinimized(true); } }; headerBar.addEventListener('mousedown', onMouseDown); btnToggle.addEventListener('click', () => ensureMinimized(!sidebar.classList.contains('minimized'))); btnNew.addEventListener('click', () => { const name = (prompt('新建卡片名称?', 'Note ' + (getMeta().length + 1)) || '').trim(); if (!name) return; const id = Date.now().toString(); const meta = getMeta(); meta.push({ id, name }); saveMeta(meta); openNote(id); }); noteEditor.addEventListener('input', debounce(() => { if (currentId) saveContent(currentId, noteEditor.value); }, 500)); let meta = getMeta(); if (!meta.length) { const id = Date.now().toString(); meta = [{ id, name: 'Note 1' }]; saveMeta(meta); } openNote(meta[0].id); sidebar.style.top = '100px'; sidebar.style.left = 'auto'; sidebar.style.right = '0px'; }; const ensureMinimized = (wantMinimize) => { if (wantMinimize) { sidebar.classList.add('minimized'); btnToggle.textContent = '☝️'; btnToggle.title = '展开'; } else { sidebar.classList.remove('minimized'); btnToggle.textContent = '👐'; btnToggle.title = '收起'; } }; const renderTabs = () => { noteTabs.innerHTML = ''; for (const { id, name } of getMeta()) { const tab = document.createElement('div'); tab.className = 'note-tab' + (id === currentId ? ' active' : ''); tab.innerHTML = `${name}`; tab.addEventListener('click', () => openNote(id)); tab.querySelector('.note-close').addEventListener('click', (e) => { e.stopPropagation(); deleteNote(id); }); noteTabs.appendChild(tab); } }; const openNote = (id) => { currentId = id; noteEditor.value = getContent(id); renderTabs(); }; const deleteNote = (id) => { let meta = getMeta().filter(v => v.id !== id); saveMeta(meta); delContent(id); if (currentId === id) { if (meta.length) openNote(meta[0].id); else { const nid = Date.now().toString(); meta = [{ id: nid, name: 'Note 1' }]; saveMeta(meta); openNote(nid); } } else renderTabs(); }; const handleVisibility = () => { if (document.visibilityState === 'visible') { if (!initialized) initSidebar(); else sidebar.style.display = 'block'; } else if (initialized) { sidebar.style.display = 'none'; } }; document.addEventListener('visibilitychange', handleVisibility); handleVisibility(); })();