// ==UserScript== // @name ⭐网页瞬间加载⭐ // @namespace fenda // @version 1.0.16 // @description 链接跳过进度条瞬间加载,并优化删除重定向链接,所有参数可高度自定义。 // @icon data:image/png;base64,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 // @author fenda // @match *://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @run-at document-end // @license MPL-2.0 // ==/UserScript== (function() { /* -------------------------------------------------------------------------- */ /* 脚本元信息/常量 */ /* -------------------------------------------------------------------------- */ const SCRIPT_META = Object.freeze({ currentVersion: '1.0.16', updateMetaUrl: '待添加', versionHistoryUrl: '待添加', updateCheckIntervalMs: 3600000, }); const DEFAULT_BLACKLIST_DOMAINS = [ 'www.bilibili.com', 'www.bing.com', 'www.huya.com', 'www.vimeo.com', 'www.tiktok.com', 'www.twitch.tv', 'www.youtube.com', 'www.dailymotion.com', 'www.liveleak.com', 'www.metacafe.com', 'www.youku.com', 'www.iqiyi.com', 'www.netflix.com', 'www.hulu.com', 'www.primevideo.com' ]; const SIGNOUT_LINK_PATH_PARTS = [ '/sign_out', '/logout', '/signoff', '/signout', '/logoff', '/exit', '/user/logout', '/account/signout', '/users/sign_out', '/session/logout', '/auth/logout', '/disconnect', '/member/signout', '/user/sign_out', '/users/logout', '/sessions/signout', '/api/logout', '/app/logout', '/dashboard/logout', '/home/logout', '/profile/logout', '/log_out', '/signoff_user', ]; // 高风险站点:默认不进行预加载/内联展示(网盘/下载站等)。 const HIGH_RISK_DOMAIN_KEYWORDS = [ 'pan.', 'drive.', 'cloud.', 'disk.', 'download', 'down.', 'file.', 'files.', 'share', ]; const HIGH_RISK_DOMAIN_EXACT = [ 'pan.baidu.com', 'yun.baidu.com', 'drive.google.com', 'dropbox.com', 'www.dropbox.com', 'mega.nz', 'www.mediafire.com', 'www.lanzou.com', 'lanzou.com', 'cowtransfer.com', 'www.wenshushu.cn', 'wenshushu.cn', 'www.weiyun.com', 'weiyun.com', ]; function isHighRiskDomain(hostname) { if (!hostname) return false; const h = String(hostname).toLowerCase(); if (HIGH_RISK_DOMAIN_EXACT.includes(h)) return true; return HIGH_RISK_DOMAIN_KEYWORDS.some(k => h.includes(k)); } const SKIP_PROTOCOLS = new Set([ 'mailto:', 'tel:', 'sms:', 'magnet:', 'javascript:', 'data:', 'blob:', 'file:', ]); const SENSITIVE_QUERY_KEYS = [ /^token$/i, /access[_-]?token/i, /^auth$/i, /^ticket$/i, /^session$/i, /^sign$/i, /^sig$/i, /^sso$/i, /^code$/i, /^state$/i, ]; // 额外的“敏感路径”默认规则:常见登录/回调/支付等。 // 说明:这里偏保守,用于“默认不预加载/不缓存”,不会阻止正常跳转。 const SENSITIVE_PATH_PATTERNS = [ /\/(?:oauth|sso|callback|authorize|auth)\b/i, /\/(?:login|signin|sign-in|logout|signout|sign-out)\b/i, /\/(?:pay|payment|checkout|cashier)\b/i, ]; function escapeRegExp(s) { return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } function compileUserRegexLine(line, opts) { try { const o = opts && typeof opts === 'object' ? opts : {}; const kind = String(o.kind || 'key'); const raw = String(line || '').trim(); if (!raw) return null; // 支持 /pattern/flags const m = raw.match(/^\/(.*)\/([gimsuy]*)$/); if (m) { const flags = String(m[2] || '').replace(/g/g, ''); return new RegExp(m[1], flags || 'i'); } if (kind === 'key') { return new RegExp(`^${escapeRegExp(raw)}$`, 'i'); } // kind === 'path':默认做“包含”匹配 return new RegExp(escapeRegExp(raw), 'i'); } catch { return null; } } function getUserRegexList(storageKey, opts) { try { const raw = Storage.get(storageKey, ''); if (!raw) return []; const lines = String(raw) .split(/\r?\n/) .map((x) => String(x || '').trim()) .filter(Boolean) .slice(0, 120); const regs = []; lines.forEach((ln) => { const r = compileUserRegexLine(ln, opts); if (r) regs.push(r); }); return regs; } catch { return []; } } function isSafeModeEnabled() { try { return !!Storage.get('safeMode', false); } catch { return false; } } function hasSensitiveQueryParams(urlObj) { try { if (!urlObj || !urlObj.searchParams) return false; const user = getUserRegexList('sensitiveQueryKeysCustom', { kind: 'key' }); const patterns = user.length ? SENSITIVE_QUERY_KEYS.concat(user) : SENSITIVE_QUERY_KEYS; for (const k of urlObj.searchParams.keys()) { if (patterns.some((re) => re.test(String(k)))) return true; } return false; } catch { return false; } } function hasSensitivePath(urlObj) { try { if (!urlObj) return false; const user = getUserRegexList('sensitivePathPatternsCustom', { kind: 'path' }); const patterns = user.length ? SENSITIVE_PATH_PATTERNS.concat(user) : SENSITIVE_PATH_PATTERNS; const path = `${String(urlObj.pathname || '')}${String(urlObj.search || '')}`; return patterns.some((re) => { try { return re.test(path); } catch { return false; } }); } catch { return false; } } function isSensitiveUrl(urlObj) { try { if (!urlObj) return false; return hasSensitiveQueryParams(urlObj) || hasSensitivePath(urlObj); } catch { return false; } } function shouldCacheUrl(urlObj) { try { if (!urlObj) return true; // 默认更稳:带敏感参数的不缓存,只做预加载/导航(可通过白名单域名绕过) if (!Storage.get('disableSensitiveCache', true)) return true; const allow = Storage.get('sensitiveCacheWhitelistDomains', []); const allowList = Array.isArray(allow) ? allow : []; if (allowList.includes(String(urlObj.hostname || ''))) return true; return !isSensitiveUrl(urlObj); } catch { return true; } } function shouldSkipPreloadForSensitive(urlObj) { try { if (!Storage.get('disableSensitivePreload', true)) return false; return isSensitiveUrl(urlObj); } catch { return false; } } // 常见下载/附件后缀(只作为快速跳过,不用于精确识别) const DOWNLOAD_EXT_RE = /\.(?:zip|rar|7z|tar|gz|bz2|xz|iso|exe|msi|apk|dmg|pkg|deb|rpm|pdf|doc|docx|xls|xlsx|ppt|pptx|csv|mp3|m4a|wav|flac|mp4|mkv|avi|mov|webm|png|jpg|jpeg|gif|webp|svg|ico)(?:$|\?)/i; // 内容类型:是否可视为 HTML function isHtmlContentType(contentType) { // 取不到时不直接判非 HTML:由“扩展名/Content-Disposition/HEAD 预检”兜底。 if (!contentType) return true; return /\btext\/html\b/i.test(contentType) || /\bapplication\/xhtml\+xml\b/i.test(contentType); } function isAttachmentDisposition(contentDisposition) { if (!contentDisposition) return false; const cd = String(contentDisposition || ''); if (/\battachment\b/i.test(cd)) return true; // 很多站点会用 inline; filename=... 触发“下载/另存为” if (/\bfilename\*\s*=/i.test(cd) || /\bfilename\s*=/i.test(cd)) return true; return false; } function isLikelyDownloadUrl(urlObj) { try { if (!urlObj || !urlObj.href) return false; return DOWNLOAD_EXT_RE.test(String(urlObj.href)); } catch { return false; } } function makePreloadSkipError(reason, extra) { const e = new Error(String(reason || 'skip')); try { e.__ilSkip = true; e.__ilReason = String(reason || 'skip'); e.__ilExtra = extra && typeof extra === 'object' ? extra : null; } catch {} return e; } // HEAD 预检:优先过滤附件/非 HTML;失败则降级为直接 GET function shouldUseHeadPrecheck() { try { return !!Storage.get('useHeadPrecheck', true); } catch { return true; } } function headPrecheckMaybeSkip(urlObj, element, signal) { return new Promise((resolve) => { try { if (!shouldUseHeadPrecheck()) return resolve({ ok: true }); if (!urlObj || !isSameOriginUrl(urlObj)) return resolve({ ok: true }); if (isDownloadLikeLink(element, urlObj)) return resolve({ ok: true }); if (shouldSkipPreloadForSensitive(urlObj)) return resolve({ ok: false, reason: 'sensitive' }); if (isSafeModeEnabled()) return resolve({ ok: false, reason: 'safe-mode' }); fetch(urlObj.href, { method: 'HEAD', cache: 'no-cache', credentials: 'include', mode: 'same-origin', signal, }).then((resp) => { try { if (!resp) return resolve({ ok: true }); const cd = resp.headers && resp.headers.get ? resp.headers.get('content-disposition') : ''; if (isAttachmentDisposition(cd)) return resolve({ ok: false, reason: 'attachment' }); const ct = resp.headers && resp.headers.get ? resp.headers.get('content-type') : ''; if (ct && !isHtmlContentType(ct)) return resolve({ ok: false, reason: 'non-html' }); // 某些站点不返回 content-type:此时用扩展名兜底,避免把附件当 HTML 预加载 if (!ct && isLikelyDownloadUrl(urlObj)) return resolve({ ok: false, reason: 'download-ext' }); return resolve({ ok: true }); } catch { return resolve({ ok: true }); } }).catch(() => resolve({ ok: true })); } catch { resolve({ ok: true }); } }); } const URL_CLEAN_TRACKING_KEYS = [ /^utm_/i, /^spm$/i, /^from$/i, /^ref$/i, /^source$/i, ]; const Storage = { get(key, fallback) { return GM_getValue(key, fallback); }, set(key, value) { GM_setValue(key, value); }, }; // 日志面板实时刷新(节流):当 panel4 处于激活态时,日志/回放写入会触发 UI 刷新。 let logPanelLastRefreshAt = 0; let logPanelRefreshTimer = null; let logPanelRefreshRaf = 0; let logPanelRefreshPending = false; function scheduleLogPanelRefresh() { try { if (currentActivePanelId !== 'panel4') return; if (!showcaseFeaturesPanel4 || typeof showcaseFeaturesPanel4.__renderLogPanel !== 'function') return; const now = Date.now(); const minIntervalMs = 250; // 若刷新过于频繁则合并到下一次 if (now - logPanelLastRefreshAt < minIntervalMs) { if (logPanelRefreshPending) return; logPanelRefreshPending = true; if (logPanelRefreshTimer) clearTimeout(logPanelRefreshTimer); logPanelRefreshTimer = setTimeout(() => { logPanelRefreshPending = false; logPanelLastRefreshAt = Date.now(); try { showcaseFeaturesPanel4.__renderLogPanel(); } catch {} }, minIntervalMs); return; } logPanelLastRefreshAt = now; if (logPanelRefreshTimer) { clearTimeout(logPanelRefreshTimer); logPanelRefreshTimer = null; } logPanelRefreshPending = false; const raf = window.requestAnimationFrame; if (typeof raf === 'function') { if (logPanelRefreshRaf) { try { window.cancelAnimationFrame(logPanelRefreshRaf); } catch {} } logPanelRefreshRaf = raf(() => { logPanelRefreshRaf = 0; try { showcaseFeaturesPanel4.__renderLogPanel(); } catch {} }); } else { setTimeout(() => { try { showcaseFeaturesPanel4.__renderLogPanel(); } catch {} }, 0); } } catch {} } /* -------------------------------------------------------------------------- */ /* 日志与诊断(本地) */ /* -------------------------------------------------------------------------- */ const LOG_LEVELS = Object.freeze({ error: 0, warn: 1, info: 2, debug: 3 }); let runtimeLogBuffer = []; function getLogLevel() { const lv = String(Storage.get('logLevel', 'warn')).toLowerCase(); return (lv in LOG_LEVELS) ? lv : 'warn'; } function shouldLog(level) { const current = getLogLevel(); return LOG_LEVELS[level] <= LOG_LEVELS[current]; } function safeStringify(obj) { try { if (obj instanceof Error) { return obj.stack || obj.message || String(obj); } if (typeof obj === 'string') return obj; return JSON.stringify(obj); } catch { try { return String(obj); } catch { return '[unserializable]'; } } } function pushLog(level, message, data) { try { if (!shouldLog(level)) return; const item = { t: Date.now(), l: level, m: String(message || ''), d: (data === undefined) ? '' : safeStringify(data), }; runtimeLogBuffer.push(item); const maxLen = Number(Storage.get('logMaxItems', 300)); const cap = (Number.isFinite(maxLen) && maxLen > 50) ? maxLen : 300; if (runtimeLogBuffer.length > cap) { runtimeLogBuffer.splice(0, runtimeLogBuffer.length - cap); } // 持久化(可关闭) if (Storage.get('persistLogs', true)) { Storage.set('runtimeLogs_v1', runtimeLogBuffer); } if (Storage.get('debugConsoleLog', false)) { const prefix = `[InstantLoad][${level}]`; if (level === 'error') console.error(prefix, message, data); else if (level === 'warn') console.warn(prefix, message, data); else if (level === 'debug') console.debug(prefix, message, data); else console.log(prefix, message, data); } // 日志写入后:若用户正在查看日志面板,则实时刷新 scheduleLogPanelRefresh(); } catch {} } const Log = { error(msg, data) { pushLog('error', msg, data); }, warn(msg, data) { pushLog('warn', msg, data); }, info(msg, data) { pushLog('info', msg, data); }, debug(msg, data) { pushLog('debug', msg, data); }, clear() { runtimeLogBuffer = []; try { Storage.set('runtimeLogs_v1', runtimeLogBuffer); } catch {} }, getAll() { try { const persisted = Storage.get('runtimeLogs_v1', []); if (Array.isArray(persisted) && persisted.length) return persisted; } catch {} return runtimeLogBuffer; } }; // 缓存条数统计(本地) let cacheMeta = { items: 0, lastUpdated: 0 }; function updateCacheMeta(cb) { try { if (!dbReady || !db) { cacheMeta = { items: 0, lastUpdated: Date.now() }; if (typeof cb === 'function') cb(cacheMeta); return; } const transaction = db.transaction([dbStoreName], 'readonly'); const store = transaction.objectStore(dbStoreName); if (typeof store.count === 'function') { const req = store.count(); req.onsuccess = function() { cacheMeta.items = Number(req.result || 0); cacheMeta.lastUpdated = Date.now(); if (typeof cb === 'function') cb(cacheMeta); try { updateCacheStatusBadge(); } catch {} }; req.onerror = function() { if (typeof cb === 'function') cb(cacheMeta); }; return; } // 兼容:count 不可用则回退 getAll 并取 length(可能更重) const req2 = store.getAll(); req2.onsuccess = function() { const arr = Array.isArray(req2.result) ? req2.result : []; cacheMeta.items = arr.length; cacheMeta.lastUpdated = Date.now(); if (typeof cb === 'function') cb(cacheMeta); try { updateCacheStatusBadge(); } catch {} }; req2.onerror = function() { if (typeof cb === 'function') cb(cacheMeta); }; } catch { if (typeof cb === 'function') cb(cacheMeta); } } // 点击拦截模式(P0): // - 'all':当前行为(拦截所有可处理链接) // - 'preloaded-only':仅当链接已预加载时才拦截展示,否则放行交给浏览器 // - 'same-origin':仅同源链接拦截 // - 'whitelist-only':仅白名单域名拦截 // - 'off':不拦截点击(仍可做预加载/重定向优化) const CLICK_INTERCEPT_MODE_KEY = 'clickInterceptMode'; const DEFAULT_CLICK_INTERCEPT_MODE = 'all'; // 按域名覆盖点击拦截策略(JSON 对象:{"example.com":"preloaded-only","foo.com":"off"}) const CLICK_INTERCEPT_DOMAIN_RULES_KEY = 'clickInterceptDomainRules'; // 扩展站点规则:支持按路径前缀/通配符/正则覆盖(可选启用) // 结构示例: // { // "example.com": {"mode":"preloaded-only"}, // "example.com/path": {"mode":"off"}, // "*.example.com": {"mode":"same-origin"}, // "re:^https?://example\\.com/(a|b)": {"mode":"off"} // } const CLICK_INTERCEPT_DOMAIN_RULES_V2_KEY = 'clickInterceptDomainRulesV2'; const CLICK_INTERCEPT_MODE_VALUES = new Set(['all', 'preloaded-only', 'same-origin', 'whitelist-only', 'off']); function validateClickInterceptDomainRules(rawText) { const raw = String(rawText ?? '').trim(); if (!raw) { return { ok: true, rules: {} }; } let obj; try { obj = JSON.parse(raw); } catch (e) { return { ok: false, message: 'JSON 解析失败', error: e }; } if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { return { ok: false, message: '必须是 JSON 对象,例如 {"example.com":"off"}' }; } const cleaned = {}; const errors = []; Object.keys(obj).forEach((k) => { try { const host = String(k || '').trim().toLowerCase(); const v = String(obj[k] || '').trim(); if (!host) return; // 允许用户写 "*.example.com" / "example.com",但不做复杂通配匹配(这里只做校验提示) const hostOk = /^[*]?[.]?[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host); if (!hostOk) { errors.push(`域名格式不合法:${host}`); return; } if (!CLICK_INTERCEPT_MODE_VALUES.has(v)) { errors.push(`值域不合法:${host}=${v}(允许:all/preloaded-only/same-origin/whitelist-only/off)`); return; } cleaned[host] = v; } catch {} }); if (errors.length) { return { ok: false, message: errors.slice(0, 3).join(';') + (errors.length > 3 ? '…' : '') }; } return { ok: true, rules: cleaned }; } function getClickInterceptDomainRules() { try { const raw = Storage.get(CLICK_INTERCEPT_DOMAIN_RULES_KEY, '{}'); const obj = (typeof raw === 'string') ? JSON.parse(raw || '{}') : (raw || {}); if (!obj || typeof obj !== 'object') return {}; return Array.isArray(obj) ? {} : obj; } catch { return {}; } } function getClickInterceptModeForHost(hostname) { const globalMode = getClickInterceptMode(); try { // 可选:按域名覆盖总开关(关闭则完全忽略 domain rules) if (!Storage.get('enableClickInterceptDomainRules', true)) return globalMode; const host = String(hostname || '').toLowerCase(); if (!host) return globalMode; const rules = getClickInterceptDomainRules(); const v = rules[host]; if (typeof v === 'string' && v) return v; } catch {} return globalMode; } function normalizeRuleKey(input) { try { let s = String(input ?? '').trim(); if (!s) return ''; // 支持直接粘贴 URL try { if (/^https?:\/\//i.test(s)) { const u = new URL(s); s = u.hostname + (u.pathname && u.pathname !== '/' ? u.pathname : ''); } } catch {} return s; } catch { return ''; } } function validateClickInterceptDomainRulesV2(rawText) { const raw = String(rawText ?? '').trim(); if (!raw) { return { ok: true, rules: {} }; } let obj; try { obj = JSON.parse(raw); } catch (e) { return { ok: false, message: 'JSON 解析失败', error: e }; } if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { return { ok: false, message: '必须是 JSON 对象,例如 {"example.com": {"mode":"off"}}' }; } const cleaned = {}; const errors = []; Object.keys(obj).forEach((k) => { try { const key = normalizeRuleKey(k); if (!key) return; const v = obj[k]; const mode = String((v && typeof v === 'object') ? (v.mode || '') : v).trim(); if (!CLICK_INTERCEPT_MODE_VALUES.has(mode)) { errors.push(`值域不合法:${key}=${mode}(允许:all/preloaded-only/same-origin/whitelist-only/off)`); return; } // key 允许几类: // - host: example.com // - host+path-prefix: example.com/foo/bar // - wildcard: *.example.com // - regex: re: const lower = key.toLowerCase(); const isRegex = lower.startsWith('re:'); if (isRegex) { const pat = String(key.slice(3) || '').trim(); if (!pat) { errors.push(`正则为空:${key}`); return; } try { // 仅校验可编译性 new RegExp(pat); } catch { errors.push(`正则无效:${key}`); return; } } else { // 宽松校验:hostname 或 hostname/path 前缀 const parts = lower.split('/'); const hostPart = parts[0]; const hostOk = /^[*]?[.]?[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(hostPart); if (!hostOk) { errors.push(`规则 key 不合法:${key}`); return; } } cleaned[key] = { mode }; } catch {} }); if (errors.length) { return { ok: false, message: errors.slice(0, 3).join(';') + (errors.length > 3 ? '…' : '') }; } return { ok: true, rules: cleaned }; } function getClickInterceptDomainRulesV2() { try { const raw = Storage.get(CLICK_INTERCEPT_DOMAIN_RULES_V2_KEY, '{}'); const obj = (typeof raw === 'string') ? JSON.parse(raw || '{}') : (raw || {}); if (!obj || typeof obj !== 'object') return {}; return Array.isArray(obj) ? {} : obj; } catch { return {}; } } function getClickInterceptModeForUrl(urlObj) { const globalMode = getClickInterceptMode(); try { if (!Storage.get('enableClickInterceptDomainRules', true)) return globalMode; if (!Storage.get('enableClickInterceptDomainRulesV2', true)) return getClickInterceptModeForHost(urlObj.hostname); const rules = getClickInterceptDomainRulesV2(); const href = String(urlObj && urlObj.href ? urlObj.href : ''); const host = String(urlObj && urlObj.hostname ? urlObj.hostname : '').toLowerCase(); const path = String(urlObj && urlObj.pathname ? urlObj.pathname : '/'); const hostPath = host + (path && path !== '/' ? path : ''); // 1) 正则优先(re:) for (const k of Object.keys(rules || {})) { const lower = String(k || '').toLowerCase(); if (!lower.startsWith('re:')) continue; const pat = String(k.slice(3) || '').trim(); if (!pat) continue; try { const re = new RegExp(pat); if (re.test(href)) { const m = rules[k] && rules[k].mode; if (typeof m === 'string' && m) return m; } } catch {} } // 2) host+path 前缀:选择最长匹配 let bestKey = ''; let bestMode = ''; for (const k of Object.keys(rules || {})) { const key = String(k || ''); if (!key) continue; const lower = key.toLowerCase(); if (lower.startsWith('re:')) continue; if (lower.startsWith('*.')) continue; if (lower.includes('/') && hostPath.toLowerCase().startsWith(lower)) { if (lower.length > bestKey.length) { bestKey = lower; bestMode = String(rules[k] && rules[k].mode ? rules[k].mode : ''); } } } if (bestMode && CLICK_INTERCEPT_MODE_VALUES.has(bestMode)) return bestMode; // 3) 精确 host for (const k of Object.keys(rules || {})) { const lower = String(k || '').toLowerCase(); if (lower === host) { const m = rules[k] && rules[k].mode; if (typeof m === 'string' && m) return m; } } // 4) 通配 *.example.com for (const k of Object.keys(rules || {})) { const lower = String(k || '').toLowerCase(); if (!lower.startsWith('*.')) continue; const suffix = lower.slice(1); // ".example.com" if (suffix && host.endsWith(suffix)) { const m = rules[k] && rules[k].mode; if (typeof m === 'string' && m) return m; } } } catch {} return getClickInterceptModeForHost(urlObj && urlObj.hostname ? urlObj.hostname : ''); } function getClickInterceptMode() { return Storage.get(CLICK_INTERCEPT_MODE_KEY, DEFAULT_CLICK_INTERCEPT_MODE); } function shouldInterceptClick(linkEl) { let mode = getClickInterceptMode(); if (mode === 'off') return false; // 安全模式:完全不拦截点击 if (isSafeModeEnabled()) return false; let urlObj; try { urlObj = new URL(linkEl.href, window.location.href); } catch { return false; } // 按域名/路径/规则覆盖 mode = getClickInterceptModeForUrl(urlObj); if (mode === 'off') return false; // 安全兜底:下载/高风险/敏感页不拦截 try { if (isDownloadLikeLink(linkEl, urlObj)) return false; } catch {} try { if (shouldDisablePreloadForUrl(urlObj)) return false; } catch {} try { if (!shouldCacheUrl(urlObj)) return false; } catch {} try { if (shouldSkipPreloadForSensitive(urlObj)) return false; } catch {} if (mode === 'same-origin') { return isSameOriginUrl(urlObj); } if (mode === 'whitelist-only') { if (!isWhitelistModeEnabled) return false; return whitelistDomains.includes(urlObj.hostname); } if (mode === 'preloaded-only') { return isLinkPreloadedReady(linkEl); } // 默认:all return true; } /* -------------------------------------------------------------------------- */ /* 运行时状态声明 */ /* -------------------------------------------------------------------------- */ // IndexedDB / 预加载 let indexedDB; let dbVersion; let dbName; let dbStoreName; let db; let dbReady = false; let dbInitializationPromise = null; let dbWriteQueue = []; let dbWriteInProgress = false; // 运行配置(从 GM_* 读取一次) let maxConcurrentPreloads; let dataCleanupInterval; let maxContentSize; let maxStorageItems; let isWhitelistModeEnabled; let isBlacklistModeEnabled; let blacklistDomains = []; let whitelistDomains = []; // 预加载队列/控制 let currentPreloads = 0; let preloadQueue = []; let preloadSet = new Set(); let abortControllers = {}; let shouldPreloadMapping = {}; // 失败冷却:避免反爬/限流场景下持续重试刷屏 let preloadCooldownUntil = {}; // 失败退避(按 URL key 维度) let preloadBackoffState = {}; function getPreloadBackoffState(key) { try { if (!key) return null; const s = preloadBackoffState[key]; if (!s || typeof s !== 'object') return null; return s; } catch { return null; } } function resetPreloadBackoff(key) { try { if (!key) return; delete preloadBackoffState[key]; delete preloadCooldownUntil[key]; } catch {} } function schedulePreloadBackoff(key, reason, opts) { try { if (!key) return; const now = Date.now(); const prev = preloadBackoffState[key] || { c: 0 }; const nextCount = Math.max(1, Number(prev.c || 0) + 1); const o = (opts && typeof opts === 'object') ? opts : {}; const baseMs = Number.isFinite(Number(o.baseMs)) ? Number(o.baseMs) : 30 * 1000; const maxMs = Number.isFinite(Number(o.maxMs)) ? Number(o.maxMs) : 30 * 60 * 1000; const factor = Number.isFinite(Number(o.factor)) ? Number(o.factor) : 2; const jitter = Number.isFinite(Number(o.jitter)) ? Number(o.jitter) : 0.2; let delay = baseMs * Math.pow(factor, Math.min(10, nextCount - 1)); delay = Math.min(maxMs, Math.max(baseMs, delay)); // 抖动:避免多个链接同时重试 const j = delay * jitter; delay = Math.round(delay + (Math.random() * 2 - 1) * j); delay = Math.max(baseMs, Math.min(maxMs, delay)); preloadBackoffState[key] = { c: nextCount, r: String(reason || ''), t: now, until: now + delay }; preloadCooldownUntil[key] = now + delay; // 记录到日志(可关闭/可降级) try { Log.info('进入退避冷却', { u: key, r: String(reason || ''), ms: delay, c: nextCount }); } catch {} } catch {} } // 内存命中缓存(本次运行期):用于规避 IndexedDB 写入队列延迟导致的点击 miss。 // 注意:必须限制容量,否则长时间浏览可能导致内存膨胀。 const MEM_CACHE_KEY = '__instantLoadMemCache'; const MEM_CACHE_META_KEY = '__instantLoadMemCacheMeta'; function getMemCacheLimits() { // 默认保守:条数 + 字节双阈值 const maxItems = Math.max(10, Math.min(800, Number(Storage.get('memCacheMaxItems', 120)) || 120)); const maxBytes = Math.max(1 * 1024 * 1024, Math.min(200 * 1024 * 1024, Number(Storage.get('memCacheMaxBytesMb', 30)) * 1024 * 1024 || 30 * 1024 * 1024)); return { maxItems, maxBytes }; } function getMemCache() { try { if (!window[MEM_CACHE_KEY]) window[MEM_CACHE_KEY] = new Map(); if (!window[MEM_CACHE_META_KEY]) window[MEM_CACHE_META_KEY] = { bytes: 0 }; return window[MEM_CACHE_KEY]; } catch { return null; } } function getMemCacheMeta() { try { if (!window[MEM_CACHE_META_KEY]) window[MEM_CACHE_META_KEY] = { bytes: 0 }; return window[MEM_CACHE_META_KEY]; } catch { return { bytes: 0 }; } } function memCacheTouch(key) { try { const mem = getMemCache(); if (!mem) return; const hit = mem.get(key); if (!hit) return; mem.delete(key); mem.set(key, hit); } catch {} } function memCacheEvictIfNeeded() { try { const mem = getMemCache(); if (!mem) return; const meta = getMemCacheMeta(); const { maxItems, maxBytes } = getMemCacheLimits(); // 条数优先淘汰 while (mem.size > maxItems) { const k = mem.keys().next().value; const v = mem.get(k); try { meta.bytes = Math.max(0, Number(meta.bytes || 0) - estimateBlobBytes(v && v.blob)); } catch {} mem.delete(k); } // 字节阈值淘汰 while (Number(meta.bytes || 0) > maxBytes && mem.size > 0) { const k = mem.keys().next().value; const v = mem.get(k); try { meta.bytes = Math.max(0, Number(meta.bytes || 0) - estimateBlobBytes(v && v.blob)); } catch {} mem.delete(k); } } catch {} } function memCacheSet(key, blob) { try { const mem = getMemCache(); if (!mem || !key || !blob) return; const meta = getMemCacheMeta(); // 替换已有项时先扣除旧 size try { const prev = mem.get(key); if (prev && prev.blob) { meta.bytes = Math.max(0, Number(meta.bytes || 0) - estimateBlobBytes(prev.blob)); } } catch {} mem.delete(key); mem.set(key, { blob, t: Date.now() }); meta.bytes = Math.max(0, Number(meta.bytes || 0) + estimateBlobBytes(blob)); memCacheEvictIfNeeded(); } catch {} } function memCacheGet(key) { try { const mem = getMemCache(); if (!mem) return null; const hit = mem.get(key); if (!hit || !hit.blob) return null; memCacheTouch(key); return hit; } catch { return null; } } function memCacheCleanupExpired() { try { // 复用 dataCleanupInterval(小时)作为“内存命中缓存 TTL”,避免增加过多概念 const ttlMs = Number(dataCleanupInterval || 0); if (!Number.isFinite(ttlMs) || ttlMs <= 0) return; const mem = getMemCache(); if (!mem) return; const meta = getMemCacheMeta(); const now = Date.now(); const keys = Array.from(mem.keys()); keys.forEach((k) => { try { const v = mem.get(k); const t = Number(v && v.t ? v.t : 0); if (t && now - t > ttlMs) { meta.bytes = Math.max(0, Number(meta.bytes || 0) - estimateBlobBytes(v && v.blob)); mem.delete(k); } } catch {} }); memCacheEvictIfNeeded(); } catch {} } // 预加载扫描调度 let preloadScanHandle = null; let lastScanAt = 0; // 预览/导航 let clickedLinks = []; let currentPreviewIndex = -1; // 预览内状态(回退/前进应保留阅读位置、已加载样式等) let __currentPreviewUrlKey = ''; const __previewPageState = new Map(); const __preloadReadyKeys = new Set(); const __MAX_PREVIEW_STATE = 60; const __MAX_READY_KEYS = 2500; function syncPreloadedMarkerAndStyleForAnchor(anchorEl) { try { if (!anchorEl || !anchorEl.href) return; if (!Storage.get('is_loadedStyle')) return; const key = getUrlKeyForCache(anchorEl.href); if (!key) return; const u = new URL(key, window.location.href); if (!isSameOriginUrl(u)) return; if (u.hostname !== window.location.hostname) return; // 先“吸收”当前 DOM 上已有的标记: // 有些站点会在滚动/懒加载时重建 ,把 dataset/style 清掉。 // 如果我们在清掉之前见过它,就把 key 记到运行期集合里,便于后续恢复。 try { if (anchorEl.dataset && anchorEl.dataset.preloaded) { rememberPreloadReadyKey(key); } } catch {} // 若站点重渲染把 dataset/style 清掉:根据“运行期 ready key / 内存命中”恢复 const memHit = !!(memCacheGet(key) && memCacheGet(key).blob); const readyHit = __preloadReadyKeys.has(key); if (!memHit && !readyHit) { // 仍允许:已标记 preloaded 的链接,重刷样式(站点可能清空 style) if (anchorEl.dataset && anchorEl.dataset.preloaded) { try { addAVisualLinkReminder(anchorEl); } catch {} } return; } try { if (anchorEl.dataset) { anchorEl.dataset.preloaded = '1'; anchorEl.dataset.preloadKey = key; } } catch {} try { addAVisualLinkReminder(anchorEl); } catch {} } catch {} } function rememberPreloadReadyKey(urlKey) { try { const k = getUrlKeyForCache(String(urlKey || '')); if (!k) return; __preloadReadyKeys.add(k); // 简单限额:过大时重建(避免无界增长) if (__preloadReadyKeys.size > __MAX_READY_KEYS) { const arr = Array.from(__preloadReadyKeys); __preloadReadyKeys.clear(); // 保留后半段(更“新”) const keep = arr.slice(Math.max(0, arr.length - Math.floor(__MAX_READY_KEYS * 0.7))); keep.forEach((x) => { try { __preloadReadyKeys.add(x); } catch {} }); } } catch {} } function capturePreviewScrollState(urlKey) { try { const k = getUrlKeyForCache(String(urlKey || '')); if (!k) return; const fp = document.getElementById('fullPageDiv'); if (!fp) return; const state = { scrollTop: Number(fp.scrollTop || 0), scrollLeft: Number(fp.scrollLeft || 0), t: Date.now(), }; __previewPageState.set(k, state); // LRU 清理 if (__previewPageState.size > __MAX_PREVIEW_STATE) { const entries = Array.from(__previewPageState.entries()); entries.sort((a, b) => Number(a[1] && a[1].t ? a[1].t : 0) - Number(b[1] && b[1].t ? b[1].t : 0)); const over = __previewPageState.size - __MAX_PREVIEW_STATE; for (let i = 0; i < over; i++) { try { __previewPageState.delete(entries[i][0]); } catch {} } } } catch {} } function restorePreviewScrollState(urlKey) { try { const k = getUrlKeyForCache(String(urlKey || '')); if (!k) return; const st = __previewPageState.get(k); if (!st) return; const fp = document.getElementById('fullPageDiv'); if (!fp) return; const top = Math.max(0, Number(st.scrollTop || 0)); const left = Math.max(0, Number(st.scrollLeft || 0)); // 双 RAF:等待 DOM 注入与样式计算后再滚动 requestAnimationFrame(() => { requestAnimationFrame(() => { try { fp.scrollTop = top; fp.scrollLeft = left; } catch {} }); }); } catch {} } function applyLoadedStyleHintsInContainer(rootEl) { try { if (!rootEl || typeof rootEl.querySelectorAll !== 'function') return; if (!Storage.get('is_loadedStyle')) return; const links = Array.from(rootEl.querySelectorAll('a[href]')).slice(0, 450); for (let i = 0; i < links.length; i++) { const a = links[i]; try { if (!a || !a.href) continue; const key = getUrlKeyForCache(a.href); if (!key) continue; const u = new URL(key, window.location.href); if (!isSameOriginUrl(u)) continue; if (u.hostname !== window.location.hostname) continue; const memHit = !!(memCacheGet(key) && memCacheGet(key).blob); const readyHit = __preloadReadyKeys.has(key); if (memHit || readyHit) { try { a.dataset.preloaded = '1'; a.dataset.preloadKey = key; } catch {} try { addAVisualLinkReminder(a); } catch {} } } catch {} } } catch {} } // 手势/指示器 let touchStartX = 0; let touchStartY = 0; let iconVisible = false; let divBack; let divForward; let navBackIcon; let navForwardIcon; // UI 面板 let currentActivePanelId = 'panel1'; let isAnimating = false; let loadingPanel; let __footerLastUpdateAt = 0; let showcaseFeaturesPanel; let showcaseFeaturesPanel1; let showcaseFeaturesPanel2; let showcaseFeaturesPanel3; let showcaseFeaturesPanel4; let settingsParameters; let additionalFeatures; let shortcutKeys; let debugPanelTab; let stats; let updateStatsVisibility; const optionsArray = ['下划线', '无样式', '高亮', '品红', '加粗', '边框']; const clickInterceptModeOptions = [ { label: '全拦截', value: 'all' }, { label: '仅命中拦截', value: 'preloaded-only' }, { label: '仅同源', value: 'same-origin' }, { label: '仅白名单', value: 'whitelist-only' }, { label: '关闭拦截', value: 'off' }, ]; const previewRenderModeOptions = [ { label: '内联(兼容优先)', value: 'inline' }, { label: 'Shadow DOM(隔离样式)', value: 'shadow' }, { label: 'Sandbox iframe(最隔离)', value: 'iframe-sandbox' }, ]; const uiDensityOptions = [ { label: '舒适(默认)', value: 'comfortable' }, { label: '紧凑', value: 'compact' }, ]; const SELECTOR_OPTIONS_REGISTRY = Object.freeze({ clickInterceptModeOptions, previewRenderModeOptions, uiDensityOptions, }); function resolveSelectorOptionsVar(varName) { return SELECTOR_OPTIONS_REGISTRY[varName]; } // 监听/Observer 生命周期集中管理(P0) let redirectObserver = null; let preloadDomObserver = null; let cleanupTimerId = null; let __scrollResyncTimerId = null; /* -------------------------------------------------------------------------- */ /* 通用工具 */ /* -------------------------------------------------------------------------- */ // 轻量设置变更通知(同页内),用于跨组件联动 const __settingChangeListeners = new Set(); function onSettingChanged(listener) { try { if (typeof listener === 'function') __settingChangeListeners.add(listener); } catch {} } function emitSettingChanged(key, value) { try { __settingChangeListeners.forEach((fn) => { try { fn(key, value); } catch {} }); } catch {} } function parseSimpleMarkdown(mdText) { let htmlText = mdText; htmlText = htmlText.replace(/\*\*(.*?)\*\*/g, "$1"); // 加粗 htmlText = htmlText.replace(/\*(.*?)\*/g, "$1"); // 斜体 htmlText = htmlText.replace(/##(.*?)\n/g, "

