// ==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}
${ui.query ? '' : ''}
网站同步笔记
登录后可把官网笔记同步到这里,脚本只保存登录状态,不保存密码。
当前缓存:${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 `
${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 ``;
}
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 `
`;
}
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);
}
})();