// ==UserScript== // @name linux.do助手 // @namespace https://scriptcat.org/zh-CN/users/212527 // @version 1.0.0 // @description 自动阅读、智能点赞、查询账号信息等功能 // @author kun&gpt-6-astra // @icon https://cdn3.ldstatic.com/optimized/4X/6/a/6/6a6affc7b1ce8140279e959d32671304db06d5ab_2_180x180.png // @license MIT // @match https://linux.do/* // @match https://idcflare.com/* // @match https://cdk.linux.do/* // @grant GM_xmlhttpRequest // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @grant GM_getTab // @grant GM_saveTab // @grant unsafeWindow // @connect connect.linux.do // @connect credit.linux.do // @connect cdk.linux.do // @connect linux.do // @connect * // @run-at document-idle // ==/UserScript== (() => { var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); // src/core/runtime-context.js var resolveWindow = (candidate) => { if (candidate) return candidate; if (typeof window !== "undefined") return window; return typeof globalThis !== "undefined" ? globalThis : void 0; }; var resolveLocation = (windowRef, candidate) => { if (candidate) return candidate; return windowRef?.location || (typeof location !== "undefined" ? location : void 0); }; var resolveFetch = (windowRef, candidate) => { if (typeof candidate === "function") return candidate; if (typeof windowRef?.fetch === "function") return windowRef.fetch.bind(windowRef); if (typeof globalThis?.fetch === "function") return globalThis.fetch.bind(globalThis); return void 0; }; var resolveTimer = (windowRef, name) => { if (typeof windowRef?.[name] === "function") return windowRef[name].bind(windowRef); if (typeof globalThis?.[name] === "function") return globalThis[name].bind(globalThis); return void 0; }; var resolveStorage = (windowRef, name) => { try { return windowRef?.[name] || (typeof globalThis !== "undefined" ? globalThis[name] : void 0); } catch (_) { return void 0; } }; var resolveGlobal = (windowRef) => { try { if (typeof unsafeWindow !== "undefined" && unsafeWindow) return unsafeWindow; } catch (_) { } return windowRef; }; var createRuntimeContext = (options = {}) => { const windowRef = resolveWindow(options.windowRef || options.window); const documentRef = options.documentRef || options.document || windowRef?.document; const locationRef = resolveLocation(windowRef, options.locationRef || options.location); const currentDomain = String( options.currentDomain ?? locationRef?.hostname ?? "" ); const baseUrl = String( options.baseUrl ?? (currentDomain ? `https://${currentDomain}` : locationRef?.origin || "") ); const fetchFn = resolveFetch(windowRef, options.fetchFn || options.fetch); const setTimeoutFn = options.setTimeoutFn || options.setTimeout || resolveTimer(windowRef, "setTimeout"); const clearTimeoutFn = options.clearTimeoutFn || options.clearTimeout || resolveTimer(windowRef, "clearTimeout"); const setIntervalFn = options.setIntervalFn || options.setInterval || resolveTimer(windowRef, "setInterval"); const clearIntervalFn = options.clearIntervalFn || options.clearInterval || resolveTimer(windowRef, "clearInterval"); const isHeartToggleUrl2 = (url, method = "PUT") => { try { const parsed = new URL(url, baseUrl); return String(method || "GET").toUpperCase() === "PUT" && parsed.origin === baseUrl && /^\/discourse-reactions\/posts\/\d+\/custom-reactions\/heart\/toggle\.json$/.test(parsed.pathname); } catch (_) { return false; } }; return { window: windowRef, document: documentRef, location: locationRef, pageWindow: resolveGlobal(windowRef), fetch: fetchFn, XMLHttpRequest: windowRef?.XMLHttpRequest, localStorage: options.localStorage || resolveStorage(windowRef, "localStorage"), sessionStorage: options.sessionStorage || resolveStorage(windowRef, "sessionStorage"), navigator: options.navigator || windowRef?.navigator, setTimeout: setTimeoutFn, clearTimeout: clearTimeoutFn, setInterval: setIntervalFn, clearInterval: clearIntervalFn, gm: options.gm || { xmlHttpRequest: typeof GM_xmlhttpRequest === "function" ? GM_xmlhttpRequest : void 0, addStyle: typeof GM_addStyle === "function" ? GM_addStyle : void 0, getValue: typeof GM_getValue === "function" ? GM_getValue : void 0, setValue: typeof GM_setValue === "function" ? GM_setValue : void 0, getTab: typeof GM_getTab === "function" ? GM_getTab : void 0, saveTab: typeof GM_saveTab === "function" ? GM_saveTab : void 0 }, CURRENT_DOMAIN: currentDomain, BASE_URL: baseUrl, isCDKPage: currentDomain === "cdk.linux.do", isHeartToggleUrl: isHeartToggleUrl2 }; }; var defaultRuntime = createRuntimeContext(); var CURRENT_DOMAIN = defaultRuntime.CURRENT_DOMAIN; var BASE_URL = defaultRuntime.BASE_URL; var isCDKPage = defaultRuntime.isCDKPage; var isHeartToggleUrl = defaultRuntime.isHeartToggleUrl; // src/core/config.js var CONFIG = { scroll: { minSpeed: 10, maxSpeed: 15, minDistance: 2, maxDistance: 4, stepMultiplier: 2.5, fastScrollChance: 0.08, fastScrollMin: 80, fastScrollMax: 200, fullTopicBacktrackChance: 8, fullTopicSlowChance: 24, fullTopicSmallStepMinRatio: 0.05, fullTopicSmallStepMaxRatio: 0.09, fullTopicNormalStepMinRatio: 0.09, fullTopicNormalStepMaxRatio: 0.18, fullTopicBacktrackMinRatio: 0.02, fullTopicBacktrackMaxRatio: 0.05, fullTopicMinStep: 160, fullTopicNormalMinSpeed: 260, fullTopicNormalMaxSpeed: 520, fullTopicSlowMinSpeed: 520, fullTopicSlowMaxSpeed: 900, fullTopicBacktrackMinSpeed: 180, fullTopicBacktrackMaxSpeed: 360, bottomSettleChecks: 3, bottomSettleDelay: 800, readConfirmationChecks: 12, readConfirmationDelay: 1e3, readConfirmationClearChecks: 2, readConfirmationNudgePixels: 6 }, time: { browseTime: 36e5, restTime: 6e5, topicLoadingTimeout: 25e3 }, article: { commentLimit: 5e3, retryLimit: 3 }, levelRequirements: { 0: { topics_entered: 5, posts_read_count: 30, time_read: 600 }, 1: { days_visited: 15, likes_given: 1, likes_received: 1, post_count: 3, topics_entered: 20, posts_read_count: 100, time_read: 3600 } }, mustRead: { posts: [ { id: "1051", url: "https://linux.do/t/topic/1051/" }, { id: "5973", url: "https://linux.do/t/topic/5973" }, { id: "102770", url: "https://linux.do/t/topic/102770" }, { id: "154010", url: "https://linux.do/t/topic/154010" }, { id: "149576", url: "https://linux.do/t/topic/149576" }, { id: "22118", url: "https://linux.do/t/topic/22118" } ] }, likeAllowedCategories: { allowed: [ "\u5F00\u53D1\u8C03\u4F18", "\u56FD\u4EA7\u66FF\u4EE3", "\u8D44\u6E90\u835F\u8403", "\u6587\u6863\u5171\u5EFA", "\u975E\u6211\u83AB\u5C5E", "\u8BFB\u4E66\u6210\u8BD7", "\u524D\u6CBF\u5FEB\u8BAF", "\u798F\u5229\u7F8A\u6BDB", "\u641E\u4E03\u637B\u4E09", "\u8FD0\u8425\u53CD\u9988" ], excluded: [ "\u7F51\u76D8\u8D44\u6E90", "\u8DF3\u86A4\u5E02\u573A", "\u6DF1\u6D77\u5E7D\u57DF", "\u79EF\u5206\u4E50\u56ED", "\u626C\u5E06\u8D77\u822A", "\u793E\u533A\u5B75\u5316" ] } }; var createConfig = (overrides = {}) => ({ ...CONFIG, ...overrides, scroll: { ...CONFIG.scroll, ...overrides.scroll || {} }, time: { ...CONFIG.time, ...overrides.time || {} }, article: { ...CONFIG.article, ...overrides.article || {} }, levelRequirements: { ...CONFIG.levelRequirements, ...overrides.levelRequirements || {} }, mustRead: { ...CONFIG.mustRead, ...overrides.mustRead || {} }, likeAllowedCategories: { ...CONFIG.likeAllowedCategories, ...overrides.likeAllowedCategories || {} } }); // src/core/storage.js var resolveStorageArea = (storageArea) => { if (storageArea) return storageArea; try { if (typeof localStorage !== "undefined") return localStorage; } catch (_) { } return void 0; }; var createStorage = (storageArea) => { const area = resolveStorageArea(storageArea); return { get: (key, defaultValue = null) => { try { const value = area?.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch (_) { return defaultValue; } }, set: (key, value) => { try { if (!area?.setItem) return false; area.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.error("Storage error:", error); return false; } }, remove: (key) => { try { if (!area?.removeItem) return false; area.removeItem(key); return true; } catch (error) { console.error("Storage error:", error); return false; } }, raw: area }; }; var Storage = createStorage(); // src/core/like-cooldown.js var resolveStorageArea2 = (storageArea) => { if (storageArea) return storageArea; try { if (typeof localStorage !== "undefined") return localStorage; } catch (_) { } return void 0; }; var createLikeCooldown = ({ storageArea, key = "likeResumeTime" } = {}) => { const area = resolveStorageArea2(storageArea); return { load() { try { if (!area?.getItem) throw new Error("Storage unavailable"); const raw = area?.getItem(key); const stored = raw ? JSON.parse(raw) : null; const state = { accounts: {}, legacyUntil: 0 }; const validUntil = (value) => { const until = Number(value ?? 0); if (!Number.isFinite(until) || until < 0) { throw new Error("Invalid like cooldown timestamp"); } return until; }; if (stored && typeof stored === "object" && !Array.isArray(stored)) { state.legacyUntil = validUntil(stored.legacyUntil); if (stored.accounts != null) { if (typeof stored.accounts !== "object" || Array.isArray(stored.accounts)) { throw new Error("Invalid like cooldown accounts"); } for (const [accountKey, until] of Object.entries(stored.accounts)) { if (/^(id|username):/.test(accountKey)) { state.accounts[accountKey] = validUntil(until); } } } } else if (stored === null || typeof stored === "number") { state.legacyUntil = validUntil(stored); } else { throw new Error("Invalid like cooldown state"); } return state; } catch (error) { console.error("[\u70B9\u8D5E\u51B7\u5374] \u5B58\u50A8\u4E0D\u53EF\u8BFB\uFF0C\u6682\u505C\u70B9\u8D5E:", error); return null; } }, read(accountKey) { const state = this.load(); if (!state) return Infinity; return Math.max( state.legacyUntil, accountKey ? Number(state.accounts[accountKey]) || 0 : 0 ); }, extend(accountKey, until) { const state = this.load(); if (!state) return Infinity; const nextUntil = Number(until); if (!Number.isFinite(nextUntil) || nextUntil < 0) return Infinity; if (nextUntil > 0) { if (accountKey) { state.accounts[accountKey] = Math.max( Number(state.accounts[accountKey]) || 0, nextUntil ); } else { state.legacyUntil = Math.max(state.legacyUntil, nextUntil); } } try { if (!area?.setItem) return Infinity; area.setItem(key, JSON.stringify(state)); } catch (error) { console.error("[\u70B9\u8D5E\u51B7\u5374] \u5B58\u50A8\u4E0D\u53EF\u5199\uFF0C\u6682\u505C\u70B9\u8D5E:", error); return Infinity; } return Math.max( state.legacyUntil, accountKey ? Number(state.accounts[accountKey]) || 0 : 0 ); }, clear(accountKeys) { const state = this.load(); if (!state) return false; try { state.legacyUntil = 0; const keys = Array.isArray(accountKeys) ? accountKeys : [accountKeys]; for (const accountKey of keys) { if (/^(id|username):/.test(String(accountKey || ""))) { delete state.accounts[accountKey]; } } if (!area?.setItem) return false; area.setItem(key, JSON.stringify(state)); return true; } catch (error) { console.error("[\u70B9\u8D5E\u51B7\u5374] \u6E05\u9664\u5931\u8D25:", error); return false; } }, key, raw: area }; }; var LikeCooldown = createLikeCooldown(); // src/core/utils.js var createUtils = (runtime = defaultRuntime) => { const documentRef = runtime.document; const setTimeoutFn = runtime.setTimeout || ((callback, ms) => setTimeout(callback, ms)); return { random: (min, max) => Math.floor(Math.random() * (max - min + 1)) + min, sleep: (ms) => new Promise((resolve) => setTimeoutFn(resolve, ms)), isPageLoaded: () => { if (!documentRef?.querySelectorAll) return true; const loadingElements = documentRef.querySelectorAll(".loading, .infinite-scroll"); return loadingElements.length === 0; }, isNearBottom: () => { const root = documentRef?.documentElement; if (!root) return false; const { scrollHeight, clientHeight, scrollTop } = root; return scrollTop + clientHeight >= scrollHeight - 200; } }; }; var Utils = createUtils(); // src/core/request-manager.js var RequestManagerClass = class { constructor({ storage = Storage, utils = Utils } = {}) { this.storage = storage; this.utils = utils; this._lastRequestAt = /* @__PURE__ */ new Map(); this._inFlight = /* @__PURE__ */ new Map(); } async run(key, requestFn, options = {}) { const { minInterval = 1200, retries = 1, backoffBase = 1200, jitter = 250, cooldownKey = null, cooldownMs = 30 * 60 * 1e3 } = options; if (this._inFlight.has(key)) { return this._inFlight.get(key); } const runner = (async () => { if (cooldownKey) { const cooldownUntil = this.storage.get(cooldownKey, 0); if (cooldownUntil > Date.now()) return null; } const lastAt = this._lastRequestAt.get(key) || 0; const elapsed = Date.now() - lastAt; const waitMs = minInterval - elapsed + this.utils.random(0, jitter); if (waitMs > 0) await this.utils.sleep(waitMs); let attempt = 0; while (attempt <= retries) { try { this._lastRequestAt.set(key, Date.now()); return await requestFn(); } catch (error) { const msg = String(error?.message || ""); const is429 = msg.includes("429") || msg.toLowerCase().includes("rate"); if (is429 && cooldownKey) { this.storage.set(cooldownKey, Date.now() + cooldownMs); } if (attempt >= retries) throw error; const backoff = backoffBase * Math.pow(2, attempt) + this.utils.random(100, 600); await this.utils.sleep(backoff); attempt++; } } return null; })(); this._inFlight.set(key, runner); try { return await runner; } finally { this._inFlight.delete(key); } } clear(key) { if (key === void 0) { this._lastRequestAt.clear(); this._inFlight.clear(); return; } this._lastRequestAt.delete(key); this._inFlight.delete(key); } }; var RequestManager = new RequestManagerClass(); // src/core/network-interceptor.js var STATE_KEY = Symbol.for("linux-do-assistant.network-interceptor"); var requestInfo = (args, transport) => { const request = args?.[0]; return { transport, url: typeof request === "string" ? request : request?.url || request?.href || "", method: String(args?.[1]?.method || request?.method || "GET").toUpperCase(), args }; }; var callSafely = (callback, payload) => { if (typeof callback !== "function") return void 0; try { return callback(payload); } catch (error) { console.error("[NetworkInterceptor] subscriber error:", error); return void 0; } }; var NetworkInterceptor = class { constructor(runtime = defaultRuntime) { this.runtime = runtime; this.subscribers = /* @__PURE__ */ new Set(); this.installed = false; this.originalFetch = null; this.originalXhrOpen = null; this.originalXhrSend = null; } register(subscriber = {}) { this.subscribers.add(subscriber); this.install(); return () => this.subscribers.delete(subscriber); } install() { if (this.installed) return true; const win = this.runtime.window || (typeof window !== "undefined" ? window : null); if (!win) return false; if (typeof win.fetch === "function") { this.originalFetch = win.fetch; const service = this; win.fetch = async function(...args) { const request = requestInfo(args, "fetch"); const contexts = service.collectContexts(request); let response; try { response = await service.originalFetch.apply(this, args); } catch (error) { service.dispatchError({ request, contexts, error }); throw error; } await service.dispatchResponse({ request, contexts, response }); return response; }; } const XHR = win.XMLHttpRequest; if (XHR?.prototype) { this.originalXhrOpen = XHR.prototype.open; this.originalXhrSend = XHR.prototype.send; const service = this; XHR.prototype.open = function(method, url, ...rest) { this.__ldaNetworkRequest = { transport: "xhr", url, method: String(method || "GET").toUpperCase() }; return service.originalXhrOpen.apply(this, [method, url, ...rest]); }; XHR.prototype.send = function(body) { const request = { ...this.__ldaNetworkRequest || {}, transport: "xhr", url: this.__ldaNetworkRequest?.url || "", method: this.__ldaNetworkRequest?.method || "GET", xhr: this, body }; const contexts = service.collectContexts(request); const hasMatchingSubscriber = contexts.some(({ value }) => value !== null && value !== void 0); if (hasMatchingSubscriber) { this.addEventListener("load", () => { service.dispatchResponse({ request, contexts, response: this }); }, { once: true }); this.addEventListener("error", (event) => { service.dispatchError({ request, contexts, error: event }); }, { once: true }); } return service.originalXhrSend.apply(this, [body]); }; } this.installed = true; return true; } collectContexts(request) { return [...this.subscribers].map((subscriber) => ({ subscriber, value: callSafely(subscriber.beforeRequest, request) })); } async dispatchResponse({ request, contexts, response }) { for (const { subscriber, value } of contexts) { if (typeof subscriber.afterResponse !== "function") continue; try { await subscriber.afterResponse({ request, response, context: value }); } catch (error) { console.error("[NetworkInterceptor] async subscriber error:", error); } } } dispatchError({ request, contexts, error }) { for (const { subscriber, value } of contexts) { callSafely(subscriber.onError, { request, context: value, error }); } } }; var getNetworkInterceptor = (runtime = defaultRuntime) => { const win = runtime.window || (typeof window !== "undefined" ? window : null); if (win) { if (!win[STATE_KEY]) win[STATE_KEY] = new NetworkInterceptor(runtime); return win[STATE_KEY]; } return new NetworkInterceptor(runtime); }; var networkInterceptor = getNetworkInterceptor(); // src/core/index.js var createCore = (options = {}) => { const runtime = options.runtime ? { ...createRuntimeContext(options.runtime), ...options.runtime } : createRuntimeContext(options); const config = options.config || createConfig(options.configOverrides || {}); const storage = options.storage || createStorage(runtime.localStorage); const likeCooldown = options.likeCooldown || createLikeCooldown({ storageArea: runtime.localStorage }); const utils = options.utils || createUtils(runtime); const requestManager = options.requestManager || new RequestManagerClass({ storage, utils }); return { ...runtime, CONFIG: config, Utils: utils, Storage: storage, LikeCooldown: likeCooldown, RequestManager: requestManager, NetworkInterceptor: getNetworkInterceptor(runtime), context: runtime }; }; var core = createCore({ runtime: defaultRuntime }); // src/features/cdk-bridge.js function initCDKBridgePage() { if (!isCDKPage || typeof window === "undefined") return; const bridgeKey = Symbol.for("linux-do-assistant.cdk-bridge"); if (window[bridgeKey]) return window[bridgeKey]; const cacheAndNotify = async () => { try { const [userRes, receivedRes] = await Promise.all([ fetch("https://cdk.linux.do/api/v1/oauth/user-info", { credentials: "include" }), fetch("https://cdk.linux.do/api/v1/projects/received?current=1&size=20&search=", { credentials: "include" }) ]); const userData = userRes.ok ? await userRes.json() : null; const receivedData = receivedRes.ok ? await receivedRes.json() : null; if (!userData?.data) return; const cacheData = { user: userData.data, received: receivedData?.data || null }; if (typeof GM_setValue === "function") { GM_setValue("lda_cdk_cache", { data: cacheData, ts: Date.now() }); } try { window.parent?.postMessage({ type: "lda-cdk-data", payload: { data: cacheData } }, "*"); } catch (_) { } console.log("[CDK Bridge] \u6570\u636E\u5DF2\u7F13\u5B58:", cacheData); } catch (error) { console.error("[CDK Bridge] \u83B7\u53D6\u6570\u636E\u5931\u8D25:", error); } }; cacheAndNotify(); window.addEventListener("message", (event) => { if (event.data?.type === "lda-cdk-request") cacheAndNotify(); }); window[bridgeKey] = { refresh: cacheAndNotify }; return window[bridgeKey]; } // src/features/like-counter.js var LikeCounter = class { constructor() { this.CONFIG = { STORAGE_KEY: `linuxdo_likes_counter_${CURRENT_DOMAIN}`, SYNC_INTERVAL: 30 * 60 * 1e3, MAX_STORED_ITEMS: 500, LIMITS: { 0: 50, 1: 50, 2: 75, 3: 100, 4: 150 } }; this.state = { timestamps: [], cooldownUntil: 0, lastSync: 0, matched: true, userTrustLevel: null }; this.currentUser = null; this.accountKey = null; this.accountGeneration = 0; this.uiUpdateCallbacks = []; this.syncTimer = null; this.loadState(); this.installInterceptors(); this.startPeriodicSync(); } getAccountKey(user) { const id = Number(user?.id ?? user?.user_id); if (Number.isSafeInteger(id) && id > 0) return `id:${id}`; const username = String(user?.username || "").trim().replace(/^@/, "").trim().toLowerCase(); return username ? `username:${username}` : null; } getAccountStorageKey() { return this.accountKey ? `${this.CONFIG.STORAGE_KEY}_${encodeURIComponent(this.accountKey)}` : null; } createEmptyState() { return { timestamps: [], cooldownUntil: 0, lastSync: 0, matched: false, userTrustLevel: null }; } loadState() { this.state = this.createEmptyState(); const storageKey = this.getAccountStorageKey(); if (!storageKey) return; try { const stored = GM_getValue(storageKey, "{}"); const parsed = typeof stored === "string" ? JSON.parse(stored) : stored; this.state.timestamps = Array.isArray(parsed?.timestamps) ? parsed.timestamps.filter((ts) => Number.isFinite(ts) && ts > 0).slice(0, this.CONFIG.MAX_STORED_ITEMS) : []; this.state.lastSync = Number(parsed?.lastSync) || 0; this.state.matched = parsed?.matched === true; this.state.userTrustLevel = parsed?.userTrustLevel ?? null; } catch (e) { console.error("[LikeCounter] \u52A0\u8F7D\u72B6\u6001\u5931\u8D25:", e); } this.cleanOldEntries(); } saveState() { const storageKey = this.getAccountStorageKey(); if (!storageKey) return false; try { const { timestamps, lastSync, matched, userTrustLevel } = this.state; GM_setValue(storageKey, JSON.stringify({ timestamps, lastSync, matched, userTrustLevel })); return true; } catch (e) { console.error("[LikeCounter] \u4FDD\u5B58\u72B6\u6001\u5931\u8D25:", e); return false; } } cleanOldEntries() { const now = Date.now(); const cutoff = now - 24 * 60 * 60 * 1e3; this.state.cooldownUntil = this.accountKey ? LikeCooldown.read(this.accountKey) : 0; this.state.timestamps = this.state.timestamps.filter((ts) => ts > cutoff); this.state.timestamps.sort((a, b) => b - a); if (this.state.cooldownUntil > 0 && this.state.cooldownUntil < now) { const expectedBase = this.state.cooldownUntil - 24 * 60 * 60 * 1e3; const beforeCount = this.state.timestamps.length; this.state.timestamps = this.state.timestamps.filter( (ts) => ts < expectedBase || ts >= expectedBase + 5e3 ); if (this.state.timestamps.length < beforeCount) { this.checkAndUpdateMismatch(); } this.state.cooldownUntil = 0; } } checkAndUpdateMismatch() { const limit = this.getDailyLimit(); const count = this.state.timestamps.length; this.state.matched = count >= limit || this.state.lastSync === 0 || this.state.lastSync > 0 && count === 0; } getDailyLimit() { if (this.currentUser && this.CONFIG.LIMITS[this.currentUser.trust_level] !== void 0) { return this.CONFIG.LIMITS[this.currentUser.trust_level]; } if (this.state.userTrustLevel !== null && this.CONFIG.LIMITS[this.state.userTrustLevel] !== void 0) { return this.CONFIG.LIMITS[this.state.userTrustLevel]; } try { const username = this.currentUser?.username; if (username) { const cacheKey = `trustLevelCache_${CURRENT_DOMAIN}_${username}`; const cachedData = Storage.get(cacheKey, null); if (cachedData?.currentLevel !== void 0) { const level = parseInt(cachedData.currentLevel); if (this.CONFIG.LIMITS[level] !== void 0) { return this.CONFIG.LIMITS[level]; } } } } catch (e) { } return 50; } getRemainingLikes() { this.cleanOldEntries(); const limit = this.getDailyLimit(); const used = this.state.timestamps.length; return Math.max(0, limit - used); } getUsedLikes() { this.cleanOldEntries(); return this.state.timestamps.length; } isInCooldown() { return this.state.cooldownUntil > Date.now(); } getCooldownRemaining() { if (!this.isInCooldown()) return 0; return Math.max(0, this.state.cooldownUntil - Date.now()); } formatCooldown() { const diff = this.getCooldownRemaining(); if (!Number.isFinite(diff)) return "\u6682\u4E0D\u53EF\u7528"; if (diff <= 0) return null; const h = Math.floor(diff / 36e5); const m = Math.floor(diff % 36e5 / 6e4); const s = Math.floor(diff % 6e4 / 1e3); if (h > 0) { return `${h}\u5C0F\u65F6${String(m).padStart(2, "0")}\u5206${String(s).padStart(2, "0")}\u79D2`; } return `${String(m).padStart(2, "0")}\u5206${String(s).padStart(2, "0")}\u79D2`; } processToggleResponse(url, data, method = "PUT", accountKey = this.accountKey, accountGeneration = this.accountGeneration, status = 0) { if (!isHeartToggleUrl(url, method) || !accountKey || accountKey !== this.accountKey || accountGeneration !== this.accountGeneration) return; this.loadState(); const now = Date.now(); const succeeded = status >= 200 && status < 300; const hasReaction = data != null && Object.prototype.hasOwnProperty.call(data, "current_user_reaction"); if (!succeeded && (status === 429 || data?.error_type === "rate_limit")) { const waitSeconds = data?.extras?.wait_seconds || 0; if (waitSeconds > 0) { const cooldownUntil = now + waitSeconds * 1e3; this.state.cooldownUntil = LikeCooldown.extend(accountKey, cooldownUntil); console.log(`[LikeCounter] \u89E6\u53D1\u9650\u6D41\uFF0C\u51B7\u5374 ${waitSeconds} \u79D2`); } const limit = this.getDailyLimit(); const currentCount = this.state.timestamps.length; this.state.matched = currentCount >= limit; if (currentCount < limit && waitSeconds > 0) { const needed = limit - currentCount; const placeholderBaseTime = now + waitSeconds * 1e3 - 24 * 60 * 60 * 1e3; const safeNeeded = Math.min(needed, 200); for (let i = 0; i < safeNeeded; i++) { this.state.timestamps.push(placeholderBaseTime + i); } this.state.timestamps.sort((a, b) => b - a); } } else if (succeeded && (!hasReaction || !!data.current_user_reaction)) { this.state.timestamps.push(now); console.log(`[LikeCounter] \u8BB0\u5F55\u70B9\u8D5E\uFF0C\u5F53\u524D\u5DF2\u7528 ${this.state.timestamps.length}/${this.getDailyLimit()}`); } else { console.log("[LikeCounter] \u8BF7\u6C42\u5931\u8D25\u6216\u5FC3\u5F62\u53CD\u5E94\u672A\u6FC0\u6D3B\uFF0C\u5FFD\u7565\u672C\u6B21\u54CD\u5E94"); } this.saveState(); this.notifyUIUpdate(); } installInterceptors() { const self = this; this._likeInterceptorUnsubscribe?.(); this._likeInterceptorUnsubscribe = networkInterceptor.register({ beforeRequest(request) { if (!isHeartToggleUrl(request.url, request.method)) return null; return { accountKey: self.accountKey, accountGeneration: self.accountGeneration }; }, async afterResponse({ request, response, context }) { if (!context || !isHeartToggleUrl(request.url, request.method)) return; try { let data = null; if (request.transport === "fetch") { try { data = await response.clone().json(); } catch (_) { } } else { try { data = response.responseType === "json" ? response.response : JSON.parse(response.responseText); } catch (_) { } } self.processToggleResponse( request.url, data, request.method, context.accountKey, context.accountGeneration, response.status ); } catch (_) { } } }); console.log("[LikeCounter] \u62E6\u622A\u5668\u5DF2\u5B89\u88C5"); } async syncRemote(force = false) { if (!this.accountKey) { const discoveryGeneration = this.accountGeneration; let user = null; try { const currentUser = window.Discourse?.User?.current?.() || window.Discourse?.currentUser || window.User?.current?.(); if (this.getAccountKey(currentUser)) user = currentUser; } catch (e) { } if (!user) { try { const preloadData = document.getElementById("data-preloaded"); if (preloadData) { const data = JSON.parse(preloadData.dataset.preloaded); if (data?.currentUser) { const cu = typeof data.currentUser === "string" ? JSON.parse(data.currentUser) : data.currentUser; if (this.getAccountKey(cu)) user = cu; } } } catch (e) { } } if (!user) { const session429Until = Storage.get("session429Until", 0); if (session429Until > Date.now()) { const remainMinutes = Math.ceil((session429Until - Date.now()) / 6e4); console.log(`[LikeCounter] session/current 429 \u51B7\u5374\u671F\u4E2D\uFF0C\u5269\u4F59 ${remainMinutes} \u5206\u949F\uFF0C\u8DF3\u8FC7\u540C\u6B65`); return; } try { const response = await fetch(`${BASE_URL}/session/current.json`); if (response.status === 429) { console.warn("[LikeCounter] session/current \u9047\u5230 429\uFF0C\u8BBE\u7F6E 30 \u5206\u949F\u51B7\u5374"); Storage.set("session429Until", Date.now() + 30 * 60 * 1e3); return; } if (response.ok) { const data = await response.json(); if (this.getAccountKey(data.current_user)) user = data.current_user; } } catch (e) { console.error("[LikeCounter] \u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25:", e); } } if (discoveryGeneration !== this.accountGeneration || !user) return; this.setCurrentUser(user); } const accountKey = this.accountKey; const accountGeneration = this.accountGeneration; const username = this.currentUser?.username; if (!accountKey || !username) return; this.loadState(); if (!force) { const timeSinceLastSync = Date.now() - (this.state.lastSync || 0); const minSyncInterval = 30 * 60 * 1e3; if (timeSinceLastSync < minSyncInterval) { const remainMinutes = Math.ceil((minSyncInterval - timeSinceLastSync) / 6e4); console.log(`[LikeCounter] \u8DDD\u79BB\u4E0A\u6B21\u540C\u6B65\u4EC5 ${Math.floor(timeSinceLastSync / 6e4)} \u5206\u949F\uFF0C\u8DF3\u8FC7\u672C\u6B21\u540C\u6B65\uFF08\u5269\u4F59 ${remainMinutes} \u5206\u949F\uFF09`); this.notifyUIUpdate(); return; } } console.log(`[LikeCounter] \u5F00\u59CB\u540C\u6B65\u7528\u6237 ${username} \u7684\u70B9\u8D5E\u6570\u636E...`); try { const limit = this.getDailyLimit(); const cutoffTime = Date.now() - 24 * 60 * 60 * 1e3; console.log(`[LikeCounter] \u53EA\u8BFB\u53D6 API \u70B9\u8D5E\u5386\u53F2\uFF0C\u7A97\u53E3\u8D77\u70B9: ${new Date(cutoffTime).toLocaleString()}`); const reactions = await this.fetchUserActions(username, cutoffTime, accountKey, accountGeneration); if (accountKey !== this.accountKey || accountGeneration !== this.accountGeneration) return; const postMap = /* @__PURE__ */ new Map(); for (const item of reactions) { if (!postMap.has(item.post_id) || postMap.get(item.post_id) < item.timestamp) { postMap.set(item.post_id, item.timestamp); } } const dedupedTimestamps = Array.from(postMap.values()); console.log(`[LikeCounter] \u7528\u6237\u4FE1\u4EFB\u7B49\u7EA7: ${this.currentUser?.trust_level}, \u9650\u989D: ${limit}`); console.log(`[LikeCounter] \u4ECE API \u83B7\u53D6\u5230 ${reactions.length} \u6761\u53CD\u5E94\u8BB0\u5F55\uFF0C\u53BB\u91CD\u540E ${dedupedTimestamps.length} \u4E2A\u4E0D\u540C\u5E16\u5B50`); this.state.cooldownUntil = LikeCooldown.read(accountKey); this.state.timestamps = dedupedTimestamps; this.state.lastSync = Date.now(); this.state.matched = true; this.cleanOldEntries(); if (this.state.timestamps.length >= limit) { const oldestTs = Math.min(...this.state.timestamps); const estimatedCooldown = oldestTs + 24 * 60 * 60 * 1e3; if (estimatedCooldown > Date.now() && estimatedCooldown > this.state.cooldownUntil) { this.state.cooldownUntil = LikeCooldown.extend(accountKey, estimatedCooldown); console.log(`[LikeCounter] API \u6570\u636E\u8FBE\u5230\u9650\u989D\uFF0C\u4F30\u7B97\u51B7\u5374\u65F6\u95F4: ${new Date(estimatedCooldown).toLocaleString()}`); } } if (this.currentUser?.trust_level !== void 0) { this.state.userTrustLevel = this.currentUser.trust_level; } this.saveState(); this.notifyUIUpdate(); console.log(`[LikeCounter] \u540C\u6B65\u5B8C\u6210\uFF0C\u5DF2\u7528 ${this.state.timestamps.length}/${limit}`); } catch (e) { console.error("[LikeCounter] \u540C\u6B65\u5931\u8D25:", e); } } async fetchUserActions(username, cutoffTime, accountKey = this.accountKey, accountGeneration = this.accountGeneration) { const allItems = []; const cutoff = cutoffTime || Date.now() - 24 * 60 * 60 * 1e3; let offset = 0; let pages = 0; console.log(`[LikeCounter] \u5F00\u59CB\u83B7\u53D6 ${username} \u7684\u70B9\u8D5E\u5386\u53F2\uFF0C\u7A97\u53E3\u8D77\u70B9: ${new Date(cutoff).toLocaleString()}`); while (pages < 5) { if (accountKey !== this.accountKey || accountGeneration !== this.accountGeneration) break; try { const url = `${BASE_URL}/user_actions.json?limit=50&username=${username}&filter=1&offset=${offset}`; const response = await RequestManager.run( `user-actions-${CURRENT_DOMAIN}-${username}`, async () => { const r = await fetch(url, { credentials: "include" }); if (r.status === 429) throw new Error("429 user_actions"); return r; }, { minInterval: 1500, retries: 1, backoffBase: 1200, cooldownKey: `userActions429Until_${CURRENT_DOMAIN}_${username}`, cooldownMs: 20 * 60 * 1e3 } ); if (!response || accountKey !== this.accountKey || accountGeneration !== this.accountGeneration) break; const res = await response.json(); if (accountKey !== this.accountKey || accountGeneration !== this.accountGeneration) break; const items = res.user_actions || []; if (!items.length) { console.log(`[LikeCounter] \u6CA1\u6709\u66F4\u591A\u6570\u636E\uFF0C\u7ED3\u675F\u83B7\u53D6`); break; } let hasOld = false; for (const item of items) { const t = new Date(item.created_at).getTime(); if (t > cutoff) { allItems.push({ post_id: item.post_id, timestamp: t }); } else { hasOld = true; } } if (hasOld || items.length < 50) { console.log(`[LikeCounter] ${hasOld ? "\u9047\u5230\u7A97\u53E3\u5916\u7684\u65E7\u6570\u636E" : "\u6570\u636E\u4E0D\u8DB350\u6761"}\uFF0C\u7ED3\u675F\u83B7\u53D6`); break; } offset += 50; pages++; await Utils.sleep(Utils.random(350, 900)); } catch (e) { console.error(`[LikeCounter] \u83B7\u53D6\u70B9\u8D5E\u5386\u53F2\u51FA\u9519:`, e); break; } } console.log(`[LikeCounter] \u70B9\u8D5E\u5386\u53F2\u83B7\u53D6\u5B8C\u6210\uFF0C\u5171 ${allItems.length} \u6761`); return allItems; } startPeriodicSync() { setTimeout(() => this.syncRemote(), 3e3); this.syncTimer = setInterval(() => { this.syncRemote(); }, this.CONFIG.SYNC_INTERVAL); } setCurrentUser(user) { const accountKey = this.getAccountKey(user); this.currentUser = accountKey ? { ...user, username: String(user?.username || "").trim().replace(/^@/, "").trim() } : null; if (accountKey !== this.accountKey) { this.accountKey = accountKey; this.accountGeneration++; this.loadState(); } this.notifyUIUpdate(); } onUIUpdate(callback) { this.uiUpdateCallbacks.push(callback); } notifyUIUpdate() { for (const callback of this.uiUpdateCallbacks) { try { callback(this.getStatus()); } catch (e) { console.error("[LikeCounter] UI\u66F4\u65B0\u56DE\u8C03\u9519\u8BEF:", e); } } } getStatus() { this.cleanOldEntries(); return { remaining: this.getRemainingLikes(), used: this.getUsedLikes(), limit: this.getDailyLimit(), isInCooldown: this.isInCooldown(), cooldownRemaining: this.getCooldownRemaining(), cooldownFormatted: this.formatCooldown(), cooldownUntil: this.state.cooldownUntil, matched: this.state.matched, lastSync: this.state.lastSync }; } manualSync() { return this.syncRemote(true); } }; // src/features/user-info.js var UserInfoHelper = class { constructor() { this.userInfoCache = /* @__PURE__ */ new Map(); this.pendingRequests = /* @__PURE__ */ new Map(); this.DAY_IN_MS = 24 * 60 * 60 * 1e3; this.isEnabled = true; this.observer = null; this.init(); } init() { if (!this.isEnabled) return; if (this.observer) { this.observer.disconnect(); } const debouncedEnhance = this.debounce(() => { if (!this.isEnabled || document.hidden) return; if (typeof requestIdleCallback === "function") { requestIdleCallback(() => this.isEnabled && this.enhanceUserInfo(), { timeout: 1200 }); } else { setTimeout(() => this.isEnabled && this.enhanceUserInfo(), 120); } }, 600); this.observer = new MutationObserver((mutations) => { if (!this.isEnabled || document.hidden) return; const hasRelevantMutation = mutations.some( (mutation) => Array.from(mutation.addedNodes || []).some((node) => { if (node.nodeType !== 1) return false; if (node.closest && node.closest("#my-gift-panel")) return false; return node.matches?.(".topic-post, article, .post-stream") || node.querySelector?.(".topic-post article, .names a[data-user-card]"); }) ); if (hasRelevantMutation) { debouncedEnhance(); } }); this.observer.observe(document.body, { childList: true, subtree: true }); this.enhanceUserInfo(); } debounce(func, wait) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; } isTopicPage() { return window.location.pathname.includes("/t/topic/"); } async enhanceUserInfo() { if (!this.isTopicPage()) return; const articles = document.querySelectorAll(".topic-post article"); for (const article of articles) { const anchor = article.querySelector(".names a[data-user-card]"); if (!anchor) continue; const slug = anchor.getAttribute("data-user-card"); if (!slug) continue; const normalizedSlug = slug.trim().toLowerCase(); if (article.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`)) { continue; } const postWrapper = article.closest(".topic-post"); const postNumber = postWrapper?.getAttribute("data-post-number"); const isFirstPost = postNumber === "1"; if (isFirstPost) { await this.loadAndDisplayUserInfo(anchor, slug, normalizedSlug); } else { this.addInfoButton(anchor, slug, normalizedSlug); } } } addInfoButton(anchor, rawSlug, normalizedSlug) { const namesContainer = anchor.closest(".names"); if (!namesContainer) return; if (namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`)) { return; } if (namesContainer.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`)) { return; } const button = document.createElement("button"); button.className = "user-info-btn"; button.setAttribute("data-user", normalizedSlug); button.setAttribute("data-raw-slug", rawSlug); button.textContent = "\u{1F4CA}"; button.title = "\u70B9\u51FB\u67E5\u770B\u7528\u6237\u6CE8\u518C\u4FE1\u606F"; button.style.cssText = ` margin-left: 6px; font-size: 14px; cursor: pointer; background: none; border: none; padding: 2px 4px; opacity: 0.6; transition: opacity 0.2s; vertical-align: middle; `; button.addEventListener("mouseenter", () => { button.style.opacity = "1"; }); button.addEventListener("mouseleave", () => { button.style.opacity = "0.6"; }); button.addEventListener("click", async (e) => { e.preventDefault(); e.stopPropagation(); if (button.disabled) return; button.disabled = true; button.textContent = "\u23F3"; try { await this.loadAndDisplayUserInfo(anchor, rawSlug, normalizedSlug); } catch (error) { console.error("\u52A0\u8F7D\u7528\u6237\u4FE1\u606F\u5931\u8D25:", error); button.textContent = "\u{1F4CA}"; button.disabled = false; } }); anchor.insertAdjacentElement("afterend", button); this.addTopicsButton(anchor, rawSlug, normalizedSlug); } addTopicsButton(anchor, rawSlug, normalizedSlug) { const namesContainer = anchor.closest(".names"); if (!namesContainer) return; if (namesContainer.querySelector(`.user-topics-btn[data-user="${normalizedSlug}"]`)) { return; } const topicsBtn = document.createElement("a"); topicsBtn.className = "user-topics-btn"; topicsBtn.setAttribute("data-user", normalizedSlug); topicsBtn.href = `${BASE_URL}/u/${rawSlug}/activity/topics`; topicsBtn.target = "_blank"; topicsBtn.textContent = "\u67E5\u770B\u8BDD\u9898"; topicsBtn.title = "\u67E5\u770B\u8BE5\u7528\u6237\u7684\u8BDD\u9898"; topicsBtn.style.cssText = ` margin-left: 6px; font-size: 12px; cursor: pointer; text-decoration: none; padding: 2px 6px; opacity: 0.7; transition: all 0.2s; vertical-align: middle; display: inline-block; color: #667eea; background: rgba(102, 126, 234, 0.1); border-radius: 4px; `; topicsBtn.addEventListener("mouseenter", () => { topicsBtn.style.opacity = "1"; topicsBtn.style.background = "rgba(102, 126, 234, 0.2)"; }); topicsBtn.addEventListener("mouseleave", () => { topicsBtn.style.opacity = "0.7"; topicsBtn.style.background = "rgba(102, 126, 234, 0.1)"; }); const infoBtn = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (infoBtn) { infoBtn.insertAdjacentElement("afterend", topicsBtn); } else { anchor.insertAdjacentElement("afterend", topicsBtn); } } async loadAndDisplayUserInfo(anchor, slug, normalizedSlug) { const namesContainer = anchor.closest(".names"); if (!namesContainer) return; const existingInfo = namesContainer.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`); if (existingInfo) { console.log(`\u7528\u6237 ${normalizedSlug} \u4FE1\u606F\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7`); const button2 = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button2) button2.remove(); return; } const info = await this.fetchUserInfo(slug, normalizedSlug); if (!info) { const button2 = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button2) { button2.textContent = "\u{1F4CA}"; button2.disabled = false; } return; } const infoNode = this.buildInfoNode(info, normalizedSlug); if (!infoNode) { const button2 = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button2) { button2.textContent = "\u{1F4CA}"; button2.disabled = false; } return; } const finalCheck = namesContainer.querySelector(`.user-reg-info[data-user="${normalizedSlug}"]`); if (finalCheck) { console.log(`\u7528\u6237 ${normalizedSlug} \u4FE1\u606F\u5728\u7B49\u5F85\u671F\u95F4\u5DF2\u88AB\u6DFB\u52A0\uFF0C\u8DF3\u8FC7`); const button2 = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button2) button2.remove(); return; } const button = namesContainer.querySelector(`.user-info-btn[data-user="${normalizedSlug}"]`); if (button) button.remove(); anchor.insertAdjacentElement("afterend", infoNode); if (!namesContainer.querySelector(`.user-topics-btn[data-user="${normalizedSlug}"]`)) { this.addTopicsButton(anchor, slug, normalizedSlug); } } async fetchUserInfo(slug, normalizedSlug) { if (this.userInfoCache.has(normalizedSlug)) { return this.userInfoCache.get(normalizedSlug); } if (this.pendingRequests.has(normalizedSlug)) { return this.pendingRequests.get(normalizedSlug); } const requestPromise = this.doFetchUserInfo(slug, normalizedSlug); this.pendingRequests.set(normalizedSlug, requestPromise); try { const info = await requestPromise; if (info) { this.userInfoCache.set(normalizedSlug, info); } return info; } finally { this.pendingRequests.delete(normalizedSlug); } } async doFetchUserInfo(slug, normalizedSlug) { try { const PROFILE_API_BUILDERS = [ (s) => `${BASE_URL}/u/${encodeURIComponent(s)}.json`, (s) => `${BASE_URL}/users/${encodeURIComponent(s)}.json` ]; const SUMMARY_API_BUILDERS = [ (s) => `${BASE_URL}/u/${encodeURIComponent(s)}/summary.json`, (s) => `${BASE_URL}/users/${encodeURIComponent(s)}/summary.json` ]; const [profileData, summaryData] = await Promise.all([ this.fetchFirstAvailable(PROFILE_API_BUILDERS, slug), this.fetchFirstAvailable(SUMMARY_API_BUILDERS, slug) ]); if (!profileData && !summaryData) { return null; } const user = profileData && (profileData.user || profileData); const summary = summaryData && (summaryData.user_summary || summaryData.summary || summaryData); const createdAt = this.pickCreatedAt(user) || summary && this.pickCreatedAt(summary); if (!createdAt) { return null; } const topicCount = this.pickFirstNumber( user && (user.topic_count ?? user.topicCount), summary && (summary.topic_count ?? summary.topics_count) ); const totalPostCount = this.pickFirstNumber( user && (user.post_count ?? user.postCount), summary && (summary.post_count ?? summary.posts_count) ); let repliesCount = this.pickFirstNumber( summary && (summary.replies_count ?? summary.reply_count) ); if (repliesCount === null && totalPostCount !== null && topicCount !== null) { repliesCount = Math.max(0, totalPostCount - topicCount); } const trustLevelRaw = this.pickFirstValue( user && (user.trust_level ?? user.trustLevel), summary && (summary.trust_level ?? summary.trustLevel) ); const trustLevel = this.normalizeTrustLevel(trustLevelRaw); const days = this.calcDays(createdAt); return { slug: normalizedSlug, createdAt, days, topicCount: typeof topicCount === "number" && Number.isFinite(topicCount) ? topicCount : void 0, repliesCount: typeof repliesCount === "number" && Number.isFinite(repliesCount) ? repliesCount : void 0, trustLevel }; } catch (error) { console.error("\u83B7\u53D6\u7528\u6237\u4FE1\u606F\u5931\u8D25:", slug, error); return null; } } async fetchFirstAvailable(builders, slug) { for (const builder of builders) { const url = builder(slug); const data = await this.safeFetchJson(url); if (data) { return data; } } return null; } async safeFetchJson(url) { try { const response = await RequestManager.run( `safe-fetch-${CURRENT_DOMAIN}-${url}`, async () => { const r = await fetch(url, { credentials: "include" }); if (r.status === 429) throw new Error(`429 ${url}`); return r; }, { minInterval: 1200, retries: 1, backoffBase: 1e3, cooldownKey: `safeFetch429Until_${CURRENT_DOMAIN}`, cooldownMs: 20 * 60 * 1e3 } ); if (!response || !response.ok) { return null; } return await response.json(); } catch (error) { return null; } } pickFirstNumber(...values) { for (const value of values) { const numberValue = Number(value); if (!Number.isNaN(numberValue)) { return numberValue; } } return null; } pickFirstValue(...values) { for (const value of values) { if (value !== void 0 && value !== null) { return value; } } return null; } normalizeTrustLevel(raw) { if (raw === void 0 || raw === null) { return void 0; } if (typeof raw === "number" && Number.isFinite(raw)) { return raw; } if (typeof raw === "string") { const TRUST_LEVEL_ALIAS = { newuser: 0, basic: 1, member: 2, regular: 3, leader: 4 }; const alias = TRUST_LEVEL_ALIAS[raw.toLowerCase()]; if (alias !== void 0) { return alias; } const numeric = Number(raw); if (!Number.isNaN(numeric)) { return numeric; } } return void 0; } pickCreatedAt(source) { if (!source) { return null; } return source.created_at || source.createdAt || source.registration_date || source.registrationDate || source.joined || source.joinedAt || null; } calcDays(createdAt) { const createdTime = new Date(createdAt).getTime(); if (Number.isNaN(createdTime)) { return 0; } const diff = Date.now() - createdTime; return Math.max(0, Math.floor(diff / this.DAY_IN_MS)); } buildInfoNode(info, normalizedSlug) { const segments = [`\u6CE8\u518C ${this.formatNumber(info.days)} \u5929`]; if (typeof info.topicCount === "number" && Number.isFinite(info.topicCount)) { segments.push(`\u53D1\u5E16 ${this.formatNumber(info.topicCount)}`); } if (typeof info.repliesCount === "number" && Number.isFinite(info.repliesCount)) { segments.push(`\u56DE\u5E16 ${this.formatNumber(info.repliesCount)}`); } if (typeof info.trustLevel === "number" && Number.isFinite(info.trustLevel)) { const FULL_TRUST_LEVEL_LABELS = { 0: "Lv0 \u65B0\u624B", 1: "Lv1 \u5165\u95E8", 2: "Lv2 \u6210\u5458", 3: "Lv3 \u5E38\u9A7B", 4: "Lv4 \u9886\u8896" }; const label = FULL_TRUST_LEVEL_LABELS[info.trustLevel] || `\u4FE1\u4EFB\u7EA7\u522B Lv${info.trustLevel}`; segments.push(label); } if (!segments.length) { return null; } const span = document.createElement("span"); span.className = "user-reg-info"; span.setAttribute("data-user", normalizedSlug); span.textContent = ` \xB7 ${segments.join(" \xB7 ")}`; span.style.cssText = ` margin-left: 6px; font-size: 12px; color: #1a4c7c; `; return span; } formatNumber(value) { return Number(value).toLocaleString("zh-CN"); } }; // src/controller/mixins/panel.js var methods = { applyTopicAgeHighlightColors() { const root = document.documentElement; root.style.setProperty("--lda-topic-age-fresh", "#86efac"); root.style.setProperty("--lda-topic-age-old", "#fdba74"); root.style.setProperty("--lda-topic-age-ancient", "#fca5a5"); root.style.setProperty("--lda-topic-age-enabled", this.topicAgeColorEnabled ? "1" : "0"); }, createTopicAgeColorSettings() { const section = document.createElement("div"); section.className = "topic-created-time-settings"; const visibilityToggleRow = this.createToggleRow( this.t("topicCreatedTimeVisible"), this.topicCreatedTimeVisible, (checked) => { this.topicCreatedTimeVisible = checked; Storage.set("topicCreatedTimeVisible", checked); this.renderTopicCreatedTimeInList(); this.showNotification(checked ? this.t("topicCreatedTimeEnabled") : this.t("topicCreatedTimeDisabled")); } ); visibilityToggleRow.title = this.t("topicCreatedTimeVisibleTip"); section.appendChild(visibilityToggleRow); const toggleRow = this.createToggleRow( this.t("topicAgeColorLabel"), this.topicAgeColorEnabled, (checked) => { this.topicAgeColorEnabled = checked; Storage.set("topicAgeColorEnabled", checked); this.applyTopicAgeHighlightColors(); this.renderTopicCreatedTimeInList(); this.showNotification(checked ? this.t("topicAgeColorEnabled") : this.t("topicAgeColorDisabled")); } ); toggleRow.title = this.t("topicAgeColorTip"); section.appendChild(toggleRow); return section; }, createInnerCollapsibleSection(title, contentNode, collapsed = true, headerControl = null) { const wrapper = document.createElement("div"); wrapper.className = `settings-inner-collapsible${collapsed ? " collapsed" : ""}`; const header = document.createElement("div"); header.className = "settings-inner-collapsible-header"; const titleElement = document.createElement("span"); titleElement.textContent = title; const collapseIcon = document.createElement("span"); collapseIcon.className = "collapse-icon"; collapseIcon.setAttribute("aria-hidden", "true"); header.appendChild(titleElement); if (headerControl) { const controls = document.createElement("span"); controls.className = "settings-inner-collapsible-controls"; headerControl.addEventListener("click", (event) => event.stopPropagation()); controls.appendChild(headerControl); controls.appendChild(collapseIcon); header.appendChild(controls); } else { header.appendChild(collapseIcon); } const body = document.createElement("div"); body.className = "settings-inner-collapsible-content"; if (contentNode) body.appendChild(contentNode); header.addEventListener("click", () => wrapper.classList.toggle("collapsed")); wrapper.appendChild(header); wrapper.appendChild(body); return wrapper; }, setupButton() { this.container = document.createElement("div"); this.container.id = "my-gift-panel"; this.container.className = "collapsed"; this.container.innerHTML = `
${this.t("panelTitle")}
\xD7
`; GM_addStyle(` #my-gift-panel { position: fixed !important; right: 20px; bottom: 20px; z-index: 2147483647 !important; width: fit-content; min-width: 280px; max-width: 450px; background: #fff !important; border: 1px solid #e0e0e0 !important; border-radius: 12px; box-shadow: 0 8px 28px rgba(0, 0, 0, .14); max-height: calc(100vh - 40px); box-sizing: border-box; overflow-x: hidden; overflow-y: auto; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Microsoft YaHei", Arial, sans-serif; color: #333; line-height: normal !important; transform-origin: bottom right; transition: width .22s ease, height .22s ease, border-radius .22s ease, box-shadow .22s ease; user-select: none; } #my-gift-panel.collapsed { width: 50px !important; height: 50px !important; min-width: 50px !important; max-width: 50px !important; padding: 0 !important; border-radius: 50% !important; display: flex !important; justify-content: center; align-items: center; cursor: grab; background: #fff !important; color: #0078d4 !important; border: 1px solid #e0e0e0 !important; box-shadow: 0 4px 16px rgba(0, 120, 212, .30) !important; transition: width .22s ease, height .22s ease, transform .18s ease, box-shadow .18s ease; } #my-gift-panel.collapsed:hover { transform: scale(1.08); box-shadow: 0 7px 22px rgba(0, 120, 212, .40) !important; } #my-gift-panel.dragging { cursor: grabbing !important; transition: none !important; transform: none !important; box-shadow: 0 10px 28px rgba(0, 120, 212, .35) !important; } #my-gift-panel.collapsed > #my-gift-icon { display: block !important; width: 31px !important; height: 31px !important; margin: 0 !important; padding: 0 !important; fill: currentColor !important; pointer-events: none !important; flex-shrink: 0; } #my-gift-panel:not(.collapsed) { width: fit-content !important; min-width: 280px; max-width: 450px; min-height: 180px; } #my-gift-panel:not(.collapsed) > #my-gift-icon { display: none !important; } #my-gift-content { width: 100%; min-width: 0; } #my-gift-panel.collapsed #my-gift-content { display: none !important; } #my-gift-header { height: 52px; padding: 0 10px 0 16px; display: flex; align-items: center; justify-content: space-between; background: linear-gradient( 135deg, #f8fbfe, #f4f7fa ) !important; border-bottom: 1px solid #eeeeee; box-shadow: 0 1px 0 rgba(0, 0, 0, .01); user-select: none; } #my-gift-title { display: flex; align-items: center; font-size: 14px; font-weight: 600; color: #30343b; } #my-gift-close { width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%; color: #8a9199; font-size: 21px; line-height: 1; cursor: pointer; transition: background .18s ease, color .18s ease, transform .18s ease; } #my-gift-close:hover { background: #e9edf1; color: #343a40; transform: rotate(90deg); } #my-gift-body { padding: 8px 12px; min-width: 0; } .my-gift-tool { width: 100%; min-height: 41px; margin: 0; padding: 0 12px; display: flex; align-items: center; gap: 0; border: 0 !important; border-radius: 0 !important; background: #fff !important; color: #333 !important; font-family: inherit; font-size: 13px; text-align: left; cursor: pointer; outline: none; transition: background .18s ease, color .18s ease; } .my-gift-tool:hover { background: #f8fbfe !important; } .my-gift-tool:active { background: #f1f6fa !important; } .my-gift-tool-text { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .my-gift-tool-arrow { color: #a0a7af; font-size: 16px; flex-shrink: 0; transition: transform .18s ease; } .my-gift-tool[aria-expanded="true"] .my-gift-tool-arrow { transform: rotate(90deg); } .my-gift-tool-item { margin-bottom: 8px; min-width: 0; overflow: hidden; border: 1px solid #e1e6eb; border-radius: 9px; background: #fff; } .my-gift-tool-item:last-child { margin-bottom: 0; } .my-gift-tool-detail { display: none; padding: 10px 0; min-width: 0; border: 0; border-top: 1px solid #e1e6eb; border-radius: 0; background: #f5f8fb; color: #8a9199; font-size: 12px; line-height: 1.5; } .my-gift-tool[aria-expanded="true"] { border-radius: 0 !important; } .my-gift-tool[aria-expanded="true"] + .my-gift-tool-detail { display: block; } #my-gift-panel .account-data-detail { padding: 4px 0 8px; } #my-gift-panel .account-data-sections > .my-gift-tool-item { margin: 0; overflow: visible; border: 0; border-radius: 0; background: transparent; } #my-gift-panel .account-data-sections > .my-gift-tool-item > .my-gift-tool { min-height: 34px; padding: 0 12px; border-radius: 0 !important; background: transparent !important; } #my-gift-panel .account-data-sections > .my-gift-tool-item > .my-gift-tool:hover { background: #edf4f9 !important; } #my-gift-panel .account-data-sections > .my-gift-tool-item > .my-gift-tool-detail { margin: 0 8px 6px; padding: 0; border: 0; background: transparent; } #my-gift-panel .account-data-sections > .my-gift-tool-item:last-child > .my-gift-tool-detail { margin-bottom: 0; } @media screen and (max-width: 400px) { #my-gift-panel:not(.collapsed) { width: calc(100vw - 30px) !important; min-width: min(220px, calc(100vw - 30px)); max-width: calc(100vw - 30px) !important; } #my-gift-panel.collapsed { right: 10px !important; bottom: 10px !important; } } #my-gift-panel .my-gift-tool-item { user-select: none; } #my-gift-panel .toggle-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 34px; margin: 0 0 6px; padding: 7px 8px; box-sizing: border-box; border: 1px solid #e1e6eb; border-radius: 7px; background: #fff; color: #333; } #my-gift-panel .toggle-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .toggle-label, #my-gift-panel .section-title, #my-gift-panel .trust-level-header, #my-gift-panel .trust-level-name, #my-gift-panel .trust-level-loading { color: #333; } #my-gift-panel .toggle-switch { position: relative; display: inline-block; width: 34px; height: 18px; flex-shrink: 0; } #my-gift-panel .toggle-switch input { width: 0; height: 0; opacity: 0; } #my-gift-panel .toggle-slider { position: absolute; inset: 0; border-radius: 18px; background: #cbd5df; cursor: pointer; transition: background .18s ease; } #my-gift-panel .toggle-slider:before { position: absolute; width: 14px; height: 14px; left: 2px; top: 2px; border-radius: 50%; background: #fff; content: ''; transition: transform .18s ease; } #my-gift-panel .toggle-switch input:checked + .toggle-slider { background: #0078d4; } #my-gift-panel .toggle-switch input:checked + .toggle-slider:before { transform: translateX(16px); } #my-gift-panel .toggle-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; margin-top: 8px; } #my-gift-panel .toggle-grid .toggle-row { margin: 0; } @media screen and (max-width: 400px) { #my-gift-panel .toggle-grid.smart-like-controls { grid-template-columns: minmax(0, 1fr); } } #my-gift-panel .main-action-btn, #my-gift-panel .read-action-btn, #my-gift-panel .reveal-users-btn { width: 100%; min-height: 38px; margin: 0 0 6px; padding: 8px 12px; border: 1px solid #d7e1e9; border-radius: 7px; background: #0078d4; color: #fff; font: inherit; cursor: pointer; } #my-gift-panel .main-action-btn.running { background: #d9534f; } #my-gift-panel .main-action-btn:hover, #my-gift-panel .read-action-btn:hover, #my-gift-panel .reveal-users-btn:hover { filter: brightness(.96); } #my-gift-panel .read-stats-container, #my-gift-panel .like-counter-container, #my-gift-panel .trust-level-row, #my-gift-panel .credit-info-row, #my-gift-panel .cdk-info-row, #my-gift-panel .rank-data-container, #my-gift-panel .topic-status-container { box-sizing: border-box; margin-top: 6px; padding: 8px; border: 1px solid #e1e6eb; border-radius: 8px; background: #f5f8fb; color: #333; } #my-gift-panel .cdk-info-row .credit-main-stat { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 8px; padding: 8px 10px; border: 1px solid #cfe3f1 !important; border-radius: 7px; background: #eaf4fb !important; } #my-gift-panel .cdk-info-row { margin-top: 0; padding: 0; border: 0; border-radius: 0; background: transparent; } #my-gift-panel .cdk-panel-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; min-height: 27px; margin-bottom: 8px; } #my-gift-panel .cdk-panel-title { min-width: 0; overflow: hidden; color: #334155; font-size: 12px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .cdk-panel-refresh, #my-gift-panel .cdk-received-refresh-btn { display: inline-flex; align-items: center; justify-content: center; width: 27px; min-width: 27px; height: 27px; min-height: 27px; padding: 0; border-radius: 6px; font-size: 15px; line-height: 1; } #my-gift-panel .cdk-score-card { display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: 10px; height: 50px; min-height: 50px; margin: 0 0 8px; padding: 6px 10px; box-sizing: border-box; border: 1px solid #cfe3f1; border-radius: 6px; background: #eaf4fb; text-align: left; } #my-gift-panel .cdk-score-card .credit-stat-label { min-width: 0; overflow: hidden; color: #64748b !important; font-size: 12px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .cdk-score-card .cdk-score-value { display: block; flex: 0 0 auto; margin-top: 0; color: #0783b5 !important; font-size: 32px; font-weight: 700; line-height: 1; text-shadow: none !important; } #my-gift-panel .cdk-account-section { min-width: 0; } #my-gift-panel .cdk-section-heading { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; margin: 0 0 3px; color: #64748b; font-size: 10px; font-weight: 600; line-height: 1.3; } #my-gift-panel .cdk-account-list { min-width: 0; } #my-gift-panel .cdk-account-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; margin: 0; padding: 5px 0; border-bottom: 1px solid #edf2f7; color: #475569; font-size: 11px; line-height: 1.4; } #my-gift-panel .cdk-account-item:last-child { border-bottom: 0; } #my-gift-panel .cdk-account-label { flex: 1 1 auto; min-width: 0; color: #64748b; } #my-gift-panel .cdk-account-value { display: inline-flex; align-items: center; justify-content: flex-end; flex: 0 0 auto; min-width: 0; gap: 5px; color: #334155; font-weight: 600; text-align: right; white-space: nowrap; } #my-gift-panel .cdk-account-value.cdk-account-name-value { flex: 0 1 72%; max-width: 72%; overflow: hidden; } #my-gift-panel .cdk-account-name, #my-gift-panel .cdk-account-username { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .cdk-account-username { color: #94a3b8; font-size: 10px; font-weight: 400; } #my-gift-panel .cdk-account-value.cdk-trust-level-value { color: #0078d4 !important; } #my-gift-panel .cdk-info-row .cdk-score-description { display: flex; align-items: center; box-sizing: border-box; margin-top: 6px; min-height: 32px; padding: 4px 2px; border: 0; border-top: 1px solid #e1e6eb; border-radius: 0; background: transparent; color: #64748b; font-size: 12px; font-weight: 500; line-height: 1.4; text-align: left; } #my-gift-panel .cdk-info-row .cdk-received-section { margin-top: 7px; padding-top: 7px; border-top: 1px solid #e1e6eb; } #my-gift-panel .cdk-info-row .cdk-received-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; min-height: 27px; margin-bottom: 6px; } #my-gift-panel .cdk-received-title { min-width: 0; overflow: hidden; color: #475569; font-size: 11px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .cdk-received-limit { color: #94a3b8; font-size: 10px; font-weight: 400; } #my-gift-panel .cdk-info-row .cdk-received-total { margin: 0 0 5px; color: #94a3b8; font-size: 10px; } #my-gift-panel .cdk-info-row .cdk-received-items { max-height: 200px; padding-right: 2px; overflow-y: auto; } #my-gift-panel .cdk-info-row .cdk-received-item { margin-bottom: 5px; padding: 7px 8px; border-color: #e1e6eb; border-radius: 6px; background: #fff; } #my-gift-panel .cdk-info-row .cdk-received-item:last-child { margin-bottom: 0; } #my-gift-panel .cdk-info-row .cdk-content-row { border: 1px solid #edf2f7; background: #f8fafc; } #my-gift-panel .cdk-info-row .cdk-copy-btn { display: inline-flex; align-items: center; justify-content: center; width: 26px; min-width: 26px; height: 24px; min-height: 24px; padding: 0; border: 1px solid #b9d9ef; border-radius: 4px; background: #eaf4fb; color: #0078d4; font-size: 14px; line-height: 1; } #my-gift-panel .cdk-info-row .cdk-copy-btn:hover { border-color: #8fc1e4; background: #dceefa; } #my-gift-panel .cdk-info-row .cdk-copy-btn.copied { border-color: #b7e4c7; background: #eaf8ef; color: #15803d; } #my-gift-panel .cdk-auth-state { display: flex; flex-direction: column; gap: 9px; padding: 2px 0 0; } #my-gift-panel .cdk-auth-status { display: inline-flex; align-items: center; gap: 6px; color: #334155; font-size: 12px; font-weight: 600; } #my-gift-panel .cdk-auth-dot { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: #94a3b8; } #my-gift-panel .cdk-auth-message { margin: 0; color: #64748b; font-size: 11px; line-height: 1.5; } #my-gift-panel .cdk-auth-actions { display: flex; align-items: center; justify-content: flex-start; gap: 8px; padding-top: 8px; border-top: 1px solid #e1e6eb; } #my-gift-panel .cdk-info-row .credit-stat-label { color: #64748b !important; font-size: 11px; } #my-gift-panel .cdk-info-row .cdk-score-value { color: #0891b2 !important; font-size: 32px; font-weight: 700; line-height: 1; text-shadow: none !important; } #my-gift-panel .cdk-info-row > .trust-level-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; margin: 4px 0; padding: 3px 0; color: #475569 !important; font-size: 11px; line-height: 1.35; } #my-gift-panel .cdk-info-row > .trust-level-item .trust-level-name { min-width: 0; color: #475569 !important; } #my-gift-panel .cdk-info-row > .trust-level-item .trust-level-value { flex-shrink: 0; color: #334155 !important; text-align: right; white-space: nowrap; } #my-gift-panel .cdk-info-row > .trust-level-item .cdk-trust-level-value { color: #0078d4 !important; font-weight: 700; } #my-gift-panel .cdk-score-description { margin-top: 8px; padding: 6px 8px; border: 1px solid #e1e6eb; border-radius: 6px; background: #f8fafc; color: #475569; font-size: 11px; font-weight: 500; line-height: 1.5; } #my-gift-panel .cdk-received-section { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e1e6eb; } #my-gift-panel .cdk-received-header { margin-bottom: 8px; } #my-gift-panel .cdk-recent-limit { color: #94a3b8; font-size: 10px; } #my-gift-panel .cdk-received-list { color: #475569; font-size: 11px; } #my-gift-panel .cdk-received-loading, #my-gift-panel .cdk-received-empty { padding: 10px; color: #64748b; text-align: center; } #my-gift-panel .cdk-received-extra-tip { color: #94a3b8; font-size: 9px; } #my-gift-panel .cdk-received-total { margin-bottom: 8px; color: #64748b; font-size: 10px; } #my-gift-panel .cdk-received-items { max-height: 200px; overflow-y: auto; } #my-gift-panel .cdk-received-item { box-sizing: border-box; margin-bottom: 6px; padding: 8px; border: 1px solid #e1e6eb; border-radius: 6px; background: #fff; } #my-gift-panel .cdk-received-item-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; margin-bottom: 4px; } #my-gift-panel .cdk-project-name { min-width: 0; overflow: hidden; color: #0078d4; font-size: 11px; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .cdk-received-time { flex-shrink: 0; color: #64748b; font-size: 9px; } #my-gift-panel .cdk-received-creator { margin-bottom: 4px; color: #64748b; font-size: 10px; } #my-gift-panel .cdk-content-row { display: flex; align-items: center; gap: 6px; padding: 4px 8px; border-radius: 4px; background: #f1f5f9; } #my-gift-panel .cdk-content { flex: 1; min-width: 0; color: #a16207; font-family: monospace; font-size: 11px; word-break: break-all; } #my-gift-panel .cdk-copy-btn { flex-shrink: 0; padding: 2px 8px; border: 1px solid #b9d9ef; border-radius: 4px; background: #eaf4fb; color: #0078d4; font-size: 10px; cursor: pointer; transition: background .18s ease, border-color .18s ease; white-space: nowrap; } #my-gift-panel .cdk-copy-btn:hover { border-color: #8fc1e4; background: #dceefa; } #my-gift-panel .cdk-auth-tip { margin-bottom: 8px; color: #64748b; font-size: 11px; text-align: center; } #my-gift-panel .cdk-auth-block { margin-top: 10px; text-align: center; } #my-gift-panel .trust-level-item, #my-gift-panel .rank-item { color: #333 !important; } #my-gift-panel .trust-level-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-height: 24px; margin-bottom: 8px; color: #334155 !important; font-size: 12px; font-weight: 600; } #my-gift-panel .trust-level-header > span { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .trust-level-refresh { flex-shrink: 0; min-height: 24px; padding: 3px 8px; border: 1px solid #cfe3f1; border-radius: 5px; background: #eaf4fb; color: #0078d4; font: inherit; font-size: 10px; cursor: pointer; transition: background .18s ease, border-color .18s ease, transform .18s ease; } #my-gift-panel .trust-level-refresh:hover { background: #dceefa; border-color: #9cc9e7; transform: translateY(-1px); } #my-gift-panel .trust-level-refresh:disabled { opacity: .55; cursor: not-allowed; transform: none; } #my-gift-panel .trust-level-row .trust-level-item { display: flex; align-items: center; justify-content: space-between; gap: 4px; min-width: 0; margin: 4px 0; padding: 3px 0; color: #475569 !important; font-size: 11px; line-height: 1.35; white-space: nowrap; } #my-gift-panel .trust-level-row .trust-level-name { flex-shrink: 0; width: 110px; min-width: 110px; margin-right: 4px; overflow: hidden; color: #475569 !important; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .trust-level-row .trust-level-progress { display: flex; align-items: center; justify-content: flex-end; flex: 1; min-width: 0; gap: 4px; } #my-gift-panel .trust-level-row .trust-level-bar { display: none; } #my-gift-panel .trust-level-row .trust-level-value { display: flex; align-items: center; justify-content: flex-end; flex-shrink: 0; min-width: 75px; gap: 3px; color: #334155 !important; text-align: right; white-space: nowrap; } #my-gift-panel .trust-level-row .change-indicator { flex-shrink: 0; font-size: 9px; font-weight: 600; line-height: 1; } #my-gift-panel .trust-level-row .change-up { color: #15803d; } #my-gift-panel .trust-level-row .change-down { color: #dc2626; } #my-gift-panel .trust-level-row [style*="background: rgba(255, 255, 255, 0.25)"] { background: #eaf8ef !important; border: 1px solid #b7e4c7; } #my-gift-panel .trust-level-row [style*="background: rgba(255, 255, 255, 0.15)"] { background: #fff7ed !important; border: 1px solid #fed7aa; } #my-gift-panel .trust-level-row [style*="color: #fff"] { color: #15803d !important; } #my-gift-panel .trust-level-row [style*="color: rgba(255, 255, 255, 0.9)"] { color: #b45309 !important; } #my-gift-panel .trust-level-row [style*="color: rgba(255,255,255,0.6)"] { color: #64748b !important; } #my-gift-panel .rank-data-container .rank-item { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; margin: 2px 0; padding: 6px 8px; border: 1px solid #e1e6eb; border-radius: 6px; background: #fff; color: #334155 !important; } #my-gift-panel .rank-period, #my-gift-panel .rank-values { display: flex; align-items: center; min-width: 0; } #my-gift-panel .rank-period { flex: 1; gap: 4px; color: #334155 !important; font-size: 11px; } #my-gift-panel .rank-period-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .rank-values { flex-shrink: 0; gap: 8px; font-size: 11px; } #my-gift-panel .rank-score { color: #a16207; font-weight: 600; white-space: nowrap; } #my-gift-panel .rank-position { min-width: 45px; color: #0078d4; text-align: right; white-space: nowrap; } #my-gift-panel .rank-footer { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; margin-top: 6px; padding-top: 4px; border-top: 1px solid #e1e6eb; } #my-gift-panel .rank-detail-link { overflow: hidden; color: #0078d4; font-size: 10px; text-overflow: ellipsis; text-decoration: none; white-space: nowrap; } #my-gift-panel .rank-update-time { flex-shrink: 0; color: #64748b; font-size: 9px; white-space: nowrap; } #my-gift-panel .read-stats-container { display: flex; align-items: center; justify-content: space-around; gap: 8px; min-width: 0; background: #f5f8fb !important; border: 1px solid #e1e6eb; color: #334155; } #my-gift-panel .read-stat { flex: 1; min-width: 0; text-align: center; } #my-gift-panel .read-stat-label { overflow: hidden; color: #64748b; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .read-stat-value { font-size: 16px; font-weight: 700; } #my-gift-panel .read-stat-today { color: #15803d; } #my-gift-panel .read-stat-total { color: #a16207; } #my-gift-panel .read-stat-divider { width: 1px; height: 24px; flex-shrink: 0; background: #d5dee7; } #my-gift-panel .like-counter-container { background: #f5f8fb !important; border-color: #e1e6eb !important; color: #475569 !important; } #my-gift-panel .like-status-row { display: flex; align-items: center; justify-content: space-between; gap: 6px; min-width: 0; } #my-gift-panel .like-status-label { display: flex; align-items: center; min-width: 0; gap: 4px; color: #475569; font-size: 11px; } #my-gift-panel .like-status-label span:last-child { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .like-sync-btn { flex-shrink: 0; cursor: pointer; opacity: .7; } #my-gift-panel .like-status-value { flex-shrink: 0; color: #0078d4; font-size: 16px; font-weight: 700; } #my-gift-panel .like-status-limit { flex-shrink: 0; color: #64748b; font-size: 11px; } #my-gift-panel .like-progress-track { width: 100%; height: 4px; margin-top: 4px; overflow: hidden; border-radius: 2px; background: #dfe7ef; } #my-gift-panel .like-progress-fill { height: 100%; border-radius: 2px; transition: width .3s; } #my-gift-panel .like-cooldown-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; min-width: 0; gap: 6px; } #my-gift-panel .like-cooldown-status { display: flex; align-items: center; flex-wrap: wrap; min-width: 0; gap: 6px; } #my-gift-panel .like-cooldown-label, #my-gift-panel .like-cooldown-time { color: #dc2626; } #my-gift-panel .like-cooldown-label { font-size: 11px; } #my-gift-panel .like-cooldown-time { font-size: 13px; font-weight: 700; white-space: nowrap; } #my-gift-panel .like-cooldown-clear-btn { flex: 0 0 auto; margin-left: auto; min-height: 24px; padding: 3px 6px; border: 1px solid #fecaca; border-radius: 4px; background: #fff; color: #b91c1c; cursor: pointer; font-size: 11px; line-height: 1.2; } #my-gift-panel .like-cooldown-clear-btn:hover { background: #fff1f2; border-color: #fca5a5; } #my-gift-panel .like-cooldown-clear-btn:disabled { cursor: default; opacity: .55; } #my-gift-panel .like-cooldown-clear-btn:focus-visible { outline: 2px solid #dc2626; outline-offset: 1px; } #my-gift-panel .topic-status-container { min-width: 0; color: #475569; font-size: 10px; line-height: 1.4; } #my-gift-panel .topic-status-heading { margin-bottom: 6px; overflow-wrap: anywhere; color: #334155; font-size: 11px; } #my-gift-panel .topic-status-type { color: #0078d4; font-weight: 600; } #my-gift-panel .topic-status-progress-row { display: flex; align-items: center; gap: 8px; min-width: 0; margin-bottom: 4px; } #my-gift-panel .topic-status-progress-track { flex: 1; min-width: 0; height: 6px; overflow: hidden; border-radius: 3px; background: #dfe7ef; } #my-gift-panel .topic-status-progress-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, #48bb78 0%, #68d391 100%); transition: width .3s; } #my-gift-panel .topic-status-percent { flex-shrink: 0; min-width: 35px; color: #64748b; text-align: right; } #my-gift-panel .topic-status-meta, #my-gift-panel .topic-status-reading-row { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; flex-wrap: wrap; color: #64748b; } #my-gift-panel .topic-status-reading-row { margin-bottom: 4px; color: #475569; font-size: 11px; } #my-gift-panel .topic-status-ready { margin-bottom: 4px; color: #15803d; font-size: 11px; } #my-gift-panel .topic-status-current, #my-gift-panel .topic-status-remaining, #my-gift-panel .topic-status-today { font-weight: 700; } #my-gift-panel .topic-status-current { color: #a16207; } #my-gift-panel .topic-status-remaining { color: #15803d; } #my-gift-panel .topic-status-today { color: #0078d4; } #my-gift-panel .topic-status-skipped, #my-gift-panel .topic-status-warning { color: #b45309; } #my-gift-panel .topic-status-warning { margin-top: 2px; font-size: 9px; } #my-gift-panel .topic-status-notification { display: block; margin-top: 6px; padding-top: 6px; border-top: 1px solid #dfe7ef; color: #0078d4 !important; font-size: 11px; font-weight: 600; line-height: 1.45; white-space: pre-line; overflow-wrap: anywhere; } #my-gift-panel .topic-status-content:empty + .topic-status-notification { margin-top: 0; padding-top: 0; border-top: 0; } #my-gift-panel [data-section-content="auto"] .toggle-row > .toggle-label:first-child, #my-gift-panel [data-section-content="auto"] .toggle-row > div:first-child > .toggle-label:first-child, #my-gift-panel [data-section-content="auto"] .read-stat-label, #my-gift-panel [data-section-content="auto"] .like-status-label, #my-gift-panel [data-section-content="auto"] .like-status-limit, #my-gift-panel [data-section-content="auto"] .topic-status-container, #my-gift-panel [data-section-content="auto"] .topic-status-heading, #my-gift-panel [data-section-content="auto"] .topic-status-meta, #my-gift-panel [data-section-content="auto"] .topic-status-reading-row { color: #475569 !important; } #my-gift-panel .settings-inner-collapsible { margin-top: 0; padding-top: 0; border-top: 1px dashed #dfe5eb; } #my-gift-panel [data-section-content="auto"] > .settings-inner-collapsible { border-top: 0; } #my-gift-panel .topic-created-time-settings, #my-gift-panel .mode-settings-options { box-sizing: border-box; margin: 0 8px; padding: 0; border: 1px solid #e1e6eb; border-radius: 8px; background: #fff; overflow: hidden; } #my-gift-panel .topic-created-time-settings > .toggle-row, #my-gift-panel .mode-settings-options > .toggle-row { margin: 0; border: 0; border-radius: 0; background: transparent; } #my-gift-panel .my-gift-tool-detail[data-section-content="auto"] { padding-left: 8px; padding-right: 8px; padding-bottom: 4px; } #my-gift-panel .cf-bypass-section { margin: 0 8px; } #my-gift-panel .my-gift-tool-detail[data-section-content="settings"] { padding-top: 0; padding-bottom: 0; } #my-gift-panel [data-section-content="settings"] > .settings-inner-collapsible:first-child { margin-top: 0; padding-top: 0; border-top: 0; } #my-gift-panel .settings-inner-collapsible:not(.collapsed) + .settings-inner-collapsible { border-top: 0; } #my-gift-panel .settings-inner-collapsible-header { display: flex; align-items: center; justify-content: space-between; min-height: 41px; box-sizing: border-box; padding: 0 12px; color: #555; font-size: 12px; cursor: pointer; } #my-gift-panel .settings-inner-collapsible-header > span:first-child { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .settings-inner-collapsible-controls { display: flex; align-items: center; gap: 8px; min-height: 18px; flex-shrink: 0; } #my-gift-panel .settings-inner-collapsible-header .collapse-icon { display: inline-block; width: 0; height: 0; border-top: 4px solid transparent; border-bottom: 4px solid transparent; border-left: 5px solid #8a9199; font-size: 0; line-height: 0; flex-shrink: 0; transform: rotate(90deg); transition: transform .18s ease; } #my-gift-panel .settings-inner-collapsible.collapsed .collapse-icon { transform: rotate(0deg); } #my-gift-panel .settings-inner-collapsible-content { margin-top: 8px; } #my-gift-panel .settings-inner-collapsible.collapsed .settings-inner-collapsible-content { display: none; } #my-gift-panel .auto-read-parameter-settings > .toggle-row:last-child { margin-bottom: 0; } #my-gift-panel .cf-bypass-description { box-sizing: border-box; margin-bottom: 8px; padding: 8px 10px; border: 1px solid #e1e6eb; border-radius: 8px; background: #f5f8fb; color: #8a9199; font-size: 12px; line-height: 1.6; text-align: center; white-space: normal; overflow-wrap: anywhere; } #my-gift-panel .cf-bypass-section .reveal-users-btn { background: #0078d4; color: #fff; white-space: nowrap; } #my-gift-panel .section-divider { display: none; } #my-gift-panel .credit-footer { display: flex; justify-content: space-between; gap: 8px; margin-top: 8px; padding-top: 8px; border-top: 1px solid #e1e6eb; } #my-gift-panel .cdk-info-row .cdk-footer { align-items: center; margin-top: 6px; padding: 5px 0 6px; border-top: 0; border-bottom: 1px solid #e1e6eb; } #my-gift-panel .cdk-info-row .cdk-footer .credit-link { display: inline-flex; align-items: center; min-width: 0; overflow: hidden; color: #0078d4 !important; font-size: 11px; line-height: 1.4; text-overflow: ellipsis; white-space: nowrap; } #my-gift-panel .cdk-info-row .cdk-footer .credit-update-time { flex: 0 0 auto; color: #64748b !important; font-size: 10px; line-height: 1.4; white-space: nowrap; } #my-gift-panel .credit-info-row { margin-top: 0; padding: 0; border: 0; border-radius: 0; background: transparent; } #my-gift-panel .credit-info-row .credit-main-stat { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin: 0 0 8px; padding: 8px 10px; border: 1px solid #d9e4ec; border-radius: 7px; background: #fff; } #my-gift-panel .credit-info-row .credit-stat-value { color: #0078d4; font-size: 24px; font-weight: 700; line-height: 1; } #my-gift-panel .credit-info-row .credit-tomorrow-stat { border-color: #ead9ad; background: #fffaf0; } #my-gift-panel .credit-info-row .credit-tomorrow-stat .credit-stat-label { color: #926c1e !important; } #my-gift-panel .credit-info-row .credit-tomorrow-stat .credit-stat-value { color: #b7791f; font-size: 22px; } #my-gift-panel .credit-info-row .credit-summary-list, #my-gift-panel .credit-info-row .credit-history-list { min-width: 0; } #my-gift-panel .credit-info-row .trust-level-item { display: flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; margin: 0; padding: 5px 0; border-bottom: 1px solid #edf2f7; color: #475569 !important; font-size: 11px; line-height: 1.4; } #my-gift-panel .credit-info-row .trust-level-item:last-child { border-bottom: 0; } #my-gift-panel .credit-info-row .trust-level-name { flex: 1 1 auto; min-width: 0; overflow-wrap: anywhere; color: #64748b !important; } #my-gift-panel .credit-info-row .trust-level-value { display: inline-flex; align-items: center; justify-content: flex-end; flex: 0 0 auto; min-width: 0; gap: 5px; color: #334155 !important; font-weight: 600; text-align: right; white-space: nowrap; } #my-gift-panel .credit-info-row .credit-value-blue { color: #0078d4 !important; } #my-gift-panel .credit-info-row .credit-value-gold { color: #a16207 !important; } #my-gift-panel .credit-info-row .credit-value-green { color: #15803d !important; } #my-gift-panel .credit-info-row .credit-value-red { color: #dc2626 !important; } #my-gift-panel .credit-info-row .credit-rank-badge { padding: 2px 5px; border: 1px solid #cfe3f1; border-radius: 4px; background: #eaf4fb; color: #0078d4; font-size: 10px; font-weight: 500; } #my-gift-panel .credit-info-row .credit-history-section { margin-top: 10px; } #my-gift-panel .credit-info-row .credit-section-title { margin: 0 0 3px; padding-top: 8px; border-top: 1px solid #e1e6eb; color: #64748b !important; font-size: 10px; font-weight: 600; line-height: 1.3; } #my-gift-panel .credit-info-row .credit-history-list .trust-level-item { padding: 4px 0; } #my-gift-panel .credit-auth-state { display: flex; flex-direction: column; gap: 10px; padding: 2px 0 0; } #my-gift-panel .credit-auth-header { display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; } #my-gift-panel .credit-auth-status { display: inline-flex; align-items: center; min-width: 0; gap: 6px; color: #334155; font-size: 12px; font-weight: 600; } #my-gift-panel .credit-auth-dot { width: 7px; height: 7px; flex: 0 0 auto; border-radius: 50%; background: #94a3b8; } #my-gift-panel .credit-auth-message { margin: 0; color: #64748b; font-size: 11px; line-height: 1.5; } #my-gift-panel .credit-auth-actions { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding-top: 8px; border-top: 1px solid #e1e6eb; } #my-gift-panel .credit-auth-refresh { display: inline-flex; align-items: center; justify-content: center; width: 27px; min-width: 27px; height: 27px; min-height: 27px; padding: 0; border-radius: 6px; font-size: 15px; line-height: 1; } #my-gift-panel .credit-link { color: #0078d4 !important; } #my-gift-panel .credit-info-row .credit-stat-label, #my-gift-panel .credit-info-row .credit-section-title, #my-gift-panel .credit-info-row .credit-update-time { color: #475569 !important; } #my-gift-panel .credit-info-row .trust-level-value[style*="color: #fff"] { color: #64748b !important; } #my-gift-panel .credit-login-btn { display: inline-flex; align-items: center; justify-content: center; gap: 5px; min-height: 26px; padding: 4px 10px; border: 1px solid #0078d4; border-radius: 6px; background: #0078d4 !important; color: #fff !important; font: inherit; font-size: 10px; font-weight: 600; line-height: 1.2; text-decoration: none; cursor: pointer; transition: background .18s ease, border-color .18s ease; } #my-gift-panel .credit-login-arrow { font-size: 13px; line-height: 1; } #my-gift-panel .credit-login-btn:hover { border-color: #005a9e; background: #005a9e !important; } @media screen and (max-width: 400px) { #my-gift-panel .trust-level-row .trust-level-name { width: 90px; min-width: 90px; } #my-gift-panel .trust-level-row .trust-level-value { min-width: 64px; } } .lda-topic-created-time { margin-left: 8px; color: #64748b; font-size: 11px; line-height: 1; white-space: nowrap; vertical-align: middle; opacity: 1; } .lda-topic-created-label { color: rgba(255, 255, 255, 0.86); background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.18); border-radius: 999px; padding: 2px 8px; font-weight: 700; white-space: nowrap; box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03) inset; } .lda-topic-created-time[data-age-level="neutral"] .lda-topic-created-label { color: rgba(255, 255, 255, 0.86); background: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.18); box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03) inset; } .lda-topic-created-time[data-age-level="old"] .lda-topic-created-label { color: var(--lda-topic-age-old, #fdba74); background: rgba(251, 146, 60, 0.16); border-color: rgba(251, 146, 60, 0.34); box-shadow: 0 0 0 1px rgba(251, 146, 60, 0.05) inset; } .lda-topic-created-time[data-age-level="ancient"] .lda-topic-created-label { color: var(--lda-topic-age-ancient, #fca5a5); background: rgba(239, 68, 68, 0.16); border-color: rgba(239, 68, 68, 0.36); box-shadow: 0 0 0 1px rgba(239, 68, 68, 0.05) inset; } .lda-topic-created-time[data-age-level="fresh"] .lda-topic-created-label { color: var(--lda-topic-age-fresh, #86efac); background: rgba(134, 239, 172, 0.14); border-color: rgba(134, 239, 172, 0.32); box-shadow: 0 0 0 1px rgba(134, 239, 172, 0.04) inset; } `); document.body.appendChild(this.container); const closeBtn = this.container.querySelector("#my-gift-close"); const autoButton = this.container.querySelector('[data-section="auto"]'); const autoContent = this.container.querySelector('[data-section-content="auto"]'); const sectionMap = { auto: [autoButton, autoContent], accountData: [this.container.querySelector('[data-section="account-data"]'), this.container.querySelector('[data-section-content="account-data"]')], account: [this.container.querySelector('[data-section="account"]'), this.container.querySelector('[data-section-content="account"]')], credit: [this.container.querySelector('[data-section="credit"]'), this.container.querySelector('[data-section-content="credit"]')], cdk: [this.container.querySelector('[data-section="cdk"]'), this.container.querySelector('[data-section-content="cdk"]')], rank: [this.container.querySelector('[data-section="rank"]'), this.container.querySelector('[data-section-content="rank"]')], settings: [this.container.querySelector('[data-section="settings"]'), this.container.querySelector('[data-section-content="settings"]')] }; const setSectionExpanded = (button, sectionContent, expanded) => { button.setAttribute("aria-expanded", String(expanded)); }; this.setSectionExpanded = setSectionExpanded; const bindSection = (key, onExpand) => { const [button, sectionContent] = sectionMap[key]; button.addEventListener("click", (event) => { event.stopPropagation(); const expanded = button.getAttribute("aria-expanded") === "true"; setSectionExpanded(button, sectionContent, !expanded); if (!expanded && onExpand) onExpand(); const rect = this.container.getBoundingClientRect(); applyPosition(rect.left, rect.top); savePosition(); }); }; const applyPosition = (left, top, dimensions = null) => { const width = dimensions?.width || this.container.offsetWidth; const height = dimensions?.height || this.container.offsetHeight; const viewportWidth = document.documentElement.clientWidth || window.innerWidth; const viewportHeight = document.documentElement.clientHeight || window.innerHeight; const maxLeft = Math.max(0, viewportWidth - width); const maxTop = Math.max(0, viewportHeight - height); this.container.style.right = "auto"; this.container.style.bottom = "auto"; this.container.style.left = `${Math.max(0, Math.min(left, maxLeft))}px`; this.container.style.top = `${Math.max(0, Math.min(top, maxTop))}px`; }; const savePosition = () => { const rect = this.container.getBoundingClientRect(); localStorage.setItem("my-gift-position", JSON.stringify({ left: rect.left, top: rect.top })); }; const hasSavedPosition = () => { try { return Boolean(localStorage.getItem("my-gift-position")); } catch (_) { return false; } }; const restorePosition = (dimensions = null) => { try { const saved = JSON.parse(localStorage.getItem("my-gift-position") || "null"); if (saved && typeof saved.left === "number" && typeof saved.top === "number") { applyPosition(saved.left, saved.top, dimensions); } } catch (_) { } }; const measurePanelSize = (collapsed) => { const wasCollapsed = this.container.classList.contains("collapsed"); const transition = this.container.style.transition; this.container.style.transition = "none"; this.container.classList.toggle("collapsed", collapsed); const size = { width: this.container.offsetWidth, height: this.container.offsetHeight }; this.container.classList.toggle("collapsed", wasCollapsed); this.container.style.transition = transition; return size; }; const getExpandedPosition = (rect, size) => { const viewportWidth = document.documentElement.clientWidth || window.innerWidth; const viewportHeight = document.documentElement.clientHeight || window.innerHeight; let left = rect.left; let top = rect.top; if (left + size.width > viewportWidth) left = rect.right - size.width; if (top + size.height > viewportHeight) top = rect.bottom - size.height; return { left, top }; }; const openPanel = () => { const collapsedRect = this.container.getBoundingClientRect(); const expandedSize = measurePanelSize(false); const expandedPosition = getExpandedPosition(collapsedRect, expandedSize); this.container.classList.remove("collapsed"); applyPosition(expandedPosition.left, expandedPosition.top, expandedSize); }; const closePanel = () => { const expandedRect = this.container.getBoundingClientRect(); const collapsedSize = measurePanelSize(true); this.container.classList.add("collapsed"); if (hasSavedPosition()) { restorePosition(collapsedSize); } else { applyPosition( expandedRect.right - collapsedSize.width, expandedRect.bottom - collapsedSize.height, collapsedSize ); } }; requestAnimationFrame(restorePosition); let dragging = false; let moved = false; let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; this.container.addEventListener("mousedown", (event) => { if (event.button !== 0 || event.target.closest?.('button, a, input, select, textarea, [role="button"]')) return; const rect = this.container.getBoundingClientRect(); dragging = true; moved = false; startX = event.clientX; startY = event.clientY; startLeft = rect.left; startTop = rect.top; applyPosition(startLeft, startTop); this.container.classList.add("dragging"); event.preventDefault(); }); document.addEventListener("mousemove", (event) => { if (!dragging) return; const deltaX = event.clientX - startX; const deltaY = event.clientY - startY; if (Math.abs(deltaX) > 3 || Math.abs(deltaY) > 3) moved = true; applyPosition(startLeft + deltaX, startTop + deltaY); }); document.addEventListener("mouseup", () => { if (!dragging) return; dragging = false; this.container.classList.remove("dragging"); if (moved) savePosition(); }); this.container.addEventListener("click", (event) => { if (moved) { moved = false; event.stopPropagation(); return; } if (this.container.classList.contains("collapsed")) openPanel(); }); closeBtn.addEventListener("click", (event) => { event.stopPropagation(); closePanel(); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && !this.container.classList.contains("collapsed")) closePanel(); }); window.addEventListener("resize", () => { const rect = this.container.getBoundingClientRect(); applyPosition(rect.left, rect.top); savePosition(); }); const observer = new MutationObserver(() => { if (document.body && !document.getElementById("my-gift-panel")) { document.body.appendChild(this.container); restorePosition(); } }); observer.observe(document.documentElement, { childList: true, subtree: true }); this.button = document.createElement("button"); this.button.className = "main-action-btn" + (this.autoRunning ? " running" : ""); this.button.innerHTML = this.autoRunning ? `\u23F8${this.t("stopReading")}` : `\u25B6${this.t("startReading")}`; this.button.addEventListener("click", () => this.handleButtonClick()); this.readStatsContainer = document.createElement("div"); this.readStatsContainer.className = "read-stats-container"; this.readStatsContainer.style.cssText = ` display: flex; justify-content: space-around; align-items: center; background: #f5f8fb; padding: 6px 10px; border-radius: 8px; margin-top: 6px; border: 1px solid #e1e6eb; color: #334155; `; this.updateReadStatsDisplay(); this.likeCounterContainer = document.createElement("div"); this.likeCounterContainer.className = "like-counter-container"; this.likeCounterContainer.style.cssText = ` display: flex; flex-direction: column; background: #f5f8fb; padding: 8px 12px; border-radius: 8px; margin-top: 6px; border: 1px solid #e1e6eb; font-size: 12px; color: #475569; `; this.likeCounterContainer.innerHTML = `
\u2764\uFE0F ${this.t("likeRemaining")} -- / --
`; this.likeCounterContainer.style.cursor = "pointer"; this.likeCounterContainer.title = this.t("likeCountMismatch"); this.likeCounterContainer.addEventListener("click", () => { if (this.likeCounter) { this.showNotification(this.t("likeSyncing")); this.likeCounter.manualSync().then(() => { this.showNotification(this.t("likeSyncSuccess")); }); } }); const smartLikeRow = this.createToggleRow( this.t("smartLike"), this.smartLikeEnabled, (checked) => this.setSmartLikeEnabled(checked) ); this.smartLikeToggleInput = smartLikeRow.querySelector('input[type="checkbox"]'); const smartLikeSpeedRow = this.createSelectRow( this.t("smartLikeSpeed"), [ { value: "slow", label: this.t("smartLikeSpeedSlow") }, { value: "normal", label: this.t("smartLikeSpeedNormal") }, { value: "fast", label: this.t("smartLikeSpeedFast") } ], this.smartLikeSpeed, (value) => { this.smartLikeSpeed = ["slow", "normal", "fast"].includes(value) ? value : "normal"; Storage.set("smartLikeSpeed", this.smartLikeSpeed); } ); const cleanModeRow = this.createToggleRow( this.t("cleanMode"), this.cleanModeEnabled, (checked) => { this.cleanModeEnabled = checked; Storage.set("cleanModeEnabled", this.cleanModeEnabled); console.log(`\u6E05\u723D\u6A21\u5F0F: ${this.cleanModeEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); this.toggleCleanMode(); } ); const grayscaleModeRow = this.createToggleRow( this.t("grayscaleMode"), this.grayscaleModeEnabled, (checked) => { this.grayscaleModeEnabled = checked; Storage.set("grayscaleModeEnabled", this.grayscaleModeEnabled); console.log(`\u9ED1\u767D\u7070\u6A21\u5F0F: ${this.grayscaleModeEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); this.toggleGrayscaleMode(); } ); const readUnreadRow = this.createToggleRow( this.t("readUnread"), this.readUnreadEnabled, (checked) => { this.readUnreadEnabled = checked; Storage.set("readUnreadEnabled", this.readUnreadEnabled); console.log(`\u8BFB\u53D6\u672A\u8BFB\u5E16\u5B50: ${this.readUnreadEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); this.topicList = []; this.setSessionStorage("topicList", []); console.log("\u5DF2\u6E05\u7A7A\u8BDD\u9898\u5217\u8868\uFF0C\u4E0B\u6B21\u5C06\u83B7\u53D6" + (this.readUnreadEnabled ? "\u672A\u8BFB" : "\u6700\u65B0") + "\u5E16\u5B50"); } ); const randomOrderRow = this.createToggleRow( this.t("randomOrder"), this.randomOrderEnabled, (checked) => { this.randomOrderEnabled = checked; Storage.set("randomOrderEnabled", this.randomOrderEnabled); console.log(`\u968F\u673A\u987A\u5E8F\u9605\u8BFB: ${this.randomOrderEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); this.topicList = []; this.setSessionStorage("topicList", []); } ); randomOrderRow.title = this.t("randomOrderTip"); const skipReadRow = this.createToggleRow( this.t("skipRead"), this.skipReadEnabled, (checked) => { this.skipReadEnabled = checked; Storage.set("skipReadEnabled", this.skipReadEnabled); console.log(`\u8DF3\u8FC7\u5DF2\u8BFB\u5E16\u5B50: ${this.skipReadEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); this.topicList = []; this.setSessionStorage("topicList", []); } ); skipReadRow.title = this.t("skipReadTip"); const fullTopicReadRow = this.createToggleRow( this.t("fullTopicRead"), this.fullTopicReadEnabled, (checked) => { this.fullTopicReadEnabled = checked; Storage.set("fullTopicReadEnabled", this.fullTopicReadEnabled); console.log(`\u5B8C\u6574\u9605\u8BFB\u5E16\u5B50: ${this.fullTopicReadEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); } ); fullTopicReadRow.title = this.t("fullTopicReadTip"); const topicLimitRow = this.createSliderRow( this.t("topicLimit"), this.topicLimitCount, 10, 500, 10, (value) => { this.topicLimitCount = value; Storage.set("topicLimitCount", this.topicLimitCount); console.log(`\u83B7\u53D6\u5E16\u5B50\u6570\u91CF: ${this.topicLimitCount}`); this.topicList = []; this.setSessionStorage("topicList", []); } ); topicLimitRow.title = this.t("topicLimitTip"); const restTimeRow = this.createSliderRow( this.t("restTimeLabel"), this.restTimeMinutes, 1, 30, 1, (value) => { this.restTimeMinutes = value; Storage.set("restTimeMinutes", this.restTimeMinutes); console.log(`\u4F11\u606F\u65F6\u95F4: ${this.restTimeMinutes} \u5206\u949F`); } ); restTimeRow.title = this.t("restTimeTip"); const stopAfterReadRow = this.createToggleRow( this.t("stopAfterRead"), this.stopAfterReadEnabled, (checked) => { this.stopAfterReadEnabled = checked; Storage.set("stopAfterReadEnabled", this.stopAfterReadEnabled); console.log(`\u9605\u8BFB\u6570\u91CF\u9650\u5236: ${this.stopAfterReadEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); if (checked) { this.currentSessionReadCount = 0; this.setSessionStorage("currentSessionReadCount", 0); } } ); stopAfterReadRow.title = this.t("stopAfterReadTip"); const stopAfterReadCountRow = this.createSliderRow( this.t("stopAfterReadCount"), this.stopAfterReadCount, 5, 100, 5, (value) => { this.stopAfterReadCount = value; Storage.set("stopAfterReadCount", this.stopAfterReadCount); console.log(`\u9605\u8BFB\u6570\u91CF\u9650\u5236: ${this.stopAfterReadCount} \u7BC7`); } ); stopAfterReadCountRow.title = this.t("stopAfterReadCountTip"); const stopOnLikeLimitRow = this.createToggleRow( this.t("stopOnLikeLimit"), this.stopOnLikeLimitEnabled, (checked) => { this.stopOnLikeLimitEnabled = checked; Storage.set("stopOnLikeLimitEnabled", this.stopOnLikeLimitEnabled); console.log(`\u70B9\u8D5E\u4E0A\u9650\u505C\u6B62\u9605\u8BFB: ${this.stopOnLikeLimitEnabled ? "\u5F00\u542F" : "\u5173\u95ED"}`); } ); stopOnLikeLimitRow.title = this.t("stopOnLikeLimitTip"); this.trustLevelContainer = document.createElement("div"); this.trustLevelContainer.className = "trust-level-row"; this.trustLevelContainer.innerHTML = `
${this.t("loadingLevel")}
`; this.autoSection = autoButton; this.autoSectionContent = autoContent; const accountButton = sectionMap.account[0]; const accountContent = sectionMap.account[1]; const creditButton = sectionMap.credit[0]; const creditContent = sectionMap.credit[1]; const cdkButton = sectionMap.cdk[0]; const cdkContent = sectionMap.cdk[1]; const rankButton = sectionMap.rank[0]; const rankContent = sectionMap.rank[1]; const settingsContent = sectionMap.settings[1]; this.accountSection = accountButton; this.accountSectionContent = accountContent; this.accountDataSection = this.container.querySelector('[data-section="account-data"]'); this.accountDataSectionContent = this.container.querySelector('[data-section-content="account-data"]'); this.accountSectionContent.appendChild(this.trustLevelContainer); this.creditSection = creditButton; this.creditSectionContent = creditContent; this.cdkSection = cdkButton; this.cdkSectionContent = cdkContent; this.rankSection = rankButton; this.rankSectionContent = rankContent; this.settingsPluginSectionContent = settingsContent; if (CURRENT_DOMAIN !== "linux.do") { this.container.querySelector('[data-section-item="credit"]').style.display = "none"; this.container.querySelector('[data-section-item="cdk"]').style.display = "none"; } bindSection("auto"); bindSection("accountData"); bindSection("account", () => this.loadUserTrustLevel()); bindSection("credit", () => this.loadCreditInfo()); bindSection("cdk", () => this.loadCdkInfo()); bindSection("rank", () => this.loadRankingData()); bindSection("settings"); if (this.autoRunning) setSectionExpanded(autoButton, autoContent, true); autoContent.appendChild(this.button); autoContent.appendChild(this.readStatsContainer); autoContent.appendChild(this.likeCounterContainer); const toggleGrid = document.createElement("div"); toggleGrid.className = "toggle-grid"; toggleGrid.appendChild(smartLikeRow); toggleGrid.classList.add("smart-like-controls"); toggleGrid.appendChild(smartLikeSpeedRow); toggleGrid.appendChild(readUnreadRow); toggleGrid.appendChild(randomOrderRow); toggleGrid.appendChild(skipReadRow); toggleGrid.appendChild(fullTopicReadRow); toggleGrid.appendChild(stopAfterReadRow); toggleGrid.appendChild(stopOnLikeLimitRow); autoContent.appendChild(toggleGrid); const readParameterSection = document.createElement("div"); readParameterSection.className = "auto-read-parameter-settings"; readParameterSection.appendChild(topicLimitRow); readParameterSection.appendChild(restTimeRow); readParameterSection.appendChild(stopAfterReadCountRow); autoContent.appendChild(this.createInnerCollapsibleSection( this.t("readParameterSettingsLabel"), readParameterSection, true )); this.topicStatusContainer = document.createElement("div"); this.topicStatusContainer.className = "topic-status-container"; this.topicStatusContainer.style.display = "none"; this.topicStatusContent = document.createElement("div"); this.topicStatusContent.className = "topic-status-content"; this.topicStatusContainer.appendChild(this.topicStatusContent); this.topicStatusNotification = document.createElement("div"); this.topicStatusNotification.className = "topic-status-notification"; this.topicStatusNotification.setAttribute("role", "status"); this.topicStatusNotification.setAttribute("aria-live", "polite"); this.topicStatusNotification.style.display = "none"; this.topicStatusContainer.appendChild(this.topicStatusNotification); autoContent.appendChild(this.topicStatusContainer); this.flushPendingNotifications(); creditContent.appendChild(this.creditContainer = document.createElement("div")); this.creditContainer.className = "credit-info-row"; this.creditContainer.innerHTML = `
${this.t("clickToLoadCredits")}
`; cdkContent.appendChild(this.cdkContainer = document.createElement("div")); this.cdkContainer.className = "cdk-info-row"; this.cdkContainer.innerHTML = `
${this.t("clickToLoadCdk")}
`; rankContent.appendChild(this.rankDataContainer = document.createElement("div")); this.rankDataContainer.className = "rank-data-container"; this.rankDataContainer.innerHTML = `
${this.t("clickToLoadRank")}
`; const modeSubSection = document.createElement("div"); modeSubSection.className = "mode-settings-options"; modeSubSection.appendChild(cleanModeRow); modeSubSection.appendChild(grayscaleModeRow); settingsContent.appendChild(this.createInnerCollapsibleSection(`\u{1F3A8} ${this.t("modeSettingsLabel")}`, modeSubSection, true)); settingsContent.appendChild(this.createInnerCollapsibleSection(this.t("topicCreatedTimeLabel"), this.createTopicAgeColorSettings(), true)); if (CURRENT_DOMAIN === "linux.do") { const cfSection = document.createElement("div"); cfSection.className = "cf-bypass-section"; const cfDescription = document.createElement("div"); cfDescription.className = "cf-bypass-description"; cfDescription.textContent = this.t("cfBypassTip"); cfSection.appendChild(cfDescription); const cfToggleRow = this.createToggleRow("", this.cfBypassEnabled, (checked) => { this.cfBypassEnabled = checked; Storage.set("cfBypassEnabled", checked); this.showNotification(checked ? this.t("cfBypassEnabled") : this.t("cfBypassDisabled")); if (checked) { this.initCloudFlareBypass(); } else { this.stopCloudFlareBypass(); } }); const manualCfBtn = document.createElement("button"); manualCfBtn.type = "button"; manualCfBtn.className = "reveal-users-btn"; manualCfBtn.textContent = `\u{1F6E1}\uFE0F ${this.t("cfBypassManual")}`; manualCfBtn.title = this.t("cfBypassManualTip"); manualCfBtn.addEventListener("click", () => this.manualTriggerCF()); cfSection.appendChild(manualCfBtn); settingsContent.appendChild(this.createInnerCollapsibleSection( this.t("cfBypassLabel"), cfSection, true, cfToggleRow.querySelector(".toggle-switch") )); } }, createToggleRow(label, checked, onChange) { const row = document.createElement("div"); row.className = "toggle-row"; const labelEl = document.createElement("span"); labelEl.className = "toggle-label"; labelEl.textContent = label; const toggleSwitch = document.createElement("label"); toggleSwitch.className = "toggle-switch"; const input = document.createElement("input"); input.type = "checkbox"; input.checked = checked; input.addEventListener("change", (e) => { onChange(e.target.checked); }); const slider = document.createElement("span"); slider.className = "toggle-slider"; toggleSwitch.appendChild(input); toggleSwitch.appendChild(slider); row.appendChild(labelEl); row.appendChild(toggleSwitch); return row; }, createSliderRow(label, value, min, max, step, onChange) { const row = document.createElement("div"); row.className = "toggle-row"; row.style.flexDirection = "column"; row.style.alignItems = "stretch"; row.style.gap = "6px"; const topRow = document.createElement("div"); topRow.style.cssText = "display: flex; justify-content: space-between; align-items: center;"; const labelEl = document.createElement("span"); labelEl.className = "toggle-label"; labelEl.textContent = label; const valueEl = document.createElement("span"); valueEl.className = "toggle-label"; valueEl.style.cssText = "color: #a16207; font-weight: bold; min-width: 40px; text-align: right;"; valueEl.textContent = value; topRow.appendChild(labelEl); topRow.appendChild(valueEl); const sliderContainer = document.createElement("div"); sliderContainer.style.cssText = "width: 100%; padding: 0 2px;"; const slider = document.createElement("input"); slider.type = "range"; slider.min = min; slider.max = max; slider.step = step; slider.value = value; slider.className = "panel-slider-input"; const percentage = (value - min) / (max - min) * 100; slider.style.cssText = ` width: 100%; height: 4px; border-radius: 2px; background: linear-gradient(to right, #0078d4 0%, #0078d4 ${percentage}%, #dfe7ef ${percentage}%, #dfe7ef 100%); outline: none; -webkit-appearance: none; -moz-appearance: none; appearance: none; cursor: pointer; `; if (!document.getElementById("panel-slider-style")) { const sliderStyle = document.createElement("style"); sliderStyle.id = "panel-slider-style"; sliderStyle.textContent = ` .panel-slider-input { -webkit-appearance: none; -moz-appearance: none; appearance: none; } .panel-slider-input::-webkit-slider-runnable-track { height: 4px; border-radius: 2px; background: transparent; } .panel-slider-input::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 14px; height: 14px; border-radius: 50%; background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%); cursor: pointer; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 120, 212, 0.35); border: none; margin-top: -5px; transition: transform 0.15s ease, box-shadow 0.15s ease; } .panel-slider-input::-webkit-slider-thumb:hover { transform: scale(1.15); box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4), 0 0 0 2px rgba(0, 120, 212, 0.5); } .panel-slider-input::-webkit-slider-thumb:active { transform: scale(1.05); background: linear-gradient(135deg, #0078d4 0%, #005a9e 100%); } .panel-slider-input::-moz-range-track { height: 4px; border-radius: 2px; background: transparent; border: none; } .panel-slider-input::-moz-range-thumb { width: 14px; height: 14px; border-radius: 50%; background: linear-gradient(135deg, #fff 0%, #f0f0f0 100%); cursor: pointer; border: none; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3), 0 0 0 1px rgba(0, 120, 212, 0.35); } .panel-slider-input::-moz-range-thumb:hover { transform: scale(1.15); } .panel-slider-input::-moz-range-progress { background: #0078d4; border-radius: 2px; height: 4px; } .panel-slider-input:focus { outline: none; } `; document.head.appendChild(sliderStyle); } slider.addEventListener("input", (e) => { const newValue = parseInt(e.target.value); valueEl.textContent = newValue; const newPercentage = (newValue - min) / (max - min) * 100; slider.style.background = `linear-gradient(to right, #0078d4 0%, #0078d4 ${newPercentage}%, #dfe7ef ${newPercentage}%, #dfe7ef 100%)`; onChange(newValue); }); sliderContainer.appendChild(slider); row.appendChild(topRow); row.appendChild(sliderContainer); return row; }, createSelectRow(label, options, selectedValue, onChange) { const row = document.createElement("div"); row.className = "toggle-row"; const labelEl = document.createElement("span"); labelEl.className = "toggle-label"; labelEl.textContent = label; const selectWrapper = document.createElement("div"); selectWrapper.style.cssText = ` position: relative; display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; height: 100%; `; const select = document.createElement("select"); select.style.cssText = ` appearance: none; -webkit-appearance: none; -moz-appearance: none; background: #fff; border: 1px solid #cbd5df; border-radius: 4px; color: #334155; padding: 5px 22px 5px 8px; font-size: 11px; cursor: pointer; outline: none; transition: all 0.2s ease; text-align: center; text-align-last: center; min-width: auto; width: auto; white-space: nowrap; overflow: visible; text-overflow: clip; margin-bottom: 0; `; select.addEventListener("mouseenter", () => { select.style.background = "#f8fbfe"; select.style.borderColor = "#9cc9e7"; }); select.addEventListener("mouseleave", () => { if (document.activeElement !== select) { select.style.background = "#fff"; select.style.borderColor = "#cbd5df"; } }); select.addEventListener("focus", () => { select.style.background = "#f8fbfe"; select.style.borderColor = "#70b7e4"; select.style.boxShadow = "0 0 0 2px rgba(0, 120, 212, 0.15)"; }); select.addEventListener("blur", () => { select.style.background = "#fff"; select.style.borderColor = "#cbd5df"; select.style.boxShadow = "none"; }); const arrow = document.createElement("span"); arrow.innerHTML = "\u25BC"; arrow.style.cssText = ` position: absolute; right: 6px; top: 50%; transform: translateY(-50%); font-size: 8px; color: #64748b; pointer-events: none; `; options.forEach((opt) => { const option = document.createElement("option"); option.value = opt.value; option.textContent = opt.label; option.selected = opt.value === selectedValue; option.style.cssText = ` background: #fff; color: #333; padding: 4px 8px; text-align: center; `; select.appendChild(option); }); select.addEventListener("change", (e) => { onChange(e.target.value); }); selectWrapper.appendChild(select); selectWrapper.appendChild(arrow); row.appendChild(labelEl); row.appendChild(selectWrapper); return row; } }; // src/controller/mixins/likes.js var methods2 = { updateLikeCounterUI(status) { if (!this.likeCounterContainer) return; const { remaining, used, limit, isInCooldown, cooldownFormatted, matched } = status; if (isInCooldown && this.likeCounterCooldownTimer) { const timeSpan = this.likeCounterContainer.querySelector(".like-cooldown-time"); if (timeSpan && cooldownFormatted) { timeSpan.textContent = cooldownFormatted; return; } } if (this.likeCounterCooldownTimer) { clearInterval(this.likeCounterCooldownTimer); this.likeCounterCooldownTimer = null; } let html = ""; if (isInCooldown && cooldownFormatted) { html = `
`; this.likeCounterContainer.style.background = "linear-gradient(135deg, #fff1f2 0%, #fff7f7 100%)"; this.likeCounterContainer.style.borderColor = "#fecdd3"; if (!this.likeCounterCooldownTimer) { this.likeCounterCooldownTimer = setInterval(() => { if (!this.likeCounter) return; const newFormatted = this.likeCounter.formatCooldown(); const timeSpan = this.likeCounterContainer?.querySelector(".like-cooldown-time"); if (timeSpan && newFormatted) { timeSpan.textContent = newFormatted; } else if (!newFormatted) { clearInterval(this.likeCounterCooldownTimer); this.likeCounterCooldownTimer = null; this.updateLikeCounterUI(this.likeCounter.getStatus()); } }, 1e3); } } else { const percentage = limit > 0 ? Math.round(remaining / limit * 100) : 0; const color = percentage > 50 ? "#15803d" : percentage > 20 ? "#b7791f" : "#dc2626"; html = `
${!matched ? `` : ""} \u2764\uFE0F ${this.t("likeRemaining")}
`; this.likeCounterContainer.style.background = "#f5f8fb"; this.likeCounterContainer.style.borderColor = "#e1e6eb"; } this.likeCounterContainer.innerHTML = html; const syncBtn = this.likeCounterContainer.querySelector(".like-sync-btn"); if (syncBtn) { syncBtn.onclick = async (e) => { e.stopPropagation(); syncBtn.textContent = "\u{1F504}"; syncBtn.style.animation = "spin 1s linear infinite"; await this.likeCounter.manualSync(); syncBtn.style.animation = ""; }; } const clearCooldownBtn = this.likeCounterContainer.querySelector(".like-cooldown-clear-btn"); if (clearCooldownBtn) { clearCooldownBtn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); clearCooldownBtn.disabled = true; const cleared = this.clearLikeCooldown(); if (!cleared) { clearCooldownBtn.disabled = false; this.showNotification(this.t("likeCooldownClearFailed")); return; } this.showNotification(this.t("likeCooldownCleared")); }; } }, async initializeSmartLikeTab() { try { let tab = {}; if (typeof GM_getTab === "function") { tab = await new Promise((resolve) => { const timeout = setTimeout(() => resolve({}), 3e3); try { GM_getTab((value) => { clearTimeout(timeout); resolve(value || {}); }); } catch (_) { clearTimeout(timeout); resolve({}); } }); } const tabId = tab.smartLikeTabId || this.smartLikeSessionId; const sameTab = tabId === this.smartLikeSessionId; this.smartLikeSessionId = tabId; if (typeof GM_saveTab === "function") { try { GM_saveTab({ ...tab, smartLikeTabId: tabId }); } catch (_) { } } const readObject = (key) => { const value = this.getSessionStorage(key, {}); return value && typeof value === "object" && !Array.isArray(value) ? value : {}; }; const generation = this.getSessionStorage("smartLikeGeneration", 0); this.smartLikeGeneration = (Number.isSafeInteger(generation) && generation >= 0 ? generation : 0) + 1; if (sameTab) { this.smartLikeCheckedPostIds = readObject("smartLikeProcessedPostIds"); const failed = this.getSessionStorage("smartLikeFailedPostIds", []); this.smartLikeFailedPostIds = new Set(Array.isArray(failed) ? failed.filter((key) => typeof key === "string") : []); const pending = this.getSessionStorage("smartLikePendingPostIds", []); if (Array.isArray(pending)) { for (const task of pending) { if (task?.phase === "requesting" && task.accountKey && task.postId) { this.smartLikeFailedPostIds.add(this.smartLikeDecisionKey(task.postId, task.accountKey)); } } } } else { this.smartLikeLastLikeAt = 0; } if (!this.setSessionStorage("smartLikeSessionId", this.smartLikeSessionId) || !this.setSessionStorage("smartLikeGeneration", this.smartLikeGeneration) || !this.persistSmartLikeSession() || !this.persistSmartLikePending()) { throw new Error("Session storage write failed"); } this.updateSmartLikeUI(); } catch (error) { this.smartLikeStorageReady = false; this.smartLikeEnabled = false; this.updateSmartLikeUI(); console.error("[\u667A\u80FD\u70B9\u8D5E] \u4F1A\u8BDD\u5B58\u50A8\u4E0D\u53EF\u7528\uFF0C\u5DF2\u6682\u505C\u53D1\u9001:", error); } }, smartLikeDecisionKey(postId, accountKey = this.smartLikeAccountKey) { return accountKey + "|" + postId; }, persistSmartLikePending() { if (!this.isSmartLikeGenerationCurrent()) return false; const task = this.smartLikeActiveTask; const pending = task && !task.pendingReleased && this.isSmartLikeGenerationCurrent(task) ? [{ postId: task.postId, topicId: task.topicId, accountKey: task.accountKey, phase: task.phase }] : []; if (!this.setSessionStorage("smartLikePendingPostIds", pending)) this.smartLikeStorageReady = false; return this.smartLikeStorageReady; }, updateSmartLikeUI() { const input = this.smartLikeToggleInput; if (input) input.checked = !!this.smartLikeEnabled; }, setSmartLikeEnabled(enabled) { const next = !!enabled; if (next && this.isLikeCoolingDown()) { this.smartLikeEnabled = false; Storage.set("smartLikeEnabled", false); this.updateSmartLikeUI(); this.showNotification(Number.isFinite(this.likeResumeTime) ? `${this.t("likeCoolingDown")}\uFF0C${new Date(this.likeResumeTime).toLocaleTimeString()}` : "\u70B9\u8D5E\u72B6\u6001\u6682\u4E0D\u53EF\u7528\uFF0C\u667A\u80FD\u70B9\u8D5E\u5DF2\u5173\u95ED"); return false; } if (next && !this.smartLikeStorageReady) { this.showNotification("\u70B9\u8D5E\u4F1A\u8BDD\u5B58\u50A8\u4E0D\u53EF\u7528\uFF0C\u8BF7\u6062\u590D\u5B58\u50A8\u540E\u5237\u65B0\u9875\u9762"); this.updateSmartLikeUI(); return false; } this.smartLikeEnabled = next; Storage.set("smartLikeEnabled", next); if (!next) this.cancelSmartLikeTasks("disabled"); this.updateSmartLikeUI(); if (next && this.autoRunning && this.getSmartLikeTopicId()) this.scheduleSmartLikeScan(); return true; }, isLikeCoolingDown() { this.likeResumeTime = LikeCooldown.read(this.smartLikeAccountKey); return !!(this.likeResumeTime && Date.now() < this.likeResumeTime); }, clearLikeCooldown() { const counterKey = this.likeCounter?.accountKey; const smartLikeKey = this.smartLikeAccountKey; const primaryKey = counterKey || smartLikeKey; if (!primaryKey) return false; const keys = /* @__PURE__ */ new Set([primaryKey]); const usernames = []; if (counterKey === primaryKey) usernames.push(this.likeCounter?.currentUser?.username); if (smartLikeKey === primaryKey) usernames.push(this.currentAccount?.username, this.currentUsername); for (const username of usernames) { const normalized = String(username || "").trim().replace(/^@/, "").trim().toLowerCase(); if (normalized) keys.add(`username:${normalized}`); } if (!LikeCooldown.clear(Array.from(keys))) return false; this.likeResumeTime = LikeCooldown.read(smartLikeKey); for (const accountKey of keys) { this.smartLike429Periods?.delete(accountKey || "legacy"); } this.likeCounter?.loadState(); this.likeCounter?.notifyUIUpdate(); return true; }, getSmartLikeSpeedConfig() { const probability = { slow: 0.05, normal: 0.15, fast: 0.25 }[this.smartLikeSpeed] ?? 0.15; return { probability, minWait: 200, maxWait: 500 }; }, persistSmartLikeSession() { if (!this.isSmartLikeGenerationCurrent()) return false; const results = [ this.setSessionStorage("smartLikeProcessedPostIds", this.smartLikeCheckedPostIds), this.setSessionStorage("smartLikeFailedPostIds", Array.from(this.smartLikeFailedPostIds)), this.setSessionStorage("smartLikeLastLikeAt", this.smartLikeLastLikeAt) ]; if (results.some((result) => !result)) this.smartLikeStorageReady = false; return this.smartLikeStorageReady; }, getSmartLikePostId(post) { const id = post?.dataset?.postId || post?.querySelector("article[data-post-id]")?.dataset.postId; return /^\d+$/.test(String(id || "")) ? String(id) : ""; }, getSmartLikeTopicId() { const parts = window.location.pathname.split("/").filter(Boolean); if (parts[0] !== "t") return ""; const id = /^\d+$/.test(parts[1] || "") ? parts[1] : parts[2]; return /^\d+$/.test(id || "") ? id : ""; }, async resolveSmartLikeAccount(force = false) { if (this.smartLikeAccountRequest) return this.smartLikeAccountRequest; const now = Date.now(); if (!force && now - (this.smartLikeAccountCheckedAt || 0) < 3e4) return this.currentAccount; if (Storage.get("session429Until", 0) > now) return null; this.smartLikeAccountRequest = (async () => { let user = null; try { const response = await fetch(`${BASE_URL}/session/current.json`, { credentials: "include", cache: "no-store", signal: AbortSignal.timeout(1e4) }); if (response.status === 429) Storage.set("session429Until", Date.now() + 30 * 60 * 1e3); if (response.ok) user = (await response.json())?.current_user || null; } catch (_) { } const id = Number(user?.id ?? user?.user_id); const username = typeof user?.username === "string" ? user.username.trim().replace(/^@/, "").trim().toLowerCase() : ""; const key = Number.isSafeInteger(id) && id > 0 ? `id:${id}` : username ? `username:${username}` : null; if (this.smartLikeAccountKey !== key) { this.cancelSmartLikeTasks("account_changed"); } this.smartLikeAccountKey = key; this.currentAccount = key ? { id, username, key } : null; this.smartLikeAccountCheckedAt = Date.now(); if (key?.startsWith("id:") && username) { const usernameKey = `username:${username}`; const usernameUntil = LikeCooldown.read(usernameKey); if (usernameUntil > LikeCooldown.read(key)) LikeCooldown.extend(key, usernameUntil); const prefix = `${usernameKey}|`; let merged = false; for (const [oldKey, value] of Object.entries(this.smartLikeCheckedPostIds)) { const newKey = `${key}|${oldKey.slice(prefix.length)}`; if (oldKey.startsWith(prefix) && !Object.hasOwn(this.smartLikeCheckedPostIds, newKey)) { this.smartLikeCheckedPostIds[newKey] = { ...value }; merged = true; } } for (const oldKey of Array.from(this.smartLikeFailedPostIds)) { const newKey = `${key}|${oldKey.slice(prefix.length)}`; if (oldKey.startsWith(prefix) && !this.smartLikeFailedPostIds.has(newKey)) { this.smartLikeFailedPostIds.add(newKey); merged = true; } } if (merged) this.persistSmartLikeSession(); } this.likeResumeTime = LikeCooldown.read(key); this.likeCounter?.setCurrentUser(key ? user : null); if (key) { this.currentUsername = username; this.loadSmartLikedPosts(); if (this.isLikeCoolingDown() && this.smartLikeEnabled) this.setSmartLikeEnabled(false); } return this.currentAccount; })(); try { return await this.smartLikeAccountRequest; } catch (error) { this.smartLikeStorageReady = false; console.error("[\u667A\u80FD\u70B9\u8D5E] \u8D26\u53F7\u8BB0\u5F55\u4E0D\u53EF\u7528\uFF0C\u5DF2\u6682\u505C\u53D1\u9001:", error); return null; } finally { this.smartLikeAccountRequest = null; } }, getSmartLikeStorageKey() { return this.smartLikeAccountKey ? `smartLikedPostIds_${encodeURIComponent(this.smartLikeAccountKey)}` : null; }, loadSmartLikedPosts() { this.smartLikedPostIds = /* @__PURE__ */ new Map(); const key = this.getSmartLikeStorageKey(); if (!key) return; const entries = this.readSmartLikedRecords(this.smartLikeAccountKey); const kept = entries.sort((a, b) => a.recordedAt - b.recordedAt).slice(-5e3); this.smartLikedPostIds = new Map(kept.map((item) => [item.postId, item.recordedAt])); }, readSmartLikedRecords(accountKey) { if (!accountKey) return []; const keys = [`smartLikedPostIds_${encodeURIComponent(accountKey)}`]; if (accountKey === this.currentAccount?.key && accountKey.startsWith("id:") && this.currentAccount.username) { keys.push(`smartLikedPostIds_${encodeURIComponent(`username:${this.currentAccount.username}`)}`); } const records = /* @__PURE__ */ new Map(); for (const key of keys) { const entries = JSON.parse(localStorage.getItem(key) || "[]"); if (!Array.isArray(entries)) throw new Error("Invalid liked post history"); for (const item of entries) { const postId = String(item?.postId || ""); const recordedAt = Number(item?.recordedAt); if (/^\d+$/.test(postId) && Number.isFinite(recordedAt) && recordedAt > 0 && (!records.has(postId) || recordedAt < records.get(postId))) { records.set(postId, recordedAt); } } } return Array.from(records, ([postId, recordedAt]) => ({ postId, recordedAt })); }, recordLikedPost(postId, accountKey = this.smartLikeAccountKey) { if (!accountKey || !postId) return false; const key = `smartLikedPostIds_${encodeURIComponent(accountKey)}`; let entries; try { entries = this.readSmartLikedRecords(accountKey); } catch (error) { this.smartLikeStorageReady = false; console.error("[\u667A\u80FD\u70B9\u8D5E] \u65E0\u6CD5\u8BFB\u53D6\u6210\u529F\u8BB0\u5F55\uFF0C\u5DF2\u6682\u505C\u540E\u7EED\u53D1\u9001:", error); return false; } const map = /* @__PURE__ */ new Map(); for (const item of [...entries, { postId: String(postId), recordedAt: Date.now() }]) { const id = String(item?.postId || ""); const time = Number(item?.recordedAt); if (!/^\d+$/.test(id) || !Number.isFinite(time) || time <= 0) continue; if (!map.has(id) || time < map.get(id)) map.set(id, time); } const kept = Array.from(map.entries()).filter(([post, time]) => post && Number.isFinite(time)).sort((a, b) => a[1] - b[1]).slice(-5e3).map(([post, time]) => ({ postId: post, recordedAt: time })); const saved = Storage.set(key, kept); if (!saved) { this.smartLikeStorageReady = false; console.error("[\u667A\u80FD\u70B9\u8D5E] \u6210\u529F\u8BB0\u5F55\u4FDD\u5B58\u5931\u8D25\uFF0C\u5DF2\u6682\u505C\u540E\u7EED\u53D1\u9001"); } if (accountKey === this.smartLikeAccountKey) this.smartLikedPostIds = new Map(kept.map((item) => [item.postId, item.recordedAt])); return saved; }, markSmartLikeChecked(postId, decision, reason = "", accountKey = this.smartLikeAccountKey) { this.smartLikeCheckedPostIds[this.smartLikeDecisionKey(postId, accountKey)] = { decision, reason, at: Date.now() }; this.persistSmartLikeSession(); }, getSmartLikeReactionState(post) { if (!post?.isConnected) return "unknown"; const actions = post.querySelector(".discourse-reactions-actions"); const button = post.querySelector('button[title="\u70B9\u8D5E\u6B64\u5E16\u5B50"], button[title="Like this post"], button.btn-toggle-reaction-like'); if (actions?.classList.contains("has-used-main-reaction") || button && (button.getAttribute("aria-pressed") === "true" || ["has-like", "my-likes", "liked"].some((name) => button.classList.contains(name)))) return "heart_active"; if (actions && /reacted/i.test(actions.className)) return "other_reaction"; if (!button || button.disabled || button.getAttribute("aria-disabled") === "true") return "unknown"; return "none"; }, isSmartLikeGenerationCurrent(task = { sessionId: this.smartLikeSessionId, generation: this.smartLikeGeneration }) { return task.sessionId === this.smartLikeSessionId && task.generation === this.smartLikeGeneration && task.sessionId === this.getSessionStorage("smartLikeSessionId") && task.generation === this.getSessionStorage("smartLikeGeneration"); }, canRunLikeFlow(task = null) { if (!this.smartLikeStorageReady || !this.isSmartLikeGenerationCurrent() || !this.smartLikeEnabled || !this.autoRunning || !this.isScrolling || !this.smartLikeAccountKey || !this.getSmartLikeTopicId() || this.isLikeCoolingDown()) return false; if (task && (!this.isSmartLikeGenerationCurrent(task) || task.runToken !== this.smartLikeRunToken || task.accountKey !== this.smartLikeAccountKey || task.topicId !== this.getSmartLikeTopicId())) return false; return true; }, shouldLikePost() { return this.smartLikeEnabled && Date.now() - this.smartLikeLastLikeAt >= 2e3 && Math.random() < this.getSmartLikeSpeedConfig().probability; }, async tryLikePost(post, task) { const key = this.smartLikeDecisionKey(task.postId, task.accountKey); const canAttempt = () => this.canRunLikeFlow(task) && post.isConnected && this.getSmartLikePostId(post) === task.postId && this.isLikeAllowedInCurrentCategory()?.allowed === true; const run = async () => { this.loadSmartLikedPosts(); if (!canAttempt() || this.smartLikedPostIds.has(task.postId) || this.smartLikeFailedPostIds.has(key)) return; const state = this.getSmartLikeReactionState(post); if (state !== "none") { if (state !== "unknown") this.markSmartLikeChecked(task.postId, "skip", state); return; } const { minWait, maxWait } = this.getSmartLikeSpeedConfig(); await Utils.sleep(Utils.random(minWait, maxWait)); if (!canAttempt() || this.getSmartLikeReactionState(post) !== "none") return; this.loadSmartLikedPosts(); if (this.smartLikedPostIds.has(task.postId)) return; this.smartLikeActiveTask = task; task.phase = "requesting"; if (!this.persistSmartLikePending()) return; const result = await this.sendLikeRequest(task.postId, task); const sameSession = this.isSmartLikeGenerationCurrent(task); if (result.success) { this.recordLikedPost(task.postId, task.accountKey); if (sameSession) { this.smartLikeLastLikeAt = Date.now(); this.markSmartLikeChecked(task.postId, "success", "http_success", task.accountKey); } console.log("[\u667A\u80FD\u70B9\u8D5E] \u70B9\u8D5E\u6210\u529F\uFF0C\u5E16\u5B50 ID=" + task.postId); } else if (result.rateLimited) { this.handleSmartLike429(result.waitSeconds, result.timeLeft, result.url, "PUT", { accountKey: task.accountKey, retryAfter: result.retryAfter, responseAt: result.responseAt }); } else if (sameSession && !result.cancelled) { this.smartLikeFailedPostIds.add(key); this.markSmartLikeChecked(task.postId, "failed", result.reason || "request_failed", task.accountKey); console.warn("[\u667A\u80FD\u70B9\u8D5E] \u70B9\u8D5E\u8BF7\u6C42\u5931\u8D25\uFF0C\u5E16\u5B50 ID=" + task.postId); } }; try { if (typeof navigator.locks?.request === "function") { await navigator.locks.request("smart-like:" + task.accountKey, { mode: "exclusive" }, run); } else { await run(); } } catch (error) { if (task.phase === "requesting" && this.isSmartLikeGenerationCurrent(task)) { this.smartLikeFailedPostIds.add(key); this.persistSmartLikeSession(); } console.error("[\u667A\u80FD\u70B9\u8D5E] \u5904\u7406\u5E16\u5B50\u5931\u8D25:", error); } finally { if (this.smartLikeActiveTask === task) this.smartLikeActiveTask = null; this.persistSmartLikePending(); } }, async sendLikeRequest(postId, task) { if (!this.canRunLikeFlow(task)) return { success: false, cancelled: true }; const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; if (!csrfToken) return { success: false, cancelled: true }; const url = BASE_URL + "/discourse-reactions/posts/" + encodeURIComponent(postId) + "/custom-reactions/heart/toggle.json"; if (!isHeartToggleUrl(url, "PUT")) return { success: false, cancelled: true }; const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 15e3); try { const response = await fetch(url, { method: "PUT", credentials: "include", headers: { "Content-Type": "application/json", "X-CSRF-Token": csrfToken }, signal: controller.signal }); if (response.ok) return { success: true }; const data = await response.json().catch(() => ({})); if (response.status === 429 || data?.error_type === "rate_limit") { return { success: false, rateLimited: true, url, waitSeconds: data?.extras?.wait_seconds, timeLeft: data?.extras?.time_left, retryAfter: response.headers?.get?.("Retry-After"), responseAt: Date.now() }; } return { success: false, reason: "http_" + response.status }; } catch (error) { return { success: false, reason: error?.name === "AbortError" ? "timeout" : "network_error" }; } finally { clearTimeout(timeout); } }, async scanSmartLikePosts() { await this.smartLikeReady; if (this.smartLikeIsScanning || !this.smartLikeStorageReady || !this.isSmartLikeGenerationCurrent() || !this.smartLikeEnabled || !this.autoRunning || !this.isScrolling || !this.getSmartLikeTopicId()) return; this.smartLikeIsScanning = true; this.smartLikeLastScanAt = Date.now(); try { await this.resolveSmartLikeAccount(); if (!this.canRunLikeFlow() || this.isLikeAllowedInCurrentCategory()?.allowed !== true) return; const topicId = this.getSmartLikeTopicId(); const runToken = this.smartLikeRunToken; const height = window.innerHeight || document.documentElement.clientHeight; for (const post of document.querySelectorAll('article[id^="post_"]')) { if (!this.canRunLikeFlow() || runToken !== this.smartLikeRunToken || topicId !== this.getSmartLikeTopicId()) break; const postId = this.getSmartLikePostId(post); const key = this.smartLikeDecisionKey(postId); if (!postId || this.smartLikeCheckedPostIds[key] || this.smartLikeFailedPostIds.has(key) || this.smartLikedPostIds?.has(postId)) continue; const rect = post.getBoundingClientRect(); if (rect.top >= height * 0.9 || rect.bottom <= height * 0.1) continue; const state = this.getSmartLikeReactionState(post); if (state === "unknown") continue; if (state !== "none") { this.markSmartLikeChecked(postId, "skip", state); continue; } this.markSmartLikeChecked(postId, "viewed", "probability_or_interval"); if (!this.shouldLikePost()) continue; await this.tryLikePost(post, { postId, topicId, accountKey: this.smartLikeAccountKey, sessionId: this.smartLikeSessionId, generation: this.smartLikeGeneration, runToken, createdAt: Date.now(), phase: "waiting" }); } } catch (error) { console.error("[\u667A\u80FD\u70B9\u8D5E] \u626B\u63CF\u5E16\u5B50\u5931\u8D25:", error); } finally { this.smartLikeIsScanning = false; } }, cancelSmartLikeTasks() { this.smartLikeRunToken++; clearTimeout(this.smartLikeScanTimer); this.smartLikeScanTimer = null; this.persistSmartLikePending(); }, scheduleSmartLikeScan() { if (this.smartLikeScanTimer) return; const delay = Math.max(0, 2e3 - (Date.now() - this.smartLikeLastScanAt)); this.smartLikeScanTimer = setTimeout(() => { this.smartLikeScanTimer = null; this.scanSmartLikePosts(); }, delay); }, bindSmartLikeScroll() { if (this.smartLikeScrollBound) return; this.smartLikeScrollBound = true; window.addEventListener("scroll", () => { if (this.smartLikeEnabled && this.autoRunning && this.isScrolling) this.scheduleSmartLikeScan(); }, { passive: true }); window.addEventListener("pagehide", () => this.cancelSmartLikeTasks("pagehide")); window.addEventListener("storage", (event) => { if (event.key === "likeResumeTime") { this.likeResumeTime = LikeCooldown.read(this.smartLikeAccountKey); this.likeCounter?.loadState(); this.likeCounter?.notifyUIUpdate(); if (this.isLikeCoolingDown()) this.setSmartLikeEnabled(false); } else if (event.key === "smartLikeEnabled" && event.newValue === "false") { this.setSmartLikeEnabled(false); } }); }, isLikeAllowedInCurrentCategory() { if (CURRENT_DOMAIN === "idcflare.com") { return { allowed: true, reason: "idcflare_no_restriction" }; } const config = CONFIG.likeAllowedCategories; if (!config || !config.allowed || config.allowed.length === 0) { return { allowed: true, reason: "no_config" }; } let subcategory = null; let parentCategory = null; const topicCategory = document.querySelector(".topic-category"); if (topicCategory) { const badges = topicCategory.querySelectorAll(".badge-category__name, .category-name"); const names = []; badges.forEach((badge) => { const name = badge.textContent?.trim(); if (name) names.push(name); }); if (names.length >= 2) { subcategory = names[0]; parentCategory = names[1]; } else if (names.length === 1) { parentCategory = names[0]; } } if (!parentCategory) { const headerCategory = document.querySelector(".extra-info-wrapper .badge-category__name"); if (headerCategory) { parentCategory = headerCategory.textContent?.trim(); } } const detectedCategories = []; if (subcategory) detectedCategories.push(subcategory); if (parentCategory) detectedCategories.push(parentCategory); if (detectedCategories.length === 0) { console.log("[\u677F\u5757\u68C0\u67E5] \u65E0\u6CD5\u83B7\u53D6\u5F53\u524D\u677F\u5757\u4FE1\u606F\uFF0C\u9ED8\u8BA4\u4E0D\u5141\u8BB8\u70B9\u8D5E"); return { allowed: false, reason: "category_not_found", categories: [] }; } if (detectedCategories.some((name) => config.excluded?.includes(name))) { return { allowed: false, reason: "excluded", categories: detectedCategories }; } console.log( "[\u677F\u5757\u68C0\u67E5] \u68C0\u6D4B\u5230\u677F\u5757:", detectedCategories.join(" > "), subcategory ? `(\u5B50\u7248\u5757: ${subcategory}, \u7236\u7248\u5757: ${parentCategory})` : `(\u9876\u7EA7\u677F\u5757: ${parentCategory})` ); if (subcategory) { if (config.excluded && config.excluded.includes(subcategory)) { console.log(`[\u677F\u5757\u68C0\u67E5] \u5B50\u7248\u5757 "${subcategory}" \u5728\u6392\u9664\u5217\u8868\u4E2D\uFF0C\u4E0D\u5141\u8BB8\u70B9\u8D5E`); return { allowed: false, reason: "subcategory_excluded", category: subcategory, categories: detectedCategories }; } if (config.allowed.includes(subcategory)) { console.log(`[\u677F\u5757\u68C0\u67E5] \u5B50\u7248\u5757 "${subcategory}" \u5728\u5141\u8BB8\u5217\u8868\u4E2D\uFF0C\u5141\u8BB8\u70B9\u8D5E`); return { allowed: true, reason: "subcategory_allowed", category: subcategory, categories: detectedCategories }; } if (parentCategory && config.allowed.includes(parentCategory)) { console.log(`[\u677F\u5757\u68C0\u67E5] \u5B50\u7248\u5757 "${subcategory}" \u4E0D\u5728\u5141\u8BB8\u5217\u8868\uFF0C\u7236\u7248\u5757 "${parentCategory}" \u5728\u5141\u8BB8\u5217\u8868\u4E2D\uFF0C\u5141\u8BB8\u70B9\u8D5E`); return { allowed: true, reason: "parent_allowed", category: parentCategory, subcategory, categories: detectedCategories }; } console.log(`[\u677F\u5757\u68C0\u67E5] \u5B50\u7248\u5757 "${subcategory}" \u548C\u7236\u7248\u5757 "${parentCategory}" \u90FD\u4E0D\u5728\u5141\u8BB8\u5217\u8868\u4E2D\uFF0C\u4E0D\u5141\u8BB8\u70B9\u8D5E`); return { allowed: false, reason: "not_in_allowed_list", categories: detectedCategories }; } else { if (config.excluded && config.excluded.includes(parentCategory)) { console.log(`[\u677F\u5757\u68C0\u67E5] \u677F\u5757 "${parentCategory}" \u5728\u6392\u9664\u5217\u8868\u4E2D\uFF0C\u4E0D\u5141\u8BB8\u70B9\u8D5E`); return { allowed: false, reason: "excluded", category: parentCategory, categories: detectedCategories }; } if (config.allowed.includes(parentCategory)) { console.log(`[\u677F\u5757\u68C0\u67E5] \u677F\u5757 "${parentCategory}" \u5728\u5141\u8BB8\u5217\u8868\u4E2D\uFF0C\u5141\u8BB8\u70B9\u8D5E`); return { allowed: true, reason: "allowed", category: parentCategory, categories: detectedCategories }; } console.log(`[\u677F\u5757\u68C0\u67E5] \u677F\u5757 "${parentCategory}" \u4E0D\u5728\u5141\u8BB8\u5217\u8868\u4E2D\uFF0C\u4E0D\u5141\u8BB8\u70B9\u8D5E`); return { allowed: false, reason: "not_in_allowed_list", categories: detectedCategories }; } } }; // src/controller/mixins/reader.js var methods3 = { getPageContextKey(urlString = window.location.href) { try { const base = window.location?.origin || BASE_URL; const url = new URL(urlString, base); const topicMatch = url.pathname.match(/^\/t\/topic\/(\d+)(?:\/\d+)?\/?$/); if (topicMatch) return `topic:${topicMatch[1]}`; return `${url.pathname}${url.search}`; } catch (_) { return String(urlString || ""); } }, getTopicProgressSnapshot() { const { scrollHeight } = document.documentElement || {}; return { scrollHeight: scrollHeight || 0, postCount: document.querySelectorAll(".topic-post").length, maxLoadedPostNumber: this.getLoadedMaxPostNumber() }; }, parseCssPixels(value) { const parsed = Number.parseFloat(value); return Number.isFinite(parsed) ? parsed : 0; }, isBlueTimelineColor(value) { const rgb = String(value || "").match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/i); if (!rgb) return false; const red = Number(rgb[1]); const green = Number(rgb[2]); const blue = Number(rgb[3]); const alpha = rgb[4] === void 0 ? 1 : Number(rgb[4]); return alpha > 0.35 && blue >= 120 && green >= 90 && red <= 80; }, isBlueDotStyle(style, fallbackRect = null) { if (!style || style.display === "none" || style.visibility === "hidden") return false; if (Number(style.opacity || 1) < 0.2) return false; const width = this.parseCssPixels(style.width) || fallbackRect?.width || 0; const height = this.parseCssPixels(style.height) || fallbackRect?.height || 0; const radius = this.parseCssPixels(style.borderTopLeftRadius); const isSmallRound = width >= 4 && width <= 18 && height >= 4 && height <= 18 && Math.abs(width - height) <= 6 && radius >= Math.min(width, height) * 0.35; if (!isSmallRound) return false; return [style.backgroundColor, style.borderColor, style.color].some((color) => this.isBlueTimelineColor(color)); }, hasUnreadTimelineBlueDot() { const containers = Array.from(document.querySelectorAll([ ".topic-timeline", ".timeline-scrollarea", ".timeline-container", ".timeline-replies", ".topic-navigation" ].join(","))); if (containers.length === 0) return false; return containers.some((container) => Array.from(container.querySelectorAll("*")).some((element) => { const rect = element.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return false; const directStyle = window.getComputedStyle(element); if (this.isBlueDotStyle(directStyle, rect)) return true; return ["::before", "::after"].some((pseudo) => { const pseudoStyle = window.getComputedStyle(element, pseudo); if (!pseudoStyle || pseudoStyle.content === "none") return false; return this.isBlueDotStyle(pseudoStyle); }); })); }, nudgeScrollForReadConfirmation() { const pixels = CONFIG.scroll.readConfirmationNudgePixels; window.dispatchEvent(new Event("scroll")); window.scrollBy({ top: -pixels, behavior: "auto" }); window.scrollBy({ top: pixels * 2, behavior: "smooth" }); }, async waitForTopicReadConfirmation() { if (!this.isTopicPage) return true; let clearChecks = 0; for (let attempt = 0; attempt < CONFIG.scroll.readConfirmationChecks; attempt++) { if (!this.autoRunning) return false; if (!this.hasUnreadTimelineBlueDot()) { clearChecks++; if (clearChecks >= CONFIG.scroll.readConfirmationClearChecks) return true; } else { clearChecks = 0; console.log("[\u5B8C\u6574\u9605\u8BFB] \u68C0\u6D4B\u5230\u697C\u5C42\u5C0F\u84DD\u70B9\u672A\u6D88\u5931\uFF0C\u7EE7\u7EED\u7B49\u5F85\u9605\u8BFB\u786E\u8BA4..."); this.nudgeScrollForReadConfirmation(); } await Utils.sleep(CONFIG.scroll.readConfirmationDelay); } return !this.hasUnreadTimelineBlueDot(); }, getLoadedMaxPostNumber() { const posts = Array.from(document.querySelectorAll(".topic-post[data-post-number]")); return posts.reduce((max, post) => { const number = Number.parseInt(post.getAttribute("data-post-number") || "0", 10); return Number.isFinite(number) ? Math.max(max, number) : max; }, 0); }, async getCurrentTopicMaxPostNumber() { const topicIdMatch = window.location.pathname.match(/\/t\/topic\/(\d+)/); const topicId = topicIdMatch ? topicIdMatch[1] : null; if (!topicId) return this.getLoadedMaxPostNumber(); if (this._topicMaxPostNumberTopicId === topicId && Number.isFinite(this._topicMaxPostNumber)) { return this._topicMaxPostNumber; } const url = new URL(window.location.href); url.hash = ""; const jsonUrl = url.toString().replace(/\/$/, "") + ".json"; try { const response = await fetch(jsonUrl, { credentials: "include" }); const data = response && response.ok ? await response.json() : null; const maxPostNumber = Number(data?.highest_post_number || data?.posts_count || 0); this._topicMaxPostNumberTopicId = topicId; this._topicMaxPostNumber = Number.isFinite(maxPostNumber) && maxPostNumber > 0 ? maxPostNumber : this.getLoadedMaxPostNumber(); return this._topicMaxPostNumber; } catch (_) { return this.getLoadedMaxPostNumber(); } }, hasTopicProgressChanged(previous, current) { return current.scrollHeight > previous.scrollHeight + 50 || current.postCount > previous.postCount || current.maxLoadedPostNumber > previous.maxLoadedPostNumber; }, async isTopicFullyRead() { const settleChecks = CONFIG.scroll.bottomSettleChecks; const settleDelay = CONFIG.scroll.bottomSettleDelay; const targetMaxPostNumber = await this.getCurrentTopicMaxPostNumber(); let previous = this.getTopicProgressSnapshot(); for (let attempt = 0; attempt < settleChecks; attempt++) { await Utils.sleep(settleDelay); if (!this.autoRunning || !this.isScrolling) return false; if (!Utils.isNearBottom() || !Utils.isPageLoaded()) return false; const current = this.getTopicProgressSnapshot(); if (targetMaxPostNumber > 0 && current.maxLoadedPostNumber < targetMaxPostNumber) return false; if (this.hasTopicProgressChanged(previous, current)) return false; if (this.hasUnreadTimelineBlueDot()) { console.log("[\u5B8C\u6574\u9605\u8BFB] \u697C\u5C42\u5C0F\u84DD\u70B9\u4ECD\u5B58\u5728\uFF0C\u6682\u4E0D\u8DF3\u8F6C\u4E0B\u4E00\u7BC7"); this.nudgeScrollForReadConfirmation(); return false; } previous = current; } const reachedLastPost = targetMaxPostNumber <= 0 || previous.maxLoadedPostNumber >= targetMaxPostNumber; return reachedLastPost && await this.waitForTopicReadConfirmation(); }, isPageLoadingStalled(timeOnPage) { return this.isTopicPage && timeOnPage > CONFIG.time.topicLoadingTimeout && !Utils.isPageLoaded(); }, getScrollPlan(shouldReadFullTopic) { if (!shouldReadFullTopic) { const delay = Utils.random(CONFIG.scroll.minSpeed, CONFIG.scroll.maxSpeed); const distance = Utils.random(CONFIG.scroll.minDistance, CONFIG.scroll.maxDistance); return { step: distance * CONFIG.scroll.stepMultiplier, behavior: "smooth", delay, allowFastScroll: true }; } const viewportHeight = Math.max( window.innerHeight || 0, document.documentElement?.clientHeight || 0, CONFIG.scroll.fullTopicMinStep ); const getStepRange = (minRatio, maxRatio) => { const minStep2 = Math.max(12, Math.floor(viewportHeight * minRatio)); const maxStep2 = Math.max(minStep2, Math.floor(viewportHeight * maxRatio)); return [minStep2, maxStep2]; }; const roll = Utils.random(1, 100); if (roll <= CONFIG.scroll.fullTopicBacktrackChance) { const [minStep2, maxStep2] = getStepRange( CONFIG.scroll.fullTopicBacktrackMinRatio, CONFIG.scroll.fullTopicBacktrackMaxRatio ); return { step: -Utils.random(minStep2, maxStep2), behavior: "smooth", delay: Utils.random(CONFIG.scroll.fullTopicBacktrackMinSpeed, CONFIG.scroll.fullTopicBacktrackMaxSpeed), allowFastScroll: false }; } const useSlowStep = roll <= CONFIG.scroll.fullTopicSlowChance; const [minStep, maxStep] = useSlowStep ? getStepRange(CONFIG.scroll.fullTopicSmallStepMinRatio, CONFIG.scroll.fullTopicSmallStepMaxRatio) : getStepRange(CONFIG.scroll.fullTopicNormalStepMinRatio, CONFIG.scroll.fullTopicNormalStepMaxRatio); const [minDelay, maxDelay] = useSlowStep ? [CONFIG.scroll.fullTopicSlowMinSpeed, CONFIG.scroll.fullTopicSlowMaxSpeed] : [CONFIG.scroll.fullTopicNormalMinSpeed, CONFIG.scroll.fullTopicNormalMaxSpeed]; return { step: Utils.random(minStep, maxStep), behavior: "smooth", delay: Utils.random(minDelay, maxDelay), allowFastScroll: false }; }, async recoverFromStuck() { console.log("\u{1F527} \u5F00\u59CB\u6062\u590D\u6D41\u7A0B..."); this.stopScrolling(); await Utils.sleep(1e3); if (!this.autoRunning) return; if (this.isTopicPage) { console.log("\u{1F4D6} \u5728\u6587\u7AE0\u9875\uFF0C\u91CD\u65B0\u5F00\u59CB\u6EDA\u52A8"); this.startScrolling(); } else { console.log("\u{1F4CB} \u5728\u5217\u8868\u9875\uFF0C\u5C1D\u8BD5\u5BFC\u822A\u5230\u4E0B\u4E00\u7BC7"); if (this.topicList.length === 0) { await this.getLatestTopics(); if (!this.autoRunning) return; } await this.navigateNextTopic(); } this.pageLoadTime = Date.now(); }, handleButtonClick() { if (this.isScrolling || this.autoRunning) { this.stopAutoReading(); } else { if (this.stopOnLikeLimitEnabled) { const likeStatus = this.likeCounter?.getStatus?.(); const isLikeCounterCooldown = likeStatus && likeStatus.isInCooldown; const isOldCooldown = this.likeResumeTime && Date.now() < this.likeResumeTime; const hasNoRemainingLikes = likeStatus && likeStatus.remaining === 0; if (isLikeCounterCooldown || isOldCooldown || hasNoRemainingLikes) { console.log(`[\u70B9\u8D5E\u4E0A\u9650] \u70B9\u8D5E\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u65E0\u6CD5\u5F00\u59CB\u9605\u8BFB (cooldown: ${isLikeCounterCooldown}, oldCooldown: ${isOldCooldown}, noRemaining: ${hasNoRemainingLikes})`); this.showNotification(this.t("stoppedByLikeLimit")); return; } } this.autoRunning = true; this.setSessionStorage("autoRunning", true); this.button.innerHTML = `\u23F8${this.t("stopReading")}`; this.button.classList.add("running"); this.startNavigationGuard(); if (this.accountDataSection && this.accountDataSectionContent) { this.setSectionExpanded(this.accountDataSection, this.accountDataSectionContent, false); } if (this.accountSection && this.accountSectionContent) { if (this.accountSection.getAttribute("aria-expanded") === "true") { this.setSectionExpanded(this.accountSection, this.accountSectionContent, false); ; } } if (this.autoSectionContent) { const autoSection = this.autoSection; if (autoSection && autoSection.classList.contains("collapsed")) { autoSection.classList.remove("collapsed"); this.setSectionExpanded(this.autoSection, this.autoSectionContent, true); } } if (!this.firstUseChecked) { this.handleFirstUse(); } else if (this.isTopicPage) { this.startScrolling(); } else { this.getLatestTopics().then(() => { if (!this.autoRunning) return; this.navigateNextTopic(); }); } } }, async handleFirstUse() { if (!this.autoRunning) return; if (CURRENT_DOMAIN !== "linux.do") { console.log("\u975E linux.do \u57DF\u540D\uFF0C\u8DF3\u8FC7\u65B0\u624B\u6559\u7A0B"); Storage.set("firstUseChecked", true); this.firstUseChecked = true; await this.getLatestTopics(); if (!this.autoRunning) return; await this.navigateNextTopic(); return; } if (!this.selectedPost) { const randomIndex = Math.floor(Math.random() * CONFIG.mustRead.posts.length); this.selectedPost = CONFIG.mustRead.posts[randomIndex]; Storage.set("selectedPost", this.selectedPost); console.log(`\u968F\u673A\u9009\u62E9\u6587\u7AE0: ${this.selectedPost.url}`); window.location.href = this.selectedPost.url; return; } const currentUrl = window.location.href; if (currentUrl.includes(this.selectedPost.url)) { Storage.set("firstUseChecked", true); this.firstUseChecked = true; if (this.isTopicPage) this.startScrolling(); else { await this.getLatestTopics(); if (this.autoRunning) await this.navigateNextTopic(); } } else { window.location.href = this.selectedPost.url; } }, async getLatestTopics() { if (!this.autoRunning) return []; let topicList = []; let retryCount = 0; let totalSkipped = 0; let emptyPageCount = 0; let topicLimit = this.topicLimitCount || 100; if (this.stopAfterReadEnabled) { const remainingToRead = this.stopAfterReadCount - this.currentSessionReadCount; if (remainingToRead > 0) { topicLimit = remainingToRead; console.log(`[\u9605\u8BFB\u9650\u5236] \u5F00\u542F\u9605\u8BFB\u9650\u5236\uFF0C\u672C\u6B21\u53EA\u83B7\u53D6 ${topicLimit} \u7BC7\u5E16\u5B50\uFF08\u9650\u5236${this.stopAfterReadCount}\u7BC7\uFF0C\u5DF2\u8BFB${this.currentSessionReadCount}\u7BC7\uFF09`); } else { console.log(`[\u9605\u8BFB\u9650\u5236] \u5DF2\u8FBE\u5230\u9605\u8BFB\u9650\u5236\uFF0C\u4E0D\u518D\u83B7\u53D6\u65B0\u5E16\u5B50`); topicLimit = 0; } } const maxPagesPerFetch = Math.max(50, Math.ceil(topicLimit / 5)); let startPage = this.getStartPage(); let page = startPage; const maxPage = startPage + maxPagesPerFetch - 1; const endpoint = this.readUnreadEnabled ? "unread" : "latest"; const topicType = this.readUnreadEnabled ? this.t("unreadTopics") : this.t("latestTopics"); console.log(`[\u9875\u7801\u7EED\u8BFB] \u4ECE\u7B2C${startPage}\u9875\u5F00\u59CB\u83B7\u53D6${this.readUnreadEnabled ? "\u672A\u8BFB" : "\u6700\u65B0"}\u5E16\u5B50\uFF08\u9650\u5236\uFF1A${topicLimit}\u7BC7\uFF0C\u6700\u5927\u7FFB\u5230\u7B2C${maxPage}\u9875\uFF09...`); this.updateTopicStatus({ fetching: true, type: topicType, current: 0, target: topicLimit, skipped: 0, startPage }); while (topicList.length < topicLimit && retryCount < CONFIG.article.retryLimit && page <= maxPage) { try { const response = await fetch(`${BASE_URL}/${endpoint}.json?no_definitions=true&page=${page}`); if (!this.autoRunning) return topicList; if (response.status === 502) { this.showNotification(this.t("stoppedByServerBusy")); this.stopAutoReading(); return topicList; } const data = await response.json(); if (!this.autoRunning) return topicList; if (data?.topic_list?.topics && data.topic_list.topics.length > 0) { emptyPageCount = 0; let filteredTopics = data.topic_list.topics.filter( (topic) => topic.posts_count < CONFIG.article.commentLimit ); if (this.skipReadEnabled) { const beforeCount = filteredTopics.length; filteredTopics = filteredTopics.filter( (topic) => !this.isTopicRead(topic.id.toString()) ); const skippedCount = beforeCount - filteredTopics.length; if (skippedCount > 0) { totalSkipped += skippedCount; console.log(`\u7B2C${page}\u9875\uFF1A\u8DF3\u8FC7\u4E86 ${skippedCount} \u7BC7\u5DF2\u8BFB\u5E16\u5B50\uFF0C\u83B7\u53D6 ${filteredTopics.length} \u7BC7\u672A\u8BFB`); } } topicList.push(...filteredTopics); if (topicList.length > 1) { const seenTopicIds = /* @__PURE__ */ new Set(); const beforeDedupCount = topicList.length; topicList = topicList.filter((topic) => { const topicId = String(topic?.id ?? ""); if (!topicId) return false; if (seenTopicIds.has(topicId)) { return false; } seenTopicIds.add(topicId); return true; }); const dedupedCount = beforeDedupCount - topicList.length; if (dedupedCount > 0) { console.log(`[\u53BB\u91CD] \u7B2C${page}\u9875\u5408\u5E76\u540E\u79FB\u9664\u4E86 ${dedupedCount} \u7BC7\u91CD\u590D topic`); } } page++; this.updateTopicStatus({ fetching: true, type: topicType, current: topicList.length, target: topicLimit, skipped: totalSkipped, page: page - 1, maxPages: maxPage, startPage }); if (this.readUnreadEnabled && data.topic_list.topics.length === 0) { console.log("\u672A\u8BFB\u5E16\u5B50\u6A21\u5F0F\uFF1AAPI\u8FD4\u56DE\u7A7A\u6570\u636E\uFF0C\u6CA1\u6709\u66F4\u591A\u672A\u8BFB\u5E16\u5B50"); break; } } else { emptyPageCount++; console.log(`\u7B2C${page}\u9875\uFF1AAPI\u8FD4\u56DE\u7A7A\u6570\u636E\uFF08\u8FDE\u7EED${emptyPageCount}\u9875\u4E3A\u7A7A\uFF09`); if (emptyPageCount >= 3) { if (startPage > 1 && topicList.length < topicLimit) { console.log(`[\u9875\u7801\u7EED\u8BFB] \u8FDE\u7EED${emptyPageCount}\u9875\u4E3A\u7A7A\uFF0C\u4ECE\u7B2C1\u9875\u91CD\u65B0\u626B\u63CF...`); page = 1; startPage = 1; emptyPageCount = 0; this.lastFetchedPage = 0; this.setSessionStorage("lastFetchedPage", 0); continue; } break; } page++; } } catch (error) { console.error("\u83B7\u53D6\u6587\u7AE0\u5217\u8868\u5931\u8D25:", error); retryCount++; await Utils.sleep(1e3); if (!this.autoRunning) return topicList; } } const finalPage = page - 1; this.lastFetchedPage = finalPage; this.setSessionStorage("lastFetchedPage", finalPage); this.saveHistoricalMaxPage(finalPage); const reachedMaxPages = page > maxPage && topicList.length < topicLimit; if (reachedMaxPages) { console.log(`[\u9875\u7801\u7EED\u8BFB] \u5DF2\u8FBE\u5230\u672C\u6B21\u6700\u5927\u9875\u6570 ${maxPage} \u9875\uFF0C\u83B7\u53D6\u5230 ${topicList.length}/${topicLimit} \u7BC7\u5E16\u5B50`); } if (topicList.length > topicLimit) { topicList = topicList.slice(0, topicLimit); } if (topicList.length > 1) { const seenTopicIds = /* @__PURE__ */ new Set(); const beforeDedupCount = topicList.length; topicList = topicList.filter((topic) => { const topicId = String(topic?.id ?? ""); if (!topicId) return false; if (seenTopicIds.has(topicId)) { return false; } seenTopicIds.add(topicId); return true; }); const dedupedCount = beforeDedupCount - topicList.length; if (dedupedCount > 0) { console.log(`[\u53BB\u91CD] \u6700\u7EC8\u5217\u8868\u79FB\u9664\u4E86 ${dedupedCount} \u7BC7\u91CD\u590D topic`); } } if (this.randomOrderEnabled && topicList.length > 1) { topicList = this.shuffleArray(topicList); console.log("\u5DF2\u968F\u673A\u6253\u4E71\u5E16\u5B50\u987A\u5E8F"); } this.topicList = topicList; this.setSessionStorage("topicList", topicList); console.log(`[\u9875\u7801\u7EED\u8BFB] \u5DF2\u83B7\u53D6 ${topicList.length} \u7BC7${this.readUnreadEnabled ? "\u672A\u8BFB" : "\u6700\u65B0"}\u6587\u7AE0${this.randomOrderEnabled ? "\uFF08\u968F\u673A\u987A\u5E8F\uFF09" : ""}\uFF08\u7B2C${startPage}-${finalPage}\u9875\uFF09`); this.updateTopicStatus({ fetching: false, type: topicType, current: topicList.length, target: topicLimit, skipped: totalSkipped, ready: true, reachedMaxPages, totalPages: finalPage - startPage + 1, startPage, endPage: finalPage }); if (topicList.length === 0) { if (this.readUnreadEnabled) { this.showNotification(this.t("noUnreadPosts")); this.readUnreadEnabled = false; Storage.set("readUnreadEnabled", false); this.resetPageProgress(); await this.getLatestTopics(); if (!this.autoRunning) return topicList; } else if (this.skipReadEnabled && startPage > 1) { console.log("[\u9875\u7801\u7EED\u8BFB] \u9AD8\u9875\u7801\u65E0\u672A\u8BFB\u5E16\u5B50\uFF0C\u4ECE\u7B2C1\u9875\u91CD\u65B0\u5F00\u59CB"); this.resetPageProgress(); this.historicalMaxPage = 0; Storage.set("historicalMaxPageData", null); await this.getLatestTopics(); if (!this.autoRunning) return topicList; } else if (this.skipReadEnabled) { this.showNotification("\u6240\u6709\u5E16\u5B50\u90FD\u5DF2\u8BFB\u8FC7\uFF0C\u505C\u6B62\u9605\u8BFB"); this.stopAutoReading(); } } return topicList; }, updateTopicStatus(status) { if (!this.topicStatusContainer || !this.topicStatusContent) return; this.topicStatusContainer.style.display = "block"; if (this.topicStatusHideTimer) { clearTimeout(this.topicStatusHideTimer); this.topicStatusHideTimer = null; } const progressPercent = status.target > 0 ? Math.min(100, Math.round(status.current / status.target * 100)) : 0; let html = ""; if (status.fetching) { this.topicStatusMode = "fetching"; const startPageInfo = status.startPage > 1 ? `\u4ECE\u7B2C${status.startPage}\u9875` : ""; const pageInfo = status.page ? `\uFF08${startPageInfo ? startPageInfo + "\u8D77\uFF0C" : ""}\u5F53\u524D\u7B2C${status.page}\u9875\uFF09` : ""; html = `
${this.t("fetchingTopics")} ${status.type} ${pageInfo}
${progressPercent}%
${this.t("totalFetched")}: ${status.current}/${status.target} ${status.skipped > 0 ? `${this.t("skippedRead")}: ${status.skipped}` : ""}
`; } else if (status.ready) { this.topicStatusMode = "ready"; const pagesInfo = status.startPage && status.endPage ? `\uFF08\u7B2C${status.startPage}-${status.endPage}\u9875\uFF0C\u5171${status.totalPages}\u9875\uFF09` : status.totalPages ? `\uFF08\u5171${status.totalPages}\u9875\uFF09` : ""; const reachedMaxInfo = status.reachedMaxPages ? `
\u26A0\uFE0F \u5DF2\u8FBE\u6700\u5927\u7FFB\u9875\u6570\uFF0C\u672A\u80FD\u83B7\u53D6\u8DB3\u591F\u5E16\u5B50
` : ""; html = `
\u2705 ${this.t("topicsReady")} ${pagesInfo}
${status.type}: ${status.current} \u7BC7 ${status.skipped > 0 ? `${this.t("skippedRead")}: ${status.skipped}` : ""}
${reachedMaxInfo} `; this.topicStatusHideTimer = setTimeout(() => { this.topicStatusHideTimer = null; if (!this.isScrolling && this.topicStatusContainer && !this.hasTopicStatusNotification()) { this.topicStatusContainer.style.display = "none"; } }, 3e3); } else { this.topicStatusMode = null; } this.topicStatusContent.innerHTML = html; }, updateReadingStatus() { if (!this.topicStatusContainer || !this.topicStatusContent || !this.autoRunning) return; if (this.topicStatusHideTimer) { clearTimeout(this.topicStatusHideTimer); this.topicStatusHideTimer = null; } this.topicStatusMode = "reading"; const remaining = this.topicList.length; const topicType = this.readUnreadEnabled ? this.t("unreadTopics") : this.t("latestTopics"); const skipped = this.skippedReadCount || 0; const todayRead = this.todayReadCount || 0; this.topicStatusContainer.style.display = "block"; this.topicStatusContent.innerHTML = `
${this.t("currentReading")}: ${topicType} ${this.t("remainingTopics")}: ${remaining}
\u{1F4C5} ${this.t("todayRead")}: ${todayRead} ${skipped > 0 ? `\u23ED\uFE0F ${this.t("skippedRead")}: ${skipped}` : ""}
`; }, shuffleArray(array) { const shuffled = [...array]; for (let i = shuffled.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]; } return shuffled; }, async getNextTopic() { if (this.topicList.length === 0) { await this.getLatestTopics(); } while (this.topicList.length > 0) { const topic = this.topicList.shift(); if (this.skipReadEnabled && this.isTopicRead(topic.id.toString())) { console.log(`\u8DF3\u8FC7\u5DF2\u8BFB\u5E16\u5B50: ${topic.title} (ID: ${topic.id})`); this.skippedReadCount++; this.setSessionStorage("skippedReadCount", this.skippedReadCount); this.setSessionStorage("topicList", this.topicList); this.updateReadingStatus(); continue; } this.setSessionStorage("topicList", this.topicList); return topic; } return null; }, async startScrolling() { if (this.isScrolling) return; this.isScrolling = true; this.button.innerHTML = `\u23F8${this.t("stopReading")}`; this.button.classList.add("running"); this.lastActionTime = Date.now(); const shouldReadFullTopic = this.fullTopicReadEnabled && this.isTopicPage; if (shouldReadFullTopic) { window.scrollTo({ top: 0, behavior: "auto" }); await Utils.sleep(300); } if (this.isTopicPage) { this.bindSmartLikeScroll(); if (this.smartLikeEnabled) this.scanSmartLikePosts(); if (shouldReadFullTopic) { window.scrollTo({ top: 0, behavior: "auto" }); await Utils.sleep(300); } } this.scrollStartTime = Date.now(); const maxScrollTime = shouldReadFullTopic ? null : 3e4; while (this.isScrolling) { const scrollPlan = this.getScrollPlan(shouldReadFullTopic); window.scrollBy({ top: scrollPlan.step, behavior: scrollPlan.behavior }); if (Utils.isNearBottom()) { await Utils.sleep(800); if (!this.autoRunning) break; const canNavigate = shouldReadFullTopic ? await this.isTopicFullyRead() : Utils.isNearBottom() && Utils.isPageLoaded(); if (canNavigate) { console.log("\u5DF2\u5230\u8FBE\u9875\u9762\u5E95\u90E8\uFF0C\u51C6\u5907\u5BFC\u822A\u5230\u4E0B\u4E00\u7BC7\u6587\u7AE0..."); await Utils.sleep(1e3); if (!this.autoRunning) break; this.isScrolling = false; await this.navigateNextTopic(); return; } } const scrolledTime = Date.now() - this.scrollStartTime; if (maxScrollTime !== null && scrolledTime > maxScrollTime) { if (this.isTopicPage && this.hasUnreadTimelineBlueDot()) { console.log("[\u9605\u8BFB\u786E\u8BA4] \u68C0\u6D4B\u5230\u697C\u5C42\u5C0F\u84DD\u70B9\u672A\u6D88\u5931\uFF0C\u53D6\u6D88\u672C\u6B21\u5F3A\u5236\u8DF3\u8F6C\u5E76\u7EE7\u7EED\u7B49\u5F85..."); this.nudgeScrollForReadConfirmation(); this.scrollStartTime = Date.now(); continue; } console.log(`\u5DF2\u5728\u5F53\u524D\u9875\u9762\u6EDA\u52A8${Math.floor(scrolledTime / 1e3)}\u79D2\uFF0C\u5F3A\u5236\u8DF3\u8F6C\u5230\u4E0B\u4E00\u7BC7\u6587\u7AE0...`); this.isScrolling = false; await this.navigateNextTopic(); return; } await Utils.sleep(scrollPlan.delay); this.accumulateTime(); if (scrollPlan.allowFastScroll && Math.random() < CONFIG.scroll.fastScrollChance) { const fastScroll = Utils.random(CONFIG.scroll.fastScrollMin, CONFIG.scroll.fastScrollMax); window.scrollBy({ top: fastScroll, behavior: "smooth" }); await Utils.sleep(200); } } }, stopScrolling() { this.isScrolling = false; this.cancelSmartLikeTasks("reading_paused"); this.button.innerHTML = `\u25B6${this.t("startReading")}`; this.button.classList.remove("running"); }, accumulateTime() { const now = Date.now(); this.accumulatedTime += now - this.lastActionTime; this.setSessionStorage("accumulatedTime", this.accumulatedTime); this.lastActionTime = now; if (this.accumulatedTime >= CONFIG.time.browseTime) { this.accumulatedTime = 0; this.setSessionStorage("accumulatedTime", 0); this.pauseForRest(); } }, async pauseForRest() { this.stopScrolling(); const restMinutes = Math.floor(CONFIG.time.restTime / 6e4); console.log(`\u4F11\u606F${restMinutes}\u5206\u949F...`); this.showNotification(`\u23F8\uFE0F ${this.t("restStart")} ${restMinutes} ${this.t("minutes")}`); await Utils.sleep(CONFIG.time.restTime); console.log("\u4F11\u606F\u7ED3\u675F\uFF0C\u7EE7\u7EED\u6D4F\u89C8..."); this.showNotification(`\u2705 ${this.t("restEnd")}`); this.startScrolling(); }, getCurrentTopicIdFromLocation() { const match = window.location.pathname.match(/\/t\/topic\/(\d+)/); return match ? String(match[1]) : null; }, async waitForTopicNavigation(targetTopicId, timeout = 8e3) { const start = Date.now(); const target = String(targetTopicId); while (Date.now() - start < timeout) { const currentTopicId = this.getCurrentTopicIdFromLocation(); if (currentTopicId === target) { await Utils.sleep(300); const topicContent = document.querySelector(".topic-post article, article[data-post-id], .post-stream"); if (topicContent) return true; } await Utils.sleep(150); } return false; }, async onTopicNavigated(targetTopicId) { console.log(`[\u5BFC\u822A\u540E] \u5F00\u59CB\u5904\u7406\u65B0\u5E16\u5B50: ${targetTopicId}`); this.isTopicPage = window.location.pathname.includes("/t/topic/"); this.pageLoadTime = Date.now(); this.lastPageContextKey = this.getPageContextKey(window.location.href); this._topicMaxPostNumberTopicId = null; this._topicMaxPostNumber = null; await Utils.sleep(800); try { this.updateReadingStatus?.(); } catch (error) { console.warn("[\u5BFC\u822A\u540E] updateReadingStatus \u5931\u8D25:", error); } if (this.autoRunning && this.isTopicPage && !this.isScrolling) { console.log("[\u5BFC\u822A\u540E] \u81EA\u52A8\u9605\u8BFB\u4ECD\u5F00\u542F\uFF0C\u6062\u590D\u6EDA\u52A8"); this.startScrolling(); } }, getPageWindow() { try { if (typeof unsafeWindow !== "undefined" && unsafeWindow) return unsafeWindow; } catch (_) { } return window; }, getDiscourseRouter() { const lookupNames = [ "router:main", "service:router", "router:application" ]; const lookupFrom = (source, label) => { if (!source || typeof source.lookup !== "function") return null; for (const name of lookupNames) { try { const router = source.lookup(name); if (router) { console.log(`[\u5BFC\u822A] \u5DF2\u4ECE ${label} \u83B7\u53D6 router: ${name}`); return router; } } catch (_) { } } return null; }; try { const container = this.getPageWindow().Discourse?.__container__; const router = lookupFrom(container, "Discourse.__container__"); if (router) return router; } catch (error) { console.warn("[\u5BFC\u822A] \u4ECE Discourse.__container__ \u83B7\u53D6 router \u5931\u8D25:", error); } try { const req = this.getPageWindow().require; const app = typeof req === "function" ? req("__DISCOURSE_APP__") : null; const router = lookupFrom(app, "__DISCOURSE_APP__"); if (router) return router; } catch (error) { console.warn("[\u5BFC\u822A] \u4ECE __DISCOURSE_APP__ \u83B7\u53D6 router \u5931\u8D25:", error); } try { const api = this.getPageWindow().Discourse?.__container__?.lookup?.("api:main"); const router = api?.container ? lookupFrom(api.container, "api:main.container") : null; if (router) return router; } catch (error) { console.warn("[\u5BFC\u822A] \u4ECE api:main \u83B7\u53D6 router \u5931\u8D25:", error); } console.warn("[\u5BFC\u822A] \u672A\u627E\u5230\u53EF\u7528\u7684 Discourse/Ember router"); return null; }, findTopicAnchor(targetTopicId) { const target = String(targetTopicId); return Array.from(document.querySelectorAll("a[href]")).find((anchor) => { const href = anchor.getAttribute("href") || ""; return href === `/t/topic/${target}` || href.startsWith(`/t/topic/${target}/`); }) || Array.from(document.querySelectorAll("a[href]")).find((anchor) => { const href = anchor.getAttribute("href") || ""; return href.includes(`/t/topic/${target}`); }); }, async waitAfterSoftNavigation(mode, targetTopicId) { console.log(`[\u5BFC\u822A] \u5DF2\u53D1\u8D77\u8DF3\u8F6C\uFF0C\u65B9\u5F0F: ${mode}`); const ok = await this.waitForTopicNavigation(targetTopicId, 8e3); if (!ok) { console.warn(`[\u5BFC\u822A] \u5728\u7B49\u5F85\u671F\u5185\u672A\u786E\u8BA4\u8FDB\u5165\u76EE\u6807\u5E16\u5B50: ${targetTopicId}`); return false; } console.log(`[\u5BFC\u822A] \u5DF2\u786E\u8BA4\u8FDB\u5165\u76EE\u6807\u5E16\u5B50: ${targetTopicId}`); if (this.navigationTimeout) { clearTimeout(this.navigationTimeout); this.navigationTimeout = null; } await this.onTopicNavigated(targetTopicId); return true; }, async tryRouteToWithDiscourseURL(target, targetTopicId, label) { if (!this.getPageWindow().DiscourseURL || typeof this.getPageWindow().DiscourseURL.routeTo !== "function") return false; try { console.log(`[\u5BFC\u822A] \u5C1D\u8BD5 DiscourseURL.routeTo(${label}):`, target); this.getPageWindow().DiscourseURL.routeTo(target); return await this.waitAfterSoftNavigation(`DiscourseURL.routeTo(${label})`, targetTopicId); } catch (error) { console.warn(`[\u5BFC\u822A] DiscourseURL.routeTo(${label}) \u5931\u8D25:`, error); return false; } }, async tryRouterTransition(router, target, targetTopicId, label) { if (!router || typeof router.transitionTo !== "function") return false; try { console.log(`[\u5BFC\u822A] \u5C1D\u8BD5 router.transitionTo(${label}):`, target); router.transitionTo(target); return await this.waitAfterSoftNavigation(`router.transitionTo(${label})`, targetTopicId); } catch (error) { console.warn(`[\u5BFC\u822A] router.transitionTo(${label}) \u5931\u8D25:`, error); return false; } }, async clickTopicLinkElement(anchor, targetTopicId, mode) { if (!anchor) return false; console.log(`[\u5BFC\u822A] \u5C1D\u8BD5\u94FE\u63A5\u70B9\u51FB(${mode}):`, anchor.href || anchor.getAttribute("href")); try { anchor.click(); } catch (error) { console.warn(`[\u5BFC\u822A] anchor.click(${mode}) \u5931\u8D25\uFF0C\u5C1D\u8BD5 dispatchEvent:`, error); const eventWindow = anchor.ownerDocument?.defaultView; const EventCtor = eventWindow?.MouseEvent || MouseEvent; anchor.dispatchEvent(new EventCtor("click", { bubbles: true, cancelable: true, button: 0 })); } return await this.waitAfterSoftNavigation(`link.click(${mode})`, targetTopicId); }, async clickTemporaryTopicLink(path, url, targetTopicId) { const anchor = document.createElement("a"); anchor.href = url; anchor.setAttribute("href", path); anchor.removeAttribute("target"); anchor.setAttribute("data-auto-route", "true"); anchor.style.cssText = "position:fixed;left:-9999px;top:-9999px;width:1px;height:1px;opacity:0;pointer-events:auto;"; document.body.appendChild(anchor); try { return await this.clickTopicLinkElement(anchor, targetTopicId, "temporary-anchor"); } finally { anchor.remove(); } }, async tryHistoryPushState(path, targetTopicId) { try { const pageWindow = this.getPageWindow(); console.log("[\u5BFC\u822A] \u5C1D\u8BD5 history.pushState:", path); pageWindow.history.pushState({}, "", path); const PopStateEventCtor = pageWindow.PopStateEvent || PopStateEvent; pageWindow.dispatchEvent(new PopStateEventCtor("popstate")); return await this.waitAfterSoftNavigation("history.pushState", targetTopicId); } catch (error) { console.warn("[\u5BFC\u822A] history.pushState \u5931\u8D25:", error); return false; } }, async softNavigateToTopic(path, url, targetTopicId) { this.cancelSmartLikeTasks("topic_changed"); if (this.navigationTimeout) { clearTimeout(this.navigationTimeout); this.navigationTimeout = null; } this.navigationTimeout = setTimeout(() => { const currentTopicId = this.getCurrentTopicIdFromLocation(); if (currentTopicId !== String(targetTopicId)) { console.warn("\u26A0\uFE0F \u8DEF\u7531\u8DF3\u8F6C\u8D85\u65F6\uFF0Cfallback \u5230\u539F\u751F\u8DF3\u8F6C:", url); window.location.assign(url); } else { console.log("\u2705 \u5DF2\u7ECF\u8FDB\u5165\u76EE\u6807\u5E16\u5B50\uFF0C\u53D6\u6D88 fallback"); } }, 12e3); try { if (await this.tryRouteToWithDiscourseURL(path, targetTopicId, "path")) return true; if (await this.tryRouteToWithDiscourseURL(url, targetTopicId, "url")) return true; const router = this.getDiscourseRouter(); if (await this.tryRouterTransition(router, path, targetTopicId, "path")) return true; if (await this.tryRouterTransition(router, url, targetTopicId, "url")) return true; const candidate = this.findTopicAnchor(targetTopicId); if (await this.clickTopicLinkElement(candidate, targetTopicId, "existing-anchor")) return true; if (await this.clickTemporaryTopicLink(path, url, targetTopicId)) return true; if (await this.tryHistoryPushState(path, targetTopicId)) return true; console.warn("[\u5BFC\u822A] \u6240\u6709\u8F6F\u8DF3\u8F6C\u65B9\u6848\u5931\u8D25\uFF0C\u4F7F\u7528\u539F\u751F\u8DF3\u8F6C:", url); window.location.assign(url); return false; } catch (error) { console.error("[\u5BFC\u822A] \u8DF3\u8F6C\u5F02\u5E38\uFF0Cfallback \u5230\u539F\u751F\u8DF3\u8F6C:", error); window.location.assign(url); return false; } }, async navigateNextTopic() { this.cancelSmartLikeTasks("navigating"); if (this.isTopicPage && !await this.waitForTopicReadConfirmation()) { console.log("[\u9605\u8BFB\u786E\u8BA4] \u697C\u5C42\u5C0F\u84DD\u70B9\u672A\u6D88\u5931\uFF0C\u5DF2\u53D6\u6D88\u8DF3\u8F6C\u4E0B\u4E00\u7BC7"); if (this.autoRunning && !this.isScrolling) this.startScrolling(); return; } if (this.stopAfterReadEnabled && this.currentSessionReadCount >= this.stopAfterReadCount) { console.log(`\u5DF2\u8FBE\u5230\u9605\u8BFB\u6570\u91CF\u9650\u5236 (${this.currentSessionReadCount}/${this.stopAfterReadCount})\uFF0C\u81EA\u52A8\u505C\u6B62`); this.showNotification(this.t("stoppedByReadLimit")); this.stopAutoReading(); return; } if (this.stopOnLikeLimitEnabled) { const likeStatus = this.likeCounter?.getStatus?.(); const isLikeCounterCooldown = !!(likeStatus && likeStatus.isInCooldown); const isOldCooldown = !!(this.likeResumeTime && Date.now() < this.likeResumeTime); const hasNoRemainingLikes = !!(likeStatus && likeStatus.remaining === 0); if (isLikeCounterCooldown || isOldCooldown || hasNoRemainingLikes) { console.log(`[\u70B9\u8D5E\u4E0A\u9650] \u70B9\u8D5E\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u81EA\u52A8\u505C\u6B62\u9605\u8BFB (cooldown: ${isLikeCounterCooldown}, oldCooldown: ${isOldCooldown}, noRemaining: ${hasNoRemainingLikes})`); this.showNotification(this.t("stoppedByLikeLimit")); this.stopAutoReading(); return; } } const nextTopic = await this.getNextTopic(); if (!nextTopic) { console.log("\u6CA1\u6709\u66F4\u591A\u6587\u7AE0\uFF0C\u8FD4\u56DE\u9996\u9875"); window.location.assign(`${BASE_URL}/latest`); return; } console.log("\u5BFC\u822A\u5230\u65B0\u6587\u7AE0:", nextTopic.title); this.incrementTodayReadCount(); this.currentSessionReadCount++; this.setSessionStorage("currentSessionReadCount", this.currentSessionReadCount); console.log(`\u5F53\u524D\u4F1A\u8BDD\u5DF2\u9605\u8BFB: ${this.currentSessionReadCount}/${this.stopAfterReadCount}`); this.updateReadingStatus(); const currentMatch = window.location.pathname.match(/\/t\/topic\/(\d+)/); if (currentMatch) { const currentTopicId = currentMatch[1]; await this.saveUserReadHistory(currentTopicId); } const targetTopicId = String(nextTopic.id); const path = this.fullTopicReadEnabled ? `/t/topic/${nextTopic.id}` : nextTopic.last_read_post_number ? `/t/topic/${nextTopic.id}/${nextTopic.last_read_post_number}` : `/t/topic/${nextTopic.id}`; const url = `${BASE_URL}${path}`; console.log("\u6B63\u5728\u8DF3\u8F6C\u5230:", url); await this.softNavigateToTopic(path, url, targetTopicId); }, stopAutoReading() { this.stopScrolling(); this.stopNavigationGuard(); this.autoRunning = false; this.setSessionStorage("autoRunning", false); this.button.innerHTML = `\u25B6${this.t("startReading")}`; this.button.classList.remove("running"); if (this.navigationTimeout) { clearTimeout(this.navigationTimeout); this.navigationTimeout = null; } console.log("\u81EA\u52A8\u9605\u8BFB\u5DF2\u505C\u6B62"); }, async loadUserReadHistory() { const username = await this.getCurrentUsername(); if (!username) { console.log("\u672A\u83B7\u53D6\u5230\u7528\u6237\u540D\uFF0C\u65E0\u6CD5\u52A0\u8F7D\u9605\u8BFB\u5386\u53F2"); this.readTopics = []; return; } const storageKey = `readTopics_${username}`; this.readTopics = Storage.get(storageKey, []); console.log(`\u5DF2\u52A0\u8F7D\u7528\u6237 ${username} \u7684\u9605\u8BFB\u5386\u53F2\uFF0C\u5171 ${this.readTopics.length} \u7BC7\u5E16\u5B50`); if (this.readTopics.length > this.totalReadCount) { console.log(`[\u6570\u636E\u540C\u6B65] \u603B\u9605\u8BFB\u6570\u4ECE ${this.totalReadCount} \u66F4\u65B0\u4E3A ${this.readTopics.length}`); this.totalReadCount = this.readTopics.length; Storage.set("totalReadCount", this.totalReadCount); this.updateReadStatsDisplay(); } }, async saveUserReadHistory(topicId) { const username = await this.getCurrentUsername(); if (!username) { console.log("\u672A\u83B7\u53D6\u5230\u7528\u6237\u540D\uFF0C\u65E0\u6CD5\u4FDD\u5B58\u9605\u8BFB\u5386\u53F2"); return; } if (!this.readTopics.includes(topicId)) { this.readTopics.push(topicId); if (this.readTopics.length > 1e3) { this.readTopics = this.readTopics.slice(-1e3); } const storageKey = `readTopics_${username}`; Storage.set(storageKey, this.readTopics); console.log(`\u5DF2\u4FDD\u5B58\u5E16\u5B50 ${topicId} \u5230\u7528\u6237 ${username} \u7684\u9605\u8BFB\u5386\u53F2`); } }, isTopicRead(topicId) { return this.readTopics.includes(topicId); }, loadTodayReadCount() { const now = /* @__PURE__ */ new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const savedData = Storage.get("todayReadStats", null); console.log(`[\u4ECA\u65E5\u9605\u8BFB] \u5F53\u524D\u65E5\u671F: ${today}, \u4FDD\u5B58\u7684\u6570\u636E:`, savedData); if (savedData && savedData.date) { const savedDate = savedData.date; const isToday = savedDate === today || savedDate === now.toDateString(); if (isToday) { console.log(`[\u4ECA\u65E5\u9605\u8BFB] \u65E5\u671F\u5339\u914D\uFF0C\u8FD4\u56DE\u5DF2\u4FDD\u5B58\u7684\u8BA1\u6570: ${savedData.count}`); return savedData.count; } } console.log(`[\u4ECA\u65E5\u9605\u8BFB] \u65B0\u7684\u4E00\u5929\u6216\u65E0\u6570\u636E\uFF0C\u91CD\u7F6E\u8BA1\u6570\u4E3A0`); Storage.set("todayReadStats", { date: today, count: 0 }); return 0; }, incrementTodayReadCount() { const now = /* @__PURE__ */ new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`; const savedData = Storage.get("todayReadStats", null); if (savedData && savedData.date) { const savedDate = savedData.date; const isToday = savedDate === today || savedDate === now.toDateString(); if (!isToday) { console.log(`[\u4ECA\u65E5\u9605\u8BFB] \u68C0\u6D4B\u5230\u8DE8\u5929\uFF01\u65E7\u65E5\u671F: ${savedDate}, \u65B0\u65E5\u671F: ${today}\uFF0C\u91CD\u7F6E\u4ECA\u65E5\u8BA1\u6570`); this.todayReadCount = 0; } } this.todayReadCount++; this.totalReadCount++; Storage.set("todayReadStats", { date: today, count: this.todayReadCount }); Storage.set("totalReadCount", this.totalReadCount); console.log(`\u4ECA\u65E5\u5DF2\u9605\u8BFB ${this.todayReadCount} \u7BC7\u5E16\u5B50\uFF0C\u603B\u9605\u8BFB ${this.totalReadCount} \u7BC7`); this.updateReadStatsDisplay(); }, updateReadStatsDisplay() { if (!this.readStatsContainer) return; const todayCount = this.todayReadCount || 0; let totalCount = this.totalReadCount || 0; if (totalCount < todayCount) { totalCount = todayCount; this.totalReadCount = totalCount; Storage.set("totalReadCount", totalCount); console.log(`[\u6570\u636E\u4FEE\u590D] \u603B\u9605\u8BFB\u6570\u5DF2\u4FEE\u6B63\u4E3A ${totalCount}`); } this.readStatsContainer.innerHTML = `
\u{1F4C5} ${this.t("todayRead")}
${todayCount}
\u{1F4DA} ${this.t("totalRead")}
${totalCount}
`; }, loadHistoricalMaxPage() { const today = (/* @__PURE__ */ new Date()).toDateString(); const savedData = Storage.get("historicalMaxPageData", null); if (savedData) { if (savedData.date === today) { console.log(`[\u9875\u7801\u7EED\u8BFB] \u4ECA\u65E5\u5DF2\u8BFB\u6700\u5927\u9875\u7801: ${savedData.maxPage}`); return savedData.maxPage; } console.log(`[\u9875\u7801\u7EED\u8BFB] \u65B0\u7684\u4E00\u5929\uFF0C\u7EE7\u627F\u6628\u65E5\u6700\u5927\u9875\u7801: ${savedData.maxPage}`); return savedData.maxPage; } return 0; }, saveHistoricalMaxPage(page) { const today = (/* @__PURE__ */ new Date()).toDateString(); const currentMax = this.historicalMaxPage || 0; if (page > currentMax) { this.historicalMaxPage = page; Storage.set("historicalMaxPageData", { date: today, maxPage: page }); console.log(`[\u9875\u7801\u7EED\u8BFB] \u66F4\u65B0\u5386\u53F2\u6700\u5927\u9875\u7801: ${page}`); } }, getStartPage() { if (this.lastFetchedPage > 0) { console.log(`[\u9875\u7801\u7EED\u8BFB] \u4F7F\u7528\u4F1A\u8BDD\u5185\u4E0A\u6B21\u9875\u7801: ${this.lastFetchedPage + 1}`); return this.lastFetchedPage + 1; } if (this.historicalMaxPage > 0) { console.log(`[\u9875\u7801\u7EED\u8BFB] \u4F7F\u7528\u5386\u53F2\u6700\u5927\u9875\u7801: ${this.historicalMaxPage + 1}`); return this.historicalMaxPage + 1; } return 1; }, resetPageProgress() { this.lastFetchedPage = 0; this.setSessionStorage("lastFetchedPage", 0); console.log("[\u9875\u7801\u7EED\u8BFB] \u5DF2\u91CD\u7F6E\u4F1A\u8BDD\u9875\u7801"); } }; // src/controller/mixins/lifecycle.js var methods4 = { startTrustLevelMonitor() { if (this.trustLevelMonitorInterval) { clearInterval(this.trustLevelMonitorInterval); } this.trustLevelMonitorInterval = setInterval(() => { if (this.shouldRefreshAccountInfo()) { console.log("\u81EA\u52A8\u5237\u65B0\u7B49\u7EA7\u4FE1\u606F..."); this.loadUserTrustLevel(false); } }, 30 * 60 * 1e3); console.log("\u7B49\u7EA7\u76D1\u63A7\u5DF2\u542F\u52A8\uFF0830\u5206\u949F\u5237\u65B0\u4E00\u6B21\uFF0C\u4EC5\u5728\u53EF\u89C1\u65F6\uFF09"); }, shouldRefreshAccountInfo() { return Boolean(this.accountSection && this.accountSectionContent && this.accountSection.getAttribute("aria-expanded") === "true"); }, shouldRefreshCreditInfo() { return Boolean(this.creditSection && this.creditSection.getAttribute("aria-expanded") === "true"); }, shouldRefreshRankInfo() { return Boolean(this.rankSection && this.rankSection.getAttribute("aria-expanded") === "true"); }, shouldRefreshCdkInfo() { return Boolean(this.cdkSection && this.cdkSection.getAttribute("aria-expanded") === "true"); }, initDataLoading() { if (this.accountSection && this.accountSection.getAttribute("aria-expanded") === "true") { this.loadUserTrustLevel(); } }, startNavigationGuard() { if (this.navigationGuardInterval) { clearInterval(this.navigationGuardInterval); } this.pageLoadTime = Date.now(); this.lastPageContextKey = this.getPageContextKey(window.location.href); this.navigationGuardInterval = setInterval(() => { if (!this.autoRunning) return; const currentTime = Date.now(); const timeOnPage = currentTime - this.pageLoadTime; const currentPageContextKey = this.getPageContextKey(window.location.href); if (currentPageContextKey !== this.lastPageContextKey) { console.log("\u2705 \u9875\u9762\u5DF2\u8DF3\u8F6C\uFF0C\u91CD\u7F6E\u5B88\u62A4\u5B9A\u65F6\u5668"); this.cancelSmartLikeTasks("topic_changed"); this.pageLoadTime = currentTime; this.lastPageContextKey = currentPageContextKey; this.isTopicPage = window.location.pathname.includes("/t/topic/"); if (this.autoRunning && this.isTopicPage && !this.isScrolling) { console.log("\u{1F6E1}\uFE0F \u5B88\u62A4\u68C0\u6D4B\u5230\u65B0\u6587\u7AE0\u9875\u4E14\u672A\u6EDA\u52A8\uFF0C\u5C1D\u8BD5\u6062\u590D\u6EDA\u52A8"); this.startScrolling(); } return; } if (this.isPageLoadingStalled(timeOnPage)) { console.warn("\u26A0\uFE0F \u68C0\u6D4B\u5230\u9875\u9762\u6301\u7EED\u52A0\u8F7D\u8D85\u65F6\uFF0C\u505C\u6B62\u81EA\u52A8\u9605\u8BFB"); this.showNotification(this.t("stoppedByPageLoading")); this.stopAutoReading(); return; } if (this.isTopicPage && timeOnPage > 6e4 && !this.isScrolling) { console.warn("\u26A0\uFE0F \u68C0\u6D4B\u5230\u9875\u9762\u53EF\u80FD\u5361\u4F4F\uFF0860\u79D2\u672A\u8DF3\u8F6C\u4E14\u672A\u6EDA\u52A8\uFF09\uFF0C\u5C1D\u8BD5\u6062\u590D..."); this.recoverFromStuck(); } if (!this.isTopicPage && timeOnPage > 3e4) { console.warn("\u26A0\uFE0F \u68C0\u6D4B\u5230\u5728\u975E\u6587\u7AE0\u9875\u5361\u4F4F\uFF0C\u5C1D\u8BD5\u6062\u590D..."); this.recoverFromStuck(); } }, 5e3); console.log("\u{1F6E1}\uFE0F \u5BFC\u822A\u5B88\u62A4\u7A0B\u5E8F\u5DF2\u542F\u52A8"); }, stopNavigationGuard() { if (this.navigationGuardInterval) { clearInterval(this.navigationGuardInterval); this.navigationGuardInterval = null; console.log("\u{1F6E1}\uFE0F \u5BFC\u822A\u5B88\u62A4\u7A0B\u5E8F\u5DF2\u505C\u6B62"); } }, getSessionStorage(key, defaultValue = null) { try { const value = sessionStorage.getItem(key); return value ? JSON.parse(value) : defaultValue; } catch { return defaultValue; } }, setSessionStorage(key, value) { try { sessionStorage.setItem(key, JSON.stringify(value)); return true; } catch (error) { console.error("SessionStorage error:", error); return false; } }, startUserSwitchMonitoring() { this.getCurrentUsername().then((username) => { this.lastDetectedUser = username; }); setInterval(async () => { const currentDetectedUser = await this.getCurrentUsername(); if (currentDetectedUser && this.lastDetectedUser && currentDetectedUser !== this.lastDetectedUser) { console.log(`\u68C0\u6D4B\u5230\u8D26\u53F7\u5207\u6362: ${this.lastDetectedUser} -> ${currentDetectedUser}`); this.lastDetectedUser = currentDetectedUser; this.currentUsername = currentDetectedUser; setTimeout(() => { console.log("\u8D26\u53F7\u5207\u6362\u540E\u91CD\u65B0\u52A0\u8F7D\u7B49\u7EA7\u4FE1\u606F"); this.loadUserTrustLevel(true); }, 1e3); } else if (currentDetectedUser) { this.lastDetectedUser = currentDetectedUser; } }, 5e3); } }; // src/controller/mixins/topic.js var methods5 = { formatTopicCreatedTime(dateInput) { if (!dateInput) return ""; const date = dateInput instanceof Date ? dateInput : new Date(dateInput); if (Number.isNaN(date.getTime())) return ""; const pad = (num) => String(num).padStart(2, "0"); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`; }, getTopicAgeLevel(dateInput) { if (!dateInput) return "fresh"; const date = dateInput instanceof Date ? dateInput : new Date(dateInput); if (Number.isNaN(date.getTime())) return "fresh"; const ageDays = (Date.now() - date.getTime()) / (1e3 * 60 * 60 * 24); if (ageDays > 90) return "ancient"; if (ageDays > 30) return "old"; return "fresh"; }, extractTopicCreatedAtFromRow(row) { if (!row) return ""; const parsePossibleDate = (value) => { if (!value) return ""; const normalized = String(value).trim(); if (!normalized) return ""; if (/^\d{13}$/.test(normalized)) { const date2 = new Date(Number(normalized)); return Number.isNaN(date2.getTime()) ? "" : date2.toISOString(); } if (/^\d{10}$/.test(normalized)) { const date2 = new Date(Number(normalized) * 1e3); return Number.isNaN(date2.getTime()) ? "" : date2.toISOString(); } const date = new Date(normalized); return Number.isNaN(date.getTime()) ? "" : normalized; }; const parseChineseDateText = (value) => { if (!value) return ""; const text = String(value).replace(/\s+/g, " ").trim(); const match = text.match(/(\d{4})\s*年\s*(\d{1,2})\s*月\s*(\d{1,2})\s*日\s*(\d{1,2}):(\d{2})/); if (!match) return ""; const [, year, month, day, hour, minute] = match; const iso = `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}T${String(hour).padStart(2, "0")}:${minute}:00`; const date = new Date(iso); return Number.isNaN(date.getTime()) ? "" : iso; }; const candidates = [ row.dataset.createdAt, row.getAttribute("data-created-at"), row.dataset.topicCreatedAt, row.getAttribute("data-topic-created-at"), row.dataset.bumpedAt, row.getAttribute("data-bumped-at") ].filter(Boolean); for (const value of candidates) { const parsed = parsePossibleDate(value); if (parsed) return parsed; } const topicLink = row.querySelector("a.title, .main-link a, a.raw-topic-link"); if (topicLink) { const activityCell2 = topicLink.closest("tr")?.querySelector("td.activity[title], td.age[title], td.topic-list-data.age[title]"); if (activityCell2) { const activityTitle = activityCell2.getAttribute("title") || ""; const createdFromTitle = activityTitle.match(/创建日期[::]\s*([^\n]+)/); if (createdFromTitle?.[1]) { const parsedChinese = parseChineseDateText(createdFromTitle[1]); if (parsedChinese) return parsedChinese; const parsedDirect = parsePossibleDate(createdFromTitle[1]); if (parsedDirect) return parsedDirect; } } const linkCandidates = [ topicLink.dataset.createdAt, topicLink.getAttribute("data-created-at"), topicLink.getAttribute("title"), topicLink.getAttribute("aria-label") ].filter(Boolean); for (const value of linkCandidates) { const parsed = parsePossibleDate(value); if (parsed) return parsed; const datetimeMatch = String(value).match(/\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(:\d{2})?/); if (datetimeMatch) { return datetimeMatch[0].replace(" ", "T"); } } } const timeEls = row.querySelectorAll("time[datetime]"); for (const timeEl of timeEls) { const dt = parsePossibleDate(timeEl.getAttribute("datetime")); if (dt) return dt; } const activityCell = row.querySelector("td.activity[title], td.age[title], td.topic-list-data.age[title]"); if (activityCell) { const activityTitle = activityCell.getAttribute("title") || ""; const createdFromTitle = activityTitle.match(/创建日期[::]\s*([^\n]+)/); if (createdFromTitle?.[1]) { const parsedChinese = parseChineseDateText(createdFromTitle[1]); if (parsedChinese) return parsedChinese; const parsedDirect = parsePossibleDate(createdFromTitle[1]); if (parsedDirect) return parsedDirect; } } return ""; }, renderTopicCreatedTimeInList() { const rows = document.querySelectorAll("tr.topic-list-item, tr.latest-topic-list-item"); rows.forEach((row) => { const activityLink = row.querySelector("td.activity a.post-activity, td.age a.post-activity, td.topic-list-data.age a.post-activity"); const activityCell = activityLink?.closest("td.activity, td.age, td.topic-list-data.age") || row.querySelector("td.activity, td.age, td.topic-list-data.age"); if (!activityCell) return; let meta = activityCell.querySelector(".lda-topic-created-time"); if (!this.topicCreatedTimeVisible) { if (meta) meta.remove(); return; } const createdAt = this.extractTopicCreatedAtFromRow(row); if (!createdAt) { if (meta) meta.remove(); return; } const formatted = this.formatTopicCreatedTime(createdAt); if (!formatted) { if (meta) meta.remove(); return; } if (!meta) { meta = document.createElement("span"); meta.className = "lda-topic-created-time"; const anchor = activityLink || activityCell.querySelector("a"); if (anchor) { meta.style.display = "inline-flex"; meta.style.alignItems = "center"; meta.style.marginLeft = "8px"; anchor.appendChild(meta); } else { meta.style.display = "inline-flex"; meta.style.alignItems = "center"; meta.style.marginLeft = "8px"; activityCell.appendChild(meta); } } meta.dataset.createdAt = createdAt; meta.dataset.ageLevel = this.topicAgeColorEnabled ? this.getTopicAgeLevel(createdAt) : "neutral"; meta.innerHTML = `\uFF5C\u521B\u5EFA: ${formatted}`; meta.title = `\u5E16\u5B50\u521B\u5EFA\u65F6\u95F4\uFF1A${formatted}`; }); }, initTopicCreatedTimeEnhancer() { this.renderTopicCreatedTimeInList(); if (this._topicCreatedTimeObserver) { try { this._topicCreatedTimeObserver.disconnect(); } catch (_) { } } let timer = null; const scheduleRender = () => { if (timer) clearTimeout(timer); timer = setTimeout(() => this.renderTopicCreatedTimeInList(), 200); }; this._topicCreatedTimeObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type !== "childList") continue; for (const node of mutation.addedNodes) { if (!(node instanceof HTMLElement)) continue; if (node.matches?.("tr.topic-list-item, tr.latest-topic-list-item, .topic-list") || node.querySelector?.("tr.topic-list-item, tr.latest-topic-list-item")) { scheduleRender(); return; } } } }); if (document.body) { this._topicCreatedTimeObserver.observe(document.body, { childList: true, subtree: true }); } }, initFloorNumberDisplay() { this.addFloorNumbers(); this.initMutationObserver(); }, addFloorNumbers() { const posts = Array.from(document.querySelectorAll(".boxed.onscreen-post")); if (!posts.length) { return; } const topicIdMatch = window.location.pathname.match(/\/t\/topic\/(\d+)/); const topicId = topicIdMatch ? topicIdMatch[1] : null; if (topicId && this._floorTopicId !== topicId) { this._floorTopicId = topicId; this._floorSmallActionNumbers = null; this._floorSmallActionPromise = null; } const ensureSmallActionNumbers = () => { if (!topicId) return; if (Array.isArray(this._floorSmallActionNumbers)) return; if (this._floorSmallActionPromise) return; const url = new URL(window.location.href); url.hash = ""; const jsonUrl = url.toString().replace(/\/$/, "") + ".json"; this._floorSmallActionPromise = fetch(jsonUrl, { credentials: "include" }).then((r) => r && r.ok ? r.json() : null).then((data) => { const arr = data?.post_stream?.posts; if (!Array.isArray(arr)) return null; const smallNums = []; for (const p of arr) { const n = p?.post_number; if (!Number.isFinite(n)) continue; const hasActionCode = typeof p?.action_code === "string" && p.action_code.length > 0; const postType = p?.post_type; const isNonRegularType = Number.isFinite(postType) && postType !== 1; let looksLikeSystemText = false; if (!hasActionCode && !isNonRegularType) { const cooked = typeof p?.cooked === "string" ? p.cooked : ""; if (cooked) { const plain = cooked.replace(/<[^>]*>/g, "").trim(); if (plain.length > 0 && plain.length <= 80) { const hasCatTag = /类别|标签|category|tag/i.test(plain); const hasUpdate = /更新|移除|removed|updated|change/i.test(plain); if (hasCatTag && hasUpdate) looksLikeSystemText = true; } } } if (hasActionCode || isNonRegularType || looksLikeSystemText) { smallNums.push(n); } } smallNums.sort((a, b) => a - b); return smallNums; }).then((smallNums) => { if (Array.isArray(smallNums)) { const prev = this._floorSmallActionNumbers; this._floorSmallActionNumbers = smallNums; if (!Array.isArray(prev) || prev.length !== smallNums.length) { setTimeout(() => { if (this._floorTopicId === topicId) { try { this.addFloorNumbers(); } catch (_) { } } }, 0); } } }).catch(() => { }).finally(() => { this._floorSmallActionPromise = null; }); }; ensureSmallActionNumbers(); let smallActionPostNumbers = Array.isArray(this._floorSmallActionNumbers) ? this._floorSmallActionNumbers : null; if (!smallActionPostNumbers) { const topicPosts = Array.from(document.querySelectorAll(".topic-post[data-post-number]")); const nums = []; for (const tp of topicPosts) { const isSmallAction = tp.classList.contains("small-action") || !!tp.querySelector(".small-action"); if (!isSmallAction) continue; const postNumberStr = tp.getAttribute("data-post-number"); const n = postNumberStr ? parseInt(postNumberStr, 10) : NaN; if (Number.isFinite(n)) nums.push(n); } nums.sort((a, b) => a - b); smallActionPostNumbers = nums; } const countSmallActionsBefore = (postNumber) => { let count = 0; for (const n of smallActionPostNumbers) { if (n < postNumber) count++; else break; } return count; }; for (const post of posts) { const postWrapper = post.closest(".topic-post"); const isSmallActionWrapper = !!(postWrapper && (postWrapper.classList.contains("small-action") || !!postWrapper.querySelector(".small-action"))); if (isSmallActionWrapper) { const existing = post.querySelector(".floor-number"); if (existing) existing.remove(); continue; } const postNumberStr = postWrapper?.getAttribute("data-post-number"); let postNumber = postNumberStr ? parseInt(postNumberStr, 10) : NaN; if (!Number.isFinite(postNumber)) { const idPart = (post.id || "").split("_")[1]; postNumber = idPart ? parseInt(idPart, 10) : NaN; } if (!Number.isFinite(postNumber)) continue; const adjustedFloor = postNumber - countSmallActionsBefore(postNumber); const meta = post.querySelector(".topic-meta-data"); if (!meta) continue; let floorNumber = post.querySelector(".floor-number"); if (!floorNumber) { floorNumber = document.createElement("div"); floorNumber.className = "floor-number"; floorNumber.style.cssText = "color: grey; margin-left: 10px;"; meta.appendChild(floorNumber); } floorNumber.textContent = "\u697C\u5C42: " + adjustedFloor; floorNumber.title = "\u539F\u59CB\u697C\u5C42: " + postNumber; } }, initMutationObserver() { if (this._floorMutationObserver) { try { this._floorMutationObserver.disconnect(); } catch (_) { } } this._floorObserverRunning = false; this._floorObserverScheduled = false; const scheduleUpdate = () => { if (this._floorObserverRunning || this._floorObserverScheduled) return; this._floorObserverScheduled = true; setTimeout(() => { this._floorObserverScheduled = false; this._floorObserverRunning = true; try { this.addFloorNumbers(); this.toggleCleanMode(); } catch (e) { console.error("[\u697C\u5C42\u53F7] \u66F4\u65B0\u5931\u8D25:", e); } finally { this._floorObserverRunning = false; } }, 80); }; this._floorMutationObserver = new MutationObserver(() => { scheduleUpdate(); }); this._floorMutationObserver.observe(document.body, { childList: true, subtree: true }); scheduleUpdate(); } }; // src/controller/mixins/modes.js var methods6 = { toggleCleanMode() { const sidebarToggle = document.querySelector("button.btn-sidebar-toggle"); if (sidebarToggle && this.cleanModeEnabled) { if (sidebarToggle.getAttribute("aria-expanded") === "true") { console.log("\u6E05\u723D\u6A21\u5F0F\u542F\u7528\uFF0C\u6536\u8D77\u8FB9\u680F"); sidebarToggle.click(); } } this.applyCleanModeStyles(); }, applyCleanModeStyles() { let styleElement = document.getElementById("clean-mode-styles"); if (styleElement) { styleElement.remove(); } if (this.cleanModeEnabled) { styleElement = document.createElement("style"); styleElement.id = "clean-mode-styles"; styleElement.textContent = ` p:contains("\u5E0C\u671B\u4F60\u559C\u6B22\u8FD9\u91CC\u3002\u6709\u95EE\u9898\uFF0C\u8BF7\u63D0\u95EE\uFF0C\u6216\u641C\u7D22\u73B0\u6709\u5E16\u5B50\u3002") { display: none !important; } div#global-notice-alert-global-notice.alert.alert-info.alert-global-notice { display: none !important; } a[href="https://linux.do/t/topic/482293"] { display: none !important; } div.link-bottom-line a.badge-category__wrapper { display: none !important; } td.posters.topic-list-data { display: none !important; } a.discourse-tag.box[href^="/tag/"] { display: none !important; } `; document.head.appendChild(styleElement); } }, toggleGrayscaleMode() { this.applyGrayscaleModeStyles(); }, applyGrayscaleModeStyles() { let styleElement = document.getElementById("grayscale-mode-styles"); if (styleElement) { styleElement.remove(); } if (this.grayscaleModeEnabled) { const isAndroid = /Android/i.test(navigator.userAgent); const isIOS = /iPhone|iPad|iPod/i.test(navigator.userAgent); const isMobile = isAndroid || isIOS; const isLowEnd = navigator.hardwareConcurrency <= 4 || navigator.deviceMemory <= 4; styleElement = document.createElement("style"); styleElement.id = "grayscale-mode-styles"; styleElement.textContent = ` @media (prefers-color-scheme: light) { #main-outlet, .d-header, .menu-panel, main { filter: grayscale(100%) contrast(108%) brightness(97%) !important; -webkit-filter: grayscale(100%) contrast(108%) brightness(97%) !important; } #main-outlet *, .d-header *, .menu-panel *, main * { text-shadow: 0 0 0.3px rgba(0, 0, 0, 0.4) !important; } } @media (prefers-color-scheme: dark) { #main-outlet, .d-header, .menu-panel, main { filter: grayscale(100%) contrast(110%) brightness(103%) !important; -webkit-filter: grayscale(100%) contrast(110%) brightness(103%) !important; } #main-outlet *, .d-header *, .menu-panel *, main * { text-shadow: 0 0 0.3px rgba(255, 255, 255, 0.5) !important; } } @media (prefers-color-scheme: no-preference) { #main-outlet, .d-header, .menu-panel, main { filter: grayscale(100%) contrast(109%) brightness(99%) !important; -webkit-filter: grayscale(100%) contrast(109%) brightness(99%) !important; } } img, svg, canvas, video { filter: grayscale(100%) contrast(110%) !important; -webkit-filter: grayscale(100%) contrast(110%) !important; } ${isMobile ? ` html { -webkit-font-smoothing: antialiased !important; -moz-osx-font-smoothing: grayscale !important; text-rendering: optimizeLegibility !important; } * { -webkit-overflow-scrolling: touch !important; } ` : ""} ${isIOS ? ` body { -webkit-transform: translateZ(0) !important; } ` : ""} ${isLowEnd ? ` *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } ` : ""} `; document.head.appendChild(styleElement); setTimeout(() => { if (document.documentElement) { const currentWillChange = document.documentElement.style.willChange; if (currentWillChange === "filter") { document.documentElement.style.willChange = "auto"; } } }, 1e3); const isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; const isLight = window.matchMedia && window.matchMedia("(prefers-color-scheme: light)").matches; const theme = isDark ? "\u6DF1\u8272" : isLight ? "\u6D45\u8272" : "\u672A\u77E5"; console.log("\u{1F3A8} \u9ED1\u767D\u7070\u6A21\u5F0F\u5DF2\u542F\u7528"); console.log(`\u{1F4F1} \u8BBE\u5907\u7C7B\u578B: ${isMobile ? isIOS ? "iOS" : "Android" : "\u684C\u9762"}`); console.log(`\u{1F527} \u4F18\u5316\u6A21\u5F0F: ${isLowEnd ? "\u4F4E\u7AEF\u8BBE\u5907" : "\u6807\u51C6"}`); console.log(`\u{1F313} \u68C0\u6D4B\u4E3B\u9898: ${theme}\u6A21\u5F0F`); console.log(`\u2728 \u6D45\u8272\u80CC\u666F: \u5BF9\u6BD4108% + \u4EAE\u5EA697%`); console.log(`\u2728 \u6DF1\u8272\u80CC\u666F: \u5BF9\u6BD4110% + \u4EAE\u5EA6103%`); console.log(`\u{1F5BC}\uFE0F \u56FE\u7247\u5BF9\u6BD4\u5EA6: 110%`); } else { console.log("\u{1F3A8} \u9ED1\u767D\u7070\u6A21\u5F0F\u5DF2\u5173\u95ED"); } } }; // src/controller/mixins/ranking.js var methods7 = { async loadRankingData(isManualRefresh = false) { if (!this.rankDataContainer) return; if (!isManualRefresh && !this.shouldRefreshRankInfo()) return; if (isManualRefresh) { const refreshBtn = this.rankDataContainer.querySelector(".rank-refresh-btn"); if (refreshBtn) { refreshBtn.textContent = this.t("refreshing"); refreshBtn.disabled = true; } } else { this.rankDataContainer.innerHTML = `
${this.t("loadingRank")}
`; } try { const periods = [ { key: "daily", name: this.t("dailyRank"), icon: "\u{1F4C5}" }, { key: "weekly", name: this.t("weeklyRank"), icon: "\u{1F4C6}" }, { key: "monthly", name: this.t("monthlyRank"), icon: "\u{1F5D3}\uFE0F" }, { key: "quarterly", name: this.t("quarterlyRank"), icon: "\u{1F4CA}" }, { key: "yearly", name: this.t("yearlyRank"), icon: "\u{1F4C8}" }, { key: "all", name: this.t("allTimeRank"), icon: "\u{1F3C5}" } ]; const rankPromises = periods.map((period) => this.fetchRankingByPeriod(period.key)); const rankResults = await Promise.all(rankPromises); this.renderRankingData(periods, rankResults); } catch (error) { console.error("\u52A0\u8F7D\u6392\u540D\u6570\u636E\u5931\u8D25:", error); this.rankDataContainer.innerHTML = `
${this.t("sectionRanking").replace(/^\s*🏆\s*/, "")}
${this.t("loadFailed")}
`; this.bindRankRefreshBtn(); } }, fetchRankingByPeriod(period) { return new Promise((resolve) => { fetch(`${BASE_URL}/leaderboard/1?period=${period}`, { credentials: "include", headers: { "Accept": "application/json" } }).then((response) => { if (response.ok) { return response.json(); } throw new Error("\u8BF7\u6C42\u5931\u8D25"); }).then((data) => { if (data && data.personal && data.personal.user) { resolve({ score: data.personal.user.total_score || 0, position: data.personal.position || data.personal.user.position || 0 }); } else { resolve({ score: 0, position: "-" }); } }).catch((error) => { console.error(`\u83B7\u53D6${period}\u6392\u540D\u5931\u8D25:`, error); resolve({ score: 0, position: "-" }); }); }); }, renderRankingData(periods, results) { const updateTime = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); let html = `
${this.t("myRanking")}
`; periods.forEach((period, index) => { const result = results[index]; const positionText = result.position === "-" ? "-" : `#${result.position}`; const scoreText = result.score || 0; html += `
${period.icon} ${period.name} ${scoreText}${this.t("points")} ${positionText}
`; }); html += ` `; this.rankDataContainer.innerHTML = html; this.bindRankRefreshBtn(); }, bindRankRefreshBtn() { setTimeout(() => { const refreshBtn = this.rankDataContainer?.querySelector(".rank-refresh-btn"); if (refreshBtn) { refreshBtn.addEventListener("click", (e) => { e.stopPropagation(); e.preventDefault(); this.loadRankingData(true); }); } }, 100); } }; // src/controller/mixins/rateLimit.js var methods8 = { checkLikeResumeTime() { this.likeResumeTime = LikeCooldown.read(this.smartLikeAccountKey); if (!Number.isFinite(this.likeResumeTime)) { console.warn("[\u70B9\u8D5E\u51B7\u5374] \u72B6\u6001\u6682\u4E0D\u53EF\u7528\uFF0C\u667A\u80FD\u70B9\u8D5E\u4FDD\u6301\u6682\u505C"); return; } if (this.likeResumeTime > Date.now()) { console.log(`[\u70B9\u8D5E\u51B7\u5374] \u5C06\u4E8E ${new Date(this.likeResumeTime).toLocaleString()} \u7ED3\u675F\uFF0C\u7ED3\u675F\u540E\u9700\u624B\u52A8\u5F00\u542F\u667A\u80FD\u70B9\u8D5E`); } }, detectIpRateLimit() { const isNormalPage = document.querySelector("#main-outlet") || document.querySelector(".topic-list") || document.querySelector(".topic-post") || document.querySelector(".d-header") || document.querySelector(".ember-application") || document.querySelector("[data-discourse-helper]"); if (isNormalPage) { if (this.ipRateLimitResumeTime) { console.log("[IP\u9650\u6D41] \u68C0\u6D4B\u5230\u6B63\u5E38\u9875\u9762\uFF0C\u6E05\u9664\u4E4B\u524D\u7684\u9650\u6D41\u72B6\u6001"); this.ipRateLimitResumeTime = null; Storage.set("ipRateLimitResumeTime", null); this.stopIpRateLimitRecoveryCheck(); } return false; } const pageText = document.body?.innerText || ""; const rateLimitIndicators = [ "You are being rate limited", "We have banned you temporarily", "Too Many Requests", "Error 429", "HTTP 429" ]; const isRateLimited = rateLimitIndicators.some( (indicator) => pageText.includes(indicator) ); const pageTitle = document.title || ""; const titleRateLimited = pageTitle.includes("Rate Limited") || pageTitle.includes("429") || pageTitle.includes("Banned"); const isShortPage = pageText.length < 2e3; if ((isRateLimited || titleRateLimited) && isShortPage) { console.warn("\u{1F6AB} [IP\u9650\u6D41] \u68C0\u6D4B\u5230 IP \u88AB\u9650\u6D41\uFF01"); this.handleIpRateLimit(); return true; } return false; }, handleIpRateLimit() { if (this.ipRateLimitResumeTime && Date.now() < this.ipRateLimitResumeTime) { console.log("[IP\u9650\u6D41] \u5DF2\u5728\u7B49\u5F85\u6062\u590D\u4E2D\uFF0C\u8DF3\u8FC7"); return; } const waitTime = 30 * 60 * 1e3; this.ipRateLimitResumeTime = Date.now() + waitTime; Storage.set("ipRateLimitResumeTime", this.ipRateLimitResumeTime); if (this.autoRunning) { console.log("[IP\u9650\u6D41] \u505C\u6B62\u81EA\u52A8\u9605\u8BFB..."); this.stopScrolling(); this.stopNavigationGuard(); this.autoRunning = false; this.setSessionStorage("autoRunning", false); if (this.button) { this.button.innerHTML = `\u25B6${this.t("startReading")}`; this.button.classList.remove("running"); } if (this.navigationTimeout) { clearTimeout(this.navigationTimeout); this.navigationTimeout = null; } } const resumeTime = new Date(this.ipRateLimitResumeTime); this.showNotification(`${this.t("ipRateLimited")} ${this.t("ipRateLimitWait")} (${resumeTime.toLocaleTimeString()})`); console.log(`[IP\u9650\u6D41] \u81EA\u52A8\u9605\u8BFB\u5DF2\u6682\u505C\uFF0C\u5C06\u5728 ${resumeTime.toLocaleString()} \u540E\u81EA\u52A8\u6062\u590D`); this.startIpRateLimitRecoveryCheck(); }, checkIpRateLimitStatus() { if (this.ipRateLimitResumeTime) { const now = Date.now(); const isNormalPage = document.querySelector("#main-outlet") || document.querySelector(".topic-list") || document.querySelector(".topic-post") || document.querySelector(".d-header") || document.querySelector(".ember-application"); if (isNormalPage) { console.log("[IP\u9650\u6D41] \u5F53\u524D\u662F\u6B63\u5E38\u9875\u9762\uFF0C\u6E05\u9664\u4E4B\u524D\u7684\u9650\u6D41\u72B6\u6001\uFF08\u53EF\u80FD\u662F\u8BEF\u5224\uFF09"); this.ipRateLimitResumeTime = null; Storage.set("ipRateLimitResumeTime", null); return; } if (now >= this.ipRateLimitResumeTime) { console.log("[IP\u9650\u6D41] \u9650\u6D41\u65F6\u95F4\u5DF2\u8FC7\uFF0C\u6E05\u9664\u72B6\u6001"); this.ipRateLimitResumeTime = null; Storage.set("ipRateLimitResumeTime", null); } else { const remainingMinutes = Math.ceil((this.ipRateLimitResumeTime - now) / (1e3 * 60)); const resumeTime = new Date(this.ipRateLimitResumeTime); console.log(`[IP\u9650\u6D41] IP \u9650\u6D41\u4E2D\uFF0C\u8FD8\u9700\u7EA6 ${remainingMinutes} \u5206\u949F\uFF0C\u5C06\u5728 ${resumeTime.toLocaleString()} \u540E\u6062\u590D`); if (this.autoRunning) { console.log("[IP\u9650\u6D41] \u68C0\u6D4B\u5230\u81EA\u52A8\u9605\u8BFB\u8FD0\u884C\u4E2D\uFF0C\u5F3A\u5236\u505C\u6B62"); this.autoRunning = false; this.setSessionStorage("autoRunning", false); } this.startIpRateLimitRecoveryCheck(); } } }, startIpRateLimitRecoveryCheck() { if (this.ipRateLimitCheckInterval) { clearInterval(this.ipRateLimitCheckInterval); } this.ipRateLimitCheckInterval = setInterval(() => { if (!this.ipRateLimitResumeTime) { clearInterval(this.ipRateLimitCheckInterval); this.ipRateLimitCheckInterval = null; return; } const now = Date.now(); if (now >= this.ipRateLimitResumeTime) { console.log("[IP\u9650\u6D41] \u9650\u6D41\u65F6\u95F4\u5230\uFF0C\u5C1D\u8BD5\u6062\u590D..."); this.tryResumeAfterIpRateLimit(); } else { const remainingMinutes = Math.ceil((this.ipRateLimitResumeTime - now) / (1e3 * 60)); console.log(`[IP\u9650\u6D41] \u7B49\u5F85\u6062\u590D\u4E2D\uFF0C\u8FD8\u9700 ${remainingMinutes} \u5206\u949F`); } }, 6e4); console.log("[IP\u9650\u6D41] \u6062\u590D\u68C0\u6D4B\u5B9A\u65F6\u5668\u5DF2\u542F\u52A8"); }, tryResumeAfterIpRateLimit() { this.ipRateLimitResumeTime = null; Storage.set("ipRateLimitResumeTime", null); if (this.ipRateLimitCheckInterval) { clearInterval(this.ipRateLimitCheckInterval); this.ipRateLimitCheckInterval = null; } this.showNotification(this.t("ipRateLimitResume")); console.log("[IP\u9650\u6D41] IP \u9650\u6D41\u5DF2\u89E3\u9664\uFF0C\u53EF\u4EE5\u6062\u590D\u81EA\u52A8\u9605\u8BFB"); window.location.href = `${BASE_URL}/latest`; }, stopIpRateLimitRecoveryCheck() { if (this.ipRateLimitCheckInterval) { clearInterval(this.ipRateLimitCheckInterval); this.ipRateLimitCheckInterval = null; console.log("[IP\u9650\u6D41] \u6062\u590D\u68C0\u6D4B\u5B9A\u65F6\u5668\u5DF2\u505C\u6B62"); } }, observeLikeLimit() { this._likeLimitHandledByAPI = false; this._lastLikeLimitTime = 0; this._likeLimitPopupLastTime = 0; this._likeLimitPopupTimeout = null; this.interceptFetchForLikeLimit(); const self = this; const observer = new MutationObserver((mutations) => { const now = Date.now(); if (now - self._likeLimitPopupLastTime < 500) { return; } for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { const isModalElement = node.classList.contains("modal") || node.classList.contains("d-modal") || node.classList.contains("bootbox") || node.classList.contains("dialog-body") || node.classList.contains("popup-menu") || node.closest?.(".modal, .d-modal, .bootbox, .dialog-container, .fk-d-modal") || node.querySelector?.(".modal, .d-modal, .bootbox, .dialog-body, .fk-d-modal__inner"); if (!isModalElement) { continue; } const text = node.textContent || ""; const isLikeLimit = (text.includes("\u70B9\u8D5E\u4E0A\u9650") || text.includes("\u5206\u4EAB\u5F88\u591A\u7231") || text.includes("\u70B9\u8D5E") && /(?:小时|分钟)后再次点赞/.test(text)) && !text.includes("\u56DE\u590D") && !text.includes("\u521B\u5EFA\u66F4\u591A\u65B0\u56DE\u590D"); if (isLikeLimit) { self._likeLimitPopupLastTime = now; if (self._likeLimitPopupTimeout) { clearTimeout(self._likeLimitPopupTimeout); } self._likeLimitPopupTimeout = setTimeout(() => { const currentTime = Date.now(); if (self._likeLimitHandledByAPI && currentTime - self._lastLikeLimitTime < 2e3) { console.log("[\u70B9\u8D5E\u9650\u5236] XHR \u5DF2\u5904\u7406\uFF0CDOM \u76D1\u542C\u5668\u4EC5\u5173\u95ED\u5F39\u7A97"); } else { console.log("[\u70B9\u8D5E\u9650\u5236] XHR \u672A\u5904\u7406\uFF0C\u4F7F\u7528 DOM \u89E3\u6790\u4F5C\u4E3A\u5907\u7528"); self.handleLikeLimit(text); } self.closeLikeLimitPopup(); }, 300); return; } } } } }); observer.observe(document.body, { childList: true, subtree: true }); }, interceptFetchForLikeLimit() { if (this._likeLimitInterceptorsInstalled) return; this._likeLimitInterceptorsInstalled = true; const self = this; const captureRequest = (url, method) => { if (!isHeartToggleUrl(url, method)) return null; self._likeLimitHandledByAPI = false; self._lastLikeLimitTime = 0; const context = { url, method, accountKey: self.smartLikeAccountKey || self.likeCounter?.accountKey || null, startedAt: Date.now() }; self._lastHeartRequestContext = context; return context; }; const handleResponse = (context, status, data, retryAfter) => { if (status >= 200 && status < 300) return; if (status !== 429 && data?.error_type !== "rate_limit") return; context.retryAfter = retryAfter; context.rateLimited = true; self.handleSmartLike429(data?.extras?.wait_seconds, data?.extras?.time_left, context.url, context.method, { accountKey: context.accountKey, retryAfter, responseAt: context.responseAt }); }; this._likeLimitInterceptorUnsubscribe?.(); this._likeLimitInterceptorUnsubscribe = networkInterceptor.register({ beforeRequest(request) { return captureRequest(request.url, request.method); }, async afterResponse({ request, response, context }) { if (!context) return; context.responseAt = Date.now(); let data = null; if (request.transport === "fetch") { try { data = await response.clone().json(); } catch (_) { } } else { try { data = response.responseType === "json" ? response.response : JSON.parse(response.responseText); } catch (_) { } } const retryAfter = request.transport === "fetch" ? response.headers?.get?.("Retry-After") : response.getResponseHeader?.("Retry-After"); handleResponse(context, response.status, data, retryAfter); } }); console.log("[\u70B9\u8D5E\u9650\u5236] \u5DF2\u542F\u7528\u5171\u4EAB\u7F51\u7EDC\u62E6\u622A\u5668"); }, handleSmartLike429(waitSeconds, timeLeft, sourceUrl, sourceMethod = "PUT", options = {}) { if (!isHeartToggleUrl(sourceUrl, sourceMethod)) return false; const now = Date.now(); const accountKey = Object.prototype.hasOwnProperty.call(options, "accountKey") ? options.accountKey : this.smartLikeAccountKey; const seconds = Number(waitSeconds); let nextResumeTime = Number.isFinite(seconds) && seconds > 0 ? now + seconds * 1e3 : 0; let dialogDisplayTime = ""; if (!nextResumeTime && options.fromDialog) { const text = String(timeLeft || ""); let waitMinutes = 0; let parsed = false; const minuteMatch = text.match(/(?:在|可以在)\s*(\d+)\s*分钟后/); if (minuteMatch) { waitMinutes = parseInt(minuteMatch[1], 10); parsed = true; } else { const hourMatch = text.match(/(?:在|可以在)\s*(\d+)\s*小时后/); if (hourMatch) { waitMinutes = parseInt(hourMatch[1], 10) * 60; parsed = true; } else { const allMinuteMatches = text.match(/(\d+)\s*分钟/g); const allHourMatches = text.match(/(\d+)\s*小时/g); if (allMinuteMatches?.length) { const match = allMinuteMatches[allMinuteMatches.length - 1].match(/(\d+)/); if (match) { waitMinutes = parseInt(match[1], 10); parsed = true; } } else if (allHourMatches?.length) { const match = allHourMatches[allHourMatches.length - 1].match(/(\d+)/); if (match) { waitMinutes = parseInt(match[1], 10) * 60; parsed = true; } } } } if (!parsed) { waitMinutes = 10 * 60; console.log(`[\u70B9\u8D5E\u9650\u5236] \u672A\u80FD\u63D0\u53D6\u7B49\u5F85\u65F6\u95F4\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u503C: 10 \u5C0F\u65F6 = ${waitMinutes} \u5206\u949F`); } nextResumeTime = now + waitMinutes * 60 * 1e3; dialogDisplayTime = waitMinutes >= 60 ? `${Math.floor(waitMinutes / 60)}${this.t("hours")}${waitMinutes % 60 > 0 ? waitMinutes % 60 + this.t("minutes") : ""}`.trim() : `${waitMinutes}${this.t("minutes")}`; } if (!nextResumeTime) return false; nextResumeTime = Math.max(now + 1e3, nextResumeTime); const storedUntil = LikeCooldown.read(accountKey); this.smartLike429Periods || (this.smartLike429Periods = /* @__PURE__ */ new Map()); const periodKey = accountKey || "legacy"; const previous = this.smartLike429Periods.get(periodKey); const alreadyCleaned = !!(previous?.cleaned && previous.until > now && storedUntil > now); const resumeTime = LikeCooldown.extend(accountKey, nextResumeTime); const period = { until: resumeTime, cleaned: alreadyCleaned }; this.smartLike429Periods.set(periodKey, period); if (accountKey !== this.smartLikeAccountKey) return resumeTime; this.likeResumeTime = resumeTime; if (!options.fromDialog) { this._likeLimitHandledByAPI = true; this._lastLikeLimitTime = now; } if (alreadyCleaned) { if (accountKey && this.likeCounter?.accountKey === accountKey) this.likeCounter.notifyUIUpdate(); return resumeTime; } period.cleaned = true; this.smartLikeEnabled = false; Storage.set("smartLikeEnabled", false); if (this.smartLikeActiveTask) this.smartLikeActiveTask.pendingReleased = true; this.cancelSmartLikeTasks("rate_limited"); this.persistSmartLikeSession(); this.updateSmartLikeUI(); if (accountKey && this.likeCounter?.accountKey === accountKey) this.likeCounter.notifyUIUpdate(); if (this.stopOnLikeLimitEnabled && this.autoRunning) this.stopAutoReading(); if (Number.isFinite(resumeTime)) { const display = options.fromDialog && dialogDisplayTime ? dialogDisplayTime : timeLeft || `${Math.ceil((resumeTime - now) / 6e4)}${this.t("minutes")}`; this.showNotification(`${this.t("likeLimitReached")}${display}`); console.warn(`[\u70B9\u8D5E\u9650\u5236] \u5DF2\u8FDB\u5165\u51B7\u5374\uFF0C\u81F3 ${new Date(resumeTime).toLocaleString()}`); } else { this.showNotification("\u70B9\u8D5E\u72B6\u6001\u6682\u4E0D\u53EF\u7528\uFF0C\u667A\u80FD\u70B9\u8D5E\u5DF2\u5173\u95ED"); } return resumeTime; }, handleLikeLimit(text) { const context = this._lastHeartRequestContext; if (!context?.accountKey || context.accountKey !== this.smartLikeAccountKey || !isHeartToggleUrl(context.url, context.method) || Date.now() - (context.responseAt || context.startedAt) > 3e3) return false; if (!/(点赞上限|分享很多爱|点赞.*(?:分钟|小时).*点赞)/.test(String(text || ""))) return false; return this.handleSmartLike429(null, text, context.url, context.method, { accountKey: context.accountKey, retryAfter: context.retryAfter, responseAt: context.responseAt, fromDialog: true }); }, closeLikeLimitPopup() { console.log("[\u70B9\u8D5E\u9650\u5236] \u5C1D\u8BD5\u5173\u95ED\u5F39\u7A97..."); const buttonSelectors = [ ".dialog-footer .btn-primary", ".modal-footer .btn-primary", ".d-modal__footer .btn-primary", ".bootbox .btn-primary", "button.btn-primary", "button.btn-default", ".modal-close", ".close-modal", ".d-modal__dismiss", 'button[aria-label="\u5173\u95ED"]', 'button[aria-label="Close"]', ".dialog-close", ".d-modal__dismiss-icon", ".modal-header .close" ]; for (const selector of buttonSelectors) { const elements = document.querySelectorAll(selector); for (const element of elements) { if (element.offsetParent === null) continue; const text = (element.textContent || "").trim(); if (selector.includes("btn-primary") || text.includes("\u786E\u5B9A") || text.includes("OK") || text.includes("\u5173\u95ED") || text.includes("\u597D") || element.classList.contains("modal-close") || element.classList.contains("close-modal") || element.classList.contains("d-modal__dismiss")) { console.log(`[\u70B9\u8D5E\u9650\u5236] \u627E\u5230\u5173\u95ED\u6309\u94AE: ${selector}, \u6587\u672C: "${text}"`); try { element.click(); console.log("[\u70B9\u8D5E\u9650\u5236] \u5DF2\u70B9\u51FB\u5173\u95ED\u6309\u94AE"); return; } catch (e) { console.error("[\u70B9\u8D5E\u9650\u5236] \u70B9\u51FB\u6309\u94AE\u5931\u8D25:", e); } } } } const modalSelectors = [".modal", ".d-modal", ".bootbox", ".dialog-body", '[role="dialog"]']; for (const modalSelector of modalSelectors) { const modal = document.querySelector(modalSelector); if (modal && modal.offsetParent !== null) { const buttons = modal.querySelectorAll("button"); for (const btn of buttons) { const text = (btn.textContent || "").trim(); if (text.includes("\u786E\u5B9A") || text.includes("OK") || text.includes("\u5173\u95ED") || text.includes("\u597D") || btn.classList.contains("btn-primary")) { console.log(`[\u70B9\u8D5E\u9650\u5236] \u5728\u5F39\u7A97\u5185\u627E\u5230\u6309\u94AE: "${text}"`); try { btn.click(); console.log("[\u70B9\u8D5E\u9650\u5236] \u5DF2\u70B9\u51FB\u5F39\u7A97\u5185\u6309\u94AE"); return; } catch (e) { console.error("[\u70B9\u8D5E\u9650\u5236] \u70B9\u51FB\u5F39\u7A97\u5185\u6309\u94AE\u5931\u8D25:", e); } } } } } console.log("[\u70B9\u8D5E\u9650\u5236] \u672A\u627E\u5230\u5173\u95ED\u6309\u94AE\uFF0C\u5C1D\u8BD5\u6309 Escape \u952E"); document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", code: "Escape", keyCode: 27, which: 27, bubbles: true })); setTimeout(() => { const visibleButtons = document.querySelectorAll(".dialog-footer button, .modal-footer button"); for (const btn of visibleButtons) { if (btn.offsetParent !== null) { console.log(`[\u70B9\u8D5E\u9650\u5236] \u5EF6\u8FDF\u540E\u627E\u5230\u6309\u94AE: "${btn.textContent}"`); btn.click(); return; } } }, 500); } }; // src/controller/mixins/notifications.js var methods9 = { flushPendingNotifications() { if (!this.topicStatusNotification || !this.pendingNotifications?.length) return; const latestMessage = this.pendingNotifications[this.pendingNotifications.length - 1]; this.pendingNotifications = []; if (latestMessage) this.showNotification(latestMessage); }, hasTopicStatusNotification() { return !!(this.topicStatusNotification && this.topicStatusNotification.style.display !== "none" && this.topicStatusNotification.textContent); }, revealTopicStatus() { if (!this.container || this.container.classList.contains("collapsed")) return; if (!this.autoSection || !this.autoSectionContent) return; if (this.autoSection.getAttribute("aria-expanded") !== "true") { this.setSectionExpanded?.(this.autoSection, this.autoSectionContent, true); } }, hideTopicStatusIfIdle() { if (!this.topicStatusContainer || this.isScrolling || this.hasTopicStatusNotification()) return; if (this.topicStatusMode === "fetching" || this.topicStatusMode === "reading") return; if (this.topicStatusMode === "ready") { if (this.topicStatusHideTimer) return; this.topicStatusHideTimer = setTimeout(() => { this.topicStatusHideTimer = null; if (!this.isScrolling && this.topicStatusContainer && !this.hasTopicStatusNotification()) { this.topicStatusContainer.style.display = "none"; } }, 3e3); return; } if (this.topicStatusHideTimer) return; this.topicStatusContainer.style.display = "none"; }, showNotification(message) { const text = String(message ?? "").trim(); if (!text) return; if (!this.topicStatusContainer || !this.topicStatusNotification) { this.pendingNotifications = [text]; return; } if (this.topicStatusNotificationTimer) { clearTimeout(this.topicStatusNotificationTimer); this.topicStatusNotificationTimer = null; } if (this.topicStatusHideTimer) { clearTimeout(this.topicStatusHideTimer); this.topicStatusHideTimer = null; } this.topicStatusNotification.textContent = text; this.topicStatusNotification.style.display = "block"; this.topicStatusContainer.style.display = "block"; this.revealTopicStatus(); const notification = this.topicStatusNotification; this.topicStatusNotificationTimer = setTimeout(() => { if (notification !== this.topicStatusNotification) return; notification.textContent = ""; notification.style.display = "none"; this.topicStatusNotificationTimer = null; this.hideTopicStatusIfIdle(); }, 3e3); } }; // src/controller/mixins/cloudflare.js var methods10 = { isChallengePage() { return window.location.pathname.startsWith(this.CF_BYPASS_CONFIG.CHALLENGE_PATH); }, isNotFoundPage() { return Boolean(document.querySelector(".page-not-found")); }, getRedirectParamUrl() { try { const raw = new URLSearchParams(window.location.search).get("redirect"); if (!raw) return void 0; const url = new URL(raw, window.location.origin); if (url.origin !== window.location.origin) return void 0; return url.href; } catch (_) { return void 0; } }, getNotFoundRedirectGuardTs() { try { const raw = sessionStorage.getItem(this.CF_BYPASS_CONFIG.NOT_FOUND_REDIRECT_GUARD_KEY); const timestamp = raw ? Number(raw) : 0; return Number.isFinite(timestamp) ? timestamp : 0; } catch (_) { return 0; } }, setNotFoundRedirectGuardTs(timestamp) { try { sessionStorage.setItem(this.CF_BYPASS_CONFIG.NOT_FOUND_REDIRECT_GUARD_KEY, String(timestamp)); } catch (_) { } }, scheduleCFRedirect(url, replace, message) { if (this.cfBypassRedirecting) return false; this.cfBypassRedirecting = true; const navigate = () => { this.cfBypassRedirectTimer = null; try { if (replace) { window.location.replace(url); } else { window.location.href = url; } } catch (error) { this.cfBypassRedirecting = false; console.warn("[CF Challenge] \u91CD\u5B9A\u5411\u5931\u8D25:", error); } }; try { this.cfBypassRedirectTimer = setTimeout(navigate, this.CF_BYPASS_CONFIG.REDIRECT_DELAY_MS); } catch (error) { this.cfBypassRedirecting = false; console.warn("[CF Challenge] \u5B89\u6392\u91CD\u5B9A\u5411\u5931\u8D25:", error); return false; } try { if (message) this.showNotification(message); } catch (error) { console.warn("[CF Challenge] \u663E\u793A\u901A\u77E5\u5931\u8D25:", error); } return true; }, redirectFromNotFoundPage() { const fallback = `${window.location.origin}/`; const target = this.getRedirectParamUrl() || fallback; const now = Date.now(); const guardTimestamp = this.getNotFoundRedirectGuardTs(); if (guardTimestamp && now - guardTimestamp < this.CF_BYPASS_CONFIG.NOT_FOUND_REDIRECT_GUARD_MS) { return false; } this.setNotFoundRedirectGuardTs(now); const destination = target === window.location.href ? fallback : target; return this.scheduleCFRedirect( destination, true, this.t("cfBypassNotFoundRedirecting") ); }, isChallengeFailure() { if (this.isChallengePage()) return false; try { const dialogElement = document.querySelector(this.CF_BYPASS_CONFIG.DIALOG_SELECTOR); if (!dialogElement) return false; const text = dialogElement.textContent || ""; return this.CF_BYPASS_CONFIG.ERROR_TEXTS.some((errorText) => text.includes(errorText)); } catch (error) { console.warn("[CF Challenge] \u68C0\u6D4B\u5931\u8D25\u9875\u9762\u65F6\u51FA\u9519:", error); return false; } }, redirectToChallenge(message = this.t("cfBypassDetected")) { if (this.isChallengePage()) return false; if (this.autoRunning || this.isScrolling) { this.stopScrolling(); this.stopNavigationGuard(); this.autoRunning = false; if (this.navigationTimeout) { clearTimeout(this.navigationTimeout); this.navigationTimeout = null; } } const url = `${this.CF_BYPASS_CONFIG.CHALLENGE_PATH}?redirect=${encodeURIComponent(window.location.href)}`; return this.scheduleCFRedirect(url, false, message); }, checkAndRedirectCF(observer = null) { if (!this.isChallengeFailure()) return false; observer?.disconnect(); if (observer === this.cfBypassObserver) this.cfBypassObserver = null; this.redirectToChallenge(); return true; }, stopCloudFlareBypass() { if (this.cfBypassObserver) { this.cfBypassObserver.disconnect(); this.cfBypassObserver = null; } if (this.cfBypassRedirectTimer !== null) { clearTimeout(this.cfBypassRedirectTimer); this.cfBypassRedirectTimer = null; } this.cfBypassRedirecting = false; }, initCloudFlareBypass() { if (CURRENT_DOMAIN !== "linux.do") return; if (!this.cfBypassEnabled) { this.stopCloudFlareBypass(); return; } if (this.cfBypassObserver) { this.cfBypassObserver.disconnect(); this.cfBypassObserver = null; } if (this.isChallengePage()) { if (this.isNotFoundPage()) this.redirectFromNotFoundPage(); return; } if (this.checkAndRedirectCF()) return; if (!document.body) { setTimeout(() => this.initCloudFlareBypass(), 0); return; } const observer = new MutationObserver((_mutations, obs) => { if (this.isChallengePage()) { if (this.isNotFoundPage() && this.redirectFromNotFoundPage()) obs.disconnect(); return; } this.checkAndRedirectCF(obs); }); observer.observe(document.body, { childList: true, subtree: true, characterData: true }); this.cfBypassObserver = observer; }, manualTriggerCF() { if (this.isChallengePage()) { if (this.isNotFoundPage()) { this.redirectFromNotFoundPage(); } else { this.showNotification(this.t("cfBypassAlreadyOnChallenge")); } return; } this.redirectToChallenge(this.t("cfBypassRedirecting")); } }; // src/controller/mixins/account.js var methods11 = { async getCurrentUsername() { if (this.currentUsername) return this.currentUsername; if (CURRENT_DOMAIN === "cdk.linux.do") { return null; } try { try { const currentUser = window.Discourse?.User?.current?.() || window.Discourse?.currentUser || window.User?.current?.(); if (currentUser?.username) { this.currentUsername = currentUser.username; return this.currentUsername; } } catch (e) { } try { const preloadData = document.getElementById("data-preloaded"); if (preloadData) { const data = JSON.parse(preloadData.dataset.preloaded); if (data?.currentUser) { const cu = JSON.parse(data.currentUser); if (cu?.username) { this.currentUsername = cu.username; return this.currentUsername; } } } } catch (e) { } const userMenuBtn = document.querySelector(".header-dropdown-toggle.current-user"); if (userMenuBtn) { const img = userMenuBtn.querySelector("img[alt]"); if (img && img.alt) { this.currentUsername = img.alt.trim().replace(/^@/, ""); return this.currentUsername; } } const userAvatar = document.querySelector(".current-user img[title]"); if (userAvatar && userAvatar.title) { this.currentUsername = userAvatar.title.trim().replace(/^@/, ""); return this.currentUsername; } const currentUserLink = document.querySelector("a.current-user, .header-dropdown-toggle.current-user a"); if (currentUserLink) { const href = currentUserLink.getAttribute("href"); if (href && href.includes("/u/")) { const username = href.split("/u/")[1].split("/")[0]; if (username) { this.currentUsername = username.trim().replace(/^@/, ""); return this.currentUsername; } } } try { const avatarLink = document.querySelector('#current-user a[href*="/u/"]'); if (avatarLink) { const match = avatarLink.href.match(/\/u\/([^\/]+)/); if (match) { this.currentUsername = match[1].trim().replace(/^@/, ""); return this.currentUsername; } } } catch (e) { } try { const stored = localStorage.getItem("discourse_current_user"); if (stored) { const parsed = JSON.parse(stored); if (parsed?.username) { this.currentUsername = parsed.username; return this.currentUsername; } } } catch (e) { } try { const userLinks = document.querySelectorAll('a[href*="/u/"]'); for (const link of userLinks) { if (link.closest(".topic-list") || link.closest(".post-stream")) continue; const href = link.getAttribute("href"); if (href && href.includes("/u/")) { const username = href.split("/u/")[1].split("/")[0]; if (username) { this.currentUsername = username.trim().replace(/^@/, ""); return this.currentUsername; } } } } catch (e) { } if (window.location.pathname.includes("/u/")) { const username = window.location.pathname.split("/u/")[1].split("/")[0]; if (username) { this.currentUsername = username.trim().replace(/^@/, ""); return this.currentUsername; } } if (CURRENT_DOMAIN === "linux.do" || CURRENT_DOMAIN === "idcflare.com") { const session429Until = Storage.get("session429Until", 0); if (session429Until > Date.now()) { const remainMinutes = Math.ceil((session429Until - Date.now()) / 6e4); console.log(`[Session] session/current 429 \u51B7\u5374\u671F\u4E2D\uFF0C\u5269\u4F59 ${remainMinutes} \u5206\u949F\uFF0C\u8DF3\u8FC7\u8BF7\u6C42`); return null; } const response = await fetch(`${BASE_URL}/session/current.json`); if (response.status === 429) { console.warn("[Session] session/current \u9047\u5230 429\uFF0C\u8BBE\u7F6E 30 \u5206\u949F\u51B7\u5374"); Storage.set("session429Until", Date.now() + 30 * 60 * 1e3); return null; } if (response.ok) { const data = await response.json(); if (data.current_user && data.current_user.username) { this.currentUsername = data.current_user.username; return this.currentUsername; } } } } catch (error) { console.error("\u83B7\u53D6\u7528\u6237\u540D\u5931\u8D25:", error); } return null; }, async loadUserTrustLevel(isManualRefresh = false) { const username = await this.getCurrentUsername(); if (!username) { this.trustLevelContainer.innerHTML = '
\u672A\u767B\u5F55
'; return; } const now = Date.now(); const TRUST_LEVEL_CACHE_INTERVAL = 30 * 60 * 1e3; const cacheKey = `trustLevelCache_${CURRENT_DOMAIN}_${username}`; const lastFetchKey = `lastTrustLevelFetch_${CURRENT_DOMAIN}_${username}`; const lastFetch = Storage.get(lastFetchKey, 0); if (!isManualRefresh && lastFetch > 0 && now - lastFetch < TRUST_LEVEL_CACHE_INTERVAL) { const cachedData = Storage.get(cacheKey, null); if (cachedData) { console.log("\u4F7F\u7528\u7F13\u5B58\u7684\u7B49\u7EA7\u6570\u636E\uFF0C\u8DDD\u4E0A\u6B21\u83B7\u53D6:", Math.round((now - lastFetch) / 1e3 / 60), "\u5206\u949F"); this.renderCachedTrustLevel(cachedData, lastFetch); return; } } if (isManualRefresh) { const refreshBtn = this.trustLevelContainer.querySelector(".trust-level-refresh"); if (refreshBtn) { refreshBtn.textContent = this.t("refreshing"); refreshBtn.disabled = true; } } try { if (CURRENT_DOMAIN === "idcflare.com") { const summaryResponse = await fetch(`${BASE_URL}/u/${username}/summary.json`); if (summaryResponse.ok) { const data = await summaryResponse.json(); if (data.user_summary) { this.renderTrustLevel(data, username); return; } } throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7B49\u7EA7\u6570\u636E"); } else if (CURRENT_DOMAIN === "linux.do") { await this.fetchLinuxDoDataWithGM(username); } } catch (error) { console.error("\u52A0\u8F7D\u4FE1\u4EFB\u7B49\u7EA7\u5931\u8D25:", error); this.trustLevelContainer.innerHTML = `
\u{1F4CA} \u4FE1\u4EFB\u7B49\u7EA7
\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u70B9\u51FB\u5237\u65B0\u91CD\u8BD5
`; } finally { if (isManualRefresh) { setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector(".trust-level-refresh"); if (refreshBtn) { refreshBtn.textContent = "\u{1F504} \u5237\u65B0"; refreshBtn.disabled = false; } }, 1e3); } } }, saveTrustLevelCache(username, data) { const cacheKey = `trustLevelCache_${CURRENT_DOMAIN}_${username}`; const lastFetchKey = `lastTrustLevelFetch_${CURRENT_DOMAIN}_${username}`; Storage.set(cacheKey, data); Storage.set(lastFetchKey, Date.now()); console.log(`\u7B49\u7EA7\u6570\u636E\u5DF2\u7F13\u5B58 (${CURRENT_DOMAIN})`); this.saveDailySnapshot(username, data); }, saveDailySnapshot(username, data) { const today = (/* @__PURE__ */ new Date()).toISOString().split("T")[0]; const historyKey = `trustLevelHistory_${CURRENT_DOMAIN}_${username}`; const history = Storage.get(historyKey, {}); const snapshot = { date: today, timestamp: Date.now(), type: data.type, currentLevel: data.currentLevel, targetLevel: data.targetLevel, items: (data.items || data.requirements || []).map((item) => { let currentNum = item.current; let requiredNum = item.required; if (typeof item.current === "string") { const match = item.current.match(/(\d+)/); currentNum = match ? parseInt(match[1]) : 0; } if (typeof item.required === "string") { const match = item.required.match(/(\d+)/); requiredNum = match ? parseInt(match[1]) : 0; } let simpleName = item.name.replace("\u5DF2\u8BFB\u5E16\u5B50\uFF08\u6240\u6709\u65F6\u95F4\uFF09", "\u5DF2\u8BFB\u5E16\u5B50").replace("\u6D4F\u89C8\u7684\u8BDD\u9898\uFF08\u6240\u6709\u65F6\u95F4\uFF09", "\u6D4F\u89C8\u8BDD\u9898").replace("\u8BBF\u95EE\u6B21\u6570\uFF08\u8FC7\u53BB", "\u8BBF\u95EE\u6B21\u6570(").replace("\u4E2A\u6708\uFF09", "\u6708)").replace("\u56DE\u590D\u6B21\u6570\uFF08\u6700\u8FD1", "\u56DE\u590D(\u8FD1").replace("\u5929\u5185\uFF09", "\u5929)"); return { name: simpleName, current: currentNum, required: requiredNum, isMet: item.isMet }; }) }; history[today] = snapshot; const dates = Object.keys(history).sort().reverse(); if (dates.length > 30) { dates.slice(30).forEach((d) => delete history[d]); } Storage.set(historyKey, history); console.log(`\u7B49\u7EA7\u5386\u53F2\u5FEB\u7167\u5DF2\u4FDD\u5B58 (${today})`); }, getYesterdaySnapshot(username) { const historyKey = `trustLevelHistory_${CURRENT_DOMAIN}_${username}`; const history = Storage.get(historyKey, {}); const yesterday = /* @__PURE__ */ new Date(); yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toISOString().split("T")[0]; return history[yesterdayStr] || null; }, calculateDataChange(currentValue, yesterdaySnapshot, itemName) { if (!yesterdaySnapshot || !yesterdaySnapshot.items) return null; const yesterdayItem = yesterdaySnapshot.items.find((item) => item.name === itemName); if (!yesterdayItem) return null; const diff = currentValue - yesterdayItem.current; return diff; }, generateChangeIndicator(diff) { if (diff === null || diff === void 0) return ""; if (diff > 0) { return `\u2191${diff}`; } else if (diff < 0) { return `\u2193${Math.abs(diff)}`; } return ""; }, renderCachedTrustLevel(cachedData, lastFetch) { if (!cachedData) return; const { type, username, currentLevel, targetLevel, items, requirements, achievedCount, totalCount, allMet } = cachedData; const cacheAge = Date.now() - lastFetch; const cacheMinutes = Math.floor(cacheAge / 1e3 / 60); const cacheTimeText = cacheMinutes < 1 ? "\u521A\u521A" : `${cacheMinutes}\u5206\u949F\u524D`; const levelNames = { 0: "Lv0 \u2192 Lv1", 1: "Lv1 \u2192 Lv2", 2: "Lv1 \u2192 Lv2", 3: "Lv2 \u2192 Lv3", 4: "Lv3 \u2192 Lv4" }; const yesterdaySnapshot = this.getYesterdaySnapshot(username); const isAllMetForHeader = type === "low_level" ? allMet : achievedCount === totalCount; const headerTitle = isAllMetForHeader ? `Lv${targetLevel} \u2713` : levelNames[type === "low_level" ? currentLevel : targetLevel] || `Lv${currentLevel} \u2192 Lv${targetLevel}`; let html = `
\u{1F4CA} ${headerTitle} (${username})
\u7F13\u5B58: ${cacheTimeText}
`; const displayItems = type === "low_level" ? items : requirements; displayItems.forEach((req) => { let currentNum, requiredNum, displayCurrent, displayRequired; if (type === "low_level") { currentNum = req.current; requiredNum = req.required; displayCurrent = req.current; displayRequired = req.required; } else { const currentMatch = req.current.match(/(\d+)/); const requiredMatch = req.required.match(/(\d+)/); currentNum = currentMatch ? parseInt(currentMatch[1]) : 0; requiredNum = requiredMatch ? parseInt(requiredMatch[1]) : 1; displayCurrent = req.current; displayRequired = req.required; } const progress = Math.min(currentNum / requiredNum * 100, 100); const isCompleted = req.isMet; const fillClass = isCompleted ? "completed" : ""; let simpleName = req.name.replace("\u5DF2\u8BFB\u5E16\u5B50\uFF08\u6240\u6709\u65F6\u95F4\uFF09", "\u5DF2\u8BFB\u5E16\u5B50").replace("\u6D4F\u89C8\u7684\u8BDD\u9898\uFF08\u6240\u6709\u65F6\u95F4\uFF09", "\u6D4F\u89C8\u8BDD\u9898").replace("\u8BBF\u95EE\u6B21\u6570\uFF08\u8FC7\u53BB", "\u8BBF\u95EE\u6B21\u6570(").replace("\u4E2A\u6708\uFF09", "\u6708)").replace("\u56DE\u590D\u6B21\u6570\uFF08\u6700\u8FD1", "\u56DE\u590D(\u8FD1").replace("\u5929\u5185\uFF09", "\u5929)"); const diff = this.calculateDataChange(currentNum, yesterdaySnapshot, simpleName); const changeIndicator = this.generateChangeIndicator(diff); html += `
${simpleName}
${displayCurrent}/${displayRequired}${changeIndicator}
`; }); const isAllMet = type === "low_level" ? allMet : achievedCount === totalCount; if (isAllMet) { html += `
\u2705 \u5DF2\u6EE1\u8DB3 Lv${targetLevel} \u8981\u6C42
`; } else { const unmetCount = totalCount - achievedCount; html += `
\u8FD8\u9700\u5B8C\u6210 ${unmetCount} \u9879\u5347\u7EA7\u5230 Lv${targetLevel}
`; } this.trustLevelContainer.innerHTML = html; setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector(".trust-level-refresh"); if (refreshBtn) { refreshBtn.addEventListener("click", () => this.loadUserTrustLevel(true)); } }, 100); }, async fetchLinuxDoDataWithGM(username) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://connect.linux.do/", timeout: 15e3, onload: (response) => { if (response.status === 200) { const responseText = response.responseText; const tempDiv = document.createElement("div"); tempDiv.innerHTML = responseText; let globalUsername = username; let currentLevel = "\u672A\u77E5"; const h1 = tempDiv.querySelector("h1"); if (h1) { const h1Text = h1.textContent.trim(); const welcomeMatch = h1Text.match(/你好,\s*([^(\s]*)\s*\(?([^)]*)\)?\s*(\d+)级用户/i); if (welcomeMatch) { globalUsername = welcomeMatch[2] || welcomeMatch[1] || username; currentLevel = welcomeMatch[3]; console.log(`\u4ECE

\u89E3\u6790: \u7528\u6237\u540D='${globalUsername}', \u5F53\u524D\u7B49\u7EA7='${currentLevel}'`); } } let userLevel = parseInt(currentLevel); if (isNaN(userLevel)) { const pageText = tempDiv.textContent || ""; const levelRequirementMatch = pageText.match(/信任级别\s*(\d+)\s*的要求\s*(已达到|未达到)/); if (levelRequirementMatch) { const targetLevel = parseInt(levelRequirementMatch[1]); const status = levelRequirementMatch[2]; if (status === "\u5DF2\u8FBE\u5230") { userLevel = targetLevel; console.log(`\u68C0\u6D4B\u5230"\u5DF2\u8FBE\u5230\u4FE1\u4EFB\u7EA7\u522B${targetLevel}\u7684\u8981\u6C42"\uFF0C\u7528\u6237\u7B49\u7EA7: ${userLevel}`); } else { userLevel = targetLevel - 1; console.log(`\u68C0\u6D4B\u5230"\u672A\u8FBE\u5230\u4FE1\u4EFB\u7EA7\u522B${targetLevel}\u7684\u8981\u6C42"\uFF0C\u7528\u6237\u7B49\u7EA7: ${userLevel}`); } currentLevel = String(userLevel); } if (isNaN(userLevel)) { const statusMatch = pageText.match(/(已达到|不符合)信任级别\s*(\d+)\s*要求/); if (statusMatch) { const status = statusMatch[1]; const targetLevel = parseInt(statusMatch[2]); if (status === "\u5DF2\u8FBE\u5230") { userLevel = targetLevel; console.log(`\u68C0\u6D4B\u5230"\u5DF2\u8FBE\u5230\u4FE1\u4EFB\u7EA7\u522B${targetLevel}\u8981\u6C42"\uFF0C\u7528\u6237\u7B49\u7EA7: ${userLevel}`); } else { userLevel = targetLevel - 1; console.log(`\u68C0\u6D4B\u5230"\u4E0D\u7B26\u5408\u4FE1\u4EFB\u7EA7\u522B${targetLevel}\u8981\u6C42"\uFF0C\u7528\u6237\u7B49\u7EA7: ${userLevel}`); } currentLevel = String(userLevel); } } } if (userLevel === 0 || userLevel === 1) { console.log(`\u68C0\u6D4B\u5230${userLevel}\u7EA7\u7528\u6237\uFF0C\u4F7F\u7528summary.json\u83B7\u53D6\u6570\u636E`); this.fetchLowLevelUserData(username, userLevel).then(resolve).catch(reject); } else if (userLevel >= 2) { console.log(`\u68C0\u6D4B\u5230${userLevel}\u7EA7\u7528\u6237\uFF0C\u4F7F\u7528connect.linux.do\u9875\u9762\u6570\u636E`); this.processHighLevelUserData(tempDiv, globalUsername, currentLevel); resolve(); } else { console.log("\u65E0\u6CD5\u4ECE connect.linux.do \u89E3\u6790\u7B49\u7EA7\uFF0C\u56DE\u9000\u5230 summary.json"); this.fetchLowLevelUserData(username, 1).then(resolve).catch(reject); } } else { console.warn(`[\u4FE1\u4EFB\u7B49\u7EA7] connect.linux.do \u8BF7\u6C42\u5931\u8D25(${response.status})\uFF0C\u964D\u7EA7\u5230 summary.json`); this.fetchSummaryTrustLevelFallback(username).then(resolve).catch((fallbackErr) => { reject(new Error(`\u8BF7\u6C42\u5931\u8D25\uFF0C\u72B6\u6001\u7801: ${response.status}\uFF1B\u964D\u7EA7\u4E5F\u5931\u8D25: ${fallbackErr.message}`)); }); } }, onerror: (error) => { console.error("GM_xmlhttpRequest \u9519\u8BEF:", error); this.fetchSummaryTrustLevelFallback(username).then(resolve).catch((fallbackErr) => { reject(new Error(`\u7F51\u7EDC\u8BF7\u6C42\u9519\u8BEF\uFF1B\u964D\u7EA7\u4E5F\u5931\u8D25: ${fallbackErr.message}`)); }); }, ontimeout: () => { console.error("GM_xmlhttpRequest \u8D85\u65F6"); this.fetchSummaryTrustLevelFallback(username).then(resolve).catch((fallbackErr) => { reject(new Error(`\u8BF7\u6C42\u8D85\u65F6\uFF1B\u964D\u7EA7\u4E5F\u5931\u8D25: ${fallbackErr.message}`)); }); } }); }); }, async fetchLowLevelUserData(username, currentLevel) { const summaryResponse = await fetch(`${BASE_URL}/u/${username}/summary.json`); if (summaryResponse.ok) { const data = await summaryResponse.json(); const userSummary = data.user_summary; this.renderTrustLevelNew(username, currentLevel, userSummary); } else { throw new Error("\u65E0\u6CD5\u83B7\u53D6\u7528\u6237summary\u6570\u636E"); } }, async fetchSummaryTrustLevelFallback(username) { const safeUsername = encodeURIComponent(username || ""); const summaryResponse = await fetch(`${BASE_URL}/u/${safeUsername}/summary.json`, { credentials: "include" }); if (!summaryResponse.ok) { throw new Error(`summary.json \u8BF7\u6C42\u5931\u8D25(${summaryResponse.status})`); } const data = await summaryResponse.json(); if (!data || !data.user_summary) { throw new Error("summary.json \u8FD4\u56DE\u6570\u636E\u4E0D\u5B8C\u6574"); } this.renderTrustLevel(data, username); }, processHighLevelUserData(tempDiv, globalUsername, currentLevel) { let targetInfoDiv = null; const cardDivs = tempDiv.querySelectorAll("div.card"); for (let i = 0; i < cardDivs.length; i++) { const div = cardDivs[i]; const h22 = div.querySelector("h2.card-title"); if (h22 && h22.textContent.includes("\u4FE1\u4EFB\u7EA7\u522B") && h22.textContent.includes("\u7684\u8981\u6C42")) { targetInfoDiv = div; break; } } if (!targetInfoDiv) { const potentialDivs = tempDiv.querySelectorAll("div.bg-white.p-6.rounded-lg"); for (let i = 0; i < potentialDivs.length; i++) { const div = potentialDivs[i]; const h22 = div.querySelector("h2"); if (h22 && h22.textContent.includes("\u4FE1\u4EFB\u7EA7\u522B")) { targetInfoDiv = div; break; } } } if (!targetInfoDiv) { const allDivs = tempDiv.querySelectorAll("div"); for (let i = 0; i < allDivs.length; i++) { const div = allDivs[i]; const headings = div.querySelectorAll("h1, h2, h3"); for (let j = 0; j < headings.length; j++) { if (headings[j].textContent.includes("\u4FE1\u4EFB\u7EA7\u522B") && headings[j].textContent.includes("\u7684\u8981\u6C42")) { targetInfoDiv = div; break; } } if (targetInfoDiv) break; } } if (!targetInfoDiv) { console.log("\u672A\u627E\u5230\u4FE1\u4EFB\u7EA7\u522B\u6570\u636E\u5757\uFF0C\u56DE\u9000\u5230\u4F7F\u7528summary.json"); this.fetchLowLevelUserData(globalUsername, parseInt(currentLevel)); return; } const h2 = targetInfoDiv.querySelector("h2"); const titleMatch = h2.textContent.match(/信任级别\s*(\d+)\s*的要求/); const targetLevel = titleMatch ? titleMatch[1] : "\u672A\u77E5"; const requirements = []; const rings = targetInfoDiv.querySelectorAll(".tl3-ring"); rings.forEach((ring) => { const label = ring.querySelector(".tl3-ring-label"); const circle = ring.querySelector(".tl3-ring-circle"); const currentEl = ring.querySelector(".tl3-ring-current"); const targetEl = ring.querySelector(".tl3-ring-target"); if (label && currentEl) { const name = label.textContent.trim(); const current = currentEl.textContent.trim(); const required = targetEl ? targetEl.textContent.replace(/^[\s/]+/, "").trim() : ""; const isMet = circle ? circle.classList.contains("met") : false; requirements.push({ name, current, required, isMet }); } }); const bars = targetInfoDiv.querySelectorAll(".tl3-bar-item"); bars.forEach((bar) => { const labelEl = bar.querySelector(".tl3-bar-label"); const numsEl = bar.querySelector(".tl3-bar-nums"); if (labelEl && numsEl) { const name = labelEl.textContent.trim(); const numsText = numsEl.textContent.trim(); const parts = numsText.split("/"); const current = parts[0] ? parts[0].trim() : numsText; const required = parts[1] ? parts[1].trim() : ""; const isMet = numsEl.classList.contains("met"); requirements.push({ name, current, required, isMet }); } }); const quotas = targetInfoDiv.querySelectorAll(".tl3-quota-card"); quotas.forEach((quota) => { const labelEl = quota.querySelector(".tl3-quota-label"); const numsEl = quota.querySelector(".tl3-quota-nums"); if (labelEl && numsEl) { const name = labelEl.textContent.trim(); const numsText = numsEl.textContent.trim(); const parts = numsText.split("/"); const current = parts[0] ? parts[0].trim() : numsText; const required = parts[1] ? parts[1].trim() : ""; const isMet = quota.classList.contains("met"); requirements.push({ name, current, required, isMet }); } }); const vetos = targetInfoDiv.querySelectorAll(".tl3-veto-item"); vetos.forEach((veto) => { const labelEl = veto.querySelector(".tl3-veto-label"); const valueEls = veto.querySelectorAll(".tl3-veto-value"); if (labelEl && valueEls.length) { const name = labelEl.textContent.trim(); const isMet = veto.classList.contains("met"); let current = "0"; const required = "0"; if (isMet) { current = valueEls[0].textContent.trim() || "0"; } else { current = valueEls[valueEls.length - 1].textContent.trim() || "0"; } requirements.push({ name, current, required, isMet }); } }); if (requirements.length === 0) { const tableRows = targetInfoDiv.querySelectorAll("table tbody tr"); tableRows.forEach((row) => { const cells = row.querySelectorAll("td"); if (cells.length >= 3) { const name = cells[0].textContent.trim(); const required = cells[1].textContent.trim(); const current = cells[2].textContent.trim(); const isMet = cells[2].classList.contains("status-met") || cells[2].classList.contains("text-green-500"); requirements.push({ name, current, required, isMet }); } }); } this.renderAdvancedTrustLevel(globalUsername, targetLevel, requirements); }, renderTrustLevelNew(username, currentLevel, userSummary) { const targetLevel = currentLevel + 1; const requirements = CONFIG.levelRequirements[currentLevel]; if (!requirements) { this.trustLevelContainer.innerHTML = '
\u65E0\u914D\u7F6E\u6570\u636E
'; return; } const trustLevelDetails = { items: [], achievedCount: 0, totalCount: 0 }; Object.entries(requirements).forEach(([key, requiredValue]) => { let currentValue = 0; let label = ""; let isMet = false; switch (key) { case "topics_entered": currentValue = userSummary.topics_entered || 0; label = "\u6D4F\u89C8\u7684\u8BDD\u9898"; isMet = currentValue >= requiredValue; break; case "posts_read_count": currentValue = userSummary.posts_read_count || 0; label = "\u5DF2\u8BFB\u5E16\u5B50"; isMet = currentValue >= requiredValue; break; case "time_read": currentValue = Math.floor((userSummary.time_read || 0) / 60); label = "\u9605\u8BFB\u65F6\u957F(\u5206)"; isMet = (userSummary.time_read || 0) >= requiredValue; requiredValue = Math.floor(requiredValue / 60); break; case "days_visited": currentValue = userSummary.days_visited || 0; label = "\u8BBF\u95EE\u5929\u6570"; isMet = currentValue >= requiredValue; break; case "likes_given": currentValue = userSummary.likes_given || 0; label = "\u7ED9\u51FA\u7684\u8D5E"; isMet = currentValue >= requiredValue; break; case "likes_received": currentValue = userSummary.likes_received || 0; label = "\u6536\u5230\u7684\u8D5E"; isMet = currentValue >= requiredValue; break; case "post_count": currentValue = userSummary.post_count || 0; label = "\u5E16\u5B50\u6570\u91CF"; isMet = currentValue >= requiredValue; break; } if (label) { trustLevelDetails.items.push({ name: label, current: currentValue, required: requiredValue, isMet }); if (isMet) { trustLevelDetails.achievedCount++; } trustLevelDetails.totalCount++; } }); const achievedCount = trustLevelDetails.achievedCount; const totalCount = trustLevelDetails.totalCount; const allMet = achievedCount === totalCount; const levelNames = { 0: "Lv0 \u2192 Lv1", 1: "Lv1 \u2192 Lv2" }; const yesterdaySnapshot = this.getYesterdaySnapshot(username); const headerTitle = allMet ? `Lv${targetLevel} \u2713` : levelNames[currentLevel] || `Lv${currentLevel} \u2192 Lv${targetLevel}`; let html = `
\u{1F4CA} ${headerTitle} (${username})
`; trustLevelDetails.items.forEach((req) => { const progress = Math.min(req.current / req.required * 100, 100); const isCompleted = req.isMet; const fillClass = isCompleted ? "completed" : ""; const diff = this.calculateDataChange(req.current, yesterdaySnapshot, req.name); const changeIndicator = this.generateChangeIndicator(diff); const isNegativeIndicator = req.name.includes("\u88AB\u7981\u8A00") || req.name.includes("\u88AB\u5C01\u7981") || req.name.includes("\u88AB\u4E3E\u62A5\u7684\u5E16\u5B50") || req.name.includes("\u53D1\u8D77\u4E3E\u62A5\u7684\u7528\u6237"); const currentValueHtml = isNegativeIndicator ? `${req.current}` : req.current; html += `
${req.name}
${currentValueHtml}/${req.required}${changeIndicator}
`; }); if (allMet) { html += `
\u2705 \u5DF2\u6EE1\u8DB3 Lv${targetLevel} \u8981\u6C42
`; } else { const unmetCount = totalCount - achievedCount; html += `
\u8FD8\u9700\u5B8C\u6210 ${unmetCount} \u9879\u5347\u7EA7\u5230 Lv${targetLevel}
`; } this.trustLevelContainer.innerHTML = html; this.saveTrustLevelCache(username, { type: "low_level", username, currentLevel, targetLevel, items: trustLevelDetails.items, achievedCount, totalCount, allMet }); setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector(".trust-level-refresh"); if (refreshBtn) { refreshBtn.addEventListener("click", () => this.loadUserTrustLevel(true)); } }, 100); }, renderTrustLevel(data, username) { const summary = data.user_summary; if (!summary) { this.trustLevelContainer.innerHTML = '
\u65E0\u6570\u636E
'; return; } const currentLevel = summary.trust_level !== void 0 ? summary.trust_level : data.user && data.user.trust_level !== void 0 ? data.user.trust_level : 1; const targetLevel = currentLevel + 1; const levelConfig = CONFIG.levelRequirements[currentLevel]; if (!levelConfig) { this.renderDefaultTrustLevel(summary, username); return; } const requirements = []; Object.entries(levelConfig).forEach(([key, requiredValue]) => { let currentValue = 0; let label = ""; switch (key) { case "topics_entered": currentValue = summary.topics_entered || 0; label = "\u6D4F\u89C8\u7684\u8BDD\u9898"; break; case "posts_read_count": currentValue = summary.posts_read_count || 0; label = "\u5DF2\u8BFB\u5E16\u5B50"; break; case "time_read": currentValue = Math.floor((summary.time_read || 0) / 60); label = "\u9605\u8BFB\u65F6\u957F(\u5206)"; requiredValue = Math.floor(requiredValue / 60); break; case "days_visited": currentValue = summary.days_visited || 0; label = "\u8BBF\u95EE\u5929\u6570"; break; case "likes_given": currentValue = summary.likes_given || 0; label = "\u7ED9\u51FA\u7684\u8D5E"; break; case "likes_received": currentValue = summary.likes_received || 0; label = "\u6536\u5230\u7684\u8D5E"; break; case "post_count": currentValue = summary.post_count || 0; label = "\u5E16\u5B50\u6570\u91CF"; break; } if (label) { requirements.push({ name: label, current: currentValue, required: requiredValue }); } }); const achievedCount = requirements.filter((req) => req.current >= req.required).length; const totalCount = requirements.length; const allMet = achievedCount === totalCount; const levelNames = { 0: "Lv0 \u2192 Lv1", 1: "Lv1 \u2192 Lv2", 2: "Lv2 \u2192 Lv3", 3: "Lv3 \u2192 Lv4" }; const yesterdaySnapshot = this.getYesterdaySnapshot(username); let html = `
\u{1F4CA} ${levelNames[currentLevel] || `Lv${currentLevel} \u2192 Lv${targetLevel}`} (${username})
`; requirements.forEach((req) => { const progress = Math.min(req.current / req.required * 100, 100); const isCompleted = req.current >= req.required; const fillClass = isCompleted ? "completed" : ""; const diff = this.calculateDataChange(req.current, yesterdaySnapshot, req.name); const changeIndicator = this.generateChangeIndicator(diff); const isNegativeIndicator = req.name.includes("\u88AB\u7981\u8A00") || req.name.includes("\u88AB\u5C01\u7981") || req.name.includes("\u88AB\u4E3E\u62A5\u7684\u5E16\u5B50") || req.name.includes("\u53D1\u8D77\u4E3E\u62A5\u7684\u7528\u6237"); const currentValueHtml = isNegativeIndicator ? `${req.current}` : req.current; html += `
${req.name}
${currentValueHtml}/${req.required}${changeIndicator}
`; }); if (allMet) { html += `
\u2705 \u5DF2\u6EE1\u8DB3 Lv${targetLevel} \u8981\u6C42
`; } else { const unmetCount = totalCount - achievedCount; html += `
\u8FD8\u9700\u5B8C\u6210 ${unmetCount} \u9879\u5347\u7EA7\u5230 Lv${targetLevel}
`; } this.trustLevelContainer.innerHTML = html; const cacheItems = requirements.map((req) => ({ name: req.name, current: req.current, required: req.required, isMet: req.current >= req.required })); this.saveTrustLevelCache(username, { type: "low_level", username, currentLevel, targetLevel, items: cacheItems, achievedCount, totalCount, allMet }); setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector(".trust-level-refresh"); if (refreshBtn) { refreshBtn.addEventListener("click", () => this.loadUserTrustLevel(true)); } }, 100); }, renderDefaultTrustLevel(summary, username) { const requirements = [ { name: "\u8BBF\u95EE\u5929\u6570", current: summary.days_visited, required: 15 }, { name: "\u7ED9\u51FA\u7684\u8D5E", current: summary.likes_given, required: 1 }, { name: "\u6536\u5230\u7684\u8D5E", current: summary.likes_received, required: 1 }, { name: "\u5E16\u5B50\u6570\u91CF", current: summary.post_count, required: 3 }, { name: "\u8FDB\u5165\u4E3B\u9898", current: summary.topics_entered, required: 20 }, { name: "\u9605\u8BFB\u5E16\u5B50", current: summary.posts_read_count, required: 100 }, { name: "\u9605\u8BFB\u65F6\u957F(\u5206)", current: Math.floor(summary.time_read / 60), required: 60 } ]; const achievedCount = requirements.filter((req) => req.current >= req.required).length; const totalCount = requirements.length; const allMet = achievedCount === totalCount; let html = `
\u{1F4CA} \u7B49\u7EA7 (L2+) (${username || ""})
`; if (allMet) { html += `
\u{1F389} \u6240\u6709\u8981\u6C42\u5DF2\u8FBE\u6807\uFF01
`; } else { const unmetCount = totalCount - achievedCount; html += `
\u8FD8\u9700\u5B8C\u6210 ${unmetCount} \u9879\u8981\u6C42
`; } requirements.forEach((req) => { const progress = Math.min(req.current / req.required * 100, 100); const isCompleted = req.current >= req.required; const fillClass = isCompleted ? "completed" : ""; const isNegativeIndicator = req.name.includes("\u88AB\u7981\u8A00") || req.name.includes("\u88AB\u5C01\u7981") || req.name.includes("\u88AB\u4E3E\u62A5\u7684\u5E16\u5B50") || req.name.includes("\u53D1\u8D77\u4E3E\u62A5\u7684\u7528\u6237"); const currentValueHtml = isNegativeIndicator ? `${req.current}` : req.current; html += `
${req.name}
${currentValueHtml}/${req.required}
`; }); this.trustLevelContainer.innerHTML = html; setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector(".trust-level-refresh"); if (refreshBtn) { refreshBtn.addEventListener("click", () => this.loadUserTrustLevel(true)); } }, 100); }, renderAdvancedTrustLevel(username, targetLevel, requirements) { const achievedCount = requirements.filter((r) => r.isMet).length; const totalCount = requirements.length; const currentLevel = parseInt(targetLevel) - 1; const levelNames = { 2: "Lv1 \u2192 Lv2", 3: "Lv2 \u2192 Lv3", 4: "Lv3 \u2192 Lv4" }; const yesterdaySnapshot = this.getYesterdaySnapshot(username); const allRequirementsMet = achievedCount === totalCount; const headerTitle = allRequirementsMet ? `Lv${targetLevel} \u2713` : levelNames[targetLevel] || `Lv${currentLevel} \u2192 Lv${targetLevel}`; let html = `
\u{1F4CA} ${headerTitle} (${username})
`; requirements.forEach((req) => { const currentMatch = req.current.match(/(\d+)/); const requiredMatch = req.required.match(/(\d+)/); const currentNum = currentMatch ? parseInt(currentMatch[1]) : 0; const requiredNum = requiredMatch ? parseInt(requiredMatch[1]) : 1; const progress = Math.min(currentNum / requiredNum * 100, 100); const isCompleted = req.isMet; const fillClass = isCompleted ? "completed" : ""; let simpleName = req.name.replace("\u5DF2\u8BFB\u5E16\u5B50\uFF08\u6240\u6709\u65F6\u95F4\uFF09", "\u5DF2\u8BFB\u5E16\u5B50").replace("\u6D4F\u89C8\u7684\u8BDD\u9898\uFF08\u6240\u6709\u65F6\u95F4\uFF09", "\u6D4F\u89C8\u8BDD\u9898").replace("\u8BBF\u95EE\u6B21\u6570\uFF08\u8FC7\u53BB", "\u8BBF\u95EE\u6B21\u6570(").replace("\u4E2A\u6708\uFF09", "\u6708)").replace("\u56DE\u590D\u6B21\u6570\uFF08\u6700\u8FD1", "\u56DE\u590D(\u8FD1").replace("\u5929\u5185\uFF09", "\u5929)"); const diff = this.calculateDataChange(currentNum, yesterdaySnapshot, simpleName); const changeIndicator = this.generateChangeIndicator(diff); const isNegativeIndicator = req.name.includes("\u88AB\u7981\u8A00") || req.name.includes("\u88AB\u5C01\u7981") || req.name.includes("\u88AB\u4E3E\u62A5\u7684\u5E16\u5B50") || req.name.includes("\u53D1\u8D77\u4E3E\u62A5\u7684\u7528\u6237"); const currentValueHtml = isNegativeIndicator ? `${req.current}` : req.current; html += `
${simpleName}
${currentValueHtml}/${req.required}${changeIndicator}
`; }); if (achievedCount === totalCount) { html += `
\u2705 \u5DF2\u6EE1\u8DB3 Lv${targetLevel} \u8981\u6C42
`; } else { const unmetCount = totalCount - achievedCount; html += `
\u8FD8\u9700\u5B8C\u6210 ${unmetCount} \u9879\u5347\u7EA7\u5230 Lv${targetLevel}
`; } this.trustLevelContainer.innerHTML = html; this.saveTrustLevelCache(username, { type: "high_level", username, targetLevel, currentLevel, requirements, achievedCount, totalCount }); setTimeout(() => { const refreshBtn = this.trustLevelContainer.querySelector(".trust-level-refresh"); if (refreshBtn) { refreshBtn.addEventListener("click", () => this.loadUserTrustLevel(true)); } }, 100); } }; // src/controller/mixins/credit.js var methods12 = { async loadCreditInfo(isManualRefresh = false) { if (!this.creditContainer) return; if (!isManualRefresh && !this.shouldRefreshCreditInfo()) return; const now = Date.now(); const MIN_INTERVAL = 30 * 60 * 1e3; const leaderboard429Until = Storage.get("leaderboard429Until", 0); if (leaderboard429Until > now) { const remainingMinutes = Math.ceil((leaderboard429Until - now) / 6e4); console.log(`[Credit] Leaderboard 429 \u51B7\u5374\u4E2D\uFF0C\u8FD8\u9700\u7B49\u5F85 ${remainingMinutes} \u5206\u949F`); const cachedData = Storage.get("creditCachedData", null); if (cachedData) { this.renderCreditInfo(cachedData.userData, cachedData.dailyStats, cachedData.leaderboardData); const footer = this.creditContainer.querySelector(".credit-footer"); if (footer) { footer.innerHTML = ` ${this.t("creditViewDetails")} \u{1F525} \u51B7\u5374\u4E2D ${remainingMinutes}\u5206\u949F `; } } else { this.renderCreditError(`\u8BF7\u6C42\u8FC7\u4E8E\u9891\u7E41\uFF0C\u8BF7\u7B49\u5F85 ${remainingMinutes} \u5206\u949F\u540E\u518D\u8BD5`); } return; } const lastCreditFetch = Storage.get("lastCreditFetch", 0); if (!isManualRefresh && lastCreditFetch > 0 && now - lastCreditFetch < MIN_INTERVAL) { const cachedData = Storage.get("creditCachedData", null); if (cachedData) { console.log("[Credit] \u4F7F\u7528\u7F13\u5B58\u6570\u636E\uFF08\u672A\u5230\u5237\u65B0\u95F4\u9694\uFF09"); this.renderCreditInfo(cachedData.userData, cachedData.dailyStats, cachedData.leaderboardData); return; } } if (isManualRefresh) { const refreshBtn = this.creditContainer.querySelector(".credit-refresh-btn"); if (refreshBtn) { refreshBtn.textContent = refreshBtn.classList.contains("credit-auth-refresh") ? "\u2026" : this.t("refreshing"); refreshBtn.setAttribute("aria-busy", "true"); refreshBtn.disabled = true; } } else { this.creditContainer.innerHTML = `
${this.t("loadingCredits")}
`; } try { const userData = await this.fetchCreditUserInfo(); if (!userData) { this.renderCreditError(this.t("creditLoginRequired"), true); return; } const dailyStats = await this.fetchCreditDailyStats(); const leaderboardData = await this.fetchLeaderboardData(); Storage.set("creditCachedData", { userData, dailyStats, leaderboardData }); Storage.set("lastCreditFetch", now); this.renderCreditInfo(userData, dailyStats, leaderboardData); } catch (error) { console.error("\u52A0\u8F7D Credit \u4FE1\u606F\u5931\u8D25:", error); this.renderCreditError("\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5"); } }, fetchCreditUserInfo() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: "https://credit.linux.do/api/v1/oauth/user-info", anonymous: false, timeout: 15e3, headers: { "Accept": "application/json", "Referer": "https://credit.linux.do/home", "Origin": "https://credit.linux.do" }, onload: (response) => { console.log("[Credit] API \u54CD\u5E94\u72B6\u6001:", response.status); if (response.status === 200) { try { const json = JSON.parse(response.responseText); if (json && json.data) { resolve(json.data); return; } } catch (e) { console.error("Credit API \u89E3\u6790\u9519\u8BEF:", e); } } else if (response.status === 401 || response.status === 403) { console.log("[Credit] \u672A\u767B\u5F55\u6216\u65E0\u6743\u9650"); resolve(null); return; } resolve(null); }, onerror: (error) => { console.error("Credit API \u8BF7\u6C42\u9519\u8BEF:", error); resolve(null); }, ontimeout: () => { console.error("Credit API \u8BF7\u6C42\u8D85\u65F6"); resolve(null); } }); }); }, fetchCreditDailyStats() { return new Promise((resolve) => { GM_xmlhttpRequest({ method: "GET", url: "https://credit.linux.do/api/v1/dashboard/stats/daily?days=7", anonymous: false, timeout: 15e3, headers: { "Accept": "application/json", "Referer": "https://credit.linux.do/home", "Origin": "https://credit.linux.do" }, onload: (response) => { if (response.status === 200) { try { const json = JSON.parse(response.responseText); if (json && json.data && Array.isArray(json.data)) { resolve(json.data); return; } } catch (e) { console.error("Credit \u6BCF\u65E5\u7EDF\u8BA1\u89E3\u6790\u9519\u8BEF:", e); } } resolve([]); }, onerror: () => resolve([]), ontimeout: () => resolve([]) }); }); }, fetchLeaderboardData() { return new Promise((resolve) => { const now = Date.now(); const leaderboard429Until = Storage.get("leaderboard429Until", 0); if (leaderboard429Until > now) { const remainMinutes = Math.ceil((leaderboard429Until - now) / 6e4); console.log(`[Leaderboard] 429 \u51B7\u5374\u671F\u4E2D\uFF0C\u5269\u4F59 ${remainMinutes} \u5206\u949F`); const cachedLeaderboard = Storage.get("cachedLeaderboardData", null); resolve(cachedLeaderboard); return; } let got429 = false; Promise.all([ fetch("https://linux.do/leaderboard/1?period=daily", { credentials: "include", headers: { "Accept": "application/json" } }).then((r) => { if (r.status === 429) { got429 = true; return null; } return r.ok ? r.json() : null; }).catch(() => null), fetch("https://linux.do/leaderboard/1?period=all", { credentials: "include", headers: { "Accept": "application/json" } }).then((r) => { if (r.status === 429) { got429 = true; return null; } return r.ok ? r.json() : null; }).catch(() => null) ]).then(([dailyData, allTimeData]) => { if (got429) { const cooldownUntil = Date.now() + 30 * 60 * 1e3; Storage.set("leaderboard429Until", cooldownUntil); console.warn("[Leaderboard] \u68C0\u6D4B\u5230 429 \u9519\u8BEF\uFF0C\u5DF2\u8BBE\u7F6E 30 \u5206\u949F\u51B7\u5374\u671F"); } let dailyScore = 0; let totalCredits = 0; let totalRank = 0; if (dailyData && dailyData.personal && dailyData.personal.user) { dailyScore = dailyData.personal.user.total_score || 0; } if (allTimeData && allTimeData.personal && allTimeData.personal.user) { totalCredits = allTimeData.personal.user.total_score || 0; totalRank = allTimeData.personal.position || allTimeData.personal.user.position || 0; } if (totalRank || totalCredits) { const result = { totalCredits, rank: totalRank, dailyScore }; Storage.set("cachedLeaderboardData", result); resolve(result); } else { const cachedLeaderboard = Storage.get("cachedLeaderboardData", null); resolve(cachedLeaderboard); } }).catch((error) => { console.error("\u83B7\u53D6\u6392\u884C\u699C\u6570\u636E\u5931\u8D25:", error); resolve(null); }); }); }, renderCreditInfo(userData, dailyStats, leaderboardData = null) { const credits = userData.available_balance || "0"; const communityBalance = userData.community_balance || "0"; const dailyLimit = userData.remain_quota || "0"; const incomeTotal = userData.total_receive || "0"; const expenseTotal = userData.total_payment || "0"; const username = userData.nickname || userData.username || "User"; let incomeList = []; let expenseList = []; if (dailyStats && dailyStats.length > 0) { dailyStats.forEach((item) => { const date = item.date.substring(5).replace("-", "/"); const income = parseFloat(item.income) || 0; const expense = parseFloat(item.expense) || 0; if (income !== 0) { incomeList.push({ date, amount: income > 0 ? "+" + income.toFixed(2) : income.toFixed(2), isNegative: income < 0 }); } if (expense > 0) expenseList.push({ date, amount: "-" + expense.toFixed(2) }); }); incomeList.reverse(); expenseList.reverse(); } const updateTime = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); let html = `
\u{1F4B0} ${this.escapeHtml(username)} ${this.t("userCredits")}
${this.t("creditAvailable")} ${this.escapeHtml(credits)}
`; if (leaderboardData) { const tomorrowCredits = (leaderboardData.totalCredits - communityBalance).toFixed(0); html += `
${this.t("creditTomorrow")} ${this.escapeHtml(tomorrowCredits)}
`; } html += `
`; if (leaderboardData) { html += `
${this.t("creditCurrentPoints")} ${this.escapeHtml(leaderboardData.totalCredits)} ${this.t("creditRankLabel")}#${this.escapeHtml(leaderboardData.rank)}
${this.t("creditYesterdayPoints")} ${this.escapeHtml(communityBalance)}
`; } html += `
${this.t("creditDailyLimit")} ${this.escapeHtml(dailyLimit)}
${this.t("creditTotalIncome")} +${this.escapeHtml(incomeTotal)}
${this.t("creditTotalExpense")} -${this.escapeHtml(expenseTotal)}
`; html += `
`; if (incomeList.length > 0) { html += `
${this.t("creditRecentIncome")}
`; incomeList.slice(0, 5).forEach((item) => { const note = item.isNegative ? " (\u793E\u533A\u70B9\u6570\u5012\u9000\u6263\u9664)" : ""; html += `
${this.escapeHtml(item.date)}${note} ${this.escapeHtml(item.amount)}
`; }); html += `
`; } if (expenseList.length > 0) { html += `
${this.t("creditRecentExpense")}
`; expenseList.slice(0, 3).forEach((item) => { html += `
${this.escapeHtml(item.date)} ${this.escapeHtml(item.amount)}
`; }); html += `
`; } html += ` `; this.creditContainer.innerHTML = html; setTimeout(() => { const refreshBtn = this.creditContainer.querySelector(".credit-refresh-btn"); if (refreshBtn) { refreshBtn.addEventListener("click", (e) => { e.stopPropagation(); this.loadCreditInfo(true); }); } }, 100); }, renderCreditError(message, showLogin = false) { if (showLogin) { const html = `
${this.t("notLoggedIn")}

${this.t("creditLoginDescription")}

`; this.creditContainer.innerHTML = html; } else { const html = `
${this.t("sectionCredit")}

${this.escapeHtml(message)}

`; this.creditContainer.innerHTML = html; } setTimeout(() => { const refreshBtn = this.creditContainer.querySelector(".credit-refresh-btn"); if (refreshBtn) { refreshBtn.addEventListener("click", (e) => { e.stopPropagation(); this.loadCreditInfo(true); }); } }, 100); } }; // src/controller/mixins/cdk.js var methods13 = { async loadCdkInfo(isManualRefresh = false) { if (!this.cdkContainer) return; if (!isManualRefresh && !this.shouldRefreshCdkInfo()) return; if (isManualRefresh) { const refreshBtn = this.cdkContainer.querySelector(".cdk-refresh-btn"); if (refreshBtn) { refreshBtn.textContent = refreshBtn.classList.contains("cdk-panel-refresh") ? "\u2026" : this.t("refreshing"); refreshBtn.setAttribute("aria-busy", "true"); refreshBtn.disabled = true; } } else { this.cdkContainer.innerHTML = `
${this.t("loadingCdk")}
`; } try { const cdkData = await this.fetchCdkUserInfo(); if (!cdkData) { this.renderCdkError(this.t("cdkNotAuth"), true); return; } this.renderCdkInfo(cdkData); } catch (error) { console.error("\u52A0\u8F7D CDK \u4FE1\u606F\u5931\u8D25:", error); this.renderCdkError(this.t("loadFailed")); } }, ensureCdkBridge() { if (this.cdkBridgeInit) return; this.cdkBridgeInit = true; this.cdkWaiters = []; window.addEventListener("message", (event) => { if (event.origin !== "https://cdk.linux.do") return; const payload = event.data?.payload || event.data; if (!payload?.data) return; console.log("[CDK] \u6536\u5230 Bridge \u6570\u636E:", payload.data); GM_setValue("lda_cdk_cache", { data: payload.data, ts: Date.now() }); const waiters = [...this.cdkWaiters]; this.cdkWaiters = []; waiters.forEach((fn) => fn(payload.data)); }); const iframe = document.createElement("iframe"); iframe.id = "lda-cdk-bridge"; iframe.src = "https://cdk.linux.do/dashboard"; iframe.style.cssText = "width:0;height:0;opacity:0;position:absolute;border:0;pointer-events:none;"; document.body.appendChild(iframe); this.cdkBridgeFrame = iframe; }, fetchCdkViaBridge() { return new Promise((resolve, reject) => { this.ensureCdkBridge(); const timer = setTimeout(() => { this.cdkWaiters = this.cdkWaiters.filter((fn) => fn !== done); reject(new Error("CDK bridge timeout")); }, 8e3); const done = (data) => { clearTimeout(timer); resolve(data); }; this.cdkWaiters.push(done); try { this.cdkBridgeFrame?.contentWindow?.postMessage({ type: "lda-cdk-request" }, "https://cdk.linux.do"); } catch (_) { } }); }, async fetchCdkUserInfo() { const cache = GM_getValue("lda_cdk_cache", null); if (cache && cache.data && cache.ts && Date.now() - cache.ts < 5 * 60 * 1e3) { console.log("[CDK] \u4F7F\u7528 GM \u7F13\u5B58\u6570\u636E"); return cache.data; } try { const directResult = await this.fetchCdkDirect(); if (directResult) { console.log("[CDK] \u76F4\u63A5\u8BF7\u6C42\u6210\u529F"); const cacheData = { user: directResult, received: null }; GM_setValue("lda_cdk_cache", { data: cacheData, ts: Date.now() }); return cacheData; } } catch (e) { console.log("[CDK] \u76F4\u63A5\u8BF7\u6C42\u5931\u8D25\uFF0C\u5C1D\u8BD5 Bridge \u65B9\u5F0F:", e.message); } try { const bridgeResult = await this.fetchCdkViaBridge(); if (bridgeResult) { console.log("[CDK] Bridge \u8BF7\u6C42\u6210\u529F"); return bridgeResult; } } catch (e) { console.log("[CDK] Bridge \u8BF7\u6C42\u5931\u8D25:", e.message); } if (cache && cache.data) { console.log("[CDK] \u4F7F\u7528\u65E7\u7F13\u5B58\u6570\u636E"); return cache.data; } return null; }, fetchCdkDirect() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://cdk.linux.do/api/v1/oauth/user-info", anonymous: false, timeout: 1e4, headers: { "Accept": "application/json", "Cache-Control": "no-cache" }, onload: (response) => { if (response.responseText && response.responseText.includes("Just a moment")) { reject(new Error("Cloudflare challenge")); return; } if (response.status === 401 || response.status === 403) { resolve(null); return; } if (response.status >= 200 && response.status < 300) { try { const json = JSON.parse(response.responseText); if (json && json.data) { resolve(json.data); return; } if (json && (json.username || json.score !== void 0)) { resolve(json); return; } } catch (e) { reject(new Error("JSON parse error")); return; } } resolve(null); }, onerror: (error) => { reject(error); }, ontimeout: () => { reject(new Error("\u8BF7\u6C42\u8D85\u65F6")); } }); }); }, fetchCdkReceived() { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://cdk.linux.do/api/v1/projects/received?current=1&size=20&search=", anonymous: false, withCredentials: true, timeout: 15e3, headers: { "Accept": "application/json", "Cache-Control": "no-cache", "Referer": "https://cdk.linux.do/received" }, onload: (response) => { console.log("[CDK] \u9886\u53D6\u8BB0\u5F55\u54CD\u5E94\u72B6\u6001:", response.status); if (response.responseText && response.responseText.includes("Just a moment")) { console.log("[CDK] \u9886\u53D6\u8BB0\u5F55\u88AB Cloudflare \u62E6\u622A"); resolve({ total: 0, results: [], cloudflareBlocked: true }); return; } if (response.status >= 200 && response.status < 300) { try { const json = JSON.parse(response.responseText); console.log("[CDK] \u9886\u53D6\u8BB0\u5F55\u89E3\u6790\u6210\u529F:", json); if (json && json.data) { resolve({ total: json.data.total || 0, results: json.data.results || [] }); return; } if (json && json.error_msg === "") { resolve({ total: 0, results: [] }); return; } } catch (e) { console.error("[CDK] \u89E3\u6790\u9886\u53D6\u8BB0\u5F55\u5931\u8D25:", e, response.responseText?.substring(0, 200)); } } else { console.error("[CDK] \u9886\u53D6\u8BB0\u5F55\u8BF7\u6C42\u5931\u8D25\uFF0C\u72B6\u6001\u7801:", response.status); } resolve({ total: 0, results: [] }); }, onerror: (error) => { console.error("[CDK] \u83B7\u53D6\u9886\u53D6\u8BB0\u5F55\u5931\u8D25:", error); resolve({ total: 0, results: [] }); }, ontimeout: () => { console.error("[CDK] \u83B7\u53D6\u9886\u53D6\u8BB0\u5F55\u8D85\u65F6"); resolve({ total: 0, results: [] }); } }); }); }, renderCdkInfo(cdkData) { const userData = cdkData.user || cdkData; const receivedData = cdkData.received || null; const score = userData.score || 0; const trustLevel = userData.trust_level ?? userData.trustLevel ?? "-"; const username = userData.username || "-"; const nickname = userData.nickname || userData.name || username; const displayNickname = nickname || username || "-"; const showUsername = username && username !== displayNickname && username !== "-"; const updateTime = (/* @__PURE__ */ new Date()).toLocaleTimeString("zh-CN", { hour: "2-digit", minute: "2-digit" }); let html = `
${this.t("cdkOverview")}
${this.t("cdkScore")} ${this.escapeHtml(score)}
${this.t("cdkAccountInfo")}
\u{1F4A1} ${this.t("cdkScoreDesc")}
\u{1F4E6} ${this.t("cdkMyReceived")} (${this.t("cdkRecentLimit")})
${this.t("cdkLoadingReceived")}
`; this.cdkContainer.innerHTML = html; setTimeout(() => { const refreshBtn = this.cdkContainer.querySelector(".cdk-refresh-btn"); if (refreshBtn) { refreshBtn.addEventListener("click", (e) => { e.stopPropagation(); this.loadCdkInfo(true); }); } const receivedRefreshBtn = this.cdkContainer.querySelector(".cdk-received-refresh-btn"); if (receivedRefreshBtn) { receivedRefreshBtn.addEventListener("click", (e) => { e.stopPropagation(); this.loadCdkReceived(); }); } }, 100); if (receivedData) { console.log("[CDK] \u4F7F\u7528\u7F13\u5B58\u7684\u9886\u53D6\u8BB0\u5F55"); this.renderCdkReceived(receivedData); } else { console.log("[CDK] \u9886\u53D6\u8BB0\u5F55\u672A\u7F13\u5B58\uFF0C\u5C1D\u8BD5\u52A0\u8F7D"); this.loadCdkReceived(); } }, async loadCdkReceived() { const listContainer = document.getElementById("cdk-received-list"); if (!listContainer) return; listContainer.innerHTML = `
${this.t("cdkLoadingReceived")}
`; try { const cache = GM_getValue("lda_cdk_cache", null); if (cache && cache.data && cache.data.received && cache.ts && Date.now() - cache.ts < 5 * 60 * 1e3) { console.log("[CDK] \u4F7F\u7528\u7F13\u5B58\u7684\u9886\u53D6\u8BB0\u5F55"); this.renderCdkReceived(cache.data.received); return; } try { const bridgeResult = await this.fetchCdkViaBridge(); if (bridgeResult && bridgeResult.received) { console.log("[CDK] \u901A\u8FC7 Bridge \u83B7\u53D6\u5230\u9886\u53D6\u8BB0\u5F55"); this.renderCdkReceived(bridgeResult.received); return; } } catch (e) { console.log("[CDK] Bridge \u83B7\u53D6\u9886\u53D6\u8BB0\u5F55\u5931\u8D25:", e.message); } const data = await this.fetchCdkReceived(); if (data.cloudflareBlocked) { listContainer.innerHTML = `
${this.t("cdkReceivedEmpty")}
\u{1F4A1} \u8BF7\u5148\u8BBF\u95EE cdk.linux.do \u5237\u65B0\u6570\u636E
`; return; } this.renderCdkReceived(data); } catch (e) { console.error("[CDK] \u52A0\u8F7D\u9886\u53D6\u8BB0\u5F55\u5931\u8D25:", e); listContainer.innerHTML = `
${this.t("cdkReceivedEmpty")}
`; } }, renderCdkReceived(data) { const listContainer = document.getElementById("cdk-received-list"); if (!listContainer) return; const { total, results } = data; if (!results || results.length === 0) { listContainer.innerHTML = `
${this.t("cdkReceivedEmpty")}
`; return; } const displayResults = results.slice(0, 20); const displayCount = Math.min(total, 20); const totalText = this.t("cdkTotal").replace("{count}", displayCount); let html = `
${totalText}
`; displayResults.forEach((item, index) => { const projectName = this.escapeHtml(item.project_name || "-"); const creator = this.escapeHtml(item.project_creator_nickname || item.project_creator || "-"); const content = this.escapeHtml(item.content || "-"); const receivedAt = item.received_at ? new Date(item.received_at).toLocaleString("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : "-"; html += `
${projectName} ${receivedAt}
${this.t("cdkCreator")}: ${creator}
${content}
`; }); html += "
"; listContainer.innerHTML = html; const copyBtns = listContainer.querySelectorAll(".cdk-copy-btn"); copyBtns.forEach((btn) => { btn.addEventListener("click", async (e) => { e.stopPropagation(); const content = btn.getAttribute("data-content"); try { await navigator.clipboard.writeText(content); const originalText = btn.textContent; const originalTitle = btn.getAttribute("title") || this.t("cdkCopy"); btn.textContent = "\u2713"; btn.classList.add("copied"); btn.setAttribute("title", this.t("cdkCopied")); btn.setAttribute("aria-label", this.t("cdkCopied")); setTimeout(() => { btn.textContent = originalText; btn.classList.remove("copied"); btn.setAttribute("title", originalTitle); btn.setAttribute("aria-label", originalTitle); }, 1500); } catch (err) { console.error("[CDK] \u590D\u5236\u5931\u8D25:", err); } }); }); }, renderCdkError(message, showLogin = false) { let html = `
${this.t("cdkOverview")}
${showLogin ? this.t("cdkNotAuth") : this.t("cdkScore")}

${this.escapeHtml(showLogin ? this.t("cdkAuthTip") : message)}

`; if (showLogin) { html += ` `; } html += "
"; this.cdkContainer.innerHTML = html; setTimeout(() => { const refreshBtn = this.cdkContainer.querySelector(".cdk-refresh-btn"); if (refreshBtn) { refreshBtn.addEventListener("click", (e) => { e.stopPropagation(); this.loadCdkInfo(true); }); } }, 100); }, escapeHtml(text) { const div = document.createElement("div"); div.textContent = String(text); return div.innerHTML; } }; // src/controller/browse-controller.js var globalLikeCounter = null; var BrowseController = class { constructor() { __publicField(this, "CF_BYPASS_CONFIG", { ERROR_TEXTS: [ "403 error", "\u8BE5\u56DE\u5E94\u662F\u5F88\u4E45\u4EE5\u524D\u521B\u5EFA\u7684", "reaction was created too long ago", "\u6211\u4EEC\u65E0\u6CD5\u52A0\u8F7D\u8BE5\u8BDD\u9898", "You are not allowed to react" ], DIALOG_SELECTOR: ".dialog-body", CHALLENGE_PATH: "/challenge", NOT_FOUND_REDIRECT_GUARD_KEY: "linux_do_auto_challenge_nf_guard", NOT_FOUND_REDIRECT_GUARD_MS: 5e3, REDIRECT_DELAY_MS: 120 }); this.isScrolling = false; this.trustLevelMonitorInterval = null; this.navigationTimeout = null; this.navigationGuardInterval = null; this.texts = { panelTitle: "Linux.do \u52A9\u624B", sectionAutoRead: "\u26A1 \u81EA\u52A8\u9605\u8BFB", sectionAccountData: "\u{1F464} \u8D26\u53F7\u4E0E\u6570\u636E", sectionAccountInfo: "\u{1F4CB} \u8D26\u53F7\u4FE1\u606F", sectionCredit: "\u{1F48E} Credit \u79EF\u5206", sectionRanking: "\u{1F3C5} \u6392\u884C\u699C", sectionPluginSettings: "\u2699\uFE0F \u63D2\u4EF6\u8BBE\u7F6E", startReading: "\u5F00\u59CB\u9605\u8BFB", stopReading: "\u505C\u6B62\u9605\u8BFB", refresh: "\u{1F504} \u5237\u65B0", refreshing: "\u5237\u65B0\u4E2D...", detailInfo: "\u{1F4CA} \u8BE6\u7EC6\u4FE1\u606F \u2192", smartLike: "\u2764\uFE0F \u667A\u80FD\u70B9\u8D5E", smartLikeSpeed: "\u70B9\u8D5E\u901F\u5EA6", smartLikeSpeedSlow: "\u6162\u901F", smartLikeSpeedNormal: "\u6B63\u5E38", smartLikeSpeedFast: "\u5FEB\u901F", cleanMode: "\u{1F31F} \u6E05\u723D\u6A21\u5F0F", grayscaleMode: "\u{1F3A8} \u9ED1\u767D\u7070\u6A21\u5F0F", readUnread: "\u{1F4EC} \u8BFB\u53D6\u672A\u8BFB", myRanking: "\u6211\u7684\u6392\u540D", dailyRank: "\u65E5\u699C", weeklyRank: "\u5468\u699C", monthlyRank: "\u6708\u699C", quarterlyRank: "\u5B63\u699C", yearlyRank: "\u5E74\u699C", allTimeRank: "\u603B\u699C", points: "\u5206", modeSettingsLabel: "\u6A21\u5F0F\u8BBE\u7F6E", readParameterSettingsLabel: "\u{1F39B}\uFE0F \u9605\u8BFB\u53C2\u6570", userCredits: "\u7684\u79EF\u5206", loading: "\u52A0\u8F7D\u4E2D...", loadingRank: "\u52A0\u8F7D\u6392\u540D\u6570\u636E...", loadingCredits: "\u52A0\u8F7D\u79EF\u5206...", loadingLevel: "\u52A0\u8F7D\u7B49\u7EA7\u4FE1\u606F...", clickToLoad: "\u70B9\u51FB\u5C55\u5F00\u52A0\u8F7D...", clickToLoadRank: "\u70B9\u51FB\u5C55\u5F00\u52A0\u8F7D\u6392\u540D...", clickToLoadCredits: "\u70B9\u51FB\u5C55\u5F00\u52A0\u8F7D\u79EF\u5206...", loadFailed: "\u52A0\u8F7D\u5931\u8D25\uFF0C\u8BF7\u70B9\u51FB\u5237\u65B0\u91CD\u8BD5", notLoggedIn: "\u672A\u767B\u5F55", notSupported: "\u5F53\u524D\u7AD9\u70B9\u4E0D\u652F\u6301\u6B64\u529F\u80FD", update: "\u66F4\u65B0", remaining: "\u5269\u4F59", hours: "\u5C0F\u65F6", minutes: "\u5206", seconds: "\u79D2", ipRateLimited: "\u{1F6AB} IP \u88AB\u9650\u6D41\uFF0C\u81EA\u52A8\u9605\u8BFB\u5DF2\u6682\u505C", ipRateLimitWait: "\u5C06\u5728 30 \u5206\u949F\u540E\u81EA\u52A8\u6062\u590D", ipRateLimitResume: "\u2705 IP \u9650\u6D41\u5DF2\u89E3\u9664\uFF0C\u6062\u590D\u81EA\u52A8\u9605\u8BFB", ipRateLimitDetected: "\u68C0\u6D4B\u5230 IP \u9650\u6D41", loadingComplete: "\u52A0\u8F7D\u5B8C\u6210", loadingFailed: "\u52A0\u8F7D\u5931\u8D25", noUnreadPosts: "\u{1F4ED} \u6CA1\u6709\u672A\u8BFB\u5E16\u5B50\uFF0C\u5C06\u5207\u6362\u5230\u6700\u65B0\u5E16\u5B50", creditAvailable: "\u53EF\u7528\u79EF\u5206", creditTomorrow: "\u{1F31F} \u660E\u65E5\u79EF\u5206", creditTodayRank: "\u{1F4CA} \u4ECA\u65E5\u6392\u540D", creditCurrentPoints: "\u{1F4C8} \u5F53\u524D\u70B9\u6570", creditYesterdayPoints: "\u{1F4C5} \u6628\u65E5\u70B9\u6570", creditRankLabel: "\u6392\u540D", creditCommunityBalance: "\u793E\u533A\u79EF\u5206", creditDailyLimit: "\u4ECA\u65E5\u5269\u4F59\u989D\u5EA6", creditTotalIncome: "\u603B\u6536\u5165", creditTotalExpense: "\u603B\u652F\u51FA", creditRecentIncome: "\u8FD17\u5929\u6536\u5165", creditRecentExpense: "\u8FD17\u5929\u652F\u51FA", creditViewDetails: "\u67E5\u770B\u8BE6\u60C5 \u2192", creditLoginRequired: "\u8BF7\u5148\u767B\u5F55 credit.linux.do", creditLoginDescription: "\u767B\u5F55 credit.linux.do \u540E\u67E5\u770B\u79EF\u5206\u4E0E\u8D26\u6237\u6570\u636E", creditGoLogin: "\u53BB\u767B\u5F55", restStart: "\u5F00\u59CB\u4F11\u606F", restEnd: "\u4F11\u606F\u7ED3\u675F\uFF0C\u7EE7\u7EED\u6D4F\u89C8", likeLimitReached: "\u70B9\u8D5E\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u5C06\u5728 ", likeCoolingDown: "\u70B9\u8D5E\u529F\u80FD\u51B7\u5374\u4E2D", likeRemaining: "\u5269\u4F59\u70B9\u8D5E", likeUsed: "\u5DF2\u7528", likeCooldown: "\u51B7\u5374\u4E2D", likeClearCooldown: "\u6E05\u9664\u51B7\u5374", likeClearCooldownTip: "\u4EC5\u6E05\u9664\u811A\u672C\u672C\u5730\u51B7\u5374\u8BB0\u5F55", likeCooldownCleared: "\u672C\u5730\u51B7\u5374\u5DF2\u6E05\u9664\uFF1B\u670D\u52A1\u5668\u4ECD\u9650\u6D41\u65F6\u4F1A\u518D\u6B21\u8FDB\u5165\u51B7\u5374", likeCooldownClearFailed: "\u6E05\u9664\u70B9\u8D5E\u51B7\u5374\u5931\u8D25", likeCountMismatch: "\u8BA1\u6570\u53EF\u80FD\u4E0D\u51C6\u786E\uFF0C\u70B9\u51FB\u540C\u6B65", likeSyncing: "\u540C\u6B65\u4E2D...", likeSyncSuccess: "\u540C\u6B65\u6210\u529F", randomOrder: "\u{1F500} \u968F\u673A\u9605\u8BFB", randomOrderTip: "\u6253\u4E71\u5E16\u5B50\u987A\u5E8F\uFF0C\u968F\u673A\u9605\u8BFB", skipRead: "\u23ED\uFE0F \u8DF3\u8FC7\u5DF2\u8BFB", skipReadTip: "\u81EA\u52A8\u8DF3\u8FC7\u5DF2\u7ECF\u9605\u8BFB\u8FC7\u7684\u5E16\u5B50", fullTopicRead: "\u{1F4DC} \u5B8C\u6574\u9605\u8BFB", fullTopicReadTip: "\u4ECE\u5E16\u5B50\u5F00\u5934\u5F00\u59CB\uFF0C\u6EDA\u52A8\u5230\u9875\u9762\u5E95\u90E8\u540E\u518D\u5207\u6362\u4E0B\u4E00\u7BC7", topicLimit: "\u{1F4DA} \u83B7\u53D6\u6570\u91CF", topicLimitTip: "\u6BCF\u6B21\u83B7\u53D6\u7684\u5E16\u5B50\u6570\u91CF", restTimeLabel: "\u23F8\uFE0F \u4F11\u606F\u65F6\u95F4", restTimeTip: "\u8FDE\u7EED\u9605\u8BFB1\u5C0F\u65F6\u540E\u4F11\u606F\u7684\u65F6\u95F4\uFF08\u5206\u949F\uFF09", stopAfterRead: "\u{1F6D1} \u9605\u8BFB\u9650\u5236", stopAfterReadTip: "\u9605\u8BFB\u6307\u5B9A\u6570\u91CF\u5E16\u5B50\u540E\u81EA\u52A8\u505C\u6B62", stopAfterReadCount: "\u{1F4D6} \u9605\u8BFB\u6570\u91CF", stopAfterReadCountTip: "\u9605\u8BFB\u591A\u5C11\u5E16\u5B50\u540E\u505C\u6B62", stopOnLikeLimit: "\u2764\uFE0F \u70B9\u8D5E\u505C\u6B62", stopOnLikeLimitTip: "\u70B9\u8D5E\u8FBE\u5230\u4E0A\u9650\u540E\u81EA\u52A8\u505C\u6B62\u9605\u8BFB", stoppedByReadLimit: "\u2705 \u5DF2\u8FBE\u5230\u9605\u8BFB\u6570\u91CF\u9650\u5236\uFF0C\u81EA\u52A8\u505C\u6B62", stoppedByLikeLimit: "\u2764\uFE0F \u70B9\u8D5E\u5DF2\u8FBE\u4E0A\u9650\uFF0C\u81EA\u52A8\u505C\u6B62\u9605\u8BFB", stoppedByServerBusy: "\u26A0\uFE0F \u68C0\u6D4B\u5230 502\uFF0C\u5DF2\u505C\u6B62\u81EA\u52A8\u9605\u8BFB\uFF0C\u907F\u514D\u8D1F\u8F7D\u8FC7\u9AD8", stoppedByPageLoading: "\u26A0\uFE0F \u9875\u9762\u52A0\u8F7D\u8D85\u65F6\uFF0C\u5DF2\u505C\u6B62\u81EA\u52A8\u9605\u8BFB\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5", sessionReadCount: "\u672C\u6B21\u5DF2\u8BFB", fetchingTopics: "\u{1F4E5} \u83B7\u53D6\u5E16\u5B50\u4E2D...", fetchProgress: "\u83B7\u53D6\u8FDB\u5EA6", totalFetched: "\u5DF2\u83B7\u53D6", skippedRead: "\u8DF3\u8FC7\u5DF2\u8BFB", unreadTopics: "\u672A\u8BFB\u5E16\u5B50", latestTopics: "\u6700\u65B0\u5E16\u5B50", topicsReady: "\u5E16\u5B50\u5DF2\u5C31\u7EEA", currentReading: "\u{1F4D6} \u5F53\u524D\u9605\u8BFB", remainingTopics: "\u5269\u4F59\u5E16\u5B50", todayRead: "\u4ECA\u65E5\u9605\u8BFB", totalRead: "\u603B\u9605\u8BFB", pageRange: "\u9875\u7801\u8303\u56F4", startFromPage: "\u4ECE\u7B2C", pageUnit: "\u9875", continueFetching: "\u7EED\u8BFB\u4E2D", sectionCdk: "\u{1F3AE} CDK \u5206\u6570", cdkScore: "CDK \u5206\u6570", cdkTrustLevel: "\u4FE1\u4EFB\u7B49\u7EA7", cdkUsername: "\u7528\u6237\u540D", cdkNickname: "\u6635\u79F0", cdkAccountInfo: "\u8D26\u6237\u4FE1\u606F", cdkOverview: "\u8D26\u6237\u6982\u89C8", cdkRefreshTip: "\u5237\u65B0 CDK \u6570\u636E", cdkReceivedRefreshTip: "\u5237\u65B0\u9886\u53D6\u8BB0\u5F55", cdkNotAuth: "\u5C1A\u672A\u767B\u5F55 CDK", cdkAuthTip: "\u9700\u5148\u5B8C\u6210\u6388\u6743\u624D\u80FD\u67E5\u770B\u793E\u533A\u5206\u6570", cdkGoAuth: "\u524D\u5F80\u767B\u5F55", cdkScoreDesc: "\u57FA\u4E8E\u5FBD\u7AE0\u8BA1\u7B97\u7684\u793E\u533A\u4FE1\u8A89\u5206", cdkMyReceived: "\u6211\u7684\u9886\u53D6", cdkReceivedEmpty: "\u6682\u65E0\u9886\u53D6\u8BB0\u5F55", cdkProjectName: "\u9879\u76EE", cdkCreator: "\u53D1\u5E03\u8005", cdkContent: "\u5185\u5BB9", cdkReceivedAt: "\u9886\u53D6\u65F6\u95F4", cdkCopy: "\u590D\u5236", cdkCopied: "\u5DF2\u590D\u5236", cdkTotal: "\u5171 {count} \u6761", cdkLoadingReceived: "\u52A0\u8F7D\u9886\u53D6\u8BB0\u5F55...", loadingCdk: "\u52A0\u8F7D CDK \u6570\u636E...", clickToLoadCdk: "\u70B9\u51FB\u5C55\u5F00\u52A0\u8F7D CDK \u5206\u6570...", cdkRecentLimit: "\u6700\u8FD120\u6761", topicCreatedTimeLabel: "\u23F0 \u521B\u5EFA\u65F6\u95F4\u6807\u7B7E", topicCreatedTimeVisible: "\u{1F441}\uFE0F \u663E\u793A\u521B\u5EFA\u65F6\u95F4", topicCreatedTimeVisibleTip: "\u5F00\u542F\u663E\u793A/\u5173\u95ED\u9690\u85CF\u5E16\u5B50\u521B\u5EFA\u65F6\u95F4\u6807\u7B7E", topicCreatedTimeEnabled: "\u521B\u5EFA\u65F6\u95F4\u6807\u7B7E\u5DF2\u663E\u793A", topicCreatedTimeDisabled: "\u521B\u5EFA\u65F6\u95F4\u6807\u7B7E\u5DF2\u9690\u85CF", topicAgeColorLabel: "\u{1F3AF} \u5E16\u9F84\u9AD8\u4EAE", topicAgeColorTip: "\u63A7\u5236\u521B\u5EFA\u65F6\u95F4\u6807\u7B7E\u662F\u5426\u6309\u5E16\u9F84\u7740\u8272", topicAgeColorEnabled: "\u5E16\u9F84\u9AD8\u4EAE\u5DF2\u542F\u7528", topicAgeColorDisabled: "\u5E16\u9F84\u9AD8\u4EAE\u5DF2\u7981\u7528", cfBypassLabel: "\u{1F6E1}\uFE0F CF 5\u79D2\u76FE", cfBypassTip: "\u68C0\u6D4B\u5230 Cloudflare \u9A8C\u8BC1\u5931\u8D25\u65F6\u81EA\u52A8\u6253\u5F00\u9A8C\u8BC1\u9875", cfBypassEnabled: "CF 5\u79D2\u76FE\u81EA\u52A8\u8DF3\u8F6C\u5DF2\u542F\u7528", cfBypassDisabled: "CF 5\u79D2\u76FE\u81EA\u52A8\u8DF3\u8F6C\u5DF2\u7981\u7528", cfBypassDetected: "\u{1F6E1}\uFE0F \u68C0\u6D4B\u5230 CF \u9A8C\u8BC1\u5931\u8D25\uFF0C\u6B63\u5728\u8DF3\u8F6C...", cfBypassManual: "\u624B\u52A8\u89E6\u53D1CF\u9A8C\u8BC1", cfBypassManualTip: "\u624B\u52A8\u8DF3\u8F6C\u5230 CloudFlare challenge \u9875\u9762", cfBypassRedirecting: "\u6B63\u5728\u8DF3\u8F6C\u5230 Cloudflare Challenge \u9875\u9762...", cfBypassAlreadyOnChallenge: "\u5DF2\u5728 Challenge \u9875\u9762\uFF0C\u65E0\u9700\u8DF3\u8F6C", cfBypassNotFoundRedirecting: "Challenge \u9875\u9762\u65E0\u6548\uFF0C\u6B63\u5728\u8FD4\u56DE\u539F\u9875\u9762..." }; this.t = (key) => this.texts[key] || key; this.accumulatedTime = this.getSessionStorage("accumulatedTime", 0); this.lastActionTime = Date.now(); this.isTopicPage = window.location.href.includes("/t/topic/"); const onChallengePage = CURRENT_DOMAIN === "linux.do" && window.location.pathname.startsWith("/challenge"); const isNewWindow = window.opener !== null; if (isNewWindow) { this.autoRunning = false; this.setSessionStorage("autoRunning", false); this.topicList = []; this.setSessionStorage("topicList", []); console.log("[\u7A97\u53E3\u72EC\u7ACB] \u68C0\u6D4B\u5230\u65B0\u5F00\u7A97\u53E3\uFF0C\u5DF2\u6E05\u9664\u7EE7\u627F\u7684\u81EA\u52A8\u9605\u8BFB\u72B6\u6001"); } else { this.autoRunning = onChallengePage ? false : this.getSessionStorage("autoRunning", false); this.topicList = this.getSessionStorage("topicList", []); } this.firstUseChecked = Storage.get("firstUseChecked", false); this.selectedPost = Storage.get("selectedPost", null); this.smartLikeEnabled = Storage.get("smartLikeEnabled", false) === true; this.smartLikeSpeed = Storage.get("smartLikeSpeed", "normal"); if (!["slow", "normal", "fast"].includes(this.smartLikeSpeed)) this.smartLikeSpeed = "normal"; this.cleanModeEnabled = Storage.get("cleanModeEnabled", false); this.grayscaleModeEnabled = Storage.get("grayscaleModeEnabled", false); this.readUnreadEnabled = Storage.get("readUnreadEnabled", false); this.randomOrderEnabled = Storage.get("randomOrderEnabled", false); this.skipReadEnabled = Storage.get("skipReadEnabled", true); this.fullTopicReadEnabled = Storage.get("fullTopicReadEnabled", false); this.topicLimitCount = Storage.get("topicLimitCount", 100); this.restTimeMinutes = Storage.get("restTimeMinutes", 10); this.topicCreatedTimeVisible = Storage.get("topicCreatedTimeVisible", true); this.topicAgeColorEnabled = Storage.get("topicAgeColorEnabled", true); this.stopAfterReadEnabled = Storage.get("stopAfterReadEnabled", false); this.stopAfterReadCount = Storage.get("stopAfterReadCount", 10); this.currentSessionReadCount = this.getSessionStorage("currentSessionReadCount", 0); this.stopOnLikeLimitEnabled = Storage.get("stopOnLikeLimitEnabled", false); this.cfBypassEnabled = Storage.get("cfBypassEnabled", true); this.likeResumeTime = LikeCooldown.read(null); this.ipRateLimitResumeTime = Storage.get("ipRateLimitResumeTime", null); this.ipRateLimitCheckInterval = null; this.currentUsername = null; this.currentAccount = null; this.lastDetectedUser = null; this.smartLikeSessionId = this.getSessionStorage("smartLikeSessionId", null) || (globalThis.crypto?.randomUUID ? globalThis.crypto.randomUUID() : Date.now() + "-" + Math.random().toString(36).slice(2)); this.smartLikeGeneration = 0; this.smartLikeCheckedPostIds = {}; this.smartLikeFailedPostIds = /* @__PURE__ */ new Set(); this.smartLikedPostIds = /* @__PURE__ */ new Map(); this.smartLikeStorageReady = true; this.smartLikeActiveTask = null; this.smartLikeRunToken = 0; this.smartLikeIsScanning = false; this.smartLikeLastScanAt = 0; const savedSmartLikeLastLikeAt = Number(this.getSessionStorage("smartLikeLastLikeAt", 0)); this.smartLikeLastLikeAt = Number.isFinite(savedSmartLikeLastLikeAt) && savedSmartLikeLastLikeAt > 0 ? savedSmartLikeLastLikeAt : 0; this.smartLikeAccountKey = null; this.smartLikeReady = this.initializeSmartLikeTab(); this.readTopics = []; this.skippedReadCount = this.getSessionStorage("skippedReadCount", 0); this.todayReadCount = this.loadTodayReadCount(); this.totalReadCount = Storage.get("totalReadCount", 0); this.lastFetchedPage = this.getSessionStorage("lastFetchedPage", 0); this.historicalMaxPage = this.loadHistoricalMaxPage(); this._topicCreatedTimeObserver = null; this.topicStatusMode = null; this.topicStatusHideTimer = null; this.topicStatusNotificationTimer = null; this.pendingNotifications = []; this.cfBypassObserver = null; this.cfBypassRedirectTimer = null; this.cfBypassRedirecting = false; this.checkLikeResumeTime(); this.observeLikeLimit(); this.checkIpRateLimitStatus(); this.detectIpRateLimit(); this.setupButton(); this.bindSmartLikeScroll(); this.smartLikeReady.then(() => this.resolveSmartLikeAccount()); this.initDataLoading(); this.startUserSwitchMonitoring(); this.initFloorNumberDisplay(); this.applyCleanModeStyles(); this.applyGrayscaleModeStyles(); this.initTopicCreatedTimeEnhancer(); if (!this.firstUseChecked && !onChallengePage) { this.handleFirstUse(); } else if (!onChallengePage && this.autoRunning) { this.loadUserReadHistory().then(() => { if (this.topicList.length > 0) { setTimeout(() => this.updateReadingStatus(), 100); } if (this.isTopicPage) { this.startScrolling(); } else { this.getLatestTopics().then(() => { if (!this.autoRunning) return; this.navigateNextTopic(); }); } }); } else if (!onChallengePage) { this.loadUserReadHistory(); } if (!onChallengePage) { this.startNavigationGuard(); } this.userInfoHelper = new UserInfoHelper(); if (CURRENT_DOMAIN === "linux.do" || CURRENT_DOMAIN === "idcflare.com") { this.initLikeCounter(); } this.startTrustLevelMonitor(); this.applyTopicAgeHighlightColors(); if (CURRENT_DOMAIN === "linux.do") { this.initCloudFlareBypass(); } } initLikeCounter() { if (!globalLikeCounter) { globalLikeCounter = new LikeCounter(); } this.likeCounter = globalLikeCounter; this.likeCounter.onUIUpdate((status) => { this.updateLikeCounterUI(status); }); setTimeout(() => { this.updateLikeCounterUI(this.likeCounter.getStatus()); }, 500); } // Backward-compatible alias retained for external userscript callers. stopNavigation() { return this.stopAutoReading(); } }; Object.assign( BrowseController.prototype, methods, methods2, methods3, methods4, methods5, methods6, methods7, methods8, methods9, methods10, methods11, methods12, methods13 ); // src/entry.js var APP_KEY = Symbol.for("linux-do-assistant.app"); if (isCDKPage) { initCDKBridgePage(); } else { if (window[APP_KEY]?.controller) { window.browseController = window[APP_KEY].controller; } else { networkInterceptor.install(); const controller = new BrowseController(); window.browseController = controller; window[APP_KEY] = { controller, version: "1.0.0" }; console.log(`[Linux.do \u52A9\u624B] \u6A21\u5757\u5316\u542F\u52A8\u5B8C\u6210 (${CURRENT_DOMAIN})`); } } })();