// ==UserScript== // @name Infinity Pro script // @namespace https://biji-20f.pages.dev/infinity-pro-script // @version 1.3.14 // @description 你的笔记助手,一键调用 · 官网同步。支持从 Infinity Pro / Supabase 同步 notes,本地模板,自定义笔记,兼容豆包/ChatGPT 输入框。 // @author ChatGPT // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImciIHgxPSIxNiIgeTE9IjE2IiB4Mj0iMTEyIiB5Mj0iMTEyIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzAyMDYxNyIvPjxzdG9wIG9mZnNldD0iLjU4IiBzdG9wLWNvbG9yPSIjNGY0NmU1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDZiNmQ0Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiIHJ4PSIzNiIgZmlsbD0idXJsKCNnKSIvPjxwYXRoIGQ9Ik0zNiA2NmMwLTEzIDktMjMgMjEtMjMgOCAwIDE1IDUgMjIgMTMgNy04IDE0LTEzIDIyLTEzIDEyIDAgMjEgMTAgMjEgMjNzLTkgMjMtMjEgMjNjLTggMC0xNS01LTIyLTEzLTcgOC0xNCAxMy0yMiAxMy0xMiAwLTIxLTEwLTIxLTIzWm0xOCAwYzAgNCAzIDcgNyA3IDQgMCA4LTMgMTMtNy01LTUtOS03LTEzLTctNCAwLTcgMy03IDdabTUwIDBjMC00LTMtNy03LTctNCAwLTggMi0xMyA3IDUgNCA5IDcgMTMgNyA0IDAgNy0zIDctN1oiIGZpbGw9IndoaXRlIi8+PC9zdmc+ // @icon64 data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMjggMTI4Ij48ZGVmcz48bGluZWFyR3JhZGllbnQgaWQ9ImciIHgxPSIxNiIgeTE9IjE2IiB4Mj0iMTEyIiB5Mj0iMTEyIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSI+PHN0b3Agc3RvcC1jb2xvcj0iIzAyMDYxNyIvPjxzdG9wIG9mZnNldD0iLjU4IiBzdG9wLWNvbG9yPSIjNGY0NmU1Ii8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjMDZiNmQ0Ii8+PC9saW5lYXJHcmFkaWVudD48L2RlZnM+PHJlY3Qgd2lkdGg9IjEyOCIgaGVpZ2h0PSIxMjgiIHJ4PSIzNiIgZmlsbD0idXJsKCNnKSIvPjxwYXRoIGQ9Ik0zNiA2NmMwLTEzIDktMjMgMjEtMjMgOCAwIDE1IDUgMjIgMTMgNy04IDE0LTEzIDIyLTEzIDEyIDAgMjEgMTAgMjEgMjNzLTkgMjMtMjEgMjNjLTggMC0xNS01LTIyLTEzLTcgOC0xNCAxMy0yMiAxMy0xMiAwLTIxLTEwLTIxLTIzWm0xOCAwYzAgNCAzIDcgNyA3IDQgMCA4LTMgMTMtNy01LTUtOS03LTEzLTctNCAwLTcgMy03IDdabTUwIDBjMC00LTMtNy03LTctNCAwLTggMi0xMyA3IDUgNCA5IDcgMTMgNyA0IDAgNy0zIDctN1oiIGZpbGw9IndoaXRlIi8+PC9zdmc+ // @match *://*/* // @run-at document-idle // @grant GM_getValue // @grant GM_setValue // @grant GM_addStyle // @grant GM_setClipboard // ==/UserScript== // ========================= // 🌟 UI REDESIGN (Behance style) // ========================= (function(){ const style = document.createElement('style'); style.textContent = ` :root { --bg: #0f172a; --panel: #111827; --card: #1f2937; --text: #e5e7eb; --sub: #9ca3af; --accent: #6366f1; --accent2: #22c55e; --radius: 16px; --shadow: 0 10px 30px rgba(0,0,0,0.3); } .ips-panel { position: fixed; inset: 40px; background: var(--panel); border-radius: var(--radius); box-shadow: var(--shadow); display: flex; overflow: hidden; font-family: Inter, system-ui, -apple-system, "Segoe UI", Arial, "Microsoft YaHei", sans-serif !important; color: var(--text); } .ips-side { width: 260px; background: rgba(255,255,255,0.02); padding: 16px; border-right: 1px solid rgba(255,255,255,0.05); overflow-y: auto; } .ips-main { flex: 1; padding: 24px; overflow-y: auto; } .ips-card { background: var(--card); border-radius: 14px; padding: 16px; margin-bottom: 14px; transition: 0.25s; } .ips-card:hover { transform: translateY(-3px); box-shadow: var(--shadow); } .ips-card h3 { margin: 0 0 6px; font-size: 16px; color: var(--text); } .ips-card-content { color: var(--sub); font-size: 13px; line-height: 1.5; } .ips-btn { background: rgba(255,255,255,0.05); border: none; padding: 8px 14px; border-radius: 10px; color: var(--text); cursor: pointer; } .ips-btn.primary { background: linear-gradient(135deg,var(--accent),#8b5cf6); } .ips-btn:hover { opacity: 0.9; } .ips-fab { position: fixed; right: 0px; bottom: 20px; width: 64px; height: 64px; border-radius: 20px; background: linear-gradient(135deg,#6366f1,#22c55e); color: #fff; border: none; box-shadow: var(--shadow); cursor: pointer; z-index: 2147483647; } .ips-search { width: 100%; padding: 10px; border-radius: 10px; background: rgba(255,255,255,0.05); border: none; color: var(--text); margin-bottom: 16px; } `; document.head.appendChild(style); })(); (function () { 'use strict'; const APP_NAME = 'Infinity Pro script'; const APP_SUBTITLE = '你的笔记助手,一键调用 · 官网同步'; const OFFICIAL_SITE = 'https://biji-20f.pages.dev/'; const SUPABASE_URL = 'https://nawiybboebkbzdqugucj.supabase.co'; const SUPABASE_KEY = 'sb_publishable_m59aowd-MCQMI6Kvjgw02A_5G1z4TKN'; const STORE_KEY = 'infinity_pro_script_store_v131'; const SESSION_KEY = 'infinity_pro_script_supabase_session_v131'; const SETTINGS_KEY = 'infinity_pro_script_settings_v131'; let store = load(STORE_KEY, null) || createDefaultStore(); let session = load(SESSION_KEY, null); normalizeStoreForDisplay(); let settings = load(SETTINGS_KEY, { opened: { sync: true, local: true }, active: 'local-writing', fab: null }); let ui = { open: false, syncOpen: false, query: '', composing: false, editor: null, catDrawer: false, modal: null, selected: {}, dragCat: null, sideScroll: 0, mainScroll: 0 }; let lastEditable = null; function createDefaultStore() { return { sync: { rootName: '网站同步笔记', categories: [], lastSyncAt: null, count: 0 }, local: { rootName: '本地笔记', categories: [ { id: 'local-writing', name: '写作模板', items: [ item('写作润色', '请帮我润色下面这段内容,使它更清晰、自然、有逻辑,保持原意,不要过度扩写:\n\n{{text}}', ['写作', '润色'], true), item('小红书文案', '请根据以下主题写一篇小红书风格文案,要求:标题吸引人、分点清晰、语气自然、有表情符号、最后有互动引导。\n\n主题:{{text}}', ['文案', '小红书'], false), item('短视频脚本', '请把以下内容改成 60 秒短视频脚本,包含:开头钩子、主体分镜、旁白、字幕重点、结尾引导。\n\n{{text}}', ['视频', '脚本'], false), item('邮件优化', '请把下面这封邮件改得更专业、礼貌、简洁,并保留核心诉求:\n\n{{text}}', ['邮件'], false) ] }, { id: 'local-code', name: '代码模板', items: [ item('代码审查', '请审查以下代码,重点检查:bug、边界条件、性能、可读性、安全性,并给出可直接应用的修改建议:\n\n```\n{{text}}\n```', ['代码', '审查'], true), item('解释代码', '请逐段解释下面的代码,说明它的作用、执行流程、关键变量和可能的坑:\n\n```\n{{text}}\n```', ['代码', '解释'], false), item('报错排查', '请帮我分析这个报错,给出原因、排查步骤和修复方案:\n\n{{text}}', ['debug'], false) ] }, { id: 'local-study', name: '学习办公', items: [ item('会议纪要', '请根据下面内容整理会议纪要,包含:会议主题、核心结论、待办事项、负责人、截止时间、风险点:\n\n{{text}}', ['会议'], false), item('论文摘要', '请根据以下内容生成论文摘要,要求:研究背景、方法、结果、结论清晰,语言正式:\n\n{{text}}', ['论文'], false), item('知识卡片', '请把以下内容整理成知识卡片,包含:核心概念、关键点、例子、易错点、复习问题:\n\n{{text}}', ['学习'], false) ] } ] } }; } function item(title, content, tags = [], starred = false) { return { id: uid(), title, content, tags, starred, source: 'local', updated_at: new Date().toISOString() }; } function uid() { return (crypto && crypto.randomUUID) ? crypto.randomUUID() : 'id_' + Math.random().toString(36).slice(2) + Date.now(); } function load(key, fallback) { try { const v = GM_getValue(key); if (v === undefined || v === null || v === '') return fallback; return typeof v === 'string' ? JSON.parse(v) : v; } catch (e) { return fallback; } } function save(key, value) { GM_setValue(key, JSON.stringify(value)); } function saveAll() { save(STORE_KEY, store); save(SETTINGS_KEY, settings); } init(); function init() { addStyles(); mount(); trackInputs(); document.addEventListener('keydown', e => { if (e.altKey && e.key.toLowerCase() === 'p') { ui.open = !ui.open; render(); } if (e.key === 'Escape' && ui.open) { ui.open = false; render(); } }); } function ipsHost() { return document.getElementById('ips-host'); } function ipsShadow() { const host = ipsHost(); return host && host.shadowRoot ? host.shadowRoot : document; } function ipsRoot() { const shadow = ipsShadow(); return shadow ? shadow.getElementById('ips-root') : null; } function q(sel) { const root = ipsRoot(); return root ? root.querySelector(sel) : null; } function qa(sel) { const root = ipsRoot(); return root ? Array.from(root.querySelectorAll(sel)) : []; } function injectShadowStyles(shadow) { if (!shadow || shadow.getElementById('ips-shadow-style')) return; const style = document.createElement('style'); style.id = 'ips-shadow-style'; style.textContent = ':host { all: initial; }\n' + (window.__IPS_CSS || ''); shadow.appendChild(style); } function mount() { if (document.getElementById('ips-host')) return; const host = document.createElement('div'); host.id = 'ips-host'; host.style.cssText = 'all: initial !important; position: fixed !important; inset: 0 !important; pointer-events: none !important; z-index: 2147483647 !important; contain: layout style !important;'; document.documentElement.appendChild(host); const shadow = host.attachShadow({ mode: 'open' }); injectShadowStyles(shadow); const root = document.createElement('div'); root.id = 'ips-root'; root.style.cssText = 'all: initial !important; pointer-events: auto !important; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial, "Microsoft YaHei", sans-serif !important; color: #0f172a !important;'; shadow.appendChild(root); render(); } function render() { const root = ipsRoot(); if (!root) return; const oldSide = root.querySelector('.ips-side'); const oldMain = root.querySelector('.ips-main'); if (oldSide) ui.sideScroll = oldSide.scrollTop || ui.sideScroll || 0; if (oldMain) ui.mainScroll = oldMain.scrollTop || ui.mainScroll || 0; const activeList = getActiveList(); const filtered = filterItems(activeList.items || []); const total = countAllItems(); root.innerHTML = `
${APP_NAME}
${APP_SUBTITLE}
网站同步笔记
登录后可把官网笔记同步到这里,脚本只保存登录状态,不保存密码。
当前缓存:${store.sync.count || 0} 条;最后同步:${store.sync.lastSyncAt ? new Date(store.sync.lastSyncAt).toLocaleString() : '尚未同步'}
${session && session.access_token ? `
登录状态:${esc(session.user && session.user.email ? session.user.email : '已登录')}
` : `
脚本不保存密码,只保存 Supabase 登录 token。不要输入 service_role key。
`}
${renderEditor()}
${esc(activeList.path || '全部')}${ui.query ? ' · 搜索结果' : ''}
${filtered.length} / ${activeList.items.length || total}
${renderBulkBar(filtered)} ${filtered.length ? filtered.map(renderCard).join('') : `
${ui.query ? '没有匹配内容' : '这个分类暂无内容'}
`}
${renderModal()} `; bind(); } function renderTree() { const syncOpen = settings.opened.sync !== false; const localOpen = settings.opened.local !== false; const syncCount = store.sync.count || countCats(store.sync.categories); const localCount = countCats(store.local.categories); return `
${(store.sync.categories || []).map(c => renderCat('sync', c)).join('') || '
未同步
'}
${(store.local.categories || []).map((c, i) => renderCat('local', c, i)).join('')}
`; } function renderCat(root, c, index = 0) { const active = settings.active === c.id; const canEdit = root === 'local'; const cats = store.local.categories || []; return `
${canEdit ? `
` : ''}
`; } function renderCard(n) { const local = n.source !== 'sync'; const contentPreview = esc(n.content || '').replace(/\n/g, '
'); return `

${n.starred ? '★ ' : ''}${esc(n.title || '无标题')}

${esc(n.category || '')}${n.updated_at ? ' · ' + esc(formatDate(n.updated_at)) : ''}
${local ? `` : ''} ${local ? `` : ''}
${contentPreview || '内容为空'}
${(n.tags || []).map(t => `${esc(t)}`).join('')}
`; } function renderBulkBar(items) { const selectedIds = selectedIdsVisibleOrAll(); const cats = store.local.categories || []; if (!selectedIds.length) return ''; return `
已选 ${selectedIds.length} 条可批量移动或删除本地笔记
`; } function renderModal() { const m = ui.modal; if (!m) return ''; return `
${esc(m.title || '操作')}
`; } function renderEditor() { if (!ui.editor) return ''; const e = ui.editor; const title = e.id ? '编辑笔记' : '新建笔记'; const cats = store.local.categories || []; const catOptions = cats.map(c => ` `).join(''); return `
${title} ${e.id ? '修改内容后保存' : '默认创建到当前选中的分类'}
`; } function bind() { const root = ipsRoot(); if (!root) return; const fab = root.querySelector('.ips-fab'); if (fab) bindFab(fab); const side = root.querySelector('.ips-side'); const main = root.querySelector('.ips-main'); if (side) side.addEventListener('scroll', () => { ui.sideScroll = side.scrollTop || 0; }, { passive: true }); if (main) main.addEventListener('scroll', () => { ui.mainScroll = main.scrollTop || 0; }, { passive: true }); const search = root.querySelector('[data-role="search"]'); if (search) { search.addEventListener('click', e => e.stopPropagation()); search.addEventListener('mousedown', e => e.stopPropagation()); search.addEventListener('compositionstart', () => { ui.composing = true; }); search.addEventListener('compositionend', e => { ui.composing = false; ui.query = e.target.value; renderKeepFocus('search'); }); search.addEventListener('input', e => { ui.query = e.target.value; clearTimeout(window.__ips_search_timer); if (!ui.composing) window.__ips_search_timer = setTimeout(() => renderKeepFocus('search'), 120); }); } root.querySelectorAll('[data-role^="edit-"]').forEach(el => { el.addEventListener('input', e => { if (!ui.editor) return; const role = e.target.dataset.role; if (role === 'edit-title') ui.editor.title = e.target.value; if (role === 'edit-content') ui.editor.content = e.target.value; if (role === 'edit-tags') ui.editor.tagsText = e.target.value; }); }); const modalInput = root.querySelector('[data-role="modal-input"]'); if (modalInput) { modalInput.focus(); modalInput.addEventListener('input', e => { if (ui.modal) ui.modal.value = e.target.value; }); modalInput.addEventListener('keydown', e => { if (e.key === 'Enter') confirmModal(); }); } root.querySelectorAll('.ips-cat-row[draggable="true"]').forEach(row => bindCatDrag(row)); root.querySelectorAll('[data-a]').forEach(el => { const a = el.dataset.a; if (el.tagName === 'BUTTON') el.addEventListener('mousedown', e => e.preventDefault()); el.addEventListener('click', e => handleAction(e.currentTarget)); }); } function renderKeepFocus(role) { render(); requestAnimationFrame(() => { const el = q(`[data-role="${role}"]`); if (el) { el.focus(); try { el.setSelectionRange(el.value.length, el.value.length); } catch (e) {} } }); } function fabStyle() { const p = settings.fab; if (p && Number.isFinite(p.top)) { return `right:0px; top:${p.top}px; left:auto; bottom:auto; border-radius:18px 0 0 18px;`; } return 'right:0px; bottom:18px; left:auto; border-radius:18px 0 0 18px;'; } function bindFab(fab) { let moved = false; let startY = 0, startTop = 0; function lockRightEdge() { fab.style.left = 'auto'; fab.style.right = '0px'; fab.style.borderRadius = '18px 0 0 18px'; } lockRightEdge(); fab.onpointerdown = e => { moved = false; startY = e.clientY; const r = fab.getBoundingClientRect(); startTop = r.top; fab.setPointerCapture && fab.setPointerCapture(e.pointerId); fab.classList.add('dragging'); lockRightEdge(); }; fab.onpointermove = e => { if (!fab.classList.contains('dragging')) return; const dy = e.clientY - startY; if (Math.abs(dy) > 3) moved = true; const margin = 8; const top = Math.max(margin, Math.min(window.innerHeight - fab.offsetHeight - margin, startTop + dy)); fab.style.top = top + 'px'; fab.style.bottom = 'auto'; lockRightEdge(); }; fab.onpointerup = e => { fab.classList.remove('dragging'); lockRightEdge(); if (moved) { const r = fab.getBoundingClientRect(); settings.fab = { top: Math.round(r.top) }; saveAll(); } else { ui.open = !ui.open; render(); } }; } function handleAction(el) { const a = el.dataset.a; const id = el.dataset.id; const rootName = el.dataset.root; if (a === 'close') { ui.open = false; render(); return; } if (a === 'openSite') { window.open(OFFICIAL_SITE, '_blank', 'noopener,noreferrer'); return; } if (a === 'syncToggle') { ui.syncOpen = !ui.syncOpen; render(); return; } if (a === 'toggleRoot') { settings.opened[rootName] = settings.opened[rootName] === false; saveAll(); render(); return; } if (a === 'selectRoot') { settings.active = rootName === 'sync' ? 'root-sync' : 'root-local'; ui.selected = {}; saveAll(); render(); return; } if (a === 'selectFavorites') { settings.active = 'root-favorites'; ui.editor = null; ui.catDrawer = false; ui.selected = {}; saveAll(); render(); return; } if (a === 'selectCat') { settings.active = id; ui.editor = null; ui.catDrawer = false; ui.selected = {}; saveAll(); render(); return; } if (a === 'login') { loginAndSync(); return; } if (a === 'syncNotes') { syncNotes(); return; } if (a === 'logout') { session = null; save(SESSION_KEY, null); toast('已退出同步账号'); render(); return; } if (a === 'export') { exportStore(); return; } if (a === 'import') { importStore(); return; } if (a === 'clearSearch') { ui.query = ''; render(); return; } if (a === 'selectAllVisible') { selectAllVisible(); return; } if (a === 'clearSelection') { ui.selected = {}; render(); return; } if (a === 'bulkMove') { bulkMove(); return; } if (a === 'bulkDelete') { bulkDelete(); return; } if (a === 'modalCancel') { ui.modal = null; render(); return; } if (a === 'modalConfirm') { confirmModal(); return; } if (a === 'addCategory') { addCategory(); return; } if (a === 'renameCategory') { renameCategory(id); return; } if (a === 'moveCategoryUp') { moveCategory(id, -1); return; } if (a === 'moveCategoryDown') { moveCategory(id, 1); return; } if (a === 'deleteCategory') { deleteCategory(id); return; } if (a === 'addLocal') { openEditor(null); return; } if (a === 'editLocal') { openEditor(findItem(id)); return; } if (a === 'cancelEditor') { ui.editor = null; render(); return; } if (a === 'toggleCatDrawer') { ui.catDrawer = !ui.catDrawer; render(); return; } if (a === 'chooseEditorCategory') { chooseEditorCategory(id); return; } if (a === 'saveEditor') { saveEditor(); return; } if (a === 'deleteLocal') { deleteLocal(id); return; } if (a === 'toggleStar') { toggleStar(id); return; } if (a === 'toggleSelect') { toggleSelect(id, el.checked); return; } const n = findItem(id); if (!n) return; if (a === 'copy') { copyText(applyVars(n.content, n)); toast('已复制'); return; } if (a === 'insert') { const text = applyVars(n.content, n); const ok = insertText(text); if (!ok) copyText(text); toast(ok ? '已插入' : '没找到输入框,已复制'); return; } } function getActiveList() { const active = settings.active || 'local-writing'; if (active === 'root-sync') return { path: '网站同步笔记 / 全部', items: flatten(store.sync.categories) }; if (active === 'root-favorites') return { path: '星标收藏', items: flatten(store.local.categories).filter(n => n.starred) }; if (active === 'root-local') return { path: '本地笔记 / 全部', items: flatten(store.local.categories) }; for (const c of store.sync.categories || []) if (c.id === active) return { path: '网站同步笔记 / ' + c.name, items: c.items || [] }; for (const c of store.local.categories || []) if (c.id === active) return { path: '本地笔记 / ' + c.name, items: c.items || [] }; return { path: '本地笔记 / 全部', items: flatten(store.local.categories) }; } function flatten(cats) { return (cats || []).flatMap(c => (c.items || []).map(n => ({ ...n, category: n.category || c.name }))); } function countCats(cats) { return (cats || []).reduce((sum, c) => sum + (c.items || []).length, 0); } function countFavorites() { return flatten(store.local.categories).filter(n => n.starred).length; } function countAllItems() { return countCats(store.sync.categories) + countCats(store.local.categories); } function isRootActive(root) { return settings.active === (root === 'sync' ? 'root-sync' : 'root-local'); } function filterItems(items) { const q = ui.query.trim().toLowerCase(); const sorted = [...items].sort((a, b) => (Number(!!b.starred) - Number(!!a.starred)) || String(b.updated_at || '').localeCompare(String(a.updated_at || ''))); if (!q) return sorted; return sorted.filter(n => [n.title, n.content, n.category].concat(n.tags || []).join('\n').toLowerCase().includes(q)); } async function loginAndSync() { const root = ipsRoot(); const email = root?.querySelector('[data-role="email"]')?.value.trim(); const password = root?.querySelector('[data-role="password"]')?.value; if (!email || !password) { alert('请输入邮箱和密码'); return; } try { toast('正在登录...'); const res = await fetch(SUPABASE_URL + '/auth/v1/token?grant_type=password', { method: 'POST', headers: { apikey: SUPABASE_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); const json = await safeJson(res); if (!res.ok) throw new Error(json.error_description || json.message || json.msg || '登录失败'); session = { access_token: json.access_token, refresh_token: json.refresh_token, expires_at: json.expires_at || Math.floor(Date.now() / 1000) + (json.expires_in || 3600), user: json.user || { email } }; save(SESSION_KEY, session); await syncNotes(); } catch (err) { console.error('[Infinity Pro script] login error', err); alert('登录失败:' + (err.message || err)); } } async function refreshSessionIfNeeded() { if (!session || !session.refresh_token) return false; const now = Math.floor(Date.now() / 1000); if (session.access_token && session.expires_at && session.expires_at - now > 90) return true; const res = await fetch(SUPABASE_URL + '/auth/v1/token?grant_type=refresh_token', { method: 'POST', headers: { apikey: SUPABASE_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ refresh_token: session.refresh_token }) }); const json = await safeJson(res); if (!res.ok) { session = null; save(SESSION_KEY, null); throw new Error(json.error_description || json.message || '登录已过期,请重新登录'); } session = { access_token: json.access_token, refresh_token: json.refresh_token || session.refresh_token, expires_at: json.expires_at || Math.floor(Date.now() / 1000) + (json.expires_in || 3600), user: json.user || session.user }; save(SESSION_KEY, session); return true; } async function syncNotes() { try { if (!session || !session.access_token) { ui.syncOpen = true; render(); alert('请先登录同步账号'); return; } toast('正在同步 notes...'); await refreshSessionIfNeeded(); const url = SUPABASE_URL + '/rest/v1/notes?select=*&or=(is_deleted.is.null,is_deleted.eq.false)&order=updated_at.desc.nullslast'; const res = await fetch(url, { headers: { apikey: SUPABASE_KEY, Authorization: 'Bearer ' + session.access_token, Accept: 'application/json' } }); const rows = await safeJson(res); if (!res.ok) throw new Error(rows.message || rows.details || rows.hint || '读取 notes 失败'); if (!Array.isArray(rows)) throw new Error('notes 返回数据不是数组'); const converted = notesRowsToCategories(rows); store.sync.categories = converted.categories; store.sync.count = converted.count; store.sync.lastSyncAt = new Date().toISOString(); normalizeStoreForDisplay(); settings.opened.sync = true; settings.active = converted.categories[0] ? converted.categories[0].id : 'root-sync'; saveAll(); toast('同步完成:' + converted.count + ' 条笔记'); render(); if (!converted.count) toast('同步完成,但暂时没有可显示的笔记'); } catch (err) { console.error('[Infinity Pro script] sync error', err); alert('同步失败:' + (err.message || err)); } } function normalizeStoreForDisplay() { try { store.sync = store.sync || { rootName: '网站同步笔记', categories: [], lastSyncAt: null, count: 0 }; store.local = store.local || { rootName: '本地笔记', categories: [] }; store.sync.categories = normalizeCategoriesForDisplay(store.sync.categories, 'sync'); store.local.categories = normalizeCategoriesForDisplay(store.local.categories, 'local'); store.sync.count = countCats(store.sync.categories); } catch (err) { console.warn('[Infinity Pro script] normalize store failed', err); } } function normalizeCategoriesForDisplay(categories, source) { if (!Array.isArray(categories)) return []; return categories.map((c, ci) => { const name = String(c && (c.name || c.title || c.category) || (source === 'sync' ? '未分类' : '默认分类')).trim() || '未分类'; const id = String(c && c.id || (source + '-' + hash(name + '-' + ci))); const rawItems = Array.isArray(c && c.items) ? c.items : (Array.isArray(c && c.notes) ? c.notes : []); return { ...c, id, name, items: rawItems.map((n, ni) => normalizeItemForDisplay(n, source, name, ni)).filter(Boolean) }; }); } function normalizeItemForDisplay(n, sourceHint, categoryHint, indexHint) { if (!n || typeof n !== 'object') return null; const source = n.source || sourceHint || 'local'; const category = String(n.category || n.category_name || categoryHint || '').trim(); const content = normalizeContent(firstValue(n, ['content', 'body', 'text', 'note', 'notes', 'markdown', 'html', 'description', 'raw_content', 'note_content', 'prompt', 'template', 'answer', 'message', 'data', 'payload', 'document', 'json', 'blocks', 'delta'])) || normalizeRowContent(n) || ''; const titleRaw = normalizeContent(firstValue(n, ['title', 'name', 'heading', 'summary', 'subject'])) || titleFromContent(content); return { ...n, id: String(n.id || (source + '-note-' + hash(category + '-' + titleRaw + '-' + indexHint))), title: cleanTitle(titleRaw || '无标题笔记'), content: String(content || ''), category, tags: normalizeTags(n.tags || n.tag || n.keywords || n.labels), starred: n.starred === true || n.is_starred === true || n.favorite === true, source, updated_at: n.updated_at || n.modified_at || n.created_at || '' }; } function notesRowsToCategories(rows) { const map = new Map(); let count = 0; for (const row of rows || []) { if (row.is_deleted === true) continue; const content = normalizeContent(firstValue(row, ['content', 'body', 'text', 'note', 'notes', 'markdown', 'html', 'description', 'raw_content', 'note_content', 'prompt', 'template', 'answer', 'message', 'data', 'payload', 'document', 'json', 'blocks', 'delta'])); const titleRaw = normalizeContent(firstValue(row, ['title', 'name', 'heading', 'summary', 'subject'])); const rowFallback = normalizeRowContent(row); const finalContent = content || rowFallback || titleRaw; if (!finalContent) continue; const cat = String(firstValue(row, ['category', 'category_name', 'folder', 'type', 'group']) || '未分类').trim() || '未分类'; if (!map.has(cat)) map.set(cat, { id: 'sync-' + hash(cat), name: cat, items: [] }); const title = cleanTitle(titleRaw || titleFromContent(finalContent)); map.get(cat).items.push({ id: 'sync-note-' + String(row.id || uid()), raw_id: row.id, title, content: finalContent, category: cat, tags: normalizeTags(firstValue(row, ['tags', 'tag', 'keywords', 'labels'])), starred: row.starred === true || row.is_starred === true || row.favorite === true, source: 'sync', updated_at: row.updated_at || row.modified_at || row.created_at || '' }); count++; } const categories = Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name, 'zh-Hans-CN')); for (const c of categories) { c.items.sort((a, b) => (Number(!!b.starred) - Number(!!a.starred)) || String(b.updated_at || '').localeCompare(String(a.updated_at || ''))); } return { categories, count }; } function firstValue(obj, keys) { for (const k of keys) { if (obj && obj[k] !== undefined && obj[k] !== null && obj[k] !== '') return obj[k]; } return ''; } function normalizeContent(value) { if (value === undefined || value === null) return ''; if (typeof value === 'string') { let s = value.trim(); if (!s) return ''; if ((s.startsWith('{') && s.endsWith('}')) || (s.startsWith('[') && s.endsWith(']'))) { try { return normalizeContent(JSON.parse(s)); } catch (e) {} } return stripHtml(s).trim(); } if (Array.isArray(value)) { return value.map(normalizeContent).filter(Boolean).join('\n'); } if (typeof value === 'object') { const candidates = []; collectText(value, candidates, 0); return candidates.join('\n').trim(); } return String(value).trim(); } function collectText(obj, out, depth) { if (!obj || depth > 6) return; if (typeof obj === 'string') { const s = stripHtml(obj).trim(); if (s) out.push(s); return; } if (Array.isArray(obj)) { obj.forEach(x => collectText(x, out, depth + 1)); return; } if (typeof obj !== 'object') return; for (const k of ['text', 'content', 'value', 'title', 'body', 'html', 'markdown', 'plain_text', 'insert', 'data', 'payload', 'document', 'json', 'delta']) { if (obj[k] !== undefined && obj[k] !== null) collectText(obj[k], out, depth + 1); } if (Array.isArray(obj.children)) collectText(obj.children, out, depth + 1); if (Array.isArray(obj.blocks)) collectText(obj.blocks, out, depth + 1); if (Array.isArray(obj.content)) collectText(obj.content, out, depth + 1); if (Array.isArray(obj.ops)) { obj.ops.forEach(op => collectText(op.insert || op.text || op, out, depth + 1)); } } function normalizeRowContent(row) { if (!row || typeof row !== 'object') return ''; const ignored = new Set(['id', 'user_id', 'created_at', 'updated_at', 'modified_at', 'deleted_at', 'is_deleted', 'category', 'category_name', 'folder', 'type', 'group', 'tags', 'tag', 'keywords', 'labels', 'starred', 'is_starred', 'favorite']); const out = []; for (const [k, v] of Object.entries(row)) { if (ignored.has(k)) continue; if (typeof v === 'string' && v.trim().length < 2) continue; const text = normalizeContent(v); if (text && !out.includes(text)) out.push(text); } return out.join('\n').trim(); } function stripHtml(s) { if (!/[<>]/.test(s)) return s; const div = document.createElement('div'); div.innerHTML = s.replace(//gi, '\n').replace(/<\/p>/gi, '\n'); return div.textContent || div.innerText || s; } function titleFromContent(s) { const line = String(s || '').split(/\n+/).map(x => x.trim()).find(Boolean) || '无标题笔记'; return line; } function cleanTitle(s) { const t = String(s || '无标题笔记').replace(/^#+\s*/, '').replace(/[*_`]/g, '').trim() || '无标题笔记'; return t.length > 42 ? t.slice(0, 42) + '…' : t; } function normalizeTags(v) { if (!v) return []; if (Array.isArray(v)) return v.map(x => String(x).trim()).filter(Boolean).slice(0, 10); if (typeof v === 'object') return Object.values(v).flat().map(x => String(x).trim()).filter(Boolean).slice(0, 10); if (typeof v === 'string') { try { return normalizeTags(JSON.parse(v)); } catch (e) {} return v.split(/[,,#\s]+/).map(x => x.trim()).filter(Boolean).slice(0, 10); } return []; } async function safeJson(res) { try { return await res.json(); } catch (e) { return {}; } } function openEditor(existing) { const cats = store.local.categories || (store.local.categories = []); if (!cats.length) cats.push({ id: 'local-default', name: '默认分类', items: [] }); let cat = null; if (existing) cat = findCategoryByItem(existing.id, 'local') || cats.find(c => c.name === existing.category); if (!cat) cat = cats.find(c => c.id === settings.active) || cats[0]; ui.editor = { id: existing ? existing.id : null, categoryId: cat.id, category: cat.name, title: existing ? (existing.title || '') : '', content: existing ? (existing.content || '') : '', tags: existing ? (existing.tags || []) : [], tagsText: existing && existing.tags ? existing.tags.join(', ') : '' }; ui.catDrawer = false; settings.active = cat.id; render(); requestAnimationFrame(() => q('[data-role="edit-title"]')?.focus()); } function chooseEditorCategory(catId) { if (!ui.editor) return; const cat = (store.local.categories || []).find(c => c.id === catId); if (!cat) return; ui.editor.categoryId = cat.id; ui.editor.category = cat.name; ui.catDrawer = false; render(); } function saveEditor() { if (!ui.editor) return; const e = ui.editor; const root = ipsRoot(); const cats = store.local.categories || (store.local.categories = []); const cat = cats.find(c => c.id === e.categoryId) || cats.find(c => c.name === e.category) || cats[0]; const category = (cat && cat.name || e.category || '').trim(); const title = (root?.querySelector('[data-role="edit-title"]')?.value || e.title || '').trim(); const content = root?.querySelector('[data-role="edit-content"]')?.value || e.content || ''; const tagsText = root?.querySelector('[data-role="edit-tags"]')?.value || e.tagsText || ''; if (!category) { toast('请填写分类'); return; } if (!title) { toast('请填写标题'); return; } if (!content.trim()) { toast('请填写内容'); return; } let targetCat = cat || cats.find(c => c.name === category); if (!targetCat) { targetCat = { id: 'local-' + hash(category), name: category, items: [] }; cats.push(targetCat); } const tags = normalizeTags(tagsText); if (e.id) { const existing = findItem(e.id) || {}; removeLocal(e.id); targetCat.items.unshift({ ...existing, id: e.id, title, content, tags, category: targetCat.name, source: 'local', updated_at: new Date().toISOString() }); } else { targetCat.items.unshift({ id: uid(), title, content, tags, category: targetCat.name, starred: false, source: 'local', updated_at: new Date().toISOString() }); } settings.active = targetCat.id; ui.editor = null; saveAll(); render(); toast('已保存'); } function deleteLocal(itemId) { if (!confirm('确定删除这条本地笔记/模板?')) return; removeLocal(itemId); saveAll(); render(); toast('已删除'); } function removeLocal(itemId) { for (const c of store.local.categories || []) c.items = (c.items || []).filter(n => n.id !== itemId); } function addCategory() { ui.modal = { type: 'addCategory', title: '新建分类', label: '分类名称', value: '' }; render(); } function deleteCategory(catId) { const cats = store.local.categories || []; const cat = cats.find(c => c.id === catId); if (!cat) return; const count = (cat.items || []).length; if (!confirm(count ? `确定删除“${cat.name}”分类和其中 ${count} 条笔记?` : `确定删除“${cat.name}”分类?`)) return; store.local.categories = cats.filter(c => c.id !== catId); if (settings.active === catId) settings.active = store.local.categories[0]?.id || 'root-local'; if (ui.editor && ui.editor.categoryId === catId) ui.editor = null; saveAll(); render(); toast('分类已删除'); } function renameCategory(catId) { const cat = (store.local.categories || []).find(c => c.id === catId); if (!cat) return; ui.modal = { type: 'renameCategory', catId, title: '重命名分类', label: '新的分类名称', value: cat.name }; render(); } function confirmModal() { const m = ui.modal; if (!m) return; const finalName = String(m.value || '').trim(); if (!finalName) { toast('名称不能为空'); return; } const cats = store.local.categories || (store.local.categories = []); if (m.type === 'addCategory') { if (cats.some(c => c.name === finalName)) { toast('分类已存在'); return; } const cat = { id: 'local-' + hash(finalName + Date.now()), name: finalName, items: [] }; cats.push(cat); settings.active = cat.id; ui.editor = null; ui.modal = null; saveAll(); render(); toast('已新增分类'); return; } if (m.type === 'renameCategory') { const cat = cats.find(c => c.id === m.catId); if (!cat) { ui.modal = null; render(); return; } if (cats.some(c => c.id !== m.catId && c.name === finalName)) { toast('分类已存在'); return; } cat.name = finalName; (cat.items || []).forEach(n => n.category = finalName); ui.modal = null; saveAll(); render(); toast('分类已重命名'); } } function moveCategory(catId, dir) { const cats = store.local.categories || []; const i = cats.findIndex(c => c.id === catId); const j = i + dir; if (i < 0 || j < 0 || j >= cats.length) return; const [cat] = cats.splice(i, 1); cats.splice(j, 0, cat); saveAll(); render(); } function bindCatDrag(row) { row.addEventListener('dragstart', e => { ui.dragCat = row.dataset.catId; row.classList.add('dragging'); try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', ui.dragCat); } catch (err) {} }); row.addEventListener('dragend', () => { row.classList.remove('dragging'); ui.dragCat = null; }); row.addEventListener('dragover', e => { e.preventDefault(); row.classList.add('drag-over'); }); row.addEventListener('dragleave', () => row.classList.remove('drag-over')); row.addEventListener('drop', e => { e.preventDefault(); row.classList.remove('drag-over'); const fromId = ui.dragCat || (e.dataTransfer && e.dataTransfer.getData('text/plain')); const toId = row.dataset.catId; reorderCategory(fromId, toId); }); } function reorderCategory(fromId, toId) { if (!fromId || !toId || fromId === toId) return; const cats = store.local.categories || []; const from = cats.findIndex(c => c.id === fromId); const to = cats.findIndex(c => c.id === toId); if (from < 0 || to < 0) return; const [cat] = cats.splice(from, 1); cats.splice(to, 0, cat); saveAll(); render(); toast('分类顺序已调整'); } function toggleSelect(itemId, checked) { const n = findItem(itemId); if (!n || n.source === 'sync') return; if (checked === false) delete ui.selected[itemId]; else ui.selected[itemId] = true; render(); } function selectedIdsVisibleOrAll() { return Object.keys(ui.selected || {}).filter(id => { const n = findItem(id); return n && n.source !== 'sync'; }); } function selectAllVisible() { const activeList = getActiveList(); const filtered = filterItems(activeList.items || []); const localItems = filtered.filter(n => n.source !== 'sync'); if (!localItems.length) { toast('当前列表没有可批量选择的本地笔记'); return; } const allSelected = localItems.every(n => ui.selected[n.id]); if (allSelected) localItems.forEach(n => delete ui.selected[n.id]); else localItems.forEach(n => { ui.selected[n.id] = true; }); render(); } function bulkMove() { const ids = selectedIdsVisibleOrAll(); if (!ids.length) return; const targetId = q('[data-role="bulk-target"]')?.value; const target = (store.local.categories || []).find(c => c.id === targetId); if (!target) { toast('请选择目标分类'); return; } ids.forEach(id => moveItemToCategory(id, target)); ui.selected = {}; saveAll(); render(); toast('已移动到 ' + target.name); } function bulkDelete() { const ids = selectedIdsVisibleOrAll(); if (!ids.length) return; if (!confirm(`确定删除选中的 ${ids.length} 条本地笔记?`)) return; ids.forEach(id => removeLocal(id)); ui.selected = {}; saveAll(); render(); toast('已批量删除'); } function moveItemToCategory(itemId, targetCat) { let moved = null; for (const c of store.local.categories || []) { const i = (c.items || []).findIndex(n => n.id === itemId); if (i >= 0) { moved = c.items.splice(i, 1)[0]; break; } } if (!moved) return; moved.category = targetCat.name; moved.updated_at = new Date().toISOString(); targetCat.items = targetCat.items || []; targetCat.items.push(moved); } function toggleStar(itemId) { const n = findItem(itemId); if (!n || n.source === 'sync') return; n.starred = !n.starred; n.updated_at = new Date().toISOString(); saveAll(); render(); toast(n.starred ? '已收藏' : '已取消收藏'); } function findItemCategory(itemId, scope) { const cat = findCategoryByItem(itemId, scope); return cat ? cat.name : ''; } function findCategoryByItem(itemId, scope) { const cats = scope === 'sync' ? store.sync.categories : store.local.categories; for (const c of cats || []) if ((c.items || []).some(n => n.id === itemId)) return c; return null; } function findItem(itemId) { for (const c of [...(store.sync.categories || []), ...(store.local.categories || [])]) { const n = (c.items || []).find(x => x.id === itemId); if (n) return n; } return null; } function applyVars(text, itemObj) { return String(text || '') .replace(/\{\{title\}\}/g, itemObj.title || '') .replace(/\{\{tags\}\}/g, (itemObj.tags || []).join(', ')) .replace(/\{\{date\}\}/g, new Date().toLocaleDateString()) .replace(/\{\{time\}\}/g, new Date().toLocaleString()) .replace(/\{\{text\}\}/g, ''); } function exportStore() { const blob = new Blob([JSON.stringify(store, null, 2)], { type: 'application/json' }); const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = 'infinity-pro-script-backup.json'; a.click(); URL.revokeObjectURL(a.href); } function importStore() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.onchange = async () => { try { const file = input.files && input.files[0]; if (!file) return; const json = JSON.parse(await file.text()); if (!json.local && !json.sync) throw new Error('不是 Infinity Pro script 备份格式'); store = json; saveAll(); render(); toast('导入成功'); } catch (e) { alert('导入失败:' + (e.message || e)); } }; input.click(); } function copyText(text) { try { GM_setClipboard(text, 'text'); } catch (e) { navigator.clipboard && navigator.clipboard.writeText(text); } } function formatDate(d) { try { return new Date(d).toLocaleDateString(); } catch (e) { return String(d || ''); } } function hash(str) { let h = 0; for (let i = 0; i < String(str).length; i++) h = Math.imul(31, h) + String(str).charCodeAt(i) | 0; return Math.abs(h).toString(36); } function toast(msg) { const shadow = ipsShadow(); let t = shadow.querySelector('.ips-toast'); if (!t) { t = document.createElement('div'); t.className = 'ips-toast'; shadow.appendChild(t); } t.textContent = msg; t.classList.add('show'); clearTimeout(window.__ips_toast_timer); window.__ips_toast_timer = setTimeout(() => t.classList.remove('show'), 1600); } function esc(s) { return String(s || '').replace(/[&<>"']/g, ch => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch])); } function trackInputs() { document.addEventListener('focusin', e => { if (!insidePanel(e.target) && isEditable(e.target)) lastEditable = e.target; }, true); document.addEventListener('click', e => { const el = e.target && e.target.closest ? e.target.closest('textarea,input,[contenteditable="true"],[contenteditable="plaintext-only"]') : null; if (el && !insidePanel(el) && isEditable(el)) lastEditable = el; }, true); } function insertText(text) { if (!text) return false; let el = null; if (lastEditable && document.contains(lastEditable) && isEditable(lastEditable)) el = lastEditable; if (!el && isEditable(document.activeElement) && !insidePanel(document.activeElement)) el = document.activeElement; if (!el) el = findBestInput(); if (!el) return false; el.focus(); if (el.tagName === 'TEXTAREA' || el.tagName === 'INPUT') { const start = el.selectionStart == null ? el.value.length : el.selectionStart; const end = el.selectionEnd == null ? el.value.length : el.selectionEnd; const next = el.value.slice(0, start) + text + el.value.slice(end); try { const proto = el.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(proto, 'value').set.call(el, next); } catch (e) { el.value = next; } try { el.selectionStart = el.selectionEnd = start + text.length; } catch (e) {} fireInput(el, text); return true; } if (el.isContentEditable) { placeCaretEnd(el); try { el.dispatchEvent(new InputEvent('beforeinput', { bubbles: true, cancelable: true, inputType: 'insertText', data: text })); } catch (e) {} let ok = false; try { ok = document.execCommand('insertText', false, text); } catch (e) {} if (!ok) { try { const sel = window.getSelection(); const range = sel.rangeCount ? sel.getRangeAt(0) : document.createRange(); range.deleteContents(); const node = document.createTextNode(text); range.insertNode(node); range.setStartAfter(node); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } catch (e) { el.textContent = (el.textContent || '') + text; } } fireInput(el, text); let p = el.parentElement; for (let i = 0; p && i < 5; i++, p = p.parentElement) fireInput(p, text); return true; } return false; } function findBestInput() { const selectors = [ 'textarea', '[contenteditable="true"][role="textbox"]', '[contenteditable="plaintext-only"][role="textbox"]', '[contenteditable="true"]', '[contenteditable="plaintext-only"]', 'input[type="text"]', 'input:not([type])' ]; const all = selectors.flatMap(s => deepQuery(s)); const candidates = Array.from(new Set(all)).filter(el => { if (!isEditable(el) || insidePanel(el)) return false; const r = el.getBoundingClientRect(); const st = getComputedStyle(el); return r.width > 80 && r.height > 12 && st.display !== 'none' && st.visibility !== 'hidden'; }); candidates.sort((a, b) => scoreInput(b) - scoreInput(a)); return candidates[0] || null; } function scoreInput(el) { const r = el.getBoundingClientRect(); const hint = ((el.placeholder || '') + ' ' + (el.getAttribute('aria-label') || '') + ' ' + (el.getAttribute('role') || '')).toLowerCase(); let score = r.bottom + Math.min(r.width * r.height / 1000, 500); if (/输入|发消息|提问|message|ask|chat|prompt|textbox/.test(hint)) score += 1000; return score; } function deepQuery(selector, root = document) { const out = []; try { out.push(...root.querySelectorAll(selector)); } catch (e) {} let nodes = []; try { nodes = Array.from(root.querySelectorAll('*')); } catch (e) {} for (const n of nodes) if (n.shadowRoot) out.push(...deepQuery(selector, n.shadowRoot)); return out; } function isEditable(el) { if (!el) return false; if (el.tagName === 'TEXTAREA') return true; if (el.tagName === 'INPUT') return /^(text|search|url|email|password|number|tel)$/i.test(el.type || 'text'); return !!el.isContentEditable; } function insidePanel(el) { const host = ipsHost(); const root = ipsRoot(); return !!(el && ((host && (el === host || (el.closest && el.closest('#ips-host')))) || (root && root.contains && root.contains(el)))); } function placeCaretEnd(el) { try { const sel = window.getSelection(); const range = document.createRange(); range.selectNodeContents(el); range.collapse(false); sel.removeAllRanges(); sel.addRange(range); } catch (e) {} } function fireInput(el, text) { try { el.dispatchEvent(new InputEvent('input', { bubbles: true, inputType: 'insertText', data: text })); } catch (e) { el.dispatchEvent(new Event('input', { bubbles: true })); } el.dispatchEvent(new Event('change', { bubbles: true })); } function addStyles() { const css = ` :host { all: initial !important; } #ips-root { all: initial !important; pointer-events: auto !important; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial, "Microsoft YaHei", sans-serif !important; color: #0f172a !important; } #ips-root * { box-sizing: border-box; font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Arial, "Microsoft YaHei", sans-serif; } #ips-root .ips-panel, #ips-root .ips-panel * { text-transform: none !important; letter-spacing: normal; } #ips-root, #ips-root * { box-sizing: border-box !important; } .ips-panel, .ips-fab, .ips-toast, .ips-modal-mask { all: initial; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',Arial,sans-serif !important; box-sizing: border-box !important; } .ips-panel *, .ips-modal-mask *, .ips-fab * { box-sizing: border-box !important; font-family: inherit !important; } .ips-panel button, .ips-panel input, .ips-panel textarea, .ips-panel select { font: inherit !important; } .ips-fab { position: fixed; touch-action: none; right: 0px; bottom: 18px; z-index: 2147483647; width: 60px; height: 60px; border: 0; border-radius: 20px; background: linear-gradient(135deg,#020617,#4f46e5 56%,#06b6d4); color: #fff; box-shadow: 0 18px 45px rgba(2,6,23,.32); cursor: grab; display:grid !important; place-items:center !important; padding:0 !important; overflow: visible !important; line-height:0 !important; text-align:center !important; } .ips-fab-logo { width:34px !important; height:34px !important; display:grid !important; place-items:center !important; color:#fff !important; pointer-events:none !important; line-height:0 !important; margin:auto !important; transform:none !important; } .ips-fab-logo svg { display:block !important; width:34px !important; height:34px !important; color:#fff !important; overflow: visible !important; margin:0 !important; transform:none !important; } .ips-fab.dragging { cursor: grabbing; transition: none; } .ips-panel { position: fixed; right: 18px; top: 54px; bottom: 18px; width: 860px; max-width: calc(100vw - 28px); z-index: 2147483646; background: rgba(255,255,255,.96); backdrop-filter: blur(18px); border: 1px solid rgba(148,163,184,.28); border-radius: 26px; box-shadow: 0 30px 88px rgba(15,23,42,.25); overflow: hidden; color: #0f172a; transform: translateX(calc(100% + 42px)); opacity: 0; pointer-events: none; transition: .22s ease; } .ips-panel.show { transform: translateX(0); opacity: 1; pointer-events: auto; } .ips-head { padding: 12px 15px; display: flex; justify-content: space-between; align-items: flex-start; background: linear-gradient(135deg,rgba(79,70,229,.13),rgba(6,182,212,.10)); border-bottom: 1px solid rgba(148,163,184,.18); } .ips-title { font-size: 19px; font-weight: 900; letter-spacing: -.04em; color: #0f172a; } .ips-sub { font-size: 12px; color: #64748b; margin-top: 4px; } .ips-close { width: 33px; height: 33px; border: 0; border-radius: 12px; background: rgba(15,23,42,.08); color: #334155; font-size: 20px; cursor: pointer; } .ips-searchbar { padding: 8px 12px 7px; position: relative; } .ips-input { width: 100%; height: 34px; border: 1px solid rgba(148,163,184,.45); border-radius: 15px; background: #fff; color: #0f172a; outline: none; padding: 0 13px; margin-top: 8px; font-size: 14px; } .ips-search { padding-right: 42px; margin-top: 0; } .ips-clear { position: absolute; right: 20px; top: 50%; transform: translateY(-50%); width: 28px; height: 28px; display:flex; align-items:center; justify-content:center; border: 0; border-radius: 999px; background: #e2e8f0; color: #475569; font-size: 17px; line-height: 1; cursor: pointer; } .ips-input:focus { border-color: #6366f1; box-shadow: 0 0 0 4px rgba(99,102,241,.13); } .ips-body { display: grid; grid-template-columns: 270px 1fr; gap: 8px; padding: 0 10px 10px; height: calc(100% - 116px); } .ips-side, .ips-main { overflow: auto; } .ips-root-cat, .ips-cat { width: 100%; display: flex; justify-content: space-between; align-items: center; border: 0; cursor: pointer; text-align: left; min-width: 0; } .ips-root-cat { margin: 4px 0 3px; padding: 8px 8px; border-radius: 15px; background: #e0f2fe; color: #075985; font-size: 13px; font-weight: 850; } .ips-root-cat.active { background: #111827; color: #fff; } .ips-root-cat.active span { color: #fff; } .ips-root-cat b, .ips-cat b { background: rgba(255,255,255,.86); color: #64748b; border-radius: 999px; padding: 1px 7px; font-size: 11px; } .ips-toggle { width: 100%; border: 0; background: transparent; color: #64748b; text-align: left; font-size: 12px; padding: 3px 6px 6px; cursor: pointer; } .ips-children { display: none; padding: 4px 0 2px; border-left: 0; margin-left: 0; } .ips-children.show { display: block; } .ips-cat-row { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 4px; align-items: center; margin: 4px 0; padding: 2px; border-radius: 12px; transition: .15s ease; width: 100%; min-width: 0; } .ips-cat-row.drag-over { background: rgba(79,70,229,.10); outline: 1px dashed rgba(79,70,229,.45); } .ips-cat-row.dragging { opacity: .55; } .ips-cat { margin: 0; padding: 6px 7px; border-radius: 11px; background: #f1f5f9; color: #334155; font-size: 12px; min-height: 30px; overflow: hidden; } .ips-cat span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; } .ips-cat.active { background: #4f46e5; color: #fff; } .ips-cat-tools { display: flex; flex: 0 0 auto; gap: 2px; opacity: .35; max-width: 0; overflow: hidden; transition: .15s ease; } .ips-cat-row:hover .ips-cat-tools, .ips-cat-row.active .ips-cat-tools { opacity: 1; max-width: 104px; } .ips-cat-tools button { width: 22px; height: 22px; border: 0; border-radius: 8px; background: #eef2ff; color: #4338ca; cursor: pointer; font-size: 10px; opacity: .88; padding:0; } .ips-cat-tools button:hover { opacity: 1; transform: translateY(-1px); } .ips-cat-tools button:disabled { opacity: .35; cursor: default; transform: none; } .ips-cat-tools .danger { background: #fee2e2; color: #dc2626; } .ips-cat.active span { color: #fff; } .ips-mini-empty { font-size: 12px; color: #94a3b8; padding: 7px 8px; } .ips-toolbar { display: flex; gap: 5px; flex-wrap: nowrap; position: sticky; top: 0; z-index: 2; background: rgba(255,255,255,.94); padding-bottom: 6px; margin-bottom: 6px; overflow-x: auto; } .ips-btn { border: 0; border-radius: 10px; background: #e2e8f0; color: #0f172a; padding: 6px 8px; font-size: 11.5px; font-weight: 800; cursor: pointer; white-space: nowrap; } .ips-btn.primary { background: #4f46e5; color: #fff; } .ips-btn.ghost { background: #eef2ff; color: #4338ca; } .ips-btn.soft { background: #f0f9ff; color: #0369a1; } .ips-btn.danger { background: #fee2e2; color: #dc2626; } .ips-syncbox { display: none; background: #f8fafc; border: 1px solid rgba(148,163,184,.28); border-radius: 14px; padding: 8px; margin-bottom: 7px; } .ips-syncbox.show { display: block; } .ips-sync-title { font-size: 14px; font-weight: 900; } .ips-note { color: #64748b; font-size: 12px; line-height: 1.5; margin-top: 6px; } .ips-row { display: flex; gap: 6px; flex-wrap: nowrap; margin-top: 6px; } .ips-editor { background: linear-gradient(180deg,#fff,#f8fafc); border: 1px solid rgba(99,102,241,.22); border-radius: 15px; padding: 10px; margin: 2px 0 8px; box-shadow: 0 14px 35px rgba(15,23,42,.07); } .ips-editor-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; margin-bottom: 8px; } .ips-editor-head b { display: block; font-size: 15px; color: #0f172a; } .ips-editor-head span { display: block; margin-top: 3px; font-size: 12px; color: #64748b; } .ips-editor-close { width: 30px; height: 30px; border: 0; border-radius: 11px; background: #e2e8f0; color: #334155; font-size: 18px; cursor: pointer; } .ips-editor-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; align-items: start; } .ips-editor label { display: block; color: #475569; font-size: 12px; font-weight: 800; } .ips-editor .ips-input { margin-top: 6px; } .ips-category-select { width: 100%; height: 40px; margin-top: 6px; border: 1px solid rgba(148,163,184,.45); border-radius: 15px; background: #fff; color: #0f172a; padding: 0 12px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; font-size: 14px; } .ips-cat-drawer { display: none; margin-top: 8px; padding: 8px; border: 1px solid rgba(148,163,184,.24); border-radius: 16px; background: #f8fafc; max-height: 150px; overflow: auto; box-shadow: inset 0 1px 0 rgba(255,255,255,.8); } .ips-cat-drawer.show { display: block; animation: ipsSlide .16s ease; } .ips-cat-option { width: 100%; border: 0; border-radius: 12px; padding: 8px 9px; margin: 3px 0; background: #fff; color: #334155; display: flex; justify-content: space-between; cursor: pointer; font-size: 12.5px; } .ips-cat-option.active { background: #4f46e5; color: #fff; } @keyframes ipsSlide { from { opacity: 0; transform: translateY(-6px); } to { opacity: 1; transform: translateY(0); } } .ips-textarea { width: 100%; min-height: 170px; resize: vertical; border: 1px solid rgba(148,163,184,.45); border-radius: 15px; background: #fff; color: #0f172a; outline: none; padding: 12px 13px; margin-top: 6px; font-size: 14px; line-height: 1.55; font-family: inherit; } .ips-textarea:focus { border-color: #6366f1; box-shadow: 0 0 0 4px rgba(99,102,241,.13); } .ips-editor-content { margin-top: 10px; } .ips-editor-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 10px; } .ips-list-head { display: flex; justify-content: space-between; align-items: center; color: #64748b; font-size: 12px; padding: 2px 2px 6px; } .ips-bulkbar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; background: linear-gradient(135deg,#eef2ff,#f0f9ff); border: 1px solid rgba(99,102,241,.18); border-radius: 16px; padding: 10px; margin: 0 0 10px; } .ips-bulkbar div { flex: 1; min-width: 140px; } .ips-bulkbar b { display: block; font-size: 13px; color: #312e81; } .ips-bulkbar span { display: block; margin-top: 2px; font-size: 11px; color: #64748b; } .ips-select { height: 34px; border: 1px solid rgba(148,163,184,.45); border-radius: 12px; background: #fff; color: #0f172a; padding: 0 9px; font-size: 12px; outline: none; } .ips-list-head b { color: #0f172a; font-size: 13px; } .ips-card { position: relative; background: #fff; border: 1px solid rgba(148,163,184,.25); border-radius: 14px; padding: 9px; margin-bottom: 7px; box-shadow: 0 10px 24px rgba(15,23,42,.05); } .ips-card.starred { border-color: rgba(245,158,11,.42); background: linear-gradient(180deg,#fff,#fffbeb); box-shadow: 0 12px 28px rgba(245,158,11,.10); } .ips-card.selected { border-color: rgba(79,70,229,.55); box-shadow: 0 14px 30px rgba(79,70,229,.12); } .ips-card-top { display: flex; justify-content: space-between; gap: 7px; align-items: flex-start; } .ips-card-titlebox { flex: 1; min-width: 0; } .ips-check { width: 22px; height: 22px; flex: 0 0 auto; margin-top: 1px; cursor: pointer; } .ips-check input { display: none; } .ips-check span { display: block; width: 20px; height: 20px; border-radius: 8px; border: 1px solid rgba(148,163,184,.55); background: #fff; } .ips-check input:checked + span { background: #4f46e5; border-color: #4f46e5; box-shadow: inset 0 0 0 4px #fff; } .ips-check input:disabled + span { opacity: .35; cursor: not-allowed; } .ips-card h3 { margin: 0; font-size: 14.5px; font-weight: 900; letter-spacing: -.02em; color: #0f172a; } .ips-meta { color: #94a3b8; font-size: 11px; margin-top: 3px; } .ips-card-content { all: revert !important; display: block !important; visibility: visible !important; opacity: 1 !important; color: #475569 !important; font-size: 12.5px !important; line-height: 1.48 !important; max-height: 140px; overflow: auto; margin: 6px 0 7px; white-space: pre-wrap !important; word-break: break-word !important; -webkit-line-clamp: unset !important; -webkit-box-orient: initial !important; } .ips-actions { display: flex; gap: 4px; flex-wrap: nowrap; justify-content: flex-end; min-width: auto; } .ips-actions button { border: 0; border-radius: 8px; background: #f1f5f9; color: #334155; padding: 4px 6px; font-size: 10.5px; cursor: pointer; white-space: nowrap; } .ips-actions .star { color: #94a3b8; font-size: 13px; } .ips-actions .star.on { background: #fef3c7; color: #d97706; } .ips-actions .danger { color: #dc2626; } .ips-tags { display: flex; gap: 4px; flex-wrap: wrap; } .ips-tags span { background: rgba(79,70,229,.09); color: #4f46e5; border-radius: 999px; padding: 2px 6px; font-size: 10.5px; } .ips-empty { text-align: center; color: #64748b; background: #f8fafc; border-radius: 18px; padding: 26px 16px; } .ips-modal-mask { position: fixed; inset: 0; z-index: 2147483647; display: flex; align-items: center; justify-content: center; background: rgba(15,23,42,.28); backdrop-filter: blur(5px); } .ips-modal { width: 360px; max-width: calc(100vw - 40px); background: #fff; border-radius: 22px; padding: 16px; box-shadow: 0 30px 90px rgba(15,23,42,.28); border: 1px solid rgba(148,163,184,.24); } .ips-modal-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; } .ips-modal-head b { font-size: 16px; color: #0f172a; } .ips-modal-head button { width: 30px; height: 30px; border: 0; border-radius: 11px; background: #f1f5f9; color: #475569; cursor: pointer; font-size: 18px; } .ips-modal label { display: block; font-size: 12px; font-weight: 850; color: #475569; } .ips-modal-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 14px; } .ips-toast { position: fixed; z-index: 2147483647; left: 50%; bottom: 28px; transform: translate(-50%,18px); opacity: 0; transition: .18s ease; background: #111827; color: #fff; padding: 10px 14px; border-radius: 999px; box-shadow: 0 16px 45px rgba(15,23,42,.25); font: 13px ui-sans-serif, system-ui; pointer-events: none; } .ips-toast.show { opacity: 1; transform: translate(-50%,0); } @media (max-width: 680px) { .ips-editor-grid { grid-template-columns: 1fr; } .ips-panel { left: 10px; right: 10px; top: 56px; bottom: 14px; width: auto; } .ips-body { grid-template-columns: 1fr; } .ips-side { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 6px; } .ips-root-cat,.ips-cat { min-width: 135px; margin: 0; } .ips-cat-row { grid-template-columns: 1fr auto; min-width: 165px; margin: 0; flex: 0 0 auto; } .ips-toggle,.ips-children { display: none !important; } .ips-card-top { flex-wrap: wrap; } .ips-actions { width: 100%; justify-content: flex-start; } } /* ===== Edge-tab FAB v2: smaller, right edge, centered white infinity ===== */ .ips-fab { right: 0px !important; left: auto !important; width: 46px !important; height: 52px !important; border-radius: 18px 0 0 18px !important; padding: 0 !important; overflow: hidden !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; color: #ffffff !important; } .ips-fab-logo { transform: none !important; display: inline-flex !important; align-items: center !important; justify-content: center !important; width: 28px !important; height: 28px !important; color: #ffffff !important; } .ips-fab svg { transform: none !important; width: 28px !important; height: 28px !important; color: #ffffff !important; fill: currentColor !important; } `; window.__IPS_CSS = css; GM_addStyle('#ips-host { all: initial !important; position: fixed !important; inset: 0 !important; pointer-events: none !important; z-index: 2147483647 !important; }\n' + css); } })();