$1

"); // 标题 htmlText = htmlText.replace(/\n/g, "
"); // 换行 htmlText = htmlText.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '
$1'); // 链接 return htmlText; } // UI:开关/设置项的“危险启用确认”白名单(只对极少数高风险项做确认,避免打扰) const DANGEROUS_ENABLE_CONFIRM = Object.freeze({ useLinkPrerender: '该功能更激进,可能导致页面异常或资源占用升高。确认要启用「预渲染」吗?', sandboxAllowScripts: '允许运行脚本风险更高(可能触发弹窗/跳转/站点脚本副作用)。确认要启用吗?', debugConsoleLog: '将日志输出到控制台可能暴露更多运行细节并影响性能。确认要启用吗?', }); // UI:设置搜索(仅影响面板显示,不改设置值) let uiSearchQuery = ''; function normalizeSearchText(s) { return String(s || '').trim().toLowerCase(); } function getSettingItemSearchText(el) { try { const label = String(el?.dataset?.ilLabel || ''); const key = String(el?.dataset?.ilKey || ''); const info = String(el?.dataset?.ilInfo || ''); return `${label} ${key} ${info}`.toLowerCase(); } catch { return ''; } } function applySettingsSearch(query) { uiSearchQuery = normalizeSearchText(query); try { const root = loadingPanel || document; const items = root.querySelectorAll ? root.querySelectorAll('.il-setting-item') : []; let hits = 0; items.forEach((el) => { const text = getSettingItemSearchText(el); const ok = !uiSearchQuery || (text && text.includes(uiSearchQuery)); // 注意:不能用 '' 覆盖原 display,否则会把原本的 flex 变回 block,导致布局换行错乱。 const original = String(el?.dataset?.ilOriginalDisplay || '') || 'flex'; el.style.display = ok ? original : 'none'; if (ok) hits++; }); // 每个 section:若全部子项都被隐藏,则隐藏整个 section(避免空卡片) try { const sections = root.querySelectorAll ? root.querySelectorAll('.il-setting-section') : []; sections.forEach((sec) => { const content = sec.querySelector ? sec.querySelector('.il-setting-section-content') : null; if (!content) return; const visible = Array.from(content.querySelectorAll('.il-setting-item')).some((it) => it.style.display !== 'none'); sec.style.display = (uiSearchQuery && !visible) ? 'none' : ''; }); } catch {} const badge = root.querySelector ? root.querySelector('#instantLoadSearchResultBadge') : null; if (badge) { if (!uiSearchQuery) { badge.style.display = 'none'; } else { badge.style.display = 'inline-flex'; badge.textContent = `命中:${hits}`; } } // 搜索期间:强制展开所有 section,避免“命中但被折叠” const sections2 = root.querySelectorAll ? root.querySelectorAll('.il-setting-section') : []; sections2.forEach((sec) => { const content = sec.querySelector ? sec.querySelector('.il-setting-section-content') : null; const btn = sec.querySelector ? sec.querySelector('.il-setting-section-toggle') : null; if (!content || !btn) return; if (!uiSearchQuery) { // 退出搜索:恢复到存储态 const k = String(sec.dataset.sectionKey || ''); const expanded = k ? !!Storage.get(k, true) : true; content.style.display = expanded ? 'flex' : 'none'; btn.textContent = expanded ? '收起' : '展开'; } else { content.style.display = 'flex'; btn.textContent = '收起'; } }); } catch {} } function applyUiDensityClass() { try { const v = String(Storage.get('uiDensity', 'comfortable') || 'comfortable'); if (!loadingPanel) return; if (v === 'compact') loadingPanel.classList.add('il-density-compact'); else loadingPanel.classList.remove('il-density-compact'); } catch {} } function getRecentPreloadCounts(winMs) { try { const w = Number(winMs || 0); const windowMs = (Number.isFinite(w) && w > 0) ? w : (10 * 60 * 1000); const list = getPreloadStatsList(); const now = Date.now(); const counts = { attempt: 0, enqueue: 0, success: 0, fail: 0 }; for (let i = list.length - 1; i >= 0; i--) { const it = list[i]; if (!it || !it.t) continue; if (now - Number(it.t || 0) > windowMs) break; const e = String(it.e || ''); if (e in counts) counts[e]++; } return counts; } catch { return { attempt: 0, enqueue: 0, success: 0, fail: 0 }; } } function getRecentPreloadMetrics(winMs) { try { const w = Number(winMs || 0); const windowMs = (Number.isFinite(w) && w > 0) ? w : (10 * 60 * 1000); const list = getPreloadStatsList(); const now = Date.now(); let rtOkSum = 0; let rtOkN = 0; let rtFailSum = 0; let rtFailN = 0; for (let i = list.length - 1; i >= 0; i--) { const it = list[i]; if (!it || !it.t) continue; if (now - Number(it.t || 0) > windowMs) break; const e = String(it.e || ''); const rt = Number(it.rt || 0); if (!Number.isFinite(rt) || rt <= 0) continue; if (e === 'success') { rtOkSum += rt; rtOkN += 1; } else if (e === 'fail') { rtFailSum += rt; rtFailN += 1; } } const avgOk = rtOkN ? (rtOkSum / rtOkN) : 0; const avgFail = rtFailN ? (rtFailSum / rtFailN) : 0; return { avgOkMs: avgOk, avgFailMs: avgFail, okN: rtOkN, failN: rtFailN, }; } catch { return { avgOkMs: 0, avgFailMs: 0, okN: 0, failN: 0 }; } } function getDomainQueueLengthsSnapshot() { try { const q = Array.isArray(preloadQueue) ? preloadQueue : []; const map = new Map(); for (let i = 0; i < q.length; i++) { const it = q[i]; const u = it && it.url ? String(it.url) : ''; if (!u) continue; let host = ''; try { host = new URL(u, window.location.href).hostname; } catch { host = ''; } if (!host) continue; map.set(host, Number(map.get(host) || 0) + 1); } return map; } catch { return new Map(); } } function formatTopKFromMap(map, k) { try { const limit = Number.isFinite(k) ? Math.max(1, k) : 3; const entries = Array.from((map instanceof Map) ? map.entries() : []); if (!entries.length) return ''; return entries .filter(([key, val]) => key && Number(val || 0) > 0) .sort((a, b) => Number(b[1] || 0) - Number(a[1] || 0)) .slice(0, limit) .map(([key, val]) => `${escapeHtml(String(key))}(${Number(val || 0)})`) .join(','); } catch { return ''; } } function getHitRateSnapshot(winMs) { try { const counts = getRecentPreloadCounts(winMs); // 口径:以“真正发起过预加载请求”的次数作为分母(attempt)。 // 兼容旧数据:attempt 可能不存在,则回退到 enqueue。 let total = Number(counts.attempt || 0); if (!total) total = Number(counts.enqueue || 0); const ok = Number(counts.success || 0); const fail = Number(counts.fail || 0); if (!total) { return { total: 0, ok: 0, fail: 0, rate: 0 }; } // 防御:旧版本或统计口径不一致可能出现 ok > total,显示上做纠正避免困惑 if (ok > total) total = ok; const rate = ok / Math.max(1, total); return { total, ok, fail, rate }; } catch { return { total: 0, ok: 0, fail: 0, rate: 0 }; } } function formatHitBadgeText() { try { const snap = getHitRateSnapshot(10 * 60 * 1000); if (!snap.total) return '命中:—'; const rate = Math.max(0, Math.min(100, Math.round(snap.rate * 100))); return `命中:${snap.ok}/${snap.total}(${rate}%)`; } catch { return '命中:—'; } } function buildIframeSandboxAttr() { try { const tokens = []; const allowScripts = !!Storage.get('sandboxAllowScripts', false); const allowForms = !!Storage.get('sandboxAllowForms', false); const allowPopups = !!Storage.get('sandboxAllowPopups', false); // 关键:默认不要授予 allow-same-origin。 // 原因:如果用户同时开启 allow-scripts + allow-same-origin,会显著降低 sandbox 隔离强度。 // 这里改为:只有用户显式允许时才添加 allow-same-origin。 if (!!Storage.get('sandboxAllowSameOrigin', true)) tokens.push('allow-same-origin'); if (allowScripts) tokens.push('allow-scripts'); if (allowForms) tokens.push('allow-forms'); if (allowPopups) tokens.push('allow-popups'); return tokens.join(' '); } catch { return ''; } } function sanitizeHtmlForInlinePreview(htmlText, baseUrl, cfg) { try { const opt = cfg && typeof cfg === 'object' ? cfg : {}; const removeScripts = (opt.removeScripts !== undefined) ? !!opt.removeScripts : true; const removeCspMeta = (opt.removeCspMeta !== undefined) ? !!opt.removeCspMeta : true; const addBase = (opt.addBase !== undefined) ? !!opt.addBase : true; const doc = new DOMParser().parseFromString(String(htmlText || ''), 'text/html'); if (!doc || !doc.documentElement) return null; // 移除脚本与潜在危险节点 try { if (removeScripts) doc.querySelectorAll('script, noscript').forEach((n) => n.remove()); } catch {} try { doc.querySelectorAll('iframe, frame, frameset, object, embed').forEach((n) => n.remove()); } catch {} // srcdoc + sandbox 下,页面自带的 CSP meta 常会导致外链样式/字体全部被拦(表现为“只剩纯文字”) // 这里移除 CSP meta,让 sandbox 承担隔离职责。 if (removeCspMeta) { try { doc.querySelectorAll('meta[http-equiv], meta[httpEquiv]').forEach((m) => { try { const v = String(m.getAttribute('http-equiv') || m.getAttribute('httpEquiv') || '').toLowerCase(); if (v === 'content-security-policy') m.remove(); } catch {} }); } catch {} try { doc.querySelectorAll('meta[content]').forEach((m) => { try { const v = String(m.getAttribute('http-equiv') || '').toLowerCase(); if (v === 'content-security-policy') m.remove(); } catch {} }); } catch {} } // 防御:移除 meta refresh(避免预览容器内触发跳转/刷新) try { doc.querySelectorAll('meta[http-equiv], meta[httpEquiv]').forEach((m) => { try { const v = String(m.getAttribute('http-equiv') || m.getAttribute('httpEquiv') || '').toLowerCase(); if (v === 'refresh') m.remove(); } catch {} }); } catch {} // 清理 on* 事件与 javascript: URL try { doc.querySelectorAll('*').forEach((el) => { try { // onxxx for (const attr of Array.from(el.attributes || [])) { const name = String(attr && attr.name ? attr.name : ''); if (!name) continue; if (/^on/i.test(name)) { el.removeAttribute(name); continue; } if ((name === 'href' || name === 'src') && attr && typeof attr.value === 'string') { const v = String(attr.value || '').trim(); if (/^javascript:/i.test(v)) { el.removeAttribute(name); } } } } catch {} }); } catch {} // 防止 rel=stylesheet 之类的相对路径失效:加 if (addBase) { try { const head = doc.head || doc.querySelector('head') || null; if (head) { const existingBase = head.querySelector('base'); if (!existingBase) { const baseEl = doc.createElement('base'); baseEl.href = String(baseUrl || window.location.href); head.insertBefore(baseEl, head.firstChild); } } } catch {} } return doc; } catch { return null; } } function buildShadowPreviewShell(doc) { try { const shell = document.createElement('div'); shell.style.cssText = 'all: initial; display:block; width:100%; height:100%;'; const shadow = shell.attachShadow({ mode: 'open' }); // 基础样式:保证可读性和滚动 const style = document.createElement('style'); style.textContent = [ ':host{ all: initial; }', '*,*::before,*::after{ box-sizing:border-box; }', 'html,body{ margin:0; padding:0; }', '#root{ font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,PingFang SC,Microsoft YaHei,Arial,sans-serif; color:#111827; }' ].join('\n'); shadow.appendChild(style); const root = document.createElement('div'); root.id = 'root'; shadow.appendChild(root); // 将 head 内样式尽量带上(仅 style/link[rel=stylesheet]) try { const head = doc.head || doc.querySelector('head'); if (head) { head.querySelectorAll('style, link[rel="stylesheet"]').forEach((n) => { try { root.appendChild(n.cloneNode(true)); } catch {} }); } } catch {} // body 内容 try { const body = doc.body || doc.querySelector('body'); if (body) { body.childNodes.forEach((n) => { try { root.appendChild(n.cloneNode(true)); } catch {} }); } } catch {} return shell; } catch { return null; } } function isSkippableUrl(url) { if (!url) return true; try { const u = new URL(url, window.location.href); if (SKIP_PROTOCOLS.has(u.protocol)) return true; // 仅锚点跳转(同页不同 hash)不需要预加载 try { const cur = new URL(window.location.href); const sameDoc = (u.origin === cur.origin && u.pathname === cur.pathname && u.search === cur.search); if (sameDoc && u.hash && (u.href.split('#')[0] === cur.href.split('#')[0])) return true; } catch {} return false; } catch { return true; } } function shouldDisablePreloadForUrl(urlObj) { try { if (!urlObj) return false; // 开关:默认开启(更稳,避免网盘/下载站破站) if (!Storage.get('disableHighRiskPreload', true)) return false; return isHighRiskDomain(urlObj.hostname); } catch { return false; } } function isDownloadLikeLink(linkElement, urlObj) { try { if (linkElement && (linkElement.hasAttribute('download') || linkElement.download)) return true; const href = (urlObj && urlObj.href) ? urlObj.href : (linkElement ? linkElement.href : ''); if (!href) return false; return DOWNLOAD_EXT_RE.test(href); } catch { return false; } } function isSameOriginUrl(urlObj) { try { return urlObj.origin === window.location.origin; } catch { return false; } } function cleanTrackingParams(urlObj) { if (!urlObj || !urlObj.searchParams) return urlObj; try { const u = new URL(urlObj.href); // 可选开关:默认关闭 if (!Storage.get('cleanTrackingParams', false)) return u; const keys = Array.from(u.searchParams.keys()); for (const key of keys) { if (URL_CLEAN_TRACKING_KEYS.some(re => re.test(key))) { u.searchParams.delete(key); } } return u; } catch { return urlObj; } } // URL 标准化(用于去重/缓存 key) function normalizeUrlForPreload(rawUrl) { try { const u = new URL(rawUrl, window.location.href); const cleaned = cleanTrackingParams(u); if (Storage.get('dedupStripHash', false)) { cleaned.hash = ''; } return cleaned.href; } catch { return rawUrl; } } function getUrlKeyForCache(rawUrl) { return normalizeUrlForPreload(rawUrl); } let cacheDegraded = false; function updateCacheStatusBadge(targetEl) { try { const el = targetEl || document.getElementById('instantLoadCacheStatus'); if (!el) return; const ok = !!(dbReady && db && !cacheDegraded); const countText = (ok && cacheMeta && Number.isFinite(Number(cacheMeta.items))) ? `(${Number(cacheMeta.items)}条)` : ''; el.textContent = ok ? `缓存:正常${countText}` : '缓存:已降级'; el.style.color = ok ? '#065F46' : '#B45309'; } catch {} } // 并发上限:用户配置为主,运行期按网络/设备做下调 let netSample = { ok: 0, fail: 0, rttMsAvg: 0, rttMsLast: 0, n: 0, lastAt: 0 }; function recordNetSample(ok, rttMs) { try { const rtt = Number(rttMs || 0); const isOk = !!ok; netSample.lastAt = Date.now(); if (isOk) netSample.ok = Number(netSample.ok || 0) + 1; else netSample.fail = Number(netSample.fail || 0) + 1; if (Number.isFinite(rtt) && rtt > 0) { netSample.rttMsLast = rtt; const prevN = Number(netSample.n || 0); const nextN = Math.min(200, prevN + 1); // 指数滑动平均(更稳) const alpha = 0.15; const prevAvg = Number(netSample.rttMsAvg || 0); netSample.rttMsAvg = prevAvg ? (prevAvg * (1 - alpha) + rtt * alpha) : rtt; netSample.n = nextN; } // 只保留“近一段”的统计:过大时做比例缩放 const total = Number(netSample.ok || 0) + Number(netSample.fail || 0); if (total > 200) { const scale = 200 / total; netSample.ok = Math.round(Number(netSample.ok || 0) * scale); netSample.fail = Math.round(Number(netSample.fail || 0) * scale); } } catch {} } function getNetSampleSnapshot() { try { const ok = Number(netSample.ok || 0); const fail = Number(netSample.fail || 0); const total = ok + fail; const failRate = total ? (fail / total) : 0; const rttAvg = Number(netSample.rttMsAvg || 0); return { ok, fail, total, failRate, rttAvg, rttLast: Number(netSample.rttMsLast || 0), lastAt: Number(netSample.lastAt || 0) }; } catch { return { ok: 0, fail: 0, total: 0, failRate: 0, rttAvg: 0, rttLast: 0, lastAt: 0 }; } } // 网络状态(navigator.connection):用于判断是否需要保守调度 let __netConnSnapshot = { ok: false, saveData: false, effectiveType: '', downlink: 0, rtt: 0, lastAt: 0 }; function getConnectionInfoSnapshot() { try { const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (!conn) return { ok: false, saveData: false, effectiveType: '', downlink: 0, rtt: 0, lastAt: 0 }; return { ok: true, saveData: !!conn.saveData, effectiveType: String(conn.effectiveType || ''), downlink: Number(conn.downlink || 0), rtt: Number(conn.rtt || 0), lastAt: Date.now(), }; } catch { return { ok: false, saveData: false, effectiveType: '', downlink: 0, rtt: 0, lastAt: 0 }; } } function refreshConnectionSnapshot() { try { __netConnSnapshot = getConnectionInfoSnapshot(); } catch {} } function setupConnectionAwareness() { try { refreshConnectionSnapshot(); const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (conn && typeof conn.addEventListener === 'function') { conn.addEventListener('change', refreshConnectionSnapshot); } } catch {} try { window.addEventListener('online', refreshConnectionSnapshot); window.addEventListener('offline', refreshConnectionSnapshot); } catch {} } function isNetworkConstrainedForPreload() { try { if (navigator && navigator.onLine === false) return true; } catch {} try { const s = __netConnSnapshot; if (s && s.ok) { if (s.saveData) return true; const et = String(s.effectiveType || '').toLowerCase(); if (et.includes('2g')) return true; if (et.includes('3g') && Number(s.downlink || 0) > 0 && Number(s.downlink || 0) < 0.8) return true; if (Number(s.rtt || 0) > 0 && Number(s.rtt || 0) >= 1800) return true; } } catch {} return false; } function getDynamicConcurrencyCap() { let cap = Number(maxConcurrentPreloads || 1); if (!Number.isFinite(cap) || cap <= 0) cap = 1; try { const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection; if (conn) { if (conn.saveData) cap = Math.min(cap, 1); const et = String(conn.effectiveType || '').toLowerCase(); if (et.includes('2g')) cap = Math.min(cap, 1); else if (et.includes('3g')) cap = Math.min(cap, 2); } } catch {} // 离线/省流/慢网:进一步收敛 try { if (isNetworkConstrainedForPreload()) cap = Math.min(cap, 1); } catch {} // 自适应并发:仅下调,避免抖动 try { if (Storage.get('enableAdaptiveConcurrency', true)) { const s = getNetSampleSnapshot(); if (s.total >= 8) { // 失败率高:强制保守 if (s.failRate >= 0.35) cap = Math.min(cap, 1); else if (s.failRate >= 0.2) cap = Math.min(cap, 2); // RTT 高:下调 if (s.rttAvg >= 1800) cap = Math.min(cap, 1); else if (s.rttAvg >= 900) cap = Math.min(cap, 2); } } } catch {} try { const hc = Number(navigator.hardwareConcurrency || 0); if (hc > 0) { if (hc <= 2) cap = Math.min(cap, 1); else if (hc <= 4) cap = Math.min(cap, 2); } } catch {} try { const dm = Number(navigator.deviceMemory || 0); if (dm > 0 && dm <= 2) cap = Math.min(cap, 1); } catch {} if (cap < 1) cap = 1; return cap; } // 预加载统计(仅本地) function recordPreloadStat(eventName, payload) { try { if (!Storage.get('enablePreloadStats', true)) return; const key = 'preloadStats_v1'; // 需要覆盖“近 10 分钟”的命中率统计:120 很容易被滚动/悬停刷掉导致计数回退 const maxItems = 800; const list = Storage.get(key, []); const item = Object.assign({ t: Date.now(), e: eventName, }, payload || {}); list.push(item); while (list.length > maxItems) list.shift(); Storage.set(key, list); // 回放写入后:若用户正在查看日志面板,则实时刷新 scheduleLogPanelRefresh(); } catch {} } // 候选池(增量维护):减少全量扫描与排序 // key -> Set const preloadCandidateMap = new Map(); let preloadCandidateListDirty = false; // IntersectionObserver:增量收集“接近视口”的候选 let preloadIntersectionObserver = null; function setupPreloadIntersectionObserver() { try { if (!('IntersectionObserver' in window)) return false; if (preloadIntersectionObserver) return true; preloadIntersectionObserver = new IntersectionObserver((entries) => { try { entries.forEach((entry) => { const el = entry && entry.target; if (!el || !el.href) return; // 视口附近(rootMargin 已扩展),就纳入候选池 if (entry.isIntersecting || entry.intersectionRatio > 0) { trackPreloadCandidateFromAnchor(el); } }); } catch {} }, { root: null, // 提前两屏范围 rootMargin: '200% 0px 200% 0px', threshold: 0 }); return true; } catch { preloadIntersectionObserver = null; return false; } } function observeAnchorForPreload(anchorEl) { try { if (!anchorEl || !anchorEl.href) return; if (!setupPreloadIntersectionObserver()) return; preloadIntersectionObserver.observe(anchorEl); } catch {} } function trackPreloadCandidateFromAnchor(anchorEl) { try { if (!anchorEl || !anchorEl.href) return; if (anchorEl.dataset && anchorEl.dataset.preloaded) return; const key = getUrlKeyForCache(anchorEl.href); if (!key) return; // 仅同源候选进入池,减少无意义候选 try { const u = new URL(key, window.location.href); if (u.hostname !== window.location.hostname) return; } catch {} let set = preloadCandidateMap.get(key); if (!set) { set = new Set(); preloadCandidateMap.set(key, set); } if (!set.has(anchorEl)) { set.add(anchorEl); preloadCandidateListDirty = true; } } catch {} } function primeCandidatePool(limit) { try { const lim = Number.isFinite(limit) ? Math.max(0, limit) : 400; let count = 0; const anchors = document.querySelectorAll('a[href]'); for (let i = 0; i < anchors.length; i++) { trackPreloadCandidateFromAnchor(anchors[i]); try { observeAnchorForPreload(anchors[i]); } catch {} count++; if (count >= lim) break; } } catch {} } function rebuildCandidateListIfNeeded() { if (!preloadCandidateListDirty) return; preloadCandidateListDirty = false; // 清理断开的节点,避免长期运行内存增长 try { preloadCandidateMap.forEach((set, key) => { if (!set || !set.size) { preloadCandidateMap.delete(key); return; } set.forEach((el) => { if (!el || el.isConnected === false) set.delete(el); }); if (!set.size) preloadCandidateMap.delete(key); }); } catch {} } // 信息提示(Tooltip):默认“贴近触发控件”定位,避免盖住鼠标造成闪烁;必要时才使用 mouse 跟随。 let activeTooltipBox = null; function clampNumber(value, min, max) { return Math.min(max, Math.max(min, value)); } function clampTooltipToViewport(tooltipEl, left, top, padding = 8) { const viewportW = window.innerWidth || document.documentElement.clientWidth || 0; const viewportH = window.innerHeight || document.documentElement.clientHeight || 0; const rect = tooltipEl.getBoundingClientRect(); const leftMin = padding; const leftMax = Math.max(padding, viewportW - rect.width - padding); const topMin = padding; const topMax = Math.max(padding, viewportH - rect.height - padding); return { left: clampNumber(left, leftMin, leftMax), top: clampNumber(top, topMin, topMax) }; } function positionTooltipNearElement(tooltipEl, anchorEl) { if (!tooltipEl || !anchorEl) return; const anchorRect = anchorEl.getBoundingClientRect(); const gap = 10; // 先显示但隐藏,用于测量尺寸 tooltipEl.style.visibility = 'hidden'; tooltipEl.style.display = 'block'; tooltipEl.style.position = 'fixed'; tooltipEl.style.transform = 'none'; // 优先放在右侧;放不下则放左侧 let left = anchorRect.right + gap; let top = anchorRect.top + anchorRect.height / 2; // 先粗定位,再测量并修正 tooltipEl.style.left = `${left}px`; tooltipEl.style.top = `${top}px`; const rect = tooltipEl.getBoundingClientRect(); top = top - rect.height / 2; const viewportW = window.innerWidth || document.documentElement.clientWidth || 0; if (left + rect.width + 8 > viewportW) { left = anchorRect.left - rect.width - gap; } const clamped = clampTooltipToViewport(tooltipEl, left, top); tooltipEl.style.left = `${clamped.left}px`; tooltipEl.style.top = `${clamped.top}px`; tooltipEl.style.visibility = 'visible'; } document.addEventListener('mousemove', function(e) { if (!activeTooltipBox) return; if (activeTooltipBox.dataset.positionMode !== 'mouse') return; const offsetX = 16; const offsetY = 16; const padding = 8; const mouseX = e.clientX; const mouseY = e.clientY; let targetLeft = mouseX + offsetX; let targetTop = mouseY + offsetY; activeTooltipBox.style.left = `${targetLeft}px`; activeTooltipBox.style.top = `${targetTop}px`; const clamped = clampTooltipToViewport(activeTooltipBox, targetLeft, targetTop, padding); activeTooltipBox.style.left = `${clamped.left}px`; activeTooltipBox.style.top = `${clamped.top}px`; }); /** * 添加一个新的样式标签到文档的头部,如果已有同名ID的标签则不执行添加。 * * @param {string} styleId - 样式标签的ID,用于检查是否已经存在具有相同ID的样式。 * @param {string} cssRules - 要添加的CSS规则的字符串表示,用于设置style标签的内容。 */ function addStyle(styleId, cssRules) { if (!document.getElementById(styleId)) { const style = document.createElement('style'); style.id = styleId; style.setAttribute('type', 'text/css'); style.innerHTML = cssRules; document.head.appendChild(style); } } function createElementWithStylesAndAttributes(tag, styles, attributes) { let element = document.createElement(tag); if (styles) { Object.assign(element.style, styles); } if (attributes) { for (const key in attributes) { if (attributes.hasOwnProperty(key)) { if (key in element) { element[key] = attributes[key]; } else { element.setAttribute(key, attributes[key]); } } } } return element; } /* ------------------------------- 以下是脚本的设置面板函数 ------------------------------- */ /** * 主动画循环。 */ function animate() { if (loadingPanel.style.display === 'block') { // 仅在启用且可见时更新监控器,避免无意义的持续计算 if (stats && Storage.get('showPerformanceMonitor', false) && currentActivePanelId === 'panel2') { stats.update(); } // 底部状态栏:轻量刷新(节流到 ~4fps,避免每帧读写 DOM) try { const now = Date.now(); if (!__footerLastUpdateAt || (now - __footerLastUpdateAt >= 250)) { __footerLastUpdateAt = now; const qEl = document.getElementById('instantLoadQueueStatus'); if (qEl) { // 队列:仅表示“等待处理”的排队数量(并发中的数量在“并发”徽标里显示) const q = (Array.isArray(preloadQueue) ? preloadQueue.length : 0); qEl.textContent = `队列:${q}`; } const iEl = document.getElementById('instantLoadInFlightStatus'); if (iEl) { iEl.textContent = `并发:${Number(currentPreloads || 0)}/${Number(maxConcurrentPreloads || 0)}`; iEl.style.color = (Number(currentPreloads || 0) > 0) ? '#1D4ED8' : '#4B5563'; } const hEl = document.getElementById('instantLoadHitStatus'); if (hEl) { hEl.textContent = formatHitBadgeText(); const snap = getHitRateSnapshot(10 * 60 * 1000); const rate = Number(snap.rate || 0); hEl.style.color = snap.total ? (rate >= 0.6 ? '#065F46' : rate >= 0.3 ? '#B45309' : '#B91C1C') : '#4B5563'; } } } catch {} } requestAnimationFrame(animate); } /** * 检查是否有更新。 */ function checkForUpdates() { var lastCheckedTime = Storage.get('lastCheckedTime', 0); var currentTime = Date.now(); if (currentTime - lastCheckedTime >= SCRIPT_META.updateCheckIntervalMs) { Storage.set('lastCheckedTime', currentTime); fetch(SCRIPT_META.updateMetaUrl).then(function(response) { response.text().then(function(text) { try { var m = String(text || '').match(/@version\s+([^\n]+)/); var latestVersion = (m && m[1]) ? String(m[1]).trim() : ''; if (latestVersion) { Storage.set('latestVersion', latestVersion); } } catch {} }); }).catch(function(error) { console.error('An error occurred while checking for updates:', error); }); } } /** * 创建特性列表项。 * * @param {Array} features - 特性描述列表。 * @returns {Array} - 转换为HTML元素列表的数组。 */ function createFeatureListItems(features) { return features.map(feature => { let parts = feature.split(',').map(part => part.trim()); // 特殊:启用操作球(右侧增加“展开/收起”样式面板) // - 展开:强制显示操作球,便于实时预览样式 // - 收起:回到原本显示逻辑(预览页显示;如果用户曾展开过,则非预览页也不强制隐藏) if (parts[1] === 'switch' && parts[2] === 'manipulatorBall') { const labelText = parts[0]; const keyName = 'manipulatorBall'; const expandedKey = 'manipulatorBallStyleExpanded'; const infoText = parts.includes('information') ? (parts[4] || '') : ''; const container = createElementWithStylesAndAttributes('div', { display: 'flex', flexDirection: 'column', width: 'calc(100% - 6px)', marginTop: '3px', marginLeft: '3px', marginRight: '3px', boxSizing: 'border-box', gap: '6px', }); try { container.classList.add('il-setting-item'); container.dataset.ilLabel = String(labelText || ''); container.dataset.ilKey = String(keyName); if (infoText) container.dataset.ilInfo = String(infoText); container.dataset.ilOriginalDisplay = 'flex'; } catch {} const headerRow = createElementWithStylesAndAttributes('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: '100%', gap: '8px', }); const leftRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', gap: '6px', minWidth: '0', }); const labelBox = createElementWithStylesAndAttributes('div', { width: '85px', height: '32px', lineHeight: '32px', backgroundColor: '#E0E5EC', borderRadius: '5px', boxShadow: 'inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF', textAlign: 'center', userSelect: 'none', whiteSpace: 'nowrap', fontSize: '14px', flex: '0 0 auto', }, { innerText: labelText }); leftRow.appendChild(labelBox); if (infoText) { let infoIcon = createElementWithStylesAndAttributes("div", { position: 'relative', cursor: 'pointer', display: 'flex', alignItems: 'left', justifyContent: 'center' }, { innerHTML: `` }); let infoBox = createElementWithStylesAndAttributes("div", { width: "300px", maxWidth: "calc(100vw - 16px)", maxHeight: "calc(100vh - 16px)", height: "auto", overflow: "auto", display: "none", position: "fixed", transform: "none", backgroundColor: "#E0E5EC", borderRadius: "12px", boxShadow: "2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF", zIndex: "2000" }); let parsedContent = parseSimpleMarkdown(infoText); let infoTextEl = createElementWithStylesAndAttributes("div", { padding: "10px", margin: "0", fontSize: "15px", fontWeight: "bold", textAlign: "left", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", color: "#4B5563" }, { innerHTML: parsedContent }); infoBox.appendChild(infoTextEl); document.body.appendChild(infoBox); leftRow.appendChild(infoIcon); infoIcon.addEventListener('mouseover', function() { infoBox.dataset.positionMode = 'anchor'; activeTooltipBox = infoBox; positionTooltipNearElement(infoBox, infoIcon); }); infoIcon.addEventListener('mouseout', function() { infoBox.style.display = 'none'; infoBox.style.visibility = 'visible'; if (activeTooltipBox === infoBox) activeTooltipBox = null; }); } const rightRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '8px', flex: '0 0 auto', }); const mainSwitch = createUIComponent('switch', { checked: !!Storage.get(keyName, true), key: keyName }); const expandBtn = createElementWithStylesAndAttributes('button', { height: '26px', padding: '0 10px', borderRadius: '999px', border: 'none', cursor: 'pointer', background: '#E0E5EC', boxShadow: '2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF', color: '#4B5563', fontSize: '12px', fontWeight: '700', userSelect: 'none' }, { innerText: Storage.get(expandedKey, false) ? '收起' : '展开' }); rightRow.appendChild(mainSwitch); rightRow.appendChild(expandBtn); headerRow.appendChild(leftRow); headerRow.appendChild(rightRow); container.appendChild(headerRow); addStyle('instantLoad-manipulatorBall-style-controls', ` /* 说明:这里用“固定左侧标签 + 中间滑条自适应 + 右侧数值固定”,避免小宽度时左侧文字被裁掉 */ .il-mb-wrap { display:flex; flex-direction: column; gap: 8px; padding-left: 10px; box-sizing: border-box; } /* 用 grid 彻底避免溢出:中间滑条 minmax(0,1fr) 自适应,左右两列可在小宽度下收缩 */ .il-mb-row { display: grid; grid-template-columns: minmax(72px, 88px) minmax(0, 1fr) minmax(44px, 52px); gap: 8px; align-items: center; width: 100%; box-sizing: border-box; } .il-mb-label { font-size: 13px; color: #4B5563; user-select:none; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; } .il-mb-range { min-width: 0; } .il-mb-range input[type="range"] { width: 100%; } /* 注意:必须用 border-box,否则 width + padding 会把输入框挤出容器 */ .il-mb-value { width: 100%; box-sizing: border-box; height: 28px; padding: 0 4px; border: none; outline: none; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; font-size: 12px; color: #374151; text-align: center; min-width: 0; } .il-mb-btnrow { display:flex; justify-content: flex-end; gap: 8px; width: 100%; } .il-mb-btn { height: 28px; padding: 0 10px; border-radius: 10px; border: none; cursor: pointer; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; font-size: 12px; font-weight: 800; color: #4B5563; user-select:none; } .il-mb-btn:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } `); const clampNum = (v, min, max, fallback) => { const n = Number(v); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, n)); }; const setStorageAndEmit = (k, v) => { try { Storage.set(k, v); } catch {} try { emitSettingChanged(k, v); } catch {} }; const applyNow = () => { try { applyManipulatorBallStyle(); } catch {} try { toggleDragIconVisibility(!!document.getElementById('fullPageDiv')); } catch {} }; const createSliderRow = (label, key, opt) => { const row = createElementWithStylesAndAttributes('div', {}, { className: 'il-mb-row' }); const text = createElementWithStylesAndAttributes('div', {}, { className: 'il-mb-label', innerText: label }); const rangeWrap = createElementWithStylesAndAttributes('div', {}, { className: 'il-mb-range' }); const range = document.createElement('input'); range.type = 'range'; range.min = String(opt.min); range.max = String(opt.max); range.step = String(opt.step); const stored = Storage.get(key, opt.defaultValue); const cur = clampNum(stored, opt.min, opt.max, opt.defaultValue); range.value = String(cur); const valueInput = createElementWithStylesAndAttributes('input', {}, { className: 'il-mb-value', type: 'text', value: '' }); const formatValue = (n) => { if (opt.decimals !== undefined) return String(Number(n).toFixed(opt.decimals)); return String(n); }; const syncValue = (n) => { const v = clampNum(n, opt.min, opt.max, opt.defaultValue); valueInput.value = formatValue(v); range.value = String(v); setStorageAndEmit(key, v); applyNow(); }; valueInput.value = formatValue(cur); range.addEventListener('input', () => syncValue(range.value)); valueInput.addEventListener('change', () => { const parsed = Number(String(valueInput.value || '').trim()); if (!Number.isFinite(parsed)) { valueInput.value = formatValue(Storage.get(key, opt.defaultValue)); return; } syncValue(parsed); }); rangeWrap.appendChild(range); row.appendChild(text); row.appendChild(rangeWrap); row.appendChild(valueInput); return row; }; const childrenWrap = createElementWithStylesAndAttributes('div', {}, { className: 'il-mb-wrap' }); childrenWrap.style.display = Storage.get(expandedKey, false) ? 'flex' : 'none'; childrenWrap.appendChild(createSliderRow('大小(px)', 'manipulatorBallSize', { min: 32, max: 96, step: 1, defaultValue: 52, decimals: 0 })); childrenWrap.appendChild(createSliderRow('圆角(px)', 'manipulatorBallRadius', { min: 8, max: 32, step: 1, defaultValue: 16, decimals: 0 })); childrenWrap.appendChild(createSliderRow('透明度', 'manipulatorBallOpacity', { min: 0.2, max: 1, step: 0.01, defaultValue: 0.96, decimals: 2 })); childrenWrap.appendChild(createSliderRow('背景透明度', 'manipulatorBallBgAlpha', { min: 0.2, max: 1, step: 0.01, defaultValue: 0.88, decimals: 2 })); childrenWrap.appendChild(createSliderRow('边框透明度', 'manipulatorBallBorderAlpha', { min: 0, max: 1, step: 0.01, defaultValue: 0.55, decimals: 2 })); childrenWrap.appendChild(createSliderRow('模糊(px)', 'manipulatorBallBlur', { min: 0, max: 20, step: 1, defaultValue: 8, decimals: 0 })); childrenWrap.appendChild(createSliderRow('阴影强度', 'manipulatorBallShadowStrength', { min: 0, max: 1.6, step: 0.05, defaultValue: 1, decimals: 2 })); childrenWrap.appendChild(createSliderRow('悬停缩放', 'manipulatorBallHoverScale', { min: 1, max: 1.12, step: 0.01, defaultValue: 1.02, decimals: 2 })); childrenWrap.appendChild(createSliderRow('上浮(px)', 'manipulatorBallHoverLiftPx', { min: 0, max: 8, step: 1, defaultValue: 1, decimals: 0 })); const btnRow = createElementWithStylesAndAttributes('div', {}, { className: 'il-mb-btnrow' }); const resetBtn = createElementWithStylesAndAttributes('button', {}, { className: 'il-mb-btn', innerText: '恢复默认' }); resetBtn.addEventListener('click', () => { const defaults = { manipulatorBallSize: 52, manipulatorBallRadius: 16, manipulatorBallOpacity: 0.96, manipulatorBallBgAlpha: 0.88, manipulatorBallBorderAlpha: 0.55, manipulatorBallBlur: 8, manipulatorBallShadowStrength: 1, manipulatorBallHoverScale: 1.02, manipulatorBallHoverLiftPx: 1, }; Object.keys(defaults).forEach((k) => { try { setStorageAndEmit(k, defaults[k]); } catch {} }); applyNow(); showAlert('已恢复默认样式'); }); btnRow.appendChild(resetBtn); childrenWrap.appendChild(btnRow); container.appendChild(childrenWrap); const refreshExpanded = () => { const expanded = !!Storage.get(expandedKey, false); childrenWrap.style.display = expanded ? 'flex' : 'none'; expandBtn.innerText = expanded ? '收起' : '展开'; applyNow(); }; expandBtn.addEventListener('click', () => { const next = !Storage.get(expandedKey, false); setStorageAndEmit(expandedKey, next); refreshExpanded(); }); // 外部导入/变更时同步 try { onSettingChanged((k) => { const kk = String(k || ''); if (kk === expandedKey) refreshExpanded(); }); } catch {} // 初次渲染时应用一次(避免用户已经自定义过但样式未刷新) try { applyNow(); } catch {} return container; } // 特殊:折叠子开关组(总开关 + 子开关),由组件自身负责布局 if (parts[1] === 'switchGroup') { const key = parts[2]; const infoText = parts.includes('information') ? (parts[4] || '') : ''; return createUIComponent('switchGroup', { label: parts[0], key, infoText }); } // 特殊:折叠子开关组(无总开关,按外部条件启用/禁用) if (parts[1] === 'switchGroupCustom') { const key = parts[2]; const infoText = parts.includes('information') ? (parts[4] || '') : ''; const renderChildRow = (label, keyName) => { const row = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', width: '100%' }); const text = createElementWithStylesAndAttributes('div', { fontSize: '13px', color: '#4B5563', userSelect: 'none', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: '1 1 auto' }, { innerText: label }); const sw = createUIComponent('switch', { checked: !!Storage.get(keyName, false), key: keyName }); row.appendChild(text); row.appendChild(sw); return row; }; // 仅当选择了 sandbox iframe 模式才允许配置 const isEnabled = () => { try { return String(Storage.get('previewRenderMode', 'inline') || 'inline') === 'iframe-sandbox'; } catch { return false; } }; const children = [ renderChildRow('允许运行脚本(风险更高)', 'sandboxAllowScripts'), renderChildRow('允许同源(样式更完整,隔离更弱)', 'sandboxAllowSameOrigin'), renderChildRow('允许表单提交', 'sandboxAllowForms'), renderChildRow('允许弹窗', 'sandboxAllowPopups'), ]; return createUIComponent('switchGroupCustom', { label: parts[0], key, infoText, children, isEnabled, }); } // 特殊:站点规则可视化编辑器(clickInterceptDomainRules) if (parts[1] === 'domainRulesEditor') { return createUIComponent('domainRulesEditor', { key: parts[2] }); } let listItem = createElementWithStylesAndAttributes('div', { width: '85px', height: '32px', lineHeight: '32px', backgroundColor: '#E0E5EC', margin: '3px 0', borderRadius: '5px', boxShadow: 'inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF', textAlign: 'center', userSelect: 'none', whiteSpace: 'nowrap', fontSize: '14px', }, { innerText: parts[0] }); let featureContainer = createElementWithStylesAndAttributes('div', { display: 'flex', justifyContent: 'space-between', alignItems: 'center', width: 'calc(100% - 6px)', marginTop: '3px', marginLeft: '3px', marginRight: '3px', boxSizing: 'border-box' }); // 供 UI 搜索过滤使用 try { featureContainer.classList.add('il-setting-item'); featureContainer.dataset.ilLabel = String(parts[0] || ''); featureContainer.dataset.ilKey = String(parts[2] || ''); if (parts.includes('information')) featureContainer.dataset.ilInfo = String(parts[4] || ''); featureContainer.dataset.ilOriginalDisplay = 'flex'; } catch {} featureContainer.appendChild(listItem); if (parts.includes('information')) { let infoIcon = createElementWithStylesAndAttributes("div", { position: 'relative', cursor: 'pointer', display: 'flex', alignItems: 'left', justifyContent: 'center' }, { innerHTML: `` }); let infoBox = createElementWithStylesAndAttributes("div", { width: "300px", maxWidth: "calc(100vw - 16px)", maxHeight: "calc(100vh - 16px)", height: "auto", overflow: "auto", display: "none", position: "fixed", transform: "none", backgroundColor: "#E0E5EC", borderRadius: "12px", boxShadow: "2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF", zIndex: "2000" }); let parsedContent = parseSimpleMarkdown(parts[4]); let infoText = createElementWithStylesAndAttributes("div", { padding: "10px", margin: "0", fontSize: "15px", fontWeight: "bold", textAlign: "left", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", color: "#4B5563" }, { innerHTML: parsedContent }); infoBox.appendChild(infoText); document.body.appendChild(infoBox); featureContainer.appendChild(infoIcon); infoIcon.addEventListener('mouseover', function() { infoBox.dataset.positionMode = 'anchor'; activeTooltipBox = infoBox; positionTooltipNearElement(infoBox, infoIcon); }); infoIcon.addEventListener('mouseout', function() { infoBox.style.display = 'none'; infoBox.style.visibility = 'visible'; if (activeTooltipBox === infoBox) activeTooltipBox = null; }); // ⭐⭐⭐ 依赖/互斥可视化:显示“原因提示”,而不是纯灰化 try { const disabledWhen = parts.includes('disabledWhen') ? (parts[parts.indexOf('disabledWhen') + 1] || '') : ''; if (disabledWhen) { const evaluate = (expr) => { try { // 支持的表达式: // - key=value // - key!=value // - key(truthy) / !key(falsy) const e = String(expr || '').trim(); if (!e) return { disabled: false, reason: '' }; const pairs = e.split('|').map(s => s.trim()).filter(Boolean); // 任一规则命中即禁用 for (const p of pairs) { const m1 = p.match(/^([^!=]+)\s*!=\s*(.+)$/); if (m1) { const k = String(m1[1] || '').trim(); const v = String(m1[2] || '').trim(); const cur = String(Storage.get(k, '') ?? '').trim(); if (cur !== v) return { disabled: true, reason: '' }; continue; } const m2 = p.match(/^([^=]+)\s*=\s*(.+)$/); if (m2) { const k = String(m2[1] || '').trim(); const v = String(m2[2] || '').trim(); const cur = String(Storage.get(k, '') ?? '').trim(); if (cur === v) return { disabled: true, reason: '' }; continue; } if (p.startsWith('!')) { const k = String(p.slice(1)).trim(); if (!!Storage.get(k, false)) return { disabled: true, reason: '' }; } else { const k = String(p).trim(); if (!Storage.get(k, false)) return { disabled: true, reason: '' }; } } return { disabled: false, reason: '' }; } catch { return { disabled: false, reason: '' }; } }; const reasonText = parts.includes('disabledReason') ? (parts[parts.indexOf('disabledReason') + 1] || '') : ''; const refreshDisabled = () => { const res = evaluate(disabledWhen); const isDisabled = !!res.disabled; featureContainer.style.opacity = isDisabled ? '0.55' : '1'; featureContainer.style.pointerEvents = isDisabled ? 'none' : 'auto'; if (isDisabled) { infoIcon.style.pointerEvents = 'auto'; infoIcon.style.opacity = '1'; if (reasonText) { const reasonMd = `**不可用原因**\n\n${reasonText}`; infoText.innerHTML = parseSimpleMarkdown(reasonMd + "\n\n---\n\n" + parts[4]); } } else { infoText.innerHTML = parseSimpleMarkdown(parts[4]); } }; refreshDisabled(); onSettingChanged(() => { refreshDisabled(); }); } } catch {} } if (parts.includes('styleSelector')) {6 addStyle("neumorphic-checkbox-style", ` .neumorphic-checkbox { -webkit-appearance: none; appearance: none; background-color: #e0e5ec; margin: 0; font: inherit; color: currentColor; width: 30px; height: 30px; border: 2px solid #d1d9e6; border-radius: 4px; transform: translateY(-0.075em); display: grid; place-content: center; } .neumorphic-checkbox { background-color: #ff3b3b; /* red for false */ } .neumorphic-checkbox:checked { background-color: #3bff3b; /* green for true */ border-color: #28a745; box-shadow: inset 3px 3px 5px #b8c4d8, inset -3px -3px 5px #ffffff; } .neumorphic-checkbox + span { vertical-align: middle; } `); let Selector = Storage.get(parts[6]); let styleSelectorCheckbox = createElementWithStylesAndAttributes("input", {}, { type: "checkbox", checked: Selector || false, className: "neumorphic-checkbox" }); styleSelectorCheckbox.addEventListener("change", function() { Storage.set(parts[6], styleSelectorCheckbox.checked); // 将背景颜色设置为红色或绿色取决于复选框的状态 styleSelectorCheckbox.style.backgroundColor = styleSelectorCheckbox.checked ? '#3bff3b' : '#ff3b3b'; }); // 设置初始背景颜色 styleSelectorCheckbox.style.backgroundColor = Selector ? '#3bff3b' : '#ff3b3b'; let checkboxContainer = createElementWithStylesAndAttributes("label", { display: 'flex', alignItems: 'center', justifyContent: 'space-between' }); checkboxContainer.appendChild(styleSelectorCheckbox); featureContainer.appendChild(checkboxContainer); }; if (parts.includes('settingButton')) { const settingButtonContainer = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'center', width: '80px', height: '32px', margin: '5px 0' }); const settingButton = createElementWithStylesAndAttributes('div', { cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', width: '24px', height: '24px', }, { innerHTML: `` }); settingButton.addEventListener('click', function() { // 使用统一的轻量弹窗(不再占用左侧切换栏空间,切换下沉到底部) addStyle('instantLoadRedirectDebugModalStyles', ` .il-rd-root { display:flex; flex-direction:column; gap: 10px; } .il-rd-content { display:flex; flex-direction:column; gap: 10px; } .il-rd-card { background:#E0E5EC; border-radius: 12px; box-shadow: inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF; padding: 10px; box-sizing: border-box; } .il-rd-row { display:flex; align-items:center; justify-content: space-between; gap: 10px; } .il-rd-title { font-size: 12px; font-weight: 900; color:#111827; user-select:none; } .il-rd-muted { font-size: 11px; color:#6B7280; line-height:1.35; word-break: break-word; overflow-wrap:anywhere; } .il-rd-kv { display:flex; flex-direction: column; gap: 4px; } .il-rd-url { font-size: 11px; color:#111827; word-break: break-word; overflow-wrap:anywhere; } .il-rd-actions { display:flex; gap: 8px; flex-wrap: wrap; justify-content: flex-end; } .il-rd-btn { height: 28px; padding: 0 10px; border-radius: 10px; border: none; cursor: pointer; background: #E0E5EC; box-shadow: 2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF; font-size: 12px; font-weight: 800; color: #374151; user-select:none; } .il-rd-btn:hover { box-shadow: inset 2px 2px 4px #AEBEC7, inset -2px -2px 4px #FFFFFF; } .il-rd-btn:disabled { opacity: 0.55; cursor: not-allowed; } .il-rd-seg { width: 100%; display:flex; gap: 8px; position: sticky; bottom: 0; z-index: 2; padding: 8px; border-radius: 999px; background: rgba(224,229,236,0.92); backdrop-filter: blur(8px); box-shadow: 2px 2px 4px rgba(174,190,199,0.65), -2px -2px 4px rgba(255,255,255,0.65); box-sizing: border-box; } .il-rd-seg-btn { flex: 1 1 50%; height: 30px; border-radius: 999px; border: none; cursor: pointer; background: #E0E5EC; box-shadow: 2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF; font-size: 12px; font-weight: 900; color:#4B5563; user-select:none; } .il-rd-seg-btn.il-active { color:#111827; box-shadow: inset 2px 2px 4px #AEBEC7, inset -2px -2px 4px #FFFFFF; } .il-rd-chip-wrap { width: 100%; max-height: 140px; overflow:auto; display:flex; flex-wrap: wrap; gap: 6px; padding: 8px; border-radius: 10px; background:#E0E5EC; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; box-sizing:border-box; } .il-rd-chip { display:inline-flex; align-items:center; gap: 6px; padding: 4px 8px; border-radius: 999px; background: #E0E5EC; box-shadow: 2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF; font-size: 12px; color:#374151; max-width: 100%; } .il-rd-chip-text { overflow:hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 240px; } .il-rd-chip-x { cursor:pointer; user-select:none; font-weight: 900; color:#6B7280; } .il-rd-chip-x:hover { color:#B91C1C; } .il-rd-input-row { display:flex; gap: 8px; align-items:center; } .il-rd-input { flex: 1 1 auto; height: 30px; padding: 0 10px; border: none; outline: none; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; font-size: 13px; color:#374151; min-width: 0; } .il-rd-bulk { display:none; flex-direction: column; gap: 6px; } .il-rd-ta { width: 100%; height: 120px; padding: 8px 10px; border: none; outline: none; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; font-size: 12px; color:#374151; resize: vertical; box-sizing:border-box; } `); const el = (tag, styles, attrs) => createElementWithStylesAndAttributes(tag, styles || {}, attrs || {}); const sanitizeParamKey = (raw) => { try { let t = String(raw ?? '').trim(); if (!t) return ''; // 支持粘贴 "url=xxx" / "?url=xxx" / "&url=xxx" 之类:只取 key t = t.replace(/^[?&]/, ''); const eq = t.indexOf('='); if (eq > 0) t = t.slice(0, eq); // 去掉首尾引号/逗号 t = t.replace(/^['"\s,]+/, '').replace(/['"\s,]+$/, ''); return t.trim(); } catch { return ''; } }; const parseParamList = (text) => { const raw = String(text ?? '').trim(); if (!raw) return []; const parts = raw.split(/[\n,;\s]+/).map((s) => sanitizeParamKey(s)).filter(Boolean); const out = []; parts.forEach((p) => { if (!out.includes(p)) out.push(p); }); return out; }; let activeTab = 'links'; let workingParams = (() => { try { const v = Storage.get('queryItemsList', []); return Array.isArray(v) ? [...v] : []; } catch { return []; } })(); const root = el('div', {}, { className: 'il-rd-root' }); const content = el('div', {}, { className: 'il-rd-content' }); const seg = el('div', {}, { className: 'il-rd-seg' }); const btnLinks = el('button', {}, { className: 'il-rd-seg-btn il-active', innerText: '本站链接' }); const btnParams = el('button', {}, { className: 'il-rd-seg-btn', innerText: '参数设置' }); seg.appendChild(btnLinks); seg.appendChild(btnParams); const renderLinksTab = () => { content.innerHTML = ''; const stored = (() => { try { const v = Storage.get('preloadedLinks', []); return Array.isArray(v) ? v : []; } catch { return []; } })(); const headerCard = el('div', {}, { className: 'il-rd-card' }); const headerRow = el('div', {}, { className: 'il-rd-row' }); headerRow.appendChild(el('div', {}, { className: 'il-rd-title', innerText: `共 ${stored.length} 条` })); const headerActions = el('div', {}, { className: 'il-rd-actions' }); const copyAllBtn = el('button', {}, { className: 'il-rd-btn', innerText: '复制全部(优化后)' }); copyAllBtn.disabled = !stored.length; copyAllBtn.addEventListener('click', () => { try { const lines = stored.map((it) => String(it && (it.optimizedUrl || it.url) || '')).filter(Boolean); tryCopyText(lines.join('\n')); } catch { showAlert('复制失败'); } }); const clearBtn = el('button', {}, { className: 'il-rd-btn', innerText: '清空记录' }); clearBtn.disabled = !stored.length; clearBtn.addEventListener('click', () => { try { Storage.set('preloadedLinks', []); try { preloadedLinks = []; } catch {} renderLinksTab(); showAlert('已清空'); } catch { showAlert('清空失败'); } }); headerActions.appendChild(copyAllBtn); headerActions.appendChild(clearBtn); headerRow.appendChild(headerActions); headerCard.appendChild(headerRow); headerCard.appendChild(el('div', {}, { className: 'il-rd-muted', innerText: '仅展示本页已被识别并“净化”的重定向链接记录。' })); content.appendChild(headerCard); if (!stored.length) { const empty = el('div', {}, { className: 'il-rd-card' }); empty.appendChild(el('div', {}, { className: 'il-rd-muted', innerText: '(空)页面产生重定向净化后,这里会显示原链接与优化后链接。' })); content.appendChild(empty); return; } stored.forEach((it, idx) => { const card = el('div', {}, { className: 'il-rd-card' }); const top = el('div', {}, { className: 'il-rd-row' }); const title = el('div', {}, { className: 'il-rd-title', innerText: `${idx + 1}. ${String(it && it.name ? it.name : '') || '(无标题)'}` }); title.title = String(it && it.name ? it.name : ''); const meta = el('div', {}, { className: 'il-rd-muted', innerText: it && it.redirectParameter ? `参数:${String(it.redirectParameter)}` : '参数:—' }); top.appendChild(title); top.appendChild(meta); card.appendChild(top); const kv = el('div', {}, { className: 'il-rd-kv' }); const orig = String(it && it.url ? it.url : ''); const opt = String(it && (it.optimizedUrl || it.url) ? (it.optimizedUrl || it.url) : ''); const mkLine = (label, value, copyTextValue) => { const line = el('div', {}, { className: 'il-rd-row' }); const left = el('div', {}, { className: 'il-rd-muted', innerText: label }); const right = el('div', { display: 'flex', gap: '8px', alignItems: 'center' }); const url = el('div', {}, { className: 'il-rd-url', innerText: String(value || '—') }); url.title = String(value || ''); const copyBtn = el('button', {}, { className: 'il-rd-btn', innerText: '复制' }); copyBtn.disabled = !String(copyTextValue || '').trim(); copyBtn.addEventListener('click', () => { try { tryCopyText(String(copyTextValue || '')); } catch { showAlert('复制失败'); } }); const openBtn = el('button', {}, { className: 'il-rd-btn', innerText: '打开' }); openBtn.disabled = !String(value || '').trim(); openBtn.addEventListener('click', () => { try { window.open(String(value || ''), '_blank', 'noopener,noreferrer'); } catch { showAlert('打开失败'); } }); right.appendChild(copyBtn); right.appendChild(openBtn); line.appendChild(left); line.appendChild(right); const block = el('div', {}, {}); block.appendChild(line); block.appendChild(url); return block; }; kv.appendChild(mkLine('原链接', orig, orig)); kv.appendChild(mkLine('优化后', opt, opt)); card.appendChild(kv); content.appendChild(card); }); }; const renderParamsTab = () => { content.innerHTML = ''; const card = el('div', {}, { className: 'il-rd-card' }); const rowTop = el('div', {}, { className: 'il-rd-row' }); rowTop.appendChild(el('div', {}, { className: 'il-rd-title', innerText: `已配置 ${workingParams.length} 项` })); const actions = el('div', {}, { className: 'il-rd-actions' }); const copyBtn = el('button', {}, { className: 'il-rd-btn', innerText: '复制列表' }); copyBtn.disabled = !workingParams.length; copyBtn.addEventListener('click', () => { try { tryCopyText(workingParams.join(',')); } catch { showAlert('复制失败'); } }); const resetBtn = el('button', {}, { className: 'il-rd-btn', innerText: '恢复默认' }); resetBtn.addEventListener('click', () => { try { const base = (typeof queryItems !== 'undefined' && Array.isArray(queryItems)) ? queryItems : []; const next = []; base.forEach((k) => { const v = sanitizeParamKey(k); if (v && !next.includes(v)) next.push(v); }); workingParams = next; renderParamsTab(); } catch { showAlert('恢复失败'); } }); const saveBtn = el('button', {}, { className: 'il-rd-btn', innerText: '保存' }); saveBtn.addEventListener('click', () => { try { Storage.set('queryItemsList', Array.isArray(workingParams) ? workingParams : []); try { queryItemsList = Array.isArray(workingParams) ? [...workingParams] : []; } catch {} showAlert('重定向参数保存成功'); } catch { showAlert('保存失败'); } }); actions.appendChild(copyBtn); actions.appendChild(resetBtn); actions.appendChild(saveBtn); rowTop.appendChild(actions); card.appendChild(rowTop); card.appendChild(el('div', {}, { className: 'il-rd-muted', innerText: '用于识别重定向链接中的“目标地址参数名”(如 url、target)。修改后建议刷新页面观察效果。' })); const chipWrap = el('div', {}, { className: 'il-rd-chip-wrap' }); const renderChips = () => { chipWrap.innerHTML = ''; if (!workingParams.length) { chipWrap.appendChild(el('div', { opacity: '0.75', fontSize: '12px' }, { innerText: '(空)可在下方输入并添加,或使用“批量粘贴”。' })); return; } workingParams.forEach((k) => { const chip = el('span', {}, { className: 'il-rd-chip' }); const text = el('span', {}, { className: 'il-rd-chip-text', innerText: String(k) }); text.title = String(k); const x = el('span', {}, { className: 'il-rd-chip-x', innerText: '×' }); x.addEventListener('click', () => { workingParams = workingParams.filter((it) => String(it) !== String(k)); renderParamsTab(); }); chip.appendChild(text); chip.appendChild(x); chipWrap.appendChild(chip); }); }; const inputRow = el('div', {}, { className: 'il-rd-input-row' }); const addInput = el('input', {}, { className: 'il-rd-input', type: 'text', placeholder: '例如:url / target / redirect' }); const addBtn = el('button', {}, { className: 'il-rd-btn', innerText: '添加' }); addBtn.addEventListener('click', () => { const v = sanitizeParamKey(addInput.value); if (!v) return; if (!workingParams.includes(v)) workingParams.push(v); addInput.value = ''; renderParamsTab(); }); addInput.addEventListener('keydown', (e) => { if (e && e.key === 'Enter') { e.preventDefault(); addBtn.click(); } }); inputRow.appendChild(addInput); inputRow.appendChild(addBtn); const bulkRow = el('div', {}, { className: 'il-rd-actions' }); const bulkToggle = el('button', {}, { className: 'il-rd-btn', innerText: '批量粘贴' }); bulkRow.appendChild(bulkToggle); const bulk = el('div', {}, { className: 'il-rd-bulk' }); bulk.appendChild(el('div', {}, { className: 'il-rd-muted', innerText: '支持逗号/换行/空格分隔;也支持粘贴类似 url=https://...(会自动取 key)。' })); const ta = el('textarea', {}, { className: 'il-rd-ta', value: workingParams.join('\n') }); bulk.appendChild(ta); const bulkApply = el('button', {}, { className: 'il-rd-btn', innerText: '应用到列表' }); bulkApply.addEventListener('click', () => { workingParams = parseParamList(ta.value); renderParamsTab(); }); const bulkApplyRow = el('div', {}, { className: 'il-rd-actions' }); bulkApplyRow.appendChild(bulkApply); bulk.appendChild(bulkApplyRow); bulkToggle.addEventListener('click', () => { const open = bulk.style.display !== 'flex'; bulk.style.display = open ? 'flex' : 'none'; if (open) { ta.value = workingParams.join('\n'); try { ta.focus(); } catch {} } }); card.appendChild(chipWrap); card.appendChild(inputRow); card.appendChild(bulkRow); card.appendChild(bulk); content.appendChild(card); // 首次渲染 chips renderChips(); }; const render = () => { btnLinks.classList.toggle('il-active', activeTab === 'links'); btnParams.classList.toggle('il-active', activeTab === 'params'); if (activeTab === 'links') renderLinksTab(); else renderParamsTab(); }; btnLinks.addEventListener('click', () => { activeTab = 'links'; render(); }); btnParams.addEventListener('click', () => { activeTab = 'params'; render(); }); root.appendChild(content); root.appendChild(seg); openSimpleModal('重定向调试菜单', root, { okText: '关闭' }); render(); }); settingButtonContainer.appendChild(settingButton); featureContainer.appendChild(settingButtonContainer); } parts.slice(1).forEach((componentType, idx) => { let component; let defaultValue; switch (componentType) { case 'numberPicker': defaultValue = Storage.get(parts[idx + 2]); component = createUIComponent('numberPicker', { value: defaultValue, min: 0, max: 100, step: 1, key: parts[idx + 2] }); break; case 'inputBox': defaultValue = Storage.get(parts[idx + 2]); component = createUIComponent('inputBox', { type: 'text', value: defaultValue, key: parts[idx + 2] }); break; case 'colorPicker': defaultValue = Storage.get(parts[idx + 2]); component = createUIComponent('colorPicker', { value: defaultValue, key: parts[idx + 2] }); break; case 'selector': defaultValue = Storage.get(parts[idx + 2]); // 兼容旧用法:默认走 optionsArray // 新用法:在 feature 描述里用 'selectorOptions,<变量名>' 指定自定义 options(不依赖固定索引) const selectorOptionsIndex = parts.indexOf('selectorOptions'); if (selectorOptionsIndex !== -1 && parts[selectorOptionsIndex + 1]) { const optVarName = parts[selectorOptionsIndex + 1]; const customOptions = resolveSelectorOptionsVar(optVarName); component = createUIComponent('selector', { options: customOptions, key: parts[idx + 2] }); } else { component = createUIComponent('selector', { options: optionsArray, key: parts[idx + 2] }); } break; case 'switch': defaultValue = Storage.get(parts[idx + 2]); component = createUIComponent('switch', { checked: defaultValue, key: parts[idx + 2] }); break; case 'switchGroup': defaultValue = Storage.get(parts[idx + 2]); component = createUIComponent('switchGroup', { label: parts[0], key: parts[idx + 2], infoText: parts.includes('information') ? (parts[4] || '') : '' }); break; case 'shortcutKeySetting': defaultValue = Storage.get(parts[idx + 2]); component = createUIComponent('shortcutKeySetting', { value: defaultValue, key: parts[idx + 2] }); break; case 'settingManager': component = createUIComponent('settingManager', { key: parts[idx + 2] }); break; default: break; } if (component) { featureContainer.appendChild(component); } }); return featureContainer; }); } /** * 创建并初始化特色功能面板。 * * @param {string} id - 该面板的HTML元素ID。 * @param {number} translateX - 初始移动位置的距离。 * @returns {HTMLElement} - 创建的特色功能面板元素。 */ function createShowcaseFeaturesPanel(id, translateX) { let showcaseFeatures = createElementWithStylesAndAttributes('div', { position: "absolute", width: "250px", height: "270px", borderRadius: "15px", display: "flex", alignItems: "flex-start", flexDirection: "column", justifyContent: "flex-start", background: "#E0E5EC", boxShadow: "2px 2px 8px #BECBD8, -2px -2px 8px #FFFFFF", transition: "transform 0.5s ease", transform: `translateX(${translateX}px)`, overflowY: "auto", overflowX: "hidden", overscrollBehavior: "contain", boxSizing: "border-box", paddingBottom: "8px" }); showcaseFeatures.id = id; return showcaseFeatures; } function formatTimestamp(ts) { try { const d = new Date(Number(ts) || Date.now()); const pad = (n) => String(n).padStart(2, '0'); return `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; } catch { return ''; } } function escapeHtml(str) { return String(str ?? '') .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function formatBytes(bytes) { const b = Number(bytes || 0); if (!Number.isFinite(b) || b <= 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; let i = 0; let v = b; while (v >= 1024 && i < units.length - 1) { v /= 1024; i++; } const fixed = (i === 0) ? String(Math.round(v)) : v.toFixed(v >= 10 ? 1 : 2); return `${fixed} ${units[i]}`; } // 轻量弹窗:用于状态栏详情(不引入复杂依赖) function openSimpleModal(title, contentNodeOrHtml, opts) { try { const options = (opts && typeof opts === 'object') ? opts : {}; const existing = document.getElementById('instantLoadSimpleModal'); if (existing && existing.parentNode) { try { existing.parentNode.removeChild(existing); } catch {} } const overlay = createElementWithStylesAndAttributes('div', { position: 'fixed', left: '0', top: '0', width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', zIndex: '10050', display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '12px', boxSizing: 'border-box', }, { id: 'instantLoadSimpleModal' }); const card = createElementWithStylesAndAttributes('div', { width: '520px', maxWidth: 'calc(100vw - 24px)', maxHeight: 'calc(100vh - 24px)', background: '#E0E5EC', borderRadius: '14px', boxShadow: '0 10px 30px rgba(0,0,0,0.25)', overflow: 'hidden', display: 'flex', flexDirection: 'column', minHeight: '0', }); const header = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px', padding: '10px 12px', borderBottom: '1px solid rgba(0,0,0,0.08)', }); const h = createElementWithStylesAndAttributes('div', { fontSize: '14px', fontWeight: '900', color: '#111827', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', }, { innerText: String(title || '') }); const closeBtn = createElementWithStylesAndAttributes('button', { border: 'none', background: 'transparent', cursor: 'pointer', fontSize: '16px', fontWeight: '900', color: '#374151', padding: '4px 8px', borderRadius: '8px', }, { innerText: '×' }); closeBtn.addEventListener('click', () => { try { overlay.remove(); } catch {} }); header.appendChild(h); header.appendChild(closeBtn); const body = createElementWithStylesAndAttributes('div', { padding: '10px 12px', overflow: 'auto', flex: '1 1 auto', // flex 子项默认 min-height:auto 可能导致内容被裁切且不出现滚动条 minHeight: '0', overscrollBehavior: 'contain', }); if (typeof contentNodeOrHtml === 'string') { body.innerHTML = contentNodeOrHtml; } else if (contentNodeOrHtml && contentNodeOrHtml.nodeType) { body.appendChild(contentNodeOrHtml); } const footer = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '8px', padding: '10px 12px', borderTop: '1px solid rgba(0,0,0,0.08)', }); const okText = String(options.okText || '关闭'); const okBtn = createElementWithStylesAndAttributes('button', { height: '30px', padding: '0 12px', borderRadius: '10px', border: 'none', cursor: 'pointer', background: '#E0E5EC', boxShadow: '2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF', fontSize: '12px', fontWeight: '800', color: '#374151', }, { innerText: okText }); okBtn.addEventListener('click', () => { try { overlay.remove(); } catch {} }); footer.appendChild(okBtn); card.appendChild(header); card.appendChild(body); card.appendChild(footer); overlay.appendChild(card); overlay.addEventListener('click', (e) => { try { if (e && e.target === overlay) overlay.remove(); } catch {} }); document.body.appendChild(overlay); return overlay; } catch { return null; } } function buildFooterStatusDetailsNode() { const wrap = createElementWithStylesAndAttributes('div', { display: 'flex', flexDirection: 'column', gap: '10px', }); // 操作 const actions = createElementWithStylesAndAttributes('div', { display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'flex-end', }); const btn = (text, onClick) => { const b = createElementWithStylesAndAttributes('button', { height: '30px', padding: '0 10px', borderRadius: '10px', border: 'none', cursor: 'pointer', background: '#E0E5EC', boxShadow: '2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF', fontSize: '12px', fontWeight: '800', color: '#374151', }, { innerText: text }); b.addEventListener('click', () => { try { onClick(); } catch {} }); return b; }; const row = (label, value, sub) => { const r = createElementWithStylesAndAttributes('div', { display: 'flex', flexDirection: 'column', gap: '2px', padding: '8px 10px', borderRadius: '12px', background: '#E0E5EC', boxShadow: 'inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF', }); const top = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '10px', }); const l = createElementWithStylesAndAttributes('div', { fontSize: '12px', fontWeight: '900', color: '#111827', }, { innerText: String(label || '') }); const v = createElementWithStylesAndAttributes('div', { fontSize: '12px', fontWeight: '900', color: '#1F2937', }, { innerText: String(value || '') }); top.appendChild(l); top.appendChild(v); r.appendChild(top); if (sub) { const s = createElementWithStylesAndAttributes('div', { fontSize: '11px', color: '#4B5563', lineHeight: '1.4', wordBreak: 'break-word', overflowWrap: 'anywhere', }, { innerText: String(sub) }); r.appendChild(s); } return r; }; const q = (Array.isArray(preloadQueue) ? preloadQueue.length : 0); const cap = Number(getDynamicConcurrencyCap() || 0); const inFlight = Number(currentPreloads || 0); const snap = getHitRateSnapshot(10 * 60 * 1000); const m10 = getRecentPreloadMetrics(10 * 60 * 1000); const mem = (function() { try { const m = getMemCache(); const meta = getMemCacheMeta(); const lim = getMemCacheLimits(); return { items: m ? m.size : 0, bytes: Number(meta && meta.bytes ? meta.bytes : 0), maxItems: lim.maxItems, maxBytes: lim.maxBytes, }; } catch { return { items: 0, bytes: 0, maxItems: 0, maxBytes: 0 }; } })(); wrap.appendChild(row('队列', `${q}`, '仅表示“等待处理”的排队数量;进行中的数量看“并发”。')); // 调度强度/网络状态(便于解释“为什么 cap 被下调/为什么暂停”) try { const capNow = Number(getDynamicConcurrencyCap() || 0); const userCap = Number(maxConcurrentPreloads || 0); const vis = (typeof document !== 'undefined') ? (document.hidden ? '后台' : '前台') : '—'; const constrained = isNetworkConstrainedForPreload(); let connText = '不可用'; try { const s = __netConnSnapshot; if (s && s.ok) { const et = String(s.effectiveType || '').toLowerCase(); const saveData = s.saveData ? '省流' : '非省流'; const dl = Number.isFinite(Number(s.downlink || 0)) && Number(s.downlink || 0) > 0 ? `${Number(s.downlink).toFixed(1)}Mbps` : '—'; const rtt = Number.isFinite(Number(s.rtt || 0)) && Number(s.rtt || 0) > 0 ? `${Math.round(Number(s.rtt))}ms` : '—'; connText = `${saveData}${et ? `,${et}` : ''},↓${dl},RTT≈${rtt}`; } else { try { connText = (navigator && navigator.onLine === false) ? '离线' : '不可用'; } catch {} } } catch {} wrap.appendChild(row('调度强度', constrained ? '暂停/极低' : '运行中', `cap=${capNow}/${userCap || '—'},页面=${vis},网络约束=${constrained ? '是' : '否'}\n${connText}`)); } catch {} const ns = getNetSampleSnapshot(); const nsText = (ns.total >= 3) ? `RTT≈${Math.round(ns.rttAvg || 0)}ms,失败率≈${Math.round((ns.failRate || 0) * 100)}%(${ns.fail}/${ns.total})` : '样本不足(需要产生几次预加载请求后才会有统计)'; wrap.appendChild(row('并发', `${inFlight}/${cap}`, `根据网络/设备/RTT/失败率做动态下调;上限来自“并发加载数”。\n${nsText}`)); wrap.appendChild(row('命中率(10分钟)', snap.total ? `${snap.ok}/${snap.total}(${Math.round(snap.rate * 100)}%)` : '—', '分母优先使用 attempt(真正发起过请求),旧数据回退 enqueue。')); wrap.appendChild(row('平均加速(10分钟)', (m10 && m10.okN) ? `≈${Math.round(m10.avgOkMs || 0)}ms(n=${m10.okN})` : '—', '近似口径:以预加载请求 RTT 估算“等待时间”减少量;并非页面渲染耗时。')); wrap.appendChild(row('失败耗时(10分钟)', (m10 && m10.failN) ? `≈${Math.round(m10.avgFailMs || 0)}ms(n=${m10.failN})` : '—', '用于判断“失败是否因超时/网络慢”导致。')); wrap.appendChild(row('缓存(IndexedDB)', cacheMeta && Number.isFinite(Number(cacheMeta.items)) ? `${Number(cacheMeta.items)} 条` : '—', cacheDegraded ? '当前处于降级状态:不会写入缓存。' : '淘汰依据:LRU + 过期清理 + 条数上限。')); wrap.appendChild(row('内存命中缓存', `${mem.items}/${mem.maxItems} 条,${formatBytes(mem.bytes)}/${formatBytes(mem.maxBytes)}`, '用于规避 DB 写入队列延迟;LRU + TTL(与清理间隔一致)+ 双阈值限制。')); // 每域名并发/队列 Top(便于定位“哪个站点占满了资源”) try { const inflightTop = formatTopKFromMap(runtimeDomainInFlight, 5); if (inflightTop) wrap.appendChild(row('每域并发 Top', inflightTop, '运行期统计:同一域名的同时进行数(受“每域并发”限制)。')); } catch {} try { const queueTop = formatTopKFromMap(getDomainQueueLengthsSnapshot(), 5); if (queueTop) wrap.appendChild(row('每域队列 Top', queueTop, '等待队列中的 URL 数量(按域名聚合)。')); } catch {} // 冷却概览 try { const now = Date.now(); const cooldownKeys = Object.keys(preloadCooldownUntil || {}).filter((k) => Number(preloadCooldownUntil[k] || 0) > now); if (cooldownKeys.length) { wrap.appendChild(row('冷却中 URL', `${cooldownKeys.length} 个`, '出现 468/429/403/5xx/网络错误后会进入退避冷却,避免刷失败日志。')); } } catch {} // 退避可视化 + 一键清空 try { const now = Date.now(); const entries = []; Object.keys(preloadCooldownUntil || {}).forEach((k) => { try { const until = Number(preloadCooldownUntil[k] || 0); if (!until || until <= now) return; const s = getPreloadBackoffState(k) || {}; const left = Math.max(0, until - now); const host = (function() { try { return new URL(String(k || ''), window.location.href).hostname; } catch { return ''; } })(); entries.push({ k: String(k || ''), host: String(host || ''), until, left, c: Number(s.c || 0), r: String(s.r || ''), t: Number(s.t || 0) }); } catch {} }); if (entries.length) { entries.sort((a, b) => (b.left - a.left)); const top = entries.slice(0, 12); const summary = top.map((it) => { const leftS = Math.ceil(Number(it.left || 0) / 1000); const reason = it.r ? it.r : 'cooldown'; const count = it.c ? `×${it.c}` : ''; const hostPart = it.host ? `(${it.host}) ` : ''; const urlShort = it.k.length > 80 ? it.k.slice(0, 77) + '…' : it.k; return `${leftS}s ${reason}${count} ${hostPart}${urlShort}`.trim(); }).join('\n'); wrap.appendChild(row('退避详情(Top)', `${entries.length} 条`, `剩余时间 / 原因 / 次数 / URL\n\n${summary}`)); // 操作:一键清空退避 actions.appendChild(btn('清空退避/冷却', () => { try { preloadBackoffState = {}; preloadCooldownUntil = {}; showTooltip('已清空退避/冷却'); } catch { try { showTooltip('清空失败'); } catch {} } try { const existing = document.getElementById('instantLoadSimpleModal'); if (existing) existing.remove(); } catch {} try { openSimpleModal('状态栏详情', buildFooterStatusDetailsNode()); } catch {} })); } } catch {} actions.appendChild(btn('复制概览', () => { try { const text = [ `队列=${q}`, `并发=${inFlight}/${cap}`, `命中=${snap.total ? (snap.ok + '/' + snap.total + '(' + Math.round(snap.rate * 100) + '%)') : '—'}`, `IDB=${cacheMeta && Number.isFinite(Number(cacheMeta.items)) ? Number(cacheMeta.items) + '条' : '—'}`, `MEM=${mem.items}/${mem.maxItems}条 ${formatBytes(mem.bytes)}/${formatBytes(mem.maxBytes)}`, ].join('\n'); tryCopyText(text); } catch {} })); wrap.appendChild(actions); return wrap; } function attachFooterStatusDetailsHandlers(versionInfoEl) { try { if (!versionInfoEl) return; const ids = ['instantLoadQueueStatus', 'instantLoadInFlightStatus', 'instantLoadHitStatus', 'instantLoadCacheStatus', 'instantLoadIntensityStatus']; ids.forEach((id) => { const el = versionInfoEl.querySelector ? versionInfoEl.querySelector('#' + id) : null; if (!el) return; el.style.cursor = 'pointer'; el.addEventListener('click', () => { try { openSimpleModal('状态栏详情', buildFooterStatusDetailsNode()); } catch {} }); }); } catch {} } function tryCopyText(text) { const t = String(text ?? ''); try { if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { navigator.clipboard.writeText(t).then(() => showAlert('已复制')).catch(() => { try { GM_setClipboard(t); showAlert('已复制'); } catch { showAlert('复制失败'); } }); return; } } catch {} try { GM_setClipboard(t); showAlert('已复制'); return; } catch {} showAlert('复制失败'); } // 统一提示:Toast(替代散落的 showAlert / showTooltip 调用) function toast(message, type) { try { const msg = String(message ?? ''); if (!msg) return; const kind = String(type || 'info'); const colors = { info: { bg: '#E0E5EC', fg: '#4B5563' }, success: { bg: '#E0E5EC', fg: '#065F46' }, warn: { bg: '#E0E5EC', fg: '#B45309' }, error: { bg: '#E0E5EC', fg: '#B91C1C' }, }; const c = colors[kind] || colors.info; // 复用单例,避免堆叠 let box = document.getElementById('instantLoadToast'); if (!box) { box = createElementWithStylesAndAttributes('div', { width: '300px', maxWidth: 'calc(100vw - 16px)', position: 'fixed', bottom: '10px', left: '50%', transform: 'translateX(-50%)', backgroundColor: '#E0E5EC', borderRadius: '10px', boxShadow: '2px 2px 2px #AEBEC7, -2px -2px 2px #FFFFFF', zIndex: '2000', opacity: '0', transition: 'opacity 0.18s ease', pointerEvents: 'none', boxSizing: 'border-box', padding: '8px 10px', }, { id: 'instantLoadToast' }); const text = createElementWithStylesAndAttributes('div', { margin: '0', fontSize: '14px', fontWeight: '800', textAlign: 'center', textShadow: '2px 2px 3px rgba(0, 0, 0, 0.15)', color: c.fg, wordBreak: 'break-word', overflowWrap: 'anywhere' }, { id: 'instantLoadToastText', innerText: msg }); box.appendChild(text); document.body.appendChild(box); } const textEl = document.getElementById('instantLoadToastText'); if (textEl) { textEl.innerText = msg; textEl.style.color = c.fg; } // reset timer try { clearTimeout(box.__hideTimer); } catch {} box.style.opacity = '1'; box.__hideTimer = setTimeout(() => { try { box.style.opacity = '0'; } catch {} }, 1500); } catch {} } function showTooltip(message) { toast(message, 'info'); } function safeJsonParse(text) { try { return { ok: true, value: JSON.parse(String(text || '')) }; } catch (e) { return { ok: false, error: e }; } } function stableStringify(obj) { try { return JSON.stringify(obj, Object.keys(obj).sort(), 2); } catch { try { return JSON.stringify(obj, null, 2); } catch { return ''; } } } function getAllSettingsSnapshot() { const snap = {}; try { if (typeof GM_listValues === 'function') { GM_listValues().forEach((k) => { try { // 过滤 UI 临时 key(避免导出污染、导入后产生不可控行为) if (String(k || '').startsWith('__domainRule_')) return; snap[k] = Storage.get(k); } catch {} }); return snap; } } catch {} // 兼容:无 GM_listValues 时,至少导出“已知 key”(默认设置 + 常用增强) try { const keys = [ // 默认设置集合(需与 initializeDefaultSettings 保持一致) 'schemaVersion', 'backToThePreviousPage','blacklistDomains','blackSelector','concurrentLoadingNumber','forward','goToTheCorrespondingPage', 'lazyLoadImages','loadedStyle','manipulatorBall','mobileGestures','previewHoverWindow','redirectOptimization','setShortcuts', 'asynchronousResources','whitelistDomains','whiteSelector','maxContentSize','maxStorageItems','dataCleanupInterval','is_loadedStyle', 'showPerformanceMonitor','clickInterceptMode','cleanTrackingParams','preloadAheadCount','preloadScanIntervalMs','maxPreloadQueueLength', 'preloadQueueExpireMs','clickInterceptDomainRules','disableSensitiveCache','sensitiveCacheWhitelistDomains','useNativePrefetch','useLinkPrefetch', 'useLinkPrerender','dedupStripHash','maxStorageItemsPerDomain','perDomainInFlightLimit','enablePreloadStats','footerCompactMode', 'operationHintCardShown','logLevel','logMaxItems','persistLogs','debugConsoleLog','disableHighRiskPreload', // 预览隔离 'previewRenderMode','sandboxAllowSameOrigin','sandboxAllowScripts','sandboxAllowForms','sandboxAllowPopups', ]; keys.forEach((k) => { try { snap[k] = Storage.get(k); } catch {} }); } catch {} return snap; } function listAvailableProfiles() { try { const names = Storage.get('__profiles_v1', []); if (Array.isArray(names)) { return names.map((x) => String(x || '').trim()).filter(Boolean); } } catch {} return []; } function saveProfile(name, snapshot) { const n = String(name || '').trim(); if (!n) return { ok: false, message: '名称不能为空' }; if (n.length > 32) return { ok: false, message: '名称过长(最多 32 字)' }; try { Storage.set(`__profile_${n}_v1`, snapshot || {}); const list = listAvailableProfiles(); if (!list.includes(n)) { list.push(n); Storage.set('__profiles_v1', list); } return { ok: true }; } catch { return { ok: false, message: '保存失败' }; } } function loadProfile(name) { const n = String(name || '').trim(); if (!n) return { ok: false, message: '名称不能为空' }; try { const snap = Storage.get(`__profile_${n}_v1`, null); if (!snap || typeof snap !== 'object') return { ok: false, message: '该预设不存在或已损坏' }; return { ok: true, snapshot: snap }; } catch { return { ok: false, message: '读取失败' }; } } function deleteProfile(name) { const n = String(name || '').trim(); if (!n) return { ok: false, message: '名称不能为空' }; try { // GM_deleteValue 可能存在,这里兼容两种方式 if (typeof GM_deleteValue === 'function') { try { GM_deleteValue(`__profile_${n}_v1`); } catch {} } else { try { Storage.set(`__profile_${n}_v1`, null); } catch {} } const list = listAvailableProfiles().filter((x) => x !== n); Storage.set('__profiles_v1', list); return { ok: true }; } catch { return { ok: false, message: '删除失败' }; } } function applySettingsSnapshot(snapshot, mode) { const dryRun = mode === 'dry-run'; const changes = []; if (!snapshot || typeof snapshot !== 'object') { return { ok: false, message: '导入内容不是 JSON 对象' }; } // 简单白名单:只允许写入这些 key(避免导入污染 Tampermonkey 其它脚本 key) // 注意:getAllSettingsSnapshot 可能包含 UI 临时 key(例如 __domainRule_*),这里不应允许导入。 const allowed = new Set(Object.keys(getAllSettingsSnapshot()).filter((k) => !String(k || '').startsWith('__domainRule_'))); // 另外允许导入 clickInterceptDomainRules 这类可能未初始化过的 key allowed.add('clickInterceptDomainRules'); allowed.add('clickInterceptDomainRulesV2'); allowed.add('enableClickInterceptDomainRulesV2'); allowed.add('preloadStats_v1'); allowed.add('preloadedLinks'); Object.keys(snapshot).forEach((k) => { if (!allowed.has(k)) return; try { const oldV = Storage.get(k); const newV = snapshot[k]; const changed = JSON.stringify(oldV) !== JSON.stringify(newV); if (changed) { changes.push({ k, oldV, newV }); if (!dryRun) Storage.set(k, newV); } } catch {} }); if (!dryRun) { try { updateDomainLists(); } catch {} try { initRuntimeConfig(); } catch {} } return { ok: true, changes }; } function getPreloadStatsList() { try { const list = Storage.get('preloadStats_v1', []); return Array.isArray(list) ? list : []; } catch { return []; } } function clearPreloadStats() { try { Storage.set('preloadStats_v1', []); } catch {} } function buildDiagnosticsText() { const lines = []; try { lines.push(`ua=${navigator.userAgent}`); } catch {} try { lines.push(`url=${location.href}`); } catch {} try { const keys = ['enablePreloadStats','showPerformanceMonitor','clickInterceptMode','disableHighRiskPreload','cleanTrackingParams','dedupStripHash','preloadAheadCount','preloadScanIntervalMs','maxPreloadQueueLength','preloadQueueExpireMs','perDomainInFlightLimit','maxStorageItemsPerDomain','disableSensitiveCache','disableSensitivePreload','safeMode','previewRenderMode','sandboxAllowSameOrigin','sandboxAllowScripts','sandboxAllowForms','sandboxAllowPopups','logLevel','logMaxItems','persistLogs','debugConsoleLog']; keys.forEach((k) => { try { lines.push(`${k}=${JSON.stringify(Storage.get(k))}`); } catch {} }); } catch {} try { const logs = Log.getAll().slice(-80); lines.push('--- logs (tail) ---'); logs.forEach((it) => { lines.push(`${formatTimestamp(it.t)} [${it.l}] ${it.m}${it.d ? ' | ' + it.d : ''}`); }); } catch {} try { const stats = getPreloadStatsList().slice(-80); lines.push('--- preloadStats (tail) ---'); stats.forEach((it) => { const e = it.e || ''; const u = it.u || ''; const r = it.r ? ` r=${it.r}` : ''; const m = it.m ? ` m=${it.m}` : ''; const d = it.d ? ` d=${it.d}` : ''; lines.push(`${formatTimestamp(it.t)} ${e}${d}${r}${m} ${u}`.trim()); }); } catch {} return lines.join('\n'); } function initLogPanel(panelEl) { if (!panelEl) return; panelEl.style.overflow = 'hidden'; panelEl.style.display = 'flex'; panelEl.style.flexDirection = 'column'; const header = createElementWithStylesAndAttributes('div', { flex: '0 0 auto', display: 'flex', gap: '6px', alignItems: 'center', padding: '8px', borderBottom: '1px solid rgba(190, 203, 216, 0.8)' }, {}); const levelSelect = createElementWithStylesAndAttributes('select', { height: '28px', borderRadius: '8px', border: 'none', outline: 'none', padding: '0 8px', boxShadow: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff', background: '#E0E5EC', color: '#4B5563', fontSize: '12px' }, {}); ['all','error','warn','info','debug'].forEach((v) => { const opt = document.createElement('option'); opt.value = v; opt.textContent = v === 'all' ? '全部级别' : v; levelSelect.appendChild(opt); }); const keywordInput = createElementWithStylesAndAttributes('input', { flex: '1 1 auto', height: '28px', borderRadius: '8px', border: 'none', outline: 'none', padding: '0 10px', boxShadow: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff', background: '#E0E5EC', color: '#4B5563', fontSize: '12px' }, { placeholder: '筛选关键词(URL/原因/文本)' }); const mkBtn = (text) => createElementWithStylesAndAttributes('button', { height: '28px', padding: '0 10px', borderRadius: '8px', border: 'none', cursor: 'pointer', background: '#E0E5EC', boxShadow: '2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff', color: '#4B5563', fontSize: '12px', userSelect: 'none' }, { innerText: text }); const refreshBtn = mkBtn('刷新'); const copyBtn = mkBtn('复制诊断'); const cacheBtn = mkBtn('缓存诊断'); const clearLogsBtn = mkBtn('清空日志'); const clearStatsBtn = mkBtn('清空回放'); header.appendChild(levelSelect); header.appendChild(keywordInput); header.appendChild(refreshBtn); header.appendChild(copyBtn); header.appendChild(cacheBtn); header.appendChild(clearLogsBtn); header.appendChild(clearStatsBtn); const body = createElementWithStylesAndAttributes('div', { flex: '1 1 auto', overflowY: 'auto', overflowX: 'hidden', padding: '8px', boxSizing: 'border-box' }, {}); const summaryBox = createElementWithStylesAndAttributes('div', { flex: '0 0 auto', padding: '8px', boxSizing: 'border-box', borderBottom: '1px solid rgba(190, 203, 216, 0.6)', fontSize: '12px', color: '#374151' }, {}); const sectionTitle = (t) => createElementWithStylesAndAttributes('div', { fontSize: '13px', fontWeight: 'bold', color: '#4B5563', margin: '6px 0' }, { innerText: t }); const logsBox = createElementWithStylesAndAttributes('div', { fontSize: '12px', lineHeight: '1.35', color: '#374151', whiteSpace: 'normal' }, {}); const statsBox = createElementWithStylesAndAttributes('div', { fontSize: '12px', lineHeight: '1.35', color: '#374151', whiteSpace: 'normal' }, {}); // 缓存诊断弹窗(估算占用 / TopN 域名 / 最近 URL / 清空当前域) addStyle('instant-load-cache-diagnostics-style', ` .cache-diag-modal { display:none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 340px; max-width: calc(100vw - 16px); height: 420px; max-height: calc(100vh - 16px); background: #e0e5ec; border-radius: 12px; box-shadow: 2px 2px 8px #BECBD8, -2px -2px 8px #FFFFFF; z-index: 2500; box-sizing: border-box; padding: 10px; } .cache-diag-title { display:flex; align-items:center; justify-content: space-between; gap: 8px; } .cache-diag-body { margin-top: 8px; height: calc(100% - 44px); overflow: auto; } .cache-diag-btn { height: 28px; padding: 0 10px; border-radius: 8px; border: none; cursor: pointer; background: #E0E5EC; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; color: #4B5563; font-size: 12px; user-select:none; } .cache-diag-btn:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .cache-diag-card { margin: 8px 0; padding: 8px 10px; border-radius: 12px; background:#E0E5EC; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .cache-diag-list { margin-top: 6px; font-size: 12px; color:#374151; } .cache-diag-list-item { margin: 4px 0; word-break: break-all; } `); const cacheModal = createElementWithStylesAndAttributes('div', {}, { className: 'cache-diag-modal' }); const cacheTitleRow = createElementWithStylesAndAttributes('div', {}, { className: 'cache-diag-title' }); const cacheTitle = createElementWithStylesAndAttributes('div', { fontSize: '14px', fontWeight: '800', color: '#111827' }, { innerText: '缓存诊断(本地)' }); const cacheCloseBtn = createElementWithStylesAndAttributes('button', {}, { className: 'cache-diag-btn', innerText: '关闭' }); cacheTitleRow.appendChild(cacheTitle); cacheTitleRow.appendChild(cacheCloseBtn); const cacheBody = createElementWithStylesAndAttributes('div', {}, { className: 'cache-diag-body' }); cacheModal.appendChild(cacheTitleRow); cacheModal.appendChild(cacheBody); document.body.appendChild(cacheModal); function getRecentSuccessUrls(limit) { try { const list = getPreloadStatsList(); const urls = []; for (let i = list.length - 1; i >= 0; i--) { const it = list[i]; if (!it || it.e !== 'success' || !it.u) continue; const u = String(it.u); if (!u) continue; if (!urls.includes(u)) urls.push(u); if (urls.length >= (limit || 12)) break; } return urls; } catch { return []; } } function renderCacheDiagnostics() { const currentHost = (function() { try { return String(window.location.hostname || '').toLowerCase(); } catch { return ''; } })(); cacheBody.innerHTML = '
正在读取缓存统计…
'; estimateDbRecordsSummary((res) => { try { if (!res || !res.ok) { cacheBody.innerHTML = `
${escapeHtml((res && res.message) ? res.message : '缓存不可用')}
`; return; } const topHosts = Array.isArray(res.topHosts) ? res.topHosts : []; const recentUrls = Array.isArray(res.recentUrls) ? res.recentUrls : []; const recentSuccess = getRecentSuccessUrls(12); const currentHostRow = topHosts.find((x) => x && String(x.host || '').toLowerCase() === currentHost); const currentHostText = currentHostRow ? `${currentHostRow.count} 条 / ${formatBytes(currentHostRow.bytes)}` : '0 条'; const topHtml = topHosts.length ? topHosts.map((x) => { const host = escapeHtml(x.host || '(unknown)'); const cnt = Number(x.count || 0); const bytes = formatBytes(x.bytes || 0); return `
- ${host}: ${cnt} 条 / ${bytes}
`; }).join('') : '
(暂无数据)
'; const recentHtml = recentUrls.length ? recentUrls.map((u) => `
- ${escapeHtml(u)}
`).join('') : '
(暂无缓存记录)
'; const hitHtml = recentSuccess.length ? recentSuccess.map((u) => `
- ${escapeHtml(u)}
`).join('') : '
(暂无命中记录)
'; const actions = `
`; cacheBody.innerHTML = `
总览 条数:${Number(res.items || 0)} 估算占用:${formatBytes(res.totalBytes || 0)} 当前域:${escapeHtml(currentHost || '(unknown)')}(${escapeHtml(currentHostText)})
${actions}
每域占用 TopN
${topHtml}
最近写入缓存 URL(Top 12)
${recentHtml}
最近命中 URL(来自回放,Top 12)
${hitHtml}
`; const refreshEl = document.getElementById('cacheDiagRefresh'); const copyEl = document.getElementById('cacheDiagCopy'); const clearEl = document.getElementById('cacheDiagClearHost'); if (refreshEl) refreshEl.onclick = renderCacheDiagnostics; if (copyEl) { copyEl.onclick = () => { try { const payload = stableStringify({ items: res.items, totalBytes: res.totalBytes, topHosts: res.topHosts, recentUrls: res.recentUrls, recentSuccessUrls: recentSuccess, host: currentHost, }); tryCopyText(payload); } catch { showAlert('复制失败'); } }; } if (clearEl) { clearEl.onclick = () => { if (!currentHost) return; clearEl.disabled = true; clearCacheForHost(currentHost, (r) => { try { if (r && r.ok) { showAlert(`已清空当前域缓存(${Number(r.removed || 0)} 条)`); } else { showAlert(`清空失败:${(r && r.message) ? r.message : '未知错误'}`); } } catch {} renderCacheDiagnostics(); }); }; } } catch (e) { cacheBody.innerHTML = `
${escapeHtml(String(e && e.message ? e.message : e))}
`; } }); } cacheBtn.addEventListener('click', () => { cacheModal.style.display = 'block'; renderCacheDiagnostics(); }); cacheCloseBtn.addEventListener('click', () => { cacheModal.style.display = 'none'; }); body.appendChild(summaryBox); body.appendChild(sectionTitle('日志(本地)')); body.appendChild(logsBox); body.appendChild(sectionTitle('命中回放(最近记录)')); body.appendChild(statsBox); panelEl.appendChild(header); panelEl.appendChild(body); function matchesFilter(text) { const kw = String(keywordInput.value || '').trim().toLowerCase(); if (!kw) return true; return String(text || '').toLowerCase().includes(kw); } function renderLogs() { const lv = levelSelect.value; const list = Log.getAll(); const view = list .filter((it) => { if (!it) return false; if (lv !== 'all' && it.l !== lv) return false; const text = `${it.m || ''} ${it.d || ''}`; return matchesFilter(text); }) .slice(-200); if (!view.length) { logsBox.innerHTML = '
(暂无日志)
'; return; } logsBox.innerHTML = view.map((it) => { const t = formatTimestamp(it.t); const l = escapeHtml(it.l); const m = escapeHtml(it.m); const d = it.d ? escapeHtml(it.d) : ''; const color = (it.l === 'error') ? '#B91C1C' : (it.l === 'warn') ? '#B45309' : (it.l === 'debug') ? '#1D4ED8' : '#065F46'; return `
${t} ${l}
${m}
${d ? `
${d}
` : ''}
`; }).join(''); } function renderStats() { const list = getPreloadStatsList(); const view = list .filter((it) => { if (!it) return false; const text = `${it.e || ''} ${it.r || ''} ${it.m || ''} ${it.u || ''} ${it.d || ''}`; return matchesFilter(text); }) .slice(-200); if (!view.length) { statsBox.innerHTML = '
(暂无回放记录)
'; return; } statsBox.innerHTML = view.map((it) => { const t = formatTimestamp(it.t); const e = escapeHtml(it.e || ''); const r = it.r ? ` r=${escapeHtml(it.r)}` : ''; const m = it.m ? `
${escapeHtml(it.m)}
` : ''; const u = escapeHtml(it.u || ''); const color = (it.e === 'fail') ? '#B91C1C' : (it.e === 'success') ? '#065F46' : '#374151'; return `
${t} ${e} ${r}
${u}
${m}
`; }).join(''); } function renderAll() { try { const stats = getPreloadStatsList(); const now = Date.now(); const winMs = 10 * 60 * 1000; const recent = stats.filter((it) => it && it.t && (now - it.t <= winMs)); const counts = { enqueue: 0, attempt: 0, success: 0, fail: 0 }; const reasons = {}; recent.forEach((it) => { const e = it.e; if (e in counts) counts[e]++; if (e === 'fail') { const m = String(it.m || '').slice(0, 80); if (m) reasons[m] = (reasons[m] || 0) + 1; } }); const topReasons = Object.entries(reasons) .sort((a, b) => b[1] - a[1]) .slice(0, 3) .map(([k, v]) => `${escapeHtml(k)}(${v})`) .join(','); // 补全关键指标:平均加速(以预加载请求 RTT 作为近似)、每域名并发/队列 Top let avgOk = 0; let avgOkN = 0; let avgFail = 0; let avgFailN = 0; try { const m = getRecentPreloadMetrics(winMs); avgOk = Number(m.avgOkMs || 0); avgOkN = Number(m.okN || 0); avgFail = Number(m.avgFailMs || 0); avgFailN = Number(m.failN || 0); } catch {} let inflightTopText = ''; try { inflightTopText = formatTopKFromMap(runtimeDomainInFlight, 3); } catch {} let queueTopText = ''; try { queueTopText = formatTopKFromMap(getDomainQueueLengthsSnapshot(), 3); } catch {} summaryBox.innerHTML = `
近 10 分钟 入队:${counts.enqueue} 尝试:${counts.attempt} 成功:${counts.success} 失败:${counts.fail} ${(avgOkN > 0) ? `平均加速:≈${Math.round(avgOk)}ms(n=${avgOkN})` : ''} ${(avgFailN > 0) ? `失败RTT:≈${Math.round(avgFail)}ms(n=${avgFailN})` : ''} ${topReasons ? `失败原因:${topReasons}` : ''} ${inflightTopText ? `每域并发Top:${inflightTopText}` : ''} ${queueTopText ? `每域队列Top:${queueTopText}` : ''}
`; } catch { summaryBox.innerHTML = '
(统计汇总不可用)
'; } renderLogs(); renderStats(); } refreshBtn.addEventListener('click', renderAll); levelSelect.addEventListener('change', renderLogs); keywordInput.addEventListener('input', () => { renderAll(); }); copyBtn.addEventListener('click', () => { tryCopyText(buildDiagnosticsText()); }); clearLogsBtn.addEventListener('click', () => { Log.clear(); renderLogs(); }); clearStatsBtn.addEventListener('click', () => { clearPreloadStats(); renderStats(); }); // 暴露给 panel4 显示时触发刷新 panelEl.__renderLogPanel = renderAll; renderAll(); } /** * 创建不同的UI组件。 * * @param {string} type - 组件类型。 * @param {Object} options - 用于创建组件的选项。 * @param {string} [options.value] - 组件的当前值。 * @param {Object} options.min - 组件的最小值。 * @param {Object} options.max - 组件的最大值。 * @param {Object} options.step - 组件的步长值。 * @param {Array} options.options - 下拉选项。 * @param {boolean} options.checked - 开关的选中状态。 * @returns {HTMLElement} - 相应类型的行内UI组件。 */ function createUIComponent(type, options) { if (type === 'switch') { addStyle('optimized-switch-style', ` .optimizedswitchcontainer input[type="checkbox"] { width: 0; height: 0; opacity: 0; } .optimizedswitchcontainer { position: relative; display: inline-block; width: 34px; height: 14px; } .optimizedswitchslider { position: absolute; cursor: pointer; top: 0; bottom: 0; left: 0; right: 0; background-color: #ccc; transition: .4s; } .optimizedswitchslider:before { position: absolute; content: ""; height: 20px; width: 20px; left: 0; bottom: -3px; background-color: white; transition: .4s; box-shadow: 0 2px 5px rgba(0,0,0,0.3); } .optimizedswitchcontainer input:checked + .optimizedswitchslider { background-color: #00FF00; } .optimizedswitchcontainer input:focus + .optimizedswitchslider { box-shadow: 0 0 1px #2196F3; } .optimizedswitchcontainer input:checked + .optimizedswitchslider:before { transform: translateX(18px); } `); let switchContainer = createElementWithStylesAndAttributes("label", {}, { className: "optimizedswitchcontainer" }); let switchInput = createElementWithStylesAndAttributes("input", {}, { type: "checkbox", checked: options.checked || false, id: `switch_${options.key}`, }); switchInput.addEventListener("change", function() { // UI:高风险开关在“开启”时二次确认 try { const key = String(options.key || ''); if (this.checked && key && DANGEROUS_ENABLE_CONFIRM && DANGEROUS_ENABLE_CONFIRM[key]) { const ok = window.confirm(DANGEROUS_ENABLE_CONFIRM[key]); if (!ok) { this.checked = false; return; } } } catch {} Storage.set(options.key, this.checked); try { emitSettingChanged(options.key, this.checked); } catch {} // 原生预取:总开关关闭时,子开关自动关闭 if (options.key === 'useNativePrefetch' && !this.checked) { try { Storage.set('useLinkPrefetch', false); } catch {} try { Storage.set('useLinkPrerender', false); } catch {} try { const prefetchEl = document.getElementById('switch_useLinkPrefetch'); if (prefetchEl) prefetchEl.checked = false; const prerenderEl = document.getElementById('switch_useLinkPrerender'); if (prerenderEl) prerenderEl.checked = false; } catch {} } // 原生预取:总开关开启时,若子开关都没开则默认开启“预取”(更稳) if (options.key === 'useNativePrefetch' && this.checked) { try { const p1 = !!Storage.get('useLinkPrefetch', true); const p2 = !!Storage.get('useLinkPrerender', false); if (!p1 && !p2) { Storage.set('useLinkPrefetch', true); const prefetchEl = document.getElementById('switch_useLinkPrefetch'); if (prefetchEl) prefetchEl.checked = true; } } catch {} } // 子开关互斥:开启一个会自动关闭另一个 if (options.key === 'useLinkPrefetch' && this.checked) { try { Storage.set('useLinkPrerender', false); } catch {} try { const prerenderEl = document.getElementById('switch_useLinkPrerender'); if (prerenderEl) prerenderEl.checked = false; } catch {} } if (options.key === 'useLinkPrerender' && this.checked) { try { Storage.set('useLinkPrefetch', false); } catch {} try { const prefetchEl = document.getElementById('switch_useLinkPrefetch'); if (prefetchEl) prefetchEl.checked = false; } catch {} } // 即时显示/隐藏性能监控 if (options.key === 'showPerformanceMonitor' && typeof updateStatsVisibility === 'function') { updateStatsVisibility(); } }); let switchSlider = createElementWithStylesAndAttributes("span", {}, { className: "optimizedswitchslider" }); switchContainer.appendChild(switchInput); switchContainer.appendChild(switchSlider); return switchContainer; } else if (type === 'switchGroup') { // 向下展开的子开关组(总开关 + 子开关) const groupKey = options.key; const expandedKey = `${groupKey}__expanded`; const isExpanded = !!Storage.get(expandedKey, false); const groupContainer = createElementWithStylesAndAttributes('div', { display: 'flex', flexDirection: 'column', width: 'calc(100% - 6px)', marginTop: '3px', marginLeft: '3px', marginRight: '3px', boxSizing: 'border-box', gap: '6px', }); const headerRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', width: '100%', }); const leftRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', gap: '6px', minWidth: '0', }); const labelBox = createElementWithStylesAndAttributes('div', { width: '85px', height: '32px', lineHeight: '32px', backgroundColor: '#E0E5EC', borderRadius: '5px', boxShadow: 'inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF', textAlign: 'center', userSelect: 'none', whiteSpace: 'nowrap', fontSize: '14px', flex: '0 0 auto', }, { innerText: options.label || '设置' }); leftRow.appendChild(labelBox); if (options.infoText) { let infoIcon = createElementWithStylesAndAttributes("div", { position: 'relative', cursor: 'pointer', display: 'flex', alignItems: 'left', justifyContent: 'center' }, { innerHTML: `` }); let infoBox = createElementWithStylesAndAttributes("div", { width: "300px", maxWidth: "calc(100vw - 16px)", maxHeight: "calc(100vh - 16px)", height: "auto", overflow: "auto", display: "none", position: "fixed", transform: "none", backgroundColor: "#E0E5EC", borderRadius: "12px", boxShadow: "2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF", zIndex: "2000" }); let parsedContent = parseSimpleMarkdown(options.infoText); let infoTextEl = createElementWithStylesAndAttributes("div", { padding: "10px", margin: "0", fontSize: "15px", fontWeight: "bold", textAlign: "left", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", color: "#4B5563" }, { innerHTML: parsedContent }); infoBox.appendChild(infoTextEl); document.body.appendChild(infoBox); leftRow.appendChild(infoIcon); infoIcon.addEventListener('mouseover', function() { infoBox.dataset.positionMode = 'anchor'; activeTooltipBox = infoBox; positionTooltipNearElement(infoBox, infoIcon); }); infoIcon.addEventListener('mouseout', function() { infoBox.style.display = 'none'; infoBox.style.visibility = 'visible'; if (activeTooltipBox === infoBox) activeTooltipBox = null; }); } headerRow.appendChild(leftRow); const rightRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '8px', flex: '0 0 auto', }); const mainSwitch = createUIComponent('switch', { checked: !!Storage.get(groupKey, false), key: groupKey }); const expandBtn = createElementWithStylesAndAttributes('button', { height: '26px', padding: '0 10px', borderRadius: '999px', border: 'none', cursor: 'pointer', background: '#E0E5EC', boxShadow: '2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF', color: '#4B5563', fontSize: '12px', fontWeight: '700', userSelect: 'none' }, { innerText: isExpanded ? '收起' : '展开' }); rightRow.appendChild(mainSwitch); rightRow.appendChild(expandBtn); headerRow.appendChild(rightRow); groupContainer.appendChild(headerRow); const childrenWrap = createElementWithStylesAndAttributes('div', { display: isExpanded ? 'flex' : 'none', flexDirection: 'column', gap: '6px', paddingLeft: '18px', }); const renderChildRow = (label, key) => { const row = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', width: '100%' }); const text = createElementWithStylesAndAttributes('div', { fontSize: '13px', color: '#4B5563', userSelect: 'none', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', flex: '1 1 auto' }, { innerText: label }); const sw = createUIComponent('switch', { checked: !!Storage.get(key, false), key }); row.appendChild(text); row.appendChild(sw); return row; }; childrenWrap.appendChild(renderChildRow('预取(更稳、更省资源)', 'useLinkPrefetch')); childrenWrap.appendChild(renderChildRow('预渲染(更激进,更耗资源)', 'useLinkPrerender')); // 根据总开关状态禁用/启用子开关区域 const refreshChildrenEnable = () => { const enabled = !!Storage.get(groupKey, false); childrenWrap.style.opacity = enabled ? '1' : '0.55'; childrenWrap.style.pointerEvents = enabled ? 'auto' : 'none'; }; refreshChildrenEnable(); expandBtn.addEventListener('click', () => { const next = !Storage.get(expandedKey, false); Storage.set(expandedKey, next); childrenWrap.style.display = next ? 'flex' : 'none'; expandBtn.innerText = next ? '收起' : '展开'; }); // 监听总开关的变化:刷新禁用态 // 注意:此时组件可能尚未挂载到 document,不能用 getElementById 取;直接从 mainSwitch 内拿 input 绑定。 try { const mainInput = mainSwitch && mainSwitch.querySelector ? mainSwitch.querySelector('input[type="checkbox"]') : null; if (mainInput) { mainInput.addEventListener('change', refreshChildrenEnable); } } catch {} groupContainer.appendChild(childrenWrap); return groupContainer; } else if (type === 'switchGroupCustom') { // 通用折叠子开关组:children 由调用方传入 const groupKey = options.key; const expandedKey = `${groupKey}__expanded`; const isExpanded = !!Storage.get(expandedKey, false); const groupContainer = createElementWithStylesAndAttributes('div', { display: 'flex', flexDirection: 'column', width: 'calc(100% - 6px)', marginTop: '3px', marginLeft: '3px', marginRight: '3px', boxSizing: 'border-box', gap: '6px', }); const headerRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: '8px', width: '100%', }); const leftRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', gap: '6px', minWidth: '0', }); const labelBox = createElementWithStylesAndAttributes('div', { width: '85px', height: '32px', lineHeight: '32px', backgroundColor: '#E0E5EC', borderRadius: '5px', boxShadow: 'inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF', textAlign: 'center', userSelect: 'none', whiteSpace: 'nowrap', fontSize: '14px', flex: '0 0 auto', }, { innerText: options.label || '设置' }); leftRow.appendChild(labelBox); if (options.infoText) { let infoIcon = createElementWithStylesAndAttributes("div", { position: 'relative', cursor: 'pointer', display: 'flex', alignItems: 'left', justifyContent: 'center' }, { innerHTML: `` }); let infoBox = createElementWithStylesAndAttributes("div", { width: "300px", maxWidth: "calc(100vw - 16px)", maxHeight: "calc(100vh - 16px)", height: "auto", overflow: "auto", display: "none", position: "fixed", transform: "none", backgroundColor: "#E0E5EC", borderRadius: "12px", boxShadow: "2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF", zIndex: "2000" }); let parsedContent = parseSimpleMarkdown(options.infoText); let infoTextEl = createElementWithStylesAndAttributes("div", { padding: "10px", margin: "0", fontSize: "15px", fontWeight: "bold", textAlign: "left", textShadow: "2px 2px 3px rgba(0, 0, 0, 0.2)", color: "#4B5563" }, { innerHTML: parsedContent }); infoBox.appendChild(infoTextEl); document.body.appendChild(infoBox); leftRow.appendChild(infoIcon); infoIcon.addEventListener('mouseover', function() { infoBox.dataset.positionMode = 'anchor'; activeTooltipBox = infoBox; positionTooltipNearElement(infoBox, infoIcon); }); infoIcon.addEventListener('mouseout', function() { infoBox.style.display = 'none'; infoBox.style.visibility = 'visible'; if (activeTooltipBox === infoBox) activeTooltipBox = null; }); } headerRow.appendChild(leftRow); const rightRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: '8px', flex: '0 0 auto', }); const expandBtn = createElementWithStylesAndAttributes('button', { height: '26px', padding: '0 10px', borderRadius: '999px', border: 'none', cursor: 'pointer', background: '#E0E5EC', boxShadow: '2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF', color: '#4B5563', fontSize: '12px', fontWeight: '700', userSelect: 'none' }, { innerText: isExpanded ? '收起' : '展开' }); rightRow.appendChild(expandBtn); headerRow.appendChild(rightRow); groupContainer.appendChild(headerRow); const childrenWrap = createElementWithStylesAndAttributes('div', { display: isExpanded ? 'flex' : 'none', flexDirection: 'column', gap: '6px', paddingLeft: '18px', }); // 由外部传入子项 try { const children = Array.isArray(options.children) ? options.children : []; children.forEach((row) => { if (row) childrenWrap.appendChild(row); }); } catch {} // 默认禁用态:外部可通过 refreshChildrenEnable 控制 const refreshChildrenEnable = () => { try { const enabled = (typeof options.isEnabled === 'function') ? !!options.isEnabled() : true; childrenWrap.style.opacity = enabled ? '1' : '0.55'; childrenWrap.style.pointerEvents = enabled ? 'auto' : 'none'; } catch {} }; refreshChildrenEnable(); expandBtn.addEventListener('click', () => { const next = !Storage.get(expandedKey, false); Storage.set(expandedKey, next); childrenWrap.style.display = next ? 'flex' : 'none'; expandBtn.innerText = next ? '收起' : '展开'; }); // 外部变更时同步 enable 状态 try { onSettingChanged((k) => { if (!k) return; refreshChildrenEnable(); }); } catch {} groupContainer.appendChild(childrenWrap); return groupContainer; } else if (type === 'inputBox') { addStyle("neumorphic-style", `.neu-display-box { font-size: 14px; background: #e0e5ec; border-radius: 10px; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; transition: all 0.3s; display: flex; justify-content: center; align-items: center; cursor: pointer; width: 80px; height: 30px; overflow: hidden; white-space: nowrap; } .neu-input-modal { width: 200px; display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #e0e5ec; border-radius: 10px; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; width: 280px; max-height: calc(100vh - 16px); overflow: auto; box-sizing: border-box; padding: 10px 0; justify-content: flex-start; align-items: center; flex-direction: column; z-index: 1500; } .neu-text-input, .button-row button { border: none; background: none; outline: none; } .neu-text-input { padding: 10px; width: 100%; max-width: 100%; box-sizing: border-box; height: 200px; resize: none; margin-bottom: 10px; border-radius: 10px; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .button-row { display: flex; justify-content: space-between; width: calc(100% - 34px); } .button-row button { width: 95px; cursor: pointer; background: #e0e5ec; border-radius: 5px; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; } .button-row button:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } /* 列表型输入(域名/站点规则等):可视化 chips 编辑器 */ .neu-list-editor { width: 250px; box-sizing: border-box; display:flex; flex-direction: column; gap: 8px; } .neu-list-title { width: 250px; font-size: 13px; font-weight: 900; color:#374151; user-select:none; text-align:left; } .neu-chip-wrap { width: 250px; max-height: 140px; overflow: auto; display:flex; flex-wrap: wrap; gap: 6px; padding: 8px; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; box-sizing: border-box; } .neu-chip { display:inline-flex; align-items:center; gap: 6px; padding: 4px 8px; border-radius: 999px; background: #E0E5EC; box-shadow: 2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF; font-size: 12px; color:#374151; max-width: 100%; } .neu-chip-text { overflow:hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 180px; } .neu-chip-x { cursor:pointer; user-select:none; font-weight: 900; color:#6B7280; } .neu-chip-x:hover { color:#B91C1C; } .neu-list-row { width: 250px; display:flex; gap: 8px; align-items:center; } .neu-list-input { flex: 1 1 auto; height: 30px; padding: 0 10px; border: none; outline: none; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; font-size: 13px; color:#374151; } .neu-small-btn { flex: 0 0 auto; height: 30px; padding: 0 10px; border: none; border-radius: 10px; cursor: pointer; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; font-size: 12px; font-weight: 800; color:#4B5563; user-select:none; } .neu-small-btn:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .neu-small-btn:disabled { opacity: 0.55; cursor: not-allowed; } .neu-bulk-toggle { width: 250px; display:flex; justify-content: space-between; align-items:center; gap: 8px; } .neu-bulk-area { width: 250px; display:none; flex-direction: column; align-items: stretch; gap: 6px; } .neu-bulk-hint { font-size: 11px; color:#6B7280; line-height: 1.25; } `); let modal = createElementWithStylesAndAttributes("div", {}, { className: "neu-input-modal", }); let textInput = createElementWithStylesAndAttributes("textarea", {}, { className: "neu-text-input", value: (() => { const v = Storage.get(options.key); if (Array.isArray(v)) return v.join('\n'); if (v === undefined || v === null) return ''; return String(v); })(), }); const isArrayMode = (() => { if (Array.isArray(options.value)) return true; const stored = Storage.get(options.key); return Array.isArray(stored); })(); const getDisplayText = (raw) => { if (Array.isArray(raw)) return raw.join(','); if (raw === undefined || raw === null) return ''; return String(raw); }; const formatPreview = (raw) => { if (Array.isArray(raw)) { const n = raw.length; if (!n) return '点击输入'; return `已配置${n}个`; } const s = String(raw || ''); if (!s) return '点击输入'; return s.length > 12 ? (s.substring(0, 12) + '...') : s; }; const sanitizeDomainLike = (s) => { try { let t = String(s || '').trim(); if (!t) return ''; // 允许用户粘贴完整 URL:只取 host 部分 t = t.replace(/^https?:\/\//i, ''); t = t.replace(/^\/\//, ''); t = t.split(/[\s\/\?#]/)[0]; t = t.replace(/:.*$/, ''); t = t.replace(/^\.+/, '').replace(/\.+$/, ''); return t.toLowerCase(); } catch { return ''; } }; const parseArrayInput = (inputText) => { const raw = String(inputText || '').trim(); if (!raw) return []; const parts = raw.split(/[\n,;\s]+/); const out = []; parts.forEach((p) => { const v = sanitizeDomainLike(p); if (!v) return; if (!out.includes(v)) out.push(v); }); return out; }; const initialValue = Storage.get(options.key, options.value); let displayBox = createElementWithStylesAndAttributes("div", { id: `displayBox_${options.key}` }, { className: "neu-display-box", innerHTML: formatPreview(initialValue), onclick: function() { modal.style.display = 'flex'; try { if (!isArrayMode) { textInput.focus(); var textLength = textInput.value.length; textInput.setSelectionRange(textLength, textLength); } else { const addEl = modal.querySelector ? modal.querySelector('input.neu-list-input') : null; if (addEl) addEl.focus(); } } catch {} } }); let buttonRow = createElementWithStylesAndAttributes("div", {}, { className: "button-row", }); // 列表型输入:可视化编辑状态(仅在 modal 内生效,保存时落盘) let workingList = Array.isArray(initialValue) ? [...initialValue] : (isArrayMode ? [] : null); const syncListTitleAndPreview = () => { try { if (!isArrayMode) return; const list = Array.isArray(workingList) ? workingList : []; displayBox.textContent = formatPreview(list); displayBox.title = list.length ? list.join('\n') : ''; } catch {} }; // 修复:当“批量粘贴”区域打开时,textarea 会参与保存逻辑。 // 若用户只在 chips 里点“×”删除,但 textarea 仍是旧内容,保存时会把删除“还原”。 // 这里保持 textarea 与 workingList 同步,避免刷新后又出现。 const syncBulkTextareaFromWorkingListIfOpen = () => { try { if (!isArrayMode) return; const bulk = modal.querySelector ? modal.querySelector('#neuBulkArea') : null; const bulkVisible = !!(bulk && bulk.style && bulk.style.display !== 'none'); if (!bulkVisible) return; textInput.value = (Array.isArray(workingList) ? workingList : []).join('\n'); } catch {} }; const renderChips = () => { try { const wrap = modal.querySelector ? modal.querySelector('#neuChipWrap') : null; if (!wrap) return; wrap.innerHTML = ''; const list = Array.isArray(workingList) ? workingList : []; if (!list.length) { wrap.innerHTML = '
(空)可在下方输入域名并添加,或使用“批量粘贴”。
'; return; } list.forEach((d) => { const chip = createElementWithStylesAndAttributes('span', {}, { className: 'neu-chip' }); const text = createElementWithStylesAndAttributes('span', {}, { className: 'neu-chip-text', innerText: String(d) }); text.title = String(d); const x = createElementWithStylesAndAttributes('span', {}, { className: 'neu-chip-x', innerText: '×' }); x.addEventListener('click', () => { workingList = (Array.isArray(workingList) ? workingList : []).filter((it) => String(it) !== String(d)); renderChips(); syncListTitleAndPreview(); syncBulkTextareaFromWorkingListIfOpen(); }); chip.appendChild(text); chip.appendChild(x); wrap.appendChild(chip); }); } catch {} }; let saveButton = createElementWithStylesAndAttributes("button", {}, { innerHTML: '保存', onclick: function() { let userInput = textInput.value.trim(); // JSON 规则:保存前做校验,避免静默失败 if (options.key === 'clickInterceptDomainRules') { const validated = validateClickInterceptDomainRules(userInput); if (!validated.ok) { showAlert(`域名拦截规则错误:${validated.message || '格式不正确'}`); modal.style.display = 'flex'; return; } try { // 统一保存为标准化 JSON(更利于 diff/迁移) userInput = JSON.stringify(validated.rules, null, 2); textInput.value = userInput; } catch {} } if (isArrayMode) { // 若用户打开了批量区,则优先以 textarea 解析为准;否则用可视化列表。 const bulk = modal.querySelector ? modal.querySelector('#neuBulkArea') : null; const bulkVisible = !!(bulk && bulk.style && bulk.style.display !== 'none'); if (bulkVisible) { // textarea 始终与 chips 同步,因此这里保持一致性即可 workingList = parseArrayInput(userInput); } const userDomains = Array.isArray(workingList) ? workingList : []; Storage.set(options.key, userDomains); if (options.key === 'blacklistDomains' || options.key === 'whitelistDomains') updateDomainLists(); displayBox.textContent = formatPreview(userDomains); displayBox.title = userDomains.length ? userDomains.join('\n') : ''; modal.style.display = 'none'; showAlert('已保存!'); } else { Storage.set(options.key, userInput); displayBox.textContent = formatPreview(userInput); modal.style.display = 'none'; showAlert('已保存!'); } } }); let cancelButton = createElementWithStylesAndAttributes("button", {}, { innerHTML: '取消', onclick: function() { modal.style.display = 'none'; // 放弃本次编辑:恢复 textarea 与 workingList try { const v = Storage.get(options.key); if (isArrayMode) { workingList = Array.isArray(v) ? [...v] : []; textInput.value = workingList.join('\n'); renderChips(); syncListTitleAndPreview(); } else { textInput.value = (v === undefined || v === null) ? '' : String(v); } } catch {} } }); buttonRow.appendChild(saveButton); buttonRow.appendChild(cancelButton); if (isArrayMode) { // 可视化列表编辑器(chips + 单个添加 + 批量粘贴) const editor = createElementWithStylesAndAttributes('div', {}, { className: 'neu-list-editor' }); const titleEl = createElementWithStylesAndAttributes('div', {}, { className: 'neu-list-title', innerText: '站点列表(支持粘贴 URL/域名)' }); const chipWrap = createElementWithStylesAndAttributes('div', {}, { className: 'neu-chip-wrap', id: 'neuChipWrap' }); const row = createElementWithStylesAndAttributes('div', {}, { className: 'neu-list-row' }); const addInput = createElementWithStylesAndAttributes('input', {}, { className: 'neu-list-input', type: 'text', placeholder: '例如:www.bilibili.com' }); const addBtn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-small-btn', innerText: '添加' }); addBtn.addEventListener('click', () => { const v = sanitizeDomainLike(addInput.value); if (!v) return; const list = Array.isArray(workingList) ? workingList : []; if (!list.includes(v)) list.push(v); workingList = list; addInput.value = ''; renderChips(); syncListTitleAndPreview(); syncBulkTextareaFromWorkingListIfOpen(); }); addInput.addEventListener('keydown', (e) => { if (e && e.key === 'Enter') { e.preventDefault(); addBtn.click(); } }); row.appendChild(addInput); row.appendChild(addBtn); const bulkToggleRow = createElementWithStylesAndAttributes('div', {}, { className: 'neu-bulk-toggle' }); const addCurrentBtn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-small-btn', innerText: '添加当前域' }); addCurrentBtn.addEventListener('click', () => { const domain = sanitizeDomainLike(window.location.hostname); if (!domain) return; const list = Array.isArray(workingList) ? workingList : []; if (list.includes(domain)) { workingList = list.filter((it) => String(it) !== String(domain)); } else { list.push(domain); workingList = list; } renderChips(); syncListTitleAndPreview(); syncBulkTextareaFromWorkingListIfOpen(); }); const bulkToggleBtn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-small-btn', innerText: '批量粘贴' }); const bulkArea = createElementWithStylesAndAttributes('div', {}, { className: 'neu-bulk-area', id: 'neuBulkArea' }); const bulkHint = createElementWithStylesAndAttributes('div', {}, { className: 'neu-bulk-hint', innerText: '支持用逗号/换行/空格分隔;也支持直接粘贴 URL(会自动提取域名)。' }); bulkArea.appendChild(bulkHint); // 批量区复用 textarea(保持原行为),仅显示/隐藏 bulkArea.appendChild(textInput); const bulkApplyRow = createElementWithStylesAndAttributes('div', { width: '250px', display: 'flex', justifyContent: 'flex-end', gap: '8px' }); const bulkApplyBtn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-small-btn', innerText: '应用到列表' }); bulkApplyBtn.addEventListener('click', () => { workingList = parseArrayInput(textInput.value); renderChips(); syncListTitleAndPreview(); syncBulkTextareaFromWorkingListIfOpen(); }); bulkApplyRow.appendChild(bulkApplyBtn); bulkArea.appendChild(bulkApplyRow); bulkToggleBtn.addEventListener('click', () => { const open = bulkArea.style.display !== 'flex'; bulkArea.style.display = open ? 'flex' : 'none'; if (open) { textInput.value = (Array.isArray(workingList) ? workingList : []).join('\n'); try { textInput.focus(); } catch {} } }); bulkToggleRow.appendChild(addCurrentBtn); bulkToggleRow.appendChild(bulkToggleBtn); editor.appendChild(titleEl); editor.appendChild(chipWrap); editor.appendChild(row); editor.appendChild(bulkToggleRow); editor.appendChild(bulkArea); modal.appendChild(editor); modal.appendChild(buttonRow); // 首次渲染 try { const v = Storage.get(options.key, []); workingList = Array.isArray(v) ? [...v] : []; renderChips(); syncListTitleAndPreview(); textInput.value = workingList.join('\n'); syncBulkTextareaFromWorkingListIfOpen(); } catch {} } else { modal.appendChild(textInput); modal.appendChild(buttonRow); } document.body.appendChild(modal); return displayBox; } else if (type === 'colorPicker') { // 颜色选择器:调色盘 + 原生拾色器 + 手动输入(更适合颜色配置项) addStyle('neu-color-picker-style', ` .neu-color-display { font-size: 12px; background: #e0e5ec; border-radius: 10px; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; transition: all 0.3s; display: inline-flex; align-items: center; justify-content: center; cursor: pointer; height: 30px; padding: 0 10px; gap: 8px; user-select:none; } .neu-color-swatch { width: 16px; height: 16px; border-radius: 6px; box-shadow: inset 2px 2px 3px rgba(0,0,0,.18), inset -2px -2px 3px rgba(255,255,255,.7), 1px 1px 2px rgba(163,177,198,.8); border: 1px solid rgba(0,0,0,.08); background: transparent; } .neu-color-code { font-weight: 800; color: #374151; max-width: 120px; overflow:hidden; text-overflow: ellipsis; white-space: nowrap; } .neu-color-modal { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #e0e5ec; border-radius: 12px; box-shadow: 2px 2px 6px #a3b1c6, -2px -2px 6px #ffffff; width: 290px; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); overflow: auto; box-sizing: border-box; padding: 12px; z-index: 1600; } .neu-color-row { display:flex; align-items:center; justify-content: space-between; gap: 10px; margin-bottom: 10px; } .neu-color-big { width: 42px; height: 42px; border-radius: 12px; box-shadow: inset 2px 2px 4px rgba(0,0,0,.18), inset -2px -2px 4px rgba(255,255,255,.7), 2px 2px 4px rgba(163,177,198,.8), -2px -2px 4px rgba(255,255,255,.9); border: 1px solid rgba(0,0,0,.08); background: transparent; flex: 0 0 auto; } .neu-color-input { flex: 1 1 auto; height: 34px; padding: 0 10px; border: none; outline: none; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; font-size: 13px; color:#374151; } .neu-color-mini { flex: 0 0 auto; height: 34px; padding: 0 10px; border: none; border-radius: 10px; cursor: pointer; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; font-size: 12px; font-weight: 800; color:#4B5563; user-select:none; } .neu-color-mini:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .neu-color-grid { display:grid; grid-template-columns: repeat(8, 1fr); gap: 8px; margin: 10px 0 12px 0; } .neu-color-dot { width: 24px; height: 24px; border-radius: 10px; border: none; cursor: pointer; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; padding: 0; } .neu-color-dot:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .neu-color-dot-inner { width: 100%; height: 100%; border-radius: 10px; border: 1px solid rgba(0,0,0,.08); box-shadow: inset 2px 2px 4px rgba(0,0,0,.14), inset -2px -2px 4px rgba(255,255,255,.75); } .neu-color-hint { font-size: 11px; color:#6B7280; line-height: 1.35; margin-top: 6px; } .neu-color-actions { display:flex; justify-content: space-between; gap: 8px; margin-top: 10px; } .neu-color-actions button { flex: 1 1 auto; height: 34px; border: none; cursor: pointer; border-radius: 10px; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; font-weight: 900; color:#374151; } .neu-color-actions button:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } `); const DEFAULT_PALETTE = [ '#111827', '#374151', '#6B7280', '#9CA3AF', '#D1D5DB', '#F3F4F6', '#FFFFFF', '#000000', '#2563EB', '#1D4ED8', '#60A5FA', '#93C5FD', '#0EA5E9', '#06B6D4', '#10B981', '#22C55E', '#F59E0B', '#F97316', '#EF4444', '#DC2626', '#EC4899', '#A855F7', '#8B5CF6', '#14B8A6', ]; const RECENT_KEY = '__il_recent_colors_v1'; const normalizeHex6 = (raw) => { try { const s = String(raw || '').trim(); if (!s) return ''; const m3 = s.match(/^#([0-9a-f]{3})$/i); if (m3) { const x = m3[1]; return '#' + x[0] + x[0] + x[1] + x[1] + x[2] + x[2]; } const m6 = s.match(/^#([0-9a-f]{6})$/i); if (m6) return '#' + m6[1].toUpperCase(); // #RRGGBBAA:color input 不支持 alpha,这里只取前 6 位 const m8 = s.match(/^#([0-9a-f]{8})$/i); if (m8) return '#' + m8[1].slice(0, 6).toUpperCase(); return ''; } catch { return ''; } }; const isValidCssColor = (val) => { try { const s = String(val || '').trim(); if (!s) return false; const el = document.createElement('div'); el.style.color = ''; el.style.color = s; return !!el.style.color; } catch { return false; } }; const getStored = () => { try { const v = Storage.get(options.key, options.value); if (v === undefined || v === null) return ''; return String(v); } catch { return ''; } }; const formatPreview = (raw) => { const s = String(raw || '').trim(); if (!s) return '点击选择'; return s.length > 16 ? (s.slice(0, 16) + '…') : s; }; let working = getStored(); const display = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-display', }); const swatch = createElementWithStylesAndAttributes('span', {}, { className: 'neu-color-swatch' }); const code = createElementWithStylesAndAttributes('span', {}, { className: 'neu-color-code', innerText: formatPreview(working) }); display.appendChild(swatch); display.appendChild(code); const modal = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-modal' }); const bigRow = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-row' }); const big = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-big' }); const input = createElementWithStylesAndAttributes('input', {}, { className: 'neu-color-input', type: 'text', placeholder: '#RRGGBB / red / rgba(...)' }); const copyBtn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-color-mini', innerText: '复制' }); bigRow.appendChild(big); bigRow.appendChild(input); bigRow.appendChild(copyBtn); const pickerRow = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-row' }); const picker = createElementWithStylesAndAttributes('input', {}, { type: 'color' }); picker.style.width = '46px'; picker.style.height = '34px'; picker.style.border = 'none'; picker.style.background = 'transparent'; picker.style.padding = '0'; picker.style.cursor = 'pointer'; const recentBtn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-color-mini', innerText: '最近' }); const resetBtn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-color-mini', innerText: '默认' }); pickerRow.appendChild(picker); pickerRow.appendChild(recentBtn); pickerRow.appendChild(resetBtn); const grid = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-grid' }); const hint = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-hint', innerText: '提示:可点选调色盘,也可手动输入任意 CSS 颜色(如 red / rgba)。' }); const actions = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-actions' }); const okBtn = createElementWithStylesAndAttributes('button', {}, { innerText: '保存' }); const cancelBtn = createElementWithStylesAndAttributes('button', {}, { innerText: '取消' }); actions.appendChild(okBtn); actions.appendChild(cancelBtn); modal.appendChild(bigRow); modal.appendChild(pickerRow); modal.appendChild(grid); modal.appendChild(hint); modal.appendChild(actions); document.body.appendChild(modal); const applyToSwatches = (val) => { try { const v = String(val || '').trim(); const ok = isValidCssColor(v) || !!normalizeHex6(v); const sw = ok ? v : 'transparent'; swatch.style.background = sw; big.style.background = sw; } catch {} }; const setWorking = (val, from) => { working = String(val || '').trim(); input.value = working; code.innerText = formatPreview(working); try { code.title = working; } catch {} applyToSwatches(working); // 同步原生拾色器(尽力而为) try { const hex = normalizeHex6(working); if (hex) picker.value = hex; } catch {} }; const renderPalette = (list) => { grid.innerHTML = ''; (Array.isArray(list) ? list : []).forEach((c) => { const btn = createElementWithStylesAndAttributes('button', {}, { className: 'neu-color-dot', title: String(c) }); const inner = createElementWithStylesAndAttributes('div', {}, { className: 'neu-color-dot-inner' }); inner.style.background = String(c); btn.appendChild(inner); btn.addEventListener('click', () => setWorking(String(c), 'palette')); grid.appendChild(btn); }); }; const readRecent = () => { try { const v = Storage.get(RECENT_KEY, []); const arr = Array.isArray(v) ? v.map((x) => String(x || '').trim()).filter(Boolean) : []; // 去重 + 限长 const out = []; arr.forEach((x) => { if (!out.includes(x)) out.push(x); }); return out.slice(0, 24); } catch { return []; } }; const pushRecent = (val) => { try { const v = String(val || '').trim(); if (!v) return; let arr = readRecent(); arr = [v].concat(arr.filter((x) => x !== v)); Storage.set(RECENT_KEY, arr.slice(0, 24)); } catch {} }; // 事件 display.addEventListener('click', () => { modal.style.display = 'block'; try { input.focus(); } catch {} }); input.addEventListener('input', () => { setWorking(input.value, 'text'); }); picker.addEventListener('input', () => { setWorking(picker.value, 'picker'); }); copyBtn.addEventListener('click', () => { try { tryCopyText(String(input.value || '')); } catch {} }); resetBtn.addEventListener('click', () => { // 默认值优先来自 options.value(由 initializeDefaultSettings 提供) const dv = (options && options.value !== undefined && options.value !== null) ? String(options.value) : ''; const fallback = dv || String(Storage.get(options.key, '') || ''); setWorking(fallback, 'reset'); }); recentBtn.addEventListener('click', () => { const recent = readRecent(); renderPalette(recent.length ? recent : DEFAULT_PALETTE); }); okBtn.addEventListener('click', () => { const v = String(working || '').trim(); if (v && !isValidCssColor(v) && !normalizeHex6(v)) { try { showAlert('颜色格式不正确'); } catch {} return; } try { Storage.set(options.key, v); } catch {} try { emitSettingChanged(options.key, v); } catch {} try { pushRecent(v); } catch {} modal.style.display = 'none'; try { showAlert('已保存!'); } catch {} }); cancelBtn.addEventListener('click', () => { modal.style.display = 'none'; setWorking(getStored(), 'cancel'); }); // 初始渲染 renderPalette(DEFAULT_PALETTE); setWorking(working, 'init'); return display; } else if (type === 'selector') { addStyle("neumorphic-selector-modal-style", `.neu-selector-display { font-size: 14px; line-height: 32px; height: 32px; width: 80px; background: #e0e5ec; border-radius: 10px; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; cursor: pointer; user-select: none; position: relative; text-align: center; box-sizing: border-box; padding: 0 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .neu-selector-modal { display: none; position: absolute; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; border-radius: 10px; z-index: 1000; flex-direction: column; box-sizing: border-box; padding: 6px; min-width: 160px; max-width: 240px; max-height: 240px; overflow-y: auto; overflow-x: hidden; } .neu-selector-modal-open { display: flex; } .neu-selector-option { font-size: 14px; line-height: 1.2; padding: 6px 8px; margin-bottom: 2px; border-radius: 8px; white-space: nowrap; cursor: pointer; user-select: none; text-align: left; width: 100%; box-sizing: border-box; overflow: hidden; text-overflow: ellipsis; } .neu-selector-option:hover { background-color: #d1d9e6; } `); let selectorDisplay = createElementWithStylesAndAttributes("div", {}, { className: "neu-selector-display" }); let modal = createElementWithStylesAndAttributes("div", {}, { className: "neu-selector-modal" }); let container = document.createElement("div"); container.style.position = "relative"; function toShortDisplayText(v) { const s = String(v ?? ''); // 显示区会用 CSS ellipsis 裁剪,这里只做一个较宽松的字符串上限保护 if (s.length <= 24) return s; return s.slice(0, 23) + '…'; } const selectorOptions = options && Array.isArray(options.options) ? options.options : optionsArray; const valueToLabel = new Map(); selectorOptions.forEach((opt) => { if (opt && typeof opt === 'object') { valueToLabel.set(String(opt.value), String(opt.label)); } }); function getDisplayTextForValue(v) { const key = String(v ?? ''); if (valueToLabel.has(key)) return toShortDisplayText(valueToLabel.get(key)); return toShortDisplayText(key); } function getFullTextForValue(v) { const key = String(v ?? ''); if (valueToLabel.has(key)) return String(valueToLabel.get(key)); return key; } function selectOption(option) { const value = (option && typeof option === 'object') ? option.value : option; Storage.set(options.key, value); selectorDisplay.textContent = getDisplayTextForValue(Storage.get(options.key)); selectorDisplay.title = getFullTextForValue(Storage.get(options.key)); try { emitSettingChanged(options.key, value); } catch {} // UI:密度切换需要即时刷新面板 class try { if (options && options.key === 'uiDensity') { if (typeof applyUiDensityClass === 'function') applyUiDensityClass(); } } catch {} modal.className = "neu-selector-modal"; } selectorOptions.forEach((option) => { let optionElement = createElementWithStylesAndAttributes("div", {}, { className: "neu-selector-option", textContent: (option && typeof option === 'object') ? option.label : option, title: (option && typeof option === 'object') ? String(option.label) : String(option), onclick: () => selectOption(option) }); modal.appendChild(optionElement); }); selectorDisplay.addEventListener("click", function(event) { event.stopPropagation(); modal.className = modal.className.includes("neu-selector-modal-open") ? "neu-selector-modal" : "neu-selector-modal neu-selector-modal-open"; let modalRect = modal.getBoundingClientRect(); // 右侧对齐:让下拉的右边界与 selectorDisplay 右边界对齐,避免长文案“溢出面板”。 const dx = Math.max(0, modalRect.width - selectorDisplay.offsetWidth); modal.style.left = `-${dx}px`; // 垂直居中:但不强求(高度变化时尽量不顶出可视区) modal.style.top = `${Math.min(0, (selectorDisplay.offsetHeight - modalRect.height) / 2)}px`; }); document.addEventListener("click", function() { modal.className = "neu-selector-modal"; }); container.appendChild(selectorDisplay); container.appendChild(modal); selectorDisplay.textContent = getDisplayTextForValue(Storage.get(options.key)); selectorDisplay.title = getFullTextForValue(Storage.get(options.key)); return container; } else if (type === 'shortcutKeySetting') { addStyle("shortcutKeySetting-style", ` .shortcutKeySetting-displayBox { width: 80px; height: 32px; overflow:auto ;background: #e0e5ec; border-radius: 5px; box-shadow: inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF; border: none; margin: 5px 0; flex: 1; text-align: center; outline: none; width: 80px; } .shortcutKeySetting-button { width: 60px; height: 32px; margin-left: 10px; background: #e0e5ec; border-radius: 10px; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; border: none; cursor: pointer; user-select: none; } .shortcutKeySetting-container { display: flex; justify-content: space-between; align-items: center; box-sizing: border-box; } `); let container = createElementWithStylesAndAttributes('div', {}, { className: "shortcutKeySetting-container" }); let shortcutKeyValue = Storage.get(options.key); let displayPlaceholder = shortcutKeyValue ? shortcutKeyValue.toUpperCase() : '快捷键'; let displayBox = createElementWithStylesAndAttributes('input', {}, { type: 'text', readOnly: true, placeholder: displayPlaceholder, className: "shortcutKeySetting-displayBox" }); let setButton = createElementWithStylesAndAttributes('button', {}, { innerText: '设定', className: "shortcutKeySetting-button" }); setButton.addEventListener('click', function() { displayBox.value = '按下任意键...'; displayBox.disabled = false; let keySequence = []; let keyDownEvent = function(event) { event.preventDefault(); let key = event.key.toLowerCase(); if (!keySequence.includes(key)) { keySequence.push(key); displayBox.value = keySequence.join('+').toUpperCase(); Storage.set(options.key, keySequence.join('+')); console.log('快捷键已设置:', Storage.get(options.key)); } }; let keyUpEvent = function() { document.removeEventListener('keydown', keyDownEvent); displayBox.disabled = true; document.removeEventListener('keyup', keyUpEvent); }; document.addEventListener('keydown', keyDownEvent); document.addEventListener('keyup', keyUpEvent); }); container.appendChild(displayBox); container.appendChild(setButton); return container; } else if (type === 'numberPicker') { addStyle("neumorphic-numberPicker-style", `.neu-numberPicker-container { max-width: 121px;width: 100%; height: 32px; display: flex; align-items: center; justify-content: space-between; background: #e0e5ec; border-radius: 10px; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; } .neu-numberPicker-button { background: #e0e5ec; border: none; border-radius: 100%; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; width: 30px; height: 30px; cursor: pointer; font-size: 15px; display: flex; align-items: center; justify-content: center; user-select: none; min-width: 30px;} .neu-numberPicker-button:active {box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .neu-numberPicker-value {width: 40px; text-align: center; font-size: 16px; background: transparent; border: none; outline: none; margin: 0 auto; }`); let container = createElementWithStylesAndAttributes("div", {}, { className: "neu-numberPicker-container" }); let intervalId = null; const updateValue = (increment) => { let currentValue = parseInt(valueDisplay.value, 10) || 0; let newValue = increment ? currentValue + 1 : currentValue - 1; if (newValue < 1) { newValue = 1; // 最小值限制 showAlert('亲,“1”难道还不够小嘛'); } valueDisplay.value = newValue; Storage.set(options.key, valueDisplay.value); try { emitSettingChanged(options.key, Number(valueDisplay.value)); } catch {} }; const createContinuousButton = (innerHTML, increment) => { let button = createElementWithStylesAndAttributes("button", {}, { innerHTML: innerHTML, className: "neu-numberPicker-button" }); button.addEventListener("mousedown", function() { updateValue(increment); intervalId = setInterval(() => updateValue(increment), 200); }); ['mouseup', 'mouseleave'].forEach(event => { button.addEventListener(event, function() { clearInterval(intervalId); }); }); return button; }; let currentValue = Storage.get(options.key, 0); let valueDisplay = createElementWithStylesAndAttributes("input", {}, { type: "text", value: currentValue, className: "neu-numberPicker-value", oninput: function() { this.value = this.value.replace(/[^0-9]/g, ''); Storage.set(options.key, this.value); try { emitSettingChanged(options.key, Number(this.value || 0)); } catch {} } }); let minusButton = createContinuousButton('➖', false); let plusButton = createContinuousButton('➕', true); container.appendChild(minusButton); container.appendChild(valueDisplay); container.appendChild(plusButton); return container; } else if (type === 'settingManager') { // 设置导入/导出/回滚 addStyle('instant-load-setting-manager-style', ` .setting-manager-btn { height: 28px; padding: 0 10px; border-radius: 10px; border: none; cursor: pointer; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; font-size: 12px; font-weight: 700; color: #4B5563; user-select: none; } .setting-manager-btn:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .setting-manager-modal { display:none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 320px; max-width: calc(100vw - 16px); height: 520px; max-height: calc(100vh - 16px); background: #e0e5ec; border-radius: 12px; box-shadow: 2px 2px 8px #BECBD8, -2px -2px 8px #FFFFFF; z-index: 2000; box-sizing: border-box; padding: 10px; overflow: hidden; flex-direction: column; } .setting-manager-row { display:flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; } .setting-manager-textarea { width: 100%; flex: 1 1 auto; min-height: 180px; height: auto; resize: none; border: none; outline: none; border-radius: 10px; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; background: #e0e5ec; padding: 10px; box-sizing: border-box; font-size: 12px; line-height: 1.35; color: #374151; } .setting-manager-title { display:flex; align-items:center; justify-content: space-between; gap: 8px; } .setting-manager-sep { height: 1px; background: rgba(190, 203, 216, 0.8); margin: 8px 0; } .setting-manager-subtitle { font-size: 12px; font-weight: 900; color: #111827; } .setting-manager-inline { display:flex; gap: 8px; align-items:center; flex-wrap: wrap; } .setting-manager-input { height: 28px; padding: 0 10px; border: none; outline: none; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; font-size: 12px; color: #374151; } `); const container = createElementWithStylesAndAttributes('div', { display: 'flex', justifyContent: 'flex-end', alignItems: 'center', width: '80px', height: '32px', margin: '5px 0' }); const openBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '管理' }); const modal = createElementWithStylesAndAttributes('div', {}, { className: 'setting-manager-modal' }); const titleRow = createElementWithStylesAndAttributes('div', {}, { className: 'setting-manager-title' }); const title = createElementWithStylesAndAttributes('div', { fontSize: '14px', fontWeight: '800', color: '#111827' }, { innerText: '设置导入 / 导出 / 回滚' }); const closeBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '关闭' }); titleRow.appendChild(title); titleRow.appendChild(closeBtn); const textarea = createElementWithStylesAndAttributes('textarea', {}, { className: 'setting-manager-textarea', placeholder: '这里会显示导出的 JSON;也可以粘贴 JSON 用于导入。' }); const row1 = createElementWithStylesAndAttributes('div', {}, { className: 'setting-manager-row' }); const exportBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '导出并复制' }); const importBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '校验并导入' }); const rollbackBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '回滚上次导入' }); row1.appendChild(exportBtn); row1.appendChild(importBtn); row1.appendChild(rollbackBtn); // Profiles(预设) const sep = createElementWithStylesAndAttributes('div', {}, { className: 'setting-manager-sep' }); const profilesTitle = createElementWithStylesAndAttributes('div', {}, { className: 'setting-manager-subtitle', innerText: '预设(Profiles)' }); const profilesRow = createElementWithStylesAndAttributes('div', {}, { className: 'setting-manager-inline' }); const profileInput = createElementWithStylesAndAttributes('input', {}, { className: 'setting-manager-input', type: 'text', placeholder: '预设名(例如:保守/激进)' }); profileInput.style.width = '140px'; const profileSelect = createElementWithStylesAndAttributes('select', { height: '28px', borderRadius: '10px', border: 'none', outline: 'none', padding: '0 8px', boxShadow: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff', background: '#E0E5EC', color: '#4B5563', fontSize: '12px' }, {}); profileSelect.style.minWidth = '120px'; const refreshProfiles = () => { try { const list = listAvailableProfiles(); profileSelect.innerHTML = ''; const opt0 = document.createElement('option'); opt0.value = ''; opt0.textContent = list.length ? '选择预设…' : '(暂无预设)'; profileSelect.appendChild(opt0); list.forEach((n) => { const opt = document.createElement('option'); opt.value = n; opt.textContent = n; profileSelect.appendChild(opt); }); } catch {} }; const saveProfileBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '保存为预设' }); const applyProfileBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '应用预设' }); const deleteProfileBtn = createElementWithStylesAndAttributes('button', {}, { className: 'setting-manager-btn', innerText: '删除预设' }); saveProfileBtn.addEventListener('click', () => { const name = String(profileInput.value || '').trim(); if (!name) { toast('请输入预设名', 'warn'); return; } const res = saveProfile(name, getAllSettingsSnapshot()); if (!res.ok) { toast(String(res.message || '保存失败'), 'error'); return; } toast('已保存预设', 'success'); refreshProfiles(); profileSelect.value = name; }); applyProfileBtn.addEventListener('click', () => { const selected = String(profileSelect.value || '').trim(); if (!selected) { toast('请先选择预设', 'warn'); return; } const loaded = loadProfile(selected); if (!loaded.ok) { toast(String(loaded.message || '读取失败'), 'error'); return; } try { Storage.set('__settingsBackup_v1', getAllSettingsSnapshot()); Storage.set('__settingsBackupAt_v1', Date.now()); } catch {} const applied = applySettingsSnapshot(loaded.snapshot, 'apply'); if (applied.ok) { const n = Array.isArray(applied.changes) ? applied.changes.length : 0; toast(`已应用预设(变更 ${n} 项)`, 'success'); } else { toast(String(applied.message || '应用失败'), 'error'); } }); deleteProfileBtn.addEventListener('click', () => { const selected = String(profileSelect.value || '').trim(); if (!selected) { toast('请先选择预设', 'warn'); return; } const ok = window.confirm(`确认删除预设:${selected} ?`); if (!ok) return; const res = deleteProfile(selected); if (!res.ok) { toast(String(res.message || '删除失败'), 'error'); return; } toast('已删除预设', 'success'); refreshProfiles(); }); profilesRow.appendChild(profileInput); profilesRow.appendChild(saveProfileBtn); profilesRow.appendChild(profileSelect); profilesRow.appendChild(applyProfileBtn); profilesRow.appendChild(deleteProfileBtn); const hint = createElementWithStylesAndAttributes('div', { marginTop: '8px', fontSize: '12px', color: '#4B5563', lineHeight: '1.4' }, { innerText: '提示:导入前会自动备份;导入只会写入本脚本相关的已知设置项。' }); function openModal() { try { textarea.value = stableStringify(getAllSettingsSnapshot()); } catch { textarea.value = ''; } try { refreshProfiles(); } catch {} modal.style.display = 'flex'; } function closeModal() { modal.style.display = 'none'; } openBtn.addEventListener('click', openModal); closeBtn.addEventListener('click', closeModal); exportBtn.addEventListener('click', () => { try { const payload = stableStringify(getAllSettingsSnapshot()); textarea.value = payload; tryCopyText(payload); } catch { showAlert('导出失败'); } }); importBtn.addEventListener('click', () => { const raw = String(textarea.value || '').trim(); if (!raw) { showAlert('请先粘贴 JSON'); return; } const parsed = safeJsonParse(raw); if (!parsed.ok) { showAlert('JSON 解析失败'); return; } // dry-run 校验 const dry = applySettingsSnapshot(parsed.value, 'dry-run'); if (!dry.ok) { showAlert(String(dry.message || '校验失败')); return; } try { Storage.set('__settingsBackup_v1', getAllSettingsSnapshot()); Storage.set('__settingsBackupAt_v1', Date.now()); } catch {} const applied = applySettingsSnapshot(parsed.value, 'apply'); if (applied.ok) { const n = Array.isArray(applied.changes) ? applied.changes.length : 0; showAlert(`导入成功(变更 ${n} 项)`); } else { showAlert(String(applied.message || '导入失败')); } }); rollbackBtn.addEventListener('click', () => { try { const bk = Storage.get('__settingsBackup_v1', null); if (!bk || typeof bk !== 'object') { showAlert('没有可回滚的备份'); return; } const applied = applySettingsSnapshot(bk, 'apply'); if (applied.ok) { const n = Array.isArray(applied.changes) ? applied.changes.length : 0; showAlert(`已回滚(恢复 ${n} 项)`); } else { showAlert(String(applied.message || '回滚失败')); } } catch { showAlert('回滚失败'); } }); modal.appendChild(titleRow); modal.appendChild(textarea); modal.appendChild(row1); modal.appendChild(sep); modal.appendChild(profilesTitle); modal.appendChild(profilesRow); modal.appendChild(hint); document.body.appendChild(modal); container.appendChild(openBtn); return container; } else if (type === 'domainRulesEditor') { // 域名 -> 点击拦截策略 的可视化编辑器(保留 JSON 高级模式兜底) addStyle('domain-rules-editor-style', ` .dre-wrap { width: 250px; box-sizing: border-box; display:flex; flex-direction: column; gap: 8px; } .dre-row { width: 250px; display:flex; gap: 8px; align-items:center; } .dre-input { flex: 1 1 auto; height: 30px; padding: 0 10px; border: none; outline: none; border-radius: 10px; background: #e0e5ec; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; font-size: 13px; color:#374151; } .dre-btn { flex: 0 0 auto; height: 30px; padding: 0 10px; border: none; border-radius: 10px; cursor: pointer; background: #e0e5ec; box-shadow: 2px 2px 4px #a3b1c6, -2px -2px 4px #ffffff; font-size: 12px; font-weight: 800; color:#4B5563; user-select:none; } .dre-btn:hover { box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; } .dre-card { width: 250px; border-radius: 12px; background:#E0E5EC; box-shadow: inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff; padding: 8px; box-sizing: border-box; } .dre-item { display:flex; align-items:center; justify-content: space-between; gap: 8px; padding: 6px 6px; border-radius: 10px; } .dre-item:nth-child(2n) { background: rgba(190,203,216,0.35); } .dre-host { font-size: 12px; font-weight: 900; color:#111827; overflow:hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 140px; } .dre-host-sub { font-size: 11px; color:#6B7280; } .dre-pill { padding: 2px 8px; border-radius: 999px; background:#E0E5EC; box-shadow: 2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF; font-size: 11px; color:#374151; user-select:none; } .dre-modal { display:none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 360px; max-width: calc(100vw - 16px); height: 520px; max-height: calc(100vh - 16px); background: #e0e5ec; border-radius: 12px; box-shadow: 2px 2px 8px #BECBD8, -2px -2px 8px #FFFFFF; z-index: 2500; box-sizing: border-box; padding: 10px; overflow: hidden; flex-direction: column; gap: 8px; } .dre-modal-title { display:flex; align-items:center; justify-content: space-between; gap: 8px; } .dre-modal-body { flex: 1 1 auto; overflow: auto; } `); const wrap = createElementWithStylesAndAttributes('div', {}, { className: 'dre-wrap' }); // 主入口只放一个“配置”按钮,详细编辑放到弹窗里(与黑白名单交互一致) const openCfgBtn = createElementWithStylesAndAttributes('button', {}, { className: 'dre-btn', innerText: '配置…' }); const modal = createElementWithStylesAndAttributes('div', {}, { className: 'dre-modal' }); const modalTitleRow = createElementWithStylesAndAttributes('div', {}, { className: 'dre-modal-title' }); const modalTitle = createElementWithStylesAndAttributes('div', { fontSize: '14px', fontWeight: '900', color: '#111827' }, { innerText: '站点规则(按域名覆盖点击拦截)' }); const modalClose = createElementWithStylesAndAttributes('button', {}, { className: 'dre-btn', innerText: '关闭' }); modalTitleRow.appendChild(modalTitle); modalTitleRow.appendChild(modalClose); const modalBody = createElementWithStylesAndAttributes('div', {}, { className: 'dre-modal-body' }); modal.appendChild(modalTitleRow); modal.appendChild(modalBody); document.body.appendChild(modal); openCfgBtn.addEventListener('click', () => { modal.style.display = 'flex'; }); modalClose.addEventListener('click', () => { modal.style.display = 'none'; }); const sanitizeHost = (s) => { try { let t = String(s || '').trim(); if (!t) return ''; t = t.replace(/^https?:\/\//i, ''); t = t.replace(/^\/\//, ''); t = t.split(/[\s\/\?#]/)[0]; t = t.replace(/:.*$/, ''); t = t.replace(/^\.+/, '').replace(/\.+$/, ''); return t.toLowerCase(); } catch { return ''; } }; const normalizeRuleKeyUI = (s) => { try { let t = String(s || '').trim(); if (!t) return ''; // 支持粘贴 URL:转为 host + path try { if (/^https?:\/\//i.test(t)) { const u = new URL(t); t = u.hostname + (u.pathname && u.pathname !== '/' ? u.pathname : ''); } } catch {} // 统一去空格 t = t.replace(/\s+/g, ''); return t; } catch { return ''; } }; const modeLabel = (v) => { const key = String(v || ''); if (key === 'all') return '全拦截'; if (key === 'preloaded-only') return '仅命中'; if (key === 'same-origin') return '仅同源'; if (key === 'whitelist-only') return '仅白名单'; if (key === 'off') return '关闭'; return key || '—'; }; const readRulesV1 = () => { try { const raw = Storage.get('clickInterceptDomainRules', '{}'); const parsed = (typeof raw === 'string') ? raw : JSON.stringify(raw || {}); const validated = validateClickInterceptDomainRules(parsed); if (validated && validated.ok) return validated.rules || {}; } catch {} return {}; }; const writeRulesV1 = (obj) => { try { const s = JSON.stringify(obj || {}, null, 2); Storage.set('clickInterceptDomainRules', s); } catch {} }; const readRulesV2 = () => { try { const raw = Storage.get('clickInterceptDomainRulesV2', '{}'); const parsed = (typeof raw === 'string') ? raw : JSON.stringify(raw || {}); const validated = validateClickInterceptDomainRulesV2(parsed); if (validated && validated.ok) return validated.rules || {}; } catch {} return {}; }; const writeRulesV2 = (obj) => { try { const s = JSON.stringify(obj || {}, null, 2); Storage.set('clickInterceptDomainRulesV2', s); } catch {} }; // 避免 __domainRule_* 临时 key 污染导入/导出:在编辑器内自管变更 const setModeForRuleKeyV2 = (ruleKey, mode) => { try { const m = String(mode || '').trim(); if (!CLICK_INTERCEPT_MODE_VALUES.has(m)) return; const key = normalizeRuleKeyUI(ruleKey); if (!key) return; const nextRules = readRulesV2(); nextRules[String(key)] = { mode: m }; writeRulesV2(nextRules); toast('已更新站点规则', 'success'); } catch {} }; const rulesCard = createElementWithStylesAndAttributes('div', {}, { className: 'dre-card' }); const render = () => { const rules = readRulesV2(); const entries = Object.entries(rules || {}).sort((a, b) => String(a[0]).localeCompare(String(b[0]))); rulesCard.innerHTML = ''; if (!entries.length) { rulesCard.innerHTML = '
(暂无规则)添加域名后可为该站点覆盖点击拦截模式。
'; return; } entries.forEach(([ruleKey, obj]) => { const mode = String(obj && obj.mode ? obj.mode : ''); const row = createElementWithStylesAndAttributes('div', {}, { className: 'dre-item' }); const left = createElementWithStylesAndAttributes('div', { minWidth: '0', flex: '1 1 auto' }); left.appendChild(createElementWithStylesAndAttributes('div', {}, { className: 'dre-host', innerText: String(ruleKey) })); left.appendChild(createElementWithStylesAndAttributes('div', {}, { className: 'dre-host-sub', innerText: `覆盖:${modeLabel(mode)}` })); const right = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', gap: '6px', flex: '0 0 auto' }); const select = createUIComponent('selector', { options: clickInterceptModeOptions, key: `__domainRule_${ruleKey}` }); // selector 内部会写 Storage:这里只同步 UI 显示,不依赖该值持久化 try { Storage.set(`__domainRule_${ruleKey}`, String(mode || '')); } catch {} try { const disp = select && select.querySelector ? select.querySelector('.neu-selector-display') : null; if (disp) { disp.style.width = '78px'; disp.style.height = '30px'; disp.style.lineHeight = '30px'; } } catch {} // 监听选择器点击:直接从 Storage 读当前值并写入规则 try { const optionEls = select && select.querySelectorAll ? select.querySelectorAll('.neu-selector-option') : null; if (optionEls && optionEls.length) { optionEls.forEach((el) => { el.addEventListener('click', () => { try { const chosen = String(Storage.get(`__domainRule_${ruleKey}`) || '').trim(); setModeForRuleKeyV2(ruleKey, chosen); } catch {} render(); }); }); } } catch {} const del = createElementWithStylesAndAttributes('span', {}, { className: 'dre-pill', innerText: '删除' }); del.style.cursor = 'pointer'; del.addEventListener('click', () => { const nextRules = readRulesV2(); delete nextRules[String(ruleKey)]; writeRulesV2(nextRules); toast('已删除规则', 'success'); render(); }); right.appendChild(select); right.appendChild(del); row.appendChild(left); row.appendChild(right); rulesCard.appendChild(row); }); }; const addRow = createElementWithStylesAndAttributes('div', {}, { className: 'dre-row' }); const hostInput = createElementWithStylesAndAttributes('input', {}, { className: 'dre-input', type: 'text', placeholder: '添加域名(支持粘贴 URL)' }); const addBtn = createElementWithStylesAndAttributes('button', {}, { className: 'dre-btn', innerText: '添加' }); addBtn.addEventListener('click', () => { const ruleKey = normalizeRuleKeyUI(hostInput.value); if (!ruleKey) return; // 允许 host / host+path / *.host / re: const lower = String(ruleKey).toLowerCase(); let ok = false; if (lower.startsWith('re:')) { try { new RegExp(String(ruleKey.slice(3) || '').trim()); ok = true; } catch { ok = false; } } else { const parts = lower.split('/'); const host = parts[0]; ok = /^[*]?[.]?[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(host); } if (!ok) { toast('规则格式不合法(支持:域名 / 域名+路径前缀 / *.域名 / re:正则)', 'warn'); return; } const nextRules = readRulesV2(); if (!nextRules[ruleKey]) nextRules[ruleKey] = { mode: 'off' }; writeRulesV2(nextRules); hostInput.value = ''; toast('已添加规则(默认关闭)', 'success'); render(); }); hostInput.addEventListener('keydown', (e) => { if (e && e.key === 'Enter') { e.preventDefault(); addBtn.click(); } }); addRow.appendChild(hostInput); addRow.appendChild(addBtn); const jsonRow = createElementWithStylesAndAttributes('div', { width: '250px', display: 'flex', justifyContent: 'space-between', gap: '8px' }); const openJsonBtn = createElementWithStylesAndAttributes('button', {}, { className: 'dre-btn', innerText: 'JSON 高级模式' }); const copyJsonBtn = createElementWithStylesAndAttributes('button', {}, { className: 'dre-btn', innerText: '复制 JSON' }); openJsonBtn.addEventListener('click', () => { try { const raw = Storage.get('clickInterceptDomainRulesV2', '{}'); const modal = createElementWithStylesAndAttributes('div', { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', width: '340px', maxWidth: 'calc(100vw - 16px)', height: '420px', maxHeight: 'calc(100vh - 16px)', background: '#e0e5ec', borderRadius: '12px', boxShadow: '2px 2px 8px #BECBD8, -2px -2px 8px #FFFFFF', zIndex: '2500', boxSizing: 'border-box', padding: '10px', display: 'flex', flexDirection: 'column', gap: '8px' }); const title = createElementWithStylesAndAttributes('div', { fontSize: '14px', fontWeight: '900', color: '#111827' }, { innerText: '站点规则(支持路径/通配/正则)' }); const ta = createElementWithStylesAndAttributes('textarea', { flex: '1 1 auto', width: '100%', resize: 'none', border: 'none', outline: 'none', borderRadius: '10px', padding: '10px', boxSizing: 'border-box', background: '#e0e5ec', boxShadow: 'inset 2px 2px 4px #a3b1c6, inset -2px -2px 4px #ffffff', fontSize: '12px', lineHeight: '1.4', color: '#374151' }, { value: (typeof raw === 'string') ? raw : JSON.stringify(raw || {}, null, 2) }); const btnRow = createElementWithStylesAndAttributes('div', { display: 'flex', justifyContent: 'flex-end', gap: '8px' }); const cancel = createElementWithStylesAndAttributes('button', {}, { className: 'dre-btn', innerText: '取消' }); const apply = createElementWithStylesAndAttributes('button', {}, { className: 'dre-btn', innerText: '应用' }); cancel.addEventListener('click', () => { try { modal.remove(); } catch {} }); apply.addEventListener('click', () => { const validated = validateClickInterceptDomainRulesV2(ta.value); if (!validated.ok) { toast(`规则错误:${validated.message || '格式不正确'}`, 'error'); return; } try { Storage.set('clickInterceptDomainRulesV2', JSON.stringify(validated.rules, null, 2)); } catch {} toast('已应用规则', 'success'); try { modal.remove(); } catch {} render(); }); btnRow.appendChild(cancel); btnRow.appendChild(apply); modal.appendChild(title); modal.appendChild(ta); modal.appendChild(btnRow); document.body.appendChild(modal); } catch { toast('打开失败', 'error'); } }); copyJsonBtn.addEventListener('click', () => { try { const raw = Storage.get('clickInterceptDomainRulesV2', '{}'); tryCopyText((typeof raw === 'string') ? raw : JSON.stringify(raw || {}, null, 2)); } catch { toast('复制失败', 'error'); } }); jsonRow.appendChild(openJsonBtn); jsonRow.appendChild(copyJsonBtn); // 组装:弹窗内包含“添加 + 列表 + JSON” modalBody.appendChild(addRow); try { const tip = createElementWithStylesAndAttributes('div', { fontSize: '12px', color: '#4B5563', lineHeight: '1.35' }, { innerText: '支持格式:example.com(域名)、example.com/path(路径前缀)、*.example.com(子域通配)、re:^https?://example\\.com/(a|b)(正则)' }); modalBody.appendChild(tip); } catch {} modalBody.appendChild(rulesCard); modalBody.appendChild(jsonRow); render(); // 外层只显示一个按钮 wrap.appendChild(openCfgBtn); return wrap; } else { throw new Error('不支持的UI组件类型'); } } /** * 创建显示版本信息的元素,并添加到页面中。 * * @returns {HTMLElement} - 包含版本信息和更新日志按钮的元素。 */ function createVersionInfoElement() { const isCompact = !!Storage.get('footerCompactMode', true); const versionInfo = createElementWithStylesAndAttributes('div', { position: 'relative', left: '0px', width: '100%', maxWidth: '100%', bottom: '0px', fontSize: '10px', color: '#666', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: '6px', marginTop: '10px', padding: '0 10px', boxSizing: 'border-box', }, { id: 'instantLoadFooterInfo' }); const currentVersion = SCRIPT_META.currentVersion; const latestVersion = Storage.get('latestVersion', SCRIPT_META.currentVersion); const versionLine = isCompact ? `v${currentVersion}(最新:${latestVersion})` : `当前版本:${currentVersion}(最新版本:${latestVersion})`; const versionText = createElementWithStylesAndAttributes('div', { width: '100%', maxWidth: '100%', lineHeight: '1.3', wordBreak: 'break-word', overflowWrap: 'anywhere', display: isCompact ? 'none' : 'block', }, { innerHTML: versionLine }); versionInfo.appendChild(versionText); const versionActionsRow = createElementWithStylesAndAttributes('div', { display: 'flex', alignItems: 'center', justifyContent: 'flex-start', gap: '6px', flexWrap: 'wrap', width: '100%', maxWidth: '100%', }); // 底部状态栏:紧凑模式也保持关键信息可见(缓存/队列/并发/降级) const statusBadge = (id, text) => createElementWithStylesAndAttributes('span', { padding: '2px 8px', borderRadius: '999px', backgroundColor: '#E0E5EC', boxShadow: 'inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF', fontSize: '11px', userSelect: 'none', color: '#4B5563' }, { id, innerText: text }); const queueBadge = statusBadge('instantLoadQueueStatus', '队列:—'); const inflightBadge = statusBadge('instantLoadInFlightStatus', '并发:—'); const hitBadge = statusBadge('instantLoadHitStatus', '命中:—'); const intensityBadge = statusBadge('instantLoadIntensityStatus', '强度:—'); const compactToggle = createElementWithStylesAndAttributes('button', { height: '26px', padding: '0 10px', borderRadius: '999px', border: 'none', cursor: 'pointer', background: '#E0E5EC', boxShadow: '2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF', color: '#4B5563', fontSize: '12px', fontWeight: '700', userSelect: 'none' }, { innerText: isCompact ? '展开' : '紧凑' }); compactToggle.addEventListener('click', () => { try { Storage.set('footerCompactMode', !Storage.get('footerCompactMode', true)); } catch {} try { const current = document.getElementById('instantLoadFooterInfo'); if (current && current.parentNode) current.parentNode.replaceChild(createVersionInfoElement(), current); } catch {} }); const updateLogButton = document.createElement('button'); updateLogButton.innerHTML = '更新日志'; updateLogButton.style.cssText = [ 'display:inline-flex', 'align-items:center', 'justify-content:center', 'height:26px', 'padding:0 10px', 'background-color:#E0E5EC', 'border:none', 'border-radius:999px', 'cursor:pointer', 'font-size:12px', 'font-weight:700', 'color:#4B5563', // 凸出:轻量“浮起”阴影 'box-shadow:2px 2px 4px #AEBEC7, -2px -2px 4px #FFFFFF', ].join(';'); if (updateLogButton) { updateLogButton.onclick = fetchAndDisplayVersionHistory; } versionActionsRow.appendChild(updateLogButton); versionActionsRow.appendChild(compactToggle); const cacheBadge = createElementWithStylesAndAttributes('span', { padding: '2px 8px', borderRadius: '999px', backgroundColor: '#E0E5EC', boxShadow: 'inset 2px 2px 4px #BECBD8, inset -2px -2px 4px #FFFFFF', fontSize: '11px', userSelect: 'none' }, { id: 'instantLoadCacheStatus', innerText: '缓存:正常' }); versionActionsRow.appendChild(cacheBadge); versionActionsRow.appendChild(queueBadge); versionActionsRow.appendChild(inflightBadge); versionActionsRow.appendChild(hitBadge); versionActionsRow.appendChild(intensityBadge); versionInfo.appendChild(versionActionsRow); updateCacheStatusBadge(cacheBadge); // 点击徽标显示详情 try { attachFooterStatusDetailsHandlers(versionInfo); } catch {} try { // 队列:仅表示“等待处理”的排队数量 const q = (Array.isArray(preloadQueue) ? preloadQueue.length : 0); queueBadge.textContent = `队列:${q}`; } catch {} try { inflightBadge.textContent = `并发:${Number(currentPreloads || 0)}/${Number(maxConcurrentPreloads || 0)}`; inflightBadge.style.color = (Number(currentPreloads || 0) > 0) ? '#1D4ED8' : '#4B5563'; } catch {} // 调度强度:综合网络约束 + 动态 cap + 页面可见性 try { const hardStop = (() => { try { if (typeof document !== 'undefined' && document.hidden) return true; } catch {} try { if (isNetworkConstrainedForPreload()) return true; } catch {} return false; })(); if (hardStop) { intensityBadge.textContent = '强度:0(暂停)'; intensityBadge.style.color = '#B45309'; } else { const cap = Number(getDynamicConcurrencyCap() || 0); const userCap = Number(maxConcurrentPreloads || 0); const ratio = (userCap > 0) ? Math.max(0, Math.min(1, cap / userCap)) : 0; const level = (cap <= 1 || ratio <= 0.34) ? '低' : (cap <= 2 || ratio <= 0.67) ? '中' : '高'; // 网络标记:优先展示 saveData / effectiveType let netHint = ''; try { const s = __netConnSnapshot; if (s && s.ok) { const et = String(s.effectiveType || '').toLowerCase(); if (s.saveData) netHint = '省流'; else if (et) netHint = et; } else { // 仅在离线时展示 try { if (navigator && navigator.onLine === false) netHint = '离线'; } catch {} } } catch {} intensityBadge.textContent = `强度:${level}${netHint ? '·' + netHint : ''}`; intensityBadge.style.color = (level === '高') ? '#065F46' : (level === '中') ? '#1D4ED8' : '#4B5563'; } } catch {} try { hitBadge.textContent = formatHitBadgeText(); const snap = getHitRateSnapshot(10 * 60 * 1000); const rate = Number(snap.rate || 0); hitBadge.style.color = snap.total ? (rate >= 0.6 ? '#065F46' : rate >= 0.3 ? '#B45309' : '#B91C1C') : '#4B5563'; } catch {} return versionInfo; } /** * 显示版本历史面板。 * * @param {string} versionHistory - 版本历史内容的HTML字符串。 */ function displayVersionHistoryPanel(versionHistory) { let versionHistoryPanel = document.getElementById('versionHistoryPanel'); if (!versionHistoryPanel) { versionHistoryPanel = document.createElement('div'); versionHistoryPanel.id = 'versionHistoryPanel'; document.body.appendChild(versionHistoryPanel); } else { versionHistoryPanel.style.display = 'block'; } versionHistoryPanel.innerHTML = `

