您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
tanhuazu.com 探花族论坛助手
// ==UserScript== // @name tanhuazu-helper // @namespace tanhuazu-helper.xyjtyskfydhqss.none // @version 0.1.4 // @author xyjtyskfydhqss // @description tanhuazu.com 探花族论坛助手 // @license MIT // @icon https://www.tanhuazu.com/favicon.ico // @include https://www.tanhuazu.com/* // @include https://tanhuazu.com/* // @require https://unpkg.com/[email protected]/umd/react.production.min.js // @require https://unpkg.com/[email protected]/umd/react-dom.production.min.js // @connect self // @connect obdown.com // @grant GM.xmlHttpRequest // @grant GM_notification // @grant GM_openInTab // ==/UserScript== (e=>{const t=document.createElement("style");t.dataset.source="vite-plugin-monkey",t.innerText=e,document.head.appendChild(t)})(" ._preview-img-wrapper_1v8wn_1{z-index:500}._preview-img-wrapper_1v8wn_1 img{max-height:100%;max-width:100%}.tanhuazu-download-btn{position:relative;display:inline-block;font-weight:400;white-space:nowrap;text-align:center;background-image:none;background-color:transparent;border:1px solid transparent;cursor:pointer;transition:all .2s cubic-bezier(.645,.045,.355,1);user-select:none;touch-action:manipulation;line-height:1.57142857;font-size:16px;height:40px;border-radius:8px;color:#fff;background-color:#1677ff;outline:none;position:absolute;left:calc(100% + 10px);width:100px;top:0;height:auto;width:auto;font-size:30px;padding:5px;display:flex;align-items:center;justify-content:center;text-decoration:none}.tanhuazu-download-btn:hover,.tanhuazu-download-btn:visited{text-decoration:none}.tanhuazu .block-body .message:first-child .message-attribution{font-size:30px}.tanhuazu .structItem.structItem--thread.last-clicked,.tanhuazu .block-row.last-clicked{background-color:#ff8c00;color:#fff}.tanhuazu .structItem.structItem--thread.last-clicked a,.tanhuazu .block-row.last-clicked a{color:#fff} "); (function(require$$0, require$$0$1) { "use strict"; function getDefaultExportFromCjs(x) { return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x; } var delayExports = {}; var delay$2 = { get exports() { return delayExports; }, set exports(v) { delayExports = v; } }; const randomInteger = (minimum, maximum) => Math.floor(Math.random() * (maximum - minimum + 1) + minimum); const createAbortError = () => { const error = new Error("Delay aborted"); error.name = "AbortError"; return error; }; const createDelay = ({ clearTimeout: defaultClear, setTimeout: set, willResolve }) => (ms, { value, signal } = {}) => { if (signal && signal.aborted) { return Promise.reject(createAbortError()); } let timeoutId; let settle; let rejectFn; const clear = defaultClear || clearTimeout; const signalListener = () => { clear(timeoutId); rejectFn(createAbortError()); }; const cleanup = () => { if (signal) { signal.removeEventListener("abort", signalListener); } }; const delayPromise = new Promise((resolve, reject) => { settle = () => { cleanup(); if (willResolve) { resolve(value); } else { reject(value); } }; rejectFn = reject; timeoutId = (set || setTimeout)(settle, ms); }); if (signal) { signal.addEventListener("abort", signalListener, { once: true }); } delayPromise.clear = () => { clear(timeoutId); timeoutId = null; settle(); }; return delayPromise; }; const createWithTimers = (clearAndSet) => { const delay2 = createDelay({ ...clearAndSet, willResolve: true }); delay2.reject = createDelay({ ...clearAndSet, willResolve: false }); delay2.range = (minimum, maximum, options) => delay2(randomInteger(minimum, maximum), options); return delay2; }; const delay$1 = createWithTimers(); delay$1.createWithTimers = createWithTimers; delay$2.exports = delay$1; delayExports.default = delay$1; function idle() { return new Promise((resolve) => { requestIdleCallback(() => resolve(void 0)); }); } const APP_NAME = "tanhuazu-helper"; function logWithLabel(...args) { const [msg, ...rest] = args; if (typeof msg === "string") { console.log(`[${APP_NAME}]: ${msg}`, ...rest); } else { console.log(`[${APP_NAME}]: `, msg, ...rest); } } async function handleReplyWait() { while (true) { await waitOverlayAndProcess(); await delayExports(1e3); } } async function waitOverlayAndProcess() { let overlay; let msgEl; let title = ""; let msg = ""; const hasWarningOverlay = async () => { var _a, _b, _c; await idle(); overlay = document.querySelector( ".overlay-container.is-active .overlay" ); if (!overlay) return; title = ((_b = (_a = overlay.querySelector(".overlay-title")) == null ? void 0 : _a.textContent) == null ? void 0 : _b.trim()) ?? ""; msgEl = overlay == null ? void 0 : overlay.querySelector( ".overlay-content .blockMessage" ); msg = ((_c = msgEl == null ? void 0 : msgEl.textContent) == null ? void 0 : _c.trim()) ?? ""; if (title === "哎呀!我们遇到了一些问题。" && msg && msg.includes("您必须等待") && msg.includes("后才可以继续执行此操作")) { return true; } }; while (!await hasWarningOverlay()) { await delayExports(500); } let seconds = Number( /您必须等待 (\d+) 秒后才可以继续执行此操作。/.exec(msg)[1] ); if (!seconds || isNaN(seconds)) return; while (seconds > 0) { if (!document.querySelector(".overlay-container.is-active .overlay")) { return; } await delayExports(1e3); seconds--; const rest = seconds >= 60 ? `${Math.floor(seconds / 60)} 分 ${seconds % 60} 秒` : `${seconds} 秒`; msgEl.textContent = `您必须等待 ${rest} 后才可以继续执行此操作。`; } await delayExports(1e3); GM_notification({ title: "tanhuazu.com 可以继续操作了", text: document.title, onclick() { GM_openInTab(location.href, { active: true, insert: true }); } }); } function parseRawHeaders(h2) { const s2 = h2.trim(); if (!s2) { return new Headers(); } const array = s2.split("\r\n").map((value) => { let s3 = value.split(":"); return [s3[0].trim(), s3[1].trim()]; }); return new Headers(array); } function parseGMResponse(res) { const r2 = new Response(res.response, { statusText: res.statusText, status: res.status, headers: parseRawHeaders(res.responseHeaders) }); Object.defineProperty(r2, "url", { value: res.finalUrl }); return r2; } async function GM_fetch(input, init) { const request = new Request(input, init); let data; if (init == null ? void 0 : init.body) { data = await request.text(); } return await XHR(request, init, data); } function XHR(request, init, data) { return new Promise((resolve, reject) => { if (request.signal && request.signal.aborted) { return reject(new DOMException("Aborted", "AbortError")); } GM.xmlHttpRequest({ url: request.url, method: gmXHRMethod(request.method.toUpperCase()), headers: Object.fromEntries(new Headers(init == null ? void 0 : init.headers).entries()), data, responseType: "blob", onload(res) { resolve(parseGMResponse(res)); }, onabort() { reject(new DOMException("Aborted", "AbortError")); }, ontimeout() { reject(new TypeError("Network request failed, timeout")); }, onerror(err) { reject(new TypeError("Failed to fetch: " + err.finalUrl)); } }); }); } const httpMethods = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "TRACE", "OPTIONS", "CONNECT"]; function includes(array, element) { return array.includes(element); } function gmXHRMethod(method) { if (includes(httpMethods, method)) { return method; } throw new Error(`unsupported http method ${method}`); } class HTTPError extends Error { constructor(response, request, options) { const code = response.status || response.status === 0 ? response.status : ""; const title = response.statusText || ""; const status = `${code} ${title}`.trim(); const reason = status ? `status code ${status}` : "an unknown error"; super(`Request failed with ${reason}`); Object.defineProperty(this, "response", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "request", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "options", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.name = "HTTPError"; this.response = response; this.request = request; this.options = options; } } class TimeoutError extends Error { constructor(request) { super("Request timed out"); Object.defineProperty(this, "request", { enumerable: true, configurable: true, writable: true, value: void 0 }); this.name = "TimeoutError"; this.request = request; } } const isObject$1 = (value) => value !== null && typeof value === "object"; const validateAndMerge = (...sources) => { for (const source of sources) { if ((!isObject$1(source) || Array.isArray(source)) && typeof source !== "undefined") { throw new TypeError("The `options` argument must be an object"); } } return deepMerge({}, ...sources); }; const mergeHeaders = (source1 = {}, source2 = {}) => { const result = new globalThis.Headers(source1); const isHeadersInstance = source2 instanceof globalThis.Headers; const source = new globalThis.Headers(source2); for (const [key, value] of source.entries()) { if (isHeadersInstance && value === "undefined" || value === void 0) { result.delete(key); } else { result.set(key, value); } } return result; }; const deepMerge = (...sources) => { let returnValue = {}; let headers = {}; for (const source of sources) { if (Array.isArray(source)) { if (!Array.isArray(returnValue)) { returnValue = []; } returnValue = [...returnValue, ...source]; } else if (isObject$1(source)) { for (let [key, value] of Object.entries(source)) { if (isObject$1(value) && key in returnValue) { value = deepMerge(returnValue[key], value); } returnValue = { ...returnValue, [key]: value }; } if (isObject$1(source.headers)) { headers = mergeHeaders(headers, source.headers); returnValue.headers = headers; } } } return returnValue; }; const supportsRequestStreams = (() => { let duplexAccessed = false; let hasContentType = false; const supportsReadableStream = typeof globalThis.ReadableStream === "function"; if (supportsReadableStream) { hasContentType = new globalThis.Request("https://a.com", { body: new globalThis.ReadableStream(), method: "POST", // @ts-expect-error - Types are outdated. get duplex() { duplexAccessed = true; return "half"; } }).headers.has("Content-Type"); } return duplexAccessed && !hasContentType; })(); const supportsAbortController = typeof globalThis.AbortController === "function"; const supportsResponseStreams = typeof globalThis.ReadableStream === "function"; const supportsFormData = typeof globalThis.FormData === "function"; const requestMethods = ["get", "post", "put", "patch", "head", "delete"]; const responseTypes = { json: "application/json", text: "text/*", formData: "multipart/form-data", arrayBuffer: "*/*", blob: "*/*" }; const maxSafeTimeout = 2147483647; const stop = Symbol("stop"); const normalizeRequestMethod = (input) => requestMethods.includes(input) ? input.toUpperCase() : input; const retryMethods = ["get", "put", "head", "delete", "options", "trace"]; const retryStatusCodes = [408, 413, 429, 500, 502, 503, 504]; const retryAfterStatusCodes = [413, 429, 503]; const defaultRetryOptions = { limit: 2, methods: retryMethods, statusCodes: retryStatusCodes, afterStatusCodes: retryAfterStatusCodes, maxRetryAfter: Number.POSITIVE_INFINITY, backoffLimit: Number.POSITIVE_INFINITY }; const normalizeRetryOptions = (retry = {}) => { if (typeof retry === "number") { return { ...defaultRetryOptions, limit: retry }; } if (retry.methods && !Array.isArray(retry.methods)) { throw new Error("retry.methods must be an array"); } if (retry.statusCodes && !Array.isArray(retry.statusCodes)) { throw new Error("retry.statusCodes must be an array"); } return { ...defaultRetryOptions, ...retry, afterStatusCodes: retryAfterStatusCodes }; }; async function timeout(request, abortController, options) { return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { if (abortController) { abortController.abort(); } reject(new TimeoutError(request)); }, options.timeout); void options.fetch(request).then(resolve).catch(reject).then(() => { clearTimeout(timeoutId); }); }); } const isDomExceptionSupported = Boolean(globalThis.DOMException); function composeAbortError(signal) { if (isDomExceptionSupported) { return new DOMException((signal == null ? void 0 : signal.reason) ?? "The operation was aborted.", "AbortError"); } const error = new Error((signal == null ? void 0 : signal.reason) ?? "The operation was aborted."); error.name = "AbortError"; return error; } async function delay(ms, { signal }) { return new Promise((resolve, reject) => { if (signal) { if (signal.aborted) { reject(composeAbortError(signal)); return; } signal.addEventListener("abort", handleAbort, { once: true }); } function handleAbort() { reject(composeAbortError(signal)); clearTimeout(timeoutId); } const timeoutId = setTimeout(() => { signal == null ? void 0 : signal.removeEventListener("abort", handleAbort); resolve(); }, ms); }); } class Ky { // eslint-disable-next-line @typescript-eslint/promise-function-async static create(input, options) { const ky2 = new Ky(input, options); const fn = async () => { if (ky2._options.timeout > maxSafeTimeout) { throw new RangeError(`The \`timeout\` option cannot be greater than ${maxSafeTimeout}`); } await Promise.resolve(); let response = await ky2._fetch(); for (const hook of ky2._options.hooks.afterResponse) { const modifiedResponse = await hook(ky2.request, ky2._options, ky2._decorateResponse(response.clone())); if (modifiedResponse instanceof globalThis.Response) { response = modifiedResponse; } } ky2._decorateResponse(response); if (!response.ok && ky2._options.throwHttpErrors) { let error = new HTTPError(response, ky2.request, ky2._options); for (const hook of ky2._options.hooks.beforeError) { error = await hook(error); } throw error; } if (ky2._options.onDownloadProgress) { if (typeof ky2._options.onDownloadProgress !== "function") { throw new TypeError("The `onDownloadProgress` option must be a function"); } if (!supportsResponseStreams) { throw new Error("Streams are not supported in your environment. `ReadableStream` is missing."); } return ky2._stream(response.clone(), ky2._options.onDownloadProgress); } return response; }; const isRetriableMethod = ky2._options.retry.methods.includes(ky2.request.method.toLowerCase()); const result = isRetriableMethod ? ky2._retry(fn) : fn(); for (const [type, mimeType] of Object.entries(responseTypes)) { result[type] = async () => { ky2.request.headers.set("accept", ky2.request.headers.get("accept") || mimeType); const awaitedResult = await result; const response = awaitedResult.clone(); if (type === "json") { if (response.status === 204) { return ""; } const arrayBuffer = await response.clone().arrayBuffer(); const responseSize = arrayBuffer.byteLength; if (responseSize === 0) { return ""; } if (options.parseJson) { return options.parseJson(await response.text()); } } return response[type](); }; } return result; } // eslint-disable-next-line complexity constructor(input, options = {}) { Object.defineProperty(this, "request", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "abortController", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_retryCount", { enumerable: true, configurable: true, writable: true, value: 0 }); Object.defineProperty(this, "_input", { enumerable: true, configurable: true, writable: true, value: void 0 }); Object.defineProperty(this, "_options", { enumerable: true, configurable: true, writable: true, value: void 0 }); this._input = input; this._options = { // TODO: credentials can be removed when the spec change is implemented in all browsers. Context: https://www.chromestatus.com/feature/4539473312350208 credentials: this._input.credentials || "same-origin", ...options, headers: mergeHeaders(this._input.headers, options.headers), hooks: deepMerge({ beforeRequest: [], beforeRetry: [], beforeError: [], afterResponse: [] }, options.hooks), method: normalizeRequestMethod(options.method ?? this._input.method), // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing prefixUrl: String(options.prefixUrl || ""), retry: normalizeRetryOptions(options.retry), throwHttpErrors: options.throwHttpErrors !== false, timeout: typeof options.timeout === "undefined" ? 1e4 : options.timeout, fetch: options.fetch ?? globalThis.fetch.bind(globalThis) }; if (typeof this._input !== "string" && !(this._input instanceof URL || this._input instanceof globalThis.Request)) { throw new TypeError("`input` must be a string, URL, or Request"); } if (this._options.prefixUrl && typeof this._input === "string") { if (this._input.startsWith("/")) { throw new Error("`input` must not begin with a slash when using `prefixUrl`"); } if (!this._options.prefixUrl.endsWith("/")) { this._options.prefixUrl += "/"; } this._input = this._options.prefixUrl + this._input; } if (supportsAbortController) { this.abortController = new globalThis.AbortController(); if (this._options.signal) { const originalSignal = this._options.signal; this._options.signal.addEventListener("abort", () => { this.abortController.abort(originalSignal.reason); }); } this._options.signal = this.abortController.signal; } if (supportsRequestStreams) { this._options.duplex = "half"; } this.request = new globalThis.Request(this._input, this._options); if (this._options.searchParams) { const textSearchParams = typeof this._options.searchParams === "string" ? this._options.searchParams.replace(/^\?/, "") : new URLSearchParams(this._options.searchParams).toString(); const searchParams = "?" + textSearchParams; const url = this.request.url.replace(/(?:\?.*?)?(?=#|$)/, searchParams); if ((supportsFormData && this._options.body instanceof globalThis.FormData || this._options.body instanceof URLSearchParams) && !(this._options.headers && this._options.headers["content-type"])) { this.request.headers.delete("content-type"); } this.request = new globalThis.Request(new globalThis.Request(url, { ...this.request }), this._options); } if (this._options.json !== void 0) { this._options.body = JSON.stringify(this._options.json); this.request.headers.set("content-type", this._options.headers.get("content-type") ?? "application/json"); this.request = new globalThis.Request(this.request, { body: this._options.body }); } } _calculateRetryDelay(error) { this._retryCount++; if (this._retryCount < this._options.retry.limit && !(error instanceof TimeoutError)) { if (error instanceof HTTPError) { if (!this._options.retry.statusCodes.includes(error.response.status)) { return 0; } const retryAfter = error.response.headers.get("Retry-After"); if (retryAfter && this._options.retry.afterStatusCodes.includes(error.response.status)) { let after = Number(retryAfter); if (Number.isNaN(after)) { after = Date.parse(retryAfter) - Date.now(); } else { after *= 1e3; } if (typeof this._options.retry.maxRetryAfter !== "undefined" && after > this._options.retry.maxRetryAfter) { return 0; } return after; } if (error.response.status === 413) { return 0; } } const BACKOFF_FACTOR = 0.3; return Math.min(this._options.retry.backoffLimit, BACKOFF_FACTOR * 2 ** (this._retryCount - 1) * 1e3); } return 0; } _decorateResponse(response) { if (this._options.parseJson) { response.json = async () => this._options.parseJson(await response.text()); } return response; } async _retry(fn) { try { return await fn(); } catch (error) { const ms = Math.min(this._calculateRetryDelay(error), maxSafeTimeout); if (ms !== 0 && this._retryCount > 0) { await delay(ms, { signal: this._options.signal }); for (const hook of this._options.hooks.beforeRetry) { const hookResult = await hook({ request: this.request, options: this._options, error, retryCount: this._retryCount }); if (hookResult === stop) { return; } } return this._retry(fn); } throw error; } } async _fetch() { for (const hook of this._options.hooks.beforeRequest) { const result = await hook(this.request, this._options); if (result instanceof Request) { this.request = result; break; } if (result instanceof Response) { return result; } } if (this._options.timeout === false) { return this._options.fetch(this.request.clone()); } return timeout(this.request.clone(), this.abortController, this._options); } /* istanbul ignore next */ _stream(response, onDownloadProgress) { const totalBytes = Number(response.headers.get("content-length")) || 0; let transferredBytes = 0; if (response.status === 204) { if (onDownloadProgress) { onDownloadProgress({ percent: 1, totalBytes, transferredBytes }, new Uint8Array()); } return new globalThis.Response(null, { status: response.status, statusText: response.statusText, headers: response.headers }); } return new globalThis.Response(new globalThis.ReadableStream({ async start(controller) { const reader = response.body.getReader(); if (onDownloadProgress) { onDownloadProgress({ percent: 0, transferredBytes: 0, totalBytes }, new Uint8Array()); } async function read() { const { done, value } = await reader.read(); if (done) { controller.close(); return; } if (onDownloadProgress) { transferredBytes += value.byteLength; const percent = totalBytes === 0 ? 0 : transferredBytes / totalBytes; onDownloadProgress({ percent, transferredBytes, totalBytes }, value); } controller.enqueue(value); await read(); } await read(); } }), { status: response.status, statusText: response.statusText, headers: response.headers }); } } /*! MIT License © Sindre Sorhus */ const createInstance = (defaults) => { const ky2 = (input, options) => Ky.create(input, validateAndMerge(defaults, options)); for (const method of requestMethods) { ky2[method] = (input, options) => Ky.create(input, validateAndMerge(defaults, options, { method })); } ky2.create = (newDefaults) => createInstance(validateAndMerge(newDefaults)); ky2.extend = (newDefaults) => createInstance(validateAndMerge(defaults, newDefaults)); ky2.stop = stop; return ky2; }; const ky = createInstance(); const ky$1 = ky; const kyfetch = ky$1.extend({ fetch: GM_fetch }); async function fetchMagnetLink() { var _a, _b, _c; const query = () => document.querySelector( `.block-body article.message .message-body a[href*="obdown.com"][href*=".torrent"]` ); const timeout2 = performance.now() + 5e3; while (!query() && performance.now() < timeout2) { await delayExports(500); } const a2 = query(); if (!a2) return; const torrentPageUrl = a2.getAttribute("href"); if (!torrentPageUrl) return; console.log("[tanhuazu-helper]: torrent download page %s", torrentPageUrl); const html = await kyfetch.get(torrentPageUrl, { retry: 5 }).text(); const p2 = new DOMParser(); const doc = p2.parseFromString(html, "text/html"); const magnetSpan = Array.from( doc.querySelectorAll("span.text-secondary") ).filter((span) => { var _a2; return ((_a2 = span.textContent) == null ? void 0 : _a2.trim()) === "MAGENT"; })[0]; const magnet = (_b = (_a = magnetSpan == null ? void 0 : magnetSpan.nextElementSibling) == null ? void 0 : _a.querySelector(`a[href^="magnet:?xt="]`)) == null ? void 0 : _b.getAttribute("href"); console.log("[tanhuazu-helper]: magnet link %s", magnet); if (!magnet) return; const firstMessage = document.querySelector( ".block-body article.message" ); firstMessage.style.position = "relative"; const btnShare = (_c = firstMessage.querySelector('a[aria-label="分享"]')) == null ? void 0 : _c.parentElement; const btnDl = document.createElement("li"); btnDl.innerHTML = ` <a href="${magnet}" class="message-attribution-gadget" title="磁力链接"> <svg viewBox="64 64 896 896" focusable="false" data-icon="download" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"></path></svg> </a> `; btnShare == null ? void 0 : btnShare.insertAdjacentElement("afterend", btnDl); btnShare == null ? void 0 : btnShare.remove(); const createBtn = () => { const btn = document.createElement("a"); btn.href = magnet; btn.innerHTML = ` <svg viewBox="64 64 896 896" focusable="false" data-icon="download" width="1em" height="1em" fill="currentColor" aria-hidden="true"><path d="M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"></path></svg> <span>下载</span> `; btn.className = "tanhuazu-download-btn"; return btn; }; const topBtn = createBtn(); firstMessage.appendChild(topBtn); const bottomBtn = createBtn(); bottomBtn.style.top = "unset"; bottomBtn.style.bottom = "0"; firstMessage.appendChild(bottomBtn); } var jsxRuntimeExports = {}; var jsxRuntime = { get exports() { return jsxRuntimeExports; }, set exports(v) { jsxRuntimeExports = v; } }; var reactJsxRuntime_production_min = {}; /** * @license React * react-jsx-runtime.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var f$1 = require$$0, k$1 = Symbol.for("react.element"), l$2 = Symbol.for("react.fragment"), m$2 = Object.prototype.hasOwnProperty, n$2 = f$1.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner, p$2 = { key: true, ref: true, __self: true, __source: true }; function q$1(c2, a2, g) { var b, d = {}, e2 = null, h2 = null; void 0 !== g && (e2 = "" + g); void 0 !== a2.key && (e2 = "" + a2.key); void 0 !== a2.ref && (h2 = a2.ref); for (b in a2) m$2.call(a2, b) && !p$2.hasOwnProperty(b) && (d[b] = a2[b]); if (c2 && c2.defaultProps) for (b in a2 = c2.defaultProps, a2) void 0 === d[b] && (d[b] = a2[b]); return { $$typeof: k$1, type: c2, key: e2, ref: h2, props: d, _owner: n$2.current }; } reactJsxRuntime_production_min.Fragment = l$2; reactJsxRuntime_production_min.jsx = q$1; reactJsxRuntime_production_min.jsxs = q$1; (function(module) { { module.exports = reactJsxRuntime_production_min; } })(jsxRuntime); const jsx = jsxRuntimeExports.jsx; var createRoot; var m$1 = require$$0$1; { createRoot = m$1.createRoot; m$1.hydrateRoot; } const e$1 = Symbol(), t$1 = Symbol(), r$1 = "a", n$1 = "w"; let o = (e2, t2) => new Proxy(e2, t2); const s = Object.getPrototypeOf, c = /* @__PURE__ */ new WeakMap(), l$1 = (e2) => e2 && (c.has(e2) ? c.get(e2) : s(e2) === Object.prototype || s(e2) === Array.prototype), f = (e2) => "object" == typeof e2 && null !== e2, i = (e2) => { if (Array.isArray(e2)) return Array.from(e2); const t2 = Object.getOwnPropertyDescriptors(e2); return Object.values(t2).forEach((e3) => { e3.configurable = true; }), Object.create(s(e2), t2); }, u$1 = (e2) => e2[t$1] || e2, a = (s2, c2, f2, p2) => { if (!l$1(s2)) return s2; let g = p2 && p2.get(s2); if (!g) { const e2 = u$1(s2); g = ((e3) => Object.values(Object.getOwnPropertyDescriptors(e3)).some((e4) => !e4.configurable && !e4.writable))(e2) ? [e2, i(e2)] : [e2], null == p2 || p2.set(s2, g); } const [y2, h2] = g; let w2 = f2 && f2.get(y2); return w2 && w2[1].f === !!h2 || (w2 = ((o2, s3) => { const c3 = { f: s3 }; let l2 = false; const f3 = (e2, t2) => { if (!l2) { let s4 = c3[r$1].get(o2); if (s4 || (s4 = {}, c3[r$1].set(o2, s4)), e2 === n$1) s4[n$1] = true; else { let r2 = s4[e2]; r2 || (r2 = /* @__PURE__ */ new Set(), s4[e2] = r2), r2.add(t2); } } }, i2 = { get: (e2, n2) => n2 === t$1 ? o2 : (f3("k", n2), a(Reflect.get(e2, n2), c3[r$1], c3.c)), has: (t2, n2) => n2 === e$1 ? (l2 = true, c3[r$1].delete(o2), true) : (f3("h", n2), Reflect.has(t2, n2)), getOwnPropertyDescriptor: (e2, t2) => (f3("o", t2), Reflect.getOwnPropertyDescriptor(e2, t2)), ownKeys: (e2) => (f3(n$1), Reflect.ownKeys(e2)) }; return s3 && (i2.set = i2.deleteProperty = () => false), [i2, c3]; })(y2, !!h2), w2[1].p = o(h2 || y2, w2[0]), f2 && f2.set(y2, w2)), w2[1][r$1] = c2, w2[1].c = f2, w2[1].p; }, p$1 = (e2, t2, r2, o2) => { if (Object.is(e2, t2)) return false; if (!f(e2) || !f(t2)) return true; const s2 = r2.get(u$1(e2)); if (!s2) return true; if (o2) { const r3 = o2.get(e2); if (r3 && r3.n === t2) return r3.g; o2.set(e2, { n: t2, g: false }); } let c2 = null; try { for (const r3 of s2.h || []) if (c2 = Reflect.has(e2, r3) !== Reflect.has(t2, r3), c2) return c2; if (true === s2[n$1]) { if (c2 = ((e3, t3) => { const r3 = Reflect.ownKeys(e3), n2 = Reflect.ownKeys(t3); return r3.length !== n2.length || r3.some((e4, t4) => e4 !== n2[t4]); })(e2, t2), c2) return c2; } else for (const r3 of s2.o || []) if (c2 = !!Reflect.getOwnPropertyDescriptor(e2, r3) != !!Reflect.getOwnPropertyDescriptor(t2, r3), c2) return c2; for (const n2 of s2.k || []) if (c2 = p$1(e2[n2], t2[n2], r2, o2), c2) return c2; return null === c2 && (c2 = true), c2; } finally { o2 && o2.set(e2, { n: t2, g: c2 }); } }, y = (e2) => l$1(e2) && e2[t$1] || null, h$1 = (e2, t2 = true) => { c.set(e2, t2); }, w = (e2, t2, r2) => { const o2 = [], s2 = /* @__PURE__ */ new WeakSet(), c2 = (e3, l2) => { if (s2.has(e3)) return; f(e3) && s2.add(e3); const i2 = f(e3) && t2.get(u$1(e3)); if (i2) { var a2, p2; if (null == (a2 = i2.h) || a2.forEach((e4) => { const t3 = `:has(${String(e4)})`; o2.push(l2 ? [...l2, t3] : [t3]); }), true === i2[n$1]) { const e4 = ":ownKeys"; o2.push(l2 ? [...l2, e4] : [e4]); } else { var g; null == (g = i2.o) || g.forEach((e4) => { const t3 = `:hasOwn(${String(e4)})`; o2.push(l2 ? [...l2, t3] : [t3]); }); } null == (p2 = i2.k) || p2.forEach((t3) => { r2 && !("value" in (Object.getOwnPropertyDescriptor(e3, t3) || {})) || c2(e3[t3], l2 ? [...l2, t3] : [t3]); }); } else l2 && o2.push(l2); }; return c2(e2), o2; }; const isObject = (x) => typeof x === "object" && x !== null; const proxyStateMap = /* @__PURE__ */ new WeakMap(); const refSet = /* @__PURE__ */ new WeakSet(); const buildProxyFunction = (objectIs = Object.is, newProxy = (target, handler) => new Proxy(target, handler), canProxy = (x) => isObject(x) && !refSet.has(x) && (Array.isArray(x) || !(Symbol.iterator in x)) && !(x instanceof WeakMap) && !(x instanceof WeakSet) && !(x instanceof Error) && !(x instanceof Number) && !(x instanceof Date) && !(x instanceof String) && !(x instanceof RegExp) && !(x instanceof ArrayBuffer), defaultHandlePromise = (promise) => { switch (promise.status) { case "fulfilled": return promise.value; case "rejected": throw promise.reason; default: throw promise; } }, snapCache = /* @__PURE__ */ new WeakMap(), createSnapshot = (target, version, handlePromise = defaultHandlePromise) => { const cache = snapCache.get(target); if ((cache == null ? void 0 : cache[0]) === version) { return cache[1]; } const snap = Array.isArray(target) ? [] : Object.create(Object.getPrototypeOf(target)); h$1(snap, true); snapCache.set(target, [version, snap]); Reflect.ownKeys(target).forEach((key) => { if (Object.getOwnPropertyDescriptor(snap, key)) { return; } const value = Reflect.get(target, key); const desc = { value, enumerable: true, // This is intentional to avoid copying with proxy-compare. // It's still non-writable, so it avoids assigning a value. configurable: true }; if (refSet.has(value)) { h$1(value, false); } else if (value instanceof Promise) { delete desc.value; desc.get = () => handlePromise(value); } else if (proxyStateMap.has(value)) { const [target2, ensureVersion] = proxyStateMap.get( value ); desc.value = createSnapshot( target2, ensureVersion(), handlePromise ); } Object.defineProperty(snap, key, desc); }); return snap; }, proxyCache = /* @__PURE__ */ new WeakMap(), versionHolder = [1, 1], proxyFunction = (initialObject) => { if (!isObject(initialObject)) { throw new Error("object required"); } const found = proxyCache.get(initialObject); if (found) { return found; } let version = versionHolder[0]; const listeners = /* @__PURE__ */ new Set(); const notifyUpdate = (op, nextVersion = ++versionHolder[0]) => { if (version !== nextVersion) { version = nextVersion; listeners.forEach((listener) => listener(op, nextVersion)); } }; let checkVersion = versionHolder[1]; const ensureVersion = (nextCheckVersion = ++versionHolder[1]) => { if (checkVersion !== nextCheckVersion && !listeners.size) { checkVersion = nextCheckVersion; propProxyStates.forEach(([propProxyState]) => { const propVersion = propProxyState[1](nextCheckVersion); if (propVersion > version) { version = propVersion; } }); } return version; }; const createPropListener = (prop) => (op, nextVersion) => { const newOp = [...op]; newOp[1] = [prop, ...newOp[1]]; notifyUpdate(newOp, nextVersion); }; const propProxyStates = /* @__PURE__ */ new Map(); const addPropListener = (prop, propProxyState) => { if (({ "BASE_URL": "/", "MODE": "production", "DEV": false, "PROD": true, "SSR": false } && "production") !== "production" && propProxyStates.has(prop)) { throw new Error("prop listener already exists"); } if (listeners.size) { const remove = propProxyState[3](createPropListener(prop)); propProxyStates.set(prop, [propProxyState, remove]); } else { propProxyStates.set(prop, [propProxyState]); } }; const removePropListener = (prop) => { var _a; const entry = propProxyStates.get(prop); if (entry) { propProxyStates.delete(prop); (_a = entry[1]) == null ? void 0 : _a.call(entry); } }; const addListener = (listener) => { listeners.add(listener); if (listeners.size === 1) { propProxyStates.forEach(([propProxyState, prevRemove], prop) => { if (({ "BASE_URL": "/", "MODE": "production", "DEV": false, "PROD": true, "SSR": false } && "production") !== "production" && prevRemove) { throw new Error("remove already exists"); } const remove = propProxyState[3](createPropListener(prop)); propProxyStates.set(prop, [propProxyState, remove]); }); } const removeListener = () => { listeners.delete(listener); if (listeners.size === 0) { propProxyStates.forEach(([propProxyState, remove], prop) => { if (remove) { remove(); propProxyStates.set(prop, [propProxyState]); } }); } }; return removeListener; }; const baseObject = Array.isArray(initialObject) ? [] : Object.create(Object.getPrototypeOf(initialObject)); const handler = { deleteProperty(target, prop) { const prevValue = Reflect.get(target, prop); removePropListener(prop); const deleted = Reflect.deleteProperty(target, prop); if (deleted) { notifyUpdate(["delete", [prop], prevValue]); } return deleted; }, set(target, prop, value, receiver) { const hasPrevValue = Reflect.has(target, prop); const prevValue = Reflect.get(target, prop, receiver); if (hasPrevValue && (objectIs(prevValue, value) || proxyCache.has(value) && objectIs(prevValue, proxyCache.get(value)))) { return true; } removePropListener(prop); if (isObject(value)) { value = y(value) || value; } let nextValue = value; if (value instanceof Promise) { value.then((v) => { value.status = "fulfilled"; value.value = v; notifyUpdate(["resolve", [prop], v]); }).catch((e2) => { value.status = "rejected"; value.reason = e2; notifyUpdate(["reject", [prop], e2]); }); } else { if (!proxyStateMap.has(value) && canProxy(value)) { nextValue = proxyFunction(value); } const childProxyState = !refSet.has(nextValue) && proxyStateMap.get(nextValue); if (childProxyState) { addPropListener(prop, childProxyState); } } Reflect.set(target, prop, nextValue, receiver); notifyUpdate(["set", [prop], value, prevValue]); return true; } }; const proxyObject = newProxy(baseObject, handler); proxyCache.set(initialObject, proxyObject); const proxyState = [ baseObject, ensureVersion, createSnapshot, addListener ]; proxyStateMap.set(proxyObject, proxyState); Reflect.ownKeys(initialObject).forEach((key) => { const desc = Object.getOwnPropertyDescriptor( initialObject, key ); if ("value" in desc) { proxyObject[key] = initialObject[key]; delete desc.value; delete desc.writable; } Object.defineProperty(baseObject, key, desc); }); return proxyObject; }) => [ // public functions proxyFunction, // shared state proxyStateMap, refSet, // internal things objectIs, newProxy, canProxy, defaultHandlePromise, snapCache, createSnapshot, proxyCache, versionHolder ]; const [defaultProxyFunction] = buildProxyFunction(); function proxy(initialObject = {}) { return defaultProxyFunction(initialObject); } function subscribe(proxyObject, callback, notifyInSync) { const proxyState = proxyStateMap.get(proxyObject); if (({ "BASE_URL": "/", "MODE": "production", "DEV": false, "PROD": true, "SSR": false } && "production") !== "production" && !proxyState) { console.warn("Please use proxy object"); } let promise; const ops = []; const addListener = proxyState[3]; let isListenerActive = false; const listener = (op) => { ops.push(op); if (notifyInSync) { callback(ops.splice(0)); return; } if (!promise) { promise = Promise.resolve().then(() => { promise = void 0; if (isListenerActive) { callback(ops.splice(0)); } }); } }; const removeListener = addListener(listener); isListenerActive = true; return () => { isListenerActive = false; removeListener(); }; } function snapshot(proxyObject, handlePromise) { const proxyState = proxyStateMap.get(proxyObject); if (({ "BASE_URL": "/", "MODE": "production", "DEV": false, "PROD": true, "SSR": false } && "production") !== "production" && !proxyState) { console.warn("Please use proxy object"); } const [target, ensureVersion, createSnapshot] = proxyState; return createSnapshot(target, ensureVersion(), handlePromise); } function ref(obj) { refSet.add(obj); return obj; } var shimExports = {}; var shim = { get exports() { return shimExports; }, set exports(v) { shimExports = v; } }; var useSyncExternalStoreShim_production_min = {}; /** * @license React * use-sync-external-store-shim.production.min.js * * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ var e = require$$0; function h(a2, b) { return a2 === b && (0 !== a2 || 1 / a2 === 1 / b) || a2 !== a2 && b !== b; } var k = "function" === typeof Object.is ? Object.is : h, l = e.useState, m = e.useEffect, n = e.useLayoutEffect, p = e.useDebugValue; function q(a2, b) { var d = b(), f2 = l({ inst: { value: d, getSnapshot: b } }), c2 = f2[0].inst, g = f2[1]; n(function() { c2.value = d; c2.getSnapshot = b; r(c2) && g({ inst: c2 }); }, [a2, d, b]); m(function() { r(c2) && g({ inst: c2 }); return a2(function() { r(c2) && g({ inst: c2 }); }); }, [a2]); p(d); return d; } function r(a2) { var b = a2.getSnapshot; a2 = a2.value; try { var d = b(); return !k(a2, d); } catch (f2) { return true; } } function t(a2, b) { return b(); } var u = "undefined" === typeof window || "undefined" === typeof window.document || "undefined" === typeof window.document.createElement ? t : q; useSyncExternalStoreShim_production_min.useSyncExternalStore = void 0 !== e.useSyncExternalStore ? e.useSyncExternalStore : u; (function(module) { { module.exports = useSyncExternalStoreShim_production_min; } })(shim); const useSyncExternalStoreExports = /* @__PURE__ */ getDefaultExportFromCjs(shimExports); const { use } = require$$0; const { useSyncExternalStore } = useSyncExternalStoreExports; const useAffectedDebugValue = (state2, affected) => { const pathList = require$$0.useRef(); require$$0.useEffect(() => { pathList.current = w(state2, affected, true); }); require$$0.useDebugValue(pathList.current); }; const targetCache = /* @__PURE__ */ new WeakMap(); function useSnapshot(proxyObject, options) { const notifyInSync = options == null ? void 0 : options.sync; const lastSnapshot = require$$0.useRef(); const lastAffected = require$$0.useRef(); let inRender = true; const currSnapshot = useSyncExternalStore( require$$0.useCallback( (callback) => { const unsub = subscribe(proxyObject, callback, notifyInSync); callback(); return unsub; }, [proxyObject, notifyInSync] ), () => { const nextSnapshot = snapshot(proxyObject, use); try { if (!inRender && lastSnapshot.current && lastAffected.current && !p$1( lastSnapshot.current, nextSnapshot, lastAffected.current, /* @__PURE__ */ new WeakMap() )) { return lastSnapshot.current; } } catch (e2) { } return nextSnapshot; }, () => snapshot(proxyObject, use) ); inRender = false; const currAffected = /* @__PURE__ */ new WeakMap(); require$$0.useEffect(() => { lastSnapshot.current = currSnapshot; lastAffected.current = currAffected; }); if (({ "BASE_URL": "/", "MODE": "production", "DEV": false, "PROD": true, "SSR": false } && "production") !== "production") { useAffectedDebugValue(currSnapshot, currAffected); } const proxyCache = require$$0.useMemo(() => /* @__PURE__ */ new WeakMap(), []); return a( currSnapshot, currAffected, proxyCache, targetCache ); } const previewImgWrapper = "_preview-img-wrapper_1v8wn_1"; const styles = { previewImgWrapper }; function showPreviewImgWhenHover(container) { if (!container) return; container.onmouseover = async (e2) => { const src = e2.target; if (src.tagName.toLowerCase() !== "a") return; const u2 = new URL(src.href, location.href); if (!u2.pathname.startsWith("/threads/")) return; const threadUrl = u2.pathname.split("/").slice(0, 3).join("/") + "/"; logWithLabel("hover: %s", threadUrl); state.a = ref(src); state.threadUrl = threadUrl; const imgs = await getPreviewImg(threadUrl); logWithLabel("fetched imgs", imgs); if (state.threadUrl === threadUrl) { state.imgs = imgs; } if (!root) { const div = document.createElement("div"); div.classList.add("preview-img-root"); document.body.appendChild(div); root = createRoot(div); root.render(/* @__PURE__ */ jsx(PreviewImg, {})); } }; container.onmouseout = function(e2) { state.a = void 0; state.threadUrl = ""; state.imgs = []; }; } async function getPreviewImg(threadUrl) { const html = await ky$1.get(threadUrl, { cache: "force-cache" }).text(); const parser = new DOMParser(); const doc = parser.parseFromString(html, "text/html"); const srcs = Array.from( doc.querySelectorAll( ".block-body .message:first-child .bbImageWrapper img" ) ).map((img) => img.src); return srcs; } const state = proxy({ threadUrl: "", a: void 0, imgs: [] }); function PreviewImg() { const { imgs, a: a2 } = useSnapshot(state); const x = require$$0.useMemo(() => { const rect = a2 == null ? void 0 : a2.getBoundingClientRect(); const x2 = ((rect == null ? void 0 : rect.right) || 0) + 50; return x2; }, [a2]); if (!imgs.length) return null; return /* @__PURE__ */ jsx( "div", { className: styles.previewImgWrapper, style: { position: "fixed", left: x, top: 10, width: `calc(100vw - ${x}px - 20px)`, height: "calc(100vh - 20px)", overflow: "hidden" }, children: imgs.slice(0, 1).map((src) => { return /* @__PURE__ */ jsx("img", { src, alt: "" }, src); }) } ); } let root; function postListMain() { const container = getThreadListContainer(); if (!container) return; container.onclick = (e2) => { markLastClicked(e2); fixPostLink(e2); }; showPreviewImgWhenHover(container); } function isSearchResultPage() { if (!/^\/search\/\d+\//.test(location.pathname)) return false; const u2 = new URL(location.href); if (u2.searchParams.get("searchform") === "1") return false; return true; } function getThreadListContainer() { let el; if (el = document.querySelector( [ '.block[data-type="thread"]', '.block[data-widget-key="whats_new_new_posts"]', // /what's new '.p-body-pageContent:has(> form[action^="/watched/threads"])' // /watched/threads ].join(",") )) { return el; } if (isSearchResultPage() && (el = document.querySelector(".p-body-pageContent"))) { return el; } if (el = document.querySelector(".p-body-pageContent")) { return el; } } function fixPostLink(e2) { const src = e2.target; if (src.tagName.toLowerCase() !== "a") return; const u2 = new URL(src.href, location.href); if (!u2.pathname.startsWith("/threads/")) return; if (u2.pathname.includes("unread")) { e2.preventDefault(); const newLink = src.href.replace(/unread/, ""); GM_openInTab(newLink); } } function markLastClicked(e2) { var _a; const lineSelector = ".structItem.structItem--thread, .block-row"; const cur = e2.target.closest(lineSelector); cur == null ? void 0 : cur.classList.add("last-clicked"); (_a = e2.currentTarget) == null ? void 0 : _a.querySelectorAll(lineSelector).forEach((item) => { item !== cur && item.classList.remove("last-clicked"); }); } const style = ""; main(); function main() { document.body.classList.add("tanhuazu"); const p2 = location.pathname; if (p2.startsWith("/threads/")) { handleReplyWait(); fetchMagnetLink(); return; } if (p2.startsWith("/forums/") || // 论坛 p2 === "/whats-new/" || // 最新消息 p2.startsWith("/whats-new/posts/") || // 新帖 p2.startsWith("/find-threads/") || // 查找主题(mine, 已回复, 未回复) p2.startsWith("/watched/threads") || // 关注主题 isSearchResultPage() || // 搜索结果 p2.startsWith("/tags/")) { return postListMain(); } } })(React, ReactDOM);