LOFTER
https://www.lofter.com
分享者: qianqilins (14961)发布时间: 3天前
功能较重,需要较高版本,使用「阅读 Sigma」,其他版本未测试
※增加了听书功能(继续优化)
{
"articleStyle": 3,
"cacheFirst": true,
"concurrentRate": "700",
"customOrder": 24,
"enableJs": true,
"enabled": true,
"enabledCookieJar": true,
"header": "@js:(function(){\n var t=''; var m=''; \n try{\n t=(JSON.parse(source.getVariable()||'{}')||{}).lofterAuth||''; \n m=String(new Date()).replace(\/(.*?)\\s(.*?)\\s(.*?)\\s(.*?)GMT.*\/,'$1, $3 $2 $4 GMT'); \n }catch(e){}\n return JSON.stringify({\n \t \/*'User-Agent':'Dalvik\/2.1.0',*\/\n 'User-Agent':'okhttp\/3.10.0',\n 'lofter-phone-login-auth':t,\n \/*'if-modified-since':m*\/\n });\n})();;",
"injectJs": "(function() {\n 'use strict';\n\n if (window.__Injected) return;\n window.__Injected = true;\n\n \/* ===== 宿主适配层 ===== *\/\n const Host = {\n vibrate(ms) { try { if (!navigator.vibrate) return; if (navigator.userActivation && !navigator.userActivation.hasBeenActive) return; navigator.vibrate(ms); } catch (e) {} }, \n ajax(u) { u = String(u); if (typeof java !== 'undefined' && java?.ajax) { try { return java.ajax(u); } catch (e) {} } return fetchAjax(u); }, \n toast(m) { try { java.toast(String(m)); } catch (e) {} },\n open(k, u, t) { try { if (t) java.open(String(k), String(u), String(t)); \n else java.open(String(k), String(u)); } catch (e) {} },\n openUrl(u) { try { java.openUrl(String(u)); } catch (e) {} },\n showPhoto(u) { try { java.showPhoto(String(u)); } catch (e) {} },\n getVariable() { try { return source.getVariable() || ''; } catch (e) { return ''; } },\n putVariable(v) { try { source.putVariable(String(v)); return true; } catch (e) { return false; } },\n downloadFile(u) { try { return java.downloadFile(String(u)); } catch (e) { return null; } }, \n base64Encode(s) { try { return java.base64Encode(String(s)); } catch (e) { return ''; } }, \n };\n\n\/* ===== 外部变量 ===== *\/\nconst TuiImg = typeof TuiImgData === 'undefined' ? '推' : TuiImgData;\nconst LeiImg = typeof LeiImgData === 'undefined' ? '雷' : LeiImgData;\nconst CuaiEmo = typeof cuaiEmo === 'undefined' ? '' : cuaiEmo;\nconst CmtImg = typeof commentImg === 'undefined' ? '𝙃𝙊𝙏 𝘾𝙊𝙈𝙈𝙀𝙉𝙏' : commentImg;\n\n \/* ===== 常量 ===== *\/\n const SCHEMA_VERSION = 1;\n\n const LONG_PRESS_MS = 500;\n const LONG_PRESS_SLOP = 10;\n const VIBRATE_MS = 50;\n\n const VIEWER_MIN_SWIPE = 50;\n const VIEWER_LOCK_MS = 350;\n\n const ALL_L2_MAX = 10;\n const COMMENTS_MIN = 5;\n\n var DEFAULT_EMO = {\n '[更新求踢]': CuaiEmo\n };\n\n const API = {\n BLOG: 'https:\/\/api.lofter.com\/v2.0\/blogHomePage.api',\n POST: 'https:\/\/api.lofter.com\/oldapi\/post\/detail.api',\n COLL: 'https:\/\/api.lofter.com\/v1.1\/postCollection.api',\n GIFT: 'https:\/\/api.lofter.com\/v1.1\/trade\/gift\/post\/newSupportInfo',\n L1: 'https:\/\/api.lofter.com\/comment\/l1\/page.json',\n L2: 'https:\/\/api.lofter.com\/comment\/l2\/page\/abtest.json',\n PARA: 'https:\/\/api.lofter.com\/comment\/pCommentList.json',\n HOT: 'https:\/\/api.lofter.com\/comment\/hotComment.json',\n COUNTS: 'https:\/\/api.lofter.com\/comment\/pCommentCounts.json',\n EMOTES: 'https:\/\/api.lofter.com\/newapi\/emotes.json',\n AUDIO: 'https:\/\/api.lofter.com\/newapi\/post\/audio\/'\n };\n const WBI = {\n haha: 'https:\/\/face.t.sinajs.cn\/t4\/appstyle\/expression\/ext\/normal\/c5\/201810_haha_mobile.png',\n bitter: 'https:\/\/face.t.sinajs.cn\/t4\/appstyle\/expression\/ext\/normal\/87\/201810_lei_mobile.png',\n sweet: 'https:\/\/face.t.sinajs.cn\/t4\/appstyle\/expression\/ext\/normal\/79\/2022_wow_mobile.png',\n ping: 'https:\/\/face.t.sinajs.cn\/t4\/appstyle\/expression\/ext\/normal\/ce\/201810_bingbujiandan_mobile.png'\n };\n\n \/* ===== 通用工具 ===== *\/\n function trimStr(s) { return String(s == null ? '' : s).trim(); }\n \/* 正则元字符转义 *\/\n function reEsc(s) { return String(s).replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&'); }\n \/* 读取隐藏 input 的值 *\/\n function val(id) { const e = document.getElementById(id); return e ? e.value : ''; }\n \/* HTML 转义 *\/\n function esc(s) {\n if (s == null) return '';\n return String(s)\n .replace(\/&\/g, '&')\n .replace(\/<\/g, '<')\n .replace(\/>\/g, '>')\n .replace(\/\"\/g, '"')\n .replace(\/'\/g, ''');\n }\n \n \/* 时间戳格式化 *\/\n function fmtTime(ts) {\n const n = Number(ts);\n if (!n) return '';\n const d = new Date(n + 8 * 36e5),\n z = v => String(v).padStart(2, '0');\n return `${d.getUTCFullYear()}-${z(d.getUTCMonth()+1)}-${z(d.getUTCDate())} ${z(d.getUTCHours())}:${z(d.getUTCMinutes())}:${z(d.getUTCSeconds())}`; \n}\n\n \/* 过滤危险 URL *\/\n function safeUrl(u) {\n if (!u) return '';\n u = String(u);\n if (\/^https?:\\\/\\\/\/i.test(u)) return u;\n if (\/^\\\/\\\/\/.test(u)) return 'https:' + u;\n if (\/^data:image\\\/\/i.test(u)) return u;\n return '';\n }\n \n \/* java.ajax 降级通道 *\/\n function fetchAjax(u){\n if(typeof fetch!='function')return null;\n let i={credentials:'include'},url=u;\n try{\n const p=u.match(\/^(.*?)\\?product=([^,]*),(.+)$\/);\n if(p){\n const o=JSON.parse(p[3]),m=(o.method||'GET').toUpperCase();\n url=p[1]+'?product='+encodeURIComponent(p[2]);\n i.method=m;\n i.headers=Object.fromEntries(Object.entries(o).filter(([k])=>!\/^(method|body|accept-encoding|content-length|host|connection|user-agent|origin|referer|cookie)$\/i.test(k)));\n if(o.body!=null&&!\/^(GET|HEAD)$\/.test(m)){i.body=''+o.body;i.headers['Content-Type']||='application\/x-www-form-urlencoded'}\n }\n return fetch(url,i).then(r=>r?.ok?r.text():null).catch(()=>null)\n }catch{return null}\n}\n \n \/* 调整头像 URL *\/\n function avaUrl(b) {\n const u = safeUrl(b);\n if (!u) return '';\n return u + '?imageView&thumbnail=84x84&stripmeta=0&quality=90&type=webp&typefallback=jpg';\n }\n \/* Body 滚动锁 *\/\n let _lockScrollY = 0;\n let _lockBarWidth = 0;\n let _scrollLocked = false;\n\n function lockBodyScroll() {\n if (_scrollLocked) return;\n _scrollLocked = true;\n _lockScrollY = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0;\n _lockBarWidth = window.innerWidth - document.documentElement.clientWidth;\n document.body.style.overflow = 'hidden';\n if (_lockBarWidth > 0) document.body.style.paddingRight = _lockBarWidth + 'px';\n }\n\n function unlockBodyScroll() {\n if (!_scrollLocked) return;\n _scrollLocked = false;\n document.body.style.overflow = '';\n document.body.style.paddingRight = '';\n const y = window.scrollY || window.pageYOffset || document.documentElement.scrollTop || 0;\n if (y !== _lockScrollY) window.scrollTo(0, _lockScrollY);\n _lockScrollY = 0;\n _lockBarWidth = 0;\n }\n\n \/* ===== Store:单一状态源 ===== *\/\n const Store = (function() {\n const state = { tags: [], collections: [], authors: [], articles: [] };\n \/\/ const dirty = new Set();\n \/\/ let flushTimer = null;\n let lastRaw = '';\n let corrupted = false;\n\n \/* 读变量并解析,返回 {ok, cfg} *\/\n function readRaw() {\n const raw = Host.getVariable();\n if (!raw || !raw.trim()) return { ok: true, cfg: {} };\n try {\n const p = JSON.parse(raw);\n if (!p || typeof p !== 'object' || Array.isArray(p)) return { ok: false };\n return { ok: true, cfg: p };\n } catch (e) { return { ok: false }; }\n }\n\n \/* 全量载入变量到 State *\/\n function load() {\n const raw = Host.getVariable();\n lastRaw = raw;\n const { ok, cfg } = readRaw();\n corrupted = !ok;\n if (!ok) {\n state.tags = [];\n state.collections = [];\n state.authors = [];\n state.articles = [];\n return;\n }\n const arr = v => Array.isArray(v) ? v : [];\n state.tags = arr(cfg.tags).map(trimStr).filter(Boolean);\n state.collections = arr(cfg.collections).filter(c => c && c.name && c.id);\n state.authors = arr(cfg.authors).filter(a => a && a.name && a.blogId);\n state.articles = arr(cfg.articles).filter(a => a && a.title && a.url);\n }\n\n \/* 若变量外部已变,重新 Load *\/\n function reload() {\n const raw = Host.getVariable();\n if (raw === lastRaw) return false;\n load();\n return true;\n }\n\n \/* 原子追加 *\/\n function add(field, item, dupFn, opts) {\n const { ok, cfg } = readRaw();\n if (!ok) { corrupted = true; return 'corrupt'; }\n\n const latest = cfg || {};\n if (!Array.isArray(latest[field])) latest[field] = [];\n\n if (dupFn && latest[field].some(dupFn)) {\n \/\/ 同步本地,UI 的\"已存在\"判断才准确\n state[field] = latest[field];\n return 'dup';\n }\n\n if (opts && opts.prepend) latest[field].unshift(item);\n else latest[field].push(item);\n latest.version = SCHEMA_VERSION;\n\n const payload = JSON.stringify(latest);\n try {\n const r = JSON.parse(payload);\n if (!r || typeof r !== 'object' || Array.isArray(r)) return 'fail';\n } catch (e) { return 'fail'; }\n\n if (!Host.putVariable(payload)) return 'fail';\n\n state[field] = latest[field];\n lastRaw = Host.getVariable();\n corrupted = false;\n return 'ok';\n }\n\n load();\n\n return {\n state, load, reload, add, isCorrupted() { return corrupted; }\n };\n })();\n\n \/* ===== 表情替换:key → <img> ===== *\/\n const Emo = (function() {\n \/*let _emotes = null;*\/\n function mk() {\n \/*if(!_emotes){_emotes = {}; try{JSON.parse(Host.ajax(API.EMOTES)).data.packages.forEach(p => p.emotes.forEach(e => e.name && e.url && (_G[e.name] = e.url)));}catch(e){}}*\/\n return Object.assign({}, DEFAULT_EMO \/*, _emotes*\/ );\n }\n\n \/* 从条目里收集 emotes 到 map *\/\n function pick(it, m) {\n if (!it || !m || !Array.isArray(it.emotes)) return;\n it.emotes.forEach(e => {\n if (e && e.name && e.url) m[e.name] = e.url;\n });\n }\n \/* 长 key 优先,避免短 key 抢先匹配 *\/\n function sub(s, m) {\n if (!s || !m) return s;\n const keys = Object.keys(m);\n if (!keys.length) return s;\n keys.sort((a, b) => b.length - a.length);\n const pattern = new RegExp(keys.map(reEsc).join('|'), 'g');\n return s.replace(pattern, k => {\n const u = safeUrl(m[k]);\n if (!u) return k;\n return `<img class=\"emo\" src=\"${esc(u)}\" alt=\"${esc(k)}\" title=\"${esc(k)}\" onerror=\"this.style.display='none';this.nextElementSibling.style.display='inline'\"><span style=\"display:none\">${esc(k)}<\/span>`;\n });\n }\n\n \/* 信任服务端清洗结果,仅做表情替换 *\/\n function render(html, m) { return sub(html || '', m); }\n return { mk, pick, sub, render };\n })();\n\n \/* 替换 [表情] \/ [图片] 占位符 *\/\n function replaceCMImages(content, images) {\n if (!content || !images || images.length === 0) return content;\n var imgHtml = buildCMImages(images);\n if (!imgHtml) return content;\n return content.replace(\/\\[(表情|图片)\\]\/g, imgHtml);\n }\n function buildCMImages(images, wrap) {\n if (wrap === undefined) wrap = true;\n if (!images || images.length === 0) return '';\n var imgs = '';\n for (var i = 0; i < images.length; i++) {\n var url = images[i] && (images[i].orign || images[i].raw || images[i].url || '');\n if (url) imgs += `<img class=\"cm-img\" src=\"${esc(url)}\" loading=\"lazy\" onclick=\"java.showPhoto('${esc(url)}')\" onerror=\"this.style.opacity='0'\">`;\n }\n if (!imgs) return '';\n return wrap ? `<div class=\"cm-imgs\">${imgs}<\/div>` : imgs;\n }\n\n \/* ===== 添加:标签 \/ 合集 \/ 作者 \/ 文章 ===== *\/\n function notifyAdd(kind, name, r) {\n if (r === 'ok') Host.toast(`${kind}「${name}」添加完成!`);\n else if (r === 'dup') Host.toast(`${kind}「${name}」已订阅`);\n else if (r === 'corrupt') Host.toast('变量损坏,无法写入');\n else Host.toast('写入失败,请重试');\n }\n function addTag(t) {\n t = trimStr(t);\n if (!t) return;\n notifyAdd('标签', t, Store.add('tags', t, x => x === t));\n }\n function addCol(n, id) {\n n = trimStr(n);\n id = trimStr(id);\n if (!n || !id) return Host.toast('合集信息不完整');\n notifyAdd('合集', n, Store.add('collections', { name: n, id }, x => x && String(x.id) === id));\n }\n function addAuthor(b, n, a) {\n b = trimStr(b);\n n = trimStr(n);\n if (!b || !n) return Host.toast('作者信息不完整');\n notifyAdd('作者', n, Store.add('authors', { name: n, blogId: b, avatar: a || '' },\n x => x && String(x.blogId) === b));\n }\n function addArticle(t, u) {\n t = trimStr(t);\n u = trimStr(u);\n if (!t) {\n const el = document.querySelector('.post-author__name');\n const a = el ? el.textContent.trim() : '';\n t = a ? `『${a}』的作品` : '无题';\n }\n if (!u) return Host.toast('文章链接缺失');\n notifyAdd('文章', t, Store.add('articles', { title: t, url: u }, x => x && x.url === u, { prepend: true }));\n }\n\n \/* ===== 共享数据 ===== *\/\n \/* 以签名检测失效,支持 SPA *\/\n let _sharedCache = null;\n let _sharedSig = '';\n\n function shared() {\n const sig = [val('blogId'), val('article-url'), val('collection-id')].join('|');\n if (_sharedCache && _sharedSig === sig) return _sharedCache;\n _sharedSig = sig;\n _sharedCache = {\n author: { blogId: val('blogId'), name: val('author-name'), avatar: val('author-avatar') },\n article: { title: val('article-title'), url: val('article-url') },\n collection: { blogId: val('blogId'), name: val('collection-name'), id: val('collection-id') }\n };\n return _sharedCache;\n }\n\n \/* ===== 链接路由 ===== *\/\n function openBlog(id) {\n id = trimStr(id);\n if (!id) return false;\n const info = \/^\\d+$\/.test(id) ? `targetblogid=${id}` : `blogdomain=${id}`;\n const body = `supportposttypes=1%2C2%2C3%2C4%2C5%2C6&${info}&offset={{(page-1)*18}}&method=getPostLists&postdigestnew=1&returnData=1&limit=18&checkpwd=1&needgetpoststat=1`;\n Host.open('sort', `${API.BLOG}?product=lofter-android-7.4.4,${JSON.stringify({ method:'POST', body })}`);\n return true;\n }\n \/* 链接路由到原生页面(文章正文 \/ 合集列表 \/ 作者列表) *\/\n function routeLink(u, t) {\n if (!u) return false;\n if (u.indexOf('\/\/') === 0) u = 'https:' + u;\n const post = u.match(\/https?:\\\/\\\/([^.]+)\\.lofter\\.com\\\/post\\\/([0-9a-fA-F]+)_([0-9a-fA-F]+)\/);\n if (post) {\n const body = `targetblogid=${parseInt(post[2],16)}&blogdomain=${post[1]}.lofter.com&postid=${parseInt(post[3],16)}`; \n Host.open('rss', `${API.POST}?product=lofter-android-8.3.64,${JSON.stringify({ method:'POST','accept-encoding':'gzip, deflate', body })}`, t);\n return true;\n }\n const col = u.match(\/collectionId=(\\d+)\/);\n if (col && \/front\\\/blog\\\/collection\/.test(u)) {\n Host.open('sort', `${API.COLL}?product=lofter-android-8.0.2&method=getCollectionDetail&offset={{(page-1)*45}}&limit=45&collectionid=${col[1]}`);\n return true;\n }\n const bid = u.match(\/blogId=(\\d+)\/);\n if (bid) return openBlog(bid[1]);\n\n const hp = u.match(\/lofter\\.com\\\/front\\\/blog\\\/home-page\\\/([^\/?#]+)\/);\n if (hp) {\n const n = hp[1].replace(\/\\.lofter\\.com$\/i, '');\n return n && n !== 'www' ? openBlog(`${n}.lofter.com`) : false;\n }\n const home = u.match(\/^https?:\\\/\\\/(.+?)\\.lofter\\.com\\\/?$\/);\n if (home && home[1] !== 'www') return openBlog(`${home[1]}.lofter.com`);\n return false;\n }\n\n \/* ===== 图片查看器:点击切换 \/ 左右滑动 ===== *\/\n const Viewer = (function() {\n function initOne(v) {\n if (v.dataset.ivBound === '1') return;\n v.dataset.ivBound = '1';\n const slides = Array.from(v.querySelectorAll('.iv-slide'));\n if (!slides.length) return;\n const n = slides.length;\n let i = 0, lock = false, vert = false;\n \n function go(next) {\n if (lock || vert || n <= 1) return;\n next = ((next % n) + n) % n;\n if (next === i) return;\n lock = true;\n slides[i].classList.remove('active');\n i = next;\n slides[i].classList.add('active');\n setTimeout(() => { lock = false; }, VIEWER_LOCK_MS);\n }\n v.addEventListener('click', e => {\n if (e.target.closest('.iv-page')) {\n e.stopPropagation();\n if (n <= 1) return;\n vert = !vert;\n v.classList.toggle('iv-vertical', vert);\n slides.forEach((s, k) => s.classList.toggle('active', vert || k === i));\n return;\n }\n go(i + 1);\n });\n \n let sx = 0, sy = 0;\n v.addEventListener('touchstart', e => {\n sx = e.touches[0].clientX;\n sy = e.touches[0].clientY;\n }, { passive: true });\n v.addEventListener('touchend', e => {\n if (vert) return;\n const dx = e.changedTouches[0].clientX - sx;\n const dy = e.changedTouches[0].clientY - sy;\n if (Math.abs(dx) > VIEWER_MIN_SWIPE && Math.abs(dx) > Math.abs(dy)) {\n go(i + (dx < 0 ? 1 : -1));\n }\n }, { passive: true });\n }\n \n function scan() { document.querySelectorAll('.iv').forEach(initOne); }\n return { scan };\n})();\n \n \/* ===== 异步请求:一次重试 ===== *\/\n const REQ_RETRY = 1;\n const REQ_RETRY_DELAY = 250;\n\n function requestRaw(url) {\n return new Promise(resolve => {\n let attempt = 0;\n\n function run() {\n const raw = Host.ajax(url);\n if (raw) { resolve(raw); return; }\n if (attempt++ < REQ_RETRY) setTimeout(run, REQ_RETRY_DELAY * attempt);\n else resolve(null);\n }\n run();\n });\n }\n async function ajaxJson(url) {\n const raw = await requestRaw(url);\n if (!raw) return null;\n try { return JSON.parse(raw); } catch (e) { return null; }\n }\n\n \/* ===== 合集弹窗 ===== *\/\n const ColDialog = (function() {\n let mask, list;\n\n function build() {\n if (list) return;\n mask = document.createElement('div');\n mask.className = 'cb-mask';\n mask.addEventListener('click', e => { if (e.target === mask) close(); });\n mask.addEventListener('touchmove', e => {\n if (e.target.closest('.cb')) return;\n e.preventDefault();\n }, { passive: false });\n\n const box = document.createElement('div');\n box.className = 'cb';\n\n const head = document.createElement('div');\n head.className = 'cb-head';\n const title = document.createElement('h3');\n title.className = 'cb-title';\n title.textContent = '📖 合集';\n const x = document.createElement('button');\n x.className = 'cb-x';\n x.type = 'button';\n x.innerHTML = '<i class=\"fa fa-times\"><\/i>';\n x.addEventListener('click', e => { e.stopPropagation();\n close(); });\n head.append(title, x);\n\n list = document.createElement('div');\n list.className = 'cb-list';\n\n box.append(head, list);\n mask.appendChild(box);\n document.body.appendChild(mask);\n }\n\n function status(text, color) {\n list.innerHTML = '';\n const d = document.createElement('div');\n d.className = 'cb-status';\n d.textContent = text;\n if (color) d.style.color = color;\n list.appendChild(d);\n }\n\n function render(cs) {\n if (!cs.length) return status('暂无', '#999');\n list.innerHTML = '';\n const frag = document.createDocumentFragment();\n cs.forEach(c => {\n const card = document.createElement('div');\n card.className = 'cb-card';\n card.dataset.collectionName = c.name;\n card.dataset.collectionId = c.id;\n\n const t = document.createElement('div');\n t.className = 'cb-card-title';\n t.textContent = c.name;\n\n const d = document.createElement('div');\n d.className = 'cb-card-desc';\n d.textContent = c.description;\n\n const i = document.createElement('div');\n i.className = 'cb-card-info';\n \/\/ i.textContent = `${c.postCount}篇 · ID: ${c.id} ‖ ${fmtTime(c.lastPublishTime)}`;\n var f = c.postCollectionHot;\n var h = f >= 10000 ? (Math.round(f \/ 1000) \/ 10) + 'w' : f;\n i.textContent = `${c.postCount}篇 · 热度: ${h} ‖ ${fmtTime(c.lastPublishTime)}`;\n\n card.append(t, d, i);\n frag.appendChild(card);\n });\n list.appendChild(frag);\n }\n\n async function open(blogId) {\n build();\n mask.classList.add('open');\n lockBodyScroll();\n status('加载中...');\n const bid = encodeURIComponent(blogId);\n const data = await ajaxJson(\n `${API.COLL}?product=lofter-android-7.4.4&method=getCollectionList&needViewCount=1&blogid=${bid}&targetblogid=${bid}`\n );\n if (!data) { status('加载失败', '#f88'); return; }\n render((data.response && data.response.collections) || []);\n }\n\n function close() {\n if (mask) mask.classList.remove('open');\n unlockBodyScroll();\n }\n\n return { open, close };\n })();\n\n \/* ===== 作者弹窗 ===== *\/\n const AuthorDialog = (function() {\n let mask, content, curId;\n\n function build() {\n if (mask) return;\n mask = document.createElement('div');\n mask.className = 'ad-mask';\n mask.addEventListener('click', e => { if (e.target === mask) close(); });\n mask.addEventListener('touchmove', e => {\n if (e.target.closest('.ad')) return;\n e.preventDefault();\n }, { passive: false });\n\n const box = document.createElement('div');\n box.className = 'ad';\n box.innerHTML = '<button class=\"ad-x\" type=\"button\"><i class=\"fa fa-times\"><\/i><\/button>';\n box.querySelector('.ad-x').addEventListener('click', close);\n\n content = document.createElement('div');\n content.className = 'ad-body';\n box.appendChild(content);\n mask.appendChild(box);\n document.body.appendChild(mask);\n }\n\n function show(msg) { content.innerHTML = `<div class=\"ad-status\">${esc(msg)}<\/div>`; }\n\n function num(v) {\n const n = Number(v) || 0;\n return n >= 1e4 ? (Math.round(n \/ 1e3) \/ 10) + 'w' : String(n);\n }\n\n function tags(labelHtml, list, warn) {\n if (!Array.isArray(list) || !list.length) return '';\n const cls = 'ad-tags' + (warn ? ' ad-tags--warn' : '');\n const items = list.map(t => `<span class=\"ad-tag\">${esc(t)}<\/span>`).join('');\n return `<div class=\"${cls}\"><span class=\"ad-tags-label\">${labelHtml}<\/span>${items}<\/div>`;\n }\n\n function render(info, resp) {\n const s = info.blogStat || {};\n\n \/* 有头像框时叠一层 frame *\/\n const avaUrl = safeUrl(info.bigAvaImg);\n const boxUrl = safeUrl(info.avatarBoxImage);\n const avaImg = `<img class=\"ad-avatar\" src=\"${esc(avaUrl)}\" alt=\"\" onerror=\"this.style.opacity='0'\">`;\n const avaHtml = boxUrl ?\n `<span class=\"ad-avatar-wrap\">${avaImg}<img class=\"ad-avatar-frame\" src=\"${esc(boxUrl)}\" alt=\"\" aria-hidden=\"true\" onerror=\"this.style.opacity='0'\"><\/span>` :\n avaImg;\n\n \/* 作者背景图 (暂不使用 \/ 注释备份) *\/\n \/\/ const bc = (((resp.bgTabs||[])[0]||{}).blogCover) || {};\n \/\/ const cv = safeUrl(bc.customBlogCover || bc.url || '');\n \/\/ const cu = cv ? cv + (cv.includes('?') ? '&' : '?') + 'imageView&thumbnail=750x0&quality=85' : '';\n \/\/ const headHtml = `<div class=\"ad-head${cv?' ad-head--cover':''}\"${cv?` style=\"background-image:url('${esc(cu)}')\"`:''}>`;\n\n \/* 是否已关注:查本地变量,与接口无关 *\/\n const followed = Store.state.authors.some(a => String(a.blogId) === String(info.blogId));\n\n const achs = (info.achievements || []).map(a => {\n const ic = safeUrl(a.icon);\n const click = ic ? ` onclick=\"java.showPhoto('${esc(ic)}')\"` : '';\n return `<span class=\"ad-ach ad-ach--icon\"${click} title=\"${esc(a.authName || '')}\"><i class=\"fa fa-trophy\"><\/i>${esc(a.authName || '')}<\/span>`;\n }).concat((resp.commentImpression || []).map(a => `<span class=\"ad-ach\">${esc(a.title || '')}<\/span>`)).join('');\n\n \/\/ ${tags('喜好', info.oshi, false)}\n \/\/ ${tags('雷点', info.trigger, true)}\n content.innerHTML = `\n <div class=\"ad-head\">\n ${avaHtml}\n <div class=\"ad-name\">${esc(info.blogNickName || '')}<\/div>\n <div class=\"ad-domain\">@${esc(info.blogName || '')}${info.ipLocation ? ' · ' + esc(info.ipLocation) : ''}<\/div>\n <\/div>\n ${info.selfIntro ? `<div class=\"ad-intro\">${esc(info.selfIntro)}<\/div>` : ''}\n <div class=\"ad-stats\">\n <div class=\"ad-stat\"><b>${num(s.publicPostCount)}<\/b><span>作品<\/span><\/div>\n <div class=\"ad-stat\"><b>${num(s.followedCount)}<\/b><span>粉丝<\/span><\/div>\n <div class=\"ad-stat\"><b>${num(s.followingCount)}<\/b><span>关注<\/span><\/div>\n <div class=\"ad-stat\"><b>${num(s.likedCount)}<\/b><span>获赞<\/span><\/div>\n <\/div>\n ${tags(TuiImg, info.oshi, false)}\n ${tags(LeiImg, info.trigger, true)}\n ${achs ? `<div class=\"ad-achievements\">${achs}<\/div>` : ''}\n <div class=\"ad-actions\">\n <button class=\"ad-btn ad-btn--follow\" type=\"button\"${followed ? ' disabled' : ''}>\n <i class=\"fa fa-user\"><\/i> ${followed ? '已订阅' : '添加作者'}\n <\/button>\n <button class=\"ad-btn ad-btn--open\" type=\"button\">\n <i class=\"fa fa-feed\"><\/i> 文章列表\n <\/button>\n <\/div>`;\n\n const btnFollow = content.querySelector('.ad-btn--follow');\n btnFollow.addEventListener('click', () => {\n addAuthor(info.blogId, info.blogNickName, info.bigAvaImg);\n \/* addAuthor 已同步 Store.state.authors,直接查表 *\/\n if (Store.state.authors.some(a => String(a.blogId) === String(info.blogId))) {\n btnFollow.disabled = true;\n btnFollow.innerHTML = '<i class=\"fa fa-user\"><\/i> 已订阅';\n }\n });\n\n content.querySelector('.ad-btn--open').addEventListener('click', () => {\n close();\n openBlog(info.blogId);\n });\n }\n\n async function open(blogId) {\n build();\n mask.classList.add('open');\n lockBodyScroll();\n if (!blogId) { show('作者信息缺失');\n close(); return; }\n Store.reload();\n show('加载中...');\n curId = blogId;\n\n const body = `targetblogid=${blogId}&method=getBlogInfoDetail&returnData=1&checkpwd=1&needgetpoststat=1`;\n const url = `${API.BLOG}?product=lofter-android-8.3.20,${JSON.stringify({ method: 'POST', 'accept-encoding': 'gzip, deflate', body })}`; \n\n const d = await ajaxJson(url);\n if (curId !== blogId) return;\n if (!d) { show('加载失败,请重试'); return; }\n\n const resp = d.response || {};\n const info = resp.blogInfo;\n if (!info || !info.blogId) { show('未找到作者信息'); return; }\n render(info, resp);\n }\n\n function close() {\n if (mask) mask.classList.remove('open');\n unlockBodyScroll();\n curId = null;\n }\n\n return { open, close };\n })();\n\n \/* ===== 评论 API ===== *\/\n const CmtApi = {\n async l1(postId, blogId, offset) {\n const d = await ajaxJson(\n `${API.L1}?postId=${encodeURIComponent(postId)}&blogId=${encodeURIComponent(blogId)}&offset=${offset}&dunType=1`\n );\n if (!d) return { list: [], hot: [], total: null };\n return {\n list: (d.data && d.data.list) || [],\n hot: (d.data && d.data.hotList) || [],\n total: (d.data && (d.data.total || d.data.count)) || null\n };\n },\n\n async l2(postId, blogId, commentId, offset) {\n const d = await ajaxJson(\n `${API.L2}?postId=${encodeURIComponent(postId)}&blogId=${encodeURIComponent(blogId)}&id=${encodeURIComponent(commentId)}&offset=${offset}`\n );\n if (!d) return { list: [], nextOffset: -1 };\n return {\n list: (d.data && d.data.list) || [],\n nextOffset: (d.data && d.data.offset !== undefined) ? Number(d.data.offset) : -1\n };\n },\n\n \/* 拉取回复(MAX 页) *\/\n async allL2(postId, blogId, commentId) {\n const all = [];\n const seen = new Set();\n let off = 0;\n for (let k = 0; k < ALL_L2_MAX; k++) {\n const r = await this.l2(postId, blogId, commentId, off);\n if (!r.list.length) break;\n for (let i = 0; i < r.list.length; i++) {\n const it = r.list[i];\n if (!it || !it.id) continue;\n const id = String(it.id);\n if (seen.has(id)) continue;\n seen.add(id);\n all.push(it);\n }\n const nxt = r.nextOffset;\n if (nxt === -1) break;\n if (nxt <= off) break;\n off = nxt;\n }\n return all;\n },\n\n async para(postId, blogId, pid, offset) {\n const d = await ajaxJson(\n `${API.PARA}?postId=${encodeURIComponent(postId)}&blogId=${encodeURIComponent(blogId)}&offset=${offset}&pid=${encodeURIComponent(pid)}&imgId=&commentId=`\n );\n if (!d) return { list: [], nextOffset: -1 };\n return {\n list: (d.data && d.data.list) || [],\n nextOffset: (d.data && d.data.offset !== undefined) ? Number(d.data.offset) : -1\n };\n },\n\n \/* 拉取热评(简化版) *\/\n async hot(postId, blogId) {\n const d = await ajaxJson(\n `${API.HOT}?postId=${encodeURIComponent(postId)}&blogId=${encodeURIComponent(blogId)}`\n );\n if (!d) return [];\n return (d.data && d.data.list) || [];\n },\n\n \/* 拉取段评计数 *\/\n async counts(postId, blogId) {\n const d = await ajaxJson(\n `${API.COUNTS}?postId=${encodeURIComponent(postId)}&blogId=${encodeURIComponent(blogId)}`\n );\n if (!d) return [];\n return (d.data && d.data.list) || [];\n },\n\n \/* 从评论链接提取 postId \/ blogId *\/\n params() {\n const a = document.querySelector('a[href*=\"comment.do\"]');\n if (!a) return null;\n const h = a.getAttribute('href');\n let p, b;\n try {\n const url = new URL(h, location.origin);\n p = url.searchParams.get('pid');\n b = url.searchParams.get('bid');\n } catch (e) {\n const g = n => {\n const m = h.match(new RegExp('[?&]' + n + '=([^&]*)'));\n return m ? decodeURIComponent(m[1]) : null;\n };\n p = g('pid');\n b = g('bid');\n }\n return p && b ? { postId: p, blogId: b } : null;\n }\n };\n \/* ===== 评论渲染 ===== *\/\n const CR = {\n \/* 次级回复 *\/\n reply(r, m, isPop) {\n const pb = r.publisherBlogInfo || {};\n const rb = r.replyBlogInfo || {};\n const bid = val('blogId');\n const im = r.images || [];\n const ua = String(pb.blogId || '') === String(bid) ? ' author' : '';\n const ub = String(rb.blogId || '') === String(bid) ? ' author' : '';\n const t = fmtTime(r.publishTime);\n const at = rb.blogNickName ?\n `<span class=\"cm-rp-at\"> 回复 <\/span><span class=\"cm-rp-user${ub}\">${rb.blogNickName}<\/span>` :\n '';\n const like = (isPop && r.likeCount) ?\n `<i class=\"fa fa-heart\"><\/i> ${esc(r.likeCount)}` :\n '';\n return `\n <div class=\"cm-rp\" data-reply-id=\"${esc(r.id || '')}\" data-pid=\"${esc(r.pid || '')}\">\n <div class=\"cm-rp-head\">\n <img class=\"cm-rp-ava\" src=\"${esc(avaUrl(pb.bigAvaImg))}\" alt=\"\" onerror=\"this.style.display='none'\"> \n <span class=\"cm-rp-user${ua}\">${pb.blogNickName || ''}<\/span>${at}\n <\/div>\n <div class=\"cm-rp-body\">${replaceCMImages(Emo.render(r.content, m),im)}<\/div>\n <div class=\"cm-rp-foot sp\">\n <span class=\"cm-like\">${like}<\/span>\n <span class=\"cm-meta\">${esc(t)} ${r.ipLocation || ''}<\/span>\n <\/div>\n <\/div>`;\n },\n\n \/* 一级评论 *\/\n item(it, m, postId, blogId, isPop) {\n const pb = it.publisherBlogInfo || {};\n const im = it.images || [];\n const ua = String(pb.blogId || '') === String(blogId) ? ' author' : '';\n const t = fmtTime(it.publishTime);\n const q = isPop ? '' : `<span class=\"cm-quote\" value=\"${esc(it.pid || '')}\" data-quote=\"${esc(it.quote || '')}\"><\/span>`;\n const top = it.top === 1 ? '<span class=\"cm-top\">置顶<\/span>' : '';\n const like = (isPop && it.likeCount) ? `<i class=\"fa fa-heart\"><\/i> ${esc(it.likeCount)}` : '';\n\n let h = `\n <div class=\"cm\" data-comment-id=\"${esc(it.id || '')}\" data-l2count=\"${esc(it.l2Count || 0)}\">\n <div class=\"cm-head\">\n <img class=\"cm-ava\" src=\"${esc(avaUrl(pb.bigAvaImg))}\" alt=\"\" onerror=\"this.style.display='none'\"> \n <span class=\"cm-user${ua}\">${pb.blogNickName || ''}<\/span>${top}${q}\n <\/div>\n <div class=\"cm-body\">${replaceCMImages(Emo.render(it.content, m),im)}<\/div>\n <div class=\"cm-foot sp\">\n <span class=\"cm-like\">${like}<\/span>\n <span class=\"cm-meta\">${esc(t)} ${it.ipLocation || ''}<\/span>\n <\/div>`;\n if (it.l2Count > 0) h += '<div class=\"cm-rp-container\"><\/div>';\n return h + '<\/div>';\n },\n\n \/* 精简评论 *\/\n hot(it, m) {\n return `<div class=\"cm\">\n <div class=\"cm-body\">${Emo.render(it.content, m)}<\/div>\n <div class=\"cm-foot\">${esc(it.commentHot || '')} <i class=\"fa fa-heart\" aria-hidden=\"true\"><\/i><\/div>\n <\/div>`;\n },\n\n list(arr, postId, blogId, isPop) {\n const m = Emo.mk();\n return arr.map(it => {\n Emo.pick(it, m);\n return CR.item(it, m, postId, blogId, !!isPop);\n }).join('');\n }\n };\n\n \/* ===== 初始评论加载(迁移) ===== *\/\n async function loadInitialComments() {\n const postId = val('postId');\n const blogId = val('blogId');\n if (!postId || !blogId) return;\n \/* 读评论模式:'0' 最新 \/ '1' 热门 \/ '' 精简 \/ '-1' 关闭 *\/\n let mode = '0';\n try {\n const raw = Host.getVariable();\n const cfg = (raw && raw.trim()) ? JSON.parse(raw) : {};\n if (cfg.comments !== undefined && cfg.comments !== null) mode = String(cfg.comments);\n } catch (e) {}\n const section = document.querySelector('.comments');\n if (!section) return;\n if (section.querySelector('.cm')) return;\n section.setAttribute('data-mode', mode);\n if (mode === '-1') return;\n const isHot = (mode === '1');\n const hidden = (mode === '');\n const r = await CmtApi.l1(postId, blogId, 0);\n const emoteMap = Emo.mk();\n (r.list || []).forEach(it => Emo.pick(it, emoteMap)); \/\/ 简评引用\n (r.hot || []).forEach(it => Emo.pick(it, emoteMap));\n let choiceHtml = '';\n let mainHtml = '';\n if (!isHot) {\n const hotList = await CmtApi.hot(postId, blogId);\n if (hotList.length) {\n hotList.forEach(it => Emo.pick(it, emoteMap));\n choiceHtml = CmtImg +\n hotList.map(it => CR.hot(it, emoteMap)).join('');\n }\n }\n if (!hidden) {\n var target = isHot ? (r.hot && r.hot.length ? r.hot : r.list) : r.list;\n if (target && target.length) {\n target.forEach(it => Emo.pick(it, emoteMap)); \/\/ ?未调用\n \/\/ 🔆\n const title = isHot ? '<b><em>热门评论<\/em><\/b>' : '<b><em>最新评论<\/em><\/b>';\n mainHtml = title + CR.list(target, postId, blogId);\n }\n }\n section.innerHTML = choiceHtml + mainHtml;\n if (!hidden && !isHot) { section.dataset.l1Offset = String((target || []).length); }\n fillEmptyReplies(postId, blogId, false);\n fillQuotes();\n initMoreComments();\n }\n\n \/* ===== 段评标记加载(迁移) ===== *\/\n async function loadCommentCounts() {\n const postId = val('postId');\n const blogId = val('blogId');\n if (!postId || !blogId) return;\n\n \/\/ 关闭评论时不加载段评标记\n let mode = '0';\n try {\n const raw = Host.getVariable();\n const cfg = (raw && raw.trim()) ? JSON.parse(raw) : {};\n if (cfg.comments !== undefined && cfg.comments !== null) mode = String(cfg.comments);\n } catch (e) {}\n if (mode === '-1') return;\n\n const list = await CmtApi.counts(postId, blogId);\n if (!list.length) return;\n\n const map = {};\n list.forEach(it => {\n if (it.pid) map[it.pid] = {\n count: it.count || 0,\n hahaNum: it.hahaNum || 0,\n sweetNum: it.sweetNum || 0,\n bitterNum: it.bitterNum || 0\n };\n });\n\n const paras = document.querySelectorAll('.post-content p');\n paras.forEach((p, idx) => {\n const fallbackPid = 'p_i' + idx;\n const actualPid = p.id || fallbackPid;\n const info = map[actualPid] || map[fallbackPid];\n if (!info) return;\n if (p.querySelector('.cm-mark')) return;\n\n let text = '',\n type = '';\n if (info.count > 0) {\n text = `<span class=\"emo-word-fb\">评论<\/span>${info.count}+`;\n type = 'comment';\n } else {\n const emos = [\n { t: 'haha', l: '哈哈哈', v: info.hahaNum, img: WBI.haha },\n { t: 'bitter', l: '好虐', v: info.bitterNum, img: WBI.bitter },\n { t: 'sweet', l: '真甜', v: info.sweetNum, img: WBI.sweet },\n ];\n let max = emos[0];\n for (let e = 1; e < emos.length; e++)\n if (emos[e].v > max.v) max = emos[e];\n if (max.v > 0) {\n const face = `<img class=\"emo-word\" src=\"${max.img}\" alt=\"${max.l}\" onerror=\"this.style.display='none';this.nextElementSibling.style.display='inline'\"><span class=\"emo-word-fb\">${max.l}<\/span>`;\n text = `${face}×${max.v}`;\n \/\/ text = `${max.l}×${max.v}`;\n type = max.t;\n }\n }\n if (!text) return;\n\n const marker = document.createElement('span');\n marker.className = 'cm-mark';\n marker.setAttribute('data-pid', actualPid);\n marker.setAttribute('data-count-type', type);\n marker.setAttribute('data-has-comments', info.count > 0);\n marker.setAttribute('data-count', info.count);\n marker.setAttribute('data-haha', info.hahaNum);\n marker.setAttribute('data-sweet', info.sweetNum);\n marker.setAttribute('data-bitter', info.bitterNum);\n marker.innerHTML = text;\n p.appendChild(marker);\n });\n }\n\n \/* ===== 次级评论异步填充 ===== *\/\n async function loadRepliesInto(container, postId, blogId, commentId, full) {\n try {\n let list, total;\n if (full) {\n list = await CmtApi.allL2(postId, blogId, commentId);\n total = list.length;\n } else {\n const r = await CmtApi.l2(postId, blogId, commentId, 0);\n list = r.list;\n const cm = container.closest('.cm');\n total = cm ? (parseInt(cm.dataset.l2count, 10) || list.length) : list.length;\n }\n const m = Emo.mk();\n list.forEach(x => Emo.pick(x, m));\n let html = list.map(x => CR.reply(x, m, full)).join('');\n if (!full && total > list.length) {\n html += `<div class=\"cm-more-rp\" data-comment-id=\"${esc(commentId)}\" data-offset=\"${list.length}\" data-total=\"${total}\">加载更多回复(已显示 ${list.length}\/${total})<\/div>`;\n }\n container.innerHTML = html;\n } catch (e) { \/* 静默失败 *\/ }\n }\n\n function fillEmptyReplies(postId, blogId, full) {\n document.querySelectorAll('.cm-rp-container:empty').forEach(c => {\n const cm = c.closest('.cm');\n if (!cm) return;\n const cid = cm.dataset.commentId;\n const cnt = parseInt(cm.dataset.l2count, 10) || 0;\n if (cid && cnt > 0) loadRepliesInto(c, postId, blogId, cid, full);\n });\n }\n\n \/* ===== 加载更多:回复 \/ 评论 ===== *\/\n async function moreReplies(btn) {\n if (!btn || btn.classList.contains('disabled')) return;\n const cid = btn.dataset.commentId;\n const total = parseInt(btn.dataset.total, 10) || 0;\n const off = parseInt(btn.dataset.offset, 10) || 0;\n if (!cid || off >= total) { btn.remove(); return; }\n\n const p = CmtApi.params();\n if (!p) {\n btn.classList.add('disabled');\n btn.textContent = '参数获取失败';\n return;\n }\n\n btn.classList.add('disabled');\n btn.textContent = '加载中...';\n const r = await CmtApi.l2(p.postId, p.blogId, cid, off);\n if (!r.list.length) {\n btn.textContent = '没有更多回复了';\n setTimeout(() => btn.remove(), 500);\n return;\n }\n const m = Emo.mk();\n r.list.forEach(x => Emo.pick(x, m));\n const box = btn.closest('.cm-rp-container') || btn.parentNode;\n const seen = new Set(Array.from(box.querySelectorAll('.cm-rp[data-reply-id]')).map(el => String(el.dataset.replyId)));\n const fresh = r.list.filter(x => x && x.id && !seen.has(String(x.id)));\n if (fresh.length) btn.insertAdjacentHTML('beforebegin', fresh.map(x => CR.reply(x, m, false)).join(''));\n\n const next = off + r.list.length;\n if (next >= total) btn.remove();\n else {\n btn.dataset.offset = next;\n btn.textContent = `加载更多回复(已显示 ${next}\/${total})`;\n btn.classList.remove('disabled');\n }\n }\n async function moreComments() {\n const btn = document.getElementById('loadMoreCommentsBtn');\n if (!btn || btn.classList.contains('disabled')) return;\n const p = CmtApi.params();\n if (!p) {\n btn.classList.add('disabled');\n btn.textContent = '参数获取失败';\n return;\n }\n\n const section = document.querySelector('.comments');\n if (!section) return;\n\n const loaded = parseInt(section.dataset.l1Offset || '', 10);\n const cursor = isNaN(loaded) ?\n document.querySelectorAll('.comments .cm[data-comment-id]').length :\n loaded;\n\n const seen = new Set(\n Array.from(section.querySelectorAll('.cm[data-comment-id]'))\n .map(el => String(el.dataset.commentId || ''))\n .filter(Boolean)\n );\n\n btn.classList.add('disabled');\n btn.textContent = '加载中...';\n const r = await CmtApi.l1(p.postId, p.blogId, cursor);\n const list = r.list || [];\n section.dataset.l1Offset = String(cursor + list.length);\n const fresh = list.filter(it => it && it.id && !seen.has(String(it.id)));\n\n if (fresh.length) {\n \/\/ const m = Emo.mk(); \/\/ 未使用\n const html = CR.list(fresh, p.postId, p.blogId);\n btn.insertAdjacentHTML('beforebegin', html);\n fillEmptyReplies(p.postId, p.blogId, false);\n fillQuotes();\n }\n\n const next = cursor + list.length;\n if (r.total != null && next >= Number(r.total)) {\n btn.textContent = '没有更多评论了';\n btn.classList.add('disabled');\n } else if (!fresh.length && !list.length) {\n btn.textContent = '没有更多评论了';\n btn.classList.add('disabled');\n } else {\n btn.textContent = '加载更多评论';\n btn.classList.remove('disabled');\n }\n }\n\n \/* 评论数达到阈值时注入\"加载更多\" *\/\n function initMoreComments() {\n if (document.getElementById('loadMoreCommentsBtn')) return;\n const section = document.querySelector('.comments');\n if (!section || section.getAttribute('data-mode') === '1' || section.getAttribute('data-mode') === '-1') return;\n if (section.dataset.lmcInit === '1') return;\n if (document.querySelectorAll('.comments .cm .cm-user').length < COMMENTS_MIN) return;\n section.dataset.lmcInit = '1';\n const b = document.createElement('div');\n b.id = 'loadMoreCommentsBtn';\n b.className = 'cm-more';\n b.textContent = '加载更多评论';\n \/\/ b.addEventListener('click', moreComments);\n section.appendChild(b);\n }\n\n \/* ===== 引用(pid)工具 ===== *\/\n let _pidMap = null;\n\n function resetPidMap() { _pidMap = null; }\n\n function buildPidMap() {\n const map = new Map();\n document.querySelectorAll('.post-content p').forEach((p, i) => {\n map.set('p_i' + i, p);\n if (p.id) map.set(p.id, p);\n });\n return map;\n }\n\n function findTarget(pid) {\n if (!pid) return null;\n if (!_pidMap) _pidMap = buildPidMap();\n return _pidMap.get(String(pid)) || null;\n }\n\n \/* 取元素文本(排除 .cm-mark 内标记) *\/\n function noMark(el) {\n const w = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);\n let t = '';\n while (w.nextNode()) {\n const n = w.currentNode;\n const parent = n.parentElement;\n if (parent && parent.closest('.cm-mark')) continue;\n t += n.textContent;\n }\n return t.replace(\/\\s+\/g, ' ').trim();\n }\n const _textCache = new WeakMap();\n\n function getText(el) {\n if (_textCache.has(el)) return _textCache.get(el);\n const t = noMark(el);\n _textCache.set(el, t);\n return t;\n }\n \/* 把引用块的文本填到 .cm-quote *\/\n function fillQuotes() {\n document.querySelectorAll('.cm-quote[value]').forEach(s => {\n const pid = s.getAttribute('value');\n if (!pid) return;\n const t = findTarget(pid);\n if (pid.indexOf('p_i') === 0) {\n s.textContent = s.getAttribute('data-quote') || '';\n if (t) t.classList.add('is-quoted');\n return;\n }\n if (!t) return;\n t.classList.add('is-quoted');\n s.textContent = getText(t);\n });\n }\n\n \/* ===== 段评弹窗 ===== *\/\n const Popup = (function() {\n let mask, quoteEl, list, stats, ctx = null;\n let quoteTarget = null;\n\n function jumpToQuote() {\n if (!quoteTarget) return;\n const t = quoteTarget;\n close();\n setTimeout(() => {\n t.scrollIntoView({ behavior: 'smooth', block: 'center' });\n t.classList.add('is-quoted');\n t.classList.add('is-flash');\n setTimeout(() => t.classList.remove('is-flash'), 2000);\n }, 50);\n }\n\n function build() {\n if (list) return;\n mask = document.createElement('div');\n mask.className = 'cpop-mask';\n mask.addEventListener('click', e => { if (e.target === mask) close(); });\n mask.addEventListener('touchmove', e => {\n if (e.target.closest('.cpop-list, .cpop-quote')) return;\n e.preventDefault();\n }, { passive: false });\n\n const box = document.createElement('div');\n box.className = 'cpop';\n\n const head = document.createElement('div');\n head.className = 'cpop-head';\n const title = document.createElement('h3');\n title.className = 'cpop-title';\n title.textContent = '段落评论';\n const x = document.createElement('button');\n x.className = 'cpop-x';\n x.type = 'button';\n x.innerHTML = '<i class=\"fa fa-times\"><\/i>';\n x.addEventListener('click', e => { e.stopPropagation();\n close(); });\n head.append(title, x);\n\n quoteEl = document.createElement('div');\n quoteEl.className = 'cpop-quote';\n quoteEl.addEventListener('click', jumpToQuote);\n\n list = document.createElement('div');\n list.className = 'cpop-list';\n list.addEventListener('scroll', onScroll);\n\n stats = document.createElement('div');\n stats.className = 'cpop-stats';\n\n box.append(head, quoteEl, list, stats);\n mask.appendChild(box);\n document.body.appendChild(mask);\n }\n\n \/* 触底加载下一页 *\/\n function onScroll() {\n if (!ctx || !ctx.more || ctx.loading) return;\n const { scrollTop, scrollHeight, clientHeight } = list;\n if (scrollTop + clientHeight >= scrollHeight - 50) next();\n }\n\n function renderStats(s) {\n if (!s) return;\n if (s.count > 0) {\n stats.innerHTML =\n `<span class=\"cpop-stat cpop-stat--sweet\">真甜 ${esc(s.sweet || 0)}<\/span>` +\n `<span class=\"cpop-stat cpop-stat--bitter\">好虐 ${esc(s.bitter || 0)}<\/span>` +\n `<span class=\"cpop-stat cpop-stat--haha\">哈哈哈 ${esc(s.haha || 0)}<\/span>` +\n `<span class=\"cpop-stat\">评论 ${esc(s.count || 0)}<\/span>`;\n } else {\n stats.innerHTML =\n `<span class=\"cpop-stat cpop-stat--sweet\"><img class=\"stat-emo\" src=\"${esc(WBI.sweet)}\" alt=\"真甜\" onerror=\"this.style.opacity='0'\">真甜 ${esc(s.sweet || 0)}<\/span>` +\n `<span class=\"cpop-stat cpop-stat--bitter\"><img class=\"stat-emo\" src=\"${esc(WBI.bitter)}\" alt=\"好虐\" onerror=\"this.style.opacity='0'\">好虐 ${esc(s.bitter || 0)}<\/span>` +\n `<span class=\"cpop-stat cpop-stat--haha\"><img class=\"stat-emo\" src=\"${esc(WBI.haha)}\" alt=\"哈哈\" onerror=\"this.style.opacity='0'\">哈哈哈 ${esc(s.haha || 0)}<\/span>` +\n `<span class=\"cpop-stat\"><img class=\"stat-emo\" src=\"${esc(WBI.ping)}\" alt=\"评论\" onerror=\"this.style.opacity='0'\">评论 ${esc(s.count || 0)}<\/span>`;\n }\n }\n\n async function appendPage(r, first) {\n ctx.off = r.nextOffset;\n ctx.more = r.nextOffset !== -1;\n\n if (first && !r.list.length) {\n list.style.display = 'none';\n list.innerHTML = '';\n return;\n }\n list.style.display = '';\n\n const m = Emo.mk();\n r.list.forEach(it => Emo.pick(it, m));\n const html = r.list.map(it => CR.item(it, m, ctx.postId, ctx.blogId, true)).join('');\n\n if (first) {\n list.innerHTML = html;\n const ind = document.createElement('div');\n ind.className = 'cpop-loading';\n ind.id = 'cpop-load';\n ind.textContent = ctx.more ? '上拉加载更多' : '没有更多了';\n list.appendChild(ind);\n } else {\n const ind = document.getElementById('cpop-load');\n if (ind) {\n ind.insertAdjacentHTML('beforebegin', html);\n ind.textContent = ctx.more ? '上拉加载更多' : '没有更多了';\n } else {\n list.insertAdjacentHTML('beforeend', html);\n }\n }\n\n fillEmptyReplies(ctx.postId, ctx.blogId, true);\n }\n\n async function next() {\n if (!ctx || ctx.loading || !ctx.more) return;\n ctx.loading = true;\n const ind = document.getElementById('cpop-load');\n if (ind) ind.textContent = '加载中...';\n const page = await CmtApi.para(ctx.postId, ctx.blogId, ctx.pid, ctx.off);\n if (!ctx) return; \/\/ 重检\n if (!page.list.length) ctx.more = false;\n await appendPage(page, false);\n ctx.loading = false;\n }\n\n function bindQuote(pid) {\n quoteTarget = findTarget(pid);\n if (quoteTarget) {\n quoteEl.textContent = getText(quoteTarget);\n quoteEl.style.display = '';\n } else {\n quoteEl.textContent = '';\n quoteEl.style.display = 'none';\n }\n }\n\n async function open(pid, st) {\n build();\n mask.classList.add('open');\n lockBodyScroll();\n\n list.scrollTop = 0;\n list.style.display = '';\n list.innerHTML = '<div class=\"cpop-loading\">加载中...<\/div>';\n\n const postId = val('postId');\n const blogId = val('blogId');\n if (!postId || !blogId || !pid) {\n quoteEl.style.display = 'none';\n list.style.display = 'none';\n stats.innerHTML = '';\n Host.toast('参数缺失');\n close();\n return;\n }\n\n bindQuote(pid);\n ctx = { pid, postId, blogId, off: 0, more: false, loading: false };\n renderStats(st);\n\n const first = await CmtApi.para(postId, blogId, pid, 0);\n await appendPage(first, true);\n }\n\n function close() {\n if (mask) mask.classList.remove('open');\n unlockBodyScroll();\n if (quoteEl) { quoteEl.textContent = '';\n quoteEl.style.display = 'none'; }\n if (list) { list.innerHTML = '';\n list.scrollTop = 0;\n list.style.display = ''; }\n if (stats) stats.innerHTML = '';\n quoteTarget = null;\n ctx = null;\n }\n\n return { open, close };\n })();\n\n\/* ===== 缓存文章 ===== *\/\nfunction exportTxtClick() {\n var w = document.querySelector('.post-content')?.cloneNode(true);\n if (!w) return Host.toast('正文为空');\n var v = id => document.getElementById(id)?.value || '';\n var ec = [...document.querySelectorAll('.unlock-item:not(:has(.support-card))')].map(it => ['__title','__content'].map(s => it.querySelector('.unlock-item'+s)?.innerText.trim()).filter(Boolean).join('\\n')).filter(Boolean); \n w.querySelectorAll('img,button,.emo,.cm-mark,.iv-bar,.iv-page').forEach(n => n.remove()); \n var t = [[v('article-title') || document.title, ' ',\n v('author-name') && `作者: ${v('author-name')}`, \n v('collection-name') && `合集: ${v('collection-name')}`, \n v('article-url') && `来源: ${v('article-url')}`, \n '='.repeat(30)].filter(Boolean).join('\\n'),\n w.innerText, ec.length && `\\n${ec.join('\\n')}`].filter(Boolean).join('\\n'); \n try { Host.toast(`已缓存:${Host.downloadFile(`data:text\/plain;charset=utf-8;base64,${Host.base64Encode(t)}`)}`); } catch (e) { Host.toast(`失败:${e?.message || e}`); } \n}\n\n \/* ===== 全局点击分发 ===== *\/\n document.addEventListener('click', e => {\n\n \/* 图片查看 *\/\n const ivBtn = e.target.closest('.iv-open, .iv-zoom');\n if (ivBtn) {\n e.stopPropagation(); e.preventDefault();\n const slide = ivBtn.closest('.iv-slide');\n const img = slide && slide.querySelector('img');\n const src = img && safeUrl(img.src);\n if (!src) return;\n if (ivBtn.classList.contains('iv-open')) Host.openUrl(src);\n else Host.showPhoto(src.replace(\/\\?[\\s\\S]+\/, ''));\n return;\n }\n\n \/* 引用跳转 *\/\n const q = e.target.closest('.cm-quote[value]');\n if (q) {\n e.preventDefault(); e.stopPropagation();\n const t = findTarget(q.getAttribute('value'));\n if (t) {\n t.scrollIntoView({ behavior: 'smooth', block: 'center' });\n t.classList.add('is-flash');\n setTimeout(() => t.classList.remove('is-flash'), 2000);\n }\n return;\n }\n\n \/* 段评标记 *\/\n const mk = e.target.closest('.cm-mark');\n if (mk) {\n e.preventDefault();\n e.stopPropagation();\n const pid = mk.getAttribute('data-pid');\n if (!pid) return;\n const n = k => parseInt(mk.getAttribute(k) || '0', 10);\n Popup.open(pid, {\n hasComments: mk.getAttribute('data-has-comments') === 'true',\n count: n('data-count'),\n haha: n('data-haha'),\n sweet: n('data-sweet'),\n bitter: n('data-bitter')\n });\n return;\n }\n \n \/* 统计数据提示 *\/\n const pc=e.target.closest('.post-counts__item');\n if(pc) {\n e.preventDefault(); e.stopPropagation();\n const n=pc.textContent.trim() || '0',\n t={\n favorite:' 人喜欢\/收藏', \n subscribe:' 人订阅',\n response:' 条评论', \n share:' 次推荐',\n showgift:' 个彩蛋', \n showaudio:' 位收听'\n };\n for(const k in t) {\n if(pc.classList.contains('p-count-'+k)) {\n return Host.toast('共有 '+n+t[k]);\n }}\n return;\n } \n\n \/* 加载更多回复 *\/\n const rp = e.target.closest('.cm-more-rp');\n if(rp){ e.preventDefault(); e.stopPropagation(); moreReplies(rp); return; }\n const lm = e.target.closest('#loadMoreCommentsBtn');\n if(lm){ e.preventDefault(); e.stopPropagation(); moreComments(); return; }\n\n \/* data-action 分发 *\/\n const a = e.target.closest('[data-action]');\n if (a) {\n const kind = a.getAttribute('data-action');\n const sd = shared();\n switch (kind) {\n case 'export-ext':\n e.stopPropagation();\n exportTxtClick();\n return; \n case 'article-add':\n e.stopPropagation();\n addArticle(sd.article.title, sd.article.url);\n return;\n case 'follow':\n e.stopPropagation();\n addAuthor(sd.author.blogId, sd.author.name, sd.author.avatar);\n return;\n case 'tts':\n e.stopPropagation(); e.preventDefault();\n ttsPlay(); return;\n case 'collection-trigger':\n e.stopPropagation();\n if (sd.author.blogId) ColDialog.open(sd.author.blogId);\n return;\n case 'collection-info':\n e.stopPropagation(); PostNav.openToc(); return;\n case 'collection-import':\n e.preventDefault(); e.stopPropagation();\n if (!sd.collection.name || !sd.collection.id) { Host.toast('合集无法添加'); return; }\n addCol(sd.collection.name, sd.collection.id);\n return;\n case 'collection-open': {\n e.preventDefault();e.stopPropagation();\n if (!sd.collection.name || !sd.collection.id) { Host.toast('合集信息无法打开'); return; }\n const o = {};\n o['📖 ' + sd.collection.name] = `${API.COLL}?product=lofter-android-8.0.2&method=getCollectionDetail&offset={{(page-1)*45}}&limit=45&collectionid=${encodeURIComponent(sd.collection.id)}`;\n Host.open('sort', JSON.stringify(o));\n return;\n }\n }\n return;\n }\n\n const link = e.target.closest('a[href]');\n if (!link) return;\n const h = link.getAttribute('href');\n if (!h || !\/lofter\\.com\/.test(h)) return;\n try {\n if (routeLink(h, link.getAttribute('title'))) {\n e.preventDefault(); e.stopPropagation();\n }\n } catch (err) { Host.toast('链接处理失败'); }\n\n }, true);\n\n \/* ===== 导入合集到书架 ===== *\/\n function importCol(n, id, bid) {\n const body = `targetblogid=${encodeURIComponent(bid)}&method=getCollectionSimple&offset=0&limit=800&blogid=${encodeURIComponent(bid)}&collectionid=${encodeURIComponent(id)}&order=1`;\n const src = `${API.COLL}?product=lofter-android-7.4.4,` + JSON.stringify({ method: 'POST', body });\n const url = `legado:\/\/import\/addToBookshelf?src=${encodeURIComponent(src)},{\"origin\":\"📖Lofter\"}`;\n try { window.location.href = url; } \n catch (e) { try { Host.openUrl(url); } catch (err) { Host.toast('导入失败'); }\n }}\n\n \/* ===== LOFTER 朗读 ===== *\/\nfunction ttsVideoPlay() {\n const p = val('postId'), b = val('blogId'), c = val('collection-id');\n if (!p || !b || !c) return Host.toast('非合集文章');\n try {\n const info = JSON.parse(Host.ajax(`${API.AUDIO}info.json?postId=${p}&blogId=${b}&collectionId=${c}`)); \n if (info.code) return Host.toast(`code=${info.code} ${info.msg || ''}`);\n const it = (info.data?.audioPost?.toneInfos || []).find(t => t && t.unlocked); \n if (!it) return Host.toast('无音轨');\n java.openVideoPlayer(it.audioUrl, info.data.post.title || 'LOFTER 朗读', true);\n } catch (e) { Host.toast(e.message); }\n}\n\n\/* ===== LOFTER 听书 ===== *\/\nvar _ttsPanel, _ttsAudio, _ttsActive, _ttsCues = [], \n _ttsTimer, _ttsEnded, _ttsPid, _ttsFill, _ttsTime,\n _ttsHls, _ttsDocBound;\n\nfunction ttsPlay() {\n if (_ttsPanel) return;\n var p = val('postId'), b = val('blogId'), c = val('collection-id');\n if (!p || !b || !c) return Host.toast('非合集');\n\n \/\/ 命中缓存 \n var cached;\n try { cached = JSON.parse(sessionStorage.getItem('ttsAuto') || 'null'); } catch (e) {}\n if (cached && cached.url && String(cached.pid) === String(p)) {\n try { sessionStorage.removeItem('ttsAuto'); } catch (e) {}\n _ttsPid = String(p); build(cached.url, cached.sub); return;\n }\n try { sessionStorage.removeItem('ttsAuto'); } catch (e) {}\n\n \/\/ 无缓存:拉接口 \n var json;\n try { json = JSON.parse(Host.ajax(`${API.AUDIO}info.json?postId=${p}&blogId=${b}&collectionId=${c}`)); }\n catch (e) { return Host.toast('请求失败'); }\n if (json.code) return Host.toast('code=' + json.code);\n var post = json.data.post, info = (json.data.audioPost.toneInfos || []).find(function (t) { return t && t.unlocked; }); \n if (!post || !info) return Host.toast('无音轨'); \n _ttsPid = String(post.postId); build(info.audioUrl, info.subtitleUrl);\n}\n\nfunction build(audioUrl, subtitleUrl) {\n _ttsPanel = document.createElement('div');\n _ttsPanel.className = 'tts-cap';\n _ttsPanel.innerHTML = '<button class=\"c-pr\"><i class=\"fa fa-skip-previous\"><\/i><\/button><button class=\"c-pp\"><i class=\"fa fa-pause-circle\"><\/i><\/button><button class=\"c-nx\"><i class=\"fa fa-skip-next\"><\/i><\/button><div class=\"c-p\"><i><\/i><\/div><span class=\"c-m\">0:00<\/span><button class=\"c-x\"><i class=\"fa fa-times\"><\/i><\/button>';\n document.body.appendChild(_ttsPanel);\n \/\/ 折叠收纳\n_ttsPanel.addEventListener('click', function () {\n if (_ttsPanel.classList.contains('collapsed')) _ttsPanel.classList.remove('collapsed');}, true); \nif (!_ttsDocBound) {_ttsDocBound = 1;\n document.addEventListener('click', function (e) {\n if (_ttsPanel && !_ttsPanel.contains(e.target)) _ttsPanel.classList.add('collapsed');\n }, true);}\n \n _ttsAudio = document.createElement('audio');\n _ttsAudio.className = 'tts-v';\n _ttsAudio.setAttribute('playsinline', '');\n document.body.appendChild(_ttsAudio);\n \/\/ if ('mediaSession' in navigator) navigator.mediaSession.metadata = new MediaMetadata({ title: val('article-title'), artist: val('author-name') }); \n \/\/ document.body.style.paddingTop = '60px';\n var playBtn = _ttsPanel.querySelector('.c-pp');\n var progressBar = _ttsPanel.querySelector('.c-p');\n _ttsFill = _ttsPanel.querySelector('.c-p i');\n _ttsTime = _ttsPanel.querySelector('.c-m');\n ttsLoad(audioUrl, subtitleUrl);\n playBtn.onclick = function (e) { e.stopPropagation(); _ttsAudio.paused ? _ttsAudio.play() : _ttsAudio.pause(); }; \n _ttsPanel.querySelector('.c-pr').onclick = function (e) { e.stopPropagation(); \/*ttsJump('pre');*\/ ttsSeekCue(-1); }; \n _ttsPanel.querySelector('.c-nx').onclick = function (e) { e.stopPropagation(); \/*ttsJump('next');*\/ ttsSeekCue(1); }; \n _ttsPanel.querySelector('.c-x').onclick = function (e) { e.stopPropagation(); ttsStop(); }; \n _ttsAudio.onplay = function () { playBtn.innerHTML = '<i class=\"fa fa-pause-circle\"><\/i>'; };\n _ttsAudio.onpause = function () { playBtn.innerHTML = '<i class=\"fa fa-play-circle\"><\/i>'; };\n _ttsAudio.onended = function () { if (!_ttsEnded) { _ttsEnded = true; ttsJump('next'); } }; \n progressBar.onclick = function (e) {\n var rect = progressBar.getBoundingClientRect();\n if (_ttsAudio.duration) _ttsAudio.currentTime = (e.clientX - rect.left) \/ rect.width * _ttsAudio.duration; \n };\n _ttsEnded = false; _ttsTimer = setInterval(ttsSync, 200);\n}\n\nfunction ttsLoad(audioUrl, subtitleUrl) {\n if (_ttsHls) { try { _ttsHls.destroy(); } catch (e) {} _ttsHls = null; }\n _ttsCues = [];\n if (_ttsActive) { _ttsActive.classList.remove('tts-hl'); _ttsActive = null; }\n if (!window.Hls) return Host.toast('插件未就绪');\n _ttsHls = new Hls(); \n _ttsHls.loadSource(audioUrl);\n _ttsHls.attachMedia(_ttsAudio);\n _ttsHls.on(Hls.Events.MANIFEST_PARSED, function () { _ttsAudio.play().catch(function(){}); }); \n if (subtitleUrl) {\n try { _ttsCues = ttsSub(String(Host.ajax(subtitleUrl) || '')); }\n catch (e) { _ttsCues = []; }\n }\n}\n\nfunction ttsSync() {\n if (!_ttsAudio) return;\n var time = _ttsAudio.currentTime || 0, duration = _ttsAudio.duration || 0;\n if (duration) {\n _ttsFill.style.width = time \/ duration * 100 + '%';\n _ttsTime.textContent = (time \/ 60 | 0) + ':' + ('0' + (time % 60 | 0)).slice(-2); \n if (!_ttsEnded && time >= duration - 0.8) { _ttsEnded = true; ttsJump('next'); return; }\n }\n var current;\n for (var i = 0; i < _ttsCues.length; i++)\n if (time >= _ttsCues[i].start && time <= _ttsCues[i].end) { current = _ttsCues[i]; break; }\n var paragraph = current && current.paragraph;\n if (paragraph === _ttsActive) return;\n if (_ttsActive) _ttsActive.classList.remove('tts-hl');\n if (paragraph) {\n paragraph.classList.add('tts-hl');\n var rect = paragraph.getBoundingClientRect();\n if (rect.top < 100 || rect.bottom > innerHeight - 100)\n scrollTo({ top: pageYOffset + rect.top - innerHeight * 0.3, behavior: 'smooth' }); \n }\n _ttsActive = paragraph;\n}\n\nfunction ttsSub(raw) {\n var text = String(raw || '').trim();\n if (!text) return [];\n var list = [];\n if (text.charAt(0) === '{' || text.charAt(0) === '[') {\n try {\n var json = JSON.parse(text);\n var arr = Array.isArray(json) ? json : (json.list || json.cues || json.subtitles || json.data || []); \n list = arr.map(function (item) {\n if (item.time_begin != null)\n return { start: +item.time_begin \/ 1000, end: +item.time_end \/ 1000, text: String(item.text || '').trim() }; \n var a = item.startTime != null ? item.startTime : (item.start != null ? item.start : (item.from != null ? item.from : (item.st != null ? item.st : 0))); \n var b = item.endTime != null ? item.endTime : (item.end != null ? item.end : (item.to != null ? item.to : (item.et != null ? item.et : 0))); \n var k = (+a > 1e4 || +b > 1e4) ? 1000 : 1;\n return { start: +a \/ k, end: +b \/ k, text: String(item.text || item.content || '').trim() }; \n }).filter(function (item) { return item.text; });\n } catch (e) {}\n } else {\n var current, lines = text.split(\/\\r?\\n\/);\n for (var i = 0; i < lines.length; i++) {\n var m = lines[i].match(\/^(\\d+):(\\d+):(\\d+)[.,](\\d+)\\s*-->\\s*(\\d+):(\\d+):(\\d+)[.,](\\d+)\/); \n if (m) {\n if (current) list.push(current);\n current = { start: +m[1] * 3600 + +m[2] * 60 + +m[3] + +m[4] \/ 1e3,\n end: +m[5] * 3600 + +m[6] * 60 + +m[7] + +m[8] \/ 1e3, text: '' };\n } else if (current && lines[i].trim() && !\/^\\d+$\/.test(lines[i].trim()) && !\/^WEBVTT\/.test(lines[i])) { \n current.text += (current.text ? ' ' : '') + lines[i].trim();\n }\n }\n if (current) list.push(current);\n }\n if (!list.length) return list;\n var paragraphs = Array.from(document.querySelectorAll('.post-content p'))\n .filter(function (p) { return (p.textContent || '').trim(); });\n if (!paragraphs.length) return list;\n var normalize = function (s) { return (s || '').replace(\/[\\s\\p{P}\\p{S}ァ-ヶぁ-ん]\/gu, ''); }; \n var cursor = 0;\n\nvar texts = paragraphs.map(function (p) { return normalize(p.textContent); });\nlist.forEach(function (cue) {\n var target = normalize(cue.text), tlen = target.length;\n \/\/ if (tlen < 3) return;\n var best = -1, bestScore = 0;\n for (var i = 0; i < texts.length; i++) {\n var source = texts[i];\n if (!source) continue;\n if (source.indexOf(target) >= 0) {\n var score = tlen \/ source.length;\n if (score > bestScore) { bestScore = score; best = i; }\n if (score === 1) break;\n continue; }\n if (tlen < 10) continue;\n var len = Math.min(20, source.length, tlen), hits = 0;\n for (var j = 0; j < len; j++) if (source.charAt(j) === target.charAt(j)) hits++;\n var ratio = hits \/ tlen;\n if (ratio > bestScore && ratio > 0.5) { bestScore = ratio; best = i; }\n }\n if (best >= 0) cue.paragraph = paragraphs[best];\n});\n\n return list;\n}\n\nfunction ttsStop() {\n if (_ttsTimer) clearInterval(_ttsTimer);\n if (_ttsHls) { try { _ttsHls.destroy(); } catch (e) {} _ttsHls = null; }\n if (_ttsAudio) { try { _ttsAudio.pause(); } catch (e) {} _ttsAudio.remove(); } \n if (_ttsPanel) _ttsPanel.remove();\n if (_ttsActive) _ttsActive.classList.remove('tts-hl');\n _ttsPid = _ttsPanel = _ttsAudio = _ttsActive = _ttsTimer = _ttsFill = _ttsTime = null; \n _ttsCues = []; _ttsEnded = false;\n document.body.style.paddingTop = '';\n}\n\n\/\/ 切播 dir: -1\/+1 = 上\/下一段\nfunction ttsSeekCue(dir) {\n if (!_ttsAudio || !_ttsCues.length) return;\n var t = _ttsAudio.currentTime, i = 0;\n while (i < _ttsCues.length - 1 && t >= _ttsCues[i].end) i++;\n if (dir < 0) {\n _ttsAudio.currentTime = (i === 0 || t - _ttsCues[i].start > 1)\n ? _ttsCues[i].start\n : _ttsCues[i - 1].start;\n } else {\n _ttsAudio.currentTime = (i < _ttsCues.length - 1)\n ? _ttsCues[i + 1].start\n : (_ttsAudio.duration || t);\n }\n ttsSync();\n}\n\nfunction ttsJump(dir) {\n var p = _ttsPid || val('postId'), b = val('blogId'), c = val('collection-id');\n if (!p || !b || !c) return;\n var json;\n try { json = JSON.parse(Host.ajax(`${API.AUDIO}${dir}.json?postId=${p}&blogId=${b}&collectionId=${c}`)); }\n catch (e) { return; }\n var data = json.data || {}, post = data.post;\n var info = (data.audioPost && data.audioPost.toneInfos || []).find(function (t) { return t && t.unlocked; }); \n if (!post || String(post.postId) === String(p)) return Host.toast(dir === 'pre' ? '已是首篇' : '已是末篇'); \n if (!info) return Host.toast('无音轨'); \n try {\n sessionStorage.setItem('ttsAuto', JSON.stringify({\n url: info.audioUrl, sub: info.subtitleUrl, pid: String(post.postId)\n }));\n } catch (e) {}\n var domain = (data.blogInfo && data.blogInfo.blogName) || 'www';\n var targetBlogId = post.blogId || b;\n var body = `targetblogid=${targetBlogId}&blogdomain=${domain}.lofter.com&postid=${post.postId}`; \n var rssUrl = `${API.POST}?product=lofter-android-8.3.64,` +\n JSON.stringify({ method: 'POST', 'accept-encoding': 'gzip, deflate', body });\n Host.toast('即将播放...\\n' + (post.title || post.postId));\n java.open('rss', rssUrl, post.title || '');\n}\n\n(function () {\n try { if (!sessionStorage.getItem('ttsAuto')) return; } catch (e) { return; }\n if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', function () { setTimeout(ttsPlay, 300); }); } \n else { setTimeout(ttsPlay, 300); }\n})();\n\n \/* ===== 长按:标签 \/ 合集 \/ 作者 \/ 列表 \/ 听书 ===== *\/\n (function() {\n let timer = null, target = null, fired = false,\n startX = 0, startY = 0;\n function clear() { if (timer) { clearTimeout(timer);\n timer = null; } target = null; }\n\n document.addEventListener('touchstart', e => {\n const t = e.target.closest('.post-tags__item, .post-col__info, .post-author__name, .cb-card, .post-nav__item, .btn--tts'); \n if (!t) return;\n target = t; fired = false;\n startX = e.touches[0].clientX;\n startY = e.touches[0].clientY;\n timer = setTimeout(() => {\n timer = null; fired = true;\n if (t.classList.contains('post-tags__item')) {\n addTag(t.textContent.trim());\n } else if (t.classList.contains('post-col__info')) {\n const c = shared().collection;\n if (c.name && c.id && c.blogId) importCol(c.name, c.id, c.blogId);\n } else if (t.classList.contains('post-nav__item')) {\n if (t.classList.contains('post-nav__item--disabled')) return;\n PostNav.openToc();\n } else if (t.classList.contains('cb-card')) {\n addCol(t.dataset.collectionName, t.dataset.collectionId);\n } else if (t.classList.contains('btn--tts')) {\n ttsVideoPlay();\n } else {\n const a = shared().author;\n if (a && a.blogId) AuthorDialog.open(a.blogId);\n else Host.toast('作者信息缺失');\n }\n Host.vibrate(VIBRATE_MS);\n }, LONG_PRESS_MS);\n }, { passive: true });\n\n document.addEventListener('touchend', e => {\n if (fired) {\n \/*e.preventDefault(); e.stopPropagation();*\/\n fired = false;\n }\n clear();\n });\n\n document.addEventListener('touchmove', e => {\n if (!timer || !target) return;\n const dx = e.touches[0].clientX - startX;\n const dy = e.touches[0].clientY - startY;\n if (Math.abs(dx) > LONG_PRESS_SLOP || Math.abs(dy) > LONG_PRESS_SLOP) clear();\n }, { passive: true });\n\n document.addEventListener('touchcancel', clear);\n })();\n\n \/* ===== 上\/下一篇(仅合集) ===== *\/\n const PostNav = (function() {\n const LIMIT = 800, LEN = 40; \/\/ 目录长度 \/ 标题长度\n const NAV_TTL = 21600; \/\/ 6h \/ 0不过期\n\n const clip = s => {\n s = String(s || '').replace(\/<[^<>]*>\/g, '').replace(\/\\s+\/g, ' ').trim() || '无题';\n const a = Array.from(s);\n return a.length > LEN ? a.slice(0, LEN).join('') + '…' : s;\n };\n\n const dom = () => (String(val('article-url')).match(\/^https?:\\\/\\\/([^.]+)\\.lofter\\.com\/i) || [, ''])[1];\n\n const hex = (b, p) => { try { return `${BigInt(b).toString(16)}_${BigInt(p).toString(16)}`; } catch (e) { return ''; } };\n\n \/* 精简结构:[{id, blogId, title}] *\/\n function slimNav(list) {\n const out = [];\n for (let i = 0; i < list.length; i++) {\n const x = list[i];\n const p = x && x.post ? x.post : x;\n const clean = s => String(s).replace(\/<[^<>]*>\/g, '').replace(\/\\s+\/g, ' ').trim();\n if (!p || p.id == null || p.blogId == null) continue;\n out.push({ id: String(p.id), blogId: String(p.blogId), title: clean(p.title || p.digest || '') });\n }\n return out;\n }\n\n async function fetch_(colId) {\n \/\/ localStorage.clear(); \/\/ 清除本地缓存\n const key = 'lofnav:' + colId;\n try {\n const raw = localStorage.getItem(key);\n if (raw) {\n const obj = JSON.parse(raw);\n if (obj && obj.t && Date.now() - obj.t < NAV_TTL * 1000 && Array.isArray(obj.d) && obj.d.length) { \n return obj.d;\n }\n }\n } catch (e) {}\n const d = await ajaxJson(`${API.COLL}?product=lofter-android-8.0.2&method=getCollectionDetail&offset=0&limit=${LIMIT}&order=1&collectionid=${encodeURIComponent(colId)}`);\n const list = slimNav((d && d.response && (d.response.items || d.response.posts)) || []); \n if (list.length) {\n try { localStorage.setItem(key, JSON.stringify({ t: Date.now(), d: list })); } catch (e) { Host.toast('目录缓存出错'); } \n }\n return list;\n }\n\n const row = (x, d, label, empty) => {\n if (!x || !x.id || !x.blogId) return `<span class=\"post-nav__item post-nav__item--disabled\"><span class=\"post-nav__label\">${label}<\/span><span class=\"post-nav__title\">${empty}<\/span><\/span>`;\n const h = hex(x.blogId, x.id),\n t = clip(x.title);\n if (!h || !d) return '';\n return `<a class=\"post-nav__item\" href=\"https:\/\/${d}.lofter.com\/post\/${h}\" title=\"${esc(t)}\"><span class=\"post-nav__label\">${label}<\/span><span class=\"post-nav__title\">${esc(t)}<\/span><\/a>`;\n };\n\n async function load() {\n const cid = val('collection-id'),\n pid = val('postId'),\n d = dom();\n const anchor = document.querySelector('.comments') || document.querySelector('.post-footer'); \n if (!cid || !pid || !d || !anchor) return; \/\/ 非合集文章直接退出\n\n let all;\n try { all = await fetch_(cid); } catch (e) { return; }\n if (!all.length) return;\n\n const i = all.findIndex(x => x && String(x.id) === String(pid));\n if (i === -1) return;\n\n const html = row(all[i - 1], d, '← 上一篇', '已是首篇') +\n row(all[i + 1], d, '下一篇 →', '已是末篇');\n if (!html.includes('<a ')) return;\n\n document.querySelectorAll('.post-nav').forEach(n => n.remove());\n const box = document.createElement('nav');\n box.className = 'post-nav';\n box.innerHTML = html;\n anchor.parentNode.insertBefore(box, anchor);\n }\n\n \/* ===== 目录抽屉 ===== *\/\n let tocMask, tocList, tocTitle, tocMeta;\n\n function tocBuild() {\n if (tocMask) return;\n tocMask = document.createElement('div');\n tocMask.className = 'toc-mask';\n tocMask.innerHTML = `\n <div class=\"toc\">\n <div class=\"toc-head\">\n <div class=\"toc-head__main\">\n <h3 class=\"toc-title\"><\/h3>\n <span class=\"toc-meta\"><\/span>\n <\/div>\n <button class=\"toc-x\" type=\"button\"><i class=\"fa fa-times\"><\/i><\/button>\n <\/div>\n <div class=\"toc-list\"><\/div>\n <\/div>`;\n tocTitle = tocMask.querySelector('.toc-title');\n tocMeta = tocMask.querySelector('.toc-meta');\n tocList = tocMask.querySelector('.toc-list');\n tocMask.addEventListener('click', e => { if (e.target === tocMask) tocClose(); });\n tocMask.addEventListener('touchmove', e => {\n if (!e.target.closest('.toc') && e.cancelable) e.preventDefault(); \n }, { passive: false });\n tocMask.querySelector('.toc-x').addEventListener('click', tocClose); \n document.body.appendChild(tocMask);\n }\n\n function tocClose() {\n if (tocMask) tocMask.classList.remove('open');\n unlockBodyScroll();\n }\n async function openToc() {\n const cid = val('collection-id');\n if (!cid) return Host.toast('非合集作品');\n tocBuild();\n tocTitle.textContent = val('collection-name') || '目录';\n tocMeta.textContent = '';\n tocList.innerHTML = '<div class=\"toc-status\">加载中...<\/div>';\n tocMask.classList.add('open');\n lockBodyScroll();\n let all;\n try { all = await fetch_(cid); }\n catch (e) { tocList.innerHTML = '<div class=\"toc-status\">加载失败<\/div>'; unlockBodyScroll(); return; }\n if (!all.length) { tocList.innerHTML = '<div class=\"toc-status\">暂无目录<\/div>'; unlockBodyScroll(); return; }\n const curId = String(val('postId')),\n d = dom();\n const cur = all.findIndex(x => String(x.id) === curId);\n tocMeta.textContent = (cur >= 0 ? (cur + 1) + ' \/ ' : '') + all.length + ' 篇';\n tocList.innerHTML = all.map((x, i) => {\n const h = hex(x.blogId, x.id);\n if (!h || !d) return '';\n const t = esc(clip(x.title));\n const a = String(x.id) === curId ? ' active' : '';\n return `<a class=\"toc-item${a}\" href=\"https:\/\/${d}.lofter.com\/post\/${h}\" title=\"${t}\"><span class=\"toc-idx\">${i + 1}<\/span><span class=\"toc-name\">${t}<\/span><\/a>`;\n }).join('');\n const curEl = tocList.querySelector('.toc-item.active');\n if (curEl) setTimeout(() => curEl.scrollIntoView({ block: 'center' }), 60);\n }\n\n return { load, openToc };\n })();\n\n \/* ===== 阅读历史(延时写入) ===== *\/\n function recordHistory() {\n try {\n var url = val('article-url');\n if (!url) return;\n var title = val('article-title') || (`『${val('author-name')}』的作品`);\n var cid = val('collection-id');\n var cname = val('collection-name');\n var list = [];\n try {\n var raw = source.get('lofHistory');\n if (raw && raw.trim()) {\n var parsed = JSON.parse(raw);\n if (Array.isArray(parsed)) list = parsed.filter(function(x) { return x && x.url; });\n }\n } catch (e) {}\n var rec = { title: title, url: url, time: Date.now() };\n if (cid) { rec.collectionId = cid;\n rec.collectionName = cname; }\n var seen = {};\n list = list.filter(function(x) {\n if (seen[x.url]) return false;\n if (String(x.url) === url) return false;\n if (cid && String(x.collectionId || '') === cid) return false; \/\/ 合集去重复\n seen[x.url] = 1;\n return true;\n });\n list.unshift(rec);\n if (list.length > 800) list.length = 800;\n source.put('lofHistory', JSON.stringify(list));\n } catch (e) {}\n }\n\n \/* ===== 初始化与生命周期 ===== *\/\n function boot() {\n Viewer.scan();\n loadCommentCounts();\n loadInitialComments();\n initMoreComments();\n fillQuotes();\n fillEmptyReplies(val('postId'), val('blogId'), false);\n PostNav.load();\n setTimeout(recordHistory, 600);\n }\n\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', boot);\n } else {\n boot();\n }\n\n \/* SPA 场景:DOM 变动后去抖重扫 *\/\n if (typeof MutationObserver !== 'undefined') {\n let _moTimer = null;\n const mo = new MutationObserver(() => {\n if (_moTimer) return;\n _moTimer = setTimeout(() => {\n _moTimer = null;\n resetPidMap();\n \/\/_sharedCache = null;\n \/\/_sharedSig = '';\n Viewer.scan();\n initMoreComments();\n fillQuotes();\n fillEmptyReplies(val('postId'), val('blogId'), false);\n }, 300);\n });\n \/*mo.observe(document.body, { childList:true, subtree:true });*\/\n mo.observe(document.documentElement, { childList: true, subtree: true });\n }\n\n})();",
"jsLib": "function logoData () { return `<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"32\" height=\"32\" viewBox=\"0 0 64 64\"><path fill=\"#666\" d=\"M22.25 17.42V25h-4.5v-8.28a3.85 3.85 0 0 1 4.15-3.86 11.18 11.18 0 0 1 4 1q1.07.5 2 1.23a9 9 0 0 1 1.95 2.2c.97 1.55 1.4 3.24 1.4 5.7h-4.5c0-1.85-.3-2.66-.72-3.32a4 4 0 0 0-.95-1.07q-.45-.36-1.07-.65a7 7 0 0 0-1.76-.53m4.5 5.57v7.51a2.25 2.25 0 0 0 4.5 0v-7.51zM41.57 46.75a7 7 0 0 0-.54-1.73q-.3-.62-.67-1.07-.49-.59-1.12-.97c-.7-.42-1.56-.73-3.5-.73H29v-4.5h6.74c2.52 0 4.24.42 5.82 1.38a9 9 0 0 1 2.27 1.95q.75.91 1.27 2a11 11 0 0 1 1.04 4.02 3.85 3.85 0 0 1-3.86 4.15H36.5v-4.5z\"\/><path fill=\"#666\" d=\"M29.15 37.75H35v4.5h-5.85q-1.24 0-2.28-.11A11 11 0 0 1 22.95 41a9 9 0 0 1-3.88-3.74c-.91-1.65-1.32-3.43-1.32-6.07V25h4.5v6.19c0 2.11.32 3.08.76 3.88A4.7 4.7 0 0 0 25 37c.62.31 1.3.55 2.36.66q.73.09 1.78.09M36 46.75h4v4.5h-4q-1.51 0-2.71-.2a9 9 0 0 1-2.79-.93 8 8 0 0 1-2.1-1.54 2.25 2.25 0 0 1 3.2-3.16q.4.4.98.71c.41.22.85.38 1.47.48.49.09 1.1.14 1.94.14\"\/><\/svg>`; }\nfunction emptyImg() { return 'data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAAEAQMAAACa8ftuAAAAA1BMVEWIiIhYZW6zAAAAAXRSTlMER4scfwAAANdJREFUeAEBzAAz\/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADMAAF8TdH\/AAAAAElFTkSuQmCC'; }\nfunction IconFontData() { return \"data:font\/woff2;base64,d09GMgABAAAAABCMAAoAAAAAGtQAABA+AAQBywAAAAAAAAAAAAAAAAAAAAAAAAAABmAAggQKqVChBwE2AiQDgQALQgAEIAWFEgcgG5YVUZSRVhrIj4V4o8v+gU6lbE6bjZBk9gd+m71PfhN0YCAqUTaTmCIKgqCClcyZickc3ok5jEW56NDVXbtdLKp1lQUoOK8uffngg1UGEBQQLk+L3WxysiFxKyjC9Gf0v9ZKu32h2RALs+RyQgelfN0d+N290Meze8cKaGZDc2HwUz7lU1GRuxuaDU6AVU4FWJsII6LO1hIL4NRY7SUKLPC9RTxnhIWOVwAcgYfMBjwAbhAnAoDe49YwjQBIP1y7kME1tjZkNVPelowvIsL3N7CauBpxvC5Ful4G8GgaS88Be8DBRyXrsIlW+w1vh9zWPEQeMh4aHuY\/HXl6\/MXhF8df4l+eGsONEcccx9hjgWO5YxVj5rHpY3PGVozjxznjwvFFP34AjCxq697b6PAv0fu096j3cRrO\/ZH7DvfN1ARqnHOqc4KzjVMU2Z5sS8aTxkmPSIdIO0jLSXNJfaSfSK6OnxxfOk4HBC7DqJkyoXUILStsihbhyhhAHuwSDlbYrvBcwNIKSMuwXGSbR6SHJ0wgk0\/r4jyez8PTf6KAxWEwPHNtadgAHM7OzhN9KfT5Jl62ni4oh4O6o26etjZkKdXLyZVIlKM0OtVVQEnF1Q0ojiP4johjtv3yEcBZwo\/amd+nc17ATFgeejiscRjpkDUcCTlEbyxDnNJRl4Fb\/SN9hmvx2VyU7bcVIXb4bnEEm2gAIwjAhg\/hgmg8u4r6w2HDbPwa2ZEQ4yGwhA4fc7Dcq18g0coclFRjeDV1MRdtBHgGO6jrTxQ\/+YyTDsCSZg7gGYZhZ8L756JBwqS6EljfL5TxdV9\/Gnb7QOjsbcd6fyfmbRsQ6VEmFwJYF+eewXPTg0eb\/T+xlWIhJTN8SWlR2SNxcmtuwizCnrisaqfWcD\/3XUppy8qfeTEZOpmZ41lkDnPKx+Lw8pm4uF7MQUFRY3F0n8lnkRQ+AyWZ\/OVkGhkzimSoxtU0tXI8jaykXCxBqEkIuRGjOCkldPZT3A4drfU0Qmu1UrsIKVBM0ztJVIeJoE4k6rjAZWGJWfBMB+ejuIXVOymUWJ6IBQeJYzXErRbnVvqXF07WhzPZAns3pCkiPhO1ekTG6Gr1jio1v3YpY6RgVJKw35nG6Fda9kvRJ1YhKammo12Uup0uxZygEfINItKdDwEGvuDQ0ZlasubKk6\/ZXvocaHJK0cKd7VMEkFhe\/DZ58te5xFH8mrZNEeNyNmXMGhu+MHxs9oKsFWbSKj4rfe7I7iV1++Zt2VSzLGK8PTl3derMkFE5hr0YtvJoRnx9ViBFDE0lviwPBI2wRgaPlEiQlCr0xtMS52fOnr1e41ELmBPL9RZApxpgrCrT9KicIbzG8Jqf0m3XQphj2FpDR66t1DeHYIayYTD8GReWXAkN++Wqi0V9EwAsfeU8lEnsEQINUop4NY0xYmBEHn1Fg8pa5LYAIlZdEhBqrfLpRw38fYBAfl5FBvw2UDMeN7Gg010PXFZdl2nyngbDbVOt\/zYHYilP54w39kJoR+vYExd0Dz418wQjjA\/jNcbvTkcTSUBmBwuQ265UcvWCT3zes5AaIoUJf+Ht3VirfdgAWOgJs4Q5hDMF1ZWBdPC3+X9FgPLXgf+bg9L4vGtXVA7JsOdfv5ZDM525V69bnJ5QuXKV5SFjNiIjFczcZGAjshinZtL\/Z8D3Fz8C\/yVMvumamouv6q3\/O\/0t8It8Ayx09fZgw1ZcAD+c1WcO4S8Nr\/3KkMqvwB9DzX7mzCyuB36WuhVJ19C6a1eq9KNmlTQYzG5cnQqZTZ7IHNHle55Fb\/L\/1+JzgDVzxsrlApQl1l1yWcI0uTLcF8rUIpKj11++VKEW01y99qLb1jBc6rouV8qFOFusucA8+WyY+j3IBlpAUlWsbEEWIJRtmvUQ2kaNhtxPHR85LmJWygxf\/pMpgfAWzQAzjbKsAmSj\/NLDi7RA1UVjACCZdi2k8zcDS4jhugNVAhbOECHKhshYIz3H1KaeberlUOUVHEjXg+W2cHD6f1mCqxHnYficSDiqZpz6SVJqDMsOh\/ILlm\/u8kJvf34dW77U\/9uIK0F81LEQe\/T1ayp4PNQZeZX5skz0RVJyDO8gXhGTO5uFQmducDSDntLmrF0e1AN9qo1Zg6xGA8aQom6F0t07AK8ILOl1YCv940HDEtwwwuGgo4UmkQsI\/88n02BoNT3VdJKIBBuaiC\/XHTQ14Q6JFegkNfJdPnWB1GwOD9\/81wtNvTETb\/omuC2SaeexdnAUmIEO\/jl3xozQs5a\/FvD4\/Lv2Ig7THwjyb+XxPr76K679bwu1ZzSLfrULLKoycf+y8jhWK9fEa8Rp\/r94QH+H5cHagoIkCFVbm0uoPnXPKr0std475cTI8fGdAiF47s75WXNTU+dmzc9dMMB3m5+xUz2nlKZGdmRQnjtrh7nsRMLs2QknylB05lRNk0bTrDHRNV8wlK+mwx6xV7QOkeNZdhC8dVk4CbjNMRyKBdoFtl0lyoKIqruzN\/gb\/f81F5pVwM+4YXb13byI0Gfo0m7WasKiw6SLM9q5V\/5oKS5Kb1d6+xqsBt2G+n0O7zbN1oY8KtOJ1C0Z1zfWzq0ZuNfwvDvkpj29WsCLdk4VKNwmfno7WO3\/88nW5TnByErbGhFNLTo\/M5E1QFCHYlsrIE9YoVpEcKS89iejafHBbAjB\/em+eKj0Wqev8hmH0oComnfsv\/WHmP+pwsCn+o3W+QT6qFE\/Kt9QAX4e8YFbO\/ZOR1SduGcslW9n6bUNi137VdWaYcPJ\/7z\/zE\/ht3iYR5atYt\/Ld58SoTf4TJR57Cra3jDZxCwfpX66Rs69OF3PQiay6VEN6ua5w1Ua0JJRE5KvyFUKKrnF3h0MCzqXmK9VRKuKkeSNWFqTuEHZ2EqvcU0KKZtCyMEkxvM7ZLIombbbZM7JzsywIOR9o7uC\/Sbp\/qSa6y+K4HdscgmRVOg8UoVTDG4kxsUgqu+I1oTVPXtcL07xe3ppBV41OR8i7zmtWG5cVruQX+fbuHVTpY2ObUpwD5jOHGDXZ7MqKlhZRvYAc3oALaGZrbep7F9czatgzy6eX75wnmZGWfnZrZOlNycbuK0ck88HfbG1bwElblq8s965n6KnxFnjKNn91hL9B58WzjSuYfI\/XKlh0\/+bm4cp8RT90dT0zEE3nWLIN1WeSTWzp3KmHTli5kxlmymZYSm+Q0qd+0CmJv2aPn3eAtJm53jc9ePXjqMqQuj9JTfnRWGUmAtDZ5dhJyEFtCnxz+4fd45Jw71fUrXExEUEyFbTJiN+HP9+x7LdS+nYwowJ77ZXJ3569Hj0xc3k74Lv52aXFiU7KBfZpFeu+0b5uzhub7fO4N9LdR+D72s3YLB54YyG883nH6e6WYcyzXz6CXxiAj4eT0jsI8THExII+Ph\/Q6nKs7\/txeav6VayXFUspdpt2Zu1ysMDM\/b0VevmRlK+mON5oManR9aEK8oVkuoH9F4PokcPXePo0Uw\/4Vni4ajxbMY8LZ73cySKJCR6IoOzuCTS+Zn5l5OvJduKcL3He7\/hhVGR2XLlL+11fsd\/yyQ0DXFbzHOPpR6dD4li8t9e1FEhpmB6VxHyXzHS04UUCEmj8F\/1iI8UqiPVyiKsAXfxfTauFKtSRkYihf7Pl6eiNukluQZ7bCaaPgfJRrHpaFoampuLpruqH+OjE1B\/O2EsSogNWVNWRoyNRYWEzwjS7k1zf9KZorUturZ5o9zzfvxEduA57ihnby37Up\/YLJp6+LAp+Cexs1k8NfjIYeFUibnvEjvhV2529rlAdiLf73yzebFt2hZTdJtu++rE0cRV8s6Qhh1awNjsmeC1ibGFsckrwXMzY4G2QV4YllcyqSz0dcMDB0CNhvmFuuQkqzVpjUDeOVkHoassYqe67IVsfW5YUeAM9fVvH25HWHxrYxIVItGNCc9G3EYjr9+4f782E5Kgr6ZtaVZGloqoSScPBSzSVjdFxXW2V38ozoQLCLBLxp6sKdx+BCGZ2Zw0PH36sFLcdb3DWACU2FilA8uhnss5glPtDxoIhC2msF5fqdNJpJKM9D3m67NwtSMjtTivCwtO3muOmnXdvCcjAxQ\/4ETlggXnZXalGGEoBSbBiZMpPYwTh4hEI7mHTjAkHsT8bnGNsS9jAd6GVWbPdxcGHdq4oU8lv2uMK6RgulZn9IyPWbNW1f2VMWdO+miEceT1eK9h\/fxPEvFl\/poURCFr9Tz9D7015IABB7KE\/oKQs\/rhFVrpckXN8lLaFZtFIrFeL7a8SPSlK6BgObRSWrzPX\/B2UI5ddSKnqyvn1aJsY6zSsQCl3O24sFh8Un30mjopCVSnXqi92LdPq7aljW3ZvGXbjg1LrgpKLpbgIlWqSNxk5SPKEmx5gvepMOWYksgQlcejUzrWJjENJUxW+hZm0snXzCEnZj5zfSkT\/h6MXX0G8hNGNsYJ3+fDmWEH6d5Htv9jiU6ZNdh4OsGIWxCTTI8TSeWxan2TQaN0dHJ8kmZavRq2UlHYTY5ZkIhkFKCoPdoSK1X5qilTVMqXzt+n20mr6uq230\/mwaZ4TncXulHvR0Xdp3Zg3ad7RgINv9Ke3Wusrzf2su1X00QT6LNoKtTu4u5OOr1z90V7C8d9Fn2iEw2Lt7954fCRI4cv3ISUONriM2cWs3JqynpLRFADkDl\/rQAPGPlMc9LWuCiVLr5LXUOTMJY\/yB7OL8qBlR7SZkBtDDZZBmLbGawkSiUmnDURzqgkEtUZfMtZ\/JgVJcGeaSMYstMiUUMb+q1\/aFH\/F7TlGzrU72Sj30zol\/6F9ZdUE7IMRHmvVRYl0xzDMuv08DIQEYh8abUB9X1\/yH39MSIlKbwgh7FVNmlDMS0sNT0sKE7yXLpOUiFda3JF1siD9C++ZpH4L2p4UmJ4kFR\/bKm4PGSpZKGLPDlJXg0tE4tXZK+Tp6YNSVZKyk9erdNM80qfiVetlmokKy0hLCAsBmFv2NpJJmEkwZwol0kq4XIQeAQOOZU1HHAvjyR\/hyJExtTDt4VrAHrKj28\/KohAqAYAFDB6e\/7i4Xb4x0fiLkcxaqOURU6cDkaTRKYlTEn4qoY9hh0Hwg+4BVWICHDpIpJEBOpP0rc5Gv\/cbIFaggM42WpCvdoC07i1uh0YUsoBjHKgcQz+N8BTOwBxh+5Rk\/hZrifZwxBgAcHZAEAuwJZCwAVyUxhwhKYUFljQeSlOKoUHDqy7lMBJEWEN\/JVCwQ2pTNkABelM2YEvMvT+jiBHToAa6sAIrdAAFVAG5dAEDIiCAmiBEptFj9VCMbRK62AQwkSQQmA5Eqr3pQw904irEouVCH3IWCBooe6nmiqMiQtf1SyVslA31gzVUPBBlj8REiAN4npZBTJfpaFjGsiCpKVS2GSYinpTYZ6fAWIIhEnnpUJ3qSRGHisolLilpZQLNdXJty7nndXHUmuwCJpsUKXr0oamUOoPQRtrgAIoXqbG9aoKpYIuR9SHC6HZHFYrcF3k9z2hYRra0\/ezh7cpN1UZeTAIglxweYGmDrTrQskiXqh2j6n1840HARYuAAAA\"; }\n\n",
"lastUpdateTime": 1792126866589,
"loadWithBaseUrl": true,
"loginUi": "[\n {\n \"name\":\"LOFTER\",\n \"type\":\"button\",\n \"action\":\"loginWeb()\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.58\n }\n },\n {\n \"name\": \"HELP\",\n \"viewName\": \"'使用说明'\",\n \"type\": \"button\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.21\n },\n \"action\": \"showHelp()\"\n},\n {\n \"name\": \"登录令牌\",\n \"viewName\": \"'lofter-phone-login-auth'\",\n \"type\": \"password\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.58\n }\n },\n {\n \"name\": \"添加令牌\",\n \"type\": \"button\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.21\n },\n \"action\": \"addAuth()\"\n },\n {\n \"name\": \"type\",\n \"viewName\": \"'切换模式'\",\n \"type\": \"select\",\n \"chars\": [\"🔺 全部\",\"📃 文字\",\"🎨 图片\",\"📺 视频\"],\n \"default\": \"🔺 全部\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n },\n {\n \"name\": \"find\",\n \"viewName\": \"'显示分栏'\",\n \"type\": \"select\",\n \"chars\": [\"🔺 全部\",\"🏷️ 标签\",\"📖 合集\",\"👤 作者\"],\n \"default\": \"🔺 全部\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.45\n }\n },\n {\n \"name\": \"comments\",\n \"viewName\": \"'评论模式'\",\n \"type\": \"select\",\n \"chars\": [\"最新评论\",\"热门评论\",\"精简评论\",\"关闭评论\"],\n \"default\": \"精简评论\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 1\n }\n },\n {\n \t \"name\": \"标签名称\",\n \t \"viewName\": \"'标签名称'\",\n \t \"type\": \"text\",\n \t \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.6\n }\n \t},\n {\n \"name\": \"添加标签\",\n \"type\": \"button\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.2\n },\n \"action\": \"addTag()\"\n },\n {\n \t \"name\": \"合集名称\",\n \t \"viewName\": \"'合集名称'\",\n \t \"type\": \"text\",\n \t \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.25\n }\n \t},\n \t{\n \t \"name\": \"ID\",\n \t \"type\": \"text\",\n \t \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.25\n }\n \t},\n \t{\n \"name\": \"添加合集\",\n \"type\": \"button\",\n \"style\": {\n \"layout_flexGrow\": 1,\n \"layout_flexBasisPercent\": 0.2\n },\n \"action\": \"addCollection()\"\n }\n]\n",
"loginUrl": "\/\/ ===== 登录信息更新 =====\nfunction login() {\n updateInfo();\n \/\/ 如需清除登录信息可取消注释\n \/\/ source.removeLoginInfo();\n}\n\nfunction loginWeb() {\n var head = {}\n head[\"User-Agent\"] = \"Dalvik\/2.1.0\";\n var header = JSON.stringify(head);\n java.startBrowser('https:\/\/www.lofter.com\/front\/account\/login?theme=lofter#login,{\"headers\": ' + header + '}','')\n}\n\n\/\/ ===== 通用变量读写 =====\nfunction getVarConfig() {\n try {\n var raw = (typeof source !== 'undefined' && source.getVariable) ? source.getVariable() || '' : ''; \n return raw.trim() ? JSON.parse(raw) : {};\n } catch (e) {\n java.toast('变量格式错误,已重置');\n return {};\n }\n}\n\nfunction saveVarConfig(config) {\n if (!source || !source.setVariable) {\n java.toast('变量不可用');\n return false;\n }\n source.setVariable(JSON.stringify(config));\n return true;\n}\n\nfunction updateInfo() {\n try {\n var info = source && source.getLoginInfoMap ? source.getLoginInfoMap() : null;\n var config = getVarConfig();\n var changed = false;\n if (!config.lofterAuth) config.lofterAuth = '';\n var fieldConfigs = [\n { name: 'type', mapping: { '📃 文字': '1', '🎨 图片': '2', '📺 视频': '4', '🔺 全部': '' }, default: '' },\n { name: 'find', mapping: { '🏷️ 标签': '1', '📖 合集': '2', '👤 作者': '3', '🔺 全部': '' }, default: '' },\n { name: 'comments', mapping: { '最新评论': '0', '热门评论': '1', '精简评论': '', '关闭评论': '-1' }, default: '' }\n];\n fieldConfigs.forEach(function (fc) {\n var raw = info ? info.get(fc.name) : null;\n var value = mapValue(raw, fc.mapping, fc.default);\n if (config[fc.name] !== value) {\n config[fc.name] = value;\n changed = true;\n }\n });\n\n if (changed) saveVarConfig(config);\n } catch (e) {}\n}\n\nfunction mapValue(raw, mapping, defaultVal) {\n if (raw == null) return defaultVal;\n return mapping.hasOwnProperty(raw) ? mapping[raw] : defaultVal;\n}\n\n\/\/ ===== 标签 \/ 合集管理 =====\nfunction tryAddTo(key, value, existsFn) {\n var config = getVarConfig();\n if (!Array.isArray(config[key])) config[key] = [];\n if (config[key].some(existsFn)) return false;\n config[key].push(value);\n return saveVarConfig(config);\n}\n\nfunction tryDelTo(key, matchFn, notFoundMsg) {\n var config = getVarConfig();\n if (!Array.isArray(config[key]) || config[key].length === 0) {\n java.toast(notFoundMsg || '没有可删除的项目');\n return false;\n }\n var idx = config[key].findIndex(matchFn);\n if (idx === -1) {\n java.toast(notFoundMsg || '未找到匹配项');\n return false;\n }\n config[key].splice(idx, 1);\n return saveVarConfig(config);\n}\n\nfunction addTag() {\n let info = source.getLoginInfoMap();\n try {\n if (\"标签名称\" in result) info = result;\n } catch (e) {}\n\n let tag = String(info.get(\"标签名称\")).trim();\n if (!tag) { java.toast(\"请输入标签名称\"); return; }\n \n if (tryAddTo('tags', tag, function (t) { return t === tag; })) {\n java.toast('标签「' + tag + '」已添加');\n } else {\n java.toast('标签「' + tag + '」已存在');\n }\n}\n\nfunction addCollection() {\n let info = source.getLoginInfoMap();\n try {\n if (\"合集名称\" in result && \"ID\" in result) info = result;\n } catch (e) {}\n\n let name = String(info.get(\"合集名称\")).trim();\n let id = String(info.get(\"ID\")).trim();\n\n if (!name || !id) {\n java.toast(\"请输入合集名称和ID\");\n return;\n }\n if (!\/^\\d+$\/.test(id)) {\n java.toast(\"合集ID必须为数字\");\n return;\n }\n\n if (tryAddTo('collections', { name: name, id: id }, function (item) {\n return item.id === id;\n })) {\n java.toast('合集「' + name + '」已添加');\n } else {\n java.toast('合集「' + name + '」已存在');\n }\n}\n\n\/\/ ===== 登录令牌处理 =====\nfunction getAuthFromCookie() {\n try {\n var c = cookie.getCookie('https:\/\/www.lofter.com');\n var m = String(c).match(\/(?:^|;\\s*)LOFTER-PHONE-LOGIN-AUTH=([^;]+)\/i);\n if (m && m[1]) return m[1].trim();\n } catch (e) {\n java.toast('登录数据读取失败: ' + (e.message || e));\n }\n return '';\n}\n\nfunction addAuth() {\n var info = source.getLoginInfoMap();\n var token = '';\n \/\/ 优先从登录信息中获取令牌\n try {\n if (info && info.get('登录令牌')) {\n token = String(info.get('登录令牌')).trim();\n }\n } catch (e) {}\n \/\/ 尝试从 Cookie 获取\n if (!token) {\n token = getAuthFromCookie();\n }\n \/\/ 获取当前配置中的令牌\n var config = getVarConfig();\n var oldToken = config.lofterAuth;\n\n if (token) {\n config.lofterAuth = token;\n saveVarConfig(config);\n java.toast('登录令牌已保存');\n } else {\n if (oldToken && oldToken !== '') {\n java.toast('登录令牌获取失败,保留原令牌');\n } else {\n config.lofterAuth = '';\n saveVarConfig(config);\n java.toast('已保存空令牌');\n }\n }\n}\n\n\/\/ ======= 使用说明界面 =======\nfunction showHelp() {\n var style = `\n<style>:root{--a:#f2efe8;--b:#fcfaf5;--c:#ebe7de;--d:#5046371a;--e:#2c2823;--f:#5f5a52;--g:#948d82;--h:#09635d;--i:#09635d14}@media(prefers-color-scheme:dark){:root{--a:#1a1815;--b:#211e1a;--c:#2a2621;--d:#ffffff12;--e:#ddd6cb;--f:#a39b8f;--g:#756d62;--i:#2a9d8f1f}}*{box-sizing:border-box;-webkit-tap-highlight-color:#0000}html,body{margin:0;padding:0}body{background:var(--a);color:var(--e);font-family:-apple-system,BlinkMacSystemFont,Segoe UI,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Roboto,Helvetica Neue,Arial,sans-serif;-webkit-font-smoothing:antialiased;padding:0 0 32px}body,.st{font-size:15px}body,#h p,#n{line-height:1.65}#h,.c.o,.sn,.r .d,.gc h4:before{background:var(--h)}#h,.c.o{color:#fff}#h{padding:22px 16px 20px;border-radius:0 0 16px 16px}#hb,.c{padding:3px 10px;border-radius:999px}#hb{display:inline-block;font-size:11px;letter-spacing:.6px;background:#ffffff2e;color:#fffffff2;margin-bottom:8px}#h h1,.st{letter-spacing:.2px}#h h1,.st,.sn,.gc h4{font-weight:700}#h h1{font-size:21px;margin:0 0 6px}#h p,.st,.gc p{margin:0}#h p{font-size:13px;color:#ffffffd1}.s,#n{margin:10px 10px 0;border-radius:12px}.s,.c{border:1px solid var(--d)}.s{background:var(--b);padding:14px}.c,.gc{background:var(--c)}.sh,.dv,.gc h4{display:flex;align-items:center}.sh,.r,#g{gap:8px}.sh{margin-bottom:10px}.sn,.r .d{border-radius:50%;flex-shrink:0}.sn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;color:var(--b)}.r{display:flex;padding:6px 0;border-bottom:1px solid var(--d);font-size:14px}.r:last-child{border-bottom:0;padding-bottom:0}.r:first-of-type{padding-top:0}.r .d{width:5px;height:5px;margin-top:8px;opacity:.75}.r .t,.c,#n{color:var(--f)}.r .t b,.gc h4{color:var(--e)}#hb,.c.o,.r .t b,#n b,.dv,#shd{font-weight:600}.r .t b{background:linear-gradient(#0000 62%,#09635d38 62%)}#ts{display:flex;flex-wrap:wrap;gap:6px;margin:2px 0 10px}.sn,.c,#f,.dv,#sb{font-size:12px}.c.o{border-color:#0000}#g{display:grid;grid-template-columns:1fr 1fr;margin-top:4px}.gc{border-radius:8px;padding:10px 10px 9px}.gc h4{margin:0 0 3px;font-size:13px;gap:5px}.gc h4:before{content:\"\";display:inline-block;width:3px;height:12px;border-radius:2px}.gc p,#shd{font-size:12.5px}.gc p{line-height:1.55}#n{padding:12px 14px;background:var(--i);font-size:13px;word-break:break-all;text-align:justify;text-align-last:left}#n b{color:var(--h)}#f,#shd,.dv,#sb,.gc p{color:var(--g)}#f,#shd{letter-spacing:.3px}#f{text-align:center;margin-top:24px}.dv{gap:10px;margin:18px 14px 0;letter-spacing:1px}.dv:before,.dv:after{content:\"\";flex:1;height:1px;background:var(--d)}#sb{font-weight:400;margin-left:auto}#shd{margin-top:12px}<\/style>`;\n var html = `<!doctype html>\n<html lang=zh-CN>\n<meta charset=UTF-8><meta name=viewport content=\"width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no\">\n<head><title>使用说明<\/title><\/head>\n${style}\n<body>\n<div id=h>\n<span id=hb>LOFTER · 订阅源<\/span>\n<h1>使用说明<\/h1>\n<p>建议使用「阅读 Sigma」,其他版本可能不兼容<\/p>\n<p>首次使用请先按流程完成登录操作<\/p>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>01<\/span><h2 class=st>登录<\/h2>\n<span id=sb>首次使用<\/span>\n<\/div>\n<div class=r><span class=d><\/span><span class=t>点击 <b>LOFTER<\/b> 按钮,在打开的网页里登录<\/span><\/div>\n<div class=r><span class=d><\/span><span class=t>回到 <b>登录 UI<\/b>,点击 <b>添加令牌<\/b> 保存登录数据<\/span><\/div>\n<div class=r><span class=d><\/span><span class=t>登录令牌长期有效,之后无需重复登录<\/span><\/div>\n<div class=r><span class=d><\/span><span class=t>未登录也能浏览,但部分 <b>个性化内容<\/b> 无法显示<\/span>\n<\/div>\n<div id=shd>登录界面还能设置<\/div>\n<div id=g>\n<div class=gc><h4>内容类型<\/h4>\n<p>从最新到总榜列表只看文字、图片或全部<\/p><\/div>\n<div class=gc><h4>显示分类<\/h4>\n<p>默认启动时显示分页,以及旧版分类 URL<\/p><\/div>\n<div class=gc><h4>评论模式<\/h4>\n<p>阅读页显示最新评论、热门评论或精简评论<\/p><\/div>\n<div class=gc><h4>快捷添加<\/h4>\n<p>标签与合集,无需进入页面<\/p><\/div>\n<\/div>\n<\/div>\n<div class=dv>启 动 页<\/div>\n<div class=s>\n<div class=sh><span class=sn>02<\/span><h2 class=st>导航<\/h2><\/div>\n<div id=ts>\n<span class=\"c o\">全部<\/span>\n<span class=c>标签<\/span>\n<span class=c>合集<\/span>\n<span class=c>作者<\/span>\n<span class=c>文章<\/span>\n<span class=c>历史<\/span>\n<\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击可切换分页,也可以左右滑动切换<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t><b>查看按钮<\/b>:显示或隐藏被隐藏的内容<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t><b>管理按钮<\/b>:进入管理模式,可排序、改名、删除<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>03<\/span><h2 class=st>标签<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击标签,打开该标签下的最新、日榜、周榜等列表<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>管理模式下可点击切换到屏蔽词页面,配置要过滤的内容<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>04<\/span><h2 class=st>合集<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击合集,查看该合集内的文章列表<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击右上角刷新按钮,可单日更新一次基本数据<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>05<\/span><h2 class=st>作者<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击作者,查看该作者的全部作品<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>查看模式下点击作者头像,可移除头像<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>查看与管理模式同时打开时,可在<b>显示<\/b>、<b>隐藏<\/b>\n与<b>屏蔽<\/b>三种状态间切换<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>屏蔽后,该作者的作品不会再出现在列表<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>06<\/span><h2 class=st>文章<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击直接打开文章<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>管理模式下可修改标题或删除<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>07<\/span>\n<h2 class=st>历史<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>自动记录你最近看过的文章,不用手动保存<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>同一合集只保留最近阅读的那一篇<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>管理模式下可收藏文章,以及删除记录<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击右上角叉号可一键清空全部历史<\/span><\/div>\n<\/div>\n<div class=dv>阅 读 页<\/div>\n<div class=s><div class=sh>\n<span class=sn>08<\/span>\n<h2 class=st>文章内容<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>简单排版,阅读更舒适<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>多图可左右滑动,点击可查看或放大保存原图<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t><b>星号<\/b> 收藏到文章栏;<b>井号<\/b> 打开合集列表弹窗<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>彩蛋可通过点击右上角 <b>LOGO<\/b> 快速跳转 APP 端操作处理<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>09<\/span>\n<h2 class=st>长按操作<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>长按作者名,查看作者详情<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>长按合集名,可尝试导入书架追阅<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>长按标签名,可将其添加到标签页<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>10<\/span>\n<h2 class=st>段落评论<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>正文段落旁的小气泡,点开可看该段的划线评论<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击评论里的引用,跳回原文对应段落<\/span><\/div>\n<\/div>\n<div class=s>\n<div class=sh><span class=sn>11<\/span>\n<h2 class=st>评论区<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>支持展开回复、加载更多评论或回复<\/span><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>点击引用可跳回原文对应段落<\/span><\/div>\n<\/div>\n<div class=s><div class=sh>\n<span class=sn>12<\/span>\n<h2 class=st>合集文章<\/h2><\/div>\n<div class=r><span class=d><\/span>\n<span class=t>底部显示上一篇和下一篇,方便连续阅读<\/span><\/div>\n<\/div>\n<div id=n><b>常见问题<\/b><br>分类里没内容?在登录界面填好标签,或在启动页里添加标签后刷新。<br>提示未登录?回到登录界面,点击「添加令牌」。<br>添加令牌未成功?点击登录界面右上角「√」,再重新打开界面继续操作。<br>想清空所有配置?在订阅源的菜单里清空「设置源变量」、「清除 Cookie」即可重新开始。<\/div>\n<div id=f>LOFTER 订阅源 · 使用说明<\/div>\n<\/body>\n<\/html>`;\n var base64 = java.base64Encode(html);\n java.startBrowser(\"data:text\/html;base64,\" + base64, \"使用说明\");\n} \n",
"preload": true,
"preloadJs": "window.cuaiEmo = `data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAMAAAC5zwKfAAABj1BMVEUAAADPz8\/\/\/\/+WmJj\/\/\/\/\/\/\/\/\/\/\/\/\/\/\/+NjY3p6ur\/\/\/\/\/\/\/\/\/\/\/\/BwcHLy8ve39\/\/\/\/\/\/\/\/+jo6OwsLC2tra7u7vb29vw8PD9\/f3\/\/\/+QkJDGxsb4+Pj\/\/\/\/BwsLk5eX6+vr\/\/\/\/\/\/\/+bnJzX19fs7Oz6+vr\/\/\/\/R0tLR0dHa29vu7u75+fn+\/\/\/\/\/\/\/BwcHj4+Pa2tr29vb4+Pj4+Pj\/\/\/\/R0tLu7u7n5+f\/\/\/\/r+fgwMDDt+vkzMzPr+vk7OztiY2NAQEDy+\/o2Njb9\/v5GR0c4ODhbXFxSU1NPUFBWV1dERET+\/\/\/6\/v49PT1yc3NfYGBKSkp5eXn3\/fz29vaJiopMTU30\/Pzx8fHZ2tqgpKOGh4f7+\/vj7+\/s7e21vr2DhYV7fX1tbm7t\/Pvv+vrh7u3Y5OPc6ejV1dXEzs61tbWrrayRlJSBgoJoaGhlZ2fq+Pfn5+fd3d3N2Ne9vb2tsrHn9PPh4eHV4eDT397I0tLLzMu\/yMi8xsW3wcClp6dqamrk8fDOzs7GxsbR3NwmQuM0AAAAOXRSTlMAziP+GhMNBf2hHhYJ3tizLSf27+njvJBtN\/7agzH+tn5PRP7KpnRZ1MfDmHJjPOPgy4x8eWTyu7CU\/0KwAAAEaElEQVRYw+2X51MaQRiHA6gBFVEUFbsxmt7r3rt7vdMRCMXQ7L2nR035w4MkMyYT7rjL+CnD842ZnWfeW3bfd39XWrRo0aLFf83VNmf7Zfo6h0a9j6Y6L83XMVvgGD3\/ou+yhDf8QkiXhfRs9z8Kuts7nU5nZ1fH1frPtluqvFk8luSBZ1fty7ruPxvyOUa9Xu+oY9DzdLqv+8rdMsSyCKVh5\/njDpu26SFHv7uqSxzDMNGQxlb8Xt9YgAt9RCinA2gTU3Z0zrnb\/liCAIkm9DjLKhonAESVNAvuBZSNAVsF5eG4ZV3bnMOtEhLa2dr4+raYTaWy+2sTpxWVIaAdoIUtoq3tx0l+2OrJnb49kgBm9zSXQr+zsL\/JkTxCG9HoBkIu2B6zWN7jfgWY2EQW\/UWesEX0NUNcYupIgRFrFY5PpqOEPUqhv3mrChvobRyS2dw2A+wtpxXftZtVwrmKqBEzcJYtxoD9OJOBkNs3bsnXEwf1aKGhr6gIG6kyCaVZArD7MGjFd\/+mAvE11JgJQckVBBJlQEknSGXSQoPou84Ce4AM2AS1KhBCMq59cSJBtmebdrH22SQoOWTENwYASGKkvuJIElxjza7z3IyQ+IgMSbklKT5zIKI6xwzXP23uC45K8iYyIXWQy14c8xGiXzfdxm5PDCopZJniDhTGTE9MP5NZQzb4xnEPxk1usOcMXKId4UIZkkMmVy7AaDlkizUpNGo8W55sQ4G3J1yogHvYcJoN6tHPr8K2hOGSrPi6jb54QFZWeXvCyIrOBHqNxuMWlPewaEs4v1cB1w0D4VSSbC5SEVtCHp+S5BOjQxNnPmFq3o5PXMKfmLjH6D\/JSIeYsrWJEQofStpgR+Mp7Ahp72jK1jfzFP1OCzm6Gk+mHk5doSk7JUYwRa+oXE+bmdBGiWGeMhO2O6TM+3Phkmj1zFA14fuM5Ghv3Lt8augDpmrwojXf+WL8IaQaXZU7O\/LJImXVGK77qMWSfGbUb4YL4KKpOktN91HkqTr0FqSHDdsr2V2mqZ\/wkbCZbh7\/8i2zQv99o4l3W+M+Y+oXmJ8XGzvFyKuLVSVGHzScpU8LkPxyXuKFMxIRwxfasChG5vmlixX0ctXswdTXk4geY+oPMF7i+Vd1eH4JY+pP8rJmNvfmXERdx5Rl6BNJ9t8zeze8jIFi2UhTJxope0wf7r2PdkAtvbGkxMt5iSSbvZeC12OESx\/uYbpJdfjNp5gglyebxqneyREOMu71L9jQSdOL1GqpwoHmv9NmIZxMPYgxEKrm11ff0Iu45q1R99TAeBHvvX5fSscZkAo371kLPkFPIJkgwOmxrePPH76vrC6\/rrO8+u5wvZQv72ZkkHV3z5jTchwODjn8ST1KgDBcQlXibJ24rklRAUCQ2MLA4LDTZiob83n97uSunuAYWRZqyOfpTKluu\/odnrvBjn8Ioc5r96bu+G71eAOBgRqBen4cunsj2NU0hZpm2zZnX+85fT8TbosWLS6BHyo3Jy8IyynCAAAAAElFTkSuQmCC`;\n\nwindow.commentImg = `<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"30\" height=\"22.5\" viewBox=\"0 0 48 36\"><defs><clipPath id=\"a\"><path d=\"M0 0h48v36H0z\"\/><\/clipPath><\/defs><g clip-path=\"url(#a)\"><path fill=\"currentColor\" d=\"M46.42 21.83q.57 0 .63-.63l.29-3.07q.07-.66-.5-.66h-4.67l.68-7.1h4.28q.58 0 .65-.64l.29-3.07q.06-.66-.52-.66H34.53q-.56 0-.62.66l-.3 3.07q-.06.63.5.63h4.31l-.68 7.11h-4.67q-.59 0-.65.66l-.3 3.07q-.05.63.53.63h4.67l-.82 7.54q-.06.63.53.63h3.25q.6 0 .65-.63l.82-7.54z\"\/><path fill=\"currentColor\" d=\"M46.15 15.22a7 7 0 0 0 1.46-3.49q.1-.68-.5-.68h-2.17q-.47 0-.7.57a1.6 1.6 0 0 1-.62.86q-.47.33-.5.74l-.28 2.88q-.07.71.52.66 1.61-.22 2.79-1.54M34.03 12.04q-.55.08-.6.71l-.3 3q-.05.62.56.6l1.66-.22q.78-.14 1.43-.33.5-.16.56-.75l.28-2.93q.06-.72-.58-.58-1.5.33-3.01.5M26.89 10.67c-.06.5.3.92.77.92h3.15c.4 0 .73-.3.78-.72l.35-2.53c.05-.49-.3-1.38-.78-1.38h-3.15a.8.8 0 0 0-.78.72zM30.35 22.31c-.34.11-.7.17-.98.17a1 1 0 0 1-.59-.16q-.7-.45-.61-1.2a3.5 3.5 0 0 1 1.06-1.94l.24-.27c.24-.27.56-.64.87-.9.3-.72.64-1.09.7-1.5l.3-2.63c.06-.5-.3-.92-.78-.92h-4.53a.8.8 0 0 0-.78.72l-.3 2.63c-.06.49.3.92.77.92h2.96c-.67.33-1.66 1.3-2.7 2.29q-1.53 1.5-1.91 4.73-.26 2.17.42 3.12a2.3 2.3 0 0 0 1.98.95q.8 0 1.6-.24c1.75-.51 2.72-2.37 2.94-4.28l.11-.9c.04-.37-.43-.7-.77-.59M22.54 19.99c.07-.6-.4-1.09-.99-1.18q-1.68-.27-1.86-1.46 0-.36.02-.62.1-.9.56-1.95.46-1.06.93-1.85c.4-.65.7-1.39.79-2.15l.23-2.27a1 1 0 0 0-.99-1.11h-4.86l.08-.8a1 1 0 0 0-1-1.1h-.85a1 1 0 0 0-1 .9l-.1 1h-1.56a1 1 0 0 0-1 .9l-.27 2.55a1 1 0 0 0 1 1.1h1.35l-.02.2a24 24 0 0 1-.33 2.18 1.7 1.7 0 0 1-1.1-1.41c-.04-.3-.28-.57-.58-.57-.27 0-.5.2-.52.46q-.28 3.45.82 5.01-.43.6-.97.87c-.46.22-.91.58-.96 1.1l-.39 3.64c-.06.59.4 1.11 1 1.02a4.7 4.7 0 0 0 2.55-1.2 8.6 8.6 0 0 0 2.15-3.78l.6.06-.06.67q-.2 2.32 1.14 3.32 1.38 1.01 3.2 1.01h1.62a1 1 0 0 0 .99-.9zm-6.65-8.03H19c-.88.6-1.57 1.68-2.16 2.63l-.2.3-1.07-.03q.11-.81.31-2.7zM5.72 23.8q.72-.72.87-2.16l.33-3.46 1.73-.05q.53-.03.6-.67l.26-2.74a.7.7 0 0 0-.1-.48q-.12-.2-.4-.16l-1.7.05.22-2.19h1.7q.55 0 .61-.6l.28-2.8q.06-.64-.5-.64h-1.7l.16-1.63q.06-.64-.52-.64h-2.3q-.56 0-.63.64l-.16 1.63H2.7q-.58 0-.64.63l-.28 2.8q-.05.62.53.62h1.77l-.23 2.4-1.8.05q-.57.04-.63.64l-.27 2.77a.7.7 0 0 0 .1.48q.14.18.42.16l1.78-.06-.16 1.7a.7.7 0 0 1-.22.46.5.5 0 0 1-.35.16H1.67q-.57 0-.64.64l-.25 2.53q-.06.64.52.64h2.46q1.24 0 1.96-.72\"\/><path fill=\"#ff6c93\" d=\"M16.26 30.5h3.78c.34 0 .64-.22.77-.56l1.27-3.27q.06-.17.07-.36v-.93c0-.52-.38-.93-.85-.93h-2.67l.42-2.13a.8.8 0 0 0-.17-.65.6.6 0 0 0-.9 0l-2.3 2.56a26 26 0 0 1-10.22 2.11H1.2a1 1 0 0 0-1 .9L0 29.4a1 1 0 0 0 1 1.1h15.26M20.19 13.77l.22.47a8 8 0 0 0 3.17 3.51L24 18l-.42.25a8 8 0 0 0-3.17 3.5l-.22.48-.23-.47a8 8 0 0 0-3.17-3.51l-.42-.25.42-.25a8 8 0 0 0 3.17-3.5z\"\/><\/g><\/svg>`; \n\nwindow.TuiImgData = `<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\"><path fill=\"#14c4bc\" d=\"M12 1a11 11 0 0 1 8.15 18.39l.07-.53a.5.5 0 0 0-.5-.56h-2.88a.5.5 0 0 1-.5-.56l.19-1.6a.5.5 0 0 1 .5-.44h2.23a.5.5 0 0 0 .5-.44l.07-.66a.5.5 0 0 0-.5-.56h-2a.5.5 0 0 1-.5-.56l.16-1.32a.5.5 0 0 1 .5-.44h2a.5.5 0 0 0 .5-.44l.08-.66a.5.5 0 0 0-.5-.56h-1.79a.5.5 0 0 1-.5-.56l.17-1.36a.5.5 0 0 1 .5-.44h2.13a.5.5 0 0 0 .5-.44l.07-.7a.5.5 0 0 0-.5-.56h-1.98a.5.5 0 0 1-.48-.38l-.44-1.74a.5.5 0 0 0-.48-.38h-.8a.5.5 0 0 0-.5.61l.3 1.28a.5.5 0 0 1-.5.61h-1.3a.5.5 0 0 1-.47-.69l.28-.69.25-.6a.5.5 0 0 0-.46-.7h-.81c-.22 0-.4.14-.48.34a56.18 56.18 0 0 1-2.85 6.12.5.5 0 0 0 .44.74h.25c.3 0 .53.26.5.56l-.94 8.37c-.03.3.2.55.5.55h9.37a10.96 10.96 0 0 1-14.93.16q.27.07.56.12.08.02.26.02.56 0 .94-.4.4-.39.48-1.08l.62-5.24a.5.5 0 0 1 .34-.42l.62-.2.6-.2a.5.5 0 0 0 .34-.41l.1-.82A.5.5 0 0 0 8.8 11l-.19.06-.13.05a.5.5 0 0 1-.66-.54l.24-1.99a.5.5 0 0 1 .5-.44h.8a.5.5 0 0 0 .5-.44l.1-.78a.5.5 0 0 0-.5-.56H8.9a.5.5 0 0 1-.5-.56l.24-1.96a.5.5 0 0 0-.5-.56h-.85a.5.5 0 0 0-.5.44l-.27 2.2a.5.5 0 0 1-.5.44H4.84a.5.5 0 0 0-.5.45l-.09.78c-.03.3.2.55.5.55h.94c.3 0 .53.26.5.56l-.36 2.89a.5.5 0 0 1-.36.42q-.6.16-1.36.33a.5.5 0 0 0-.38.43l-.12.92a.5.5 0 0 0 .61.54l.6-.16c.36-.1.7.18.66.54l-.43 3.51q0 .1-.02.17l-.04.1-.08.03q-.08 0-.14-.02-.3-.07-.7-.21a.52.52 0 0 0-.71.4l-.02.17A11 11 0 0 1 12 1m2.16 14.7c.3 0 .53.26.5.56l-.19 1.6a.5.5 0 0 1-.5.44h-1.75a.5.5 0 0 1-.5-.56l.19-1.6a.5.5 0 0 1 .5-.44zm.46-3.98c.3 0 .53.26.5.56l-.17 1.32a.5.5 0 0 1-.5.44H12.7a.5.5 0 0 1-.5-.55l.15-1.32a.5.5 0 0 1 .5-.45zm.46-4.02c.3 0 .53.26.5.56l-.17 1.36a.5.5 0 0 1-.5.44h-1.75a.5.5 0 0 1-.5-.55l.15-1.36a.5.5 0 0 1 .5-.45z\"\/><\/svg>`; \nwindow.LeiImgData = `<svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"20\" height=\"20\" viewBox=\"0 0 24 24\"><path fill=\"#fb923c\" d=\"M17.8 21.34a11 11 0 0 1-11.6 0zM12 1c2.46 0 4.73.8 6.56 2.17H5.9a.5.5 0 0 0-.5.45l-.08.76c-.03.3.2.55.5.55h5.55c.3 0 .53.26.5.55l-.01.04a.5.5 0 0 1-.5.45h-5.4q-.86 0-1.36.46-.5.44-.58 1.25l-.29 2.73c-.03.3.2.55.5.55h1.01a.5.5 0 0 0 .5-.45l.22-2.42a.5.5 0 0 1 .5-.45h4.6c.3 0 .53.25.5.55l-.37 3.47c-.03.3.2.55.5.55h1.01a.5.5 0 0 0 .5-.44l.4-3.68a.5.5 0 0 1 .49-.45h4.56c.3 0 .53.25.5.55l-.24 2.22c-.03.3.2.55.5.55h.97a.5.5 0 0 0 .5-.45l.28-2.72q.1-.92-.4-1.36-.45-.46-1.4-.46h-4.97a.5.5 0 0 1-.5-.56v-.03a.5.5 0 0 1 .5-.45h5.44a.5.5 0 0 0 .42-.22 10.98 10.98 0 0 1-1.59 16.05l.74-6.15q.03-.13.02-.35 0-.7-.44-1.06-.4-.37-1.23-.37H6.52q-1.47 0-1.71 1.34l-.65 5.54A11 11 0 0 1 12 1m-1.9 16.74c.3 0 .53.26.5.55l-.11.98a.5.5 0 0 1-.5.45H6.71a.5.5 0 0 1-.5-.56l.1-.98a.5.5 0 0 1 .5-.44zm6.36 0c.3 0 .53.26.5.55l-.11.98a.5.5 0 0 1-.5.45h-3.37a.5.5 0 0 1-.5-.56l.1-.98a.5.5 0 0 1 .5-.44zm-5.99-3.28c.3 0 .53.26.5.55l-.09.74a.5.5 0 0 1-.5.45H7.1a.5.5 0 0 1-.5-.56l.1-.74a.5.5 0 0 1 .49-.44zm6.37 0c.3 0 .54.26.5.56l-.1.74a.5.5 0 0 1-.49.44h-3.38a.5.5 0 0 1-.5-.56l.1-.74a.5.5 0 0 1 .49-.44zM6.9 10.72a.5.5 0 0 0-.5.44l-.03.27c-.04.3.2.56.5.56H9.8a.5.5 0 0 0 .5-.44l.03-.27a.5.5 0 0 0-.5-.56zm7.8 0a.5.5 0 0 0-.49.44l-.03.27c-.04.3.2.56.5.56h2.73a.5.5 0 0 0 .5-.44l.03-.27a.5.5 0 0 0-.5-.56zm-7.58-2.2a.5.5 0 0 0-.5.44l-.03.27c-.04.3.2.56.5.56h2.93a.5.5 0 0 0 .5-.44l.03-.27a.5.5 0 0 0-.5-.56zm7.81 0a.5.5 0 0 0-.5.44l-.03.27c-.04.3.2.56.5.56h2.73a.5.5 0 0 0 .5-.44l.03-.27a.5.5 0 0 0-.5-.56z\"\/><\/svg>`; \n\nwindow.java = java;\nwindow.source = source;\n",
"ruleArticles": "<js>var c={},J=JSON,S=String,K=Object.keys,IA=Array.isArray,T=[],B=[],D,F=0; \ntry{var r=typeof source!=='undefined'&&source.getVariable?source.getVariable():'';if(r?.trim)c=J.parse(r)}catch(e){} \nif(IA(c.cleandTags)){D=IA(c.disCleandTags)?c.disCleandTags.map(x=>S(x).trim()):[];c.cleandTags.forEach(x=>{var s=S(x).trim();s&&!D.includes(s)&&T.push(s)})} \nif(IA(c.authors))c.authors.forEach(a=>{S(a.hidden)===\"2\"&&a.blogId&&B.push(S(a.blogId))}); \nvar O=v=>v&&typeof v==='object'; \nvar H=v=>typeof v==='string'?T.some(x=>v.includes(x)):IA(v)?v.some(H):O(v)?K(v).some(k=>H(v[k])):0; \nvar A=v=>O(v)&&(v.blogId&&B.includes(S(v.blogId))||K(v).some(k=>A(v[k]))); \nvar C=i=>O(i)&&(['tagList','tagLists','tags'].some(f=>i[f]!=null&&H(i[f]))||K(i).some(k=>C(i[k]))); \nvar R=x=>{if(IA(x)){for(var i=x.length-1;i>=0;i--){var o=x[i];if(O(o)&&C(o)){x.splice(i,1);F++;continue}}}else if(O(x))K(x).forEach(k=>R(x[k]));return x}; \nvar Q=x=>{if(!B.length)return x;if(IA(x)){for(var i=x.length-1;i>=0;i--){var o=x[i];if(O(o)){if(A(o)){x.splice(i,1);F++;continue}K(o).forEach(k=>Q(o[k]))}}}else if(O(x))K(x).forEach(k=>Q(x[k]));return x}; \nvar out=R(Q(typeof result==='string'?J.parse(result):result)); \n var ST=out&&(out.meta&&out.meta.status||out.code);if(ST!=null&&['200','0'].indexOf(S(ST))===-1){var TM=S(out.meta&&out.meta.msg||out.msg||'请求失败');java.toast(TM);\/*if(!out.data)out={data:{list:[{name:TM,id:\"\"}]}};*\/} \nif(F>0){try{java.toast('过滤 '+F+' 项')}catch(e){}}\nJ.stringify(out);<\/js>\n$.data.posts||$.response.items||$.data.list||$.response.posts||$.data.items||$.data.recommend||$.data.grainList[*].posts[*]",
"ruleContent": "@js: \/\/java.log(result);\n\/* ===== 日志 ===== *\/\nvar Log = {\n _on: true,\n _out(tag, msg) {\n if (!Log._on) return;\n try { java.log(`[lofter][${tag}] ${msg}`); } catch (e) {}\n },\n debug(m) { Log._out('DEBUG', m); },\n warn(m) { Log._out('WARN', m); },\n error(m) { Log._out('ERROR', m); }\n};\n\n\/* ===== 环境初始化 ===== *\/\nfunction initEnvironment() {\n var tipMsg = \"{{$.meta.msg}}\";\n if (tipMsg) java.toast(tipMsg);\n try {\n var c = String(cookie.getCookie(\"https:\/\/www.lofter.com\"));\n var m = c.match(\/(?:^|;\\s*)(LOFTER-[^-]+-LOGIN-AUTH)=([^;]+)\/);\n if (m) {\n var h = {};\n h[m[1]] = m[2];\n source.putLoginHeader(JSON.stringify(h));\n Log.debug(`登录头已注入:${m[1]}`);\n }\n } catch (e) { Log.warn(`读取登录 Cookie 失败:${e}`); }\n}\n\n\/* ===== 工具 ===== *\/\nfunction escapeHtml(s) {\n if (!s) return '';\n return String(s)\n .replace(\/&\/g, '&')\n .replace(\/<\/g, '<')\n .replace(\/>\/g, '>')\n .replace(\/\"\/g, '"')\n .replace(\/'\/g, ''');\n}\n\nfunction getZSFontCss(fontId, weight) {\n var uri = \"https:\/\/fontsapi.zeoseven.com\/\" + fontId + \"\/\" + weight + \"\/\"; \n var url = uri + \"result.css\"; \n var cacheKey = java.md5Encode16(url);\n var cached = cache.getFile(cacheKey);\n if (cached && cached!='') { return cached; } \n else { var css = String(java.cacheFile(url))\n .replace(\/url\\(\"\\.\\\/\/g, 'url(\"' + uri)\n .replace(\/ur\\('\\.\\\/\/g, \"url('\" + uri);\n cache.putFile(cacheKey, css, 0);\n return css; }\n}\nfunction getHlsJs() {\n var url = \"https:\/\/cdn.jsdelivr.net\/npm\/hls.js@1\/dist\/hls.min.js\";\n var cacheKey = java.md5Encode16(url);\n var cached = cache.getFile(cacheKey);\n if (cached && cached!='') { return cached; } \n else {var js = String(java.cacheFile(url));\n cache.putFile(cacheKey, js, 0);\n return js; }\n}\n\n\/* ===== 提取数据 ===== *\/\nfunction getPostData() {\n const g = (rule) => java.getString(rule) || '';\n const n = (rule) => Number(java.getString(rule)) || 0;\n const z = (rule) => g(rule) || '0';\n var postId = '', blogId = '';\n try { postId = g('$..post.id'); blogId = g('$..post.blogId'); }\n catch (e) { Log.warn(`postId \/ blogId 提取失败:${e}`); }\n\n \/* 字段提取 *\/\n const errMsg = g('$..msg');\n const titleRaw = g('$..post.title||$..post.longInfo.title');\n const content = g('$..post.content||$..post.longInfo.content');\n const coName = g('$..postCollection.name');\n const coId = g('$..postCollection.id');\n const coDesc = g('$..postCollection.description');\n const coBlogId = g('$..postCollection.blogId') || blogId;\n const blogUrl = g('$..blogPageUrl');\n const blogName = g('$..blogInfo.blogName');\n const authorName = g('$..blogInfo.blogNickName');\n const authorImg = g('$..blogInfo.bigAvaImg');\n const tagsRaw = g('$..tag');\n const photoLinks = g('$..photoLinks');\n const embedData = g('$..embed');\n const imagesUrl = g('$..post.firstImageUrl');\n const publishTime = n('$..post.publishTime');\n const modifyTime = n('$..post.modifyTime');\n const showGift = n('$..showGift');\n const showAudio = n('$..showAudio');\n const audioListen = n('$..audioPostVO.listenCount');\n const favorite = z('$..postCount.favoriteCount');\n const response = z('$..postCount.responseCount');\n const subscribe = z('$..postCount.subscribeCount');\n const share = z('$..postCount.shareCount');\n const showVote = ['true', '1'].includes(g('$..showVote'));\n\n \/* 回礼解析 *\/\n var returnContent = [];\n try { var posts = (JSON.parse(src).response || {}).posts || []; \n posts.forEach(p => {\n var list = (p && p.post && p.post.returnContent) || [];\n list.forEach(rc => {\n if (rc && rc.content) returnContent.push({\n title: rc.planTypeName ? `🎈 ${rc.planTypeName}` : '',\n content: rc.content || '',\n images: rc.images || []\n });});});\n } catch (e) { Log.warn(`回礼解析失败:${e}`); }\n \/* 回礼未解锁 → 拉卡片 *\/\n if (showGift > 0 && !returnContent.length && postId && blogId) { \n var si = fetchSupportInfo(postId, blogId, coId); \n var rg = si && si.returnGifts && si.returnGifts[0]; \n if (rg) returnContent.push({ title: '', content: buildSupportCard(rg, blogUrl), images: [] }); \n }\n \/* 投票处理 *\/\n if (showVote && postId && blogId) {\n var vh = buildVoteCard(postId, blogId, blogUrl);\n if (vh) returnContent.push({ title: '', content: vh, images: [] });\n }\n \/* 数据删缺 *\/\n var deleted = \/删除|不存在|下架|失效|不可见\/i.test(errMsg)\n || (!postId && !blogId && !titleRaw && !content && !authorName);\n\n return {\n postId, blogId, blogUrl, blogName,\n coId, coBlogId, coName, coDesc,\n titleRaw, tagsRaw,\n authorImg, authorName,\n publishTime, modifyTime,\n photoLinks, imagesUrl, embedData,\n favorite, response,subscribe, share,\n showGift, showAudio, audioListen,\n content, returnContent,\n errMsg, deleted\n };\n}\n\n\/* ===== 页面 ===== *\/\n\/* 首图组(无正文图时兜底) *\/\nfunction buildFirstImageHtml(json) {\n if (!json) return '';\n try {\n var urls = JSON.parse(json);\n if (!urls.length) return '';\n var html = '<div class=\"iv iv--first\"><div class=\"iv-stage\">';\n for (var i = 0; i < urls.length; i++) {\n if (!urls[i]) continue;\n html += `\n<div class=\"iv-slide ${i===0?'active':''}\" data-index=\"${i}\"><img src=\"${escapeHtml(urls[i])}\" alt=\"图片\" ${i>0?'loading=\"lazy\"':''}><span class=\"iv-bar\"><button class=\"iv-btn iv-zoom\" type=\"button\" tite=\"放大\"><i class=\"fa fa-search-plus\" aria-hidden=\"true\"><\/i><\/button><button class=\"iv-btn iv-open\" type=\"button\" title=\"查看\"><i class=\"fa fa-location-arrow\" aria-hidden=\"true\"><\/i><\/button><span class=\"iv-page\">${i+1}\/${urls.length}<\/span><\/span><\/div>`;\n }\n return html + '<\/div><\/div>';\n } catch (e) { Log.warn(`firstImageUrl 解析失败:${e}`); return ''; }\n}\n\n\/* 图组 *\/\nfunction buildImageHtml(json) {\n if (!json) return '';\n try {\n var ps = JSON.parse(json);\n var html = '<div class=\"iv\"><div class=\"iv-stage\">';\n for (var i = 0; i < ps.length; i++) {\n var u = ps[i].orign || ps[i].small || ps[i].url || '';\n if (!u) continue;\n html += `\n<div class=\"iv-slide ${i===0?'active':''}\" data-index=\"${i}\"><img src=\"${escapeHtml(u)}\" alt=\"图片\" ${i>2?'loading=\"lazy\"':''}><span class=\"iv-bar\"><button class=\"iv-btn iv-zoom\" type=\"button\"title=\"放大\"><i class=\"fa fa-search-plus\" aria-hidden=\"true\"><\/i><\/button><button class=\"iv-btn iv-open\" type=\"button\" title=\"查看\"><i class=\"fa fa-location-arrow\" aria-hidden=\"true\"><\/i><\/button><span class=\"iv-page\">${i+1}\/${ps.length}<\/span><\/span><\/div>`; \n }\n return html + '<\/div><\/div>';\n } catch (e) { Log.warn(`photoLinks 解析失败:${e}`); return ''; }\n}\n\n\/* 视频 *\/\nfunction buildVideoHtml(json) {\n if (!json || json.indexOf(\"originUrl\") === -1) return '';\n try {\n var u = JSON.parse(json).originUrl;\n return u ? `<div class=\"video\"><iframe src=\"${escapeHtml(u)}\" sandbox=\"allow-scripts allow-same-origin\" allowfullscreen><\/iframe><\/div>` : '';\n } catch (e) { Log.warn(`embed 解析失败:${e}`); return ''; }\n}\n\n\/* 作者栏 *\/\nfunction buildAuthorHtml(img, name) {\n var authorAvatar = `<img src=\"${escapeHtml(img).replace(\/\\?.+$\/,'')}?imageView&thumbnail=84x84&stripmeta=0&quality=90&type=webp&typefallback=jpg\" class=\"post-author__avatar\" onerror=\"this.style.display='none'\">`; \n return '<div class=\"post-author\">' + authorAvatar + \n '<span class=\"post-author__name\">' + escapeHtml(name) + '<\/span>' + \n '<span class=\"btn btn--follow\" data-action=\"follow\" title=\"关注作者\"><i class=\"fa fa-user\"><\/i><\/span>' + \n '<span class=\"btn btn--article-add\" data-action=\"article-add\" title=\"收藏文章\"><i class=\"fa fa-asterisk\"><\/i><\/span>' + \n '<span class=\"btn btn--collection-trigger\" data-action=\"collection-trigger\" title=\"合集列表\"><i class=\"fa fa-slack\"><\/i><\/span>' + \n '<\/div>';\n}\n\n\/* 段落处理 *\/\nfunction processParagraphs(html) {\n if (!html) return html;\n var idx = 0;\n\n return String(html).replace(\/<p\\b([^>]*)>([\\s\\S]*?)<\\\/p>\/gi, (m, attrs, content) => {\n var fallbackPid = 'p_i' + idx;\n var idm = attrs.match(\/\\bid=\"([^\"]*)\"\/i);\n var actualPid = idm ? idm[1] : fallbackPid;\n if (!idm) attrs += ` id=\"${fallbackPid}\"`;\n idx++;\n\n var c = content\n .replace('&type=jpg', '')\n .replace(\/(\\s*<br\\s*\\\/?>\\s*)+\/gi, '<br>')\n .replace(\/<[a-z]{1,10}>\\s*<\\\/[a-z]{1,10}>\\s*\/gi, '')\n .replace(\/\\[\\s+\/g, '[').replace(\/\\s+\\]\/g, ']')\n .replace(\/【\\s+\/g, '【').replace(\/\\s+】\/g, '】')\n .replace(\/〖\\s+\/g, '〖').replace(\/\\s+〗\/g, '〗')\n .replace(\/^[ \\s]+$\/g, '').trim()\n .split(\/(<[^>]+>)\/).map(p => p[0] === '<' ? p : p.trim()).join('');\n\n if (\/^(?:\\s| |<br>| | | )*$\/i.test(c)) {\n attrs = \/\\bstyle\\s*=\/i.test(attrs) ?\n attrs.replace(\/\\bstyle\\s*=\\s*(['\"])([\\s\\S]*?)\\1\/i,\n (_, q, s) => ` style=${q}${s.replace(\/;\\s*$\/,'')};display:none;${q}`) :\n attrs.trim() + ' style=\"display:none\"';\n return `<p ${attrs}><\/p>`;\n }\n if (!\/^\\u3000\/.test(c)) c = '\\u3000' + c;\n\n return `<p ${attrs.trim()}>${c}<\/p>`;\n });\n}\n\n\/* 付费内容(已解锁) *\/\nfunction buildUnlockHtml(list) {\n if (!list || !list.length) return '';\n return list.map(item => {\n var imgs = '';\n if (item.images && item.images.length) {\n imgs = '<div class=\"unlock-item__images\">';\n item.images.forEach(img => {\n var u = img.orign || img.raw || img.url || '';\n if (u) imgs += `<img src=\"${escapeHtml(u)}\" alt=\"回礼图片\">`;\n });\n imgs += '<\/div>';\n }\n var title = item.title ? `<div class=\"unlock-item__title\">${escapeHtml(item.title)}<\/div>` : '';\n return `<div class=\"unlock-item\">${title}<div class=\"unlock-item__content\">${item.content}<\/div>${imgs}<\/div>`;\n }).join('');\n}\n\n\/* 付费内容(未解锁) *\/\nfunction fetchSupportInfo(pid, bid, cid) {\n if (!pid || !bid) return null;\n \/* 移去下述内置函数 encodeURIComponent() *\/\n var q = `postId=${pid}&blogId=${bid}&openFansVipPlan=0&vipFans=0${cid?`&collectionId=${cid}`:''}&scene=note&postType=1`;\n try {\n var d = JSON.parse(java.ajax(`https:\/\/api.lofter.com\/v1.1\/trade\/gift\/post\/newSupportInfo?${q}`));\n if (d && d.code === 200) return d.data;\n } catch (e) { Log.warn(`newSupportInfo 拉取失败:${e}`); }\n return null;\n}\nfunction buildSupportCard(rg, url) {\n var tags = [];\n var appUrl = \/^https:\/.test(url) ? url.replace('https:', 'lofter:') : '';\n (rg.labels || []).forEach(l => tags.push(`<span class=\"support-tag\">${escapeHtml(l.label)}<\/span>`));\n (rg.feedbacks || []).forEach(f => tags.push(`<span class=\"support-tag support-tag--comment\">${escapeHtml(f)} <i class=\"fa fa-angle-right\"><\/i><\/span>`));\n\n var plan = (rg.planType && rg.planType.name) || '';\n var badge = plan ? `<span class=\"support-badge\">${escapeHtml(plan)}<\/span>` : '';\n var title = rg.title ? `<span class=\"support-title\">${escapeHtml(rg.title)}<\/span>` : '<span class=\"support-title\">惊喜衍生剧场<\/span>';\n var appBtn = appUrl ? `<button class=\"support-app-btn\" type=\"button\" onclick=\"java.openUrl('${escapeHtml(appUrl)}')\" title=\"在 App 中打开\">${logoData()}<\/button>` : '';\n var header = (badge || title) ? `<div class=\"support-header\">${badge}${title}${appBtn}<\/div>` : '';\n const lockCount = [\n rg.wordCount > 0 && `${rg.wordCount} 字`,\n rg.imgCount > 0 && `${rg.imgCount} 图`\n ].filter(Boolean).join(' + ');\n\n return `<div class=\"support-card\">${header}` +\n (tags.length ? `<div class=\"support-tags\">${tags.join('')}<\/div>` : '') +\n (rg.promotion ? `<div class=\"support-promotion\">${escapeHtml(rg.promotion)}<\/div>` : '') +\n (rg.digest ? `<div class=\"support-digest\">${escapeHtml(rg.digest).trim().replace(\/\\s+\/g,'<br>')}...<\/div>` : '') +\n (lockCount ? `<div class=\"support-lock\"><i class=\"fa fa-lock\"><\/i><span>解锁余 ${lockCount}<\/span><\/div>` : '') +\n '<\/div>';\n}\n\n\/* 投票详情 *\/\nfunction buildVoteCard(pid, bid, url) {\n try {\n var d = JSON.parse(java.ajax(`https:\/\/api.lofter.com\/newapi\/vote\/detail.json?postId=${pid}&blogId=${bid}`)); \n var vo = d && d.data && d.data.voteInfoVO;\n if (!vo || !Array.isArray(vo.voteOptionList) || !vo.voteOptionList.length) return '';\n var total = 0;\n vo.voteOptionList.forEach(function(o){ total += Number(o.count) || 0; });\n var votedId = Number(vo.votedOptionId) || 0;\n var appUrl = \/^https:\/.test(url) ? url.replace('https:', 'lofter:') : '';\n var appBtn = appUrl ? `<button class=\"support-app-btn\" type=\"button\" onclick=\"java.openUrl('${escapeHtml(appUrl)}')\" title=\"在 App 中打开\">${logoData()}<\/button>` : '';\n var rows = vo.voteOptionList.map(function(o){\n var c = Number(o.count) || 0;\n var pct = total > 0 ? Math.round(c * 100 \/ total) : 0;\n var cls = (votedId && Number(o.id) === votedId) ? 'vote-row voted' : 'vote-row';\n return `<div class=\"${cls}\"><span class=\"vote-bar\" style=\"width:${pct}%\"><\/span><span class=\"vote-name\">${escapeHtml(o.optionContent || '')}<\/span><span class=\"vote-num\">${c} · ${pct}%<\/span><\/div>`;\n }).join('');\n var badge = Number(vo.status) === 1 ? '投票' : '投票已结束';\n return `<div class=\"vote-card\"><div class=\"vote-title\"><b>${badge}<\/b><span class=\"vote-title__text\">${escapeHtml(vo.title || '')}<\/span>${appBtn}<\/div>${rows}<div class=\"vote-foot\">共 ${(Number(vo.joinCount) || total)} 人参与<\/div><\/div>`; \n } catch (e) { Log.warn('vote 拉取失败:' + e); return ''; }\n}\n\n\/* 合集 *\/\nfunction buildCollectionHtml(name, desc, listen) {\n if (name) name = String(name).trim();\n if (!name) return '';\n var tts = listen? '<span class=\"btn btn--tts\" data-action=\"tts\" title=\"听书\"><i class=\"fa fa-listen\" aria-hidden=\"true\"><\/i><\/span>' : '';\n var html = `<div class=\"post-col\"><span class=\"post-col__label\">合集: <\/span><span class=\"post-col__info\" data-action=\"collection-info\" title=\"查看合集\">${escapeHtml(name)}<\/span><span class=\"btn btn--import\" data-action=\"collection-import\" title=\"收藏合集\"><i class=\"fa fa-bookmark\" aria-hidden=\"true\"><\/i><\/span><span class=\"btn btn--open\" data-action=\"collection-open\" title=\"打开合集\"><i class=\"fa fa-folder\" aria-hidden=\"true\"><\/i><\/span>${tts}<\/div>`;\n if (desc && String(desc).trim()) html += `<div><details class=\"post-col__details\"><summary><\/summary><div class=\"post-col__desc\">${escapeHtml(desc)}<\/div><\/details><\/div>`;\n return html;\n}\n\n\/* 标签 *\/\nfunction buildTagsHtml(raw) {\n if (!raw) return '';\n var parts = ['<div class=\"post-tags\"><span class=\"post-tags__label\">标签: <\/span>'];\n raw.split(',').forEach(t => { t = t.trim(); if (t) parts.push(`<span class=\"post-tags__item\">${escapeHtml(t)}<\/span>`); });\n return parts.join('') + '<\/div>';\n}\n\n\/* 计数 *\/\nfunction buildCountsHtml(fav, resp, sub, share, gift, audio, listen) {\n const itemGift = gift!=0?`<span class=\"post-counts__item p-count-showgift\">${gift}<\/span>`:'';\n const itemAudio = audio!=0?`<span class=\"post-counts__item p-count-showaudio\">${listen}<\/span>`:''; \n return '<div class=\"post-counts\">' +\n `<span class=\"post-counts__item p-count-favorite\">${fav}<\/span>` + \n `<span class=\"post-counts__item p-count-subscribe\">${sub}<\/span>` + \n `<span class=\"post-counts__item p-count-response\">${resp}<\/span>` + \n `<span class=\"post-counts__item p-count-share\">${share}<\/span>` + \n itemGift + itemAudio + \n '<\/div>'; \n}\n\n\/* 底链 *\/\nfunction buildLinksHtml(pid, bid, url) {\n return '<div class=\"post-links\"><span>点击跳转: <\/span>' + \n `<a href=\"https:\/\/www.lofter.com\/comment.do?pid=${pid}&bid=${bid}&listlinkcolor=828d95&listcontentcolor=737373&listbordercolor=ddd&inputbordercolor=ddd\">评论链接<\/a> ` + \n `<a href=\"javascript:void(0)\" onclick=\"java.openUrl('${escapeHtml(url)}')\">查看原网页<\/a> ` + \n '<a href=\"javascript:void(0)\" data-action=\"export-ext\">缓存文章<\/a>' + \n '<\/div>';\n}\n\n\/* ===== 组装 HTML ===== *\/\nfunction assembleHtml(pd, cm) {\n if (pd.deleted) { return `<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\"><title>作品已删除<\/title><\/head><body><div class=\"post-deleted\"><div class=\"post-deleted__icon\">🚫<\/div><div class=\"post-deleted__title\">作品已删除或不可见<\/div><div class=\"post-deleted__desc\">${escapeHtml(pd.errMsg||'')}<\/div><\/div><\/body><\/html>`; } \n var cardHtml = ''; \n var titleTop = pd.titleRaw != '' ? `<title>${escapeHtml(pd.titleRaw)}<\/title>` : '<title>LOFTER<\/title>'; \n var titleHtml = pd.titleRaw != '' ? `<h1 class=\"post-title\">${escapeHtml(pd.titleRaw)}<\/h1>` : ''; \n \/\/ var hlsScript = pd.showAudio ? '<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/hls.js@1\/dist\/hls.min.js\"><\/script>' : ''; \n \/\/ var hlsScript = pd.showAudio ? '<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/hls.js@latest\"><\/script>':''; \n var hlsScript = pd.showAudio ? '<script>' + getHlsJs() + '<\/script>':''; \n var authorHtml = buildAuthorHtml(pd.authorImg, pd.authorName);\n var imagesHtml = buildImageHtml(pd.photoLinks);\n var videoHtml = buildVideoHtml(pd.embedData);\n var contentHtml = `<section class=\"post-content\">\\n ${pd.content}\\n <\/section>`;\n if (imagesHtml.indexOf('<img') === -1 && videoHtml.indexOf('<iframe') === -1 && contentHtml.indexOf('<img') === -1) \n imagesHtml = buildFirstImageHtml(pd.imagesUrl); \n\n var unlockHtml = buildUnlockHtml(pd.returnContent);\n var tagsHtml = buildTagsHtml(pd.tagsRaw);\n var mtime = pd.modifyTime, ptime = pd.publishTime, ed = mtime > ptime;\n var timeHtml = `<div class=\"post-time\">𝕋𝕀𝕄𝔼 <span>${java.timeFormatUTC((ed?mtime:ptime) + 28800000, 'yyyy-MM-dd HH:mm', 8)}${ed?' · 已编辑':''}<\/span><\/div>`; \n var countsHtml = buildCountsHtml(pd.favorite, pd.response, pd.subscribe, pd.share, pd.showGift, pd.showAudio, pd.audioListen); \n var collectionHtml = buildCollectionHtml(pd.coName, pd.coDesc, pd.showAudio); \n var linksHtml = buildLinksHtml(pd.postId, pd.blogId, pd.blogUrl);\n\n \/\/ 已将对应字体提取到本地 <link rel=\"stylesheet\" href=\"\/\/cdnjs.cloudflare.com\/ajax\/libs\/font-awesome\/4.7.0\/css\/font-awesome.min.css\"\/> \n \/* <link rel=\"preconnect\" href=\"https:\/\/fontsapi.zeoseven.com\" crossorigin \/> \n <link rel=\"stylesheet\" href=\"https:\/\/fontsapi.zeoseven.com\/442\/semi-bold\/result.css\" onerror=\"this.href='https:\/\/fontsapi-storage.zeoseven.com\/442\/main\/result.css'\" \/> *\/ \n var headHtml = `<meta charset=\"UTF-8\"> \n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"> \n ${titleTop}\n ${hlsScript}\n <style type=\"text\/css\">${getZSFontCss('442','semi-bold')}<\/style>\n <style type=\"text\/css\">@font-face{font-family:'iconfont';font-display:swap;src:url(\"${IconFontData()}\") format('woff2');}<\/style>`; \n\n return `<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n ${headHtml}\n<\/head>\n<body>\n <input type=\"hidden\" id=\"blogId\" value=\"${escapeHtml(pd.blogId)}\"> \n <input type=\"hidden\" id=\"postId\" value=\"${escapeHtml(pd.postId)}\"> \n <input type=\"hidden\" id=\"author-name\" value=\"${escapeHtml(pd.authorName)}\"> \n <input type=\"hidden\" id=\"author-avatar\" value=\"${escapeHtml(pd.authorImg)}\"> \n <input type=\"hidden\" id=\"article-title\" value=\"${escapeHtml(pd.titleRaw)}\"> \n <input type=\"hidden\" id=\"article-url\" value=\"${escapeHtml(pd.blogUrl)}\"> \n <input type=\"hidden\" id=\"collection-name\" value=\"${escapeHtml(pd.coName)}\"> \n <input type=\"hidden\" id=\"collection-id\" value=\"${escapeHtml(pd.coId)}\"> \n <article class=\"post\">\n <header class=\"post-header\">\n ${titleHtml}\n ${authorHtml}\n ${collectionHtml}\n ${timeHtml}\n <\/header>\n <section class=\"post-media\">\n ${imagesHtml}\n ${videoHtml}\n <\/section>\n ${contentHtml}\n <section class=\"unlock-section\">\n ${unlockHtml}\n <\/section>\n <footer class=\"post-footer\">\n ${tagsHtml}\n ${countsHtml}\n <\/footer>\n <section class=\"comments\" data-mode=\"\">\n <\/section>\n <nav class=\"post-links-container\">\n ${linksHtml}\n <\/nav>\n <\/article>\n<\/body>\n<\/html>`;\n}\nfunction main() {\n initEnvironment();\n var pd = getPostData();\n pd.content = processParagraphs(pd.content);\n return assembleHtml(pd);\n}\n\nmain();\n",
"ruleImage": "$.postData.postView.firstImage.orign||$.postData.postView.firstImage.raw||$.post.firstImageUrl||$.firstImage.orign||$.firstImage.raw||$.askCardVO.quotePost.postView.firstImage.orign||$.coverUrl||$.postCollection.coverUrl||$.image##^\\[\"([^\"]*)\".*$##$1@js:var type = java.getString(\"$.askCardVO.quotePost.postView.type||$.postData.postView.type||$.post.type||$.type\"); \nif(type==\"2\"||type==\"4\"||(type!=\"1\"&&result!=\"\")){result;}else{emptyImg();}",
"ruleLink": "https:\/\/api.lofter.com\/oldapi\/post\/detail.api?product=lofter-android-8.3.20,{\"method\":\"POST\",\"accept-encoding\":\"gzip, deflate\",\"body\":\"targetblogid={{$.askCardVO.quotePost.postView.blogId||$.postData.postView.blogId||$.post.blogId||$.blogId}}&blogdomain={{$.askCardVO.quotePost.blogInfo.blogName||$.blogInfo.blogName||$.post.blogInfo.blogName||$.blogInfo.blogName}}.lofter.com&postid={{$.askCardVO.quotePost.postView.id||$.postData.postView.id||$.post.id||$.id||$.postId}}\"}",
"ruleNextPage": "@js:baseUrl;",
"rulePubDate": "🗓 {{t=java.getString(\"$.postData.postView.publishTime||$.post.publishTime||$.publishTime||$.updateTime||$.lastPublishTime||$.postCollection.lastPublishTime\"); \nparseInt(t)>0?java.timeFormat(parseInt(t)):\"\"}} {{var r=\"═\".repeat(13);if(baseUrl.includes(\"newComment\")||baseUrl.includes(\"exploreRecom\")){var c=java.getString(\"$.mountCommentList[0].content||$.reasonInfo.msg\");c.trim()!=\"\"?\"\\n\"+r+\"\\n♦️ 𝙃𝙊𝙏 𝘾𝙊𝙈𝙈𝙀𝙉𝙏 \\n\"+c.trim()+\"\\n\"+r:\"\"}else if(baseUrl.includes(\"tagChat\/\")){var a=java.getString(\"$.postData.postView.answerPostView.answer\").trim();var q=java.getString(\"$.askCardVO.pCommentVO.quote\");a!=\"\"?\"\\n\"+r+(q!=\"\"?\"\\n📝 \"+q:\"\")+\"\\n💬 \"+a+\"\\n\"+r:\"\"};}}\n{{n=java.getString(\"$.askCardVO.quotePost.blogInfo.blogNickName||$.blogInfo.blogNickName||$.post.blogInfo.blogNickName||$.postData.blogInfo.blogNickName\");n==\"\"?\"\":\"👤 \"+n;}}{{i=java.getString(\"$.askCardVO.quotePost.postView.type||$.postData.postView.type||$.post.type||$.type\"),e={\"1\":\"【📃:文】\",\"2\":\"【🎨:图】\",\"4\":\"【📺:视频】\"},e[i]||\"\"}} \n🏷 {{$.postData.postView.tagList||$.post.tagList||$.postCollection.tagList||$.tagList||$.tags##\\s+## }} \n🧡 {{var f=Number(java.getString(\"$.askCardVO.quotePost.postCountView.favoriteCount||$.postData.postCount.favoriteCount||$.post.postCount.favoriteCount||$.postCount.favoriteCount||$.favoriteCount\"));var c=f>=10000?(Math.round(f\/1000)\/10)+'w':f;c;}} ⭐ {{var f=Number(java.getString(\"$.askCardVO.quotePost.postCountView.subscribeCount||$.postData.postCount.subscribeCount||$.post.postCount.subscribeCount||$.postCount.subscribeCount||$.subscribedCount\"));var c=f>=10000?(Math.round(f\/1000)\/10)+'w':f;c;}} 🗨️ {{var f=Number(java.getString(\"$.askCardVO.quotePost.postCountView.responseCount||$.postData.postCount.responseCount||$.post.postCount.responseCount||$.postCount.responseCount||$.responseCount\"));var c=f>=10000?(Math.round(f\/1000)\/10)+'w':f;c;}}##\\n*([🗓🏷]\\s+\\n|[🧡⭐🗨️ 0]{14})$|<[^<>]+>",
"ruleTitle": "$.askCardVO.quotePost.postView.title||$.askCardVO.quotePost.postView.digest||$.postData.postView.title||$.post.title||$.title||$.noticeLinkTitle||$.post.noticeLinkTitle||$.postData.postView.noticeLinkTitle||$.digest||$.post.digest||$.postData.postView.digest||$.postData.blogInfo.blogNickName||$.name||$.postCollection.name||$.askCardVO.quotePost.postView.tagList[0]||$.postData.postView.tagList[0]||$.tagList[0]||$.post.tagList[0]||##<[^<>]*>?|(?<=\\s)\\s+@js:result.trim();",
"searchUrl": "@js:\nhttps:\/\/api.lofter.com\/newsearch\/post.json?key={{key}}&sortType=0&offset={{(page-1)*20}}&limit=20,{\"headers\":{\"Content-Type\":\"application\/x-www-form-urlencoded;charset=utf-8\",\"deviceid\":\"{{java.androidId()}}\"}}",
"shouldOverrideUrlLoading": "if(url.match(\/^https?:\\\/\\\/([\\w-]+)\\.lofter\\.com\\\/post\\\/(\\w+)_(\\w+)\/)){\n var m = url.match(\/^https?:\\\/\\\/([\\w-]+)\\.lofter\\.com\\\/post\\\/(\\w+)_(\\w+)\/);\n java.open(\"rss\", \"https:\/\/api.lofter.com\/oldapi\/post\/detail.api?product=lofter-android-8.3.20,\" + JSON.stringify({method:\"POST\",\"accept-encoding\":\"gzip, deflate\", body:\"targetblogid=\" + parseInt(m[2],16) + \"&blogdomain=\" + m[1] + \".lofter.com&postid=\" + parseInt(m[3],16)}));\n}\nelse if(\/front\\\/blog\\\/collection\/.test(url) && url.match(\/collectionId=(\\d+)\/)){\n var m = url.match(\/collectionId=(\\d+)\/);\n java.open(\"sort\", \"https:\/\/api.lofter.com\/v1.1\/postCollection.api?product=lofter-android-8.0.2&method=getCollectionDetail&offset={{(page-1)*45}}&limit=45&collectionid=\" + m[1]);\n}\nelse if(url.match(\/blogId=(\\d+)\/)){\n var m = url.match(\/blogId=(\\d+)\/);\n java.open(\"sort\", \"https:\/\/api.lofter.com\/v2.0\/blogHomePage.api?product=lofter-android-7.4.4,\" + JSON.stringify({method:\"POST\", body:\"supportposttypes=1%2C2%2C3%2C4%2C5%2C6&targetblogid=\" + m[1] + \"&offset={{(page-1)*18}}&method=getPostLists&postdigestnew=1&returnData=1&limit=18&checkpwd=1&needgetpoststat=1\"}));\n}\nelse if(url.match(\/home-page\\\/([^\/?#]+)\/) || url.match(\/^https?:\\\/\\\/([\\w-]+)\\.lofter\\.com\\\/?$\/)){\n var m = url.match(\/home-page\\\/([^\/?#]+)\/) || url.match(\/^https?:\\\/\\\/([\\w-]+)\\.lofter\\.com\\\/?$\/);\n var n = m[1].replace(\/\\.lofter\\.com$\/,\"\");\n if(n !== \"www\") java.open(\"sort\", \"https:\/\/api.lofter.com\/v2.0\/blogHomePage.api?product=lofter-android-7.4.4,\" + JSON.stringify({method:\"POST\", body:\"supportposttypes=1%2C2%2C3%2C4%2C5%2C6&blogdomain=\" + n + \".lofter.com&offset={{(page-1)*18}}&method=getPostLists&postdigestnew=1&returnData=1&limit=18&checkpwd=1&needgetpoststat=1\"}));\n}",
"showWebLog": true,
"singleUrl": false,
"sortUrl": "@js:let config = {};\nconst raw = source.getVariable();\nif (raw && raw.trim()) {\n try {\n config = JSON.parse(raw);\n } catch (e) {}\n}\n\nconst type = config.type || '';\nconst find = config.find || '';\n\nconst tags = (Array.isArray(config.tags) ? config.tags : [])\n .filter(t => typeof t === 'string' && t.trim())\n .map(t => t.trim());\n\nconst collections = (Array.isArray(config.collections) ? config.collections : [])\n .filter(c => c && c.name && c.id)\n .filter(c => !(String(c.hidden) === \"1\"))\n .map(c => ({ name: String(c.name).trim(), id: String(c.id).trim() }));\n\nconst authors = (Array.isArray(config.authors) ? config.authors : [])\n .filter(a => a && a.name && a.blogId)\n .filter(a => String(a.hidden) !== \"2\")\n .map(a => ({ name: String(a.name).trim(), blogId: String(a.blogId).trim() }));\n \nlet entries = [];\n\nif (tags.length) {\n tags.forEach(tag => {\n entries.push({\n label: '🏷️ ' + tag + '·发现',\n url: 'https:\/\/api.lofter.com\/recommend\/tagRecom.json?lofter-android-8.3.20' +\n ',{\"method\": \"POST\",\"accept-encoding\":\"gzip, deflate\",\"body\":\"offset={{(page-1)*10}}&count=2&tag=' + encodeURIComponent(tag) + '&source=\",\"headers\":{\"Content-Type\":\"application\/x-www-form-urlencoded;charset=utf-8\",\"deviceid\":\"{{java.androidId()}}\"}}',\n type: 'tag'\n });\n \n entries.push({\n label: '🏷️ ' + tag + '·热门', \/\/ 同「综合」\n url: 'https:\/\/api.lofter.com\/newsearch\/v2\/all.json?product=lofter-android-8.3.64&sortType=0&source=hot&version=1&key=' + encodeURIComponent(tag),\n type: 'tag'\n });\n \n entries.push({\n label: '🏷️ ' + tag + '·动态',\n url: 'https:\/\/api.lofter.com\/question\/tagChat\/pageNew.json?offset={{(page-1)*20}}&tag=' + encodeURIComponent(tag),\n type: 'tag'\n });\n \n entries.push({\n label: '🏷️ ' + tag + '·评论', \n url: 'https:\/\/api.lofter.com\/newapi\/tagPosts.json' + \n ',{\"method\": \"POST\",\"accept-encoding\":\"gzip, deflate\", \"body\":\"product=lofter-android-8.3.20&offset=0&type=newComment&postTypes=&tag=' + encodeURIComponent(tag) + '\"}',\n type: 'tag'\n });\n \n entries.push({\n label: '🏷️ ' + tag + '·粮单',\n url: 'https:\/\/api.lofter.com\/newsearch\/grain.json?limit=10&offset={{(page-1)*10}}&key=' + encodeURIComponent(tag),\n type: 'tag'\n });\n \n [['最新','new'],['日榜','date'],['周榜','week'],['月榜','month'],['总榜','total']].forEach(([suffix, param]) => {\n entries.push({\n label: '🏷️ ' + tag + '·' + suffix,\n url: 'https:\/\/api.lofter.com\/oldapi\/tagPosts.api?product=lofter-android-8.0.2&offset={{(page-1)*12}}&firstpermalink=&postTypes=' + type + '&tag=' + encodeURIComponent(tag) + '&type=' + param + ',{\"method\":\"POST\",\"accept-encoding\":\"gzip, deflate\",\"body\":\"null\"}',\n type: 'tag'\n });\n });\n });\n} else {\n [\n ['发现', 'https:\/\/api.lofter.com\/recommend\/exploreRecom.json?product=lofter-android-8.3.20,{\"method\":\"POST\",\"body\":\"offset={{(page-1)*10}}&feedTime=0&count=0\"}'],\n ['绘画', 'https:\/\/api.lofter.com\/recommend\/domainPosts.json?product=lofter-android-8.3.20,{\"method\":\"POST\",\"body\":\"domainId=1370406202794&offset={{(page-1)*10}}&feedTime=0&count=0\"}'],\n ['影视', 'https:\/\/api.lofter.com\/recommend\/domainPosts.json?product=lofter-android-8.3.20,{\"method\":\"POST\",\"body\":\"domainId=1442207751226&offset={{(page-1)*10}}&feedTime=0&count=0\"}'],\n ['二次元', 'https:\/\/api.lofter.com\/recommend\/domainPosts.json?product=lofter-android-8.3.20,{\"method\":\"POST\",\"body\":\"domainId=1370406175120&offset={{(page-1)*10}}&feedTime=0&count=0\"}'],\n ['娱乐', 'https:\/\/api.lofter.com\/recommend\/domainPosts.json?product=lofter-android-8.3.20,{\"method\":\"POST\",\"body\":\"domainId=1430907486999&offset={{(page-1)*10}}&feedTime=0&count=0\"}'],\n ['视频', 'https:\/\/api.lofter.com\/recommend\/videoFlow.json?offset={{(page-1)*10}}&limit=10']\n ].forEach(([label, url]) => {\n entries.push({ label, url, type: 'tag' });\n });\n}\n\ncollections.forEach(c => {\n entries.push({\n label: '📖 ' + c.name,\n url: 'https:\/\/api.lofter.com\/v1.1\/postCollection.api?product=lofter-android-8.0.2&method=getCollectionDetail&offset={{(page-1)*45}}&limit=45&collectionid=' + c.id,\n type: 'collection'\n });\n});\n\nauthors.forEach(a => {\n entries.push({\n label: '👤 ' + a.name,\n url: 'https:\/\/api.lofter.com\/v2.0\/blogHomePage.api?product=lofter-android-7.4.4,' +\n JSON.stringify({\n method: 'POST',\n body: 'supportposttypes=1%2C2%2C3%2C4%2C5%2C6&targetblogid=' + a.blogId +\n '&offset={{(page-1)*18}}&method=getPostLists&postdigestnew=1&returnData=1&limit=18&checkpwd=1&needgetpoststat=1'\n }),\n type: 'author'\n });\n});\n\nif (find === '1') {\n entries = entries.filter(e => e.type === 'tag');\n} else if (find === '2') {\n entries = entries.filter(e => e.type === 'collection');\n} else if (find === '3') {\n entries = entries.filter(e => e.type === 'author');\n}\n\nlet lists = entries.map(e => e.label + '::' + e.url).join('\\n');\nlists;",
"sourceComment": "\/* =========================\n继续优化页面链接缓存本地相关代码\n修改:点击 hls.js 加载 \/ 长按视频播放\n增加听书功能,先简单走视频播放函数\n===========================\n增加缓存文章功能,可在文件管理查看\n添加图片竖排显示模式,点击页码切换\n调整启动页合集更新时间上的显示模式\n修改图标字体,显示细节更趋近原应用\n增加一些零碎的 toast 提示\n===========================\n增加 java.ajax 降级处理,兼容性调整\n增加点击合集名称打开侧栏目录\n增加投票展示页面,优化段评显示标记\n震动报错修复:用户未交互前禁止调用\n===========================\n为图片显示错误添加回退或隐藏补丁\n修复自定义的[图片][表情]评论表情包\n增加对评论数据的去重复处理\nsourceIcon 采用 Base64 本地化方案\n图标字体本地化:已合并 18 个字码点\n===========================\n增加列表和文章已注销\/隐藏\/删除提示\n移除部分已停用函数\/变量\/死代码等\n===========================\n简单兼容原版阅读变量存储(仅可用)\n将登录 UI 改为 source.setVariable()\nsource.putVariable()\/.setVariable()\n===========================\n历史分栏增加「记录导入导出」功能\n将上\/下一篇数据存储到本地「六小时」\n文章读取标题 java.open(\"rss\",url,title)\n===========================\n增加「好粮榜、新评榜、动态」\n当前动态为其打开原文,列表自动去重\n原动态方案有可能会造成单文评论刷屏\n原动态需移除链接规则的 askCardVO\n===========================\n迁移评论及其相关函数到 InjectJs 区域\n段评标记改为微博表情图片(仅日间)\n===========================\n优化彩蛋相关代码,调整其需解锁信息\n增加了「关闭评论」的选项,静默阅读\n===========================\n增加了醒目的「首次使用请点击此处」\n设置完成后刷新下就会自动隐藏本按钮\n===========================\n当前版本瀑布流列表的显示有串图问题\n解决方式:给无图列表配置空白图\n更新内容:重写了空白图 Base64\n===========================\n●登录填要看的内容标签然后刷新分类\n※请在登录数据中配置自己的手机令牌\n\"lofter-phone-login-auth\":\"\"\n========================= *\/",
"sourceGroup": "💽 博客",
"sourceIcon": "data:image\/svg;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMTQiIGhlaWdodD0iMTE0IiB2aWV3Qm94PSIwIDAgMzggMzgiPjxwYXRoIGZpbGw9IiMzMzYxNjIiIGQ9Ik0xOSAzOGExOSAxOSAwIDEgMCAwLTM4IDE5IDE5IDAgMCAwIDAgMzgiLz48cGF0aCBmaWxsPSIjZmZmIiBkPSJNMTMuODMgMTEuMmMwLS4zLjI2LS41My41OC0uNTNhMSAxIDAgMCAxIC4yMy4wNSAzIDMgMCAwIDEgLjc1LjQzcS4zLjI3LjUuNmMuMjkuNDguNC45OS40IDEuODl2Ny4xM2MwIC43OC42IDEuNDMgMS40IDEuNi4yMi4wNi40NC0uMS40NC0uMzF2LTguNDhjMC0xLjEzLS4yLTEuOS0uNjEtMi42YTQgNCAwIDAgMC0xLjgtMS42NCA0IDQgMCAwIDAtMS44OS0uMzNBMS45NyAxLjk3IDAgMCAwIDEyIDEwLjk0djEwYzAgMS4xNi4yIDEuOTMuNjQgMi42NnEuNjIgMSAxLjcyIDEuNTZjLjU4LjI4IDEuMTcuNDUgMS45LjUycS41LjA2IDEuMS4wNmg0LjQ1Yy45OSAwIDEuNTYuMSAyLjA3LjM1YTIuNSAyLjUgMCAwIDEgMS4wNi45NmwuMTMuMjYuMDIuMTNjMCAuMjktLjI2LjUyLS41OC41MmgtMi45MnEtLjY0IDAtMS4wOS0uMDdhMyAzIDAgMCAxLS45Mi0uMjhjLS4yNC0uMTItLjQzLS4zLS42NC0uNDQtLjYtLjM5LTEuMzYtLjMtMS42Ny0uMjctLjMuMDQtLjQ5LjMyLS4zNi41M3MuNDMuNjMuNy45cS40OS40NSAxLjEuNzRjLjQ2LjIyLjkyLjM2IDEuNDcuNDVxLjYzLjEgMS40Ny4xaDNjMS41IDAgMi41LTEuMDQgMi4zMy0yLjFhMyAzIDAgMCAwLS40My0xLjI1cS0uMjctLjQ3LS42Ni0uODRhNCA0IDAgMCAwLTEuMTUtLjggNiA2IDAgMCAwLTIuODctLjU1aC00LjU4cS0uNDYgMC0uODEtLjA0YTQgNCAwIDAgMS0xLjEtLjI3Yy0uNTEtLjIzLS45NC0uNjEtMS4yLTEuMDhhMy41IDMuNSAwIDAgMS0uMzUtMS44eiIvPjwvc3ZnPg==",
"sourceName": "LOFTER",
"sourceUrl": "https:\/\/www.lofter.com",
"startHtml": "<svg style=\"display:none\">\n <symbol id=\"eyeOpen\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\" \/><circle cx=\"12\" cy=\"12\" r=\"3\" \/><\/symbol>\n <symbol id=\"eyeClosed\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24\" \/><line x1=\"1\" y1=\"1\" x2=\"23\" y2=\"23\" \/><\/symbol>\n <symbol id=\"eyeCleand\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z\" \/><circle cx=\"12\" cy=\"12\" r=\"3\" \/><line x1=\"4\" y1=\"4\" x2=\"20\" y2=\"20\" \/><\/symbol>\n <symbol id=\"trash\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"3 6 5 6 21 6\" \/><path d=\"M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2\" \/><line x1=\"10\" y1=\"11\" x2=\"10\" y2=\"17\" \/><line x1=\"14\" y1=\"11\" x2=\"14\" y2=\"17\" \/><\/symbol>\n <symbol id=\"editor\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 20h9\" \/><path d=\"M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z\" \/><path d=\"M15 5l3 3\" \/><\/symbol>\n <symbol id=\"drag\" viewBox=\"0 0 20 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"4\" y1=\"7\" x2=\"20\" y2=\"7\" \/><line x1=\"4\" y1=\"12\" x2=\"20\" y2=\"12\" \/><line x1=\"4\" y1=\"17\" x2=\"20\" y2=\"17\" \/><\/symbol>\n <symbol id=\"note\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-lnejoin=\"round\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"\/><path d=\"M7 8h8\"\/><path d=\"M7 12h10\"\/><path d=\"M7 16h6\"\/><\/symbol>\n <symbol id=\"image\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" ry=\"2\"\/><circle cx=\"9\" cy=\"9\" r=\"2\"\/><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"\/><\/symbol>\n <symbol id=\"video\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"\/><path d=\"M7 3v18\"\/><path d=\"M3 7.5h4\"\/><path d=\"M3 12h18\"\/><path d=\"M3 16.5h4\"\/><path d=\"M17 3v18\"\/><path d=\"M17 7.5h4\"\/><path d=\"M17 16.5h4\"\/><\/symbol>\n <symbol id=\"star\" viewBox=\"0 0 24 24\" fill=\"currentColor\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"><path d=\"M12 2.6l2.95 5.98 6.6.96-4.77 4.65 1.12 6.57L12 17.66l-5.9 3.1 1.12-6.57L2.45 9.54l6.6-.96L12 2.6z\" \/><\/symbol>\n <symbol id=\"staro\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linejoin=\"round\"><path d=\"M12 2.6l2.95 5.98 6.6.96-4.77 4.65 1.12 6.57L12 17.66l-5.9 3.1 1.12-6.57L2.45 9.54l6.6-.96L12 2.6z\" \/><\/symbol>\n <symbol id=\"filevideo\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15 4V8H19V20H5V4H15ZM3.9985 2C3.44749 2 3 2.44405 3 2.9918V21.0082C3 21.5447 3.44476 22 3.9934 22H20.0066C20.5551 22 21 21.5489 21 20.9925L20.9997 7L16 2H3.9985ZM15.0008 11.667L10.1219 8.41435C10.0562 8.37054 9.979 8.34717 9.9 8.34717C9.6791 8.34717 9.5 8.52625 9.5 8.74717V15.2524C9.5 15.3314 9.5234 15.4086 9.5672 15.4743C9.6897 15.6581 9.9381 15.7078 10.1219 15.5852L15.0008 12.3326C15.0447 12.3033 15.0824 12.2656 15.1117 12.2217C15.2343 12.0379 15.1846 11.7895 15.0008 11.667Z\" \/><\/symbol>\n <symbol id=\"filenote\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M21 8V20.9932C21 21.5501 20.5552 22 20.0066 22H3.9934C3.44495 22 3 21.556 3 21.0082V2.9918C3 2.45531 3.4487 2 4.00221 2H14.9968L21 8ZM19 9H14V4H5V20H19V9ZM8 7H11V9H8V7ZM8 11H16V13H8V11ZM8 15H16V17H8V15Z\" \/><\/symbol>\n <symbol id=\"fileimage\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M15 8V4H5V20H19V8H15ZM3 2.9918C3 2.44405 3.44749 2 3.9985 2H16L20.9997 7L21 20.9925C21 21.5489 20.5551 22 20.0066 22H3.9934C3.44476 22 3 21.5447 3 21.0082V2.9918ZM11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8C10.3284 8 11 8.67157 11 9.5ZM17.5 17L13.5 10L8 17H17.5Z\" \/><\/symbol>\n<\/svg>\n\n<div class=\"filter-bar\">\n <button class=\"filter-btn active\" data-filter=\"all\">\n <svg viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect width=\"7\" height=\"7\" x=\"3\" y=\"3\" rx=\"1\"\/><rect width=\"7\" height=\"7\" x=\"14\" y=\"3\" rx=\"1\"\/><rect width=\"7\" height=\"7\" x=\"14\" y=\"14\" rx=\"1\"\/><rect width=\"7\" height=\"7\" x=\"3\" y=\"14\" rx=\"1\"\/><\/svg>\n <span>全部<\/span>\n <\/button>\n <button class=\"filter-btn\" data-filter=\"tag\">\n <svg viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12.586 2.586A2 2 0 0 0 11.172 2H4a2 2 0 0 0-2 2v7.172a2 2 0 0 0 .586 1.414l8.704 8.704a2.426 2.426 0 0 0 3.42 0l6.58-6.58a2.426 2.426 0 0 0 0-3.42z\"\/><circle cx=\"7.5\" cy=\"7.5\" r=\".5\" fill=\"currentColor\"\/><\/svg> \n <span>标签<\/span>\n <\/button>\n <button class=\"filter-btn\" data-filter=\"col\">\n <svg viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20\"\/><\/svg>\n <span>合集<\/span>\n <\/button>\n <button class=\"filter-btn\" data-filter=\"author\">\n <svg viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M18 21a8 8 0 0 0-16 0\"\/><circle cx=\"10\" cy=\"8\" r=\"5\"\/><path d=\"M22 20c0-3.37-2-6.5-4-8a5 5 0 0 0-.45-8.3\"\/><\/svg>\n <span>作者<\/span>\n <\/button>\n <button class=\"filter-btn\" data-filter=\"article\">\n <svg viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\" \/><polyline points=\"14 2 14 8 20 8\" \/><line x1=\"16\" y1=\"13\" x2=\"8\" y2=\"13\" \/><line x1=\"16\" y1=\"17\" x2=\"8\" y2=\"17\" \/><\/svg>\n <span>文章<\/span>\n <\/button>\n <button class=\"filter-btn\" data-filter=\"history\">\n <svg viewBox=\"0 0 24 24\" width=\"1em\" height=\"1em\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"12\" cy=\"12\" r=\"10\"\/><path d=\"M12 6v6l4 2\"\/><\/svg>\n <span>历史<\/span>\n <\/button>\n <button class=\"toggle-btn\" id=\"toggleHidden\" title=\"查看模式\">\n <!-- 查看 -->\n <svg class=\"eye-open\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 20 20\" width=\"20\" height=\"20\" fill=\"currentColor\"><path d=\"M10,6c-2.2,0-4,1.8-4,4s1.8,4,4,4s4-1.8,4-4S12.2,6,10,6z M7.9,10c0-1.2,1-2.1,2.1-2.1s2,1,2,2.1s-1,2-2,2S7.9,11.1,7.9,10z\" \/><path d=\"M19.6,9.7c0,0.1,0,0.2,0,0.3s0,0.2,0,0.3c-1.4,4.2-5.2,7.3-9.7,7.3s-8.2-3.1-9.6-7.3c0-0.1,0-0.2,0-0.3s0-0.2,0-0.3C1.7,5.5,5.5,2.4,10,2.4S18.3,5.5,19.6,9.7z M10,15.5c3.4,0,6.2-2.2,7.5-5.5c-1.2-3.3-4.1-5.6-7.5-5.6S3.8,6.6,2.5,10C3.8,13.3,6.6,15.5,10,15.5z\" \/><\/svg>\n <svg class=\"eye-closed\" style=\"display:none\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 20 20\" width=\"20\" height=\"20\" fill=\"currentColor\"><path d=\"M18.7,6.2C19,5.6,18.7,5,18.1,4.8c-0.6-0.2-1.2,0.1-1.4,0.7c-0.9,2.5-3.4,4.3-6.5,4.3c-3,0-5.5-1.8-6.5-4.3C3.5,4.9,2.9,4.6,2.3,4.8C1.7,5,1.5,5.7,1.7,6.2C2.2,7.6,3,8.7,4.1,9.7l-1.5,2.2c-0.3,0.5-0.2,1.2,0.3,1.5c0.5,0.3,1.2,0.2,1.5-0.3l1.5-2.2c1,0.5,2,0.8,3.2,1v2.3c0,0.6,0.5,1.1,1.1,1.1c0.6,0,1.1-0.5,1.1-1.1v-2.3c1.1-0.1,2.2-0.5,3.2-1l1.5,2.2c0.3,0.5,1,0.6,1.5,0.3c0.5-0.3,0.6-1,0.3-1.5l-1.5-2.2C17.4,8.7,18.2,7.6,18.7,6.2z\" \/><\/svg>\n <!-- -->\n <\/button>\n <button class=\"manage-btn\" id=\"manageBtn\" title=\"管理模式\">\n <!-- 管理 -->\n <svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" viewBox=\"0 0 20 20\" width=\"18\" height=\"18\" fill=\"currentColor\"><path d=\"M11.6,3.8c1,0,1.2,0,1.6,0C14.4,4,15.4,5,15.7,6.3c0,0.2,0,0.6,0,1.6c0,0.7,0.6,1.2,1.2,1.2c0.7,0,1.2-0.6,1.2-1.2V7.8c0-0.8,0-1.3-0.1-1.8c-0.4-2.3-2.2-4.1-4.6-4.6c-0.4-0.1-1-0.1-1.8-0.1h-0.1H9.4c-1.2,0-2.1,0-2.9,0.1C5.8,1.6,5,1.7,4.3,2C3.2,2.6,2.4,3.4,1.9,4.4C1.6,5.1,1.4,5.9,1.3,6.7C1.2,7.3,1.2,8.2,1.2,9.4l0,0v1.8l0,0c0,1.2,0,2.1,0.1,2.9c0.1,0.8,0.2,1.6,0.6,2.2c0.6,1.1,1.4,1.9,2.4,2.4c0.7,0.3,1.4,0.4,2.2,0.6s1.8,0.1,2.9,0.1l0,0h0.8c0.7,0,1.2-0.6,1.2-1.2c0-0.7-0.6-1.2-1.2-1.2H9.6c-1.2,0-2.1,0-2.8,0s-1-0.1-1.2-0.3c-0.6-0.3-1.1-0.8-1.3-1.3c-0.1-0.2-0.2-0.7-0.3-1.2c0-0.7,0-1.6,0-2.8V9.6c0-1.2,0-2.1,0-2.8s0.1-1,0.3-1.2c0.2-0.7,0.7-1.1,1.2-1.4C5.7,4,6.1,3.9,6.7,3.8c0.7,0,1.6,0,2.8,0H11.6zM6.9,6.2c-0.7,0-1.2,0.6-1.2,1.2s0.6,1.2,1.2,1.2h5c0.7,0,1.2-0.6,1.2-1.2s-0.6-1.2-1.2-1.2H6.9z M14.9,15c0-0.8,0.6-1.3,1.3-1.3c0.8,0,1.3,0.6,1.3,1.3s-0.6,1.3-1.3,1.3C15.4,16.3,14.9,15.8,14.9,15z M16.2,11.4c-2,0-3.6,1.7-3.6,3.6c0,2,1.7,3.6,3.6,3.6s3.6-1.7,3.6-3.6C19.9,13,18.2,11.4,16.2,11.4z M5.7,11.9c0-0.7,0.6-1.2,1.2-1.2h2.6c0.7,0,1.2,0.6,1.2,1.2s-0.6,1.2-1.2,1.2H6.9C6.2,13.1,5.7,12.6,5.7,11.9z\" \/><\/svg>\n <!-- -->\n <\/button>\n<\/div>\n\n<div id=\"addArea\" class=\"add-area-container\" style=\"display:none;\">\n <div id=\"tagAddArea\" class=\"sub-add-area\">\n <button id=\"filterCleandBtn\" class=\"filter-cleand-btn\" title=\"筛选\">\n <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"currentColor\"><path d=\"M12.6844 4.02547C12.4588 4.00646 12.2306 3.9967 12 3.9967C7.58172 3.9967 4 7.57843 4 11.9967C4 16.415 7.58172 19.9967 12 19.9967C16.4183 19.9967 20 16.415 20 11.9967C20 11.7662 19.9902 11.5379 19.9711 11.3123C19.8996 10.4646 19.6953 9.6442 19.368 8.87344L20.8682 7.37114C21.2031 8.01192 21.4706 8.6935 21.6613 9.40649C21.8213 10.0063 21.9258 10.6222 21.9723 11.248C21.9907 11.4952 22 11.7449 22 11.9967C22 17.5196 17.5228 21.9967 12 21.9967C6.47715 21.9967 2 17.5196 2 11.9967C2 6.47386 6.47715 1.9967 12 1.9967C12.2518 1.9967 12.5015 2.00601 12.7487 2.02431C13.3745 2.07081 13.9904 2.17542 14.5898 2.3358C15.3032 2.52609 15.9848 2.79359 16.6256 3.1285L15.1247 4.62934C14.3525 4.30143 13.5321 4.09707 12.6844 4.02547ZM20.4853 2.09721L21.8995 3.51142L12.7071 12.7038L11.2954 12.7063L11.2929 11.2896L20.4853 2.09721Z\"\/><\/svg>\n <\/button>\n <input id=\"newTagInput\" placeholder=\"标签名称\">\n <button id=\"addTagBtn\">添加<\/button>\n <\/div>\n <div id=\"colAddArea\" class=\"sub-add-area\" style=\"display:none;\">\n <button id=\"subUpdateBtn\" class=\"sub-update-btn\" title=\"更新数据\">\n <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"currentColor\"><path d=\"M12 4C9.25144 4 6.82508 5.38626 5.38443 7.5H8V9.5H2V3.5H4V5.99936C5.82381 3.57166 8.72764 2 12 2C17.5228 2 22 6.47715 22 12H20C20 7.58172 16.4183 4 12 4ZM4 12C4 16.4183 7.58172 20 12 20C14.7486 20 17.1749 18.6137 18.6156 16.5H16V14.5H22V20.5H20V18.0006C18.1762 20.4283 15.2724 22 12 22C6.47715 22 2 17.5228 2 12H4Z\"\/><\/svg>\n <\/button>\n <input id=\"newColNameInput\" placeholder=\"合集名称\">\n <input id=\"newColIdInput\" placeholder=\"ID\">\n <button id=\"addColBtn\">添加<\/button>\n <\/div>\n <div id=\"authorAddArea\" class=\"sub-add-area\" style=\"display:none;\">\n <input id=\"newAuthorNameInput\" placeholder=\"作者名称\">\n <input id=\"newAuthorIdInput\" placeholder=\"BID\">\n <button id=\"addAuthorBtn\">添加<\/button>\n <\/div>\n <div id=\"articleAddArea\" class=\"sub-add-area\" style=\"display:none;\">\n <input id=\"newArticleTitleInput\" placeholder=\"文章名称\">\n <input id=\"newArticleUrlInput\" placeholder=\"URL\">\n <button id=\"addArticleBtn\">添加<\/button>\n <\/div>\n <div id=\"historyArea\" class=\"sub-add-area\" style=\"display:none;\">\n <button id=\"clearHistoryBtn\" class=\"history-clear-btn\" title=\"清空记录\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M8 19.9967V14.9967H10V19.9967H19V12.9967H5V19.9967H8ZM4 10.9967H20V7.9967H14V3.9967H10V7.9967H4V10.9967ZM3 20.9967V12.9967H2V6.9967C2 6.44442 2.44772 5.9967 3 5.9967H8V2.9967C8 2.44442 8.44772 1.9967 9 1.9967H15C15.5523 1.9967 16 2.44442 16 2.9967V5.9967H21C21.5523 5.9967 22 6.44442 22 6.9967V12.9967H21V20.9967C21 21.549 20.5523 21.9967 20 21.9967H4C3.44772 21.9967 3 21.549 3 20.9967Z\" \/><\/svg>\n <\/button>\n <button id=\"exportHistoryBtn\" class=\"history-clear-btn\" title=\"导出历史\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M6.9998 6V3C6.9998 2.44772 7.44752 2 7.9998 2H19.9998C20.5521 2 20.9998 2.44772 20.9998 3V17C20.9998 17.5523 20.5521 18 19.9998 18H16.9998V20.9991C16.9998 21.5519 16.5499 22 15.993 22H4.00666C3.45059 22 3 21.5554 3 20.9991L3.0026 7.00087C3.0027 6.44811 3.45264 6 4.00942 6H6.9998ZM5.00242 8L5.00019 20H14.9998V8H5.00242ZM8.9998 6H16.9998V16H18.9998V4H8.9998V6ZM7 11H13V13H7V11ZM7 15H13V17H7V15Z\" \/><\/svg>\n <\/button>\n <button id=\"importHistoryBtn\" class=\"history-clear-btn\" title=\"导入历史\">\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"currentColor\"><path d=\"M20 2C20.5523 2 21 2.44772 21 3V6.757L19 8.757V4H5V20H19V17.242L21 15.242V21C21 21.5523 20.5523 22 20 22H4C3.44772 22 3 21.5523 3 21V3C3 2.44772 3.44772 2 4 2H20ZM21.7782 8.80761L23.1924 10.2218L15.4142 18L13.9979 17.9979L14 16.5858L21.7782 8.80761ZM13 12V14H8V12H13ZM16 8V10H8V8H16Z\" \/><\/svg>\n <\/button>\n <input id=\"historyInput\" placeholder=\"\" disabled>\n <button id=\"searchBtn\" class=\"search-btn\" title=\"搜索\" style=\"display:none;\">\n <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"11\" cy=\"11\" r=\"8\" \/><line x1=\"21\" y1=\"21\" x2=\"16.65\" y2=\"16.65\" \/><\/svg>\n <\/button>\n <button id=\"exportDataBtn\" class=\"export-data-btn\" title=\"导出数据\">\n <svg viewBox=\"0 0 24 24\" width=\"16\" height=\"16\" fill=\"currentColor\"><path d=\"M8 1.5C8 0.947715 7.55228 0.5 7 0.5C6.44772 0.5 6 0.947715 6 1.5V2.5C6 2.50686 6.00042 2.51285 6.00081 2.51843C6.00385 2.56193 6.00516 2.58063 5.79289 2.79289L5.77277 2.81298C5.50599 3.07912 5 3.58391 5 4.5V5.5C5 6.05228 5.44772 6.5 6 6.5C6.55228 6.5 7 6.05228 7 5.5V4.5C7 4.49314 6.99958 4.48715 6.99919 4.48157C6.99615 4.43807 6.99484 4.41937 7.20711 4.20711L7.22723 4.18702C7.49401 3.92088 8 3.41609 8 2.5V1.5ZM19 1.5C19 0.947715 18.5523 0.5 18 0.5C17.4477 0.5 17 0.947715 17 1.5V2.5C17 2.50686 17.0004 2.51285 17.0008 2.51843C17.0038 2.56193 17.0052 2.58063 16.7929 2.79289L16.7728 2.81298C16.506 3.07912 16 3.58391 16 4.5V5.5C16 6.05228 16.4477 6.5 17 6.5C17.5523 6.5 18 6.05228 18 5.5V4.5C18 4.49314 17.9996 4.48715 17.9992 4.48157C17.9962 4.43807 17.9948 4.41937 18.2071 4.20711L18.2272 4.18702C18.494 3.92088 19 3.41609 19 2.5V1.5ZM12.5 0.5C13.0523 0.5 13.5 0.947715 13.5 1.5V2.5C13.5 3.41609 12.994 3.92088 12.7272 4.18702L12.7071 4.20711C12.4948 4.41937 12.4962 4.43807 12.4992 4.48157C12.4996 4.48715 12.5 4.49314 12.5 4.5V5.5C12.5 6.05228 12.0523 6.5 11.5 6.5C10.9477 6.5 10.5 6.05228 10.5 5.5V4.5C10.5 3.58391 11.006 3.07912 11.2728 2.81298L11.2929 2.79289C11.5052 2.58063 11.5038 2.56193 11.5008 2.51843C11.5004 2.51285 11.5 2.50686 11.5 2.5V1.5C11.5 0.947715 11.9477 0.5 12.5 0.5ZM4 10H20C20 14.4183 16.4183 18 12 18C7.58172 18 4 14.4183 4 10ZM3 8C2.44772 8 2 8.44771 2 9V10C2 14.1006 4.46819 17.6248 8 19.1679V20C8 20.5523 8.44772 21 9 21H15C15.5523 21 16 20.5523 16 20V19.1679C19.5318 17.6248 22 14.1006 22 10V9C22 8.44772 21.5523 8 21 8H3Z\"\/><\/svg>\n <\/button> \n <\/div>\n<\/div>\n\n<div id=\"list-content\"><\/div>",
"startJs": "(function() {\n'use strict';\n\n\/\/ if(window.__startInject) return;\n\/\/ window.__startInject = true;\nconst HISTORY_KEY = 'lofHistory';\nconst VERSION_KEY = 'lofVersion';\n\n\/\/ 宿主桥接\nconst Host = {\n ajax(u) { try { return java.ajax(String(u)); } catch (e) { return null; } },\n toast(m) { try { java.toast(String(m)); } catch (e) {} },\n open(k, u, t) { try { if(t) java.open(String(k), String(u), String(t)); else java.open(String(k), String(u)); } catch (e) {} },\n openUrl(u) { try { java.openUrl(String(u)); } catch (e) {} },\n getVariable() { try { return source.getVariable() || ''; } catch (e) { return ''; } },\n putVariable(v) { try { source.putVariable(String(v)); return true; } catch (e) { return false; } },\n \/\/ getVersion(){ try{ const v = source.get(VERSION_KEY); return v == null ? 0 : (Number(v) || 0); }catch(e){ return 0; } },\n putVersion(n) { try { source.put(VERSION_KEY, String(n)); return true; } catch (e) { return false; } },\n androidId() { try { return java.androidId() || ''; } catch (e) { return ''; } },\n base64Encode(s) { try { return java.base64Encode(String(s)); } catch (e) { return ''; } },\n timeFormatUTC(n, f, t) { try { return java.timeFormatUTC(n, f, t); } catch (e) { return ''; } },\n vibrate(ms) { try { if(navigator.vibrate) navigator.vibrate(ms); } catch (e) {} }\n};\n\n\/* ===== 常量 ===== *\/\nconst API_BASE = 'https:\/\/api.lofter.com';\nconst VER = { R1: '8.3.20', R2: '8.0.2', R3: '7.4.4', R4: '8.3.64' };\nconst PAGE = { TAG: 12, COLL: 15, AUTH: 18 };\nconst DRAG_HOLD_MS = 350, DRAG_SLOP = 10;\nconst SCHEMA_VERSION = 1;\n\n\/* 类型 → Store 字段名,拖拽 \/ 排序共用同一张表 *\/\nconst TYPE_TO_FIELD = {\n tag:'tags', cleandTag:'cleandTags',\n col:'collections', author:'authors',\n article:'articles', history:'history'\n};\n\/* 值本身就是字符串的类型 *\/\nconst STRING_TYPES = { tag: 1, cleandTag: 1 };\n\nconst PERSIST_FIELDS = [\n 'tags','hiddenTags',\n 'cleandTags','disCleandTags',\n 'collections','authors','articles',\n 'find','type','allType'\n];\nconst PERSIST_SET = new Set(PERSIST_FIELDS);\n\nconst RANKS = [\n { sortNo: 1, type: 'hotword', label: '热词' },\n { sortNo: 3, type: 'hotarticle', label: '热文' },\n { sortNo: 5, type: 'hotuser', label: '太太' },\n { sortNo: 6, type: 'hottag', label: '游戏' },\n { sortNo: 7, type: 'hottag', label: '影视' },\n { sortNo: 8, type: 'hottag', label: '二次元' }\n];\n\nconst ALL_TYPES = [\n { key: 'tag', label: '标签' },\n { key: 'col', label: '合集' },\n { key: 'author', label: '作者' },\n { key: 'article', label: '文章' }\n];\n\nconst HDR_TAG_POST = JSON.stringify({\n'accept-encoding':'gzip, deflate',\nmethod:'POST',\nbody:'null'\n});\n\nconst Hidden = { SHOW: '0', HIDE: '1', CLEAN: '2' };\n\n\/* ===== 纯函数工具 ===== *\/\nfunction trimStr(s) { return String(s == null ? '' : s).trim(); }\n\nfunction findItem(arr, predicate) { return arr.find(predicate) || null; }\n\nfunction escapeHtml(s) {\n return String(s).replace(\/[&<>\"']\/g, c => ({ '&': '&', '<': '<', '>': '>', '\"': '"', \"'\": ''' }[c]));\n}\n\nfunction escapeAttr(o) { return escapeHtml(JSON.stringify(o)); }\n\nfunction extractDigits(s) { const m = trimStr(s).match(\/\\d+\/); return m ? m[0] : ''; }\n\nfunction extractColId(s) {\n const t = trimStr(s);\n if(\/^\\d+$\/.test(t)) return t;\n const m = t.match(\/collection[Ii]d=(\\d+)\/i);\n return m ? m[1] : extractDigits(t);\n}\n\nfunction extractBlogId(s) {\n const t = trimStr(s);\n if(\/^\\d+$\/.test(t)) return t;\n const m = t.match(\/blog[Ii]d=(\\d+)\/i);\n if(m) return m[1];\n const h = t.match(\/home-page\\\/([^\\\/?#]+)\/);\n return h ? h[1].replace(\/\\.lofter\\.com$\/i, '') : extractDigits(t);\n}\n\nfunction parseColCount(s) {\n if(!s || typeof s !== 'string') return null;\n const p = s.split('\/');\n if(p.length !== 3) return null;\n const o = parseInt(p[1], 10), n = parseInt(p[2], 10);\n if(isNaN(o) || isNaN(n)) return null;\n return { lastDate: p[0], oldCount: o, newCount: n };\n}\n\nfunction normalizeAllType(v) {\n const d = {};\n for (let i = 0; i < ALL_TYPES.length; i++) d[ALL_TYPES[i].key] = 1;\n if(!v || typeof v !== 'object') return d;\n for (let i = 0; i < ALL_TYPES.length; i++) {\n const k = ALL_TYPES[i].key;\n if(k in v) {\n const x = v[k];\n d[k] = (x === 0 || x === '0' || x === false || x === 'false') ? 0 : 1;\n }\n }\n return d;\n}\n\nfunction safeUrl(u) {\n if(!u) return '';\n u = String(u);\n if(\/^https?:\\\/\\\/\/i.test(u)) return u;\n if(\/^\\\/\\\/\/.test(u)) return 'https:' + u;\n if(\/^data:image\\\/\/i.test(u)) return u;\n return '';\n}\n\nfunction stripHtml(s) {\n if(!s) return '';\n try {\n const doc = new DOMParser().parseFromString('<div>' + String(s) + '<\/div>', 'text\/html');\n const el = doc.body.firstChild;\n return ((el && (el.textContent || '')) || '').replace(\/\\s+\/g, ' ').trim();\n } catch (e) {\n return String(s).replace(\/<[^>]*>\/g, '').replace(\/\\s+\/g, ' ').trim();\n }\n}\n\nfunction getToday() {\n const d = new Date();\n return d.getFullYear() + ('0' + (d.getMonth() + 1)).slice(-2) + ('0' + d.getDate()).slice(-2);\n}\n\nfunction fmtUpdate(ts) {\n let n = +ts; if (!n) return '';\n if (n < 1e12) n *= 1000;\n \/\/ return Host.timeFormatUTC(n, 'YYYY\/MM\/dd HH:mm', 8);\n const h = (Date.now() - n) \/ 36e5;\n if (h < 1) return '刚刚';\n if (h < 24) return Math.floor(h) + '小时前';\n if (h < 48) return '昨天';\n if (h < 24 * 30) return Math.floor(h \/ 24) + '天前';\n if (h < 24 * 365) return Math.floor(h \/ 24 \/ 30) + '个月前';\n return Math.floor(h \/ 24 \/ 365) + '年前';\n}\n\n\n\/* 显示键(visibleKeys)在全部(arr)中的数据重排 *\/\nfunction reorderByKey(arr, visibleKeys, keyOf) {\n if(!Array.isArray(arr) || !Array.isArray(visibleKeys)) return arr;\n if(!visibleKeys.length) return arr;\n const keySet = new Set(visibleKeys);\n const bucket = new Map();\n for (let i = 0; i < arr.length; i++) {\n const k = keyOf(arr[i]);\n if(!keySet.has(k)) continue;\n if(!bucket.has(k)) bucket.set(k, []);\n bucket.get(k).push(arr[i]);\n }\n const ordered = [];\n for (let i = 0; i < visibleKeys.length; i++) {\n const q = bucket.get(visibleKeys[i]);\n if(q && q.length) ordered.push(q.shift());\n }\n let oi = 0;\n for (let i = 0; i < arr.length && oi < ordered.length; i++) {\n if(keySet.has(keyOf(arr[i]))) {\n arr[i] = ordered[oi++];\n }\n }\n return arr;\n}\n\n\/* ===== 配置规范化 ===== *\/\nfunction normalizeCfg(cfg) {\n cfg = cfg || {};\n const arr = v => Array.isArray(v) ? v : [];\n return {\n cleandTags: arr(cfg.cleandTags).map(trimStr).filter(Boolean),\n disCleandTags: arr(cfg.disCleandTags).map(trimStr).filter(Boolean),\n tags: arr(cfg.tags).map(trimStr).filter(Boolean),\n collections: arr(cfg.collections).filter(c => c && typeof c === 'object').map(c => {\n const o = { name: trimStr(c.name), id: trimStr(c.id) };\n if(c.hidden != null) o.hidden = c.hidden;\n if(c.count != null) o.count = c.count;\n if(c.update && +c.update > 0) o.update = String(c.update);\n return o;\n }).filter(c => c.name && c.id),\n authors: arr(cfg.authors).filter(a => a && typeof a === 'object').map(a => {\n const o = { name: trimStr(a.name), blogId: trimStr(a.blogId) };\n if(a.hidden != null) o.hidden = a.hidden;\n if(a.avatar && trimStr(a.avatar) !== '') o.avatar = a.avatar;\n if(a.update && +a.update > 0) o.update = String(a.update);\n return o;\n }).filter(a => a.name && a.blogId),\n articles: arr(cfg.articles).filter(a => a && typeof a === 'object').map(a => ({\n title: trimStr(a.title),\n url: trimStr(a.url),\n hidden: String(a.hidden || Hidden.SHOW) === Hidden.HIDE ? Hidden.HIDE : Hidden.SHOW\n })).filter(a => a.title && a.url),\n hiddenTags: arr(cfg.hiddenTags).map(trimStr).filter(Boolean)\n };\n}\n\n\/* ===== Store ===== *\/\nconst Store = (function() {\n const state = {\n tags: [], hiddenTags: [], cleandTags: [], disCleandTags: [],\n collections: [], authors: [], articles: [], history: [],\n find: '', type: '', allType: null,\n ui: {\n currentFilter: 'all',\n manageMode: false,\n showHidden: false,\n showCleandTags: false,\n subscrCollections: null,\n currentRank: null\n }\n };\n const listeners = new Map();\n const dirtyFields = new Set();\n let flushTimer = null;\n let lastRaw = '';\n\n function on(ev, fn) {\n if(typeof fn !== 'function') return () => {};\n if(!listeners.has(ev)) listeners.set(ev, new Set());\n listeners.get(ev).add(fn);\n return () => { const s = listeners.get(ev); if(s) s.delete(fn); };\n }\n\n function emit(ev, payload) {\n const set = listeners.get(ev);\n if(!set) return;\n set.forEach(fn=>{\n try{ fn(payload); }catch(e){}\n });\n }\n\n function markDirty(fields){\n if(Array.isArray(fields)){\n for(let i=0;i<fields.length;i++) dirtyFields.add(fields[i]);\n }else if(fields) dirtyFields.add(fields);\n }\n\n function scheduleFlush() {\n if(flushTimer) clearTimeout(flushTimer);\n flushTimer = setTimeout(flush, 100);\n }\n\n function flush() {\n if(flushTimer) { clearTimeout(flushTimer); flushTimer = null; }\n if(!dirtyFields.size) return;\n const raw = Host.getVariable();\n let latest;\n if(raw && raw.trim()) {\n try {\n latest = JSON.parse(raw);\n if(!latest || typeof latest !== 'object' || Array.isArray(latest)) latest = null;\n } catch (e) { latest = null; }\n if(latest === null) return;\n } else {\n latest = {};\n }\n\n const flushedFields = [];\n dirtyFields.forEach(f => {\n if(!PERSIST_SET.has(f)) return;\n latest[f] = state[f];\n flushedFields.push(f);\n });\n\n const payload = JSON.stringify(latest);\n try {\n const roundTrip = JSON.parse(payload);\n if(!roundTrip || typeof roundTrip !== 'object' || Array.isArray(roundTrip)) throw new Error('round-trip');\n } catch (e) { return; }\n\n if(Host.putVariable(payload)) {\n Host.putVersion(SCHEMA_VERSION);\n dirtyFields.clear();\n lastRaw = Host.getVariable();\n emit('flush', { fields: flushedFields });\n }\n }\n\n \/* ===== 历史记录(缓存版) ===== *\/\n function readHistory() {\n try {\n const raw = source.get(HISTORY_KEY);\n if(!raw || !raw.trim()) return [];\n const parsed = JSON.parse(raw);\n if(!Array.isArray(parsed)) return [];\n return parsed.filter(h => h && typeof h === 'object' && h.url).map(h => {\n const o = { title: trimStr(h.title), url: trimStr(h.url), time: Number(h.time) || 0 };\n if(h.collectionId) o.collectionId = trimStr(h.collectionId);\n if(h.collectionName) o.collectionName = trimStr(h.collectionName);\n return o;\n });\n } catch (e) { return []; }\n }\n\n function writeHistory() {\n try {\n const payload = JSON.stringify(state.history || []);\n if(!Array.isArray(JSON.parse(payload))) return false;\n source.put(HISTORY_KEY, payload);\n return true;\n } catch (e) { return false; }\n }\n\n \/\/ 载入变量\n function load() {\n const raw = Host.getVariable();\n lastRaw = raw;\n\n let parsed = null;\n if(raw && raw.trim()) { try { parsed = JSON.parse(raw); } catch (e) { parsed = null; } }\n if(!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) parsed = {};\n\n const normalized = normalizeCfg(parsed);\n state.tags = normalized.tags;\n state.hiddenTags = normalized.hiddenTags;\n state.cleandTags = normalized.cleandTags;\n state.disCleandTags = normalized.disCleandTags;\n state.collections = normalized.collections;\n state.authors = normalized.authors;\n state.articles = normalized.articles;\n state.history = readHistory();\n state.find = parsed.find || '';\n state.type = parsed.type || '';\n state.allType = normalizeAllType(parsed.allType);\n emit('load', state);\n }\n \/\/ 重载变量\n function reload() {\n const raw = Host.getVariable();\n if(raw === lastRaw) return false;\n load();\n return true;\n }\n\n function set(field, value, opts) {\n opts = opts || {};\n if(field === 'ui' || !(field in state)) return;\n if(state[field] === value) return;\n state[field] = value;\n if(PERSIST_SET.has(field)) {\n markDirty(field);\n if(opts.now) flush();\n else scheduleFlush();\n }\n emit('change:' + field, value);\n emit('change', { field, value });\n if(opts.render !== false) emit('render', { field });\n }\n\n function mutate(field, fn, opts) {\n opts = opts || {};\n if(field === 'ui' || !(field in state)) return;\n const next = fn(state[field]);\n if(next === undefined) return;\n state[field] = next;\n if(PERSIST_SET.has(field)) {\n markDirty(field);\n if(opts.now) flush();\n else scheduleFlush();\n }\n emit('change:' + field, next);\n emit('change', { field, value: next });\n if(opts.render !== false) emit('render', { field });\n }\n\n function getUI(key) { return state.ui[key]; }\n\n function setUI(key, value, opts) {\n opts = opts || {};\n if(!(key in state.ui) || state.ui[key] === value) return;\n state.ui[key] = value;\n emit('ui:' + key, value);\n if(opts.render !== false) emit('render', { ui: key });\n }\n\n \/* 原子追加 *\/\n function appendField(field, item, dupFn, opts) {\n if(!PERSIST_SET.has(field)) return 'fail';\n\n const raw = Host.getVariable();\n let latest;\n if(raw && raw.trim()) {\n try {\n latest = JSON.parse(raw);\n if(!latest || typeof latest !== 'object' || Array.isArray(latest)) return 'fail';\n } catch (e) { return 'fail'; }\n } else {\n latest = {};\n }\n\n if(!Array.isArray(latest[field])) latest[field] = [];\n\n if(dupFn && latest[field].some(dupFn)) {\n state[field] = latest[field];\n lastRaw = raw;\n emit('change:' + field, state[field]);\n emit('render', { field });\n return 'dup';\n }\n\n if(opts && opts.prepend) latest[field].unshift(item);\n else latest[field].push(item);\n\n const payload = JSON.stringify(latest);\n try {\n const r = JSON.parse(payload);\n if(!r || typeof r !== 'object' || Array.isArray(r)) return 'fail';\n } catch (e) { return 'fail'; }\n\n if(!Host.putVariable(payload)) return 'fail';\n state[field] = latest[field];\n lastRaw = Host.getVariable();\n Host.putVersion(SCHEMA_VERSION);\n emit('change:' + field, state[field]);\n emit('change', { field, value: state[field] });\n emit('render', { field });\n return 'ok';\n }\n\n return {\n state, PERSIST_FIELDS,\n \/\/ get(f){ return state[f]; },\n set, mutate,\n getUI, setUI,\n appendField,\n on, emit,\n load, reload, flush, markDirty, scheduleFlush,\n reloadHistory() { state.history = readHistory(); },\n saveHistory() { return writeHistory(); }\n };\n})();\n\n\/* ===== DOM 缓存 ===== *\/\nconst container = document.getElementById('list-content');\nconst filterBar = document.querySelector('.filter-bar');\nconst toggleHiddenBtn = document.getElementById('toggleHidden');\nconst eyeOpenGlobal = toggleHiddenBtn.querySelector('.eye-open');\nconst eyeClosedGlobal = toggleHiddenBtn.querySelector('.eye-closed');\nconst manageBtn = document.getElementById('manageBtn');\nconst addArea = document.getElementById('addArea');\nconst tagAddArea = document.getElementById('tagAddArea');\nconst colAddArea = document.getElementById('colAddArea');\nconst authorAddArea = document.getElementById('authorAddArea');\nconst articleAddArea = document.getElementById('articleAddArea');\nconst historyArea = document.getElementById('historyArea');\nconst newTagInput = document.getElementById('newTagInput');\nconst addTagBtn = document.getElementById('addTagBtn');\nconst filterCleandBtn = document.getElementById('filterCleandBtn');\nconst subUpdateBtn = document.getElementById('subUpdateBtn');\n\/\/ const searchBtn = document.getElementById('searchBtn');\nconst clearHistoryBtn = document.getElementById('clearHistoryBtn');\nconst exportHistoryBtn = document.getElementById('exportHistoryBtn');\nconst importHistoryBtn = document.getElementById('importHistoryBtn');\nconst filterBtns = [].slice.call(document.querySelectorAll('.filter-btn'));\n\nconst allTypeArea = document.createElement('div');\nallTypeArea.id = 'allTypeArea';\nallTypeArea.className = 'sub-add-area';\nallTypeArea.style.display = 'none';\naddArea.insertBefore(allTypeArea, addArea.firstChild);\n\nfunction syncFilterButtons() {\n const current = Store.getUI('currentFilter');\n filterBtns.forEach(btn => btn.classList.toggle('active', btn.dataset.filter === current));\n}\n\n\/* ===== 内联 SVG && MARKERS ===== *\/\nconst svg = {};\ndocument.querySelectorAll('symbol').forEach(s => {\n const a = [...s.attributes].map(x => `${x.name}=\"${x.value}\"`).join(' ');\n svg[s.id] = `<svg ${a} width=\"16\" height=\"16\">${s.innerHTML}<\/svg>`;\n});\n\nconst MARKERS = {\n '[图]': `<i class=\"tag-avatar\">${svg.image}<\/i>`, \n '[文]': `<i class=\"tag-avatar\">${svg.note}<\/i>`, \n '[热]': '<img class=\"tag-avatar\" src=\"https:\/\/lofter.lf127.net\/1785934065133\/hot.png\">',\n '[新]': '<img class=\"tag-avatar\" src=\"https:\/\/imglf3.lf127.net\/bkimg\/20231017100643\/fac327c8-c314-46be-98ac-10e12d390a8d.png\">'\n};\n\n\/* ===== 请求层 ===== *\/\nconst REQ_RETRY = 1;\nconst REQ_RETRY_DELAY = 250;\n\nfunction requestRaw(url, opts) {\n opts = opts || {};\n const retries = opts.retries == null ? REQ_RETRY : opts.retries;\n return new Promise((resolve) => {\n let attempt = 0;\n\n function run() {\n const raw = Host.ajax(url);\n if(raw) { resolve(raw); return; }\n if(attempt++ < retries) setTimeout(run, REQ_RETRY_DELAY * attempt);\n else resolve(null);\n }\n run();\n });\n}\n\nasync function ajaxJson(url, opts) {\n const raw = await requestRaw(url, opts);\n if(!raw) return null;\n try { return JSON.parse(raw); } catch (e) { return null; }\n}\n\nfunction runAsync(promise, opts) {\n opts = opts || {};\n return Promise.resolve(promise).catch(() => {\n if(opts.silent !== true) Host.toast('操作失败,请重试');\n });\n}\n\n\/* ===== 入口构造 ===== *\/\nlet portraitCache = null;\nlet avatarHandlerBound = false;\n\nfunction jump(kind, url, title) { Store.flush(); Host.open(kind, url, title); }\n\nfunction jumpUrl(url) { Store.flush(); Host.openUrl(url); }\n\nfunction getPortrait() {\n if(portraitCache) return portraitCache;\n const androidId = Host.androidId();\n portraitCache = Host.base64Encode(JSON.stringify({imei: androidId, oaid:'null', mac:'02:00:00:00:00:00', androidId: androidId}));\n return portraitCache;\n}\n\nfunction openRandom() {\n const domain = id => `${API_BASE}\/recommend\/domainPosts.json?product=lofter-android-${VER.R1},${JSON.stringify({method:'POST', body:`domainId=${id}&offset={{(page-1)*10}}&feedTime=0&count=0`})}`;\n jump('sort', JSON.stringify({'发现':`${API_BASE}\/recommend\/exploreRecom.json?product=lofter-android-${VER.R1},${JSON.stringify({method:'POST',body:'offset={{(page-1)*10}}&feedTime=0&count=0'})}`,\n '文学': domain(1538118079721),\n '绘画': domain(1370406202794),\n '影视': domain(1442207751226),\n '二次元': domain(1370406175120),\n '娱乐': domain(1430907486999),\n \/*'乙游': domain(18243400),*\/\n \/*'听书': domain(18242400),*\/\n '视频': `${API_BASE}\/recommend\/videoFlow.json?offset={{(page-1)*10}}&limit=10`\n }));\n}\n\nfunction buildTag(tag) {\n const encoded = encodeURIComponent(trimStr(tag));\n const postTypes = Store.state.type;\n const obj = {};\n obj[`🏷️ ${trimStr(tag)}·发现`] = `${API_BASE}\/recommend\/tagRecom.json?lofter-android-${VER.R1},${JSON.stringify({method:'POST','accept-encoding':'gzip, deflate',body:`offset={{(page-1)*10}}&count=2&source=&tag=${encoded}`,headers:{'Content-Type':'application\/x-www-form-urlencoded;charset=utf-8',deviceid:'{{java.androidId()}}'}})}`;\n obj['综合'] = `${API_BASE}\/newsearch\/all\/post.json?product=lofter-android-${VER.R1}&sortType=0&limit=20&offset={{(page-1)*20}}&version=1&key=${encoded}`;\n [['最新','new'],['日榜','date'],['周榜','week'],['月榜','month'],['总榜','total']].forEach(pair=>{\n obj[pair[0]] = `${API_BASE}\/oldapi\/tagPosts.api?product=lofter-android-${VER.R2}&offset={{(page-1)*${PAGE.TAG}}}&firstpermalink=&postTypes=${postTypes}&tag=${encoded}&type=${pair[1]},${HDR_TAG_POST}`;\n });\n obj['好粮榜'] = `https:\/\/api.lofter.com\/newapi\/tagPosts.json,{\"method\": \"POST\",\"accept-encoding\":\"gzip, deflate\", \"body\":\"abFlag=old&postYm=&postYmdSt=0&postYmdEt=0&recentDay=0&protectedFlag=0&style=0&returnGiftCombination=&range=0&firstpermalink=null&type=good&product=lofter-android-${VER.R4}&offset={{(page-1)*10}}&postTypes=${postTypes}&tag=${encoded}\"}`;\n obj['新评榜'] = `https:\/\/api.lofter.com\/newapi\/tagPosts.json,{\"method\": \"POST\",\"accept-encoding\":\"gzip, deflate\", \"body\":\"abFlag=old&postYm=&postYmdSt=0&postYmdEt=0&recentDay=0&protectedFlag=0&style=0&returnGiftCombination=&range=0&firstpermalink=null&type=newComment&product=lofter-android-${VER.R4}&offset={{(page-1)*10}}&postTypes=${postTypes}&tag=${encoded}\"}`;\n obj['动态'] = `https:\/\/api.lofter.com\/question\/tagChat\/pageNew.json?offset={{(page-1)*20}}&tag=${encoded}`;\n \/\/ obj['合集'] = `https:\/\/api.lofter.com\/newapi\/postCollection\/tagPage.json?offset={{(page-1)*10}}&tag=${encoded}`;\n \/\/ obj['粮单'] = `https:\/\/api.lofter.com\/newsearch\/grain.json?limit=10&offset={{(page-1)*10}}&key=${encoded}`;\n return JSON.stringify(obj);\n}\n\nfunction buildCol(col) {\n const obj = {};\n obj[`📖 ${col.name}`] = `${API_BASE}\/v1.1\/postCollection.api?product=lofter-android-${VER.R2}&method=getCollectionDetail&offset={{(page-1)*${PAGE.COLL}}}&limit=${PAGE.COLL}&order=1&collectionid=${encodeURIComponent(col.id)}`;\n return JSON.stringify(obj);\n}\n\nfunction buildAuthor(author) {\n const obj = {};\n obj[`💐 ${author.name}`] = `${API_BASE}\/v2.0\/blogHomePage.api?product=lofter-android-${VER.R3},${JSON.stringify({method:'POST', body:`supportposttypes=1%2C2%2C3%2C4%2C5%2C6&targetblogid=${encodeURIComponent(author.blogId)}&offset={{(page-1)*${PAGE.AUTH}}}&method=getPostLists&postdigestnew=1&returnData=1&limit=${PAGE.AUTH}&checkpwd=1&needgetpoststat=1`})}`;\n return JSON.stringify(obj);\n}\n\nfunction openBookStore() {\n const portrait = getPortrait(), now = Date.now();\n const recommendPost = \"https:\/\/www.lofter.com\/newweb\/bookStore\/recommendPost.json\";\n jump('sort', JSON.stringify({\n '推荐': `${API_BASE}\/recommend\/exploreRecom.json?product=lofter-android-${VER.R1},${JSON.stringify({method:'POST',body:`feedTime=0&count={{page}}&offset={{(page-1)*10}}&portrait=${portrait}`})}`,\n '广场': `${API_BASE}\/question\/tagChatExplore.json?limit=10&count={{page-1}}&offset={{(page-1)*10}}&portrait=${portrait}`,\n '偏好推荐': `${API_BASE}\/recommend\/bookStoreFeedRecV2.json?tab=&count={{page-1}}&offset={{(page-1)*10}}&vipTime=${now}&portrait=${portrait}&product=lofter-android-${VER.R1}&_=${now}`,\n '精选推荐': `${recommendPost}?tag=精选推荐&count={{page-1}}&offset={{(page-1)*10}}&portrait=${portrait}&_=${now}`,\n '近期热门': `${recommendPost}?tag=近期热门&count={{page-1}}&offset={{(page-1)*10}}&portrait=${portrait}&_=${now}`,\n '潜力新作': `${recommendPost}?tag=潜力新作&count={{page-1}}&offset={{(page-1)*10}}&portrait=${portrait}&_=${now}`\n }));\n}\n\nfunction parseArticle(url) {\n const reUrl = \/https?:\\\/\\\/([^.]+)\\.lofter\\.com\\\/post\\\/([0-9a-fA-F]+)_([0-9a-fA-F]+)\/; \n const m = url.match(reUrl);\n return m ? { \n domain: m[1],\n blogId: parseInt(m[2], 16),\n postId: parseInt(m[3], 16)\n } : null;\n}\n\nfunction openArticle(url, title) {\n const parsed = parseArticle(url);\n if(!parsed) return false;\n jump('rss', `${API_BASE}\/oldapi\/post\/detail.api?product=lofter-android-${VER.R1},${JSON.stringify({\n method:'POST','accept-encoding':'gzip, deflate',\n body:`targetblogid=${parsed.blogId}&blogdomain=${parsed.domain}.lofter.com&postid=${parsed.postId}`\n })}`, title);\n return true;\n}\n\n\/* ===== 网络请求 ===== *\/\nasync function fetchColName(id) {\n const data = await ajaxJson(`${API_BASE}\/v1.1\/postCollection.api?product=lofter-android-${VER.R2}&method=getCollectionDetail&offset=0&limit=1&collectionid=${encodeURIComponent(id)}`);\n if(!data) return '';\n const collection = (data.response && data.response.collection) || (data.data && data.data.collection) || data.collection || {};\n return trimStr(collection.name || collection.title || '');\n}\n\nasync function fetchAuthor(blogId) {\n const body = `supportposttypes=1%2C2%2C3%2C4%2C5%2C6&targetblogid=${encodeURIComponent(blogId)}&offset=0&method=getPostLists&postdigestnew=1&returnData=1&limit=1&checkpwd=1&needgetpoststat=1`;\n const data = await ajaxJson(`${API_BASE}\/v2.0\/blogHomePage.api?product=lofter-android-${VER.R3},${JSON.stringify({method:'POST',body})}`);\n if(!data) return '';\n const root = data.response || data.data || data;\n const info = root.blogInfo || (root.posts && root.posts[0] && root.posts[0].blogInfo) || {};\n return trimStr(info.blogNickName || info.name || '');\n}\n\nasync function fetchArticleTitle(url) {\n const parsed = parseArticle(url);\n if(!parsed) return '';\n const data = await ajaxJson(`${API_BASE}\/oldapi\/post\/detail.api?product=lofter-android-${VER.R1},${JSON.stringify({\n method:'POST','accept-encoding':'gzip, deflate',\n body:`targetblogid=${parsed.blogId}&blogdomain=${parsed.domain}.lofter.com&postid=${parsed.postId}`\n })}`);\n if(!data) return '';\n const candidatePaths = [\n ['response', 'posts', 0, 'post', 'title'],\n ['response', 'posts', 0, 'post', 'digest'],\n ['response', 'posts', 0, 'post', 'tagList', 0]\n ];\n let title = '';\n for (let i = 0; i < candidatePaths.length; i++) {\n let value = data;\n for (let j = 0; j < candidatePaths[i].length; j++) {\n if(value == null) { value = undefined; break; }\n value = value[candidatePaths[i][j]];\n }\n if(value !== undefined && value !== null && String(value).trim() !== '') {\n title = String(value);\n break;\n }\n }\n title = stripHtml(title);\n if(Array.from(title).length > 30) title = Array.from(title).slice(0, 30).join('');\n return title;\n}\n\nasync function updateColCount(col) {\n const today = getToday();\n const parsed = parseColCount(col.count);\n const hasUpdate = col.update && +col.update > 0;\n if(parsed && parsed.lastDate === today && hasUpdate) return false;\n const data = await ajaxJson(`${API_BASE}\/v1.1\/postCollection.api?product=lofter-android-${VER.R2}&method=getCollectionDetail&offset=0&limit=1&collectionid=${encodeURIComponent(col.id)}`);\n if(!data) return false;\n const collection = (data.response && data.response.collection) || (data.data && data.data.collection);\n if(!collection) return false;\n \/* 写回之前重新定位 *\/\n const fresh = findItem(Store.state.collections, c => String(c.id) === String(col.id));\n if(!fresh) return false;\n let changed = false;\n if(collection.postCount !== undefined && !isNaN(collection.postCount)) {\n fresh.count = `${today}\/${parsed?parsed.newCount:collection.postCount}\/${collection.postCount}`;\n changed = true;\n }\n const ts = Math.max(+collection.updateTime || 0, +collection.lastPublishTime || 0);\n if(ts > 0) { fresh.update = String(ts); changed = true; }\n if(changed) { Store.markDirty('collections'); Store.scheduleFlush(); }\n return changed;\n}\n\nasync function loadRank(sortNo, rankType) {\n const data = await ajaxJson(`${API_BASE}\/newapi\/hotsearch\/ranklist.json?product=lofter-android-${VER.R1}&t=0`);\n if(!data || !data.data) { Host.toast('获取榜单失败'); return; }\n const rankList = Array.isArray(data.data.rankList) ? data.data.rankList : [];\n const rank = rankList.find(r => +r.sortNo === +sortNo);\n if(!rank) { Host.toast('未找到榜单'); return; }\n Store.setUI('currentRank', {\n type: rankType, sortNo, title: rank.listName||'榜单',\n items: (Array.isArray(rank.hotLists)?rank.hotLists:[]).map(it=>({\n title: trimStr(it.title), url: it.url||'',\n blogId: it.blogId||'', postId: it.postId||'',\n postType: it.postType || '',\n img: it.img||'', icon: it.icon||'', iconInfo: it.iconInfo||null\n }))\n });\n}\n\nasync function loadSubscrCols() {\n Store.reload();\n const data = await ajaxJson(`${API_BASE}\/newapi\/subscribeCollection\/list.json?offset=0&limit=${PAGE.COLL}`);\n const list = (data && data.data && Array.isArray(data.data.guessLikeList)) ? data.data.guessLikeList : [];\n if(!list.length) { Host.toast('暂无推荐合集'); return; }\n Store.setUI('subscrCollections', list);\n}\n\n\/* ===== 区块 diff 渲染 ===== *\/\nconst SECTION_DEFS = [\n ['special', () => renderSpecial()],\n ['subscr', () => renderSubscrCols()],\n ['rank', () => renderRank()],\n ['tags', () => renderTags()],\n ['cols', () => renderCols()],\n ['authors', () => renderAuthors()],\n ['articles', () => renderArticles()],\n ['history', () => renderHistory()]\n];\nconst sectionHtmlCache = {};\nlet sectionNodes = null;\n\nfunction ensureSections() {\n if(sectionNodes) return sectionNodes;\n sectionNodes = {};\n container.innerHTML = '';\n for (let i = 0; i < SECTION_DEFS.length; i++) {\n const key = SECTION_DEFS[i][0];\n const wrap = document.createElement('div');\n wrap.className = 'section-wrap';\n wrap.dataset.section = key;\n wrap.style.display = 'contents';\n container.appendChild(wrap);\n sectionNodes[key] = wrap;\n }\n return sectionNodes;\n}\n\nfunction invalidateSection(key) {\n if(key) sectionHtmlCache[key] = undefined;\n else for (const k in sectionHtmlCache) sectionHtmlCache[k] = undefined;\n}\n\nfunction pickList(key) {\n if(key === 'tags') {\n const cleandMode = Store.getUI('showCleandTags') && Store.getUI('currentFilter') === 'tag';\n const list = cleandMode ? Store.state.cleandTags : Store.state.tags;\n return list.filter(t => cleandMode || shouldShow(isHidden(t, 'tag')));\n }\n if(key === 'cols') return Store.state.collections.filter(c => shouldShow(isHidden(c, 'col')));\n if(key === 'authors') return Store.state.authors.filter(a => {\n const hiddenState = String(a.hidden || Hidden.SHOW);\n if(hiddenState === Hidden.CLEAN) return Store.getUI('showHidden') && Store.getUI('manageMode');\n return shouldShow(isHidden(a, 'author'));\n });\n if(key === 'articles') return Store.state.articles.filter(a => shouldShow(String(a.hidden) === Hidden.HIDE));\n return [];\n}\n\n\/* ===== 行级渲染 ===== *\/\nfunction itemAttrs(type, value) {\n return `data-item-type=\"${type}\" data-item-value=\"${value}\"`;\n}\n\nfunction buildEye(type, value, icon, title) {\n const label = title || '切换隐藏';\n return `<button type=\"button\" class=\"eye-btn\" aria-label=\"${label}\" title=\"${label}\" ${itemAttrs(type,value)}>${icon}<\/button>`;\n}\n\nfunction buildActions(type, value, opts) {\n opts = opts || {};\n const attrs = itemAttrs(type, value);\n let html = '<span class=\"item-actions\">';\n if(opts.editable) html += `<button type=\"button\" class=\"edit-btn\" aria-label=\"修改\" title=\"修改\" ${attrs}>${svg.editor}<\/button>`;\n html += `<button type=\"button\" class=\"del-btn\" aria-label=\"删除\" title=\"删除\" ${attrs}>${svg.trash}<\/button>`;\n return html + '<\/span>';\n}\n\nfunction buildDrag(type, value) {\n return `<span class=\"drag-handle\" role=\"button\" tabindex=\"0\" aria-label=\"拖动排序\" data-item-type=\"${type}\" data-item-value=\"${value}\">${svg.drag}<\/span>`;\n}\n\nfunction buildItem(cfg) {\n let cls = 'item ' + cfg.itemClass;\n if(cfg.extraClass) cls += ' ' + cfg.extraClass;\n if(cfg.hidden) cls += ' hidden-item';\n let body = '';\n if(Store.getUI('manageMode') && cfg.dragType) body += buildDrag(cfg.dragType, cfg.dragValue);\n if(cfg.eye) body += cfg.eye;\n if(cfg.avatar) body += cfg.avatar;\n body += cfg.nameHtml;\n if(cfg.actions) body += cfg.actions;\n return `<div class=\"${cls}\"${cfg.attrs||''}>${body}<\/div>`;\n}\n\nfunction buildSection(title, bodyHtml) {\n return `<div class=\"section\"><div class=\"section-title\">${title}<\/div><div class=\"list\">${bodyHtml}<\/div><\/div>`;\n}\n\nfunction renderTagRow(tag) {\n const cleandMode = Store.getUI('showCleandTags') && Store.getUI('currentFilter') === 'tag';\n const isItemHidden = cleandMode ? false : isHidden(tag, 'tag');\n const isDisabled = cleandMode && Store.state.disCleandTags.indexOf(tag) !== -1;\n const type = cleandMode ? 'cleandTag' : 'tag';\n const value = escapeHtml(tag);\n let eye = '';\n if(cleandMode) eye = buildEye('cleandTag', value, isDisabled ? svg.eyeClosed : svg.eyeCleand, isDisabled ? '启用' : '禁用');\n else if(Store.getUI('showHidden')) eye = buildEye('tag', value, isItemHidden ? svg.eyeClosed : svg.eyeOpen);\n return buildItem({\n itemClass: 'tag-item',\n extraClass: cleandMode ? 'cleand-tag-item' : '',\n hidden: isItemHidden || isDisabled,\n dragType: type,\n dragValue: value,\n eye: eye,\n nameHtml: `<span class=\"name-text\">${value}<\/span>`,\n actions: Store.getUI('manageMode') ? buildActions(type, value) : '',\n attrs: cleandMode ? ` data-cleand-tag=\"${value}\"` : ` data-tag=\"${value}\"`\n });\n}\n\nfunction renderColRow(col) {\n const isItemHidden = isHidden(col, 'col');\n const today = getToday();\n const parsed = parseColCount(col.count);\n const updatedToday = parsed && parsed.lastDate === today && parsed.oldCount !== parsed.newCount;\n const hasUpdate = col.update && +col.update > 0;\n const value = escapeAttr(col);\n let svgPrefix = '', nameText = col.name;\n Object.keys(MARKERS).sort((a, b) => col.name.indexOf(a) - col.name.indexOf(b)).forEach(marker => {\n let idx;\n while ((idx = nameText.indexOf(marker)) !== -1) {\n svgPrefix += MARKERS[marker];\n nameText = nameText.slice(0, idx) + nameText.slice(idx + marker.length);\n }\n });\n nameText = escapeHtml(nameText);\n let nameHtml = svgPrefix + `<span class=\"name-text\">${nameText}<\/span>`;\n if(Store.getUI('manageMode')) nameHtml += ` <span class=\"id-text\">(ID:${escapeHtml(col.id)})<\/span>`;\n else if(Store.getUI('showHidden') && hasUpdate) nameHtml += `<span class=\"id-text${updatedToday?' up':''}\">${fmtUpdate(col.update)}<\/span>`;\n else if(col.count) {\n const displayCount = parsed ? parsed.newCount : col.count;\n nameHtml += `<span class=\"id-text${updatedToday?' up':''}\">${displayCount}篇<\/span>`;\n }\n return buildItem({\n itemClass: 'col-item',\n hidden: isItemHidden,\n dragType: 'col',\n dragValue: value,\n eye: Store.getUI('showHidden') ? buildEye('col', value, isItemHidden ? svg.eyeClosed : svg.eyeOpen) : '',\n nameHtml: nameHtml,\n actions: Store.getUI('manageMode') ? buildActions('col', value, { editable: true }) : '',\n attrs: ` data-col-id=\"${escapeHtml(col.id)}\" data-col-name=\"${escapeHtml(col.name)}\"`\n });\n}\n\nfunction renderAuthorRow(author) {\n const hiddenState = String(author.hidden || Hidden.SHOW);\n const isItemHidden = isHidden(author, 'author');\n const value = escapeAttr(author);\n const showEye = Store.getUI('showHidden') || Store.getUI('manageMode');\n const eyeIcon = hiddenState === Hidden.HIDE ? svg.eyeClosed : hiddenState === Hidden.CLEAN ? svg.eyeCleand : svg.eyeOpen;\n let nameHtml = `<span class=\"name-text\">${escapeHtml(author.name)}<\/span>`;\n if(Store.getUI('manageMode')) nameHtml += ` <span class=\"id-text\">(BID:${escapeHtml(author.blogId)})<\/span>`;\n const showAvatar = !Store.getUI('manageMode') && author.avatar; \n return buildItem({\n itemClass: 'author-item',\n extraClass: hiddenState === Hidden.CLEAN ? 'cleand-author-item' : '',\n hidden: isItemHidden,\n dragType: 'author',\n dragValue: value,\n eye: showEye ? buildEye('author', value, eyeIcon) : '',\n avatar: showAvatar ? `<img class=\"author-avatar\" src=\"${escapeHtml(safeUrl(author.avatar))}\" alt=\"头像\">` : '', \n nameHtml: nameHtml,\n actions: Store.getUI('manageMode') ? buildActions('author', value, { editable: true }) : '',\n attrs: ` data-author-blog-id=\"${escapeHtml(author.blogId)}\" data-author-name=\"${escapeHtml(author.name)}\" data-hidden=\"${hiddenState}\"`\n });\n}\n\nfunction renderArticleRow(article) {\n const hiddenState = String(article.hidden || Hidden.SHOW);\n const isItemHidden = hiddenState === Hidden.HIDE;\n const value = escapeAttr(article);\n return buildItem({\n itemClass: 'article-item',\n hidden: isItemHidden,\n dragType: 'article',\n dragValue: value,\n eye: Store.getUI('showHidden') ? buildEye('article', value, isItemHidden ? svg.eyeClosed : svg.eyeOpen) : '',\n nameHtml: `<span class=\"name-text\">${escapeHtml(article.title)}<\/span>`,\n actions: Store.getUI('manageMode') ? buildActions('article', value, { editable: true }) : '',\n attrs: ` data-article-url=\"${escapeHtml(article.url)}\" data-article-title=\"${escapeHtml(article.title)}\" data-hidden=\"${hiddenState}\"`\n });\n}\n\n\/* ===== 历史记录 ===== *\/\nfunction renderHistoryRow(entry) {\n const manageMode = Store.getUI('manageMode');\n const value = escapeAttr(entry);\n let body = '';\n if(manageMode) body += buildDrag('history', value);\n body += `<span class=\"name-text\">${escapeHtml(entry.title || entry.url)}<\/span>`;\n if(Store.getUI('showHidden') && entry.collectionName) {\n body += `<span class=\"id-text\">${escapeHtml(entry.collectionName)}<\/span>`;\n }\n if(manageMode) {\n const isFaved = Store.state.articles.some(a => a.url === entry.url);\n const label = isFaved ? '取消收藏' : '收藏到文章';\n body += `<button type=\"button\" class=\"fav-btn${isFaved?' active':''}\" data-fav-url=\"${escapeHtml(entry.url)}\" aria-label=\"${label}\" title=\"${label}\">${isFaved?svg.star:svg.staro}<\/button>`;\n body += buildActions('history', value);\n }\n return `<div class=\"item history-item\" data-history-url=\"${escapeHtml(entry.url)}\" data-history-title=\"${escapeHtml(entry.title)}\">${body}<\/div>`;\n}\n\nfunction renderHistory() {\n if(Store.getUI('currentFilter') !== 'history') return '';\n const list = Store.state.history || [];\n if(!list.length) return '';\n const body = list.map(entry => renderHistoryRow(entry)).join('');\n return buildSection('🕘 历史', body);\n}\n\n\/* ===== 收藏切换 ===== *\/\nfunction toggleFav(url, btn) {\n Store.reload();\n Store.reloadHistory();\n const isFaved = Store.state.articles.some(a => a.url === url);\n if(isFaved) {\n Store.mutate('articles', l => l.filter(a => a.url !== url));\n Store.flush();\n Host.toast('已取消收藏');\n } else {\n const entry = findItem(Store.state.history, x => x.url === url);\n if(!entry) { Host.toast('历史记录不存在'); return; }\n const title = entry.title || url;\n Store.mutate('articles', l => (l.unshift({ title, url, hidden: Hidden.SHOW }), l));\n Store.flush();\n Host.toast(`已收藏「${title}」`);\n }\n invalidateSection('articles');\n const isNowFaved = Store.state.articles.some(a => a.url === url);\n if(btn) {\n btn.classList.toggle('active', isNowFaved);\n btn.innerHTML = isNowFaved ? svg.star : svg.staro;\n const label = isNowFaved ? '取消收藏' : '收藏到文章';\n btn.title = label;\n btn.setAttribute('aria-label', label);\n }\n if(Store.getUI('currentFilter') === 'article') { invalidateSection(); render(); }\n}\n\n\/* ===== 区块渲染 ===== *\/\nfunction renderSpecial() {\n if(Store.getUI('manageMode') || Store.getUI('showHidden') || Store.getUI('currentFilter') !== 'all') return '';\n const currentRank = Store.getUI('currentRank');\n const rankButtons = RANKS.map(b => {\n const active = currentRank && currentRank.sortNo === b.sortNo && currentRank.type === b.type ? ' active' : ''; \n return `<div class=\"item rank-btn${active}\" role=\"button\" tabindex=\"0\" aria-pressed=\"${active?'true':'false'}\" data-sortno=\"${b.sortNo}\" data-type=\"${b.type}\">${b.label}<\/div>`; \n }).join('');\n const readmeButton = Host.getVariable() ? '' : '<div class=\"item readme-item\">首次使用请点击此处<\/div>';\n const subCollections = Store.getUI('subscrCollections');\n const subActive = !!(subCollections && subCollections.length);\n \n return `<div class=\"section\"><div class=\"section-title\" style=\"display:none\">🌱 常规<\/div><div class=\"list\">${readmeButton}<div class=\"item random-item\">随便看看<\/div><div class=\"item bookstore-item\">书馆<\/div><div class=\"item subscribe-btn${subActive?' active':''}\">推荐订阅<\/div>${rankButtons}<\/div><\/div>`;\n}\n\nfunction renderSubscrCols() {\n const subCollections = Store.getUI('subscrCollections');\n if(!subCollections || !subCollections.length) return '';\n if(Store.getUI('manageMode') || Store.getUI('showHidden') || Store.getUI('currentFilter') !== 'all') return '';\n const body = subCollections.filter(c => c.collectionId).map(c => {\n const name = c.name || '未命名合集';\n const info = c.postCount !== undefined ? `${c.postCount}篇` : '';\n const countHtml = info ? `<span class=\"id-text\">${escapeHtml(info)}<\/span>` : '';\n return `<div class=\"item sub-col-item\" data-id=\"${c.collectionId}\" data-name=\"${escapeHtml(name)}\">${escapeHtml(name)}${countHtml}<\/div>`;\n }).join('');\n if(!body) return '';\n return `<div class=\"section\"><div class=\"section-title\">📒 推荐订阅合集<\/div><div class=\"list\">${body}<\/div><\/div>`;\n}\n\nfunction renderRank() {\n const currentRank = Store.getUI('currentRank');\n if(!currentRank) return '';\n if(Store.getUI('manageMode') || Store.getUI('showHidden') || Store.getUI('currentFilter') !== 'all') return '';\n let html = `<div class=\"section\"><div class=\"section-title\">🔥 ${currentRank.title}<\/div><div class=\"list\">`; \n const rankType = currentRank.type;\n currentRank.items.forEach((item, idx) => {\n let displayTitle = item.title, avatarHtml = '';\n if(rankType === 'hottag') {\n displayTitle = displayTitle.replace(\/^#\\s*\/, '');\n const icon = safeUrl(item.icon);\n if(icon) avatarHtml = `<img class=\"tag-avatar\" src=\"${escapeHtml(icon)}\" alt=\"图标\">`;\n }\n if(rankType === 'hotarticle') {\n const pt = String(item.postType || '');\n const icon = pt === '2' ? svg.fileimage : pt === '4' ? svg.filevideo : svg.filenote;\n avatarHtml = `<i class=\"tag-avatar\">${icon}<\/i>`;\n } else if(rankType === 'hotuser') {\n const icon = safeUrl(item.icon);\n if(icon) avatarHtml += `<img class=\"tag-avatar\" src=\"${escapeHtml(icon)}\" alt=\"图标\">`;\n const avatar = safeUrl(item.img);\n if(avatar) avatarHtml += `<img class=\"author-avatar\" src=\"${escapeHtml(avatar).replace(\/\\?.+$\/,'')}?imageView&thumbnail=84x84&stripmeta=0&quality=90&type=webp&typefallback=jpg\" alt=\"图像\">`;\n } else if(rankType === 'hotword') {\n const iconInfo = item.iconInfo && safeUrl(item.iconInfo.icon);\n if(iconInfo) avatarHtml = `<img class=\"tag-avatar\" src=\"${escapeHtml(iconInfo)}\" alt=\"活动\">`;\n const icon = safeUrl(item.icon);\n if(icon) avatarHtml = `<img class=\"tag-avatar\" src=\"${escapeHtml(icon)}\" alt=\"图标\">`;\n }\n html += `<div class=\"item rank-item\" data-index=\"${idx}\">${avatarHtml}<span class=\"name-text\">${escapeHtml(displayTitle)}<\/span><\/div>`;\n });\n return html + '<\/div><\/div>';\n}\n\n\/* 四个列表区块共用:全量渲染 *\/\nfunction renderListSection(key, title, visibleItems, renderRow) {\n if(!visibleItems.length) return '';\n return buildSection(title, visibleItems.map(renderRow).join(''));\n}\n\nfunction renderTags() {\n const currentFilter = Store.getUI('currentFilter');\n if(currentFilter !== 'all' && currentFilter !== 'tag') return '';\n if(currentFilter === 'all' && !Store.state.allType.tag) return '';\n const cleandMode = Store.getUI('showCleandTags') && currentFilter === 'tag';\n return renderListSection('tags', cleandMode ? '🎯 屏蔽' : '🏷️ 标签', pickList('tags'), renderTagRow);\n}\n\nfunction renderCols() {\n const currentFilter = Store.getUI('currentFilter');\n if(currentFilter !== 'all' && currentFilter !== 'col') return '';\n if(currentFilter === 'all' && !Store.state.allType.col) return '';\n return renderListSection('cols', '📖 合集', pickList('cols'), renderColRow);\n}\n\nfunction renderAuthors() {\n const currentFilter = Store.getUI('currentFilter');\n if(currentFilter !== 'all' && currentFilter !== 'author') return '';\n if(currentFilter === 'all' && !Store.state.allType.author) return '';\n return renderListSection('authors', '✏️ 作者', pickList('authors'), renderAuthorRow);\n}\n\nfunction renderArticles() {\n const currentFilter = Store.getUI('currentFilter');\n if(currentFilter !== 'all' && currentFilter !== 'article') return '';\n if(currentFilter === 'all' && !Store.state.allType.article) return '';\n return renderListSection('articles', '📑 文章', pickList('articles'), renderArticleRow);\n}\n\n\/* ===== 主渲染 ===== *\/\nfunction render() {\n const windowY = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop || 0;\n const containerY = container.scrollTop;\n const nodes = ensureSections();\n let anyVisible = false;\n\n for (let i = 0; i < SECTION_DEFS.length; i++) {\n const key = SECTION_DEFS[i][0];\n const renderFn = SECTION_DEFS[i][1];\n const html = renderFn();\n if(html === sectionHtmlCache[key]) {\n if(html) anyVisible = true;\n continue;\n }\n sectionHtmlCache[key] = html;\n const wrap = nodes[key];\n if(wrap.innerHTML !== html) wrap.innerHTML = html;\n if(html) anyVisible = true;\n }\n\n let emptyEl = container.querySelector('.empty');\n if(!anyVisible) {\n if(!emptyEl) {\n emptyEl = document.createElement('div');\n emptyEl.className = 'empty';\n emptyEl.textContent = '暂无内容';\n container.appendChild(emptyEl);\n }\n } else if(emptyEl && emptyEl.parentNode) {\n emptyEl.parentNode.removeChild(emptyEl);\n }\n\n \/*同步方案*\/\n \/\/ if(container.scrollTop !== containerY) container.scrollTop = containerY;\n \/\/ if(window.pageYOffset !== windowY) window.scrollTo(0, windowY);\n \/* 异步方案 *\/\n requestAnimationFrame(() => {\n if(container.scrollTop !== containerY) container.scrollTop = containerY;\n if(window.pageYOffset !== windowY) window.scrollTo(0, windowY);\n });\n}\n\n\/* ===== 隐藏与状态辅助 ===== *\/\nfunction shouldShow(hiddenFlag) { return Store.getUI('showHidden') ? true : !hiddenFlag; }\n\nfunction isHidden(item, type) {\n if(type === 'tag') return Store.state.hiddenTags.indexOf(item) !== -1;\n if(type === 'col') return String(item.hidden) === Hidden.HIDE || item.hidden === 1;\n if(type === 'author') {\n const stateStr = String(item.hidden == null ? Hidden.SHOW : item.hidden);\n return stateStr === Hidden.HIDE || stateStr === Hidden.CLEAN;\n }\n if(type === 'article') return String(item.hidden == null ? Hidden.SHOW : item.hidden) === Hidden.HIDE;\n return false;\n}\n\nfunction updateRow(rowEl, item, type) {\n if(!rowEl) return;\n let fresh = item;\n if(type === 'col') fresh = findItem(Store.state.collections, c => String(c.id) === String(item.id) && c.name === item.name) || item;\n else if(type === 'article') fresh = findItem(Store.state.articles, a => a.url === item.url && a.title === item.title) || item;\n else if(type === 'author') fresh = findItem(Store.state.authors, a => String(a.blogId) === String(item.blogId)) || item;\n\n let isItemHidden, eyeIcon;\n if(type === 'cleandTag') {\n const disabled = Store.state.disCleandTags.indexOf(item) !== -1;\n isItemHidden = disabled;\n eyeIcon = disabled ? svg.eyeClosed : svg.eyeCleand;\n } else if(type === 'author') {\n const hiddenState = String(fresh.hidden || Hidden.SHOW);\n isItemHidden = hiddenState === Hidden.HIDE || hiddenState === Hidden.CLEAN;\n eyeIcon = hiddenState === Hidden.HIDE ? svg.eyeClosed : hiddenState === Hidden.CLEAN ? svg.eyeCleand : svg.eyeOpen;\n rowEl.dataset.hidden = hiddenState;\n rowEl.classList.toggle('cleand-author-item', hiddenState === Hidden.CLEAN);\n } else {\n isItemHidden = isHidden(fresh, type);\n eyeIcon = isItemHidden ? svg.eyeClosed : svg.eyeOpen;\n }\n rowEl.classList.toggle('hidden-item', isItemHidden);\n const eyeBtn = rowEl.querySelector('.eye-btn');\n if(eyeBtn) eyeBtn.innerHTML = eyeIcon;\n invalidateSection(\n type === 'tag' || type === 'cleandTag' ? 'tags' :\n type === 'col' ? 'cols' :\n type === 'author' ? 'authors' :\n type === 'article' ? 'articles' : null\n );\n}\n\n\/* ===== 隐藏切换 ===== *\/\nfunction toggleItem(item, type, rowEl) {\n const opts = { render: false };\n if(type === 'tag') {\n Store.mutate('hiddenTags', list => {\n const i = list.indexOf(item);\n i === -1 ? list.push(item) : list.splice(i, 1);\n return list;\n }, opts);\n } else if(type === 'col') {\n Store.mutate('collections', list => {\n const target = list.find(x => String(x.name) === item.name && String(x.id) === String(item.id));\n if(target) target.hidden = (String(target.hidden) === Hidden.HIDE || target.hidden === 1) ? Hidden.SHOW : Hidden.HIDE;\n return list;\n }, opts);\n } else if(type === 'author') {\n Store.mutate('authors', list => {\n const target = list.find(a => String(a.blogId) === String(item.blogId));\n if(!target) return list;\n const current = String(target.hidden || Hidden.SHOW);\n target.hidden = (Store.getUI('manageMode') && Store.getUI('showHidden')) ?\n (current === Hidden.SHOW ? Hidden.HIDE : current === Hidden.HIDE ? Hidden.CLEAN : Hidden.SHOW) :\n (current === Hidden.SHOW ? Hidden.HIDE : Hidden.SHOW);\n return list;\n }, opts);\n } else if(type === 'article') {\n Store.mutate('articles', list => {\n const target = list.find(a => a.title === item.title && a.url === item.url);\n if(target) target.hidden = String(target.hidden) === Hidden.HIDE ? Hidden.SHOW : Hidden.HIDE;\n return list;\n }, opts);\n }\n updateRow(rowEl, item, type);\n}\n\nfunction toggleCleand(tag, rowEl) {\n Store.mutate('disCleandTags', (list) => {\n const i = list.indexOf(tag);\n if(i === -1) list.push(tag); else list.splice(i, 1);\n return list;\n }, { render: false });\n updateRow(rowEl, tag, 'cleandTag');\n}\n\n\/* ===== 过滤栏 ===== *\/\nfunction setFilter(filter) {\n if(filter === Store.getUI('currentFilter')) { syncFilterButtons(); return; }\n if(filter === 'history') Store.reloadHistory();\n Store.setUI('currentFilter', filter, { render: false });\n Store.setUI('showCleandTags', false, { render: false });\n const findMap = { all: '', tag: '1', col: '2', author: '3', article: '-1', history: '-2' };\n Store.set('find', findMap[filter] !== undefined ? findMap[filter] : '', { render: false });\n \/\/ Store.markDirty('find');\n \/\/ Store.scheduleFlush();\n syncFilterButtons();\n updateAddArea();\n invalidateSection();\n render();\n}\n\nfunction toggleHidden() {\n const next = !Store.getUI('showHidden');\n Store.setUI('showHidden', next, { render: false });\n eyeOpenGlobal.style.display = next ? 'none' : '';\n eyeClosedGlobal.style.display = next ? '' : 'none';\n updateAddArea();\n invalidateSection();\n render();\n}\n\nfunction toggleManage() {\n const next = !Store.getUI('manageMode');\n Store.setUI('manageMode', next, { render: false });\n if(!next) Store.setUI('showCleandTags', false, { render: false });\n manageBtn.classList.toggle('active', next);\n updateAddArea();\n invalidateSection();\n render();\n}\n\n\/* ===== 新增区 ===== *\/\nfunction renderAllTypeButtons() {\n allTypeArea.innerHTML = ALL_TYPES.map(item =>\n `<button type=\"button\" class=\"all-type-btn${Store.state.allType[item.key]?' active':''}\" data-key=\"${item.key}\">${item.label}<\/button>`\n ).join('');\n}\n\nfunction updateAddArea() {\n const currentFilter = Store.getUI('currentFilter');\n tagAddArea.style.display = currentFilter === 'tag' ? 'flex' : 'none';\n colAddArea.style.display = currentFilter === 'col' ? 'flex' : 'none';\n authorAddArea.style.display = currentFilter === 'author' ? 'flex' : 'none';\n articleAddArea.style.display = currentFilter === 'article' ? 'flex' : 'none';\n historyArea.style.display = currentFilter === 'history' ? 'flex' : 'none';\n allTypeArea.style.display = currentFilter === 'all' ? 'flex' : 'none';\n if(currentFilter === 'all') renderAllTypeButtons();\n if(currentFilter === 'tag') {\n if(Store.getUI('showCleandTags')) {\n newTagInput.placeholder = '屏蔽关键词';\n addTagBtn.textContent = '屏蔽';\n filterCleandBtn.classList.add('active');\n } else {\n newTagInput.placeholder = '标签名称';\n addTagBtn.textContent = '添加';\n filterCleandBtn.classList.remove('active');\n }\n }\n addArea.style.display = (Store.getUI('manageMode') || (currentFilter === 'all' && Store.getUI('showHidden'))) ? 'flex' : 'none';\n}\n\n\/* ===== 增删改 ===== *\/\nfunction delItem(item, type) {\n Store.reload();\n const message = type === 'tag' ? `确定删除标签「${item}」?` :\n type === 'cleandTag' ? `确定删除屏蔽词「${item}」?` :\n type === 'col' ? `确定移除合集「${item.name}」?` :\n type === 'author' ? `确定移除作者「${item.name}」?` :\n type === 'article' ? `确定移除文章「${item.title}」?` :\n type === 'history' ? `确定移除历史「${item.title}」?` : '';\n if(!message || !confirm(message)) return;\n\n if(type === 'tag') {\n Store.mutate('tags', (l) => { const i = l.indexOf(item); if(i !== -1) l.splice(i, 1); return l; });\n Store.mutate('hiddenTags', (l) => l.filter(x => x !== item), { render: false });\n Host.toast('标签已删除');\n } else if(type === 'cleandTag') {\n Store.mutate('cleandTags', (l) => { const i = l.indexOf(item); if(i !== -1) l.splice(i, 1); return l; });\n Store.mutate('disCleandTags', (l) => { const j = l.indexOf(item); if(j !== -1) l.splice(j, 1); return l; }, { render: false });\n Host.toast('屏蔽词已删除');\n } else if(type === 'col') {\n Store.mutate('collections', (l) => l.filter(c => !(c.name === item.name && String(c.id) === String(item.id))));\n Host.toast('合集已移除');\n } else if(type === 'author') {\n Store.mutate('authors', (l) => l.filter(a => String(a.blogId) !== String(item.blogId)));\n Host.toast('作者已移除');\n } else if(type === 'article') {\n Store.mutate('articles', (l) => l.filter(a => !(a.title === item.title && a.url === item.url)));\n Host.toast('文章已移除');\n } else if(type === 'history') {\n Store.mutate('history', (l) => l.filter(a => a.url !== item.url), { render: false });\n Store.saveHistory(); Host.toast('历史已移除');\n }\n invalidateSection();\n render();\n}\n\nfunction editItem(item, type, button) {\n Store.reload();\n const rowEl = button.closest('.item');\n if(!rowEl) return;\n const nameSpan = rowEl.querySelector('.name-text');\n if(!nameSpan) return;\n rowEl.classList.add('editing');\n rowEl.style.width = rowEl.getBoundingClientRect().width + 'px';\n rowEl.style.flexShrink = '0';\n const currentName = type === 'tag' ? item : type === 'article' ? item.title : item.name;\n const input = document.createElement('input');\n input.type = 'text';\n input.value = currentName;\n input.className = 'edit-input';\n input.style.flex = '1 1 auto';\n input.style.minWidth = '0';\n const idSpan = rowEl.querySelector('.id-text');\n if(idSpan) idSpan.style.display = 'none';\n nameSpan.replaceWith(input);\n input.focus();\n input.addEventListener('click', e => e.stopPropagation());\n input.addEventListener('mousedown', e => e.stopPropagation());\n let finished = false;\n\n function finish(save) {\n if(finished) return;\n finished = true;\n rowEl.classList.remove('editing');\n const newName = input.value.trim();\n if(save && newName && newName !== currentName) {\n Store.reload();\n let ok = false;\n if(type === 'tag') {\n Store.mutate('tags', (l) => {\n let i = l.indexOf(item);\n if(i === -1) i = l.indexOf(newName);\n if(i !== -1) { l[i] = newName; ok = true; }\n return l;\n }, { render: false });\n Store.mutate('hiddenTags', (l) => l.map(x => x === item ? newName : x), { render: false });\n } else if(type === 'col') {\n Store.mutate('collections', (l) => {\n const target = l.find(x => x.name === item.name && String(x.id) === String(item.id));\n if(target) { target.name = newName; ok = true; }\n return l;\n }, { render: false });\n } else if(type === 'author') {\n Store.mutate('authors', (l) => {\n const target = l.find(x => String(x.blogId) === String(item.blogId));\n if(target) { target.name = newName; ok = true; }\n return l;\n }, { render: false });\n } else if(type === 'article') {\n Store.mutate('articles', (l) => {\n const target = l.find(x => x.title === item.title && x.url === item.url);\n if(target) { target.title = newName; ok = true; }\n return l;\n }, { render: false });\n }\n if(ok) {\n Store.flush();\n const label = { tag: '标签', col: '合集', author: '作者', article: '文章' } [type] || '项';\n Host.toast(`${label}名称已修改为「${newName}」`);\n } else {\n Host.toast('目标项已不存在,可能已被外部修改');\n }\n invalidateSection();\n render();\n } else {\n invalidateSection();\n render();\n }\n }\n input.addEventListener('keydown', e => {\n if(e.key === 'Enter') { e.preventDefault(); finish(true); }\n else if(e.key === 'Escape') { e.preventDefault(); finish(false); }\n });\n input.addEventListener('blur', () => setTimeout(() => finish(true), 0));\n}\n\nasync function addItem(type) {\n Store.reload();\n try {\n if(type === 'tag') {\n const value = newTagInput.value.trim();\n if(!value) { Host.toast(Store.getUI('showCleandTags') ? '请输入屏蔽词' : '请输入标签名称'); return; }\n if(Store.getUI('showCleandTags')) {\n const r = Store.appendField('cleandTags', value, x => x === value);\n if(r === 'dup') { Host.toast('该屏蔽词已存在'); return; }\n if(r !== 'ok') { Host.toast('写入失败,请重试'); return; }\n \/* 同名的 disCleandTags 项顺带清掉 *\/\n Store.mutate('disCleandTags', (l) => {\n const i = l.indexOf(value);\n if(i !== -1) l.splice(i, 1);\n return l;\n }, { render: false });\n Store.flush();\n Host.toast(`屏蔽词「${value}」已添加`);\n } else {\n const r = Store.appendField('tags', value, x => x === value);\n if(r === 'dup') { Host.toast('标签已存在'); return; }\n if(r !== 'ok') { Host.toast('写入失败,请重试'); return; }\n Host.toast(`标签「${value}」已添加`);\n }\n newTagInput.value = '';\n invalidateSection();\n render();\n } else if(type === 'col') {\n const nameInput = document.getElementById('newColNameInput');\n const idInput = document.getElementById('newColIdInput');\n let name = nameInput.value.trim();\n const id = extractColId(idInput.value);\n if(!id) { Host.toast('请输入合集ID或链接'); return; }\n if(!\/^\\d+$\/.test(id)) { Host.toast('合集ID必须为数字'); return; }\n if(!name) {\n name = await fetchColName(id);\n if(!name) { Host.toast('无法自动获取合集名称,请手动填写'); return; }\n nameInput.value = name;\n }\n const r = Store.appendField('collections', { name, id }, x => x && String(x.id) === id);\n if(r === 'dup') { Host.toast('合集已存在'); return; }\n if(r !== 'ok') { Host.toast('写入失败,请重试'); return; }\n nameInput.value = ''; idInput.value = '';\n Host.toast(`合集「${name}」已添加`);\n invalidateSection();\n render();\n } else if(type === 'author') {\n const nameInput = document.getElementById('newAuthorNameInput');\n const idInput = document.getElementById('newAuthorIdInput');\n let name = nameInput.value.trim();\n const blogId = extractBlogId(idInput.value);\n if(!blogId) { Host.toast('请输入作者BID或链接'); return; }\n if(!\/^\\d+$\/.test(blogId)) { Host.toast('作者BID必须为数字'); return; }\n if(!name) {\n name = await fetchAuthor(blogId);\n if(!name) { Host.toast('无法自动获取作者名称,请手动填写'); return; }\n nameInput.value = name;\n }\n const r = Store.appendField('authors', { name, blogId: blogId }, x => x && String(x.blogId) === blogId);\n if(r === 'dup') { Host.toast('作者已存在'); return; }\n if(r !== 'ok') { Host.toast('写入失败,请重试'); return; }\n nameInput.value = ''; idInput.value = '';\n Host.toast(`作者「${name}」已添加`);\n invalidateSection();\n render();\n } else if(type === 'article') {\n const titleInput = document.getElementById('newArticleTitleInput');\n const urlInput = document.getElementById('newArticleUrlInput');\n let title = titleInput.value.trim();\n const url = urlInput.value.trim();\n if(!url) { Host.toast('请输入文章链接'); return; }\n if(!\/^https?:\\\/\\\/\/i.test(url)) { Host.toast('链接需以 http(s):\/\/ 开头'); return; }\n if(!title) {\n const fetched = await fetchArticleTitle(url);\n if(!fetched) { Host.toast('无法自动获取标题,请手动填写'); return; }\n title = fetched;\n titleInput.value = fetched;\n }\n const r = Store.appendField('articles', { title, url, hidden: Hidden.SHOW }, x => x && x.url === url, { prepend: true });\n if(r === 'dup') { Host.toast('文章链接已存在'); return; }\n if(r !== 'ok') { Host.toast('写入失败,请重试'); return; }\n titleInput.value = ''; urlInput.value = '';\n Host.toast(`文章「${title}」已添加`);\n invalidateSection();\n render();\n }\n } catch (e) {\n Host.toast('添加失败');\n }\n}\n\n\/* ===== 移除头像 ===== *\/\nfunction removeAvatar() {\n if(avatarHandlerBound) return;\n avatarHandlerBound = true;\n document.addEventListener('click', e => {\n if(!Store.getUI('showHidden')) return;\n const avatarEl = e.target.closest('.author-avatar');\n if(!avatarEl) return;\n const authorItemEl = avatarEl.closest('.author-item');\n if(!authorItemEl) return;\n const blogId = String(authorItemEl.dataset.authorBlogId || '');\n if(!blogId) return;\n Store.reload();\n const author = findItem(Store.state.authors, a => String(a.blogId) === blogId);\n if(!author || !('avatar' in author)) return;\n e.stopPropagation();\n e.preventDefault();\n if(!confirm(`确定移除「${author.name}」的头像?`)) return;\n Store.mutate('authors', (l) => {\n const target = l.find(a => String(a.blogId) === blogId);\n if(target) delete target.avatar;\n return l;\n });\n invalidateSection('authors');\n render();\n Host.toast('已移除头像');\n }, true);\n}\n\n\/* ===== 拖拽排序 ===== *\/\nlet dragState = null, holdTimer = null, holdTarget = null;\nlet holdStartX = 0, holdStartY = 0, suppressClickUntil = 0;\n\nfunction arrForType(type) { return TYPE_TO_FIELD[type] ? Store.state[TYPE_TO_FIELD[type]] : null; }\n\nfunction fieldForType(type) { return TYPE_TO_FIELD[type] || null; }\n\nfunction keyOfType(type) { return STRING_TYPES[type] ? (x => x) : (x => JSON.stringify(x)); }\n\nfunction clearHold() {\n if(holdTimer) { clearTimeout(holdTimer); holdTimer = null; }\n holdTarget = null;\n}\n\nfunction beginDrag(item, handle, x, y) {\n const rect = item.getBoundingClientRect();\n const parent = item.parentNode;\n if(!parent) return;\n const placeholder = document.createElement('div');\n placeholder.className = 'drag-placeholder';\n placeholder.style.width = rect.width + 'px';\n placeholder.style.height = rect.height + 'px';\n const ghost = item.cloneNode(true);\n ghost.classList.add('drag-ghost');\n ghost.style.width = rect.width + 'px';\n ghost.style.height = rect.height + 'px';\n ghost.style.left = rect.left + 'px';\n ghost.style.top = rect.top + 'px';\n parent.insertBefore(placeholder, item);\n document.body.appendChild(ghost);\n item.style.display = 'none';\n dragState = { item, handle, placeholder, ghost, parent, offsetX: x - rect.left, offsetY: y - rect.top };\n Host.vibrate(30);\n}\n\nfunction updateDrag(x, y) {\n if(!dragState) return;\n const drag = dragState;\n drag.ghost.style.left = (x - drag.offsetX) + 'px';\n drag.ghost.style.top = (y - drag.offsetY) + 'px';\n const el = document.elementFromPoint(x, y);\n if(!el) return;\n const target = el.closest('.item');\n if(!target || target === drag.item || target === drag.placeholder || target.parentNode !== drag.parent) return;\n const rect = target.getBoundingClientRect();\n const refNode = x < rect.left + rect.width \/ 2 ? target : target.nextSibling;\n if(drag.placeholder !== refNode) drag.parent.insertBefore(drag.placeholder, refNode);\n}\n\nfunction reorderFromDOM(type, parentList) {\n const arr = arrForType(type);\n if(!arr) return;\n const items = parentList.querySelectorAll('.item');\n const keys = [];\n for (let i = 0; i < items.length; i++) {\n const handle = items[i].querySelector('.drag-handle');\n if(!handle || handle.dataset.itemType !== type) continue;\n keys.push(handle.dataset.itemValue);\n }\n if(!keys.length) return;\n reorderByKey(arr, keys, keyOfType(type));\n}\n\nfunction endDrag(commit) {\n if(!dragState) return;\n const drag = dragState;\n drag.item.style.display = '';\n if(commit) {\n drag.parent.insertBefore(drag.item, drag.placeholder);\n const type = drag.handle.dataset.itemType;\n reorderFromDOM(type, drag.parent);\n const field = fieldForType(type);\n if(field) {\n Store.markDirty(field);\n Store.flush();\n if(!PERSIST_SET.has(field)) Store.saveHistory();\n }\n }\n if(drag.placeholder.parentNode) drag.placeholder.parentNode.removeChild(drag.placeholder);\n if(drag.ghost.parentNode) drag.ghost.parentNode.removeChild(drag.ghost);\n dragState = null;\n suppressClickUntil = Date.now() + 300;\n}\n\n\/* ===== 事件绑定 ===== *\/\nfilterBar.addEventListener('click', e => {\n const btn = e.target.closest('.filter-btn');\n if(!btn) return;\n setFilter(btn.dataset.filter);\n});\n\n\/\/ searchBtn.addEventListener('click', ()=>{ Store.flush(); Host.open('search'); });\nmanageBtn.addEventListener('click', toggleManage);\ntoggleHiddenBtn.addEventListener('click', toggleHidden);\n\nallTypeArea.addEventListener('click', e => {\n const btn = e.target.closest('.all-type-btn');\n if(!btn) return;\n e.stopPropagation();\n Store.reload();\n const key = btn.dataset.key;\n if(!key || !(key in Store.state.allType)) return;\n Store.mutate('allType', (current) => {\n const next = Object.assign({}, current);\n next[key] = next[key] ? 0 : 1;\n return next;\n }, { render: false });\n Store.flush();\n btn.classList.toggle('active', !!Store.state.allType[key]);\n invalidateSection();\n render();\n});\n\ncontainer.addEventListener('error', e => {\n const target = e.target;\n if(target && target.tagName === 'IMG') target.style.display = 'none';\n}, true);\n\ncontainer.addEventListener('pointerdown', e => {\n if(dragState) return;\n const handle = e.target.closest('.drag-handle');\n if(!handle) return;\n const item = handle.closest('.item');\n if(!item) return;\n holdStartX = e.clientX;\n holdStartY = e.clientY;\n holdTarget = { handle, item, x: e.clientX, y: e.clientY };\n holdTimer = setTimeout(() => {\n const pending = holdTarget;\n holdTarget = null;\n holdTimer = null;\n if(pending) beginDrag(pending.item, pending.handle, pending.x, pending.y);\n }, DRAG_HOLD_MS);\n});\n\ndocument.addEventListener('pointermove', e => {\n if(dragState) {\n if(e.cancelable) e.preventDefault();\n updateDrag(e.clientX, e.clientY);\n return;\n }\n if(holdTimer && holdTarget) {\n if(Math.abs(e.clientX - holdStartX) > DRAG_SLOP ||\n Math.abs(e.clientY - holdStartY) > DRAG_SLOP) {\n clearHold();\n }\n }\n}, { passive: false });\n\ndocument.addEventListener('pointerup', e => {\n if(dragState) { if(e.cancelable) e.preventDefault(); endDrag(true); return; }\n clearHold();\n});\n\ndocument.addEventListener('pointercancel', () => {\n if(dragState) endDrag(false);\n clearHold();\n});\n\ncontainer.addEventListener('click', e => {\n if(suppressClickUntil && Date.now() < suppressClickUntil) return;\n\n const editingItem = container.querySelector('.item.editing');\n if(editingItem) {\n const editInput = editingItem.querySelector('.edit-input');\n if(!editingItem.contains(e.target)) {\n if(editInput) editInput.blur();\n } else if(!e.target.closest('.item-actions')) {\n if(editInput) editInput.blur();\n return;\n }\n }\n\n const favBtn = e.target.closest('.fav-btn');\n if(favBtn) {\n e.preventDefault();\n e.stopPropagation();\n toggleFav(favBtn.dataset.favUrl, favBtn);\n return;\n }\n\n const actionBtn = e.target.closest('.eye-btn, .del-btn, .edit-btn');\n if(actionBtn) {\n e.stopPropagation();\n const type = actionBtn.dataset.itemType;\n const value = actionBtn.dataset.itemValue;\n let item;\n if(type === 'tag' || type === 'cleandTag') item = value;\n else { try { item = JSON.parse(value); } catch (err) { return; } }\n if(actionBtn.classList.contains('eye-btn')) {\n const rowEl = actionBtn.closest('.item');\n if(type === 'cleandTag') toggleCleand(item, rowEl);\n else toggleItem(item, type, rowEl);\n } else if(actionBtn.classList.contains('del-btn')) {\n delItem(item, type);\n } else if(actionBtn.classList.contains('edit-btn')) {\n editItem(item, type, actionBtn);\n }\n return;\n }\n\n if(e.target.closest('.readme-item')) { java.open('login'); return; }\n if(e.target.closest('.random-item')) { openRandom(); return; }\n if(e.target.closest('.bookstore-item')) { openBookStore(); return; }\n if(e.target.closest('.cleand-tag-item')) return;\n\n if(e.target.closest('.subscribe-btn')) {\n Store.setUI('currentRank', null, { render: false });\n if(Store.getUI('subscrCollections')) {\n Store.setUI('subscrCollections', null, { render: false });\n invalidateSection();\n render();\n } else {\n runAsync(loadSubscrCols());\n }\n return;\n }\n\n const rankBtn = e.target.closest('.rank-btn');\n if(rankBtn) {\n Store.setUI('subscrCollections', null, { render: false });\n const sortNo = parseInt(rankBtn.dataset.sortno, 10);\n const rankType = rankBtn.dataset.type;\n const current = Store.getUI('currentRank');\n if(current && current.sortNo === sortNo && current.type === rankType) {\n Store.setUI('currentRank', null, { render: false });\n invalidateSection();\n render();\n } else {\n runAsync(loadRank(sortNo, rankType));\n }\n return;\n }\n\n const rankItem = e.target.closest('.rank-item');\n if(rankItem) {\n const idx = parseInt(rankItem.dataset.index, 10);\n const current = Store.getUI('currentRank');\n const item = current && current.items[idx];\n if(!item) return;\n const rankType = current.type;\n if(rankType === 'hotword') jump('sort', buildTag(item.title));\n else if(rankType === 'hotarticle') {\n if(item.postId && item.blogId) {\n const body = `targetblogid=${encodeURIComponent(item.blogId)}&postid=${encodeURIComponent(item.postId)}`;\n jump('rss', `${API_BASE}\/oldapi\/post\/detail.api,${JSON.stringify({method:'POST','accept-encoding':'gzip, deflate',body})}`, item.title);\n } else if(!openArticle(item.url, item.title)) jumpUrl(item.url);\n } else if(rankType === 'hotuser') jump('sort', buildAuthor({ name: item.title, blogId: item.blogId }));\n else if(rankType === 'hottag') jump('sort', buildTag(item.title.replace(\/^#\\s*\/, '').trim()));\n return;\n }\n\n const subColItem = e.target.closest('.sub-col-item');\n if(subColItem) {\n const id = subColItem.dataset.id;\n let name = subColItem.dataset.name;\n if(id && name) {\n name = name.replace(\/\\[[图文热新]\\]\/gi, '');\n jump('sort', buildCol({ name, id }));\n }\n return;\n }\n\n const tagItem = e.target.closest('.tag-item');\n if(tagItem) { Store.reload(); const tag = tagItem.dataset.tag; if(tag) jump('sort', buildTag(tag)); return; }\n\n const colItem = e.target.closest('.col-item');\n if(colItem) {\n const id = colItem.dataset.colId;\n let name = colItem.dataset.colName;\n if(id && name) {\n const col = findItem(Store.state.collections, x => String(x.id) === String(id) && x.name === name);\n if(col) runAsync(updateColCount(col), { silent: true });\n name = name.replace(\/\\[[图文热新]\\]\/gi, '');\n jump('sort', buildCol({ name, id }));\n }\n return;\n }\n\n const authorItem = e.target.closest('.author-item');\n if(authorItem) {\n const blogId = authorItem.dataset.authorBlogId;\n const name = authorItem.dataset.authorName;\n const hiddenState = authorItem.dataset.hidden || Hidden.SHOW;\n if(hiddenState === Hidden.CLEAN) return;\n if(blogId && name) jump('sort', buildAuthor({ name, blogId: blogId }));\n return;\n }\n\n const articleItem = e.target.closest('.article-item');\n if(articleItem) {\n const url = articleItem.dataset.articleUrl;\n const title = articleItem.dataset.articleTitle;\n const hiddenState = articleItem.dataset.hidden || Hidden.SHOW;\n if(hiddenState === Hidden.HIDE) return;\n if(url && !openArticle(url, title)) jumpUrl(url);\n return;\n }\n\n const historyItem = e.target.closest('.history-item');\n if(historyItem) {\n const url = historyItem.dataset.historyUrl;\n const title = historyItem.dataset.historyTitle;\n if(url && !openArticle(url, title)) jumpUrl(url);\n return;\n }\n});\n\nfilterCleandBtn.addEventListener('click', () => {\n if(Store.getUI('currentFilter') !== 'tag') return;\n Store.setUI('showCleandTags', !Store.getUI('showCleandTags'), { render: false });\n updateAddArea();\n invalidateSection();\n render();\n});\n\nsubUpdateBtn.addEventListener('click', () => {\n runAsync((async () => {\n Store.reload();\n const cols = Store.state.collections;\n if(!cols.length) { Host.toast('暂无合集'); return; }\n const today = getToday();\n const pendingCols = cols.filter(c => {\n const parsed = parseColCount(c.count);\n const hasUpdate = c.update && +c.update > 0;\n return !(parsed && parsed.lastDate === today && hasUpdate);\n });\n if(!pendingCols.length) { Host.toast('今日合集数据均已更新'); return; }\n Host.toast(`正在更新 ${pendingCols.length}\/${cols.length} 个合集...`);\n let changedCount = 0;\n for (let i = 0; i < pendingCols.length; i++) {\n if(await updateColCount(pendingCols[i])) changedCount++;\n await new Promise(r => setTimeout(r, 300));\n }\n Store.markDirty('collections');\n Store.flush();\n invalidateSection('cols');\n render();\n Host.toast(changedCount > 0 ? `已更新 ${changedCount} 个合集` : '今日合集数据均已更新');\n })());\n});\n\naddTagBtn.addEventListener('click', () => runAsync(addItem('tag')));\ndocument.getElementById('addColBtn').addEventListener('click', () => runAsync(addItem('col')));\ndocument.getElementById('addAuthorBtn').addEventListener('click', () => runAsync(addItem('author')));\ndocument.getElementById('addArticleBtn').addEventListener('click', () => runAsync(addItem('article')));\n\n\/* ===== 导入历史 ===== *\/\nlet histMask = null;\n\nfunction histDialog(open) {\n if(!histMask) {\n histMask = document.createElement('div');\n histMask.className = 'hist-io-mask';\n histMask.innerHTML = '<div class=\"hist-io\"><div class=\"hist-io-head\"><h3 class=\"hist-io-title\">导入历史<\/h3><button type=\"button\" class=\"hist-io-x\">×<\/button><\/div><div class=\"hist-io-body\"><textarea class=\"hist-io-text\" placeholder=\"请在此处粘贴历史记录...\"><\/textarea><button type=\"button\" class=\"hist-io-btn\">导入<\/button><\/div><\/div>';\n histMask.addEventListener('click', e => {\n if(e.target === histMask || e.target.closest('.hist-io-x')) return histDialog(false);\n if(e.target.closest('.hist-io-btn')) histImport(histMask.querySelector('.hist-io-text').value);\n });\n document.body.appendChild(histMask);\n }\n if(open) {\n const ta = histMask.querySelector('.hist-io-text');\n ta.value = '';\n histMask.classList.add('open');\n setTimeout(() => ta.focus(), 50);\n } else {\n histMask.classList.remove('open');\n }\n}\n\nfunction histImport(text) {\n let d;\n try { d = JSON.parse(String(text || '').trim()); } catch (e) { return Host.toast('JSON 格式错误'); }\n if(!Array.isArray(d)) return Host.toast('数据格式错误');\n Store.reloadHistory();\n const cur = Store.state.history || [], seen = new Set(cur.map(x => x.url)), merged = cur.slice(); \n let n = 0;\n d.forEach(x => {\n if(!x || !x.url || seen.has(x.url)) return;\n seen.add(x.url);\n merged.push({ title: String(x.title || x.url).trim(), url: String(x.url).trim(), time: Number(x.time) || 0, ...(x.collectionId ? { collectionId: String(x.collectionId) } : 0), ...(x.collectionName ? { collectionName: String(x.collectionName) } : 0) }); \n n++;\n });\n if(!n) { histDialog(false); return Host.toast('无新增记录'); }\n merged.sort((a, b) => (+b.time || 0) - (+a.time || 0));\n if(merged.length > 500) merged.length = 500;\n Store.mutate('history', () => merged, { render: false });\n Store.saveHistory();\n invalidateSection('history');\n render();\n histDialog(false);\n Host.toast(`已导入 ${n} 条`);\n}\n\nimportHistoryBtn.addEventListener('click', () => histDialog(true));\nexportHistoryBtn.addEventListener('click', () => {\n Store.reloadHistory();\n const l = Store.state.history || [];\n if(!l.length) return Host.toast('暂无历史记录');\n const t = document.createElement('textarea');\n t.value = JSON.stringify(l);\n t.style.cssText = 'position:fixed;left:-9999px';\n document.body.appendChild(t); t.select();\n let ok = 0;\n try { ok = document.execCommand('copy'); } catch (e) {}\n document.body.removeChild(t);\n Host.toast(ok ? `已复制 ${l.length} 条` : '复制失败');\n});\nclearHistoryBtn.addEventListener('click', () => {\n Store.reloadHistory();\n const n = Store.state.history.length;\n if(!n) { Host.toast('历史记录为空'); return; }\n if(!confirm(`确定清空全部历史记录?共 ${n} 条`)) return;\n Store.mutate('history', () => [], { render: false });\n Store.saveHistory();\n invalidateSection('history');\n render();\n Host.toast('历史记录已清空');\n});\n\n\/* ===== 左右滑动切换分类 ===== *\/\nconst SWIPE_FILTERS = filterBtns.map(btn => btn.dataset.filter);\nlet swipeActive = false, swipeStartX = 0, swipeStartY = 0;\n\ndocument.addEventListener('touchstart', e => {\n const t = e.target;\n \/* 输入框、弹窗、拖拽中不触发滑动切换 *\/\n const blocked = !!(t && t.closest('input, textarea, select, #addArea, .drag-handle, .drag-ghost, .drag-placeholder'));\n\n swipeActive = !dragState && !blocked && e.touches.length === 1;\n if(swipeActive) {\n swipeStartX = e.touches[0].clientX;\n swipeStartY = e.touches[0].clientY;\n }\n}, { passive: true });\n\ndocument.addEventListener('touchend', e => {\n if(!swipeActive) return;\n swipeActive = false;\n const touch = e.changedTouches[0];\n if(!touch) return;\n const dx = touch.clientX - swipeStartX;\n const dy = touch.clientY - swipeStartY;\n if(Math.abs(dx) < 50 || Math.abs(dy) > 60 || Math.abs(dx) < Math.abs(dy) * 1.5) return;\n const currentIdx = Math.max(0, SWIPE_FILTERS.indexOf(Store.getUI('currentFilter')));\n suppressClickUntil = Date.now() + 350;\n setFilter(SWIPE_FILTERS[(currentIdx + (dx < 0 ? 1 : -1) + SWIPE_FILTERS.length) % SWIPE_FILTERS.length]);\n Host.vibrate(15);\n}, { passive: true });\n\n\/* ===== 生命周期 ===== *\/\nwindow.addEventListener('pagehide', () => Store.flush());\nwindow.addEventListener('beforeunload', () => Store.flush());\ndocument.addEventListener('visibilitychange', () => {\n if(document.visibilityState === 'hidden') Store.flush();\n});\n\n\/* ===== 初始化 ===== *\/\nStore.load();\nconst initialFind = Store.state.find;\nlet initialFilter = 'all';\nif(initialFind === '1') initialFilter = 'tag';\nelse if(initialFind === '2') initialFilter = 'col';\nelse if(initialFind === '3') initialFilter = 'author';\nelse if(initialFind === '-1') initialFilter = 'article';\nelse if(initialFind === '-2') initialFilter = 'history';\nStore.state.ui.currentFilter = initialFilter;\nStore.on('render', () => render());\n\nsyncFilterButtons();\nupdateAddArea();\nremoveAvatar();\nrender();\n})();",
"startStyle": "\/* ===== 01. 全局基础 ===== *\/\nhtml, body { touch-action: pan-x pan-y; -webkit-text-size-adjust: 100%; overscroll-behavior: none; }\nbody { margin: 0; padding: 0; font-family: sans-serif; background: #f5f5f5; }\n\n\/* ===== 02. 顶部筛选 ===== *\/\n.filter-bar { position: sticky; top: 0; z-index: 10; display: flex; align-items: center; gap: 4px; padding: 5px; background: #fff; box-shadow: 0 1px 1.6px rgba(0, 0, 0, 0.05); }\n\n\/* ===== 03. 筛选按钮 ===== *\/\n.filter-btn { display: flex; align-items: center; gap: 4px; padding: 4px 8px; border: 1px solid #ddd; border-radius: 5px; background: #fff; color: #666; font-size: 14px; cursor: pointer; transition: 0.2s; }\n.filter-btn.active { border-color: #09635d66; background: #09635d; color: #fff; }\n.filter-btn span { display: none; }\n.filter-btn.active span { display: inline; }\n\n\/* ===== 04. 搜索\/切换\/管理 ===== *\/\n.toggle-btn, .manage-btn { display: flex; align-items: center; padding: 4px; border: none; background: none; color: #666; cursor: pointer; transition: 0.2s; }\n.toggle-btn { margin-left: auto; }\n.manage-btn.active { color: #09635d; }\n\n\/* ===== 05. 显示\/隐藏按钮 ===== *\/\n.eye-btn { display: inline-flex; flex-shrink: 0; align-items: center; margin-right: 2px; padding: 2px; border: none; background: none; color: #555; cursor: pointer; transition: color 0.2s; }\n.eye-btn:hover { color: #000; }\n\n\/* ===== 06. 添加区域容器 ===== *\/\n.add-area-container { box-sizing: border-box; display: flex; align-items: center; gap: 5px; width: 100%; padding: 8px; border-bottom: 1px solid #eee; background: #fff; }\n.sub-add-area { box-sizing: border-box; display: flex; flex: 1; align-items: center; gap: 5px; width: 100%; max-width: 100%; }\n.sub-add-area input { box-sizing: border-box; flex: 1; min-width: 0; padding: 8px; border: 1px solid #ccc; border-radius: 5px; }\n.sub-add-area button { flex-shrink: 0; padding: 8px 12px; border: none; border-radius: 5px; background: #09635d; color: #fff; white-space: nowrap; }\n\n\/* ===== 07. 内容分区 ===== *\/\n.section { margin: 20px; }\n.section-title { display: flex; align-items: center; margin-bottom: 10px; color: #333; font-size: 18px; font-weight: bold; }\n.section-title::before { content: \"\"; display: inline-block; flex-shrink: 0; width: 3px; height: 16px; margin-right: 8px; border-radius: 2px; background: #09635d; }\n.list { display: flex; flex-wrap: wrap; gap: 8px; }\n\n\/* ===== 08. 通用列表 ===== *\/\n.item { display: flex; flex-wrap: nowrap; align-items: center; gap: 2px; min-width: 0; overflow: hidden; padding: 6px 10px; border-radius: 20px; background: #fff; box-shadow: 0 2px 3px rgba(0, 0, 0, 0.05); font-size: 15px; cursor: pointer; transition: 0.2s; }\n.item:active { background: #09635d; color: #fff; }\n.hidden-item { background: #fff !important; opacity: 0.45; }\n\n\/* ===== 09. 名称与备注 ===== *\/\n.name-text { display: block; flex: 1 1 0; min-width: 0; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }\n.id-text { flex-shrink: 0; margin-left: 2px; color: #999; font-size: 12px; white-space: nowrap; }\n.id-text.up { color: #09635d; }\n\n\/* ===== 10. 项目操作按钮组 ===== *\/\n.item-actions { display: inline-flex; flex-shrink: 0; align-items: center; gap: 2px; margin-left: auto; }\n.item-actions button { display: flex; flex-shrink: 0; align-items: center; padding: 2px; border: none; background: none; color: #666; cursor: pointer; }\n.item-actions button:hover { color: #000; }\n.item-actions .del-btn { color: #555; }\n.item-actions .del-btn:hover { color: #dc3545; }\n\n\/* ===== 11. 内联编辑输入框 ===== *\/\n.item .edit-input { flex: 0 0 auto; width: auto; min-width: 3em; max-width: 100%; margin: 0; padding: 0; border: none; border-radius: 2px; outline: none; background: transparent; color: #000; font: inherit; }\n\n\/* ===== 12. 特殊按钮样式 ===== *\/\n.readme-item { background: #dc3545; color: #fff; }\n.random-item, .bookstore-item { background: #09635d; color: #fff; }\n.subscribe-btn { background: #fff; outline: 0.8px solid #09635d; outline-offset: -0.8px; }\n\n\/* ===== 13. 拖拽手柄 ===== *\/\n.drag-handle { display: inline-flex; flex-shrink: 0; align-items: center; margin: 0; padding: 2px; color: #999; cursor: grab; touch-action: none; }\n.drag-handle:active { cursor: grabbing; }\n\n\/* ===== 14. 拖拽占位虚框 ===== *\/\n.drag-placeholder { box-sizing: border-box; display: flex; flex-shrink: 0; flex-wrap: nowrap; align-items: center; border: 1px dashed #09635d; border-radius: 20px; background: rgba(9, 99, 93, 0.07); pointer-events: none; animation: phPulse 1.2s ease-in-out infinite; }\n@keyframes phPulse { 0%, 100% { background: rgba(9, 99, 93, 0.05); } 50% { background: rgba(9, 99, 93, 0.14); } }\n\n\/* ===== 15. 拖拽跟随浮层 ===== *\/\n.drag-ghost { position: fixed !important; z-index: 9999; box-sizing: border-box; display: flex; flex-wrap: nowrap; align-items: center; overflow: hidden; margin: 0 !important; border: 1px solid #09635d; box-shadow: 0 6px 16px rgba(0, 0, 0, 0.25) !important; opacity: 0.92; transform: scale(1.04); transition: none !important; pointer-events: none; }\n.drag-ghost .name-text { flex: 1 1 auto; min-width: 0; max-width: 100%; overflow: hidden; white-space: nowrap; text-overflow: clip; }\n.item.manage-item .name-text { overflow: hidden; }\n\n\/* ===== 16. 图像及图标 ===== *\/\n.author-avatar, .tag-avatar, .cover-avatar { \/* width: 1.1em; *\/ flex-shrink: 0; width: auto; height: 1.1em; margin-right: 2px; background: transparent; color: #555; object-fit: cover; }\n.author-avatar { border-radius: 50%; }\n.cover-avatar { border-radius: 20%; }\n\n\/* ===== 17. 榜单\/订阅按钮 ===== *\/\n.rank-btn.active, .subscribe-btn.active { border: none; background: #09635d; color: #fff; }\n\n\/* ===== 18. 屏蔽词过滤按钮 \/ 更新 \/ 搜索 \/ 清空记录 ===== *\/\n.filter-cleand-btn, .sub-update-btn, .search-btn, .history-clear-btn { display: flex; align-items: center; padding: 4px; border: none; background: none; color: #fff; cursor: pointer; }\n\n\/* ===== 19. 特殊项目 ===== *\/\n.cleand-tag-item { align-items: center; background: #fdf0f0; }\n.cleand-author-item { align-items: center; background: #fdf0f0 !important; opacity: 1 !important; }\n\n\/* ===== 20. 空状态提示 ===== *\/\n.empty { padding: 40px 0; color: #666; text-align: center; }\n\n\/* ===== 21. 禁止文本选择 ===== *\/\n.item, .name-text, .id-text, .section-title { -webkit-touch-callout: none; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none; }\n\n\/* ===== 22. 全部区块按钮 ===== *\/\n#allTypeArea { box-sizing: border-box; display: flex; flex: 1 1 100%; flex-wrap: nowrap; align-items: center; width: 100%; max-width: 100%; padding: 3px; border-radius: 8px; background: #efefef; }\n#allTypeArea .all-type-btn { box-sizing: border-box; flex: 1 1 0; min-width: 0; overflow: hidden; padding: 5px 4px; border: none; border-radius: 6px; background: transparent; color: #8a8a8a; font-size: 13px; line-height: 1.2; text-align: center; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; transition: background 0.18s, color 0.18s, box-shadow 0.18s; -webkit-tap-highlight-color: transparent; }\n#allTypeArea .all-type-btn.active { background: #09635d; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); color: #fff; font-weight: 600; }\n\n\/* ===== 23. 虚拟化(已弃用) ===== *\/\n\n\/* ===== 24. 历史收藏 ===== *\/\n.history-item .fav-btn { display: inline-flex; flex-shrink: 0; align-items: center; justify-content: center; margin-left: 6px; padding: 2px; border: none; background: none; color: #666; cursor: pointer; transition: color 0.2s ease, transform 0.18s cubic-bezier(0.4, 0, 0.2, 1); }\n.history-item .fav-btn:hover { color: #09635d; }\n.history-item .fav-btn:active { transform: scale(0.86); }\n.history-item .fav-btn.active { color: #09635d; }\n\n\/* ===== 25. 历史导入弹窗 ===== *\/\n.hist-io-mask{position:fixed;inset:0;z-index:1002;display:none;align-items:center;justify-content:center;padding:16px;background:rgba(0,0,0,.5);}\n.hist-io-mask.open{display:flex;}\n.hist-io{width:100%;max-width:420px;background:#fff;border-radius:12px;box-shadow:0 12px 32px rgba(0,0,0,.2);overflow:hidden;animation:hioUp .24s ease;}\n@keyframes hioUp{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:translateY(0);}}\n.hist-io-head{display:flex;justify-content:space-between;align-items:center;padding:12px 16px;background:#09635d;color:#fff;}\n.hist-io-title{margin:0;font-size:15px;font-weight:600;}\n.hist-io-x{font-size:16px;border:none;background:rgba(255,255,255,.15);color:#fff;width:24px;height:24px;border-radius:50%;display:flex;align-items:center;justify-content:center;cursor:pointer;font-size:13px;line-height:1;padding:0;}\n.hist-io-x:active{transform:scale(.9);}\n.hist-io-body{padding:14px;display:flex;flex-direction:column;gap:10px;}\n.hist-io-text{width:100%;min-height:180px;box-sizing:border-box;font-family:monospace;font-size:12px;line-height:1.5;padding:10px;border-radius:8px;border:1px solid #ddd;background:#f5f5f5;color:#333;resize:vertical;outline:none;}\n.hist-io-text:focus{border-color:#09635d;}\n.hist-io-btn{padding:10px;border:none;border-radius:8px;background:#09635d;color:#fff;font-size:14px;font-weight:600;cursor:pointer;font-family:inherit;}\n.hist-io-btn:active{transform:scale(.97);}\n\n\/* ===== 26. 深色模式 ===== *\/\n@media (prefers-color-scheme: dark) {\n .history-item .fav-btn.active, .history-item .fav-btn:hover { color: #4fbfb0; }\n \/* .item { background: rgb(45, 45, 45); } *\/\n}\n\n",
"style": "\/* ===== 01. 变量与全局 ===== *\/\n:root{\n \/* 主题 *\/\n --brand:#09635d;--brand-rgb:9,99,93;\n --brand-light:#4fbfb0;--brand-dark:#064a45;--brand-fixed:#09635d;\n --brand-soft:#09635d0f;\n --brand-shadow:#09635d33;\n --brand-line:#09635d4d;\n --brand-border:#09635d80;\n \/* 背景 *\/\n --bg:#f2efe8;\n --surface:#fcfaf5;--surface-2:#ebe7de;--surface-3:#e2ddd2;\n --tint:var(--brand-soft);\n \/* 文字 *\/\n --text:#2c2823;--text-2:#5f5a52;--text-3:#948d82;\n --white:#ffffff;--link:#007590;\n \/* 描边 *\/\n --line:rgba(80,70,55,.1);--line-2:rgba(80,70,55,.18);\n \/* 遮罩 & 叠加 *\/\n --mask:rgba(0,0,0,.6);--mark-bg:transparent;\n --overlay:rgba(0,0,0,.5); --overlay-2:rgba(0,0,0,.72);\n --glass:rgba(255,255,255,.15); --glass-2:rgba(255,255,255,.28);\n --edge:rgba(0,0,0,.13);\n --text-outline:1px 0 var(--edge),-1px 0 var(--edge),0 1px var(--edge),0 -1px var(--edge);\n --chip:rgba(200,206,215,.14);\n \/* 代码 *\/\n --code-bg:#1e2429;--code-fg:#e6edf3;\n \/* 金 & 暖 *\/\n --gold:#c8901c;--gold-bg:#faf1dd;--gold-fg:#a87634;\n --gold-grad:linear-gradient(135deg,rgba(255,200,90,.22),rgba(255,150,60,.14));\n \/* 警告 &态度 & 作者 *\/\n --warn:#d97a1e;--warn-bg:rgba(240,130,40,.1);\n --haha:#c47d0e;--haha-bg:rgba(196,125,14,.1);\n --sweet:#c2367a;--sweet-bg:rgba(194,54,122,.1);\n --bitter:#7a3b94;--bitter-bg:rgba(122,59,148,.1);\n --author:#1f4977;--author-bg:rgba(70,130,180,.1);\n \/* 阴影 & 圆角 *\/\n --sh-sm:0 1px 2px rgba(80,70,55,.04);\n --sh-lg:0 12px 32px rgba(40,30,20,.14),0 4px 12px rgba(40,30,20,.08);\n --r-bento:10px;--r-chip:6px;\n --r-sm:6px;--r-md:10px;--r-lg:14px;\n \/* 间距 *\/\n --sp-1:2px;--sp-2:4px;--sp-3:6px;--sp-4:8px;--sp-5:12px;--sp-6:16px;\n \/* 字号 *\/\n --fs-body:18px;--fs-title:20px;--fs-lg:16px;--fs-md:15px;\n --fs-sm:14px;--fs-xs:13px;--fs-2xs:12px;--fs-3xs:11px;\n \/* 字体 *\/\n --mono:\"Maple Mono NF CN\",ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;\n --sans:-apple-system,BlinkMacSystemFont,\"Segoe UI\",\"PingFang SC\",\"Hiragino Sans GB\",\"Microsoft YaHei\",Roboto,\"Helvetica Neue\",Arial,sans-serif;\n \/* 过渡 *\/\n --ease:background-color .2s ease,color .2s ease,opacity .2s ease;\n --ease-box:background-color .2s ease,color .2s ease,border-color .2s ease,box-shadow .2s ease; \n --ease-tf:transform .18s cubic-bezier(.4,0,.2,1);\n}\n\nhtml,body{-webkit-text-size-adjust:100%;overscroll-behavior:none;color:var(--text);font-family:var(--sans);background:var(--bg);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;}\n*{box-sizing:border-box;max-width:100%;overflow-wrap:break-word;}\nimg,video,iframe{max-width:100%!important;height:auto;}\niframe{border:none;vertical-align:middle;}\na{color:var(--brand);text-decoration:none;transition:var(--ease);}\na:active{opacity:.7;}\n.post{margin:0;padding:0 var(--sp-3);}\nul{padding-left:var(--sp-5);margin:0 0 1em;}\n\/* 焦点可见性 *\/\n:where(button,a,[tabindex]):focus-visible{outline:2px solid var(--brand);outline-offset:2px;}\n\/* 降级:减少动效 \/ 弱化模糊 *\/\n@media (prefers-reduced-motion:reduce){\n.cpop-mask,.cb-mask,.ad-mask{backdrop-filter:none;-webkit-backdrop-filter:none;}\n.post-col__details[open] .post-col__desc{animation:none;}\n}\n\n\/* ===== 02. 标题 ===== *\/\n.post-title{font-size:var(--fs-title);font-weight:700;line-height:1.4;padding:var(--sp-4) var(--sp-2);text-align:center;letter-spacing:.2px;margin:0 0 var(--sp-2);}\n\n\/* ===== 03. 作者栏 ===== *\/\n.post-author{display:flex;align-items:center;font-size:var(--fs-md);margin:10px 0;padding:var(--sp-4) 10px;background:var(--surface);border-radius:var(--r-md);border:1px solid var(--line);box-shadow:var(--sh-sm);gap:var(--sp-4);}\n.post-author__avatar{height:36px;width:36px;border-radius:50%;box-shadow:0 0 0 1.5px var(--brand);flex-shrink:0;-webkit-touch-callout:none;user-select:none;object-fit:cover;}\n.post-author__name{font-size:var(--fs-md);font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;-webkit-touch-callout:none;user-select:none;}\n.btn{cursor:pointer;background:transparent;border:none;color:var(--text-3);font-size:1.1em;padding:var(--sp-2);border-radius:var(--r-sm);transition:var(--ease-box);display:inline-flex;align-items:center;justify-content:center;}\n.btn:hover{color:var(--brand);background:var(--tint);}\n.btn:active{transform:scale(.92);}\n.btn--follow{margin-left:auto;margin-right:var(--sp-2);}\n.btn--article-add{margin-right:var(--sp-2);}\n\n\/* ===== 04. 合集栏 ===== *\/\n.post-col{display:flex;align-items:center;flex-wrap:wrap;font-size:var(--fs-md);margin:var(--sp-4) 0;padding:var(--sp-3) 0;gap:var(--sp-1);}\n.post-col__label{color:var(--text-2);margin-right:var(--sp-1);font-size:var(--fs-xs);}\n.post-col__info{background:var(--brand);border-radius:999px;padding:3px 10px;margin:var(--sp-1) var(--sp-2);font-size:var(--fs-xs);font-weight:500;color:var(--white);cursor:pointer;box-shadow:0 2px 6px var(--brand-shadow);-webkit-touch-callout:none;user-select:none;transition:var(--ease-tf);}\n.post-col__info:active{transform:scale(.96);}\n.btn--import,.btn--open,.btn--tts{cursor:pointer;color:var(--brand);background:var(--tint);border-radius:var(--r-chip);font-size:var(--fs-sm);margin:0 3px;padding:var(--sp-2) var(--sp-3);display:inline-flex;align-items:center;transition:var(--ease-box),var(--ease-tf);}\n.btn--import:active,.btn--open:active,.btn--tts:active{transform:scale(.9);background:var(--surface-3);}\n\n\/* ===== 05. 折叠介绍 ===== *\/\n.post-col__details{display:block;width:100%;flex-basis:100%;margin:var(--sp-1) 0;border-radius:var(--r-bento);background:transparent;overflow:hidden;}\n.post-col__details[open]{margin:0 0 var(--sp-3);background:var(--surface-2);}\n.post-col__details summary{cursor:pointer;color:var(--text-3);font-size:var(--fs-xs);font-weight:500;padding:var(--sp-2) var(--sp-3);list-style:none;display:inline-flex;align-items:center;gap:var(--sp-3);user-select:none;transition:var(--ease);}\n.post-col__details[open] summary{display:flex;padding:var(--sp-4) var(--sp-5) var(--sp-2);color:var(--text-2);}\n.post-col__details summary::-webkit-details-marker{display:none;}\n.post-col__details summary::marker{display:none;content:\"\";}\n.post-col__details summary::before{content:\"\";width:0;height:0;border-left:4.5px solid currentColor;border-top:3.5px solid transparent;border-bottom:3.5px solid transparent;opacity:.5;flex-shrink:0;transition:transform .24s cubic-bezier(.4,0,.2,1);transform-origin:30% 50%;}\n.post-col__details[open] summary::before{transform:rotate(90deg);opacity:.65;}\n.post-col__details summary:hover,.post-col__details summary:active{color:var(--brand);}\n.post-col__desc{color:var(--text-2);font-size:var(--fs-xs);text-align:justify;line-height:1.75;padding:0 var(--sp-5) var(--sp-5) 24px;}\n.post-col__details[open] .post-col__desc{animation:detailsOpen .22s ease;}\n@keyframes detailsOpen{from{opacity:0;transform:translateY(-4px);}to{opacity:1;transform:translateY(0);}}\n\n\/* ===== 06. 标签 ===== *\/\n.post-tags{display:flex;flex-wrap:wrap;align-items:center;font-size:var(--fs-md);margin:var(--sp-2) 0;gap:var(--sp-1);}\n.post-tags__label{color:var(--text-2);font-size:var(--fs-xs);}\n.post-tags__item{background:var(--surface-2);border:1px solid var(--line);border-radius:var(--r-chip);padding:var(--sp-1) var(--sp-3);margin:var(--sp-1) var(--sp-2);font-size:var(--fs-xs);color:var(--text-2);cursor:pointer;-webkit-touch-callout:none;user-select:none;transition:var(--ease-box);}\n.post-tags__item:hover{background:var(--tint);color:var(--brand);border-color:var(--brand-line);}\n.post-tags__item:active{transform:scale(.96);}\n\n\/* ===== 07. 时间 ===== *\/\n.post-time{font-size:var(--fs-2xs);text-align:right;color:var(--text-3);padding:var(--sp-1) var(--sp-2) 0 0;letter-spacing:.3px;}\n\n\/* ===== 08. 计数 ===== *\/\n.fa{display:inline-block;font:normal normal normal 14px\/1 'iconfont';font-size:inherit;text-rendering:auto;font-smoothing:antialiased;}\n.post-counts{font-size:var(--fs-2xs);color:var(--text-2);display:flex;flex-wrap:wrap;gap:14px;margin:0 0 var(--sp-3);padding:var(--sp-3) 0;line-height:1.2;}\n.post-counts__item{display:inline-flex;align-items:center;gap:var(--sp-2);font-family:\"iconfont\"!important;font-size:15px;font-style:normal;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;color:var(--text-2);transition:var(--ease);}\n.post-counts__item:hover{color:var(--brand);}\n.p-count-favorite:before{content:\"\\e61f\";}\n.p-count-response:before{content:\"\\e601\";}\n.p-count-subscribe:before{content:\"\\e65A\";}\n.p-count-share:before{content:\"\\e660\";}\n.p-count-showgift:before{content:\"\\edbf\";}\n.p-count-showaudio:before{content:\"\\ee05\";}\n.fa-heart:before{content:\"\\f004\"}\n.fa-times:before{content:\"\\f00d\"}\n.fa-user:before{content:\"\\f007\"}\n.fa-lock:before{content:\"\\f023\"}\n.fa-bookmark:before{content:\"\\f02e\"}\n.fa-asterisk:before{content:\"\\f069\"}\n.fa-folder:before{content:\"\\f07b\"}\n.fa-trophy:before{content:\"\\f091\"}\n.fa-feed:before{content:\"\\f09e\"}\n.fa-angle-right:before{content:\"\\f105\"}\n.fa-location-arrow:before{content:\"\\f124\"}\n.fa-search-plus:before{content:\"\\f00e\"}\n.fa-slack:before{content:\"\\f198\"}\n.fa-ban:before{content:\"\\f05e\"}\n.fa-listen:before{content:\"\\f087\"}\n.fa-play-circle:before{content:\"\\e038\"}\n.fa-pause-circle:before{content:\"\\e035\"}\n.fa-skip-next:before{content:\"\\e044\"}\n.fa-skip-previous:before{content:\"\\e045\"}\n\n\/* ===== 09. 正文 ===== *\/\n.post-content{font-family:var(--mono);font-size:var(--fs-body);line-height:1.5;text-align:justify;overflow-wrap:break-word;margin:0 0 var(--sp-6);padding:12px 0;\/*var(--sp-1);*\/white-space:normal;text-align-last:left;word-break:auto-phrase;word-break:break-all;}\n.post-content img{max-width:100%;height:auto;border-radius:var(--r-md);margin:10px 0;\/*border:1px solid var(--line);*\/display:block;}\n.post-content a{color:var(--brand);border-bottom:1px solid transparent;\/*var(--brand-line);*\/padding-bottom:1px;word-break:break-all;text-align:justify;text-align-last:left;}\n.post-content a:hover{border-bottom-color:var(--brand);}\n.post-content h1,.post-content h2,.post-content h3{line-height:1.4;margin:1.2em 0 .6em;font-weight:700;}\n.post-content p{margin:0 0 1em;text-align-last:left;}\n.post-content blockquote{margin:1em 0;padding:.6em 1em;border-left:3px solid var(--brand);background:var(--tint);border-radius:0 var(--r-sm) var(--r-sm) 0;color:var(--text-2);}\n.post-content code{font-family:var(--mono);font-size:.92em;background:var(--surface-2);padding:.15em .4em;border-radius:4px;color:var(--brand-dark);}\n.post-content pre{background:var(--code-bg);color:var(--code-fg);padding:14px;border-radius:var(--r-md);overflow-x:auto;margin:1em 0;}\n.post-content pre code{background:transparent;color:inherit;padding:0;}\n.post-content br+br{display:block;content:\"\";margin-top:var(--sp-4);}\n\n\/* ===== 10. 图片查看器 ===== *\/\n.iv{position:relative;margin:var(--sp-5) 0;border-radius:var(--r-bento);overflow:hidden;background:var(--surface-2);}\n.iv-stage{position:relative;width:100%;}\n.iv-slide{display:none;width:100%;}\n.iv-slide.active{display:block;}\n.iv-slide img{display:block;width:100%;height:auto;border-radius:var(--r-sm);}\n.iv-bar{position:absolute;top:8px;right:8px;display:flex;align-items:center;gap:var(--sp-3);z-index:2;}\n.iv-page,.iv-btn{background:var(--overlay);color:var(--white);font-size:var(--fs-2xs);padding:var(--sp-2) var(--sp-4);border-radius:999px;line-height:1;}\n.iv-page{pointer-events:auto;cursor:pointer;font-weight:500;}\n.iv-btn{border:none;cursor:pointer;display:inline-flex;align-items:center;gap:var(--sp-2);transition:var(--ease);}\n.iv-btn:hover{background:var(--overlay-2);}\n.iv-btn:active{transform:scale(.94);}\n\n.iv.iv-vertical .iv-slide img{display:block;width:100%;height:auto;border-radius:0;margin:0}\n.iv.iv-vertical .iv-zoom,.iv.iv-vertical .iv-open,.iv.iv-vertical .iv-slide:not(:first-child) .iv-page{display:none}\n\n\/* ===== 11. 视频查看器 ===== *\/\n.video{border-radius:var(--r-lg);overflow:hidden;margin-bottom:var(--sp-5);border:1px solid var(--line);}\n.video iframe{width:100%;aspect-ratio:4\/3;border:none;display:block;}\n\n\/* ===== 12. 解锁内容 ===== *\/\n.unlock-section{margin-bottom:var(--sp-6);}\n.unlock-item{margin:10px 0;padding:var(--sp-5) 14px;background:var(--surface-2);border:1px solid var(--line);border-left:3px solid var(--brand-border);border-radius:var(--r-bento);}\n.unlock-item__title{font-weight:700;color:var(--brand);margin-bottom:var(--sp-3);font-size:var(--fs-md);}\n.unlock-item__content{font-family:var(--sans);font-weight:600;font-size:17px;line-height:1.5;overflow-wrap:break-word;white-space:pre-wrap;text-align:justify;}\n.unlock-item__images{display:flex;flex-wrap:wrap;gap:var(--sp-4);margin-top:var(--sp-4);}\n.unlock-item__images img{max-width:100%;height:auto;border-radius:var(--r-sm);}\n\n\/* 付费未解锁卡片 *\/\n.unlock-item--support .unlock-item__content{font-family:var(--sans);font-size:var(--fs-sm);line-height:1.5;white-space:normal;text-align:left;}\n.support-card{color:var(--text);}\n.support-header{display:flex;align-items:center;gap:var(--sp-3);margin-bottom:var(--sp-4);}\n.support-badge{flex-shrink:0;padding:var(--sp-1) 7px;border-radius:4px;font-size:var(--fs-2xs);font-weight:500;background:var(--gold);color:var(--white);}\n.support-title{font-size:var(--fs-md);font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}\n.support-tags{display:flex;flex-wrap:nowrap;gap:var(--sp-3);margin-bottom:10px;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:none;-webkit-mask-image:linear-gradient(to right,#000 calc(100% - 32px),transparent 100%);mask-image:linear-gradient(to right,#000 calc(100% - 32px),transparent 100%);}\n.support-tags::-webkit-scrollbar{display:none;}\n.support-tag{padding:3px var(--sp-4);border-radius:4px;font-size:var(--fs-2xs);font-weight:500;background:var(--gold-bg);color:var(--gold-fg);white-space:nowrap;flex-shrink:0;}\n.support-tag--comment{background:var(--gold-bg);color:var(--gold-fg);}\n.support-tag--comment i{margin-left:var(--sp-1);font-size:var(--fs-3xs);opacity:.75;}\n.support-promotion{font-size:var(--fs-sm);line-height:1.6;color:var(--text-3);margin-bottom:var(--sp-5);text-align:justify;}\n.support-digest{font-size:var(--fs-lg);line-height:1.6;color:var(--text-2);margin-bottom:var(--sp-6);position:relative;max-height:6.2em;overflow:hidden;text-align:justify;word-break:break-word;white-space:pre-line;}\n.support-digest::after{content:'';position:absolute;bottom:0;left:0;right:0;height:2em;background:linear-gradient(transparent,var(--surface-2));pointer-events:none;}\n.support-lock{display:flex;align-items:center;justify-content:center;gap:var(--sp-3);color:var(--text-3);font-size:var(--fs-xs);}\n.support-lock i{font-size:var(--fs-2xs);}\n.support-app-btn{margin-left:auto;flex-shrink:0;background:transparent;border:none;padding:0;color:var(--text-3);font-size:15px;line-height:1;cursor:pointer;transition:var(--ease);}\n.support-app-btn:hover{color:var(--brand);}\n.support-app-btn:active{transform:scale(.9);}\n\n\/* ===== 13. 评论 ===== *\/\n.comments{margin-bottom:20px;}\n.cm,.cm-rp{font-size:var(--fs-md);padding:5px 6px;margin-bottom:var(--sp-2);background:var(--surface);border:1px solid var(--line);border-radius:var(--r-bento);transition:var(--ease-box);}\n.cm:hover,.cm-rp:hover{border-color:var(--line-2);}\n.cm:last-child,.cm-rp:last-child{margin-bottom:0;}\n.cm-head,.cm-rp-head{display:flex;align-items:center;flex-wrap:nowrap;gap:var(--sp-3);padding-bottom:var(--sp-2);min-width:0;}\n.cm-rp-head{white-space:nowrap;gap:var(--sp-1);}\n.cm-ava,.cm-rp-ava{height:20px;width:20px;border-radius:50%;box-shadow:0 0 0 1px var(--line);flex-shrink:0;}\n.cm-user,.cm-rp-user{font-family:var(--sans);color:var(--brand);font-weight:600;font-size:var(--fs-xs);background:var(--tint);padding:var(--sp-1) var(--sp-3);border-radius:var(--r-chip);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 0 auto;min-width:0;}\n.cm-user{max-width:85vw;}\n.cm-top{background:var(--gold-bg);color:var(--gold-fg);font-size:var(--fs-xs);padding:var(--sp-1) var(--sp-2);border-radius:var(--r-chip);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:0 0 auto;min-width:0;}\n.cm-rp-user{max-width:8em;}\n.cm-rp-head .cm-rp-user:first-of-type{max-width:50vw;}\n.cm-rp-head .cm-rp-user:last-of-type{flex:0 1 auto;max-width:60vw;}\n.cm-head .cm-quote,.cm-rp-head .cm-quote{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-3);font-size:var(--fs-xs);}\n.cm-body,.cm-rp-body{font-size:var(--fs-lg);line-height:1.5;text-align:justify;overflow-wrap:anywhere;text-align-last:left;word-break:break-all;}\n.cm-foot,.cm-rp-foot{text-align:right;font-size:var(--fs-3xs);color:var(--text-3);line-height:1.2;margin-top:var(--sp-2);padding-right:var(--sp-1);}\n.cm-foot{margin-bottom:var(--sp-1);margin-right:6px;}\n.cm-rp{background:var(--surface-2);padding:5px 6px;margin-bottom:var(--sp-1);}\n.cm-rp-at{color:var(--text-3);flex-shrink:0;}\n.emo{height:28px!important;border-radius:4px!important;vertical-align:text-bottom;}\n.cm-img,.cm-imgs{max-height:48px;border-radius:var(--r-sm)!important;display:inline-block;vertical-align:top;}\n.cm-more{text-align:center;padding:12px 14px;margin:var(--sp-2) 0;border:1px solid var(--line);border-radius:var(--r-bento);color:var(--brand);font-size:var(--fs-sm);font-weight:600;cursor:pointer;user-select:none;transition:var(--ease-box);}\n.cm-more:hover{background:var(--tint);border-color:var(--brand-line);}\n.cm-more:active{transform:scale(.98);}\n.cm-more.disabled{color:var(--text-3);pointer-events:none;border-color:transparent;}\n.cm-more-rp{text-align:center;color:var(--brand);font-size:var(--fs-2xs);cursor:pointer;user-select:none;padding:var(--sp-1) 0;font-weight:500;}\n\/*.is-quoted::after{visibility:hidden;content:'✦';font-size:15px;margin-right:var(--sp-1);color:var(--text-3);opacity:.6;pointer-events:none;display:inline-block;transform:rotate(90deg);}*\/\n.is-flash{color:var(--link);transition:color .5s ease;}\n.cm-user.author,.cm-rp-user.author{color:var(--author);background:var(--author-bg);}\n.cm a{word-break:break-all;text-align:justify;text-align-last:left;}\n.comments b em{background:linear-gradient(transparent 62%,rgba(9,99,93,.62) 62%,transparent);font-style:normal;-webkit-font-smoothing:antialiased;font-weight:900}\n\n\/* ===== 14. 评论计数标记 ===== *\/\n.cm-mark{\/*position:relative;*\/margin-left:3px;vertical-align:middle;font-size:10px;color:var(--text-3);border:1px solid var(--line);padding:1.6px 4px;border-radius:8px 8px 8px 2px;white-space:nowrap;font-weight:600;cursor:pointer;transition:var(--ease-box);background:var(--mark-bg);user-select:none;text-shadow:none!important;}\n.cm-mark:hover{background:var(--surface-3);color:var(--text-2);border-color:var(--line-2);}\n\/* 标记内的图片表情 *\/\n.cm-mark .emo-word{height:1.5em;max-height:1.5em;width:auto;border-radius:2px;vertical-align:middle;display:inline-block;padding-bottom:2px;}\n.cm-mark .emo-word-fb{display:none;}\n\/* 标记外沿小尾巴 *\/\/*\n.cm-mark::before{content:\"\";position:absolute;left:3px;bottom:-4px;width:0;height:0;border-top:4px solid var(--line);border-right:6px solid transparent;}\n.cm-mark::after{content:\"\";position:absolute;left:4px;bottom:-2px;width:0;height:0;border-top:3px solid var(--bg);border-right:4px solid transparent;}*\/\n\n\/* ===== 15. 链接区域 ===== *\/\n.post-links-container{margin-top:var(--sp-6);}\n.post-links{font-size:var(--fs-sm);color:var(--text-2);border-top:1px solid var(--line);padding-top:8px;display:flex;flex-wrap:wrap;gap:10px;vertical-align:middle;}\n.post-links span{padding:5px 0;}\n.post-links a{padding:5px 10px;background:var(--surface-2);border-radius:999px;font-size:var(--fs-xs);}\n.post-links a:hover{background:var(--tint);color:var(--brand);}\n\n\/* ===== 16. 弹窗通用 ===== *\/\n.cpop-mask,.cb-mask{display:none;position:fixed;inset:0;width:100%;height:100%;background:var(--mask);z-index:999;justify-content:center;align-items:flex-start;touch-action:none;overscroll-behavior:contain;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);}\n.cpop-mask{padding-top:16px;z-index:1000;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);}\n.cb-mask{padding-top:var(--sp-4);padding-bottom:16px;}\n.cpop-mask.open,.cb-mask.open{display:flex;animation:fadeIn .2s ease;}\n@keyframes fadeIn{from{opacity:0;}to{opacity:1;}}\n@keyframes slideUp{from{opacity:0;transform:translateY(12px);}to{opacity:1;transform:translateY(0);}}\n.cpop-x,.cb-x{border:none;color:var(--white);width:24px;height:24px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;transition:var(--ease);flex-shrink:0;font-size:var(--fs-sm);}\n.cpop-x{background:var(--glass);}.cb-x{background:var(--edge);}\n.cpop-x:hover,.cb-x:hover{background:var(--glass-2);}\n.cpop-x:active,.cb-x:active{transform:scale(.9);}\n\n\/* ===== 17. 段评弹窗 ===== *\/\n.cpop{width:88vw;max-width:520px;max-height:90vh;background:var(--surface);border-radius:var(--r-lg);overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--sh-lg);}\n.cpop.open{animation:slideUp .24s cubic-bezier(.4,0,.2,1);}\n.cpop-head{display:flex;justify-content:space-between;align-items:center;padding:var(--sp-4) 14px;background:var(--brand);color:var(--white);border-radius:var(--r-lg) var(--r-lg) 0 0;flex-shrink:0;}\n.cpop-title{margin:0;font-size:var(--fs-sm);font-weight:600;letter-spacing:.3px;opacity:.92;}\n.cpop-quote{flex-shrink:0;max-height:5.5em;overflow-y:auto;padding:10px 16px;background:var(--surface-2);color:var(--text-2);font-size:var(--fs-sm);line-height:1.5;text-align:justify;cursor:pointer;user-select:none;border-bottom:1px solid var(--line);}\n.cpop-quote:hover{background:var(--surface-3);}\n.cpop-quote::-webkit-scrollbar{width:3px;}\n.cpop-quote::-webkit-scrollbar-thumb{background:var(--line-2);border-radius:3px;}\n.cpop-list{flex:1;overflow-y:auto;padding:var(--sp-5);overscroll-behavior:contain;background:var(--bg);}\n.cpop .cm,.cpop .cm-rp{margin:var(--sp-3) 0;}\n.cpop .cm-user,.cpop .cm-rp-user{max-width:80vw;\/*flex-shrink:1;*\/min-width:0;display:inline-block;}\n.cpop .cm-rp-user:first-of-type{max-width:50vw;}\n.cpop .cm-foot.sp,.cpop .cm-rp-foot.sp{display:flex;justify-content:space-between;align-items:center;gap:var(--sp-4);margin-top:var(--sp-2);line-height:1.2;}\n.cpop-stats{flex-shrink:0;display:flex;justify-content:space-around;align-items:center;padding:10px var(--sp-1);border-top:1px solid var(--line);background:var(--surface);font-size:var(--fs-2xs);}\n.cpop-stat{display:inline-flex;align-items:center;gap:var(--sp-2);color:var(--text-2);padding:3px 10px;border-radius:999px;background:var(--surface-2);font-weight:500;}\n.cpop-stat--haha{color:var(--haha);background:var(--haha-bg);}\n.cpop-stat--sweet{color:var(--sweet);background:var(--sweet-bg);}\n.cpop-stat--bitter{color:var(--bitter);background:var(--bitter-bg);}\n\/* 弹窗底部统计项 *\/\n.cpop-stat{display:inline-flex;flex-direction:column;align-items:center;gap:2px;padding:4px 10px;border-radius:var(--r-bento);font-weight:500;line-height:1.2;text-align:center;}\n\/* 统计项里的表情图 *\/\n.cpop-stat .stat-emo{height:1.5em;width:auto;display:block;border-radius:2px;flex-shrink:0;}\n\n\/* ===== 18. 合集弹窗 ===== *\/\n.cb{max-height:100%;min-width:82vw;max-width:88vw;overflow-y:auto;background:transparent;display:block;}\n.cb-head{display:flex;align-items:center;justify-content:space-between;position:sticky;top:0;z-index:1;padding:var(--sp-4) var(--sp-1);background:transparent;flex-shrink:0;}\n.cb-title{color:var(--white);margin:var(--sp-2);text-shadow:0 2px 8px var(--mask);font-size:var(--fs-title);font-weight:700;letter-spacing:.3px;}\n.cb-list{padding:0;}\n.cb-card{margin:var(--sp-4) 0;padding:14px 16px;background:var(--surface);border-radius:var(--r-bento);cursor:pointer;transition:var(--ease-box);border:1px solid var(--line);}\n.cb-card:hover{border-color:var(--line-2);}\n.cb-card:active{background:var(--surface-2);transform:scale(.99);}\n.cb-card-title{display:block;margin-bottom:var(--sp-3);font-weight:600;font-size:var(--fs-md);}\n.cb-card-desc{font-size:var(--fs-sm);color:var(--text-2);line-height:1.6;}\n.cb-card-info{font-size:var(--fs-xs);color:var(--text-3);margin-top:var(--sp-2);}\n\n\/* ===== 19. 加载与通用 ===== *\/\n.cpop-loading{text-align:center;padding:15px;color:var(--text-3);font-size:var(--fs-sm);user-select:none;opacity:.85;}\nsvg{vertical-align:bottom;color:currentColor;}\nstrong{font-weight:normal;text-shadow:var(--text-outline);}\n\n\/* ===== 20. 作者弹窗 ===== *\/\n.ad-mask{display:none;position:fixed;inset:0;background:var(--mask);z-index:1001;justify-content:center;align-items:center;touch-action:none;overscroll-behavior:contain;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);padding:16px;}\n.ad-mask.open{display:flex;animation:fadeIn .2s ease;}\n.ad{position:relative;width:100%;max-width:400px;max-height:85vh;max-height:85dvh;overflow-y:auto;background:var(--surface);border-radius:var(--r-lg);box-shadow:var(--sh-lg);padding:26px 20px 18px;animation:slideUp .24s cubic-bezier(.4,0,.2,1);overscroll-behavior:contain;}\n.ad-x{position:absolute;top:12px;right:12px;background:var(--surface-2);border:none;color:var(--text-2);width:26px;height:26px;border-radius:50%;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:var(--fs-sm);transition:var(--ease-box);}\n.ad-x:hover{background:var(--surface-3);color:var(--text);}\n.ad-x:active{transform:scale(.9);}\n.ad-status{text-align:center;padding:40px 0;color:var(--text-3);font-size:var(--fs-sm);}\n\n.ad-head{display:flex;flex-direction:column;align-items:center;gap:var(--sp-4);margin-bottom:var(--sp-6);}\n.ad-avatar{width:72px;height:72px;border-radius:50%;box-shadow:0 0 0 2px var(--brand);object-fit:cover;background:var(--surface-2);}\n.ad-avatar-wrap{position:relative;display:inline-block;width:120px;height:120px;flex-shrink:0;vertical-align:middle;}\n.ad-avatar-wrap .ad-avatar{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:block;width:72px;height:72px;box-shadow:none;}\n.ad-avatar-wrap .ad-avatar-frame{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);display:block;width:108px;height:108px;object-fit:contain;pointer-events:none;user-select:none;-webkit-touch-callout:none;z-index:2;filter:drop-shadow(0 1px 2px rgba(0,0,0,.18));}\n.ad-name{font-size:var(--fs-body);font-weight:700;color:var(--text);text-align:center;line-height:1.3;}\n.ad-domain{font-size:var(--fs-xs);color:var(--text-3);text-align:center;}\n.ad-intro{font-size:var(--fs-sm);line-height:1.65;color:var(--text-2);text-align:justify;padding:10px 14px;background:var(--surface-2);border-radius:var(--r-bento);margin-bottom:var(--sp-5);white-space:pre-wrap;overflow-wrap:break-word;max-height:10em;overflow-y:auto;}\n.ad-stats{display:flex;justify-content:space-around;margin-bottom:var(--sp-5);padding:14px var(--sp-2);background:var(--surface-2);border-radius:var(--r-bento);}\n.ad-stat{display:flex;flex-direction:column;align-items:center;gap:var(--sp-2);flex:1;min-width:0;}\n.ad-stat b{font-size:var(--fs-lg);font-weight:700;color:var(--brand);line-height:1;}\n.ad-stat span{font-size:var(--fs-2xs);color:var(--text-3);line-height:1;}\n.ad-tags{display:flex;flex-wrap:wrap;gap:var(--sp-3);align-items:center;margin-bottom:10px;font-size:var(--fs-xs);}\n.ad-tags-label{color:var(--text-3);margin-right:var(--sp-1);font-weight:600;flex-shrink:0;display:inline-flex;align-items:center;gap:var(--sp-2);}\n.ad-tags-label i{font-size:var(--fs-xs);line-height:1;display:inline-block;}\n.ad-tag{padding:3px 10px;border-radius:var(--r-chip);background:var(--tint);color:var(--brand);font-size:var(--fs-2xs);font-weight:500;line-height:1.5;}\n.ad-tags--warn .ad-tag{background:var(--warn-bg);color:var(--warn);}\n.ad-tags--warn .ad-tags-label{color:var(--warn);}\n.ad-achievements{display:flex;flex-wrap:wrap;gap:var(--sp-3);margin-bottom:10px;justify-content:center;}\n.ad-ach{padding:var(--sp-2) 10px;border-radius:var(--r-chip);background:var(--chip);color:var(--text-2);font-size:var(--fs-2xs);font-weight:600;line-height:1.5;display:inline-flex;align-items:center;gap:var(--sp-2);}\n.ad-ach i{font-size:var(--fs-2xs);line-height:1;display:inline-block;}\n.ad-ach--icon{padding-left:var(--sp-3);gap:var(--sp-2);background:var(--gold-grad);color:var(--gold-fg);}\n.ad-ach-icon{width:16px;height:16px;display:block;object-fit:contain;flex-shrink:0;}\n.ad-actions{display:flex;gap:10px;margin-top:var(--sp-6);}\n.ad-btn{flex:1;display:flex;align-items:center;justify-content:center;gap:var(--sp-3);padding:11px 14px;border:none;border-radius:var(--r-bento);font-size:var(--fs-sm);font-weight:600;cursor:pointer;font-family:inherit;transition:var(--ease-box),var(--ease-tf);}\n.ad-btn--follow{background:var(--brand);color:var(--white);}\n.ad-btn--follow:active{transform:scale(.97);background:var(--brand-dark);}\n.ad-btn--follow:disabled{background:var(--surface-3);color:var(--text-3);cursor:default;}\n.ad-btn--follow:disabled:active{transform:none;}\n.ad-btn--open{background:var(--surface-2);color:var(--text-2);}\n.ad-btn--open:active{transform:scale(.97);background:var(--surface-3);}\n\n\/* 头图:伪元素遮罩替代 inset box-shadow *\/\n.ad-head--cover{position:relative;background-size:cover;background-position:center;border-radius:var(--r-bento);padding:18px var(--sp-5);overflow:hidden;}\n.ad-head--cover::before{content:\"\";position:absolute;inset:0;background:var(--edge);border-radius:inherit;backdrop-filter:blur(.8px);}\n.ad-head--cover>*{position:relative;z-index:1;}\n.ad-head--cover .ad-name,.ad-head--cover .ad-domain{color:var(--white);text-shadow:0 1px 4px var(--mask);}\n\n.ad-tags-icon {height: 1.5em;width: auto;display: block;object-fit: contain;opacity:.8;}\n\n\/* ===== 21. 上\/下一篇 ===== *\/\n.post-nav{display:flex;gap:var(--sp-2);margin:var(--sp-2) 0;}\n.post-nav__item{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:var(--sp-1);padding:var(--sp-3);background:var(--surface);border:1px solid var(--line);border-radius:var(--r-bento);cursor:pointer;user-select:none;-webkit-touch-callout:none;text-decoration:none;color:inherit;transition:var(--ease-box);}\n.post-nav__item:hover{background:var(--tint);border-color:var(--brand-line);}\n.post-nav__item:active{transform:scale(.98);}\n.post-nav__item--disabled{opacity:.45;pointer-events:none;background:var(--surface-2);}\n.post-nav__label{font-size:var(--fs-3xs);color:var(--text-3);line-height:1;letter-spacing:.3px;}\n.post-nav__title{font-size:var(--fs-sm);color:var(--text);line-height:1.4;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;word-break:break-all;}\n.post-nav__item:nth-child(2){text-align:right;}\n.post-nav__item:nth-child(2) .post-nav__title{margin-left:auto;}\n.post-nav__item{-webkit-touch-callout:none;user-select:none;}\n\n\/* ===== 22.已删除占位 ===== *\/\n.post-deleted{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:60vh;padding:32px 20px;text-align:center;color:var(--text-2,#666);}\n.post-deleted__icon{font-size:52px;margin-bottom:14px;color:var(--warn);}\n.post-deleted__title{font-size:18px;font-weight:600;color:var(--text-1,#1f2328);margin-bottom:8px;}\n.post-deleted__desc{font-size:14px;line-height:1.6;margin-bottom:22px;word-break:break-all;}\n.post-deleted__btn{display:inline-block;padding:10px 22px;border-radius:var(--r-lg,10px);background:var(--brand,#09635d);color:#fff;font-size:14px;text-decoration:none;}\n\n\/* ===== 23. 投票卡片 ===== *\/\n.vote-card{margin:0;}\n.vote-title{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:var(--fs-md);margin-bottom:10px;line-height:1.4;}\n.vote-title b{flex-shrink:0;padding:2px 8px;border-radius:4px;font-size:var(--fs-2xs);font-weight:500;background:var(--brand);color:var(--white);}\n.vote-row{position:relative;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:7px 10px;margin-bottom:6px;border-radius:var(--r-sm);background:var(--surface-2);border:0.8px solid var(--line);overflow:hidden;font-size:var(--fs-sm);}\n.vote-bar{position:absolute;left:0;top:0;bottom:0;background:var(--brand-soft);pointer-events:none;border-radius:var(--r-sm);}\n.vote-name{position:relative;z-index:1;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.vote-num{position:relative;z-index:1;flex-shrink:0;color:var(--text-3);font-size:var(--fs-2xs);}\n.vote-row.voted{border-color:var(--brand-border);}\n.vote-row.voted .vote-bar{background:var(--tint);}\n.vote-row.voted .vote-name{color:var(--brand);font-weight:600;}\n.vote-foot{margin-top:8px;color:var(--text-3);font-size:var(--fs-2xs);text-align:right;}\n.vote-title__text{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}\n.vote-title .support-app-btn{margin-left:0;color:var(--text-3);}\n.vote-title .support-app-btn:hover{color:var(--brand);}\n\n\/* ===== 24. 合集目录 ===== *\/\n.toc-mask{display:none;position:fixed;inset:0;z-index:1003;background:var(--mask);backdrop-filter:blur(3px)}\n.toc-mask.open{display:block;animation:fadeIn .2s}\n.toc{position:absolute;inset:0 0 0 auto;width:74vw;max-width:420px;background:var(--surface);box-shadow:-14px 0 40px #0003;display:flex;flex-direction:column;animation:tocIn .34s cubic-bezier(.32,.72,0,1)}\n@keyframes tocIn{from{transform:translateX(100%)}}\n.toc-head{display:grid;grid-template-columns:1fr auto auto;align-items:start;gap:0px;padding:14px;border-bottom:1px solid var(--line);flex-shrink:0}\n.toc-title{margin:0;min-width:0;font-size:var(--fs-lg);font-weight:700;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n.toc-meta{font-size:var(--fs-3xs);color:var(--brand);background:var(--tint);padding:3px 10px;border-radius:999px;font-weight:600;letter-spacing:.3px;font-variant-numeric:tabular-nums;white-space:nowrap}\n.toc-x{width:32px;height:32px;margin:-4px -2px 4px 2px;padding:0 10px 2px 0;border:0;border-radius:50%;background:var(--surface-2);color:var(--text-2);font-size:var(--fs-md);display:flex;align-items:center;justify-content:right;cursor:pointer;transition:background .2s,transform .22s cubic-bezier(.34,1.56,.64,1),color .2s;}\n.toc-x:hover{background:var(--surface-3);color:var(--text)}\n.toc-x:active{transform:scale(.85) rotate(90deg)}\n.toc-list{flex:1;overflow-y:auto;padding:8px 10px 48px;overscroll-behavior:contain}\n.toc-item{position:relative;display:flex;align-items:flex-start;gap:12px;padding:8px 8px;margin-bottom:6px;border-radius:var(--r-md);color:var(--text);text-decoration:none;font-size:var(--fs-sm);line-height:1.5;background:var(--surface-2);transition:background .2s,color .2s,transform .2s cubic-bezier(.4,0,.2,1),box-shadow .2s;word-break:break-all;justify-content:space-between;}\n.toc-item:hover{background:var(--surface-3)}\n.toc-item:active{transform:scale(.98)}\n.toc-idx{flex-shrink:0;min-width:22px;margin-top:2px;text-align:center;color:var(--text-3);font-size:var(--fs-3xs);font-weight:600;letter-spacing:.3px;font-variant-numeric:tabular-nums;transition:color .2s}\n.toc-name{flex:1;min-width:0;word-break:break-all}\n.toc-item.active{background:var(--brand);color:var(--white);font-weight:600;box-shadow:0 4px 14px var(--brand-shadow)}\n.toc-item.active .toc-idx{color:#fffb}\n.toc-status{text-align:center;padding:60px 20px;color:var(--text-3);font-size:var(--fs-sm)}\n\n\/* ===== 25. LOFTER 听书 ===== *\/\n.tts-cap{position:fixed;top:8px;left:8px;right:8px;z-index:99999;width:calc(100% - 16px);height:52px;border-radius:26px;background:var(--surface);color:#1c1c1e;display:flex;align-items:center;gap:8px;padding:0 8px 0 10px;box-sizing:border-box;font:13px\/1 sans-serif;border:1.5px solid #09635d47;box-shadow:0 8px 28px #09635d2e,0 2px 8px #0002,inset 0 1px 0 #fff8;overflow:hidden;transform-origin:left center;transform:translate3d(0,0,0);will-change:transform,width,padding;transition:transform .42s cubic-bezier(.22,1,.36,1),width .42s cubic-bezier(.22,1,.36,1),padding .42s cubic-bezier(.22,1,.36,1),border-radius .3s cubic-bezier(.4,0,.2,1),background .3s ease,border-color .3s ease,box-shadow .3s ease} \n.tts-cap>*{opacity:.8;transform:scale(1);transform-origin:left center;will-change:opacity,transform;transition:opacity .28s ease,transform .34s cubic-bezier(.4,0,.2,1)} \n.tts-cap.collapsed{transform:translate3d(-34px,0,0);width:52px;padding:0;border-radius:26px;background:var(--glass)}\n.tts-cap.collapsed>*{opacity:0;transform:scale(.72);pointer-events:none;transition:opacity .18s ease,transform .24s cubic-bezier(.4,0,.2,1)} \n.tts-cap.collapsed::before{content:\"\";position:absolute;inset:0;border-radius:inherit;z-index:2}\n.tts-cap.collapsed::after{content:\"\";position:absolute; top:50%;left:50%;width:8px;height:8px;margin:-4px 0 0 -4px;border-radius:50%;background:var(--brand-fixed);opacity:.85;box-shadow:0 0 0 0 #09635d6b;animation:ttsPulse 2.2s ease-out infinite;pointer-events:none} \n@keyframes ttsPulse{0%{box-shadow:0 0 0 0 #09635d6b}70%{box-shadow:0 0 0 9px #09635d00}100%{box-shadow:0 0 0 0 #09635d00}}\n\n.tts-cap button,.tts-cap .c-x{width:32px;height:32px;border-radius:50%;border:none;background:rgba(9,99,93,.1);color:var(--brand);cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;padding:0;font-family:inherit;font-size:18px;line-height:1;overflow:hidden;transition:background .2s,transform .12s} \n.tts-cap button:active,.tts-cap .c-x:active{background:#09635d38;transform:scale(.9)} \n.tts-cap .c-pp{background:var(--brand);color:var(--white);box-shadow:0 3px 10px #09635d66,inset 0 1px 0 rgba(255,255,255,.2)} \n.tts-cap .c-pp:active{background:var(--brand-dark)}\n.tts-cap .c-x{width:30px;height:30px;font-size:15px;background:transparent;opacity:.7} \n.tts-cap .c-x:active{opacity:1}\n.tts-cap .c-p{flex:1;height:4px;border-radius:2px;background:var(--line-2);position:relative;cursor:pointer;min-width:40px;transition:height .15s,background .15s} \n.tts-cap .c-p::before{content:\"\";position:absolute;left:-8px;right:-8px;top:-16px;bottom:-16px}\n.tts-cap .c-p.on{height:6px;background:var(--line-2)}\n.tts-cap .c-p i{position:absolute;left:0;top:0;bottom:0;width:0;background:var(--brand);border-radius:2px;transition:width .1s linear}\n.tts-cap .c-p i::after{content:\"\";position:absolute;right:-7px;top:50%;transform:translateY(-50%) scale(.85);width:14px;height:14px;border-radius:50%;background:var(--brand);border:2px solid #fffd;box-shadow:0 2px 6px #0005;opacity:0;transition:opacity .15s,transform .15s;pointer-events:none}\n.tts-cap .c-p.on i::after{opacity:1;transform:translateY(-50%) scale(1)}\n.tts-cap .c-m{font-size:11px;color:var(--brand);opacity:.85;min-width:34px;text-align:right;font-variant-numeric:tabular-nums;flex-shrink:0;letter-spacing:.3px;font-weight:600} \n.tts-v{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;left:-9999px}\n.tts-hl, .tts-hl strong, .tts-hl strong *{color:var(--link)!important;text-shadow:none!important;transition:color .4s ease} \n\n\n\/* ===== 26. 深色模式 ===== *\/\n@media (prefers-color-scheme:dark){\n:root {\n--brand:#2a9d8f;--brand-rgb:42,157,143;--brand-dark:#1f7a70;\n--brand-border:#2a9d8f80;--tint:#2a9d8f1a;\n--surface:#211e1a;--surface-2:#2a2621;--surface-3:#332e28;\n--text:#ddd6cb;--text-2:#a39b8f;--text-3:#756d62;--white:#fafafa;\n--line:#ffffff12;--line-2:#ffffff21;\n--sh-sm:0 1px 2px #00000040;--sh-lg:0 12px 32px #00000080;\n--bg:#1a1815;--code-bg:#12110f;--code-fg:#ddd6cb;\n--edge:#ffffff33;--mark-bg:#66666621;\n--gold-fg:#d4a04c;--gold-bg:#c8901c24;\n--gold-grad:linear-gradient(135deg,#ffc85a1a,#ff963c14);\n--warn:#ee805a;--warn-bg:#f082282e;\n--haha:#e8a94a;--haha-bg:#e8a94a1a;\n--sweet:#e57ba8;--sweet-bg:#e57ba81a;\n--bitter:#b583c9;--bitter-bg:#b583c91a;\n--author:#7fb3e0;--author-bg:#7fb3e024;\n}\n\n\/* 顶部主题 *\/\n.cpop-head,.post-col__info{background:var(--brand-fixed);}\n.toc-item.active{background:var(--brand-fixed);}\n\/* 正文区 *\/\n.post-content{border-radius:var(--r-lg);background:var(--surface);border:1px solid var(--line);padding:12px var(--sp-3);color:var(--text);}\n.post-content code{color:var(--brand-light);}\n.post-content a{color:var(--brand-light);border-bottom-color:transparent;text-shadow:none;}\nstrong{text-shadow:var(--text-outline);color:var(--white);}\n\/* 段评弹窗微调 *\/\n.cpop .cm-rp{border-color:var(--line);}\n\/* 成就统一走金色 *\/\n.ad-ach{color:var(--gold-fg);background:var(--gold-grad);}\n\/* 标记回退文字 *\/\n.cm-mark .emo-word{display:none!important;}\n.cm-mark .emo-word-fb{display:inline!important;}\n.tts-cap.collapsed::after{background:var(--brand);box-shadow:0 0 0 0 var(--brand-border);animation-name:ttsPulseDark}\n@keyframes ttsPulseDark{0%{box-shadow:0 0 0 0 var(--brand-border)}70%{box-shadow:0 0 0 9px #2a9d8f00}100%{box-shadow:0 0 0 0 #2a9d8f00}}\n.tts-cap{color:var(--white);border-color:var(--brand-border);box-shadow:0 8px 28px #0008,0 2px 8px #0005,inset 0 1px 0 var(--line);}\n.tts-cap button,.tts-cap .c-x{background:var(--tint);color:var(--brand);}\n.tts-cap button:active,.tts-cap .c-x:active{background:var(--brand-border)}\n.tts-cap .c-pp{background:var(--brand);color:var(--white);box-shadow:0 3px 12px var(--brand-border),inset 0 1px 0 var(--edge)}\n.tts-cap .c-pp:active{background:var(--brand-dark)}\n.tts-cap .c-m{color:var(--brand-fixed)}\n.tts-cap .c-p i{background:var(--brand);}\n.tts-cap .c-p i::after{background:var(--brand);border-color:#0008;box-shadow:0 2px 6px #0009}\n.tts-cap .c-p,.tts-cap .c-p.on{background:#8884;}\n}",
"type": 0
}