更新日志

${versionHistory}
`; document.getElementById('closeVersionHistory').addEventListener('click', function() { versionHistoryPanel.style.display = 'none'; }); } /** * 通过网站获取版本历史信息并显示在自定义面板中。 */ async function fetchAndDisplayVersionHistory() { const url = SCRIPT_META.versionHistoryUrl; try { const response = await fetch(url); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); const versionHistory = doc.querySelector('.history_versions'); if (versionHistory) { displayVersionHistoryPanel(versionHistory.innerHTML); } else { throw new Error('无法找到更新历史信息的元素'); } } catch (error) { console.error('Failed to fetch version history', error); showAlert('无法获取更新历史,请稍后重试。'); } } /** * 初始化默认设置并存储。 */ function initializeDefaultSettings() { const defaultSettings = { //设置版本号(用于迁移)。注意:只在首次安装或未写入时设置。 schemaVersion: 1, backToThePreviousPage: "shift+r", // 默认黑名单仅在“首次安装/未设置过”时写入一次;后续用户可自由增删,不会被回填。 blacklistDomains: [...DEFAULT_BLACKLIST_DOMAINS], blackSelector: true, concurrentLoadingNumber: 2, forward: "shift+f", goToTheCorrespondingPage: "shift+e", addToBlacklistShortcut: "shift+b", addToWhitelistShortcut: "shift+w", lazyLoadImages: false, loadedStyle: "下划线", manipulatorBall: true, // 操作球:展开样式设置(展开时用于实时预览) manipulatorBallStyleExpanded: false, // 操作球:高自由度样式参数 manipulatorBallSize: 52, manipulatorBallRadius: 16, manipulatorBallOpacity: 0.96, manipulatorBallBgAlpha: 0.88, manipulatorBallBorderAlpha: 0.55, manipulatorBallBlur: 8, manipulatorBallShadowStrength: 1, manipulatorBallHoverScale: 1.02, manipulatorBallHoverLiftPx: 1, mobileGestures: true, previewHoverWindow: false, redirectOptimization: true, setShortcuts: "shift+s", asynchronousResources: true, whitelistDomains: [], whiteSelector: false, maxContentSize: 5, maxStorageItems: 100, dataCleanupInterval: 1, is_loadedStyle: true, loadedUnderlineColor: '#2563EB', loadedHighlightColor: '#FEF08A', loadedBorderColor: '#EF4444', loadedTextColor: '#FF00FF', showPerformanceMonitor: false, clickInterceptMode: 'all', cleanTrackingParams: false, preloadAheadCount: 6, preloadScanIntervalMs: 300, maxPreloadQueueLength: 200, preloadQueueExpireMs: 180000, clickInterceptDomainRules: '{}', enableClickInterceptDomainRules: true, enableClickInterceptDomainRulesV2: true, clickInterceptDomainRulesV2: '{}', disableSensitiveCache: true, //敏感 URL 默认不预加载(只影响预加载/预览/拦截,不影响正常跳转) disableSensitivePreload: true, //安全模式(只做重定向净化,不做预加载/内联预览/点击拦截) safeMode: false, //用户自定义敏感参数 key(每行一个;支持 /regex/flags) sensitiveQueryKeysCustom: '', //用户自定义敏感路径匹配(每行一个;支持 /regex/flags;也可写普通字符串做包含匹配) sensitivePathPatternsCustom: '', sensitiveCacheWhitelistDomains: [], useNativePrefetch: false, useLinkPrefetch: true, useLinkPrerender: false, useHeadPrecheck: true, dedupStripHash: false, maxStorageItemsPerDomain: 40, perDomainInFlightLimit: 2, enablePreloadStats: true, enableAdaptiveConcurrency: true, footerCompactMode: true, operationHintCardShown: false, uiDensity: 'comfortable', logLevel: 'warn', logMaxItems: 300, persistLogs: true, debugConsoleLog: false, memCacheMaxItems: 120, memCacheMaxBytesMb: 30, previewRenderMode: 'inline', sandboxAllowSameOrigin: true, sandboxAllowScripts: false, sandboxAllowForms: false, sandboxAllowPopups: false, }; Object.keys(defaultSettings).forEach((key) => { let currentValue = Storage.get(key); if (currentValue === undefined || currentValue === null) { Storage.set(key, defaultSettings[key]); } }); } function applySettingsMigrations() { try { // 当前配置版本 const cur = Number(Storage.get('schemaVersion', 0) || 0); let v = Number.isFinite(cur) ? cur : 0; if (v < 1) { try { const key = 'sandboxAllowSameOrigin'; const val = Storage.get(key); if (val === undefined || val === null) Storage.set(key, true); } catch {} v = 1; } // 写回最终版本 try { Storage.set('schemaVersion', v); } catch {} } catch {} } function replaceDownloadLink() { if (/lanz/.test(window.location.hostname)) { let downloadLinks = document.querySelectorAll('a'); downloadLinks.forEach(link => { if (link.textContent.includes('立即下载')) { let scripts = document.querySelectorAll('script'); scripts.forEach(script => { let match = script.textContent.match(/var link = '(.*?)';/); if (match) { let newLink = window.location.hostname + '/' + match[1] + "##"; console.log('已提取下载链接:', newLink); link.href = '//' + newLink; } }); } else if (/下载\(\s*[\d\.]+\s*K\s*\)/.test(link.textContent)) { let scripts = document.querySelectorAll('script'); let vkjxld, hyggid; scripts.forEach(script => { let match_vk = script.textContent.match(/var vkjxld = '(.*?)';/); let match_hy = script.textContent.match(/var hyggid = '(.*?)';/); if (match_vk) { vkjxld = match_vk[1]; } if (match_hy) { hyggid = match_hy[1]; } }); if (vkjxld && hyggid) { let newLink = vkjxld + hyggid; console.log('已提取下载链接:', newLink); link.href = newLink; } } }); } } /** * 设置当前激活的面板,并对动画进行处理。 * * @param {string} activePanelId - 要激活的面板ID。 */ function setActivePanel(activePanelId) { if (isAnimating) return; isAnimating = true; updateActiveIndicator(activePanelId); const panelIds = ['panel1', 'panel2', 'panel3', 'panel4']; // 面板ID列表按顺序 const currentActiveIndex = panelIds.indexOf(currentActivePanelId); const targetActiveIndex = panelIds.indexOf(activePanelId); const direction = targetActiveIndex > currentActiveIndex ? -1 : 1; // 目标在右边则向左(-1), 否则向右(1) // 根据目标和当前激活的面板,计算出移动距离 let moveDistance = direction * Math.abs(targetActiveIndex - currentActiveIndex) * 290; // 每个面板间隔250px宽度 // 创建移动函数,面板根据方向和距离移动 const animatePanel = (panelId, distance) => { const panel = document.getElementById(panelId); let currentTranslateX = parseFloat(panel.style.transform.replace('translateX(', '').replace('px)', '')) || 0; panel.style.transform = `translateX(${currentTranslateX + distance}px)`; }; panelIds.forEach(id => { animatePanel(id, moveDistance); }); // 动画结束后,重置面板位置并只显示目标面板 setTimeout(() => { panelIds.forEach(id => { const panel = document.getElementById(id); panel.style.transform = `translateX(${(panelIds.indexOf(id) - targetActiveIndex) * 270}px)`; }); currentActivePanelId = activePanelId; if (typeof updateStatsVisibility === 'function') updateStatsVisibility(); if (activePanelId === 'panel4' && showcaseFeaturesPanel4 && typeof showcaseFeaturesPanel4.__renderLogPanel === 'function') { showcaseFeaturesPanel4.__renderLogPanel(); } isAnimating = false; }, 500); } /** * 在屏幕上显示一段提示信息。 * * @param {string} message - 需要展示的消息。 */ function showAlert(message) { toast(message, 'info'); } /** * 性能分析工具的主要类。 */ var Stats = function() { var currentMode = 0; var container = createElementWithStylesAndAttributes('div', { position: 'fixed', bottom: '10px', right: '10px', left: '10px', cursor: 'pointer', opacity: '0.9', zIndex: '10000' }); container.addEventListener('click', function(event) { event.preventDefault(); showPanel(++currentMode % container.children.length); }, false); // 初始化性能监视计时 var startTime = (performance || Date).now(), prevTime = startTime; var frames = 0; // 创建并添加面板 var fpsPanel = addPanel(new Stats.Panel('FPS', '#0ff', '#002')); var msPanel = addPanel(new Stats.Panel('MS', '#0f0', '#020')); var memPanel; // 判断performance.memory是否可用以监视内存使用 if (self.performance && self.performance.memory) { memPanel = addPanel(new Stats.Panel('MB', '#f08', '#201')); } showPanel(0); function addPanel(panel) { container.appendChild(panel.dom); return panel; } function showPanel(mode) { for (var i = 0; i < container.children.length; i++) { container.children[i].style.display = i === mode ? 'block' : 'none'; } currentMode = mode; } return { dom: container, addPanel: addPanel, showPanel: showPanel, refreshNow: function() { // 立即刷新一次:用于首次显示时不必等到下一个 1s 周期 var now = (performance || Date).now(); startTime = now; prevTime = now - 1001; frames = 0; this.end(); }, begin: function() { startTime = (performance || Date).now(); }, end: function() { frames++; var time = (performance || Date).now(); msPanel.update(time - startTime, 200); if (time > prevTime + 1000) { fpsPanel.update((frames * 1000) / (time - prevTime), 100); prevTime = time; frames = 0; if (memPanel) { var memory = performance.memory; memPanel.update(memory.usedJSHeapSize / 1048576, memory.jsHeapSizeLimit / 1048576); } } return time; }, update: function() { startTime = this.end(); } }; }; /** * Stats中的Panel类,用来展示性能数据。 * * @param {string} name - 面板显示的标题。 * @param {string} fg - 前景色。 * @param {string} bg - 背景色。 */ Stats.Panel = function(name, fg, bg) { var min = Infinity, max = 0; var round = Math.round; var pixelRatio = round(window.devicePixelRatio || 1); var width = 80 * pixelRatio, height = 48 * pixelRatio; var textPadding = 3 * pixelRatio, textHeight = 2 * pixelRatio; var graphX = 3 * pixelRatio, graphY = 15 * pixelRatio; var graphWidth = 74 * pixelRatio, graphHeight = 30 * pixelRatio; var canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; canvas.style.cssText = 'width:80px;height:48px'; var context = canvas.getContext('2d'); context.font = 'bold ' + (9 * pixelRatio) + 'px Helvetica,Arial,sans-serif'; context.textBaseline = 'top'; context.fillStyle = bg; context.fillRect(0, 0, width, height); context.fillStyle = fg; context.fillText(name, textPadding, textHeight); context.fillRect(graphX, graphY, graphWidth, graphHeight); context.fillStyle = bg; context.globalAlpha = 0.9; context.fillRect(graphX, graphY, graphWidth, graphHeight); return { dom: canvas, update: function(value, maxValue) { min = Math.min(min, value); max = Math.max(max, value); context.fillStyle = bg; context.globalAlpha = 1; context.fillRect(0, 0, width, graphY); context.fillStyle = fg; context.fillText(round(value) + ' ' + name + ' (' + round(min) + '-' + round(max) + ')', textPadding, textHeight); context.drawImage(canvas, graphX + pixelRatio, graphY, graphWidth - pixelRatio, graphHeight, graphX, graphY, graphWidth - pixelRatio, graphHeight); context.fillRect(graphX + graphWidth - pixelRatio, graphY, pixelRatio, graphHeight); context.fillStyle = bg; context.globalAlpha = 0.9; context.fillRect(graphX + graphWidth - pixelRatio, graphY, pixelRatio, round((1 - value / maxValue) * graphHeight)); } }; }; /** * 更新当前激活面板的指示器样式。 * * @param {string} activePanelId - 当前激活的面板ID。 */ function updateActiveIndicator(activePanelId) { // 首先重置所有按钮的样式 [settingsParameters, additionalFeatures, shortcutKeys, debugPanelTab] .filter(Boolean) .forEach(button => { button.style.fontSize = "20px"; button.style.textShadow = "none"; }); // 根据当前激活的面板ID,将对应按钮的字体放大并添加立体效果 if (activePanelId === 'panel1') { settingsParameters.style.fontSize = "24px"; settingsParameters.style.textShadow = "1px 1px #888888"; } else if (activePanelId === 'panel2') { additionalFeatures.style.fontSize = "24px"; additionalFeatures.style.textShadow = "1px 1px #888888"; } else if (activePanelId === 'panel3') { shortcutKeys.style.fontSize = "24px"; shortcutKeys.style.textShadow = "1px 1px #888888"; } else if (activePanelId === 'panel4') { if (debugPanelTab) { debugPanelTab.style.fontSize = "24px"; debugPanelTab.style.textShadow = "1px 1px #888888"; } } } /*-------------以下为重定向优化的具体实现函数-------------*/ /** * 重定向优化的主要参数,每个参数代表重定向的查询参数名称(基本涵盖95%的重定向链接)。 */ const queryItems = [ 'url', 'target', 'href', 'tid', 'u', 'goto', 'link', 'remoteUrl', 'to', 'redirect', 'iv', 'safecheck', 'black', 'sinaurl', 'newredirectconfirmcgi', 'view', 'go.shtml', 'link', 'linkout', 'link2', 'go-wild', 'id', 'jump', 'jump.php', 'web', 'security', 'r', 'redirect_link', 'youtube.com/redirect', ]; if (typeof GM_getValue === 'function') { var queryItemsList = Storage.get('queryItemsList', []); var queryItemsLists = [...new Set([...queryItemsList, ...queryItems])]; Storage.set('queryItemsList', queryItemsLists); } /** * 额外的重定向判断条件。 * @param {string} urlObj * @param {HTMLAnchorElement} linkElement * @returns */ function extractTargetUrl(urlObj, linkElement) { if (urlObj.pathname.includes('jump.php')) { extractFromJumpPhp(urlObj, linkElement); return; } for (const item of queryItemsList) { try { const target = urlObj.searchParams.get(item); if (target && isValidHttpUrl(decodeURIComponent(target))) { updateLink(target, linkElement, item); return; } } catch (e) { console.error('URI 解码错误:', e); } } } /** * 单独对jump.php的处理。 * @param {string} urlObj * @param {HTMLAnchorElement} linkElement */ function extractFromJumpPhp(urlObj, linkElement) { const fullUrl = urlObj.href; const targetUrl = fullUrl.substring(fullUrl.indexOf('jump.php?') + 9); updateLink(targetUrl, linkElement); } let preloadedLinks = []; /** * 对链接优化的主要函数 * @param {string} target * @param {HTMLAnchorElement} linkElement * @param {string} item */ function updateLink(target, linkElement, item) { const originalHref = linkElement.href; const linkName = linkElement.textContent || linkElement.innerText; try { const targetUrl = decodeURIComponent(target); if (isValidHttpUrl(targetUrl)) { linkElement.href = targetUrl; console.log('已优化重定向链接:', originalHref, '→', targetUrl); preloadedLinks.push({ redirectParameter: item, name: linkName, url: originalHref, optimizedUrl: targetUrl }); Storage.set('preloadedLinks', preloadedLinks); } } catch (e) { console.error('URI 解码错误:', e); } } function isValidHttpUrl(string) { try { const url = new URL(string); return url.protocol === "http:" || url.protocol === "https:"; } catch (_) { return false; } } function optimizeRedirects() { document.querySelectorAll('a[href]').forEach(link => { if (link.dataset.optimized) return; try { const urlObj = new URL(link.href); extractTargetUrl(urlObj, link); link.dataset.optimized = true; } catch (e) { console.error('错误处理链接:', link.href, '; Error:', e); } }); } /** * 初始化重定向链接 */ function init() { if (Storage.get('redirectOptimization')) { window.addEventListener('load', () => { optimizeRedirects(); replaceDownloadLink(); // 降低轮询频率,减少开销 setInterval(replaceDownloadLink, 500); // 每0.5秒执行一次 }); if (redirectObserver) { redirectObserver.disconnect(); } redirectObserver = new MutationObserver(() => { optimizeRedirects(); replaceDownloadLink(); }); redirectObserver.observe(document.body, { childList: true, subtree: true }); window.addEventListener('beforeunload', () => { if (redirectObserver) redirectObserver.disconnect(); }); } } init(); //安全模式下也保留重定向净化;并提供一键切换入口。 try { GM_registerMenuCommand('切换安全模式(仅重定向净化)', function() { const next = !Storage.get('safeMode', false); Storage.set('safeMode', next); try { emitSettingChanged('safeMode', next); } catch {} try { showAlert(next ? '已开启安全模式:不预加载/不预览/不拦截' : '已关闭安全模式'); } catch {} }); } catch {} /* ----------------------------- 以下为预加载链接的具体代码实现 ---------------------------- */ /** * 为链接添加视觉提醒,指示其已被预加载。 * * @param {HTMLElement} element - 链接元素。 */ function addAVisualLinkReminder(element) { var href = element.href; try { var linkURL = new URL(href); var currentOrigin = window.location.origin; if (linkURL.origin === currentOrigin) { // 仅当“预加载内容可用(ready)”时才显示“加载完成”样式,避免出现“已标记但点进去不是预加载”的错觉。 if (isLinkPreloadedReady(element) && Storage.get('is_loadedStyle')) { const styleName = String(Storage.get('loadedStyle', "下划线") || '下划线'); const underlineColor = String(Storage.get('loadedUnderlineColor', '#2563EB') || '#2563EB'); const highlightColor = String(Storage.get('loadedHighlightColor', '#FEF08A') || '#FEF08A'); const borderColor = String(Storage.get('loadedBorderColor', '#EF4444') || '#EF4444'); const magentaColor = String(Storage.get('loadedTextColor', '#FF00FF') || '#FF00FF'); switch (styleName) { case '下划线': element.style.textDecoration = 'underline'; element.style.textDecorationSkipInk = 'none'; try { element.style.textDecorationColor = underlineColor; } catch {} break; case '高亮': element.style.backgroundColor = highlightColor; break; case '品红': element.style.color = magentaColor; break; case '加粗': element.style.fontWeight = 'bold'; break; case '边框': element.style.border = `2px solid ${borderColor}`; element.style.borderRadius = '4px'; break; default: break; } } } } catch (e) { console.error('Error adding visual link reminder:', e); } } function isLinkPreloadedReady(anchorEl) { try { if (!anchorEl || !anchorEl.dataset) return false; const v = anchorEl.dataset.preloaded; // 兼容历史写法:true/"true" 也视为 ready return v === '1' || v === 'true' || v === true; } catch { return false; } } /** * 添加可拖动的图标到页面上。 */ function applyManipulatorBallStyle() { try { const dragIcon = document.getElementById('draggableIcon'); if (!dragIcon) return; const clampNum = (v, min, max, fallback) => { const n = Number(v); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, n)); }; const size = clampNum(Storage.get('manipulatorBallSize', 52), 24, 160, 52); const radius = clampNum(Storage.get('manipulatorBallRadius', 16), 0, 80, 16); const opacity = clampNum(Storage.get('manipulatorBallOpacity', 0.96), 0.05, 1, 0.96); const bgA = clampNum(Storage.get('manipulatorBallBgAlpha', 0.88), 0, 1, 0.88); const borderA = clampNum(Storage.get('manipulatorBallBorderAlpha', 0.55), 0, 1, 0.55); const blur = clampNum(Storage.get('manipulatorBallBlur', 8), 0, 50, 8); const shadowS = clampNum(Storage.get('manipulatorBallShadowStrength', 1), 0, 3, 1); const hoverScale = clampNum(Storage.get('manipulatorBallHoverScale', 1.02), 1, 1.3, 1.02); const hoverLift = clampNum(Storage.get('manipulatorBallHoverLiftPx', 1), 0, 20, 1); dragIcon.style.setProperty('--il-mb-size', `${Math.round(size)}px`); dragIcon.style.setProperty('--il-mb-radius', `${Math.round(radius)}px`); dragIcon.style.setProperty('--il-mb-opacity', String(opacity)); dragIcon.style.setProperty('--il-mb-bg-alpha', String(bgA)); dragIcon.style.setProperty('--il-mb-border-alpha', String(borderA)); dragIcon.style.setProperty('--il-mb-blur', `${Math.round(blur)}px`); dragIcon.style.setProperty('--il-mb-shadow-strength', String(shadowS)); dragIcon.style.setProperty('--il-mb-hover-scale', String(hoverScale)); dragIcon.style.setProperty('--il-mb-hover-lift', `${Math.round(hoverLift)}px`); // 尺寸变化时尽量保持在屏幕内(轻量兜底) try { const rect = dragIcon.getBoundingClientRect(); const vw = document.documentElement.clientWidth || window.innerWidth; const vh = document.documentElement.clientHeight || window.innerHeight; const padding = 8; let left = parseFloat(dragIcon.style.left); let top = parseFloat(dragIcon.style.top); if (!Number.isFinite(left)) left = rect.left; if (!Number.isFinite(top)) top = rect.top; const w = rect.width || size; const h = rect.height || size; left = Math.min(Math.max(left, padding), Math.max(padding, vw - w - padding)); top = Math.min(Math.max(top, padding), Math.max(padding, vh - h - padding)); dragIcon.style.left = `${left}px`; dragIcon.style.top = `${top}px`; dragIcon.style.right = ''; dragIcon.style.bottom = ''; dragIcon.style.transform = ''; } catch {} } catch {} } function addDraggableIcon() { // 美化:玻璃拟态 + 渐变描边 + 阴影,支持 hover/active 状态 try { addStyle('instantLoad-draggable-icon-style', ` #draggableIcon { width: var(--il-mb-size, 52px); height: var(--il-mb-size, 52px); border-radius: var(--il-mb-radius, 16px); color: #111827; background: rgba(224, 229, 236, var(--il-mb-bg-alpha, 0.88)); box-shadow: calc(10px * var(--il-mb-shadow-strength, 1)) calc(10px * var(--il-mb-shadow-strength, 1)) calc(22px * var(--il-mb-shadow-strength, 1)) rgba(163, 177, 198, 0.55), calc(-10px * var(--il-mb-shadow-strength, 1)) calc(-10px * var(--il-mb-shadow-strength, 1)) calc(22px * var(--il-mb-shadow-strength, 1)) rgba(255, 255, 255, 0.85); backdrop-filter: blur(var(--il-mb-blur, 8px)); -webkit-backdrop-filter: blur(var(--il-mb-blur, 8px)); border: 1px solid rgba(255,255,255,var(--il-mb-border-alpha, 0.55)); transition: transform .18s ease, box-shadow .18s ease, opacity .18s ease; opacity: var(--il-mb-opacity, .96); user-select: none; touch-action: none; } #draggableIcon:hover { transform: translateY(calc(-1 * var(--il-mb-hover-lift, 1px))) scale(var(--il-mb-hover-scale, 1.02)); box-shadow: calc(12px * var(--il-mb-shadow-strength, 1)) calc(12px * var(--il-mb-shadow-strength, 1)) calc(26px * var(--il-mb-shadow-strength, 1)) rgba(163, 177, 198, 0.55), calc(-12px * var(--il-mb-shadow-strength, 1)) calc(-12px * var(--il-mb-shadow-strength, 1)) calc(26px * var(--il-mb-shadow-strength, 1)) rgba(255, 255, 255, 0.9); } #draggableIcon.il-dragging { opacity: .88; transform: scale(0.98); } #draggableIcon.il-hidden { display: none !important; } `); } catch {} var svgHTML = '', div = document.createElement('div'); div.innerHTML = svgHTML; document.body.appendChild(div.firstChild); function clampAndSnapIconPosition() { var rect = dragIcon.getBoundingClientRect(); var iconWidth = rect.width || 50; var iconHeight = rect.height || 50; var vw = document.documentElement.clientWidth || window.innerWidth; var vh = document.documentElement.clientHeight || window.innerHeight; var padding = 8; var left = parseFloat(dragIcon.style.left); var top = parseFloat(dragIcon.style.top); if (Number.isNaN(left)) left = rect.left; if (Number.isNaN(top)) top = rect.top; left = Math.min(Math.max(left, padding), Math.max(padding, vw - iconWidth - padding)); top = Math.min(Math.max(top, padding), Math.max(padding, vh - iconHeight - padding)); // 贴边:根据当前位置自动吸附左右边缘 var centerX = left + iconWidth / 2; left = (centerX < vw / 2) ? padding : Math.max(padding, vw - iconWidth - padding); dragIcon.style.left = left + 'px'; dragIcon.style.top = top + 'px'; dragIcon.style.right = ''; dragIcon.style.bottom = ''; dragIcon.style.transform = ''; Storage.set('iconPosition', { left: dragIcon.style.left, top: dragIcon.style.top }); } function onDrag(e, move) { var startX = ('touches' in e ? e.touches[0] : e).clientX - dragIcon.getBoundingClientRect().left, startY = ('touches' in e ? e.touches[0] : e).clientY - dragIcon.getBoundingClientRect().top; var pointerStartX = ('touches' in e ? e.touches[0] : e).clientX; var pointerStartY = ('touches' in e ? e.touches[0] : e).clientY; var moved = false; var movedThreshold = 6; // 开始拖拽后统一使用 left/top 定位,避免 right/translate 影响计算 dragIcon.style.right = ''; dragIcon.style.bottom = ''; dragIcon.style.transform = ''; function dragging(ev) { if (!move && ev.cancelable) ev.preventDefault(); var clientX = ('touches' in ev ? ev.touches[0] : ev).clientX, clientY = ('touches' in ev ? ev.touches[0] : ev).clientY; if (!moved && (Math.abs(clientX - pointerStartX) > movedThreshold || Math.abs(clientY - pointerStartY) > movedThreshold)) { moved = true; } dragIcon.style.left = clientX - startX + 'px'; dragIcon.style.top = clientY - startY + 'px'; } // 视觉状态:拖拽中 try { dragIcon.classList.add('il-dragging'); } catch {} function endDrag() { document.removeEventListener(move ? 'mousemove' : 'touchmove', dragging); document.removeEventListener(move ? 'mouseup' : 'touchend', endDrag); document.body.style.overflow = ''; dragIcon.style.transition = ''; clampAndSnapIconPosition(); try { dragIcon.classList.remove('il-dragging'); } catch {} // 拖拽发生时,抑制随后的 click(避免触发“返回上一页”) if (moved) { dragIcon.dataset.suppressNextClick = '1'; setTimeout(function() { if (dragIcon) { dragIcon.dataset.suppressNextClick = ''; } }, 400); } } document.addEventListener(move ? 'mousemove' : 'touchmove', dragging, move ? false : { passive: false }); document.addEventListener(move ? 'mouseup' : 'touchend', endDrag); document.body.style.overflow = 'hidden'; dragIcon.style.transition = 'none'; if (!move) { dragIcon.dataset.pressTimer = setTimeout(endDrag, 500); } } var dragIcon = document.getElementById('draggableIcon'); // 初始显示:由开关控制(下面会绑定实时响应) dragIcon.classList.add('il-hidden'); // 应用用户自定义样式 try { applyManipulatorBallStyle(); } catch {} var savedPosition = Storage.get('iconPosition'); if (savedPosition) { dragIcon.style.left = savedPosition.left; dragIcon.style.top = savedPosition.top; dragIcon.style.right = ''; dragIcon.style.bottom = ''; dragIcon.style.transform = ''; // 兜底:历史版本可能保存了出屏位置,这里强制贴边并限制在视口内 setTimeout(clampAndSnapIconPosition, 0); } dragIcon.ontouchstart = function(e) { onDrag(e, false); }; dragIcon.ontouchend = function() { clearTimeout(dragIcon.dataset.pressTimer); }; dragIcon.onmousedown = function(e) { e.preventDefault(); onDrag(e, true); }; // 单击才触发“返回上一页”;如果刚刚发生过拖拽则不触发 dragIcon.onclick = function(e) { if (dragIcon.dataset.suppressNextClick === '1') { e.preventDefault(); e.stopPropagation(); dragIcon.dataset.suppressNextClick = ''; return; } handleBackNavigation(); }; // 显示策略:只在“预加载预览页(fullPageDiv)出现时”显示;且受开关 manipulatorBall 控制。 // 进入/退出预览由 displayPreloadedContent -> toggleDragIconVisibility(true/false) 统一控制。 // 这里仅补齐:当用户在预览页内切换 manipulatorBall 时,操作球能实时响应。 try { toggleDragIconVisibility(!!document.getElementById('fullPageDiv')); } catch {} try { onSettingChanged((k) => { if (String(k || '') === 'manipulatorBall') { toggleDragIconVisibility(!!document.getElementById('fullPageDiv')); } }); } catch {} // 样式设置变更:实时刷新 try { const styleKeys = new Set([ 'manipulatorBallStyleExpanded', 'manipulatorBallSize', 'manipulatorBallRadius', 'manipulatorBallOpacity', 'manipulatorBallBgAlpha', 'manipulatorBallBorderAlpha', 'manipulatorBallBlur', 'manipulatorBallShadowStrength', 'manipulatorBallHoverScale', 'manipulatorBallHoverLiftPx', ]); onSettingChanged((k) => { const kk = String(k || ''); if (!styleKeys.has(kk)) return; try { applyManipulatorBallStyle(); } catch {} try { toggleDragIconVisibility(!!document.getElementById('fullPageDiv')); } catch {} }); } catch {} } /** * 将一个链接添加到预加载队列中。 * * @param {string} url - 链接的地址。 * @param {HTMLElement} element - 对应的链接元素。 */ function addToPreloadQueue(url, element) { // 增加了对集合中存在性的检查 const key = getUrlKeyForCache(url); if (isInViewport(element) && !preloadSet.has(key)) { preloadSet.add(key); // 队列容量保护:避免无限滚动/巨型页面导致队列膨胀 try { const maxLen = Number(Storage.get('maxPreloadQueueLength', 200)); const cap = (Number.isFinite(maxLen) && maxLen >= 20) ? Math.min(maxLen, 2000) : 200; const expireMsRaw = Number(Storage.get('preloadQueueExpireMs', 180000)); const expireMs = (Number.isFinite(expireMsRaw) && expireMsRaw >= 10000) ? Math.min(expireMsRaw, 3600000) : 180000; const now = Date.now(); // 清理过期项 preloadQueue = preloadQueue.filter((it) => { try { if (!it) return false; const ts = Number(it.t || 0); if (ts && now - ts > expireMs) { preloadSet.delete(String(it.url || '')); return false; } return true; } catch { return false; } }); // 超过容量:丢弃最旧的 while (preloadQueue.length >= cap) { const drop = preloadQueue.shift(); try { preloadSet.delete(drop && drop.url ? String(drop.url) : ''); } catch {} } } catch {} preloadQueue.push({ url: key, element: element, t: Date.now() }); } } // 运行期 LRU:仅影响本次运行的调度/优先级 const runtimeLru = new Map(); function touchRuntimeLru(urlKey) { try { runtimeLru.delete(urlKey); runtimeLru.set(urlKey, Date.now()); while (runtimeLru.size > 500) { const firstKey = runtimeLru.keys().next().value; runtimeLru.delete(firstKey); } } catch {} } // 每域名并发计数:用于限流,避免单站点占满 const runtimeDomainInFlight = new Map(); function getDomainKey(urlObj) { try { return (urlObj && urlObj.hostname) ? urlObj.hostname : ''; } catch { return ''; } } function canScheduleForDomain(domainKey) { if (!domainKey) return true; const limit = Number(Storage.get('perDomainInFlightLimit', 2)); if (!Number.isFinite(limit) || limit <= 0) return false; const current = Number(runtimeDomainInFlight.get(domainKey) || 0); return current < limit; } function incDomainInFlight(domainKey) { if (!domainKey) return; runtimeDomainInFlight.set(domainKey, Number(runtimeDomainInFlight.get(domainKey) || 0) + 1); } function decDomainInFlight(domainKey) { if (!domainKey) return; const next = Math.max(0, Number(runtimeDomainInFlight.get(domainKey) || 0) - 1); if (next === 0) runtimeDomainInFlight.delete(domainKey); else runtimeDomainInFlight.set(domainKey, next); } /** * 将二进制大对象(blob)转换为Base64编码的字符串。 * * @param {Blob} blob - 需要转换的blob对象。 * @returns {Promise} - 包含Base64编码字符串的Promise。 */ function blobToBase64(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onloadend = () => resolve(reader.result); reader.onerror = e => reject(e); reader.readAsDataURL(blob); }); } /** * 取消一个指定链接的预加载。 * * @param {string} url - 需要取消预加载的链接URL。 */ function cancelPreload(url) { var key = getUrlKeyForCache(url); if (abortControllers[key]) { abortControllers[key].abort(); delete abortControllers[key]; console.log('已取消预加载(链接已离开视口):', url); currentPreloads = Math.max(0, currentPreloads - 1); // 更新计数(防止负数) preloadNext(); // 尝试开始下一个预加载 } // 从预加载队列中移除链接 preloadQueue = preloadQueue.filter(item => item.url !== key); preloadSet.delete(key); // 从预加载集合中移除链接 } /** * 检查预加载的锚点链接并添加相应的视觉提醒。 */ function checkAndAddBulletsForPreloadedLinks() { var links = document.querySelectorAll('a'); links.forEach(function(link) { // 站点可能在滚动/懒加载时重建 DOM,导致已预加载链接的 dataset/style 丢失。 // 这里统一做一次“标记+样式”同步,避免看起来像“网页被重新加载”。 syncPreloadedMarkerAndStyleForAnchor(link); }); } /** * 清理预加载队列,移出不符合条件的链接。 */ function cleanPreloadQueue() { // 清理已经预加载或者不在视口内的链接 preloadQueue = preloadQueue.filter(item => { try { const key = item && item.url ? String(item.url) : ''; if (!key) return false; const el = item.element; if (!el || (el.isConnected === false)) { preloadSet.delete(key); return false; } // 若元素不在视口:移出队列(isInViewport 内部会处理“已预加载但离开视口”的取消逻辑) if (!isInViewport(el)) { preloadSet.delete(key); return false; } // 已命中预加载则无需再排队 if (el.dataset && el.dataset.preloaded) { preloadSet.delete(key); return false; } return true; } catch { try { preloadSet.delete(item && item.url ? String(item.url) : ''); } catch {} return false; } }); } /** * 清理已经超时或已经中止的AbortController实例。 */ function cleanAbortControllers() { // 获取当前时间 var now = Date.now(); // 遍历abortControllers对象的属性 for (var url in abortControllers) { // 如果请求已经很久没有响应,那么我们认为它可能已经失效,需要删除控制器 // 或如果请求已经被中止,亦应删除 var controller = abortControllers[url]; if ((controller.timestamp && now - controller.timestamp > 30000) || controller.signal.aborted) { // 30秒或已中止 delete abortControllers[url]; } } } /** * 防抖函数,用于延迟执行并防止函数在短时间内多次触发。 * * @param {Function} func - 需要防抖的函数。 * @param {number} wait - 延迟执行的时间,单位为毫秒。 * @param {boolean} immediate - 是否立即执行。 * @returns 防抖处理后的函数。 */ function debounce(func, wait, immediate) { var timeout, called = false; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate && !called) func.apply(context, args); called = false; // 重置调用标志 }; var callNow = immediate && !timeout; if (callNow) { func.apply(context, args); called = true; // 立即执行时,设置调用标志 } clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // 调度器:优先 idle,其次 setTimeout function scheduleWork(fn, timeoutMs) { try { if (typeof requestIdleCallback === 'function') { return requestIdleCallback(function() { try { fn(); } catch (e) { console.error(e); } }, { timeout: timeoutMs || 500 }); } } catch {} return setTimeout(function() { try { fn(); } catch (e) { console.error(e); } }, Math.min(50, timeoutMs || 0)); } function cancelScheduledWork(handle) { try { if (typeof cancelIdleCallback === 'function') { cancelIdleCallback(handle); return; } } catch {} clearTimeout(handle); } /** * 从IndexedDB数据库中删除旧内容。 */ function deleteOldContentFromDB() { const now = Date.now(); const transaction = db.transaction([dbStoreName], 'readwrite'); const store = transaction.objectStore(dbStoreName); var contentRequest = store.getAll(); // 每域名存储上限(粗略) const perDomainMax = Number(Storage.get('maxStorageItemsPerDomain', 40)); contentRequest.onsuccess = function() { var contents = contentRequest.result; // 优先使用 lru(最近访问时间)作为淘汰依据;旧记录没有 lru 时回退 timestamp contents.sort((a, b) => { const at = Number((a && (a.lru || a.timestamp)) || 0); const bt = Number((b && (b.lru || b.timestamp)) || 0); return bt - at; }); // 先按域名统计数量(按新到旧) var domainCounts = new Map(); // 判断记录是否超出最大存储量或者是否过期,然后执行删除 contents.forEach((content, index) => { const baseTime = Number((content && (content.lru || content.timestamp)) || 0); var shouldDelete = (baseTime && (now - baseTime > dataCleanupInterval)) || (index >= maxStorageItems); if (!shouldDelete && Number.isFinite(perDomainMax) && perDomainMax > 0) { try { var host = new URL(content.url, window.location.href).hostname; var cnt = Number(domainCounts.get(host) || 0) + 1; domainCounts.set(host, cnt); if (cnt > perDomainMax) { shouldDelete = true; } } catch {} } if (shouldDelete) { store.delete(content.url); } }); }; contentRequest.onerror = function(event) { console.error("Error fetching contents from IndexedDB", event.target.error); }; } /** * 显示预加载的内容。 * * @param {string} base64Content - 预加载内容的Base64编码。 * @param {string} url - 内容对应的链接地址。 */ const LAZY_ATTR_MAP = Object.freeze({ src: [ 'data-actualsrc', 'data-original', 'data-src', 'data-lazy', 'data-lazy-src', 'data-url', 'data-img', 'data-image', 'data-bg', ], srcset: [ 'data-srcset', 'data-original-srcset', 'data-lazy-srcset', ], poster: [ 'data-poster', 'data-original-poster', 'data-video-poster', ], href: [ 'data-href', 'data-src', 'data-url', ], }); function getLazyCandidate(el, attrName) { try { const keys = LAZY_ATTR_MAP[attrName]; if (!keys || !keys.length) return ''; for (const k of keys) { const v = el.getAttribute(k); if (v) return String(v); } } catch {} return ''; } function shouldApplyLazyAttr(currentValue) { try { const cur = String(currentValue || ''); if (!cur) return true; if (cur.startsWith('data:image/svg+xml')) return true; if (cur.startsWith('data:image/gif')) return true; // 常见 1x1 占位 if (cur.startsWith('data:image/png')) return false; if (/^(about:blank|javascript:|#)$/i.test(cur)) return true; } catch {} return false; } function hydrateLazyImages(rootEl) { try { if (!rootEl || typeof rootEl.querySelectorAll !== 'function') return; // img / source[srcset] const imgs = rootEl.querySelectorAll('img'); imgs.forEach((img) => { try { const curSrc = img.getAttribute('src'); const candidateSrc = getLazyCandidate(img, 'src'); if (candidateSrc && shouldApplyLazyAttr(curSrc)) { img.setAttribute('src', candidateSrc); try { img.src = candidateSrc; } catch {} } const curSrcset = img.getAttribute('srcset'); const candidateSrcset = getLazyCandidate(img, 'srcset'); if (candidateSrcset && (!curSrcset || shouldApplyLazyAttr(curSrcset))) { img.setAttribute('srcset', candidateSrcset); } if (img.classList && img.classList.contains('lazy')) img.classList.remove('lazy'); if (img.loading) img.loading = 'eager'; } catch {} }); const sources = rootEl.querySelectorAll('source'); sources.forEach((srcEl) => { try { const cur = srcEl.getAttribute('srcset'); const candidate = getLazyCandidate(srcEl, 'srcset'); if (candidate && (!cur || shouldApplyLazyAttr(cur))) { srcEl.setAttribute('srcset', candidate); } } catch {} }); // video[poster] const videos = rootEl.querySelectorAll('video'); videos.forEach((v) => { try { const curPoster = v.getAttribute('poster'); const candidatePoster = getLazyCandidate(v, 'poster'); if (candidatePoster && shouldApplyLazyAttr(curPoster)) { v.setAttribute('poster', candidatePoster); } } catch {} }); // link[rel=preload] / link[rel=stylesheet]:回填 href const links = rootEl.querySelectorAll('link'); links.forEach((ln) => { try { const rel = String(ln.getAttribute('rel') || '').toLowerCase(); if (rel !== 'preload' && rel !== 'stylesheet') return; const curHref = ln.getAttribute('href'); const candidateHref = getLazyCandidate(ln, 'href'); if (candidateHref && shouldApplyLazyAttr(curHref)) { ln.setAttribute('href', candidateHref); } } catch {} }); } catch {} } function displayPreloadedContent(base64Content, url) { // 安全模式:不做内联预览,直接跳转 try { if (isSafeModeEnabled()) { window.location.href = String(url || ''); return; } } catch {} // 内联预览仅允许同源:减少跨域注入/行为异常风险 try { const urlObj = new URL(url, window.location.href); if (!isSameOriginUrl(urlObj)) { window.location.href = urlObj.href; return; } // 高风险站点默认不做内联预览 if (shouldDisablePreloadForUrl(urlObj)) { window.location.href = urlObj.href; return; } // 敏感 URL 默认不做内联预览 if (shouldSkipPreloadForSensitive(urlObj)) { window.location.href = urlObj.href; return; } } catch { window.location.href = url; return; } if (base64Content.startsWith('data:')) { // 仅允许展示 HTML(避免误展示下载/图片等 data: 内容) const mime = base64Content.slice(5, base64Content.indexOf(';')); if (mime && !/^text\/html$/i.test(mime)) { window.location.href = url; return; } var binary = atob(base64Content.split(',')[1]); var array = new Uint8Array(binary.length); for (var i = 0; i < binary.length; i++) { array[i] = binary.charCodeAt(i); } var documentEncoding = document.characterSet || 'UTF-8'; var blobContent = new Blob([array], { type: `text/html;charset=${documentEncoding}` }); var reader = new FileReader(); reader.onload = function() { var existingFullPageDiv = document.getElementById('fullPageDiv'); if (existingFullPageDiv) { existingFullPageDiv.parentNode.removeChild(existingFullPageDiv); existingFullPageDiv.remove(); } // 同步 iframe(异步资源模式)引用:确保退出/回退时能清理,避免页面底部出现“空白占位”。 let syncIframe = null; var fullPageDiv = document.createElement('div'); fullPageDiv.id = 'fullPageDiv'; fullPageDiv.style.cssText = ` position: fixed; top: 0; left: 0; width: 100%; height: 100%; overflow: auto; z-index: 1000; background: white; `; // 预览容器:可选隔离渲染(inline / shadow / iframe-sandbox) const previewRenderMode = String(Storage.get('previewRenderMode', 'inline') || 'inline'); // 预览渲染:使用 DOMParser 解析后再注入 const htmlText = String(reader.result || ''); let parsedDoc = null; try { // inline / shadow:先做清理(移除脚本/危险标签/事件),减少冲突 if (previewRenderMode === 'shadow' || previewRenderMode === 'inline') { parsedDoc = sanitizeHtmlForInlinePreview(htmlText, url, { removeScripts: true, removeCspMeta: true, addBase: true }); } else { // iframe-sandbox: // - 默认仍移除 CSP meta,避免 srcdoc 内自带 CSP 造成“无样式/字体全丢” // - 若用户允许脚本,可保留 script(仅用于少数必须脚本才能渲染的站点) const keepScripts = !!Storage.get('sandboxAllowScripts', false); parsedDoc = sanitizeHtmlForInlinePreview(htmlText, url, { removeScripts: !keepScripts, removeCspMeta: true, addBase: true }); if (!parsedDoc) parsedDoc = new DOMParser().parseFromString(htmlText, 'text/html'); } } catch {} toggleDragIconVisibility(true); document.body.style.overflow = 'hidden'; document.body.appendChild(fullPageDiv); // 解析失败/空 body:回退正常跳转 try { const bodyOk = !!(parsedDoc && parsedDoc.body && parsedDoc.body.childNodes && parsedDoc.body.childNodes.length > 0); if (!parsedDoc || !parsedDoc.documentElement || !bodyOk) { try { Log.warn('预览渲染异常,回退跳转', { u: url }); } catch {} try { window.location.href = url; } catch { window.location.href = String(url); } return; } } catch { try { window.location.href = url; } catch { window.location.href = String(url); } return; } // 根据模式注入: // - inline:注入完整文档(已清理) // - shadow:把 body 内容放进 shadowRoot,隔离样式 // - iframe-sandbox:用 sandbox iframe 展示(最隔离,可能因 CSP 影响资源加载) let sandboxFrame = null; try { if (previewRenderMode === 'iframe-sandbox') { sandboxFrame = document.createElement('iframe'); sandboxFrame.style.cssText = 'position:absolute; inset:0; width:100%; height:100%; border:0; background:#fff;'; // 说明: // - 纯预览(所有 sandbox 权限都关闭):使用 srcdoc 快速展示缓存 HTML // - 开启任意 sandbox 权限(脚本/表单/弹窗):切换为“实时 iframe”模式(iframe.src=url) // 因为 srcdoc 里开启这些权限很容易出现样式/字体缺失、且权限也难以生效。 const allowScripts = !!Storage.get('sandboxAllowScripts', false); const allowForms = !!Storage.get('sandboxAllowForms', false); const allowPopups = !!Storage.get('sandboxAllowPopups', false); const anyPermEnabled = allowScripts || allowForms || allowPopups; // sandbox tokens:默认更安全(不含 allow-same-origin)。 // 若用户确实需要资源/样式更完整,可显式开启 sandboxAllowSameOrigin。 const sandboxTokens = []; if (!!Storage.get('sandboxAllowSameOrigin', true)) sandboxTokens.push('allow-same-origin'); if (allowScripts) sandboxTokens.push('allow-scripts'); if (allowForms) sandboxTokens.push('allow-forms'); if (allowPopups) sandboxTokens.push('allow-popups'); sandboxFrame.setAttribute('sandbox', sandboxTokens.join(' ')); sandboxFrame.setAttribute('referrerpolicy', 'no-referrer'); if (anyPermEnabled) { // 实时模式:让浏览器按正常路径加载资源/样式/字体/登录态 sandboxFrame.src = url; } else { // 纯预览:srcdoc 展示缓存内容 sandboxFrame.srcdoc = (parsedDoc && parsedDoc.documentElement) ? String(parsedDoc.documentElement.outerHTML) : htmlText; } fullPageDiv.innerHTML = ''; fullPageDiv.appendChild(sandboxFrame); } else if (previewRenderMode === 'shadow') { fullPageDiv.innerHTML = ''; const shell = parsedDoc ? buildShadowPreviewShell(parsedDoc) : null; if (shell) { fullPageDiv.appendChild(shell); } else { fullPageDiv.innerHTML = (parsedDoc && parsedDoc.documentElement) ? String(parsedDoc.documentElement.outerHTML) : htmlText; } } else { fullPageDiv.innerHTML = (parsedDoc && parsedDoc.documentElement) ? String(parsedDoc.documentElement.outerHTML) : htmlText; } } catch { fullPageDiv.innerHTML = htmlText; } // 修复“懒加载占位图”导致的空白:在预览容器内把 data-actualsrc/data-original 等回填到 src // sandbox iframe 模式下无法跨域操作其内部 DOM,这里仅对容器自身做处理 try { if (previewRenderMode !== 'iframe-sandbox') { hydrateLazyImages(fullPageDiv); } } catch {} // 预览内“回退/前进”应尽量保持:阅读位置 + 已加载链接样式 try { __currentPreviewUrlKey = getUrlKeyForCache(url); } catch { try { __currentPreviewUrlKey = String(url || ''); } catch { __currentPreviewUrlKey = ''; } } try { restorePreviewScrollState(__currentPreviewUrlKey); } catch {} try { applyLoadedStyleHintsInContainer(fullPageDiv); } catch {} // 监听滚动:节流保存阅读位置 try { const lastKey = __currentPreviewUrlKey; let tmr = 0; fullPageDiv.addEventListener('scroll', function() { try { if (!lastKey) return; if (tmr) return; tmr = setTimeout(function() { tmr = 0; capturePreviewScrollState(lastKey); }, 180); } catch {} }, { passive: true }); } catch {} // 预览页仅保留 Esc 退出 const existingExitBtn = document.getElementById('preloadedExitBtn'); if (existingExitBtn) existingExitBtn.remove(); let escHandler = null; function exitPreview() { // 退出前记住当前阅读位置(仅对预览覆盖层有效) try { if (__currentPreviewUrlKey) capturePreviewScrollState(__currentPreviewUrlKey); } catch {} const fp = document.getElementById('fullPageDiv'); if (fp) fp.remove(); if (syncIframe && syncIframe.parentNode) syncIframe.parentNode.removeChild(syncIframe); if (sandboxFrame && sandboxFrame.parentNode) sandboxFrame.parentNode.removeChild(sandboxFrame); toggleDragIconVisibility(false); document.body.style.overflow = ''; // 重要:Esc 退出预览时也要清理“预览内导航栈”。 // 否则:用户曾预览过 A(Esc 退出)后再预览 B,下一次“返回上一页”会错误回到 A。 try { clickedLinks = []; currentPreviewIndex = -1; __currentPreviewUrlKey = ''; } catch {} if (escHandler) { document.removeEventListener('keydown', escHandler, true); escHandler = null; } try { fullPageDiv.onscroll = null; } catch {} try { if (iframe && iframe.contentWindow) iframe.contentWindow.onscroll = null; } catch {} } escHandler = function(ev) { if (ev.key === 'Escape') { exitPreview(); } }; document.addEventListener('keydown', escHandler, true); if (Storage.get('asynchronousResources')) { var iframe = document.createElement('iframe'); // 关键:必须脱离文档流,否则会在页面底部“顶出空白”。 iframe.style.cssText = 'position: fixed; top: 0; left: 0; width: 100%; height: 100%; display: block; visibility: hidden; border: 0; pointer-events: none; z-index: -1;'; iframe.src = url; syncIframe = iframe; fullPageDiv.appendChild(iframe); fullPageDiv.onscroll = function() { if (iframe.contentWindow) { iframe.contentWindow.scrollTo(0, fullPageDiv.scrollY); iframe.contentWindow.scrollTo(0, fullPageDiv.scrollTop); } }; iframe.onload = function() { // iframe 同步模式兜底 // - 跨域时访问 contentDocument/contentWindow.document 会抛异常 // - 失败时自动关闭异步同步模式并移除 iframe try { if (!iframe.contentWindow) return; // 重要:不要在滚动时把 fullPageDiv.innerHTML 整页重写。 // 这会导致“预览页看起来被刷新/重新加载”,并丢失已应用的样式与阅读位置。 // 同步滚动已经由 fullPageDiv.onscroll -> iframe.contentWindow.scrollTo 完成。 // 这里仅做一次性同步(可选):在 iframe 首次加载后,尝试把最终 DOM 拷贝到预览容器。 try { var iframeDocument = iframe.contentDocument || iframe.contentWindow.document; if (iframeDocument && iframeDocument.documentElement) { fullPageDiv.innerHTML = iframeDocument.documentElement.outerHTML; try { hydrateLazyImages(fullPageDiv); } catch {} try { applyLoadedStyleHintsInContainer(fullPageDiv); } catch {} } } catch (e) { console.warn('Iframe sync failed (cross-origin or blocked), fallback:', e); Storage.set('asynchronousResources', false); if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe); if (syncIframe === iframe) syncIframe = null; try { Log.warn('预览同步失败,已降级为非同步模式', { u: url, m: String(e && e.message ? e.message : e) }); } catch {} } } catch (e) { console.warn('Iframe sync init failed (cross-origin or blocked), fallback:', e); Storage.set('asynchronousResources', false); if (iframe && iframe.parentNode) iframe.parentNode.removeChild(iframe); if (syncIframe === iframe) syncIframe = null; try { Log.warn('预览同步初始化失败,已降级为非同步模式', { u: url, m: String(e && e.message ? e.message : e) }); } catch {} } } } }; reader.readAsText(blobContent, documentEncoding); } } /** * 初始化IndexedDB数据库。 * * @param {Function} success - 数据库就绪后执行的回调函数。 */ function initDB(success) { if (!dbReady) { openDB(success); } else if (typeof success === 'function') { success(); } } /** * 判断一个元素是否在视口内。 * * @param {HTMLElement} element - 需要判断的HTML元素。 * @returns {boolean} - 元素是否在视口内。 */ function isInViewport(element) { if (!element || typeof element.getBoundingClientRect !== 'function') return false; var rect = element.getBoundingClientRect(); var inViewport = ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth) ); if (!inViewport && element.dataset.preloaded === 'pending') { cancelPreload(element.href); // 如果链接不在视窗中并且已被标记为预加载,取消它的预加载 element.dataset.preloaded = ''; // 移除进行中标记 } return inViewport; } /** * 判断当前设备是否为移动设备。 * * @returns {boolean} - 是否为移动设备。 */ function isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); } /** * 使用MutationObserver监听DOM变化。 */ function observeDOMChanges() { var handledLinks = new Set(); // 用于储存处理过的链接 var config = { childList: true, subtree: true }; var callback = function(mutationsList, observer) { requestAnimationFrame(function() { mutationsList.forEach(debounce(function(mutation) { if (mutation.type == 'childList' && mutation.addedNodes.length) { mutation.addedNodes.forEach(function(node) { if (node.nodeType === 1 && node.matches('a[href]')) { var href = node.getAttribute('href'); var key = getUrlKeyForCache(href); try { // 安全模式:不做任何自动预加载 if (!isSafeModeEnabled()) { // 统一过滤:跳过不可处理协议/同页 hash/高风险等 // 约定:PC 端以“鼠标悬停预加载”为主;移动端才启用“视口扫描/自动预加载”。 if (isMobileDevice()) { if (!isSkippableUrl(href) && !preloadSet.has(key) && !handledLinks.has(key)) { preloadLink(href, node); handledLinks.add(key); // 将链接添加到 handledLinks 中进行标记 preloadSet.add(key); // 将链接添加到 preloadSet 中 } } } } catch {} try { trackPreloadCandidateFromAnchor(node); } catch {} try { observeAnchorForPreload(node); } catch {} // 站点可能在滚动/懒加载时“重建 a 标签”,导致 dataset/style 丢失。 // 对新增的链接立即做一次轻量同步,避免样式又“变回去”。 try { syncPreloadedMarkerAndStyleForAnchor(node); } catch {} } else if (node.nodeType === 1) { // 节点可能是一个容器:只对其子树中的 a[href] 做少量同步 try { if (node.querySelectorAll) { const as = node.querySelectorAll('a[href]'); const max = 40; for (let i = 0; i < as.length && i < max; i++) { try { syncPreloadedMarkerAndStyleForAnchor(as[i]); } catch {} } } } catch {} } }); } }, 250)); // 仅移动端:DOM 变化后触发一次轻量扫描(安全模式下跳过) try { if (!isSafeModeEnabled() && isMobileDevice()) { try { if (preloadScanHandle) cancelScheduledWork(preloadScanHandle); preloadScanHandle = scheduleWork(function() { preloadScanHandle = null; preloadVisibleLinks(); }, 600); } catch { preloadVisibleLinks(); } } } catch {} }); }; if (preloadDomObserver) { preloadDomObserver.disconnect(); } preloadDomObserver = new MutationObserver(callback); preloadDomObserver.observe(document.body, config); } /** * 打开IndexedDB数据库。 * * @param {Function} callback - 数据库打开成功的回调函数。 */ function openDB(callback) { if (dbInitializationPromise) { // 如果存在初始化的Promise,直接返回它并添加回调 dbInitializationPromise.then(callback).catch(err => console.error('IndexedDB init error:', err)); return; } // 新建一个Promise来处理初始化过程 dbInitializationPromise = new Promise((resolve, reject) => { var request = indexedDB.open(dbName, dbVersion); request.onupgradeneeded = function(event) { var db = event.target.result; if (!db.objectStoreNames.contains(dbStoreName)) { db.createObjectStore(dbStoreName, { keyPath: 'url' }); } }; request.onsuccess = function(event) { db = event.target.result; dbReady = true; cacheDegraded = false; console.log('IndexedDB 数据库已打开'); db.onerror = function(event) { console.error("Database error: " + event.target.error.message); }; try { updateCacheStatusBadge(); } catch {} try { updateCacheMeta(); } catch {} resolve(db); }; request.onerror = function(event) { console.error('IndexedDB database open error:', event.target.errorCode); dbInitializationPromise = null; // 如果Promise失败,我们重置这个变量 dbReady = false; cacheDegraded = true; try { updateCacheStatusBadge(); } catch {} reject(event.target.error); }; }); // 调用传入的回调函数 dbInitializationPromise.then(callback).catch(err => console.error('IndexedDB init error:', err)); } /** * 将一个链接的属性值视为适当。 * * @param {string} url - 链接的URL地址。 * @returns {string} - 适当的属性值。 */ function appropriateAsAttributeValue(url) { if (url.endsWith('.css')) { return 'style'; } else if (url.endsWith('.js')) { return 'script'; } else if (url.match(/(.jpg|.jpeg|.png|.gif)$/)) { return 'image'; } else if (url.endsWith('.json')) { return 'fetch'; } else { return 'fetch'; } } function maybeHintPrefetchOrPrerender(urlObj) { try { if (!Storage.get('useNativePrefetch', false)) return; if (!urlObj || !urlObj.href) return; if (!isSameOriginUrl(urlObj)) return; if (shouldDisablePreloadForUrl(urlObj)) return; if (hasSensitiveQueryParams(urlObj)) return; function fnv1a32(str) { let h = 0x811c9dc5; for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193); } return (h >>> 0); } function makeHintId(prefix, href) { // 不能用 btoa(href).slice(0, N) 作为 id:同域不同路径会共享相同前缀,导致严重碰撞。 const s = String(href || ''); const h1 = fnv1a32(s).toString(36); const h2 = fnv1a32('x' + s).toString(36); return `${prefix}_${h1}_${h2}`; } function trimHintLinks(rel, maxKeep) { try { const max = Number(maxKeep || 20); if (!Number.isFinite(max) || max <= 0) return; const all = Array.from(document.head.querySelectorAll(`link[rel="${rel}"][data-instantload-hint="1"]`)); if (all.length <= max) return; all.sort((a, b) => Number(a.dataset.t || 0) - Number(b.dataset.t || 0)); for (let i = 0; i < all.length - max; i++) { try { all[i].remove(); } catch {} } } catch {} } const href = urlObj.href; // 约束:prefetch 与 prerender 二选一(prerender 更激进),避免重复提示造成额外资源消耗。 const usePrerender = !!Storage.get('useLinkPrerender', false); const usePrefetch = !usePrerender && !!Storage.get('useLinkPrefetch', true); // prefetch if (usePrefetch) { const id = makeHintId('instantload_prefetch', href); if (!document.getElementById(id)) { const l = document.createElement('link'); l.id = id; l.rel = 'prefetch'; l.href = href; l.as = 'document'; l.dataset.instantloadHint = '1'; l.dataset.t = String(Date.now()); document.head.appendChild(l); trimHintLinks('prefetch', 20); } } // prerender(浏览器可能忽略) if (usePrerender) { const id2 = makeHintId('instantload_prerender', href); if (!document.getElementById(id2)) { const l2 = document.createElement('link'); l2.id = id2; l2.rel = 'prerender'; l2.href = href; l2.dataset.instantloadHint = '1'; l2.dataset.t = String(Date.now()); document.head.appendChild(l2); trimHintLinks('prerender', 10); } } } catch {} } /** * 预加载指定的链接。 * * @param {string} url - 需要预加载的链接URL。 * @param {HTMLElement} element - 对应的链接元素。 */ function preloadLink(url, element) { try { if (isSkippableUrl(url)) { try { Log.debug('跳过预加载:不可处理协议', { u: url }); } catch {} return; } var rawLinkURL = new URL(url, window.location.href); rawLinkURL = cleanTrackingParams(rawLinkURL); var key = getUrlKeyForCache(rawLinkURL.href); var linkURL = new URL(key, window.location.href); // 安全模式:不预加载 if (isSafeModeEnabled()) { try { Log.info('安全模式:跳过预加载', { u: linkURL.href }); } catch {} return; } // 敏感 URL:默认不预加载 if (shouldSkipPreloadForSensitive(linkURL)) { try { Log.info('跳过预加载:敏感 URL', { u: linkURL.href }); } catch {} return; } // 高风险站点:默认跳过预加载 if (shouldDisablePreloadForUrl(linkURL)) { console.log('跳过预加载:高风险站点', linkURL.hostname); try { Log.info('跳过预加载:高风险站点', { u: linkURL.href, h: linkURL.hostname }); } catch {} return; } if (linkURL.protocol !== 'http:' && linkURL.protocol !== 'https:') { console.log('跳过预加载:非 HTTP(S) 链接', url); try { Log.debug('跳过预加载:非 HTTP(S)', { u: url }); } catch {} return; } if (isDownloadLikeLink(element, linkURL)) { // 下载/附件链接不做预加载,避免破坏下载行为 try { Log.info('跳过预加载:疑似下载/附件链接', { u: linkURL.href }); } catch {} return; } if (isBlacklistModeEnabled && blacklistDomains.includes(linkURL.hostname)) { console.log('跳过预加载:黑名单域名', linkURL.hostname); try { Log.info('跳过预加载:黑名单域名', { u: linkURL.href, h: linkURL.hostname }); } catch {} return; } // 跳过与当前页面相同或者跨域的URL if (linkURL.hostname !== window.location.hostname || linkURL.href === window.location.href) { console.warn('Not preloading: Same page or cross-origin link:', url); try { Log.debug('跳过预加载:跨域或同页', { u: linkURL.href, h: linkURL.hostname }); } catch {} return; } // 已完成的不重复 if (isLinkPreloadedReady(element)) { console.log('跳过预加载:已完成', url); try { Log.debug('跳过预加载:已完成', { u: url }); } catch {} return; } // 已在进行中的不重复:以 abortControllers 判断更可靠 if (abortControllers[key]) { console.log('跳过预加载:进行中', url); try { Log.debug('跳过预加载:进行中', { u: url }); } catch {} return; } // 标记“已入队/进行中”,用于 UI 与点击兜底逻辑 try { if (element && element.dataset) { element.dataset.preloaded = 'pending'; element.dataset.preloadKey = key; } } catch {} // 原生预取提示:不依赖 fetch 成功,尽早注入(即使脚本预加载失败也可能对浏览器有帮助) try { maybeHintPrefetchOrPrerender(linkURL); } catch {} if (currentPreloads < maxConcurrentPreloads) { currentPreloads++; let options = { cache: "force-cache", // 使用force-cache可以帮助减少不必要的网络请求 as: appropriateAsAttributeValue(url), // 根据不同的链接类型,为 'as' 属性设置适当的值 credentials: "include", mode: 'same-origin', headers: new Headers(window.headers) }; if (element.rel && (element.rel.includes('noreferrer') || element.rel.includes('noopener'))) { options.referrerPolicy = 'no-referrer'; } // 创建一个新的abortController实例,并将signal传给fetch var controller = new AbortController(); controller.timestamp = Date.now(); var signal = controller.signal; options.signal = signal; // 将signal添加到fetch选项中 abortControllers[key] = controller; headPrecheckMaybeSkip(linkURL, element, signal).then((chk) => { try { if (chk && chk.ok === false) { throw makePreloadSkipError('precheck-blocked', { reason: String(chk.reason || 'blocked') }); } } catch (e) { return Promise.reject(e); } return fetch(linkURL.href, options); }).then(function(response) { if (!response.ok) { throw new Error('HTTP error, status = ' + response.status); } // 成功拿到可用响应:解除退避/冷却 try { resetPreloadBackoff(key); } catch {} // 记录 RTT(粗略:从发起到首包返回) try { const rtt = Date.now() - Number(controller.timestamp || 0); if (rtt > 0) recordNetSample(true, rtt); } catch {} // 统计:attempt 表示“已真正发起预加载请求”(命中率分母) try { const u = new URL(key, window.location.href); const rt = Date.now() - Number(controller.timestamp || 0); recordPreloadStat('attempt', { u: key, d: u.hostname, rt: (rt > 0 ? rt : 0) }); } catch {} // 响应校验:识别附件/非 HTML,避免误缓存/误预览 try { const cd = response.headers && response.headers.get ? response.headers.get('content-disposition') : ''; if (isAttachmentDisposition(cd)) { try { Log.info('跳过缓存:attachment 响应', { u: key, cd: cd }); } catch {} throw makePreloadSkipError('attachment', { cd: String(cd || '') }); } const ct = response.headers && response.headers.get ? response.headers.get('content-type') : ''; if (!isHtmlContentType(ct)) { try { Log.info('跳过缓存:非 HTML content-type', { u: key, ct: ct }); } catch {} throw makePreloadSkipError('non-html', { ct: String(ct || '') }); } } catch (e) { throw e; } return response.blob(); }).then(function(blob) { try { if (shouldCacheUrl(linkURL)) { saveContentToDB(key, blob); } else { try { Log.info('跳过缓存:疑似敏感参数 URL', { u: key }); } catch {} } } catch {} // 命中优化:写入内存缓存,规避 IndexedDB 写入延迟 try { memCacheSet(key, blob); } catch {} // 记录 ready key:用于预览覆盖层内恢复“已加载”样式 try { rememberPreloadReadyKey(key); } catch {} try { if (element && element.dataset) { element.dataset.preloaded = '1'; element.dataset.preloadKey = key; } } catch {} addAVisualLinkReminder(element); // 已在 fetch 前注入过(这里保留无害,id 去重) try { maybeHintPrefetchOrPrerender(linkURL); } catch {} try { touchRuntimeLru(key); const u = new URL(key, window.location.href); const rt = Date.now() - Number(controller.timestamp || 0); recordPreloadStat('success', { u: key, d: u.hostname, s: blob && blob.size ? blob.size : 0, rt: (rt > 0 ? rt : 0) }); try { Log.info('预加载成功', { u: key, s: blob && blob.size ? blob.size : 0 }); } catch {} } catch {} }).catch(function(error) { console.error('Preload failed for ', url, ':', error.message); // 失败采样(RTT:同样按开始时间粗略计算) try { const rtt = Date.now() - Number(controller && controller.timestamp ? controller.timestamp : 0); if (rtt > 0) recordNetSample(false, rtt); else recordNetSample(false, 0); } catch {} // 失败:清理 pending 标记,避免误导 UI try { if (element && element.dataset && element.dataset.preloaded === 'pending') { element.dataset.preloaded = ''; } } catch {} // 反爬/限流类失败:进入冷却,避免持续重试 try { // 跳过类失败:不进入退避(例如附件/非 HTML/预检阻止) if (error && error.__ilSkip) { try { Log.info('跳过预加载:识别为非预览目标', { u: key, r: String(error.__ilReason || ''), d: error.__ilExtra || null }); } catch {} } else { const msg = String(error && error.message ? error.message : ''); const isAntiBot = /status\s*=\s*(468|429)/i.test(msg); const isForbidden = /status\s*=\s*(403)/i.test(msg); const isServer = /status\s*=\s*(5\d\d)/i.test(msg); if (isAntiBot) { // 反爬/限流:更长冷却 schedulePreloadBackoff(key, 'anti-bot', { baseMs: 5 * 60 * 1000, maxMs: 60 * 60 * 1000, factor: 2, jitter: 0.15 }); } else if (isForbidden) { // 403:常见于鉴权/登录,退避更保守 schedulePreloadBackoff(key, 'forbidden', { baseMs: 10 * 60 * 1000, maxMs: 2 * 60 * 60 * 1000, factor: 2, jitter: 0.15 }); } else if (isServer) { // 5xx:短退避 schedulePreloadBackoff(key, 'server-error', { baseMs: 30 * 1000, maxMs: 15 * 60 * 1000, factor: 2, jitter: 0.2 }); } else { // 其它:轻退避 schedulePreloadBackoff(key, 'network', { baseMs: 10 * 1000, maxMs: 5 * 60 * 1000, factor: 2, jitter: 0.25 }); } } } catch {} try { const u = new URL(key, window.location.href); const rt = Date.now() - Number(controller && controller.timestamp ? controller.timestamp : 0); recordPreloadStat('fail', { u: key, d: u.hostname, m: String(error && error.message ? error.message : ''), rt: (rt > 0 ? rt : 0) }); try { Log.warn('预加载失败', { u: key, m: String(error && error.message ? error.message : '') }); } catch {} } catch {} }).finally(function() { currentPreloads = Math.max(0, currentPreloads - 1); try { const u = new URL(key, window.location.href); decDomainInFlight(getDomainKey(u)); } catch {} preloadNext(); delete abortControllers[key]; }); } else { addToPreloadQueue(key, element); return; } } catch (e) { console.error('Error preloading link:', e.message); try { Log.error('预加载异常', { u: url, m: String(e && e.message ? e.message : e) }); } catch {} } } /** * 继续预加载队列中的下一个链接。 */ function preloadNext() { var cap = getDynamicConcurrencyCap(); while (preloadQueue.length > 0 && currentPreloads < cap) { var nextPreload = preloadQueue.shift(); preloadLink(nextPreload.url, nextPreload.element); delete abortControllers[nextPreload.url]; } } /** * 预加载所有可见的链接。 */ function preloadVisibleLinks() { // 安全模式:不做视口扫描预加载 try { if (isSafeModeEnabled()) return; } catch {} // 后台标签页:跳过本轮扫描 try { if (typeof document !== 'undefined' && document.hidden) { return; } } catch {} // 离线/省流/慢网:跳过本轮扫描(点击/悬停仍可触发) try { if (isNetworkConstrainedForPreload()) { return; } } catch {} // 扫描策略:移动端偏“视口内 + 前 N 个”;桌面端以 hover 为主 // 限制扫描频率,避免频繁全量 querySelectorAll var now = Date.now(); var scanIntervalMs = Number(Storage.get('preloadScanIntervalMs', 300)); if (!Number.isFinite(scanIntervalMs) || scanIntervalMs < 50) scanIntervalMs = 50; // 页面刚从后台切回:加一个额外冷却,避免立即扫一大波 try { if (typeof document !== 'undefined' && document.visibilityState === 'visible') { const lastVisAt = Number(Storage.get('__il_lastVisibleAt', 0) || 0); if (lastVisAt && now - lastVisAt < 250) { return; } } } catch {} if (now - lastScanAt < scanIntervalMs) return; lastScanAt = now; var cap = getDynamicConcurrencyCap(); if (currentPreloads >= cap) return; rebuildCandidateListIfNeeded(); // 优先从候选池扫描;为空时才退化为全量扫描 var candidatesFromPool = []; try { // 限制每次处理的候选数量,避免巨型页面导致卡顿 const maxCandidates = 700; let n = 0; preloadCandidateMap.forEach((set) => { if (n >= maxCandidates) return; if (!set) return; set.forEach((el) => { if (n >= maxCandidates) return; if (el && el.isConnected !== false) { candidatesFromPool.push(el); n++; } }); }); } catch { candidatesFromPool = []; } var links = candidatesFromPool.length ? candidatesFromPool : Array.from(document.querySelectorAll('a[href]')); function isMeaninglessPreloadCandidate(linkEl, urlObj) { try { if (!linkEl || !urlObj) return true; if (linkEl.target && String(linkEl.target).toLowerCase() === '_blank') return true; if (linkEl.rel && (linkEl.rel.includes('nofollow') || linkEl.rel.includes('external'))) return true; if (linkEl.hasAttribute('onclick') || linkEl.getAttribute('role') === 'button') return true; // 登录态/一次性 token 或敏感路径:默认放弃预加载 if (shouldSkipPreloadForSensitive(urlObj)) return true; return false; } catch { return false; } } function scheduleCandidate(linkEl, reason) { try { var rawHref = linkEl.href; var normHref = getUrlKeyForCache(rawHref); var u = new URL(normHref, window.location.href); if (isSkippableUrl(normHref)) return; if (shouldDisablePreloadForUrl(u)) return; if (shouldSkipPreloadForSensitive(u)) return; if (u.hostname !== window.location.hostname) return; if (u.href === window.location.href) return; if (!canScheduleForDomain(getDomainKey(u))) { try { Log.debug('跳过入队:域名配额/并发限制', { u: normHref, d: getDomainKey(u) }); } catch {} return; } if (isMeaninglessPreloadCandidate(linkEl, u)) return; if (shouldPreloadMapping[normHref] === false) return; if (preloadSet.has(normHref) || linkEl.dataset.preloaded) return; // 并发不够则入队 if (currentPreloads >= cap) { addToPreloadQueue(normHref, linkEl); return; } preloadSet.add(normHref); incDomainInFlight(getDomainKey(u)); recordPreloadStat('enqueue', { r: reason, u: normHref, d: u.hostname }); try { Log.debug('预加载入队', { r: reason, u: normHref }); } catch {} preloadLink(normHref, linkEl); } catch {} } // 视口内优先 var candidates = Array.from(links); candidates = candidates.filter(function(link) { try { return link && link.href && !link.dataset.preloaded; } catch { return false; } }); // 避免全量排序:只扫描视口附近/下一屏附近,并限制最大候选数 const maxScan = 220; const viewH = (window.innerHeight || document.documentElement.clientHeight || 800); const upper = -viewH * 0.3; const lower = viewH * 2.2; candidates = candidates.filter(function(linkEl) { try { const rect = linkEl.getBoundingClientRect(); return rect.bottom >= upper && rect.top <= lower; } catch { return false; } }).slice(0, maxScan); candidates.sort(function(a, b) { var aRect = a.getBoundingClientRect(); var bRect = b.getBoundingClientRect(); return (aRect.top - bRect.top); }); // 移动端:视口内 + 首屏前 N 个 var aheadCount = Number(Storage.get('preloadAheadCount', 6)); if (!Number.isFinite(aheadCount) || aheadCount < 0) aheadCount = 0; if (aheadCount > 30) aheadCount = 30; var scheduled = 0; for (var idx = 0; idx < candidates.length; idx++) { if (currentPreloads >= cap) break; var linkEl = candidates[idx]; var rect = linkEl.getBoundingClientRect(); var inView = rect.bottom >= 0 && rect.right >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) && rect.left <= (window.innerWidth || document.documentElement.clientWidth); var nearNextScreen = rect.top > 0 && rect.top <= (window.innerHeight * 2); if (inView) { scheduleCandidate(linkEl, 'in-viewport'); scheduled++; } else if (isMobileDevice() && nearNextScreen && scheduled < aheadCount) { scheduleCandidate(linkEl, 'ahead'); scheduled++; } } } /** * 处理数据库写入队列。 */ function processDBWriteQueue() { if (dbWriteInProgress || dbWriteQueue.length === 0) { return; } dbWriteInProgress = true; var item = dbWriteQueue.shift(); blobToBase64(item.blob).then(base64data => { if (!dbReady || !db) { throw new Error('IndexedDB not ready'); } var transaction = db.transaction([dbStoreName], 'readwrite'); var objectStore = transaction.objectStore(dbStoreName); // 处理事务完成 return new Promise((resolve, reject) => { transaction.onabort = function() { reject(transaction.error || new Error('IndexedDB transaction aborted')); }; transaction.onerror = function() { reject(transaction.error || new Error('IndexedDB transaction error')); }; // 持久缓存记录: // - timestamp: 最后写入时间 // - lru: 最近访问时间(点击命中时会更新),用于更合理的淘汰 var request = objectStore.put({ url: item.url, htmlContent: base64data, timestamp: Date.now(), lru: Date.now() }); request.onsuccess = () => resolve(); request.onerror = () => reject(request.error || new Error('IndexedDB request error')); }); }).then(() => { console.log('页面内容已写入 IndexedDB:', item.url); try { updateCacheMeta(); } catch {} dbWriteInProgress = false; processDBWriteQueue(); // 递归处理队列中的下一项 }).catch(error => { console.error('IndexedDB save operation failed for', item.url, error); // 写入失败自动降级:避免无限重试/卡死 // - 配额不足/阻止写入等:清空队列并关闭 dbReady // - 其它错误:最多重试一次 var msg = String((error && error.name) ? error.name : '') + ' ' + String((error && error.message) ? error.message : ''); var isQuota = /QuotaExceededError/i.test(msg) || /quota/i.test(msg); item._retryCount = (item._retryCount || 0) + 1; if (isQuota || item._retryCount > 1) { dbWriteQueue = []; dbWriteInProgress = false; dbReady = false; db = null; dbInitializationPromise = null; cacheDegraded = true; try { updateCacheStatusBadge(); } catch {} try { showAlert('缓存写入失败,已自动降级(将不再缓存预加载内容)'); } catch {} return; } dbWriteQueue.unshift(item); // 发生错误时重新将项目放入队列 dbWriteInProgress = false; setTimeout(processDBWriteQueue, 1000); // 延迟重试 }); } /** * 从数据库中读取内容。 * * @param {string} url - 内容的URL。 * @param {Function} callback - 读取到内容后的回调函数。 */ function readContentFromDB(url, callback) { const key = getUrlKeyForCache(url); // 优先读取“内存命中缓存”,避免 DB 写入队列延迟造成“已标记但点进去不是预加载”。 try { const hit = memCacheGet(key); if (hit && hit.blob) { const reader = new FileReader(); reader.onloadend = function() { callback({ url: key, htmlContent: String(reader.result || ''), timestamp: Date.now(), source: 'memory' }); }; reader.onerror = function() { callback(null); }; reader.readAsDataURL(hit.blob); return; } } catch {} if (!dbReady || !db) { callback(null); return; } var transaction = db.transaction([dbStoreName], 'readonly'); var objectStore = transaction.objectStore(dbStoreName); var request = objectStore.get(key); request.onsuccess = function(event) { const result = event.target.result; callback(result); // 读取命中后,异步更新 lru(不阻塞主流程) try { if (result && result.url && dbReady && db) { const tx2 = db.transaction([dbStoreName], 'readwrite'); const store2 = tx2.objectStore(dbStoreName); const updated = Object.assign({}, result, { lru: Date.now() }); store2.put(updated); } } catch {} }; request.onerror = function(event) { console.error('IndexedDB read failed for', url); callback(null); }; } /** * 将内容保存到IndexedDB数据库中。 * * @param {string} url - 内容的URL。 * @param {Blob} blob - 包含要保存内容的Blob对象。 */ function saveContentToDB(url, blob) { if (!dbReady) { console.error('IndexedDB is not ready for writing data.'); return; } if (blob.size > maxContentSize) { console.log('内容超过大小上限,已跳过写入 IndexedDB'); return; } const key = getUrlKeyForCache(url); dbWriteQueue.push({ url: key, blob }); if (!dbWriteInProgress) { processDBWriteQueue(); } } function estimateBlobBytes(blob) { try { if (!blob) return 0; if (typeof blob.size === 'number') return Math.max(0, Number(blob.size) || 0); } catch {} return 0; } function estimateDbRecordsSummary(cb) { try { if (!dbReady || !db) { cb({ ok: false, message: '缓存不可用(已降级或未初始化)' }); return; } const tx = db.transaction([dbStoreName], 'readonly'); const store = tx.objectStore(dbStoreName); const req = store.getAll(); req.onsuccess = function() { try { const arr = Array.isArray(req.result) ? req.result : []; const perHost = new Map(); let totalBytes = 0; const recent = []; arr.forEach((it) => { try { const u = String(it && it.url ? it.url : ''); const host = new URL(u, window.location.href).hostname || ''; const b = estimateBlobBytes(it && it.blob); totalBytes += b; const prev = perHost.get(host) || { host, count: 0, bytes: 0 }; prev.count += 1; prev.bytes += b; perHost.set(host, prev); recent.push({ url: u, t: Number(it && it.timestamp ? it.timestamp : 0), bytes: b }); } catch {} }); const topHosts = Array.from(perHost.values()) .sort((a, b) => b.bytes - a.bytes) .slice(0, 8); const recentUrls = recent .sort((a, b) => (b.t || 0) - (a.t || 0)) .slice(0, 12) .map((x) => x.url); cb({ ok: true, items: arr.length, totalBytes, topHosts, recentUrls, }); } catch (e) { cb({ ok: false, message: String(e && e.message ? e.message : e) }); } }; req.onerror = function() { cb({ ok: false, message: '读取缓存失败' }); }; } catch (e) { cb({ ok: false, message: String(e && e.message ? e.message : e) }); } } function clearCacheForHost(hostname, cb) { try { const host = String(hostname || '').toLowerCase(); if (!host) { cb({ ok: false, message: '域名为空' }); return; } if (!dbReady || !db) { cb({ ok: false, message: '缓存不可用(已降级或未初始化)' }); return; } const tx = db.transaction([dbStoreName], 'readwrite'); const store = tx.objectStore(dbStoreName); const req = store.getAll(); req.onsuccess = function() { try { const arr = Array.isArray(req.result) ? req.result : []; let removed = 0; arr.forEach((it) => { try { const u = String(it && it.url ? it.url : ''); const h = new URL(u, window.location.href).hostname.toLowerCase(); if (h === host) { store.delete(u); removed += 1; } } catch {} }); tx.oncomplete = function() { try { updateCacheMeta(); } catch {} cb({ ok: true, removed }); }; tx.onerror = function() { cb({ ok: false, message: '删除失败' }); }; } catch (e) { cb({ ok: false, message: String(e && e.message ? e.message : e) }); } }; req.onerror = function() { cb({ ok: false, message: '读取缓存失败' }); }; } catch (e) { cb({ ok: false, message: String(e && e.message ? e.message : e) }); } } /** * 安排下一次数据库内容清理。 */ function scheduleNextCleanup() { // 删除旧内容后,再次调用此函数以依据当前间隔设定继续调度 if (cleanupTimerId) { clearTimeout(cleanupTimerId); } cleanupTimerId = setTimeout(function() { deleteOldContentFromDB(); // 同步清理内存命中缓存(TTL + LRU) try { memCacheCleanupExpired(); } catch {} scheduleNextCleanup(); }, dataCleanupInterval); } /** * 设置鼠标悬停预加载行为。 */ function setupMouseHoverPreload() { document.addEventListener('mouseover', function(event) { var target = event.target.closest('a'); if (target && !target.dataset.preloaded) { // 安全模式:不做悬停预加载 try { if (isSafeModeEnabled()) return; } catch {} // 判断链接是否指向图片,如果是,就不进行预加载处理 var href = target.getAttribute('href'); // 更新正则表达式来匹配 Greasy Fork 的特定图片链接模式 if (href.match(/\.(jpeg|jpg|gif|png|webp)$/i) || href.includes("active_storage/blobs/redirect")) { console.log('跳过预加载:图片链接', href); return; // 如果链接指向图片,直接返回,不设置预加载 } // 鼠标悬停65毫秒以上就启动预加载 target.dataset.hoverTimeout = setTimeout(function() { // 统一过滤:跳过不可处理协议/同页 hash/高风险等 if (isSkippableUrl(target.href)) return; // 敏感 URL 默认不预加载 try { const u = new URL(target.href, window.location.href); if (shouldSkipPreloadForSensitive(u)) return; } catch {} shouldPreload(target.href).then(function(shouldPreloadResult) { if (shouldPreloadResult && !target.dataset.preloaded) { preloadLink(target.href, target); // preloadLink 内部会设置 pending/ready 并在完成后再展示样式 try { addAVisualLinkReminder(target); } catch {} } }); }, 65); } }); document.addEventListener('mouseout', function(event) { var target = event.target.closest('a'); if (target && target.dataset.hoverTimeout) { // 当鼠标移开时清除定时器 clearTimeout(target.dataset.hoverTimeout); target.dataset.hoverTimeout = null; } }); } /** * 判断一个链接是否应该被预加载。 * * @param {string} url - 需要判断的链接URL。 * @returns {Promise} - 是否应该预加载该链接。 */ function shouldPreload(url) { // 失败冷却:短期内跳过 try { const key = getUrlKeyForCache(String(url || '')); const until = Number(preloadCooldownUntil[key] || 0); if (until && until > Date.now()) return Promise.resolve(false); } catch {} // 安全模式:不预加载 try { if (isSafeModeEnabled()) return Promise.resolve(false); } catch {} // 跳过非 http(s) 伪协议链接(如 javascript:、mailto:、tel:、#) try { const raw = String(url || '').trim(); if (!raw) return Promise.resolve(false); if (raw.startsWith('#')) return Promise.resolve(false); const lower = raw.toLowerCase(); // 兼容:旧变量名 IGNORED_PROTOCOLS 不一定存在,这里直接按 SKIP_PROTOCOLS 判断 for (const p of SKIP_PROTOCOLS) { if (lower.startsWith(p)) return Promise.resolve(false); } if (lower.startsWith('javascript:')) return Promise.resolve(false); } catch {} var linkURL = (url instanceof URL) ? url : new URL(url, window.location.href); // 敏感 URL 默认不预加载 try { if (shouldSkipPreloadForSensitive(linkURL)) return Promise.resolve(false); } catch {} // 只对 http(s) 且同源做判断;不再用 no-cors 读取 text(opaque 响应不可读,易误判) try { if (linkURL.protocol !== 'http:' && linkURL.protocol !== 'https:') return Promise.resolve(false); if (!isSameOriginUrl(linkURL)) return Promise.resolve(false); } catch { return Promise.resolve(false); } if (shouldDisablePreloadForUrl(linkURL)) { console.log('跳过预加载:高风险站点', linkURL.hostname); return Promise.resolve(false); } if (SIGNOUT_LINK_PATH_PARTS.some(link => linkURL.pathname.includes(link))) { console.log('跳过预加载:疑似退出/注销链接', url); return Promise.resolve(false); } var domain = linkURL.hostname; if (isWhitelistModeEnabled && !whitelistDomains.includes(domain)) { return Promise.resolve(false); } if (isBlacklistModeEnabled && blacklistDomains.includes(domain)) { return Promise.resolve(false); } // 轻量策略:只做规则判断,通过则允许进入 preloadLink;避免重复网络请求。 return Promise.resolve(true); } /** * 切换可拖动图标的显示状态。 * * @param {boolean} show - 是否显示图标。 */ function toggleDragIconVisibility(show) { var dragIcon = document.getElementById('draggableIcon'); if (dragIcon) { // 仅在预览覆盖层出现时显示;同时尊重“启用操作球”开关 try { const enabled = !!Storage.get('manipulatorBall', true); const forceVisible = !!Storage.get('manipulatorBallStyleExpanded', false); const hasPreview = !!document.getElementById('fullPageDiv'); const inPreview = !!show || hasPreview; // 展开时强制显示;收起后回到“预览页显示”逻辑。 const shouldShow = enabled && (forceVisible || inPreview); // 注意:图标本身可能带有 il-hidden(display:none !important),仅改 style.display 不足以显示。 if (shouldShow) { try { dragIcon.classList.remove('il-hidden'); } catch {} dragIcon.style.display = 'block'; try { applyManipulatorBallStyle(); } catch {} } else { try { dragIcon.classList.add('il-hidden'); } catch {} dragIcon.style.display = 'none'; } } catch { dragIcon.style.display = show ? 'block' : 'none'; } } } /** * 开始预加载链接。 */ function startLinkPreloading() { // 安全模式:仍初始化 DB(供读/诊断),但不启动任何预加载逻辑 if (isSafeModeEnabled()) { try { initDB(function() {}); } catch {} return; } // 约定: // - PC 端:仅鼠标悬停触发预加载(避免“进页面就自动预加载一堆链接”) // - 移动端:启用视口扫描/首屏预测 if (isMobileDevice()) { // 首次预热候选池,减少首次滚动时的全量扫描成本 try { primeCandidatePool(500); } catch {} initDB(preloadVisibleLinks); } else { // 仍需要初始化 DB,供点击命中时读取缓存 initDB(function() {}); } } /** * 处理回退导航。 */ function handleBackNavigation() { if (currentPreviewIndex > 0) { try { if (__currentPreviewUrlKey) capturePreviewScrollState(__currentPreviewUrlKey); } catch {} currentPreviewIndex -= 1; var prevURL = clickedLinks[currentPreviewIndex]; readContentFromDB(prevURL, function(data) { if (data) { displayPreloadedContent(data.htmlContent, prevURL); } else { location.href = prevURL; } }); } else { var fullPageDiv = document.getElementById('fullPageDiv'); if (fullPageDiv) { fullPageDiv.remove(); toggleDragIconVisibility(false); document.body.style.overflow = ''; } clickedLinks = []; currentPreviewIndex = -1; try { __currentPreviewUrlKey = ''; } catch {} } } /** * 处理前进导航。 */ function handleForwardNavigation() { if (currentPreviewIndex < clickedLinks.length - 1) { try { if (__currentPreviewUrlKey) capturePreviewScrollState(__currentPreviewUrlKey); } catch {} currentPreviewIndex += 1; var nextURL = clickedLinks[currentPreviewIndex]; readContentFromDB(nextURL, function(data) { if (data) { displayPreloadedContent(data.htmlContent, nextURL); } else { location.href = nextURL; } }); } } /** * 导航到URL。 */ function navigateToURL() { if (clickedLinks.length > 0 && currentPreviewIndex >= 0) { var currentURL = clickedLinks[currentPreviewIndex]; window.location.href = currentURL; } } /* -------------------------------------------------------------------------- */ /* 启动/初始化 */ /* -------------------------------------------------------------------------- */ function initRuntimeConfig() { indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB; dbVersion = 1; dbName = 'preloadedPagesDB'; dbStoreName = 'preloadedPages'; maxConcurrentPreloads = Storage.get('concurrentLoadingNumber', 2); dataCleanupInterval = Storage.get('dataCleanupInterval', 1) * 3600000; maxContentSize = Storage.get('maxContentSize', 5) * 1024 * 1024; maxStorageItems = Storage.get('maxStorageItems', 100); isWhitelistModeEnabled = Storage.get('whiteSelector', false); isBlacklistModeEnabled = Storage.get('blackSelector', true); // 设置变更:同步运行期并发上限 try { onSettingChanged((k, v) => { if (String(k || '') !== 'concurrentLoadingNumber') return; const next = Number(v); if (Number.isFinite(next) && next > 0) { maxConcurrentPreloads = next; } else { const fallback = Number(Storage.get('concurrentLoadingNumber', 2)); if (Number.isFinite(fallback) && fallback > 0) maxConcurrentPreloads = fallback; } }); } catch {} if (typeof GM_getValue === 'function') { // 修复:不再在这里“合并默认黑名单”或“回填写入”,避免用户移除后又被加回来。 // 默认黑名单仅由 initializeDefaultSettings 在首次安装时写入一次。 const storedBlacklistDomains = Storage.get('blacklistDomains', DEFAULT_BLACKLIST_DOMAINS); blacklistDomains = Array.isArray(storedBlacklistDomains) ? storedBlacklistDomains : []; const storedWhitelistDomains = Storage.get('whitelistDomains', []); whitelistDomains = Array.isArray(storedWhitelistDomains) ? storedWhitelistDomains : []; } } function initGestureIndicators() { const svgBack = ''; const svgForward = ''; divBack = document.createElement('div'); divForward = document.createElement('div'); divBack.innerHTML = svgBack; divForward.innerHTML = svgForward; document.body.appendChild(divBack); document.body.appendChild(divForward); navBackIcon = divBack.firstChild; navForwardIcon = divForward.firstChild; } function initUIPanel() { dbInitializationPromise = null; currentActivePanelId = 'panel1'; isAnimating = false; loadingPanel = createElementWithStylesAndAttributes('div', { position: "fixed", width: "305px", height: "auto", top: "50%", left: "50%", transform: "translate(-50%, -50%)", zIndex: "1001", backgroundColor: "#E0E5EC", borderRadius: "15px", boxShadow: "2px 2px 8px #BECBD8, -2px -2px 8px #FFFFFF", display: "none", flexDirection: "column" }, { className: 'loadingPanel customFontStyle' }); showcaseFeaturesPanel = createElementWithStylesAndAttributes('div', { position: "relative", width: "275px", height: "300px", borderRadius: "15px", display: "flex", alignItems: "center", justifyContent: "center", margin: "10px auto", overflowY: "hidden", overflowX: "hidden", flexWrap: "wrap", background: "#E0E5EC", boxShadow: "inset 9px 9px 16px #BABEC6, inset -9px -9px 16px #FFFFFF" }, { className: "showcaseFeaturesPanel" }); const versionInfoElement = createVersionInfoElement(); let font_style = `.customFontStyle *:not([class*='icon']):not(.fa):not(.fas):not(i) { font-family: 'PingFang SC', 'Heiti SC', 'myfont', 'Microsoft YaHei', 'Source Han Sans SC', 'Noto Sans CJK SC', 'HanHei SC', 'sans-serif' ,'icomoon','Icons' ,'brand-icons' ,'FontAwesome','Material Icons','Material Icons Extended','Glyphicons Halflings' !important; text-shadow: 1px 1px 10px #c3c3c3 !important; font-weight: bold !important; }`; const sectionCatalog = { quick: { title: '快捷', description: '常用功能集中在这里,一眼就能找到。', defaultExpanded: true, items: [ "安全模式,switch,safeMode,information,默认关闭(推荐为: 临时开关)\n\n⑴作用: 一键进入“安全优先”模式:只保留重定向净化,不做预加载/内联预览/点击拦截。\n⑵适用: 网银/支付/企业 SSO/登录流程等对预加载非常敏感的场景。\n⑶说明: 不影响你正常点击跳转,只是避免脚本接管与缓存副作用。", "点击拦截,selector,clickInterceptMode,information,默认:全拦截(保持旧行为)\n\n⑴设计原则: 避免一刀切拦截所有链接导致破站,同时给高级用户足够的可控性。\n⑵功能: 选择点击拦截策略(决定脚本是否接管你点击链接后的行为)。\n\n【各选项含义】\n- 全拦截:所有普通左键点击链接都会被脚本接管;如果该链接已预加载则展示预览,否则会进入预加载/跳转流程。优点是体验最“强”,缺点是更容易对少数网站造成干扰。\n- 仅命中拦截:只拦截“已经预加载命中”的链接;没命中的链接保持网站原本点击行为(更稳、更不容易破站)。\n- 仅同源:只拦截与当前页面同源(协议+域名+端口一致)的链接;跨域链接不拦截。\n- 仅白名单:只拦截白名单域名内的链接;其它域名不拦截(适合你只想在少数站点启用)。\n- 关闭拦截:完全不接管点击;脚本仍可能执行其它非点击相关能力(如缓存/清理等)。,selectorOptions,clickInterceptModeOptions", "预览渲染,selector,previewRenderMode,information,默认:内联(兼容优先)\n\n⑴作用: 决定“命中预加载后”的预览如何渲染,以减少白屏/样式污染/脚本冲突。\n⑵内联: 直接把缓存 HTML 注入到预览容器(会做基础清理),兼容性最好。\n⑶Shadow DOM: 把页面内容放进 ShadowRoot,隔离大部分样式污染(不保证所有站点完美)。\n⑷Sandbox iframe: 使用