// ==UserScript== // @name V2EX Polish - 体验更现代化的 V2EX // @namespace LeoKu(https://leoku.top) // @version 1.3.6.2 // @description 一款专为 V2EX 用户设计的浏览器插件,提供了丰富的扩展功能,让原生页面焕然一新! // @author LeoKu // @match https://*.v2ex.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=v2ex.com // @run-at document-start // @grant GM_addStyle // @license MIT // @downloadURL none // ==/UserScript== "use strict"; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; // src/constants.ts var EXTENSION_NAME, emoticons, READABLE_CONTENT_HEIGHT, MAX_CONTENT_HEIGHT, dataExpiryTime, imgurClientIdPool, defaultOptions; var init_constants = __esm({ "src/constants.ts"() { "use strict"; EXTENSION_NAME = "V2EX_Polish"; emoticons = [ { 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; 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 }, replyContent: { autoFold: true }, nestedReply: { display: "indent" } }; } }); // src/icons.ts var iconEmoji, iconHeart, iconHide, iconReply, iconStar, iconTwitter, iconIgnore, iconLove, iconLoading, iconLogo, iconChromeWebStore, iconGitHub, iconScrollTop, iconTool, iconLight, iconDark; var init_icons = __esm({ "src/icons.ts"() { "use strict"; iconEmoji = ``; iconHeart = ``; iconHide = ``; iconReply = ``; iconStar = ``; iconTwitter = ``; iconIgnore = ``; iconLove = ``; iconLoading = ``; iconLogo = ``; iconChromeWebStore = ``; iconGitHub = ``; iconScrollTop = ``; iconTool = ``; iconLight = ``; iconDark = ``; } }); // src/deep-merge.ts function isObject(value) { return typeof value === "object" && value !== null; } function deepMerge(obj1, obj2) { const result = {}; for (const key in obj1) { if (isObject(obj1[key]) && isObject(obj2[key])) { result[key] = deepMerge(obj1[key], obj2[key]); } else if (Reflect.has(obj2, key)) { result[key] = obj2[key]; } else { result[key] = obj1[key]; } } for (const key in obj2) { if (!Reflect.has(obj1, key)) { result[key] = obj2[key]; } } return result; } var init_deep_merge = __esm({ "src/deep-merge.ts"() { "use strict"; } }); // 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 === "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 getRunEnv() { if (typeof chrome !== "undefined" && typeof chrome.extension !== "undefined") { return "chrome"; } return null; } function getStorage(useCache = true) { return new Promise((resolve, reject) => { if (useCache) { if (typeof window !== "undefined" && window.__V2P_StorageCache) { resolve(window.__V2P_StorageCache); } } const runEnv = getRunEnv(); if (runEnv !== "chrome") { const data = { ["options" /* Options */]: defaultOptions }; window.__V2P_StorageCache = data; return resolve(data); } chrome.storage.sync.get().then((items) => { let data; const options = items["options" /* Options */]; if (options) { data = { ...items, ["options" /* Options */]: deepMerge(defaultOptions, options) }; } else { data = { ...items, ["options" /* Options */]: defaultOptions }; } if (typeof window !== "undefined") { window.__V2P_StorageCache = data; } resolve(data); }).catch((err) => { reject(err); }); }); } function getStorageSync() { const storage = window.__V2P_StorageCache; if (!storage) { throw new Error(`${EXTENSION_NAME}: \u65E0\u53EF\u7528\u7684 Storage \u7F13\u5B58\u6570\u636E\u3002`); } return storage; } async function setStorage(storageKey, storageItem) { switch (storageKey) { case "options" /* Options */: case "api" /* API */: case "daily" /* Daily */: case "member-tag" /* MemberTag */: case "settings-sync" /* SyncInfo */: await chrome.storage.sync.set({ [storageKey]: storageItem }); break; default: throw new Error(``); } } function escapeHTML(htmlString) { return htmlString.replace(/[<>&"'']/g, (match) => { switch (match) { case "<": return "<"; case ">": return ">"; case "&": return "&"; case '"': return """; case "'": return "'"; default: return match; } }); } var init_utils = __esm({ "src/utils.ts"() { "use strict"; init_constants(); init_deep_merge(); } }); // src/contents/common.ts var common_exports = {}; var init_common = __esm({ "src/contents/common.ts"() { "use strict"; init_constants(); init_icons(); init_utils(); void (async () => { const storage = await getStorage(); const options = storage["options" /* Options */]; if (options.theme.autoSwitch) { const perfersDark = window.matchMedia("(prefers-color-scheme: dark)"); if (perfersDark.matches) { $("#Wrapper").addClass("Night"); } perfersDark.addEventListener("change", ({ matches }) => { if (matches) { $("#Wrapper").addClass("Night"); } else { $("#Wrapper").removeClass("Night"); } }); } { $("#Top .site-nav .tools > .top").addClass("v2p-hover-btn"); } { const $toggle = $("#Rightbar .light-toggle").addClass("v2p-color-mode-toggle"); const $toggleImg = $toggle.find("> img"); if ($toggleImg.prop("alt") === "Light") { $toggle.prop("title", "\u4F7F\u7528\u6DF1\u8272\u4E3B\u9898"); $toggleImg.replaceWith(iconDark); } if ($toggleImg.prop("alt") === "Dark") { $toggle.prop("title", "\u4F7F\u7528\u6D45\u8272\u4E3B\u9898"); $toggleImg.replaceWith(iconLight); } } { const $extraFooter = $(`
`); $(` `).prependTo($extraFooter); $("#Bottom .content").append($extraFooter); } })(); } }); // src/components/button.ts function createButton(props) { const { children, className = "", type = "button", tag = "button" } = props; const $button = $(`<${tag} class="normal button ${className}">${children}${tag}>`); if (tag === "button") { $button.prop("type", type); } return $button; } var init_button = __esm({ "src/components/button.ts"() { "use strict"; } }); // src/components/model.ts function createModel(props) { const { root, title, onOpen, onClose, onMount } = props; const $mask = $('\u8BE5\u4E3B\u9898\u6CA1\u6709\u6B63\u6587\u5185\u5BB9