// ==UserScript== // @name V2EX Plus - plus // @namespace https://v2ex.com/ // @version 3.5.1 // @description V2EX Plus userscript port of plus.js // @match https://v2ex.com/* // @match https://*.v2ex.com/* // @run-at document-start // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @icon https://v2ex.com/static/apple-touch-icon-180.png // @require https://code.jquery.com/jquery-3.7.1.min.js // @connect * // @updateURL https://raw.githubusercontent.com/thinktip/V2ex-Polish-Plus/main/v2ex.polish.plus.user.js // @downloadURL https://raw.githubusercontent.com/thinktip/V2ex-Polish-Plus/main/v2ex.polish.plus.user.js // ==/UserScript== (function () { if (typeof window.chrome !== "object") { window.chrome = {}; } if (typeof window.chrome.extension === "undefined") { window.chrome.extension = {}; } const runtimeState = { lastError: null, }; function normalizeStorageKeys(keys) { if (keys == null) return []; if (Array.isArray(keys)) return keys; if (typeof keys === "string") return [keys]; if (typeof keys === "object") return Object.keys(keys); return []; } function readStoredValues(keys) { const data = {}; for (const key of keys) { data[key] = GM_getValue(key); } return data; } function v2pGMRequest(details) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ timeout: 30000, ...details, onload: (response) => resolve(response), onerror: (error) => reject(error), ontimeout: () => reject(new Error("Request timed out")), }); }); } class WebDAV { constructor(serverUrl, username, password) { this.serverUrl = serverUrl.endsWith("/") ? serverUrl : `${serverUrl}/`; this.username = username; this.password = password; this.authHeader = `Basic ${btoa(`${username}:${password}`)}`; } async test() { const response = await v2pGMRequest({ method: "PROPFIND", url: this.serverUrl, headers: { Authorization: this.authHeader, Depth: "0", }, }); return response.status >= 200 && response.status < 300; } async get(filename) { const response = await v2pGMRequest({ method: "GET", url: this.serverUrl + filename, headers: { Authorization: this.authHeader, }, }); if (response.status === 404) { return null; } if (response.status < 200 || response.status >= 300) { throw new Error("WebDAV GET failed"); } return response.responseText ? JSON.parse(response.responseText) : null; } async put(filename, data) { const response = await v2pGMRequest({ method: "PUT", url: this.serverUrl + filename, headers: { Authorization: this.authHeader, "Content-Type": "application/json", }, data: JSON.stringify(data), }); if (response.status < 200 || response.status >= 300) { throw new Error("WebDAV PUT failed"); } return true; } } window.chrome.storage = window.chrome.storage || {}; window.chrome.storage.local = window.chrome.storage.local || { get(keys, callback) { callback(readStoredValues(normalizeStorageKeys(keys))); }, set(items, callback) { for (const [key, value] of Object.entries(items || {})) { GM_setValue(key, value); } if (typeof callback === "function") { callback(); } }, }; window.chrome.runtime = window.chrome.runtime || {}; Object.defineProperty(window.chrome.runtime, "lastError", { configurable: true, enumerable: true, get() { return runtimeState.lastError; }, }); window.chrome.runtime.sendMessage = window.chrome.runtime.sendMessage || function sendMessage(message, callback) { const done = (payload, error) => { runtimeState.lastError = error ? { message: error.message || String(error) } : null; if (typeof callback === "function") { callback(payload); } runtimeState.lastError = null; }; (async () => { try { if (!message || typeof message !== "object") { throw new Error("Invalid runtime message"); } if (message.type === "v2p_sync_test") { const client = new WebDAV( message.config.url, message.config.user, message.config.password, ); done({ ok: await client.test() }); return; } if (message.type === "v2p_sync_push") { const client = new WebDAV( message.config.url, message.config.user, message.config.password, ); await client.put(message.filename, message.data); done({ ok: true }); return; } if (message.type === "v2p_sync_pull") { const client = new WebDAV( message.config.url, message.config.user, message.config.password, ); const data = await client.get(message.filename); done({ ok: true, data }); return; } done({ ok: false, error: `Unsupported runtime message: ${message.type}`, }); } catch (error) { done({ ok: false, error: error.message || String(error) }, error); } })(); }; })(); (function () { /* WebDAV class removed from content script as it is now in background.js */ /** * Sync Manager */ const V2PSyncManager = { STORAGE_KEY: "v2p_webdav_config", SYNC_FILENAME: "v2p_sync_v1.json", async getConfig() { return new Promise((resolve) => { chrome.storage.local.get([this.STORAGE_KEY], (res) => resolve(res[this.STORAGE_KEY] || null), ); }); }, async saveConfig(config) { return new Promise((resolve) => { chrome.storage.local.set({ [this.STORAGE_KEY]: config }, resolve); }); }, async getClient() { const config = await this.getConfig(); if (config && config.url && config.user && config.password) { return new WebDAV(config.url, config.user, config.password); } return null; }, /** * Collect local data to sync */ getLocalData() { const navConfig = null; // Will be handled by chrome.storage logic const themeMode = localStorage.getItem("user_preferred_theme_mode"); const sidebarOrder = localStorage.getItem("v2p_sidebar_order"); const navCollapsed = localStorage.getItem("v2p_nav_collapsed"); return { themeMode, sidebarOrder, navCollapsed, updatedAt: Date.now(), }; }, async push() { const config = await this.getConfig(); if (!config) throw new Error("未配置 WebDAV"); const localData = this.getLocalData(); const navConfig = await new Promise((r) => chrome.storage.local.get(["v2p_nav_config"], (res) => r(res["v2p_nav_config"]), ), ); localData.navConfig = navConfig; return new Promise((resolve, reject) => { chrome.runtime.sendMessage( { type: "v2p_sync_push", config, filename: this.SYNC_FILENAME, data: localData, }, (res) => { if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message)); if (res && res.ok) resolve(true); else reject(new Error((res && res.error) || "推送失败")); }, ); }); }, async pull() { const config = await this.getConfig(); if (!config) throw new Error("未配置 WebDAV"); return new Promise((resolve, reject) => { chrome.runtime.sendMessage( { type: "v2p_sync_pull", config, filename: this.SYNC_FILENAME, }, (res) => { if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message)); if (res && res.ok) { const remoteData = res.data; if (remoteData) { if (remoteData.themeMode) localStorage.setItem( "user_preferred_theme_mode", remoteData.themeMode, ); if (remoteData.sidebarOrder) localStorage.setItem( "v2p_sidebar_order", remoteData.sidebarOrder, ); if (remoteData.navCollapsed) localStorage.setItem( "v2p_nav_collapsed", remoteData.navCollapsed, ); if (remoteData.navConfig) { chrome.storage.local.set({ v2p_nav_config: remoteData.navConfig, }); } resolve(true); } else { reject(new Error("云端文件为空")); } } else { reject(new Error((res && res.error) || "拉取失败")); } }, ); }); }, async testConnection(config) { return new Promise((resolve, reject) => { chrome.runtime.sendMessage({ type: "v2p_sync_test", config }, (res) => { if (chrome.runtime.lastError) return reject(new Error(chrome.runtime.lastError.message)); if (res && res.ok) resolve(true); else reject(new Error((res && res.error) || "连接失败")); }); }); }, }; window.V2PSyncManager = V2PSyncManager; const v2pShowToast = (message, duration = 3000) => { const existing = document.querySelector(".v2p-toast"); if (existing) existing.remove(); const toast = document.createElement("div"); toast.className = "v2p-toast"; toast.textContent = message; const append = () => { document.body.appendChild(toast); if (duration !== 0) { setTimeout(() => toast.remove(), duration); } }; if (document.body) { append(); } else { const wait = () => { if (document.body) return append(); requestAnimationFrame(wait); }; wait(); } }; window.v2pShowToast = v2pShowToast; // 前台自动签到 const CHECKIN_DATE_KEY = "v2p_checkin_date"; const CHECKIN_USER_KEY = "v2p_checkin_user"; let checking = false; const getUserName = () => { const link = document.querySelector('#Top .tools a[href^="/member/"]'); return link ? link.textContent.trim() : null; }; const alreadyCheckedToday = (username) => { if (!username) return false; const today = new Date().toISOString().split("T")[0]; return ( localStorage.getItem(CHECKIN_DATE_KEY) === today && localStorage.getItem(CHECKIN_USER_KEY) === username ); }; const markCheckedToday = (username) => { const today = new Date().toISOString().split("T")[0]; localStorage.setItem(CHECKIN_DATE_KEY, today); localStorage.setItem(CHECKIN_USER_KEY, username || ""); }; const extractRedeemUrl = (html) => { const doc = new DOMParser().parseFromString(html, "text/html"); const btn = doc.querySelector('input[value^="领取"]'); if (btn) { const onclick = btn.getAttribute("onclick") || ""; const match = onclick.match(/'(\/mission\/daily\/redeem\?once=\d+)'/); if (match && match[1]) return match[1]; } const match = html.match(/\/mission\/daily\/redeem\?once=\d+/); return match ? match[0] : null; }; const parseDays = (html) => { const match = html.match(/已连续登[^0-9]*?(\d+)\s*天/); return match ? match[1] : null; }; const parseCoins = (html) => { const match = html.match(/每日登录奖励\s*(\d+)\s*铜币/); return match ? match[1] : null; }; const runCheckin = async () => { if (checking) return; const username = getUserName(); if (!username) return; if (alreadyCheckedToday(username)) return; checking = true; try { const dailyHtml = await fetch("/mission/daily", { credentials: "include", }).then((r) => r.text()); if (!dailyHtml.includes("/signout")) { checking = false; return; } if ( dailyHtml.includes("每日登录奖励已领取") || dailyHtml.includes("已领取") ) { markCheckedToday(username); checking = false; return; } const redeemUrl = extractRedeemUrl(dailyHtml); if (!redeemUrl) { checking = false; return; } const redeemHtml = await fetch(redeemUrl, { credentials: "include", }).then((r) => r.text()); const days = parseDays(redeemHtml); markCheckedToday(username); let message = "签到成功 · 今日登录奖励已领取"; try { const balanceHtml = await fetch("/balance", { credentials: "include", }).then((r) => r.text()); const coins = parseCoins(balanceHtml); if (days && coins) message = `连续签到 ${days} 天,本次 ${coins} 铜币`; else if (coins) message = `签到成功 · 本次 ${coins} 铜币`; } catch (e) {} v2pShowToast(message); } catch (e) { console.log("V2P: Front-end checkin error", e); } finally { checking = false; } }; const scheduleCheckin = () => { let retries = 0; const tryRun = () => { if (getUserName()) { runCheckin(); return; } retries += 1; if (retries < 10) setTimeout(tryRun, 300); }; tryRun(); }; if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", scheduleCheckin); } else { scheduleCheckin(); } ("use strict"); // 想要的头像尺寸上限,自己改,比如 48 / 64 / 96 都行 const MAX_GRAVATAR_SIZE = 64; const clamp = (n, min, max) => Math.max(min, Math.min(max, n)); function getDesiredAvatarSize(img) { const dpr = window.devicePixelRatio || 1; const w = img.clientWidth || 0; const h = img.clientHeight || 0; const base = Math.max(w, h); if (base > 0) { return clamp(Math.ceil(base * dpr), 32, MAX_GRAVATAR_SIZE); } return MAX_GRAVATAR_SIZE; } function upgradeAvatar(img) { // 1. 基础检查:必须是图片元素且有 src if (!img || !img.src) return; // 2. 防重复处理检查 if (img.dataset.v2pProcessed) return; // 3. 快速字符串检查,避免不必要的 URL 对象创建 // V2EX 头像通常包含 /avatar/ 或 /gravatar/ const src = img.src; if (!src.includes("/avatar/") && !src.includes("/gravatar/")) { return; } // 标记为已处理,避免重复计算 img.dataset.v2pProcessed = "true"; let url; try { url = new URL(src, location.href); } catch (e) { return; } const pathname = url.pathname; const hostname = url.hostname; // 4. 处理逻辑 // Case A: /avatar/ 路径且带 _normal 的图片 -> 视显示尺寸决定是否升级 if (pathname.includes("/avatar/") && pathname.includes("_normal")) { const desired = getDesiredAvatarSize(img); if (desired >= 48) { // 使用字符串替换比正则稍快,且对于这种固定格式足够安全 img.src = src.replace("_normal", "_large"); } return; } // Case B: V2EX 的 gravatar 头像:通过 s=xx 控制尺寸 if (hostname.endsWith("v2ex.com") && pathname.includes("/gravatar/")) { const params = url.searchParams; const s = params.get("s"); if (s) { const curSize = parseInt(s, 10); const desired = getDesiredAvatarSize(img); if (!Number.isNaN(curSize) && curSize < desired) { params.set("s", String(desired)); img.src = url.toString(); } } } } function scanAll() { document.querySelectorAll("img.avatar").forEach(scheduleUpgrade); } const idle = window.requestIdleCallback || function (cb) { return setTimeout(cb, 16); }; const io = "IntersectionObserver" in window ? new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { io.unobserve(entry.target); idle(() => upgradeAvatar(entry.target)); } }); }) : null; function scheduleUpgrade(img) { if (io) { io.observe(img); } else { idle(() => upgradeAvatar(img)); } } // 首次扫描 scanAll(); // 监听后续动态加载的头像(比如 PJAX / AJAX) const observer = new MutationObserver((mutations) => { for (const m of mutations) { if (m.addedNodes.length === 0) continue; for (const node of m.addedNodes) { if (node.nodeType !== 1) continue; // 只处理元素节点 // 检查节点本身 if (node.tagName === "IMG" && node.classList.contains("avatar")) { scheduleUpgrade(node); } // 检查子节点 (如果插入的是一个容器) if (node.querySelectorAll) { // 使用更具体的选择器,减少遍历范围 const avatars = node.querySelectorAll("img.avatar"); if (avatars.length > 0) { avatars.forEach(scheduleUpgrade); } } } } }); observer.observe(document.documentElement, { childList: true, subtree: true, }); })(); ("use strict"); var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return (fn && (res = (0, fn[__getOwnPropNames(fn)[0]])((fn = 0))), res); }; // src/contents/polyfill.ts var init_polyfill = __esm({ "src/contents/polyfill.ts"() { "use strict"; { if (!window.requestIdleCallback) { window.requestIdleCallback = function (callback) { const start = Date.now(); return setTimeout(function () { callback({ didTimeout: false, timeRemaining: function () { return Math.max(0, 50 - (Date.now() - start)); }, }); }, 1); }; } if (!window.cancelIdleCallback) { window.cancelIdleCallback = function (id) { clearTimeout(id); }; } } }, }); // src/constants.ts var EXTENSION_NAME, emojiLinks, emoticons, READABLE_CONTENT_HEIGHT, MAX_CONTENT_HEIGHT, READING_CONTENT_LIMIT, dataExpiryTime, imgurClientIdPool, defaultOptions; var init_constants = __esm({ "src/constants.ts"() { "use strict"; EXTENSION_NAME = "V2EX_Polish"; emojiLinks = { // B 站表情。 ["[\u8131\u5355doge]" /* 脱单doge */]: { ld: "https://i.imgur.com/L62ZP7V.png", hd: "https://i.imgur.com/3mPhudo.png", }, ["[doge]" /* doge */]: { ld: "https://i.imgur.com/agAJ0Rd.png", hd: "https://i.imgur.com/HZL0hOa.png", }, ["[\u8FA3\u773C\u775B]" /* 辣眼睛 */]: { ld: "https://i.imgur.com/n119Wvk.png", hd: "https://i.imgur.com/A5WXoZJ.png", }, ["[\u7591\u60D1]" /* 疑惑 */]: { ld: "https://i.imgur.com/U3hKhrT.png", hd: "https://i.imgur.com/3gCygBS.png", }, ["[\u6342\u8138]" /* 捂脸 */]: { ld: "https://i.imgur.com/14cwgsI.png", hd: "https://i.imgur.com/fLp3t8s.png", }, ["[\u54E6\u547C]" /* 哦呼 */]: { ld: "https://i.imgur.com/km62MY2.png", hd: "https://i.imgur.com/CXXgF4E.png", }, ["[\u50B2\u5A07]" /* 傲娇 */]: { ld: "https://i.imgur.com/TkdeN49.png", hd: "https://i.imgur.com/m7IlCrD.png", }, ["[\u601D\u8003]" /* 思考 */]: { ld: "https://i.imgur.com/MAyk5GN.png", hd: "https://i.imgur.com/eRJTCx7.png", }, ["[\u5403\u74DC]" /* 吃瓜 */]: { ld: "https://i.imgur.com/Ug1iMq4.png", hd: "https://i.imgur.com/Gy3nwkC.png", }, ["[\u65E0\u8BED]" /* 无语 */]: { ld: "https://i.imgur.com/e1q9ScT.png", hd: "https://i.imgur.com/wMfcBqD.png", }, ["[\u5927\u54ED]" /* 大哭 */]: { ld: "https://i.imgur.com/YGIx7lh.png", hd: "https://i.imgur.com/SNHJxtv.png", }, ["[\u9178\u4E86]" /* 酸了 */]: { ld: "https://i.imgur.com/5FDsp6L.png", hd: "https://i.imgur.com/wnQBodT.png", }, ["[\u6253call]" /* 打call */]: { ld: "https://i.imgur.com/pmNOo2w.png", hd: "https://i.imgur.com/4GfTlV0.png", }, ["[\u6B6A\u5634]" /* 歪嘴 */]: { ld: "https://i.imgur.com/XzEYBoY.png", hd: "https://i.imgur.com/84ycU43.png", }, ["[\u661F\u661F\u773C]" /* 星星眼 */]: { ld: "https://i.imgur.com/2spsghH.png", hd: "https://i.imgur.com/oEIJRru.png", }, ["[OK]" /* OK */]: { ld: "https://i.imgur.com/6DMydmQ.png", hd: "https://i.imgur.com/PE2dyjY.png", }, ["[\u8DEA\u4E86]" /* 跪了 */]: { ld: "https://i.imgur.com/TYtySHv.png", hd: "https://i.imgur.com/0pjsMf0.png", }, ["[\u54CD\u6307]" /* 响指 */]: { ld: "https://i.imgur.com/Ac88cMm.png", hd: "https://i.imgur.com/nkoevMu.png", }, ["[\u8C03\u76AE]" /* 调皮 */]: { ld: "https://i.imgur.com/O6ZZSLk.png", hd: "https://i.imgur.com/ggHTLzH.png", }, ["[\u7B11\u54ED]" /* 笑哭 */]: { ld: "https://i.imgur.com/NIvxivj.png", hd: "https://i.imgur.com/h8edr5G.png", }, ["[\u55D1\u74DC\u5B50]" /* 嗑瓜子 */]: { ld: "https://i.imgur.com/rjR4rdr.png", hd: "https://i.imgur.com/GMzq0tq.png", }, ["[\u559C\u6781\u800C\u6CE3]" /* 喜极而泣 */]: { ld: "https://i.imgur.com/N9E3iZ2.png", hd: "https://i.imgur.com/L1N27tb.png", }, ["[\u60CA\u8BB6]" /* 惊讶 */]: { ld: "https://i.imgur.com/aptfuiN.png", hd: "https://i.imgur.com/cuzxGOI.png", }, ["[\u7ED9\u5FC3\u5FC3]" /* 给心心 */]: { ld: "https://i.imgur.com/4aXVwxJ.png", hd: "https://i.imgur.com/q663Mor.png", }, ["[\u5446]" /* 呆 */]: { ld: "https://i.imgur.com/c1Q76Cd.png", hd: "https://i.imgur.com/xMXlmxm.png", }, // 小红薯表情。 ["[\u54ED\u60F9R]" /* 哭惹 */]: { ld: "https://i.imgur.com/HgxsUD2.png", hd: "https://i.imgur.com/0aOdQJd.png", }, ["[\u54C7R]" /* 哇 */]: { ld: "https://i.imgur.com/OZySWIG.png", hd: "https://i.imgur.com/ngoi2I6.png", }, ["[\u6C57\u989CR]" /* 汗颜 */]: { ld: "https://i.imgur.com/jrVZoLi.png", hd: "https://i.imgur.com/O8alqc1.png", }, ["[\u5BB3\u7F9ER]" /* 害羞 */]: { ld: "https://i.imgur.com/OVQjxIr.png", hd: "https://i.imgur.com/1PeoVR5.png", }, ["[\u840C\u840C\u54D2R]" /* 萌萌哒 */]: { ld: "https://i.imgur.com/Ue1kikn.png", hd: "https://i.imgur.com/vOHzwus.png", }, ["[\u5077\u7B11R]" /* 偷笑 */]: { ld: "https://i.imgur.com/aF7QiE5.png", hd: "https://i.imgur.com/WneGpK9.png", }, ["[\u4E70\u7206R]" /* 买爆 */]: { ld: "https://i.imgur.com/2JhZFtb.png", hd: "https://i.imgur.com/za9t585.png", }, ["[\u8272\u8272R]" /* 色色 */]: { ld: "https://i.imgur.com/ZA1jRv1.png", hd: "https://i.imgur.com/mEGRKJy.png", }, ["[\u62A0\u9F3BR]" /* 抠鼻 */]: { ld: "https://i.imgur.com/pYtTFnj.png", hd: "https://i.imgur.com/ErnQrMJ.png", }, ["[\u9ED1\u85AF\u95EE\u53F7R]" /* 黑薯问号 */]: { ld: "https://i.imgur.com/aCjmFLD.png", hd: "https://i.imgur.com/i4Wgtyv.png", }, ["[\u6276\u5899R]" /* 扶墙 */]: { ld: "https://i.imgur.com/RV7y6tR.png", hd: "https://i.imgur.com/PjhjZsJ.png", }, ["[\u9119\u89C6R]" /* 鄙视 */]: { ld: "https://i.imgur.com/LaO5dh3.png", hd: "https://i.imgur.com/StrGaFx.png", }, ["[\u8E72R]" /* 蹲 */]: { ld: "https://i.imgur.com/t876WSv.png", hd: "https://i.imgur.com/jdTq0YI.png", }, ["[\u5E86\u795DR]" /* 庆祝 */]: { ld: "https://i.imgur.com/wQw2kD0.png", hd: "https://i.imgur.com/lx6jrkm.png", }, ["[\u516DR]" /* 六 */]: { ld: "https://i.imgur.com/JqoC4L5.png", hd: "https://i.imgur.com/cUVWKc2.png", }, ["[\u53EFR]" /* 可 */]: { ld: "https://i.imgur.com/I70yy88.png", hd: "https://i.imgur.com/nRgXwUT.png", }, ["[\u52A0\u4E00R]" /* 加一 */]: { ld: "https://i.imgur.com/hpVvbVh.png", hd: "https://i.imgur.com/abBCCK9.png", }, }; emoticons = [ { title: "\u6D41\u884C", list: [ "[\u8131\u5355doge]" /* 脱单doge */, "[doge]" /* doge */, "[\u6253call]" /* 打call */, "[\u661F\u661F\u773C]" /* 星星眼 */, "[\u5403\u74DC]" /* 吃瓜 */, "[OK]" /* OK */, "[\u54E6\u547C]" /* 哦呼 */, "[\u601D\u8003]" /* 思考 */, "[\u7591\u60D1]" /* 疑惑 */, "[\u8FA3\u773C\u775B]" /* 辣眼睛 */, "[\u50B2\u5A07]" /* 傲娇 */, "[\u6342\u8138]" /* 捂脸 */, "[\u65E0\u8BED]" /* 无语 */, "[\u5927\u54ED]" /* 大哭 */, "[\u9178\u4E86]" /* 酸了 */, "[\u6B6A\u5634]" /* 歪嘴 */, "[\u8C03\u76AE]" /* 调皮 */, "[\u7B11\u54ED]" /* 笑哭 */, "[\u55D1\u74DC\u5B50]" /* 嗑瓜子 */, "[\u559C\u6781\u800C\u6CE3]" /* 喜极而泣 */, "[\u60CA\u8BB6]" /* 惊讶 */, "[\u7ED9\u5FC3\u5FC3]" /* 给心心 */, "[\u5446]" /* 呆 */, "[\u8DEA\u4E86]" /* 跪了 */, "[\u54CD\u6307]" /* 响指 */, "[\u54C7R]" /* 哇 */, "[\u840C\u840C\u54D2R]" /* 萌萌哒 */, "[\u5BB3\u7F9ER]" /* 害羞 */, "[\u5077\u7B11R]" /* 偷笑 */, "[\u54ED\u60F9R]" /* 哭惹 */, "[\u6C57\u989CR]" /* 汗颜 */, "[\u8272\u8272R]" /* 色色 */, "[\u62A0\u9F3BR]" /* 抠鼻 */, "[\u9119\u89C6R]" /* 鄙视 */, "[\u4E70\u7206R]" /* 买爆 */, "[\u9ED1\u85AF\u95EE\u53F7R]" /* 黑薯问号 */, "[\u6276\u5899R]" /* 扶墙 */, "[\u8E72R]" /* 蹲 */, "[\u53EFR]" /* 可 */, "[\u516DR]" /* 六 */, "[\u52A0\u4E00R]" /* 加一 */, "[\u5E86\u795DR]" /* 庆祝 */, ], }, { title: "\u5C0F\u9EC4\u8138", list: [ "\u{1F600}", "\u{1F601}", "\u{1F602}", "\u{1F923}", "\u{1F605}", "\u{1F60A}", "\u{1F60B}", "\u{1F618}", "\u{1F970}", "\u{1F617}", "\u{1F929}", "\u{1F914}", "\u{1F928}", "\u{1F610}", "\u{1F611}", "\u{1F644}", "\u{1F60F}", "\u{1F62A}", "\u{1F62B}", "\u{1F971}", "\u{1F61C}", "\u{1F612}", "\u{1F614}", "\u{1F628}", "\u{1F630}", "\u{1F631}", "\u{1F975}", "\u{1F621}", "\u{1F973}", "\u{1F97A}", "\u{1F92D}", "\u{1F9D0}", "\u{1F60E}", "\u{1F913}", "\u{1F62D}", "\u{1F911}", "\u{1F92E}", ], }, { title: "\u624B\u52BF", list: [ "\u{1F64B}", "\u{1F64E}", "\u{1F645}", "\u{1F647}", "\u{1F937}", "\u{1F90F}", "\u{1F449}", "\u270C\uFE0F", "\u{1F918}", "\u{1F919}", "\u{1F44C}", "\u{1F90C}", "\u{1F44D}", "\u{1F44E}", "\u{1F44B}", "\u{1F91D}", "\u{1F64F}", "\u{1F44F}", ], }, { title: "\u5E86\u795D", list: ["\u2728", "\u{1F389}", "\u{1F38A}"], }, { title: "\u5176\u4ED6", list: [ "\u{1F47B}", "\u{1F921}", "\u{1F414}", "\u{1F440}", "\u{1F4A9}", "\u{1F434}", "\u{1F984}", "\u{1F427}", "\u{1F436}", "\u{1F412}", "\u{1F648}", "\u{1F649}", "\u{1F64A}", "\u{1F435}", ], }, ]; READABLE_CONTENT_HEIGHT = 250; MAX_CONTENT_HEIGHT = 550; READING_CONTENT_LIMIT = 150; dataExpiryTime = 60 * 60 * 1e3; imgurClientIdPool = [ "3107b9ef8b316f3", // 以下 Client ID 来自「V2EX Plus」 "442b04f26eefc8a", "59cfebe717c09e4", "60605aad4a62882", "6c65ab1d3f5452a", "83e123737849aa9", "9311f6be1c10160", "c4a4a563f698595", "81be04b9e4a08ce", ]; defaultOptions = { openInNewTab: false, autoCheckIn: { enabled: true, }, theme: { autoSwitch: false, }, reply: { preload: "off", layout: "vertical", }, replyContent: { autoFold: true, hideReplyTime: true, hideRefName: true, showImgInPage: true, }, nestedReply: { display: "indent", multipleInsideOne: "nested", }, userTag: { display: "inline", }, contextMenu: { enabled: true, }, }; }, }); // src/icons.ts var iconLogo, iconGitHub; var init_icons = __esm({ "src/icons.ts"() { "use strict"; iconLogo = ` `; iconGitHub = ` `; }, }); // src/utils.ts function getOS() { const userAgent = window.navigator.userAgent.toLowerCase(); const macosPlatforms = /(macintosh|macintel|macppc|mac68k|macos)/i; const windowsPlatforms = /(win32|win64|windows|wince)/i; const iosPlatforms = /(iphone|ipad|ipod)/i; let os = null; if (macosPlatforms.test(userAgent)) { os = "macos"; } else if (iosPlatforms.test(userAgent)) { os = "ios"; } else if (windowsPlatforms.test(userAgent)) { os = "windows"; } else if (userAgent.includes("android")) { os = "android"; } else if (userAgent.includes("linux")) { os = "linux"; } return os; } function formatTimestamp(timestamp, { format = "YMD" } = {}) { const date = new Date( timestamp.toString().length === 10 ? timestamp * 1e3 : timestamp, ); const year = date.getFullYear().toString(); const month = (date.getMonth() + 1).toString().padStart(2, "0"); const day = date.getDate().toString().padStart(2, "0"); const YMD = `${year}-${month}-${day}`; if (format === "YMDHM") { const hour = date.getHours().toString().padStart(2, "0"); const minute = date.getMinutes().toString().padStart(2, "0"); return `${YMD} ${hour}:${minute}`; } if (format === "YMDHMS") { const hour = date.getHours().toString().padStart(2, "0"); const minute = date.getMinutes().toString().padStart(2, "0"); const second = date.getSeconds().toString().padStart(2, "0"); return `${YMD} ${hour}:${minute}:${second}`; } return YMD; } function isSameDay(timestamp1, timestamp2) { const date1 = new Date(timestamp1); const date2 = new Date(timestamp2); return ( date1.getFullYear() === date2.getFullYear() && date1.getMonth() === date2.getMonth() && date1.getDate() === date2.getDate() ); } function isObject(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } function deepMerge(target, source) { const result = {}; for (const key in target) { const targetProp = target[key]; const sourceProp = source[key]; if (isObject(targetProp) && isObject(sourceProp)) { result[key] = deepMerge(targetProp, sourceProp); } else if (Reflect.has(source, key)) { result[key] = sourceProp; } else { result[key] = targetProp; } } for (const key in source) { if (!Reflect.has(target, key)) { result[key] = source[key]; } } return result; } function getRunEnv() { if (typeof chrome === "object" && typeof chrome.extension !== "undefined") { return "chrome"; } if (typeof browser === "object" && typeof browser.extension !== "undefined") { return "web-ext"; } return null; } function isBrowserExtension() { return false; } function escapeHTML(htmlString) { return htmlString.replace(/[<>&"'']/g, (match) => { switch (match) { case "<": return "<"; case ">": return ">"; case "&": return "&"; case '"': return """; case "'": return "'"; default: return match; } }); } function injectScript(scriptSrc) { const script = document.createElement("script"); script.setAttribute("type", "text/javascript"); script.setAttribute("src", scriptSrc); document.body.appendChild(script); } function isValidSettings(settings) { return ( !!settings && typeof settings === "object" && "options" /* Options */ in settings ); } function sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } async function getV2P_Settings() { let noteId; { const res = await fetch(`${V2EX_ORIGIN}/notes`); const htmlText = await res.text(); const $page = $(htmlText); const $note = $page.find( '.note_item > .note_item_title > a[href^="/notes"]', ); $note.each((_, dom) => { const $dom = $(dom); if ($dom.text().startsWith(mark)) { const href = $dom.attr("href"); if (typeof href === "string") { const id = href.split("/").at(2); noteId = id; } return false; } }); } if (noteId) { const res = await fetch(`${V2EX_ORIGIN}/notes/edit/${noteId}`); const htmlText = await res.text(); const $editor = $(htmlText).find("#note_content.note_editor"); const value = $editor.val(); if (typeof value === "string") { const syncSettings = JSON.parse(value.replace(mark, "")); if (isValidSettings(syncSettings)) { return { noteId, config: syncSettings }; } } } } async function setV2P_Settings(storageSettings, signal) { const data = await getV2P_Settings(); const updating = !!data; const formData = new FormData(); const syncVersion = updating ? data.config["settings-sync" /* SyncInfo */].version + 1 : 1; const syncInfo = { version: syncVersion, lastSyncTime: Date.now(), }; formData.append( "content", mark + JSON.stringify({ ...storageSettings, ["settings-sync" /* SyncInfo */]: syncInfo, }), ); formData.append("syntax", "0"); if (updating) { const { noteId } = data; await fetch(`${V2EX_ORIGIN}/notes/edit/${noteId}`, { method: "POST", body: formData, signal, }); } else { formData.append("parent_id", "0"); await fetch(`${V2EX_ORIGIN}/notes/new`, { method: "POST", body: formData, signal, }); } await setStorage("settings-sync" /* SyncInfo */, syncInfo); return syncInfo; } function getStorage(useCache = true) { return new Promise((resolve, reject) => { if (useCache) { if (window.__V2P_StorageCache) { resolve(window.__V2P_StorageCache); } } if (!isBrowserExtension()) { const data = { ["options" /* Options */]: defaultOptions }; if (typeof window !== "undefined") { window.__V2P_StorageCache = data; } resolve(data); } }); } function getStorageSync() { const storage = window.__V2P_StorageCache; if (!storage) { throw new Error( `${EXTENSION_NAME}: \u65E0\u53EF\u7528\u7684 Storage \u7F13\u5B58\u6570\u636E`, ); } return storage; } async function setStorage(storageKey, storageItem) { switch (storageKey) { case "options" /* Options */: case "api" /* API */: case "daily" /* Daily */: case "settings-sync" /* SyncInfo */: case "reading-list" /* ReadingList */: try { // await chrome.storage.sync.set({ [storageKey]: storageItem }); if ( storageKey !== "api" /* API */ && storageKey !== "settings-sync" /* SyncInfo */ && typeof $ !== "undefined" ) { const settings = await getStorage(false); if (controller) { controller.abort(); } controller = new AbortController(); setV2P_Settings(settings, controller.signal); } } catch (err) { if (String(err).includes("QUOTA_BYTES_PER_ITEM quota exceeded")) { console.error( `${EXTENSION_NAME}: \u65E0\u6CD5\u8BBE\u7F6E ${storageKey}\uFF0C \u5355\u4E2A item \u4E0D\u80FD\u8D85\u51FA 8 KB\uFF0C\u8BE6\u60C5\u67E5\u770B\uFF1Ahttps://developer.chrome.com/docs/extensions/reference/storage/#storage-areas`, ); } console.error(err); throw new Error(`\u274C \u65E0\u6CD5\u8BBE\u7F6E\uFF1A${storageKey}`); } break; default: throw new Error(`\u672A\u77E5\u7684 storageKey\uFF1A ${storageKey}`); } } var V2EX_ORIGIN, mark, controller; var init_utils = __esm({ "src/utils.ts"() { "use strict"; init_constants(); V2EX_ORIGIN = typeof window !== "undefined" && window.location.origin.includes("v2ex.com") ? window.location.origin : "https://www.v2ex.com" /* Origin */; mark = `${EXTENSION_NAME}_settings`; controller = null; }, }); // src/contents/globals.ts function updateCommentCells() { $commentCells = $commentBox.find('.cell[id^="r_"]'); $commentTableRows = $commentCells.find("> table > tbody > tr"); } var $body, $wrapper, $wrapperContent, $main, $topicList, $infoCard, $topicContentBox, $topicHeader, $commentBox, $commentCells, $commentTableRows, $replyBox, $replyForm, $replyTextArea, replyTextArea, loginName, topicOwnerName, pathTopicId; var init_globals = __esm({ "src/contents/globals.ts"() { "use strict"; $body = $(document.body); $wrapper = $("#Wrapper"); $wrapperContent = $wrapper.find("> .content"); $main = $("#Main"); $topicList = $( "#Main #Tabs ~ .cell.item, #Main #TopicsNode > .cell, #Main .cell.item:has(.item_title > .topic-link)", ); $infoCard = $('#Rightbar > .box:has("#member-activity")'); // 主题内容区域 $topicContentBox = $("#Main .box:has(.topic_buttons)"); if ($topicContentBox.length === 0) { // 移动端 / 特殊布局兜底:不依赖 #Main $topicContentBox = $(".box:has(.topic_buttons)"); } $topicHeader = $topicContentBox.find(".header"); // 评论列表区域 $commentBox = $('#Main .box:has(.cell[id^="r_"])'); if ($commentBox.length === 0) { // 移动端兜底:不依赖 #Main $commentBox = $('.box:has(.cell[id^="r_"])'); } $commentCells = $commentBox.find('.cell[id^="r_"]'); $commentTableRows = $commentCells.find("> table > tbody > tr"); $replyBox = $("#reply-box"); $replyForm = $replyBox.find('form[action^="/t"]'); $replyTextArea = $("#reply_content"); replyTextArea = document.querySelector("#reply_content"); loginName = $('#Top .tools > a[href^="/member"]').text(); topicOwnerName = $topicHeader.find('> small > a[href^="/member"]').text(); pathTopicId = window.location.pathname.match(/\/t\/(\d+)/)?.at(1); }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/createElement.js var createElement, createElement$1; var init_createElement = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/createElement.js"() { "use strict"; createElement = (tag, attrs, children = []) => { const element = document.createElementNS( "http://www.w3.org/2000/svg", tag, ); Object.keys(attrs).forEach((name) => { element.setAttribute(name, String(attrs[name])); }); if (children.length) { children.forEach((child) => { const childElement = createElement(...child); element.appendChild(childElement); }); } return element; }; createElement$1 = ([tag, attrs, children]) => createElement(tag, attrs, children); }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/replaceElement.js var getAttrs, getClassNames, combineClassNames, toPascalCase, replaceElement; var init_replaceElement = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/replaceElement.js"() { "use strict"; init_createElement(); getAttrs = (element) => Array.from(element.attributes).reduce((attrs, attr) => { attrs[attr.name] = attr.value; return attrs; }, {}); getClassNames = (attrs) => { if (typeof attrs === "string") return attrs; if (!attrs || !attrs.class) return ""; if (attrs.class && typeof attrs.class === "string") { return attrs.class.split(" "); } if (attrs.class && Array.isArray(attrs.class)) { return attrs.class; } return ""; }; combineClassNames = (arrayOfClassnames) => { const classNameArray = arrayOfClassnames.flatMap(getClassNames); return classNameArray .map((classItem) => classItem.trim()) .filter(Boolean) .filter((value, index, self) => self.indexOf(value) === index) .join(" "); }; toPascalCase = (string) => string.replace( /(\w)(\w*)(_|-|\s*)/g, (g0, g1, g2) => g1.toUpperCase() + g2.toLowerCase(), ); replaceElement = (element, { nameAttr, icons, attrs }) => { const iconName = element.getAttribute(nameAttr); if (iconName == null) return; const ComponentName = toPascalCase(iconName); const iconNode = icons[ComponentName]; if (!iconNode) { return; } const elementAttrs = getAttrs(element); const [tag, iconAttributes, children] = iconNode; const iconAttrs = { ...iconAttributes, "data-lucide": iconName, ...attrs, ...elementAttrs, }; const classNames = combineClassNames([ "lucide", `lucide-${iconName}`, elementAttrs, attrs, ]); if (classNames) { Object.assign(iconAttrs, { class: classNames, }); } const svgElement = createElement$1([tag, iconAttrs, children]); return element.parentNode?.replaceChild(svgElement, element); }; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/defaultAttributes.js var defaultAttributes; var init_defaultAttributes = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/defaultAttributes.js"() { "use strict"; defaultAttributes = { xmlns: "http://www.w3.org/2000/svg", width: 24, height: 24, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round", "stroke-linejoin": "round", }; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/book-open-check.js var BookOpenCheck; var init_book_open_check = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/book-open-check.js"() { "use strict"; init_defaultAttributes(); BookOpenCheck = [ "svg", defaultAttributes, [ ["path", { d: "M12 21V7" }], ["path", { d: "m16 12 2 2 4-4" }], [ "path", { d: "M22 6V4a1 1 0 0 0-1-1h-5a4 4 0 0 0-4 4 4 4 0 0 0-4-4H3a1 1 0 0 0-1 1v13a1 1 0 0 0 1 1h6a3 3 0 0 1 3 3 3 3 0 0 1 3-3h6a1 1 0 0 0 1-1v-1.3", }, ], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevron-down.js var ChevronDown; var init_chevron_down = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevron-down.js"() { "use strict"; init_defaultAttributes(); ChevronDown = ["svg", defaultAttributes, [["path", { d: "m6 9 6 6 6-6" }]]]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevrons-up.js var ChevronsUp; var init_chevrons_up = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/chevrons-up.js"() { "use strict"; init_defaultAttributes(); ChevronsUp = [ "svg", defaultAttributes, [ ["path", { d: "m17 11-5-5-5 5" }], ["path", { d: "m17 18-5-5-5 5" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/eye-off.js var EyeOff; var init_eye_off = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/eye-off.js"() { "use strict"; init_defaultAttributes(); EyeOff = [ "svg", defaultAttributes, [ [ "path", { d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49", }, ], ["path", { d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }], [ "path", { d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143", }, ], ["path", { d: "m2 2 20 20" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/heart.js var Heart; var init_heart = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/heart.js"() { "use strict"; init_defaultAttributes(); Heart = [ "svg", defaultAttributes, [ [ "path", { d: "M19 14c1.49-1.46 3-3.21 3-5.5A5.5 5.5 0 0 0 16.5 3c-1.76 0-3 .5-4.5 2-1.5-1.5-2.74-2-4.5-2A5.5 5.5 0 0 0 2 8.5c0 2.3 1.5 4.05 3 5.5l7 7Z", }, ], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/house.js var House; var init_house = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/house.js"() { "use strict"; init_defaultAttributes(); House = [ "svg", defaultAttributes, [ ["path", { d: "M15 21v-8a1 1 0 0 0-1-1h-4a1 1 0 0 0-1 1v8" }], [ "path", { d: "M3 10a2 2 0 0 1 .709-1.528l7-5.999a2 2 0 0 1 2.582 0l7 5.999A2 2 0 0 1 21 10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z", }, ], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square-plus.js var MessageSquarePlus; var init_message_square_plus = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square-plus.js"() { "use strict"; init_defaultAttributes(); MessageSquarePlus = [ "svg", defaultAttributes, [ [ "path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", }, ], ["path", { d: "M12 7v6" }], ["path", { d: "M9 10h6" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square.js var MessageSquare; var init_message_square = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/message-square.js"() { "use strict"; init_defaultAttributes(); MessageSquare = [ "svg", defaultAttributes, [ [ "path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z", }, ], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/moon.js var Moon; var init_moon = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/moon.js"() { "use strict"; init_defaultAttributes(); Moon = [ "svg", defaultAttributes, [["path", { d: "M12 3a6 6 0 0 0 9 9 9 9 0 1 1-9-9Z" }]], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/package-plus.js var PackagePlus; var init_package_plus = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/package-plus.js"() { "use strict"; init_defaultAttributes(); PackagePlus = [ "svg", defaultAttributes, [ ["path", { d: "M16 16h6" }], ["path", { d: "M19 13v6" }], [ "path", { d: "M21 10V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l2-1.14", }, ], ["path", { d: "m7.5 4.27 9 5.15" }], ["polyline", { points: "3.29 7 12 12 20.71 7" }], ["line", { x1: "12", x2: "12", y1: "22", y2: "12" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-right.js var PanelRight; var init_panel_right = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-right.js"() { "use strict"; init_defaultAttributes(); PanelRight = [ "svg", defaultAttributes, [ ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], ["path", { d: "M15 3v18" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-top.js var PanelTop; var init_panel_top = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/panel-top.js"() { "use strict"; init_defaultAttributes(); PanelTop = [ "svg", defaultAttributes, [ ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], ["path", { d: "M3 9h18" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/smile.js var Smile; var init_smile = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/smile.js"() { "use strict"; init_defaultAttributes(); Smile = [ "svg", defaultAttributes, [ ["circle", { cx: "12", cy: "12", r: "10" }], ["path", { d: "M8 14s1.5 2 4 2 4-2 4-2" }], ["line", { x1: "9", x2: "9.01", y1: "9", y2: "9" }], ["line", { x1: "15", x2: "15.01", y1: "9", y2: "9" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/square-arrow-up-right.js var SquareArrowUpRight; var init_square_arrow_up_right = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/square-arrow-up-right.js"() { "use strict"; init_defaultAttributes(); SquareArrowUpRight = [ "svg", defaultAttributes, [ ["rect", { width: "18", height: "18", x: "3", y: "3", rx: "2" }], ["path", { d: "M8 8h8v8" }], ["path", { d: "m8 16 8-8" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/star.js var Star; var init_star = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/star.js"() { "use strict"; init_defaultAttributes(); Star = [ "svg", defaultAttributes, [ [ "polygon", { points: "12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2", }, ], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/sun.js var Sun; var init_sun = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/sun.js"() { "use strict"; init_defaultAttributes(); Sun = [ "svg", defaultAttributes, [ ["circle", { cx: "12", cy: "12", r: "4" }], ["path", { d: "M12 2v2" }], ["path", { d: "M12 20v2" }], ["path", { d: "m4.93 4.93 1.41 1.41" }], ["path", { d: "m17.66 17.66 1.41 1.41" }], ["path", { d: "M2 12h2" }], ["path", { d: "M20 12h2" }], ["path", { d: "m6.34 17.66-1.41 1.41" }], ["path", { d: "m19.07 4.93-1.41 1.41" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/twitter.js var Twitter; var init_twitter = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/twitter.js"() { "use strict"; init_defaultAttributes(); Twitter = [ "svg", defaultAttributes, [ [ "path", { d: "M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z", }, ], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/image.js var Image; var init_image = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/image.js"() { "use strict"; init_defaultAttributes(); Image = [ "svg", defaultAttributes, [ [ "rect", { width: "18", height: "18", x: "3", y: "3", rx: "2", ry: "2" }, ], ["circle", { cx: "8.5", cy: "8.5", r: "1.5" }], ["polyline", { points: "21 15 16 10 5 21" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/file-text.js var FileText; var init_file_text = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/file-text.js"() { "use strict"; init_defaultAttributes(); FileText = [ "svg", defaultAttributes, [ [ "path", { d: "M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z", }, ], ["polyline", { points: "14 2 14 8 20 8" }], ["line", { x1: "16", x2: "8", y1: "13", y2: "13" }], ["line", { x1: "16", x2: "8", y1: "17", y2: "17" }], ["line", { x1: "10", x2: "8", y1: "9", y2: "9" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/globe.js var Globe; var init_globe = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/globe.js"() { "use strict"; init_defaultAttributes(); Globe = [ "svg", defaultAttributes, [ ["circle", { cx: "12", cy: "12", r: "10" }], ["line", { x1: "2", x2: "22", y1: "12", y2: "12" }], [ "path", { d: "M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z", }, ], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/clock.js var Clock; var init_clock = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/icons/clock.js"() { "use strict"; init_defaultAttributes(); Clock = [ "svg", defaultAttributes, [ ["circle", { cx: "12", cy: "12", r: "10" }], ["polyline", { points: "12 6 12 12 16 14" }], ], ]; }, }); // node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/lucide.js var createIcons; var init_lucide = __esm({ "node_modules/.pnpm/lucide@0.445.0/node_modules/lucide/dist/esm/lucide.js"() { "use strict"; init_replaceElement(); init_createElement(); init_book_open_check(); init_chevron_down(); init_chevrons_up(); init_eye_off(); init_heart(); init_house(); init_message_square_plus(); init_message_square(); init_moon(); init_package_plus(); init_panel_right(); init_panel_top(); init_smile(); init_square_arrow_up_right(); init_star(); init_sun(); init_twitter(); init_image(); init_file_text(); init_globe(); init_clock(); createIcons = ({ icons = {}, nameAttr = "data-lucide", attrs = {}, } = {}) => { if (!Object.values(icons).length) { throw new Error( "Please provide an icons object.\nIf you want to use all the icons you can import it like:\n `import { createIcons, icons } from 'lucide';\nlucide.createIcons({icons});`", ); } if (typeof document === "undefined") { throw new Error("`createIcons()` only works in a browser environment."); } const elementsToReplace = document.querySelectorAll(`[${nameAttr}]`); Array.from(elementsToReplace).forEach((element) => replaceElement(element, { nameAttr, icons, attrs }), ); if (nameAttr === "data-lucide") { const deprecatedElements = document.querySelectorAll("[icon-name]"); if (deprecatedElements.length > 0) { Array.from(deprecatedElements).forEach((element) => replaceElement(element, { nameAttr: "icon-name", icons, attrs }), ); } } }; }, }); // src/components/toast.ts function createToast(props) { const { message, duration = 3e3 } = props; const $existTosat = $(".v2p-toast"); if ($existTosat.length > 0) { $existTosat.remove(); } const $toast = $(`
${message}
`).hide(); $body.append($toast); $toast.fadeIn("fast"); if (duration !== 0) { setTimeout(() => { $toast.fadeOut("fast", () => { $toast.remove(); }); }, duration); } return { clear() { $toast.remove(); }, }; } var init_toast = __esm({ "src/components/toast.ts"() { "use strict"; init_globals(); }, }); // src/contents/helpers.ts function focusReplyInput() { if (replyTextArea instanceof HTMLTextAreaElement) { replyTextArea.focus(); } } function insertTextToReplyInput(text) { if (replyTextArea instanceof HTMLTextAreaElement) { const startPos = replyTextArea.selectionStart; const endPos = replyTextArea.selectionEnd; const valueToStart = replyTextArea.value.substring(0, startPos); const valueFromEnd = replyTextArea.value.substring( endPos, replyTextArea.value.length, ); replyTextArea.value = `${valueToStart}${text}${valueFromEnd}`; focusReplyInput(); replyTextArea.selectionStart = replyTextArea.selectionEnd = startPos + text.length; } } async function addToReadingList(params) {} function customEscape(str) { return str.replace( /[^a-zA-Z0-9_.!~*'()-]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`, ); } function decodeBase64TopicPage() { const dataTitle = "\u70B9\u51FB\u590D\u5236"; if (window.__V2P_DecodeStatus === "decodeed") { createToast({ message: "\u5DF2\u89E3\u6790\u5B8C\u672C\u9875\u6240\u6709\u7684 Base64 \u5B57\u7B26\u4E32", }); } else { const $topicContentBox2 = $("#Main .box:has(.topic_content)"); const $commentBox2 = $('#Main .box:has(.cell[id^="r_"])'); const $commentCells2 = $commentBox2.find('.cell[id^="r_"]'); let count = 0; const excludeList = [ "boss", "bilibili", "Bilibili", "Encrypto", "encrypto", "Window10", "airpords", "Windows7", ]; const convertHTMLText = (text, excludeTextList) => { if (text.length % 4 !== 0 || text.length <= 8) { return text; } if (excludeList.includes(text)) { return text; } if (text.includes("=")) { const paddingIndex = text.indexOf("="); if ( paddingIndex !== text.length - 1 && paddingIndex !== text.length - 2 ) { return text; } } if (excludeTextList?.some((excludeText) => excludeText.includes(text))) { return text; } try { const decodedStr = decodeURIComponent(customEscape(window.atob(text))); count += 1; return `${text}(${decodedStr})`; } catch (err) { if (err instanceof Error) { console.error(`\u89E3\u6790 Base64 \u51FA\u9519\uFF1A${err.message}`); } return text; } }; const base64regex = /[A-z0-9+/=]+/g; const contentHandler = (_, content) => { const excludeTextList = [ ...content.getElementsByTagName("a"), ...content.getElementsByTagName("img"), ].map((ele) => ele.outerHTML); content.innerHTML = content.innerHTML.replace(base64regex, (htmlText) => convertHTMLText(htmlText, excludeTextList), ); }; $commentCells2.find(".reply_content").each(contentHandler); $topicContentBox2.find(".topic_content").each(contentHandler); if (count === 0) { createToast({ message: "\u672C\u9875\u672A\u53D1\u73B0 Base64 \u5B57\u7B26\u4E32", }); } else { window.__V2P_DecodeStatus = "decodeed"; createToast({ message: `\u2705 \u5DF2\u89E3\u6790\u672C\u9875\u6240\u6709\u7684 Base64 \u5B57\u7B26\u4E32\uFF0C\u5171 ${count} \u6761`, }); } $(".v2p-decode").on("click", (ev) => { const text = ev.target.innerText; void navigator.clipboard.writeText(text).then(() => { ev.target.dataset.title = "\u2705 \u5DF2\u590D\u5236"; setTimeout(() => { ev.target.dataset.title = dataTitle; }, 1e3); }); }); } } function postTask(expression, callback) { if (!isBrowserExtension()) { const result = Function(`"use strict"; ${expression}`)(); callback?.(result); } else { if (callback) { if (window.__V2P_Tasks) { window.__V2P_Tasks.set(Date.now(), callback); } else { window.__V2P_Tasks = /* @__PURE__ */ new Map([[Date.now(), callback]]); } } const messageData = { from: 0 /* Content */, payload: { task: { id: Date.now(), expression } }, }; window.postMessage(messageData); } } function loadIcons() { setTimeout(() => { createIcons({ attrs: { width: "100%", height: "100%", }, icons: { MessageSquarePlus, MessageSquare, BookOpenCheck, ChevronsUp, Heart, EyeOff, Sun, Moon, Smile, PackagePlus, Star, Twitter, ChevronDown, ArrowUpRightSquare: SquareArrowUpRight, House, Image, FileText, Globe, Clock, }, }); }, 0); } function transformEmoji(textValue) { return textValue.replace(/\[[^\]]+\]/g, (x) => { if (Object.hasOwn(emojiLinks, x)) { const emojiLink = emojiLinks[x].ld; if (typeof emojiLink === "string") { return `${emojiLink} `; } } return x; }); } function getTagsText(tags) { return tags.map((it) => it.name).join("\uFF0C"); } function replaceEmojiWithHD($emojiImgs) { if ($emojiImgs.length > 0) { const srcMap = /* @__PURE__ */ new Map(); Object.values(emojiLinks).forEach(({ ld, hd }) => { srcMap.set(ld, hd); }); $emojiImgs.each((_, img) => { const $img = $(img); const src = $img.attr("src"); if (typeof src === "string") { const hd = srcMap.get(src); if (typeof hd === "string") { $img.attr("src", hd); $img.css({ width: "var(--v2p-emoji-size)", height: "var(--v2p-emoji-size)", }); } } }); } } function replaceCommentEmojiWithHD($cells = $commentCells) { const srcMap = /* @__PURE__ */ new Map(); Object.values(emojiLinks).forEach(({ ld, hd }) => { srcMap.set(ld, hd); }); const $embedImages = $cells.find( `.reply_content .embedded_image, .payload .embedded_image`, ); if ($embedImages.length > 0) { replaceEmojiWithHD($embedImages); } } function getRegisterDays(created) { const registerDays = Math.ceil((Date.now() / 1e3 - created) / (60 * 60 * 24)); return registerDays; } var init_helpers = __esm({ "src/contents/helpers.ts"() { "use strict"; init_lucide(); init_toast(); init_constants(); init_utils(); init_globals(); }, }); // src/contents/common.ts var common_exports = {}; var init_common = __esm({ "src/contents/common.ts"() { "use strict"; init_polyfill(); init_constants(); init_icons(); init_utils(); init_globals(); init_helpers(); if ($("#site-header").length > 0) { $body.addClass("v2p-mobile"); } void (async () => { const isBrowserExt = isBrowserExtension(); const storage = await getStorage(); const options = storage["options" /* Options */]; // ==================== 开始添加自动签到逻辑 ==================== if (options.autoCheckIn.enabled) { const dailyUrl = "/mission/daily"; const giftLinkSelector = 'a[href="/mission/daily"]'; const giftLink = document.querySelector(giftLinkSelector); // 如果在当前页面找到了“领取今日的登录奖励”链接 if ( giftLink && (giftLink.innerText.includes("领取今日的登录奖励") || giftLink.closest(".box")?.querySelector(".fa-gift")) ) { // 使用 AJAX 静默签到 (async () => { try { // 1. 获取签到页面内容以提取 once token const resDict = await fetch(dailyUrl); const htmlText = await resDict.text(); // 2. 解析签到按钮的 URL // 支持单引号和双引号,以及不严格的空格 const match = htmlText.match( /location\.href\s*=\s*['"]([^'"]+)['"]/, ); if (match && match[1]) { const redeemUrl = match[1]; // CRITICAL: 必须校验 URL 是否包含了 redeem 关键字,否则可能误抓取到“登出”链接 (/signout) if (redeemUrl.includes("/mission/daily/redeem")) { // 3. 发送签到请求 const redeemRes = await fetch(redeemUrl); if (redeemRes.ok) { // 4. 签到成功,提示用户并移除链接 createToast({ message: "✅ V2EX Polish+: 已自动领取今日登录奖励", }); // 移除顶部的签到提示 const tipBox = giftLink.closest(".box"); if (tipBox) { tipBox.remove(); } // 如果是在签到页面,尝试刷新一下状态或按钮 if (window.location.pathname === dailyUrl) { const redeemBtn = document.querySelector('input[value^="领取"]'); if (redeemBtn) { redeemBtn.value = "已领取"; redeemBtn.disabled = true; } } } } // end if (redeemUrl.includes) } // end if (match) } catch (err) { console.error("自动签到失败:", err); } })(); } } // ==================== 自动签到逻辑结束 ==================== // ==================== 自动签到逻辑结束 ==================== if (options.theme.mode === "compact") { $body.addClass("v2p-mode-compact"); } const $toggle = $("#Rightbar .light-toggle").addClass( "v2p-color-mode-toggle", ); const syncInfo = storage["settings-sync" /* SyncInfo */]; if (syncInfo) { const lastCheckTime = syncInfo.lastCheckTime; const twoHours = 2 * 60 * 1e3 * 60; const neverChecked = !lastCheckTime; if ( (lastCheckTime && Date.now() - lastCheckTime >= twoHours) || neverChecked ) { const isSignInPage = window.location.href.includes("/signin"); if (!isSignInPage) { void getV2P_Settings().then(async (res) => { const settings = res?.config; const remoteSyncInfo = settings?.["settings-sync" /* SyncInfo */]; if (settings && remoteSyncInfo) { if (syncInfo.version < remoteSyncInfo.version || neverChecked) { } } }); } } } { const $toggleImg = $toggle.find("> img"); const alt = $toggleImg.prop("alt"); if (alt === "Light") { $toggle.prop("title", "\u4F7F\u7528\u6DF1\u8272\u4E3B\u9898"); $toggleImg.replaceWith(''); } else if (alt === "Dark") { $toggle.prop("title", "\u4F7F\u7528\u6D45\u8272\u4E3B\u9898"); $toggleImg.replaceWith(''); } } { $("#Top .site-nav .tools > .top").addClass("v2p-hover-btn"); } if (options.hideAccount) { const faviconLink = $("link[rel~='icon']"); faviconLink.prop("href", "https://v2p.app/favicon.svg"); $("#Logo").append(``).addClass("v2p-logo"); $("#Top").find('a[href^="/member/"]').remove(); $infoCard .find( 'a[href^="/member/"], table:nth-of-type(1) td:nth-of-type(3) .fade', ) .addClass("v2p-hide-account"); $infoCard.find(".balance_area").addClass("v2p-hide-balance"); } })(); }, }); // src/services.ts async function legacyRequest(url, options) { const res = await fetch(url, options); if (res.ok) { return res.json(); } throw new Error("\u8C03\u7528 V2EX API v1 \u51FA\u9519", { cause: res.status, }); } async function fetchUserInfo(memberName, options) { try { const member = await legacyRequest( `${V2EX_LEGACY_API}/members/show.json?username=${memberName}`, options, ); return member; } catch (err) { if (err instanceof Error) { if (err.name === "AbortError") { throw new Error("\u8BF7\u6C42\u88AB\u53D6\u6D88"); } else if (err.cause === 404) { throw new Error( "\u67E5\u65E0\u6B64\u7528\u6237\uFF0C\u7591\u4F3C\u5DF2\u88AB\u5C01\u7981", { cause: err.cause }, ); } } throw new Error("\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25"); } } async function request(url, options) { const storage = await getStorage(); const PAT = storage["api" /* API */]?.pat; const res = await fetch(url, { ...options, headers: { Authorization: PAT ? `Bearer ${PAT}` : "", ...options?.headers }, }); { const limit = res.headers.get("X-Rate-Limit-Limit"); const reset = res.headers.get("X-Rate-Limit-Reset"); const remaining = res.headers.get("X-Rate-Limit-Remaining"); const api = { pat: PAT, limit: limit ? Number(limit) : void 0, reset: reset ? Number(reset) : void 0, remaining: remaining ? Number(remaining) : void 0, }; void setStorage("api" /* API */, api); } const resultData = await res.json(); if (typeof resultData.success === "boolean" && !resultData.success) { throw new Error(resultData.message, { cause: resultData }); } return resultData; } function fetchTopic(topicId, options) { return request(`${V2EX_API}/topics/${topicId}`, { method: "GET", ...options, }); } async function uploadImage(file) { const formData = new FormData(); formData.append("image", file); const randomIndex = Math.floor(Math.random() * imgurClientIdPool.length); const clidenId = imgurClientIdPool[randomIndex]; const res = await fetch("https://api.imgur.com/3/upload", { method: "POST", headers: { Authorization: `Client-ID ${clidenId}` }, body: formData, }); if (res.ok) { const resData = await res.json(); if (resData.success) { return resData.data.link; } } throw new Error("\u4E0A\u4F20\u5931\u8D25"); } async function refreshMoney() { const res = await fetch("/ajax/money", { method: "POST" }); const data = await res.text(); $("#money").html(data); } function getOnceToken(replyId) { // 1. 当前脚本里的 window.once if (typeof window.once === "string" && window.once) { return window.once; } // 2. 尝试从页面原始 window 取(油猴环境) try { if ( typeof unsafeWindow !== "undefined" && typeof unsafeWindow.once === "string" && unsafeWindow.once ) { return unsafeWindow.once; } } catch (e) { // ignore } // 3. 从 DOM 中原生的感谢链接解析 once // 尝试根据 replyId 定位 thank_area if (typeof replyId === "string" || typeof replyId === "number") { const selector = `#thank_area_${replyId} a.thank[href*="once="]`; const $link = $(selector); if ($link.length) { const href = $link.attr("href") || ""; const match = href.match(/[?&]once=([^&]+)/); if (match && match[1]) { return match[1]; } } } // 4. 兜底:随便找一个带 once 的链接(比如顶部的登出、夜间模式切换等) const $anyLink = $('a[href*="once="]').first(); if ($anyLink.length) { const href = $anyLink.attr("href") || ""; const match = href.match(/[?&]once=([^&]+)/); if (match && match[1]) { return match[1]; } } // 实在拿不到,返回空字符串(让请求至少不是 "undefined") return ""; } async function thankReply(params) { try { const once = getOnceToken(params.replyId); const res = await fetch( `/thank/reply/${params.replyId}?once=${encodeURIComponent(once)}`, { method: "POST", }, ); const data = await res.json(); postTask(`window.once = ${data.once}`); window.once = data.once; if (data.success) { $("#thank_area_" + params.replyId) .addClass("thanked") .html("\u611F\u8C22\u5DF2\u53D1\u9001"); params.onSuccess?.(); await refreshMoney(); } else { alert(data.message); } } catch { params.onFail?.(); } } async function crawlTopicPage(path, page = "1") { const res = await fetch(`${V2EX_ORIGIN}${path}?p=${page}`); const htmlText = await res.text(); return htmlText; } async function getCommentPreview(params) { const formData = new FormData(); formData.append("text", params.text); const res = await fetch(`${V2EX_ORIGIN}/preview/${params.syntax}`, { method: "POST", body: formData, }); if (res.ok) { const renderedContent = await res.text(); return renderedContent; } else { throw new Error("\u9884\u89C8\u5931\u8D25"); } } var V2EX_LEGACY_API, V2EX_API; var init_services = __esm({ "src/services.ts"() { "use strict"; init_constants(); init_helpers(); init_utils(); V2EX_LEGACY_API = `${V2EX_ORIGIN}/api`; V2EX_API = `${V2EX_ORIGIN}/api/v2`; }, }); // src/components/button.ts function createButton(props) { const { children, className = "", type = "button", tag = "button" } = props; const $button = $( `<${tag} class="normal button ${className}">${children}`, ); if (tag === "button") { $button.prop("type", type); } return $button; } var init_button = __esm({ "src/components/button.ts"() { "use strict"; }, }); // src/components/modal.ts function createModal(props) { const { root, title, onMount, onOpen, onClose } = props; const $mask = $('
'); const $content = $('
'); const $closeBtn = createButton({ children: "\u5173\u95EDEsc", className: "v2p-modal-close-btn", }); const $title = $(`
${title ?? ""}
`); const $actions = $('
').append($closeBtn); const $header = $('
').append($title, $actions); const $main2 = $('
') .append($header, $content) .on("click", (ev) => { ev.stopPropagation(); }); const $container = $mask.append($main2).hide(); const modalElements = { $mask, $main: $main2, $header, $container, $title, $actions, $content, }; let boundEvent = false; let mouseDownTarget; const mouseDownHandler = (ev) => { mouseDownTarget = ev.target; }; const mouseUpHandler = (ev) => { if ( mouseDownTarget === $mask.get(0) && ev.target === $mask.get(0) && ev.currentTarget === ev.target ) { handleModalClose(); } }; const keyupHandler = (ev) => { if (ev.key === "Escape") { handleModalClose(); } }; const handleModalClose = () => { $mask.off("mousedown", mouseDownHandler); $mask.off("mouseup", mouseUpHandler); $(document).off("keydown", keyupHandler); boundEvent = false; $container.fadeOut("fast"); document.body.classList.remove("v2p-modal-open"); onClose?.(modalElements); }; const handleModalOpen = () => { if (root && !$container.parent().length) { root.append($container); $closeBtn.on("click", handleModalClose); onMount?.(modalElements); } setTimeout(() => { if (!boundEvent) { $mask.on("mousedown", mouseDownHandler); $mask.on("mouseup", mouseUpHandler); $(document).on("keydown", keyupHandler); boundEvent = true; } }); $container.fadeIn("fast"); document.body.classList.add("v2p-modal-open"); onOpen?.(modalElements); }; return { ...modalElements, open: handleModalOpen, close: handleModalClose }; } var init_modal = __esm({ "src/components/modal.ts"() { "use strict"; init_button(); }, }); // src/contents/topic/content.ts function handleTopicImgHeight() { const $topicContentImgs = $topicContentBox.find( ".topic_content .embedded_image", ); $topicContentImgs.each((_, img) => { const $img = $(img); const height = $img.height() ?? 0; const shouldWrap = height > 600; if (shouldWrap) { const collapsedCSS = { maxHeight: `${READABLE_CONTENT_HEIGHT}px`, overflow: "hidden", paddingBottom: "0", "--bg-reply": "var(--v2p-color-bg-content)", }; const $contentBox = $( '
', ).css(collapsedCSS); const $expandBtn = createButton({ children: "\u5C55\u5F00\u56FE\u7247", className: "v2p-expand-btn", }); const toggleContent = () => { const collapsed = $contentBox.hasClass("v2p-collapsed"); $contentBox .toggleClass("v2p-collapsed") .css( collapsed ? { maxHeight: "none", overflow: "auto", paddingBottom: "40px" } : collapsedCSS, ); $expandBtn.html( collapsed ? "\u6536\u8D77\u56FE\u7247" : "\u5C55\u5F00\u56FE\u7247", ); }; $expandBtn.on("click", () => { toggleContent(); }); $contentBox.append($img.clone()).replaceAll($img).append($expandBtn); } }); } function handleContent() { const storage = getStorageSync(); const options = storage["options" /* Options */]; if (options.openInNewTab) { $topicContentBox .find(".topic_content a[href]") .prop("target", "_blank") .prop("rel", "noopener noreferrer"); } { const $topicContents = $topicContentBox.find(".subtle > .topic_content"); const textLength = $topicContents.text().length; if (textLength >= 200) { $topicContents.each((_, topicContent) => { if (textLength >= 400) { topicContent.style.fontSize = "14px"; } topicContent.style.fontSize = "14.5px"; }); } } { const $topicBtns = $(".topic_buttons"); const $topicBtn = $topicBtns.find(".tb").addClass("v2p-tb v2p-hover-btn"); const $favoriteBtn = $topicBtn.eq(0); $favoriteBtn.append( '', ); $topicBtn .eq(1) .append(''); $topicBtn .eq(2) .append(''); $topicBtn .eq(3) .append(''); if (pathTopicId) { $topicBtns.append( `  \u5206\u4EAB`, ); } loadIcons(); } window.requestIdleCallback(() => { handleTopicImgHeight(); }); } function processReplyContent($cellDom) { if ($cellDom.find(".v2p-reply-content").length > 0) { return; } const $replyContent = $cellDom.find(".reply_content"); const contentHeight = $replyContent.height() ?? 0; const shouldCollapsed = contentHeight + READABLE_CONTENT_HEIGHT >= MAX_CONTENT_HEIGHT; if (shouldCollapsed) { const collapsedCSS = { maxHeight: `${READABLE_CONTENT_HEIGHT}px`, overflow: "hidden", paddingBottom: "0", }; const $contentBox = $('
').css( collapsedCSS, ); const $expandBtn = createButton({ children: "\u5C55\u5F00\u56DE\u590D", className: "v2p-expand-btn", }); const toggleContent = () => { const collapsed = $contentBox.hasClass("v2p-collapsed"); $contentBox .toggleClass("v2p-collapsed") .css( collapsed ? { maxHeight: "none", overflow: "auto", paddingBottom: "40px" } : collapsedCSS, ); $expandBtn.html( collapsed ? "\u6536\u8D77\u56DE\u590D" : "\u5C55\u5F00\u56DE\u590D", ); }; $expandBtn.on("click", () => { toggleContent(); }); $contentBox .append($replyContent.clone()) .replaceAll($replyContent) .append($expandBtn); } } function updateMemberTag() {} function openTagsSetter() {} function handleTopicImg() { const $imgs = $(".embedded_image"); if ($imgs.length > 0) { const modal = createModal({ root: $body, onMount: ({ $main: $main2, $header, $content }) => { $main2.css({ width: "auto", height: "auto", display: "flex", "justify-content": "center", "background-color": "transparent", "pointer-events": "none", }); $header.remove(); $content.css({ display: "flex", "justify-content": "center", "align-items": "center", "pointer-events": "none", }); }, onOpen: ({ $content }) => { $content.empty(); }, }); $imgs.each((_, img) => { const $img = $(img); if (img instanceof HTMLImageElement) { $img.parent().removeAttr("href"); if (img.clientWidth !== img.naturalWidth) { $img.css({ cursor: "zoom-in" }); $img.on("click", () => { const $clonedImg = $img.clone(); $clonedImg.css({ cursor: "default", "pointer-events": "auto" }); modal.open(); modal.$content.append($clonedImg); }); } } }); } } var init_content = __esm({ "src/contents/topic/content.ts"() { "use strict"; init_button(); init_modal(); init_constants(); init_use_topic_preview(); init_utils(); init_globals(); init_helpers(); }, }); // src/contents/dom.ts function getCommentDataList({ options, $commentTableRows: $commentTableRows2, $commentCells: $commentCells2, }) { return $commentTableRows2 .map((idx, tr) => { const id = $commentCells2[idx].id; const $tr = $(tr); const $td = $tr.find("> td:nth-child(3)"); const thanked = $tr .find("> td:last-of-type > .fr") .find("> .thank_area") .hasClass("thanked"); const $member = $td.find("> strong > a"); const memberName = $member.text(); const memberLink = $member.prop("href"); const memberAvatar = $tr.find(".avatar").prop("src"); const $content = $td.find("> .reply_content"); const content = $content.text(); const likes = Number($td.find("span.small").text()); const floor = $td.find("span.no").text(); const memberNameMatches = Array.from( content.matchAll(/@([a-zA-Z0-9]+)/g), ); const refMemberNames = memberNameMatches.length > 0 ? memberNameMatches.map(([, name]) => { return name; }) : void 0; const floorNumberMatches = Array.from(content.matchAll(/#(\d+)/g)); const refFloors = floorNumberMatches.length > 0 ? floorNumberMatches.map(([, floor2]) => { return floor2; }) : void 0; let contentHtml = void 0; if (refMemberNames) { const canHideRefName = options.nestedReply.display === "indent" && !!options.replyContent.hideRefName; if (canHideRefName) { if (refMemberNames.length === 1) { contentHtml = $content.html(); const pattern = /(@[\w\s]+<\/a>)\s+/g; const replacement = '$1 '; contentHtml = contentHtml.replace(pattern, replacement); } } } return { id, memberName, memberLink, memberAvatar, content, contentHtml, likes, floor, index: idx, refMemberNames, refFloors, thanked, }; }) .get(); } function handleNestedComment({ options, $commentCells: $commentCells2, commentDataList: commentDataList2, }) { const display = options.nestedReply.display; if (display !== "off") { $commentCells2.each((i, cellDom) => { const $cellDom = $(cellDom); const dataFromIndex = commentDataList2.at(i); if (options.replyContent.autoFold) { processReplyContent($cellDom); } const currentComment = dataFromIndex?.id === cellDom.id ? dataFromIndex : commentDataList2.find((data) => data.id === cellDom.id); if (currentComment) { const { refMemberNames, refFloors } = currentComment; if (!refMemberNames || refMemberNames.length === 0) { return; } const moreThanOneRefMember = refMemberNames.length > 1; if ( options.nestedReply.multipleInsideOne === "off" && refMemberNames.length > 1 ) { return; } for (const refName of moreThanOneRefMember ? refMemberNames.toReversed() : refMemberNames) { for (let j = i - 1; j >= 0; j--) { const { memberName: compareName, floor: eachFloor } = commentDataList2.at(j) || {}; if (compareName === refName) { let refCommentIdx = j; const firstRefFloor = moreThanOneRefMember ? refFloors?.toReversed().at(0) : refFloors?.at(0); if (firstRefFloor && firstRefFloor !== eachFloor) { const targetIdx = commentDataList2 .slice(0, j) .findIndex( (data) => data.floor === firstRefFloor && data.memberName === refName, ); if (targetIdx >= 0) { refCommentIdx = targetIdx; } } if (display === "indent") { cellDom.classList.add("v2p-indent"); } $commentCells2.eq(refCommentIdx).append(cellDom); return; } } } } }); } } var init_dom = __esm({ "src/contents/dom.ts"() { "use strict"; init_content(); }, }); var invalidTemplate, topicDataCache; var init_use_topic_preview = __esm({ "src/use-topic-preview.ts"() { "use strict"; init_lucide(); init_button(); init_modal(); init_constants(); init_dom(); init_globals(); init_helpers(); init_content(); init_icons(); init_services(); init_utils(); invalidTemplate = (tip) => `
${tip}
\u8BF7\u524D\u5F80 > \u8BBE\u7F6E \u8FDB\u884C\u8BBE\u7F6E\u3002
1. \u5728\u6269\u5C55\u7A0B\u5E8F\u5217\u8868\u4E2D\u627E\u5230\u5E76\u70B9\u51FB\u300CV2EX Polish\u300D\u3002
2. \u5728\u5F39\u51FA\u7684\u5C0F\u7A97\u53E3\u4E2D\u627E\u5230\u300C\u2699\uFE0F \u6309\u94AE\u300D\uFF0C\u8F93\u5165 PAT\u3002
`; topicDataCache = /* @__PURE__ */ new Map(); }, }); var init_topic_list = __esm({ "src/contents/home/topic-list.ts"() { "use strict"; init_toast(); init_constants(); init_services(); init_use_topic_preview(); init_utils(); init_globals(); }, }); // src/contents/home/index.ts var home_exports = {}; var init_home = __esm({ "src/contents/home/index.ts"() { "use strict"; init_constants(); init_utils(); init_globals(); init_helpers(); init_topic_list(); function handleNodeNavToggle() { // 1. 查找包含“节点导航”文本的标题区域 // 这里的筛选逻辑是为了确保只定位到正确的那个 box const $headerSpan = $(".box .cell span.fade").filter((_, el) => { return $(el).text().includes("节点导航"); }); if ($headerSpan.length === 0) return; // 2. 获取容器和需要折叠的内容 // 逻辑:找到父级 .box,然后选取除了第一个子元素(标题栏)以外的所有子元素 const $box = $headerSpan.closest(".box"); const $headerCell = $headerSpan.closest(".cell"); const $content = $box.children().not(":first-child"); // 3. 定义存储 Key const STORAGE_KEY = "v2p_nav_collapsed"; // 4. 创建切换按钮 // 使用 v2p-hover-btn 类以保持样式一致,行内样式微调位置 const $toggleBtn = $( '
', ); const icons = { arrowUp: '', arrowDown: '', }; // 5. 定义切换状态的逻辑 const applyState = (isCollapsed) => { if (isCollapsed) { $content.hide(); $toggleBtn.html(icons.arrowDown); // 折叠状态显示“向下展开” // 稍微减小 box 的下边距,折叠后看起来更紧凑 $box.css("margin-bottom", "10px"); $headerCell.css("border-bottom", "none"); } else { $content.show(); $toggleBtn.html(icons.arrowUp); // 展开状态显示“向上收起” $box.css("margin-bottom", ""); $headerCell.css("border-bottom", ""); } // 存储状态:'1' 为折叠,'0' 为展开 localStorage.setItem(STORAGE_KEY, isCollapsed ? "1" : "0"); }; // 6. 绑定点击事件 $toggleBtn.on("click", function () { const isContentVisible = $content.is(":visible"); // 如果当前内容可见,则点击意图是折叠 (isCollapsed = true) applyState(isContentVisible); }); // 7. 初始化:读取存储的状态并应用 const savedState = localStorage.getItem(STORAGE_KEY) === "1"; applyState(savedState); // 8. 将按钮插入到“节点导航”文字后面 $headerSpan.append($toggleBtn); } // 通用的右侧边栏区域折叠功能 function handleSidebarSectionToggle(sectionTitle, storageKey) { // 1. 查找包含指定文本的标题区域 const $headerSpan = $( "#Rightbar .box .cell span.fade, #Rightbar .box .inner span.fade, #Rightbar .box .inner span.f12.gray, #Rightbar .box .cell span.f12.gray", ).filter((_, el) => { return $(el).text().trim() === sectionTitle; }); if ($headerSpan.length === 0) return; // 如果是"我收藏的节点",将字体样式改为和其他栏一致的 fade 类 if (sectionTitle === "我收藏的节点") { $headerSpan.removeClass("f12 gray").addClass("fade"); } // 2. 获取容器和需要折叠的内容 const $box = $headerSpan.closest(".box"); const $headerContainer = $headerSpan.closest(".cell, .inner"); const $content = $box.children().not($headerContainer); // 标记为可排序区块 $box.attr("data-v2p-sortable", sectionTitle); // 3. 创建切换按钮 const $toggleBtn = $( '', ); const icons = { arrowUp: '', arrowDown: '', }; // 4. 定义切换状态的逻辑 const applyState = (isCollapsed) => { if (isCollapsed) { $content.hide(); $toggleBtn.html(icons.arrowDown); $box.css("margin-bottom", "10px"); $headerContainer.css("border-bottom", "none"); } else { $content.show(); $toggleBtn.html(icons.arrowUp); $box.css("margin-bottom", ""); $headerContainer.css("border-bottom", ""); } localStorage.setItem(storageKey, isCollapsed ? "1" : "0"); }; // 5. 绑定点击事件 $toggleBtn.on("click", function () { const isContentVisible = $content.is(":visible"); applyState(isContentVisible); }); // 6. 初始化:读取存储的状态并应用 const savedState = localStorage.getItem(storageKey) === "1"; applyState(savedState); // 7. 将按钮插入到标题文字后面 $headerSpan.append($toggleBtn); } // 右侧边栏拖拽排序功能 function handleSidebarDragSort() { const STORAGE_KEY = "v2p_sidebar_order"; const $sortableBoxes = $("#Rightbar [data-v2p-sortable]"); if ($sortableBoxes.length < 2) return; // 添加拖拽相关 CSS $("