// ==UserScript== // @name Discourse 新标签页 // @name:en Discourse New Tab // @namespace https://github.com/selaky/discourse-new-tab // @version 1.2.0 // @description 专注优化 Discourse 论坛多种情况下点击链接的体验,可在新标签页打开主题帖等页面,支持大量可自定义细节,自动识别 Discourse 站点 // @description:en Optimize link-click experience on Discourse: open topics and related pages in new tabs, highly customizable, auto-detects Discourse // @author selaky // @homepageURL https://github.com/selaky/discourse-new-tab // @supportURL https://github.com/selaky/discourse-new-tab/issues // @match http*://*/* // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM.openInTab // @grant GM_openInTab // @run-at document-start // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/552284/Discourse%20%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5.user.js // @updateURL https://update.greasyfork.icu/scripts/552284/Discourse%20%E6%96%B0%E6%A0%87%E7%AD%BE%E9%A1%B5.meta.js // ==/UserScript== (() => { var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/storage/gm.ts function isPromise(v) { return v && typeof v.then === "function"; } async function gmGet(key, def) { try { const gmg = globalThis.GM_getValue; if (typeof gmg === "function") { const r = gmg(key, def); return isPromise(r) ? await r : r; } const GM = globalThis.GM; if (GM?.getValue) { return await GM.getValue(key, def); } } catch (err) { console.warn(LABEL, "GM_getValue \u8C03\u7528\u5931\u8D25\uFF0C\u5C1D\u8BD5\u4F7F\u7528 localStorage", err); } try { const raw = localStorage.getItem(`dnt:${key}`); return raw == null ? def : JSON.parse(raw); } catch (err) { console.warn(LABEL, "localStorage \u8BFB\u53D6\u5931\u8D25", err); return def; } } async function gmSet(key, value) { try { const gms = globalThis.GM_setValue; if (typeof gms === "function") { const r = gms(key, value); if (isPromise(r)) await r; return; } const GM = globalThis.GM; if (GM?.setValue) { await GM.setValue(key, value); return; } } catch (err) { console.warn(LABEL, "GM_setValue \u8C03\u7528\u5931\u8D25\uFF0C\u5C1D\u8BD5\u4F7F\u7528 localStorage", err); } try { localStorage.setItem(`dnt:${key}`, JSON.stringify(value)); } catch (err) { console.warn(LABEL, "localStorage \u5199\u5165\u5931\u8D25", err); } } function gmRegisterMenu(label, cb) { try { const reg = globalThis.GM_registerMenuCommand; if (typeof reg === "function") { reg(label, cb); return; } } catch (err) { console.warn(LABEL, "GM_registerMenuCommand \u8C03\u7528\u5931\u8D25\uFF0C\u5FFD\u7565", err); } } function gmOnValueChange(key, handler) { try { const addLegacy = globalThis.GM_addValueChangeListener; if (typeof addLegacy === "function") { const id = addLegacy(key, (_k, oldV, newV, remote) => handler(oldV, newV, remote)); return () => { try { const rm = globalThis.GM_removeValueChangeListener; if (typeof rm === "function") rm(id); } catch (err) { console.warn(LABEL, "GM_removeValueChangeListener \u5931\u8D25", err); } }; } const GM = globalThis.GM; if (GM?.addValueChangeListener) { const id = GM.addValueChangeListener(key, (_k, oldV, newV, remote) => handler(oldV, newV, remote)); return () => { try { GM.removeValueChangeListener?.(id); } catch (err) { console.warn(LABEL, "GM.removeValueChangeListener \u5931\u8D25", err); } }; } } catch (err) { console.warn(LABEL, "GM_valueChangeListener \u4E0D\u53EF\u7528\uFF0C\u964D\u7EA7\u4E3A storage \u4E8B\u4EF6", err); } try { const storageKey = `dnt:${key}`; const listener = (e) => { if (e.key !== storageKey) return; let ov = void 0; let nv = void 0; try { ov = e.oldValue != null ? JSON.parse(e.oldValue) : void 0; } catch { } try { nv = e.newValue != null ? JSON.parse(e.newValue) : void 0; } catch { } handler(ov, nv, true); }; window.addEventListener("storage", listener); return () => window.removeEventListener("storage", listener); } catch (err) { console.warn(LABEL, "storage \u4E8B\u4EF6\u76D1\u542C\u5931\u8D25", err); return () => { }; } } var LABEL; var init_gm = __esm({ "src/storage/gm.ts"() { LABEL = "[discourse-new-tab]"; } }); // src/debug/settings.ts async function getDebugEnabled() { const v = await gmGet(KEY_DEBUG_ENABLED, false); return !!v; } async function setDebugEnabled(enabled) { await gmSet(KEY_DEBUG_ENABLED, !!enabled); } async function getDebugCategories() { const saved = await gmGet(KEY_DEBUG_CATEGORIES, {}) || {}; return { ...DEFAULT_DEBUG_CATEGORIES, ...saved }; } async function setDebugCategory(cat, enabled) { const cats = await getDebugCategories(); cats[cat] = !!enabled; await gmSet(KEY_DEBUG_CATEGORIES, cats); } async function setAllDebugCategories(enabled) { const all = { site: enabled, click: enabled, link: enabled, rules: enabled, final: enabled, bg: enabled }; await gmSet(KEY_DEBUG_CATEGORIES, all); } var DEBUG_LABEL, KEY_DEBUG_ENABLED, KEY_DEBUG_CATEGORIES, DEFAULT_DEBUG_CATEGORIES; var init_settings = __esm({ "src/debug/settings.ts"() { init_gm(); DEBUG_LABEL = "[discourse-new-tab]"; KEY_DEBUG_ENABLED = "debug:enabled"; KEY_DEBUG_CATEGORIES = "debug:categories"; DEFAULT_DEBUG_CATEGORIES = { site: true, click: true, link: true, rules: true, final: true, bg: true }; } }); // src/utils/url.ts function toAbsoluteUrl(href, base) { try { if (!href || typeof href !== "string") return null; return new URL(href, base); } catch (err) { void logError("link", "URL \u7EDD\u5BF9\u5316\u5931\u8D25", err); return null; } } function extractTopicId(pathname) { try { const p = (pathname || "").toLowerCase(); const patterns = [ /\/t\/[\w%\-\.]+\/(\d+)(?:\/|$)/i, // 带 slug /\/t\/(\d+)(?:\/|$)/i // 仅 id ]; for (const re of patterns) { const m = p.match(re); if (m && m[1]) { const id = parseInt(m[1], 10); if (!Number.isNaN(id)) return id; } } } catch (err) { void logError("link", "\u89E3\u6790\u4E3B\u9898\u5E16 ID \u5931\u8D25", err); } return void 0; } function extractUsername(pathname) { try { const p = (pathname || "").toLowerCase(); const m = p.match(/\/u\/([\w%\-\.]+)/i); if (m && m[1]) return decodeURIComponent(m[1]); } catch (err) { void logError("link", "\u89E3\u6790\u7528\u6237\u540D\u5931\u8D25", err); } return void 0; } function isLikelyAttachment(pathname) { const p = (pathname || "").toLowerCase(); if (p.includes("/uploads/")) return true; if (/\.(png|jpe?g|gif|webp|svg|zip|rar|7z|pdf|mp4|mp3)$/i.test(p)) return true; return false; } var init_url = __esm({ "src/utils/url.ts"() { init_logger(); } }); // src/debug/logger.ts async function shouldLog(category) { const enabled = await getDebugEnabled(); if (!enabled) return false; const cats = await getDebugCategories(); return !!cats[category]; } async function logSiteDetection(result) { if (!await shouldLog("site")) return; const head = `${DEBUG_LABEL} \u8BC6\u522B\u4E3A Discourse \u7AD9\u70B9\uFF08\u5F97\u5206\uFF1A${result.score}/${result.threshold}\uFF09`; const signals = result.matchedSignals.map((s) => `${s.name}(+${s.weight})`).join(" | "); console.log(head + "\u3002"); if (signals) console.log(signals); } async function logClickFilter(reason) { if (!await shouldLog("click")) return; console.log(`${DEBUG_LABEL} \u70B9\u51FB\u4E8B\u4EF6\u5FFD\u7565\uFF1A${reason}`); } async function logLinkInfo(ctx) { if (!await shouldLog("link")) return; const current = ctx.currentUrl?.href; const target = ctx.targetUrl?.href; const currentTopicId = extractTopicId(ctx.currentUrl.pathname); const targetTopicId = extractTopicId(ctx.targetUrl.pathname); const currentUser = extractUsername(ctx.currentUrl.pathname); const targetUser = extractUsername(ctx.targetUrl.pathname); const parts = []; if (currentTopicId != null) parts.push(`currentTopicId=${currentTopicId}`); if (targetTopicId != null) parts.push(`targetTopicId=${targetTopicId}`); if (currentUser) parts.push(`currentUser=${currentUser}`); if (targetUser) parts.push(`targetUser=${targetUser}`); const extra = parts.length ? `\uFF08${parts.join("\uFF0C")}\uFF09` : ""; console.log(`${DEBUG_LABEL} \u94FE\u63A5\uFF1A\u5F53\u524D ${current} \u2192 \u76EE\u6807 ${target}${extra}`); } async function logRuleDetail(rule, enabled, matched, action, meta) { if (!await shouldLog("rules")) return; const cats = await getDebugCategories(); void cats; const enabledText = enabled ? "\u5F00" : "\u5173"; const hit = matched ? "\u547D\u4E2D" : "\u672A\u547D\u4E2D"; const act = action ? action === "new_tab" ? "\u65B0\u6807\u7B7E\u9875" : action === "same_tab" ? "\u540C\u9875" : "\u4FDD\u7559\u539F\u751F" : "\u2014"; console.log(`${DEBUG_LABEL} \u89C4\u5219\uFF1A${rule.name}\uFF08${enabledText}\uFF09 \u2192 ${hit}${action ? `\uFF0C\u52A8\u4F5C\uFF1A${act}` : ""}`); if (matched && meta && (meta.note || meta.data)) { const note = meta.note ? `\u8BF4\u660E\uFF1A${meta.note}` : ""; const data = meta.data ? `\u6570\u636E\uFF1A${safeInline(meta.data)}` : ""; const line = [note, data].filter(Boolean).join("\uFF1B"); if (line) console.log(`${DEBUG_LABEL} ${line}`); } } async function logFinalDecision(ruleId, action) { if (!await shouldLog("final")) return; const act = action === "new_tab" ? "\u65B0\u6807\u7B7E\u9875" : action === "same_tab" ? "\u540C\u9875" : "\u4FDD\u7559\u539F\u751F"; console.log(`${DEBUG_LABEL} \u6700\u7EC8\u89C4\u5219\u4E0E\u52A8\u4F5C\uFF1A${ruleId} \u2192 ${act}`); } async function logBackgroundOpenApplied(mode) { if (!await shouldLog("final")) return; const m = mode === "all" ? "\u5168\u90E8" : "\u4EC5\u4E3B\u9898\u5E16"; console.log(`${DEBUG_LABEL} \u540E\u53F0\u6253\u5F00\uFF1A${m}`); } function safeInline(obj) { const parts = []; for (const k of Object.keys(obj)) { const v = obj[k]; if (v == null) continue; parts.push(`${k}=${String(v)}`); } return parts.join(", "); } async function logError(category, message, err) { if (!await shouldLog(category)) return; console.error(`${DEBUG_LABEL} \u9519\u8BEF\uFF1A${message}`, err); } async function logBgBallVisibility(visible) { if (!await shouldLog("bg")) return; console.log(`${DEBUG_LABEL} \u60AC\u6D6E\u7403\uFF1A${visible ? "\u663E\u793A" : "\u9690\u85CF"}`); } async function logBgModeChange(mode, source) { if (!await shouldLog("bg")) return; const m = mode === "all" ? "\u5168\u90E8" : mode === "topic" ? "\u4EC5\u4E3B\u9898\u5E16" : "\u65E0"; const s = source === "ball" ? "\u60AC\u6D6E\u7403" : "\u8BBE\u7F6E"; console.log(`${DEBUG_LABEL} \u540E\u53F0\u6253\u5F00\u5207\u6362\uFF08${s}\uFF09\uFF1A${m}`); } var init_logger = __esm({ "src/debug/logger.ts"() { init_settings(); init_url(); } }); // src/detectors/siteDetector.ts function detectDiscourse(doc = document, win = window) { const url = win.location?.href || ""; const signals = [ // 强信号:meta generator 包含 Discourse(官方默认输出) metaGeneratorSignal(doc), // 强信号:窗口上暴露 Discourse 对象(不少站点保留) windowDiscourseSignal(win), // 中等信号:常见的 Discourse 专用 meta metaDiscourseSpecificSignal(doc), // 中等信号:常见的 DOM 结构(保守选择) domStructureSignal(doc), // 弱信号:URL 路径包含 Discourse 常见路由段 urlPathPatternSignal(url) ]; const matchedSignals = signals.filter((s) => s.matched).map(({ name, weight, note }) => ({ name, weight, note })); const score = matchedSignals.reduce((sum, s) => sum + s.weight, 0); const isDiscourse = score >= THRESHOLD; return { isDiscourse, score, threshold: THRESHOLD, matchedSignals }; } function metaGeneratorSignal(doc) { try { const meta = doc.querySelector('meta[name="generator"]'); const content = meta?.content?.toLowerCase?.() || ""; const matched = content.includes("discourse"); return { name: "meta:generator=Discourse", weight: 3, matched, note: content || void 0 }; } catch (err) { void logError("site", '\u8BFB\u53D6 meta[name="generator"] \u5931\u8D25', err); return { name: "meta:generator=Discourse", weight: 3, matched: false }; } } function windowDiscourseSignal(win) { try { const matched = typeof win.Discourse !== "undefined"; return { name: "window.Discourse \u5B58\u5728", weight: 3, matched }; } catch (err) { void logError("site", "\u63A2\u6D4B window.Discourse \u5931\u8D25", err); return { name: "window.Discourse \u5B58\u5728", weight: 3, matched: false }; } } function metaDiscourseSpecificSignal(doc) { try { const metas = Array.from(doc.querySelectorAll("meta[name]")); const names = metas.map((m) => m.getAttribute("name") || ""); const hasDiscourseMeta = names.some((n) => n.startsWith("discourse_")) || !!doc.querySelector('meta[name="application-name"][content*="Discourse" i]'); return { name: "meta:discourse_* \u6216 application-name=Discourse", weight: 2, matched: !!hasDiscourseMeta }; } catch (err) { void logError("site", "\u8BFB\u53D6 Discourse \u76F8\u5173 meta \u5931\u8D25", err); return { name: "meta:discourse_* \u6216 application-name=Discourse", weight: 2, matched: false }; } } function domStructureSignal(doc) { try { const matched = !!(doc.getElementById("main-outlet") || doc.querySelector(".topic-list") || doc.querySelector('meta[property="og:site_name"]')); return { name: "DOM: #main-outlet/.topic-list/og:site_name", weight: 2, matched }; } catch (err) { void logError("site", "\u68C0\u67E5 DOM \u7ED3\u6784\u5931\u8D25", err); return { name: "DOM: #main-outlet/.topic-list/og:site_name", weight: 2, matched: false }; } } function urlPathPatternSignal(url) { try { const u = new URL(url); const p = u.pathname.toLowerCase(); const patterns = ["/t/", "/u/", "/c/", "/tags", "/latest", "/top"]; const matched = patterns.some((s) => p.includes(s)); return { name: "URL \u8DEF\u5F84\u5305\u542B Discourse \u5E38\u89C1\u6BB5", weight: 1, matched, note: p }; } catch (err) { void logError("site", "URL \u89E3\u6790\u5931\u8D25", err); return { name: "URL \u8DEF\u5F84\u5305\u542B Discourse \u5E38\u89C1\u6BB5", weight: 1, matched: false }; } } var THRESHOLD; var init_siteDetector = __esm({ "src/detectors/siteDetector.ts"() { init_logger(); THRESHOLD = 3; } }); // src/storage/domainLists.ts function normalizeDomain(input) { try { const s = (input || "").trim().toLowerCase(); if (/^https?:\/\//i.test(s)) { return new URL(s).hostname; } return s.split(":")[0]; } catch (err) { void logError("final", "\u57DF\u540D\u89C4\u8303\u5316\u5931\u8D25", err); return (input || "").trim().toLowerCase(); } } function uniqSort(arr) { return Array.from(new Set(arr.filter(Boolean).map(normalizeDomain))).sort(); } async function getLists() { const whitelist = await gmGet(KEY_WHITE, []) || []; const blacklist = await gmGet(KEY_BLACK, []) || []; return { whitelist: uniqSort(whitelist), blacklist: uniqSort(blacklist) }; } async function addToWhitelist(domain) { const { whitelist } = await getLists(); const d = normalizeDomain(domain); if (!whitelist.includes(d)) { whitelist.push(d); await gmSet(KEY_WHITE, uniqSort(whitelist)); return { added: true, list: uniqSort(whitelist) }; } return { added: false, list: whitelist }; } async function removeFromWhitelist(domain) { const { whitelist } = await getLists(); const d = normalizeDomain(domain); const next = whitelist.filter((x) => x !== d); const removed = next.length !== whitelist.length; if (removed) await gmSet(KEY_WHITE, uniqSort(next)); return { removed, list: uniqSort(next) }; } async function addToBlacklist(domain) { const { blacklist } = await getLists(); const d = normalizeDomain(domain); if (!blacklist.includes(d)) { blacklist.push(d); await gmSet(KEY_BLACK, uniqSort(blacklist)); return { added: true, list: uniqSort(blacklist) }; } return { added: false, list: blacklist }; } async function removeFromBlacklist(domain) { const { blacklist } = await getLists(); const d = normalizeDomain(domain); const next = blacklist.filter((x) => x !== d); const removed = next.length !== blacklist.length; if (removed) await gmSet(KEY_BLACK, uniqSort(next)); return { removed, list: uniqSort(next) }; } function getCurrentHostname() { try { return location.hostname.toLowerCase(); } catch (err) { void logError("final", "\u8BFB\u53D6\u5F53\u524D\u4E3B\u673A\u540D\u5931\u8D25", err); return ""; } } async function getEnablement(autoIsDiscourse, host) { const { whitelist, blacklist } = await getLists(); const h = normalizeDomain(host || getCurrentHostname()); if (blacklist.includes(h)) return { enabled: false, reason: "blacklist" }; if (whitelist.includes(h)) return { enabled: true, reason: "whitelist" }; if (autoIsDiscourse) return { enabled: true, reason: "auto" }; return { enabled: false, reason: "disabled" }; } var KEY_WHITE, KEY_BLACK; var init_domainLists = __esm({ "src/storage/domainLists.ts"() { init_gm(); init_logger(); KEY_WHITE = "whitelist"; KEY_BLACK = "blacklist"; } }); // src/storage/settings.ts async function getRuleFlags() { const saved = await gmGet(KEY_RULES, {}) || {}; return { ...DEFAULTS, ...saved }; } async function getRuleEnabled(ruleId) { const flags = await getRuleFlags(); const v = flags[ruleId]; return typeof v === "boolean" ? v : DEFAULTS[ruleId] ?? true; } async function setRuleEnabled(ruleId, enabled) { const flags = await getRuleFlags(); flags[ruleId] = enabled; await gmSet(KEY_RULES, flags); } var RULE_TOPIC_OPEN_NEW_TAB, RULE_TOPIC_IN_TOPIC_OPEN_OTHER, RULE_TOPIC_SAME_TOPIC_KEEP_NATIVE, RULE_USER_OPEN_NEW_TAB, RULE_USER_IN_PROFILE_OPEN_OTHER, RULE_USER_SAME_PROFILE_KEEP_NATIVE, RULE_ATTACHMENT_KEEP_NATIVE, RULE_POPUP_USER_CARD, RULE_POPUP_USER_MENU, RULE_POPUP_SEARCH_MENU, RULE_SIDEBAR_NON_TOPIC_KEEP_NATIVE, RULE_SIDEBAR_IN_TOPIC_NEW_TAB, DEFAULTS, KEY_RULES; var init_settings2 = __esm({ "src/storage/settings.ts"() { init_gm(); RULE_TOPIC_OPEN_NEW_TAB = "topic:open-new-tab"; RULE_TOPIC_IN_TOPIC_OPEN_OTHER = "topic:in-topic-open-other"; RULE_TOPIC_SAME_TOPIC_KEEP_NATIVE = "topic:same-topic-keep-native"; RULE_USER_OPEN_NEW_TAB = "user:open-new-tab"; RULE_USER_IN_PROFILE_OPEN_OTHER = "user:in-profile-open-other"; RULE_USER_SAME_PROFILE_KEEP_NATIVE = "user:same-profile-keep-native"; RULE_ATTACHMENT_KEEP_NATIVE = "attachment:keep-native"; RULE_POPUP_USER_CARD = "popup:user-card"; RULE_POPUP_USER_MENU = "popup:user-menu"; RULE_POPUP_SEARCH_MENU = "popup:search-menu-results"; RULE_SIDEBAR_NON_TOPIC_KEEP_NATIVE = "sidebar:non-topic-keep-native"; RULE_SIDEBAR_IN_TOPIC_NEW_TAB = "sidebar:in-topic-open-new-tab"; DEFAULTS = { [RULE_TOPIC_OPEN_NEW_TAB]: true, [RULE_TOPIC_IN_TOPIC_OPEN_OTHER]: true, [RULE_TOPIC_SAME_TOPIC_KEEP_NATIVE]: true, [RULE_USER_OPEN_NEW_TAB]: true, [RULE_USER_IN_PROFILE_OPEN_OTHER]: true, [RULE_USER_SAME_PROFILE_KEEP_NATIVE]: true, [RULE_ATTACHMENT_KEEP_NATIVE]: true, [RULE_POPUP_USER_CARD]: true, [RULE_POPUP_USER_MENU]: true, [RULE_POPUP_SEARCH_MENU]: true, [RULE_SIDEBAR_NON_TOPIC_KEEP_NATIVE]: true, [RULE_SIDEBAR_IN_TOPIC_NEW_TAB]: true }; KEY_RULES = "ruleFlags"; } }); // src/storage/openMode.ts async function getBackgroundOpenMode() { const v = await gmGet(KEY_BG_MODE, DEFAULT_BG_MODE); if (v === "topic" || v === "all" || v === "none") return v; return DEFAULT_BG_MODE; } async function setBackgroundOpenMode(mode) { await gmSet(KEY_BG_MODE, mode); } var KEY_BG_MODE, DEFAULT_BG_MODE; var init_openMode = __esm({ "src/storage/openMode.ts"() { init_gm(); KEY_BG_MODE = "open:bg-mode"; DEFAULT_BG_MODE = "none"; } }); // src/storage/floatBall.ts async function getFloatBallEnabled() { const v = await gmGet(KEY_FB_ENABLED, true); return !!v; } async function setFloatBallEnabled(enabled) { await gmSet(KEY_FB_ENABLED, !!enabled); } async function getFloatBallFixed() { const v = await gmGet(KEY_FB_FIXED, false); return !!v; } async function setFloatBallFixed(fixed2) { await gmSet(KEY_FB_FIXED, !!fixed2); } async function getFloatBallPos() { const pos = await gmGet(KEY_FB_POS, DEFAULT_POS); const xr = Math.min(0.98, Math.max(0.02, pos?.xRatio ?? DEFAULT_POS.xRatio)); const yr = Math.min(0.98, Math.max(0.02, pos?.yRatio ?? DEFAULT_POS.yRatio)); return { xRatio: xr, yRatio: yr }; } async function setFloatBallPos(pos) { const xr = Math.min(0.98, Math.max(0.02, pos.xRatio)); const yr = Math.min(0.98, Math.max(0.02, pos.yRatio)); await gmSet(KEY_FB_POS, { xRatio: xr, yRatio: yr }); } async function resetFloatBallPos() { await gmSet(KEY_FB_POS, DEFAULT_POS); return DEFAULT_POS; } async function getAllowedModes() { const saved = await gmGet(KEY_FB_ALLOWED, DEFAULT_ALLOWED) || DEFAULT_ALLOWED; return normalizeAllowed(saved); } async function setAllowedModes(m) { const nm = normalizeAllowed(m); await gmSet(KEY_FB_ALLOWED, nm); return nm; } function normalizeAllowed(m) { const nm = { none: !!m?.none, topic: !!m?.topic, all: !!m?.all }; const count = (nm.none ? 1 : 0) + (nm.topic ? 1 : 0) + (nm.all ? 1 : 0); if (count >= 2) return nm; if (!nm.topic) nm.topic = true; if (!nm.all && (nm.none ? 1 : 0) + (nm.topic ? 1 : 0) < 2) nm.all = true; return nm; } var KEY_FB_ENABLED, KEY_FB_FIXED, KEY_FB_POS, KEY_FB_ALLOWED, DEFAULT_POS, DEFAULT_ALLOWED, __keys; var init_floatBall = __esm({ "src/storage/floatBall.ts"() { init_gm(); KEY_FB_ENABLED = "ui:floatball:enabled"; KEY_FB_FIXED = "ui:floatball:fixed"; KEY_FB_POS = "ui:floatball:pos"; KEY_FB_ALLOWED = "ui:floatball:allowed-modes"; DEFAULT_POS = { xRatio: 0.94, yRatio: 0.66 }; DEFAULT_ALLOWED = { none: true, topic: true, all: true }; __keys = { KEY_FB_ENABLED, KEY_FB_FIXED, KEY_FB_POS, KEY_FB_ALLOWED }; } }); // src/ui/i18n.ts async function initI18n() { currentLang = await gmGet(KEY_LANG) || "zh"; } function getLanguage() { return currentLang; } async function setLanguage(lang) { currentLang = lang; await gmSet(KEY_LANG, lang); } async function toggleLanguage() { const idx = LANGUAGES.indexOf(currentLang); const next = LANGUAGES[(idx + 1) % LANGUAGES.length]; await setLanguage(next); } function t(key) { const keys = key.split("."); let obj = translations[currentLang]; for (const k of keys) { if (obj && typeof obj === "object") { obj = obj[k]; } else { return key; } } return typeof obj === "string" ? obj : key; } var KEY_LANG, LANGUAGES, LanguageIcon, currentLang, translations; var init_i18n = __esm({ "src/ui/i18n.ts"() { init_gm(); KEY_LANG = "ui-language"; LANGUAGES = ["zh", "en"]; LanguageIcon = { zh: ` \u4E2D `, en: ` EN ` }; currentLang = "zh"; translations = { zh: { settings: { title: "\u8BBE\u7F6E", close: "\u5173\u95ED", theme: { light: "\u65E5\u95F4\u6A21\u5F0F", dark: "\u591C\u95F4\u6A21\u5F0F", auto: "\u81EA\u52A8\u6A21\u5F0F" }, language: { zh: "\u4E2D\u6587", en: "English" }, categories: { recognition: "\u8BBA\u575B\u8BC6\u522B", rules: "\u8DF3\u8F6C\u89C4\u5219", open: "\u540E\u53F0\u6253\u5F00", debug: "\u8C03\u8BD5" }, status: { title: "\u5F53\u524D\u72B6\u6001", domain: "\u5F53\u524D\u57DF\u540D", enabled: "\u5DF2\u542F\u7528", disabled: "\u672A\u542F\u7528", reason: { auto: "\u81EA\u52A8\u8BC6\u522B", whitelist: "\u767D\u540D\u5355", blacklist: "\u9ED1\u540D\u5355", disabled: "\u672A\u8BC6\u522B\u4E3A Discourse" } }, domain: { title: "\u9ED1\u767D\u540D\u5355", whitelist: "\u767D\u540D\u5355 - \u5F3A\u5236\u542F\u7528\u811A\u672C", blacklist: "\u9ED1\u540D\u5355 - \u5F3A\u5236\u7981\u7528\u811A\u672C", placeholder: "\u8F93\u5165\u57DF\u540D", add: "\u6DFB\u52A0", addCurrent: "\u6DFB\u52A0\u5F53\u524D\u57DF\u540D", edit: "\u7F16\u8F91", delete: "\u5220\u9664", empty: "\u6682\u65E0\u57DF\u540D" }, rules: { title: "\u8DF3\u8F6C\u89C4\u5219", topic: { title: "\u4E3B\u9898\u5E16", openNewTab: "\u4ECE\u4EFB\u610F\u9875\u9762\u6253\u5F00\u4E3B\u9898\u5E16\u65F6\uFF0C\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00", inTopicOpenOther: "\u5728\u4E3B\u9898\u5E16\u5185\u90E8\u70B9\u51FB\u5176\u4ED6\u94FE\u63A5\u65F6,\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00", sameTopicKeepNative: "\u697C\u5C42\u8DF3\u8F6C\u65F6\u4FDD\u7559\u539F\u751F\u8DF3\u8F6C\u65B9\u5F0F" }, user: { title: "\u4E2A\u4EBA\u4E3B\u9875", openNewTab: "\u4ECE\u4EFB\u610F\u9875\u9762\u6253\u5F00\u7528\u6237\u4E2A\u4EBA\u4E3B\u9875\u65F6,\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00", inProfileOpenOther: "\u5728\u7528\u6237\u4E2A\u4EBA\u4E3B\u9875\u5185\u70B9\u51FB\u5176\u4ED6\u94FE\u63A5\u65F6,\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00", sameProfileKeepNative: "\u540C\u4E00\u7528\u6237\u4E3B\u9875\u5185\u8DF3\u8F6C\u65F6\u4FDD\u7559\u539F\u751F\u65B9\u5F0F" }, attachment: { title: "\u9644\u4EF6", keepNative: "\u6253\u5F00\u56FE\u7247\u7B49\u9644\u4EF6\u65F6,\u4FDD\u7559\u539F\u751F\u8DF3\u8F6C\u65B9\u5F0F" }, popup: { title: "\u5F39\u7A97", userCard: "\u7528\u6237\u5361\u7247\u5185\u94FE\u63A5\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00", userMenu: "\u7528\u6237\u83DC\u5355\u5185\u94FE\u63A5\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00", // 新增:搜索框结果与“更多”按钮 searchMenu: "\u641C\u7D22\u6846\u94FE\u63A5\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00" }, sidebar: { title: "\u4FA7\u8FB9\u680F", nonTopicKeepNative: "\u975E\u4E3B\u9898\u5E16\u5185\u4FA7\u8FB9\u680F\u7528\u539F\u751F\u65B9\u5F0F", inTopicNewTab: "\u4E3B\u9898\u5E16\u5185\u4FA7\u8FB9\u680F\u7528\u65B0\u6807\u7B7E\u9875\u6253\u5F00" } }, openMode: { title: "\u6253\u5F00\u65B9\u5F0F", description: "\u540E\u53F0\u6253\u5F00\u662F\u6307\u5728\u65B0\u6807\u7B7E\u9875\u6253\u5F00\u94FE\u63A5\u65F6,\u4FDD\u6301\u5F53\u524D\u9875\u9762\u4E3A\u6D3B\u52A8\u6807\u7B7E,\u65B0\u6807\u7B7E\u5728\u540E\u53F0\u6253\u5F00", selectLabel: "\u5F53\u524D\u6A21\u5F0F", options: { none: "\u524D\u53F0\u6253\u5F00", topic: "\u4E3B\u9898\u5E16\u540E\u53F0", all: "\u5168\u90E8\u540E\u53F0" }, optionDesc: { none: "\u65B0\u6807\u7B7E\u7ACB\u5373\u6FC0\u6D3B", topic: "\u6253\u5F00\u4E3B\u9898\u5E16\u65F6\u5728\u540E\u53F0", all: "\u6240\u6709\u65B0\u6807\u7B7E\u90FD\u5728\u540E\u53F0" }, floatball: { title: "\u60AC\u6D6E\u7403\u8BBE\u7F6E", tip: "\u82E5\u9700\u8981\u7ECF\u5E38\u5207\u6362\u524D\u540E\u53F0\u6253\u5F00\u65B9\u5F0F,\u53EF\u5F00\u542F\u60AC\u6D6E\u7403,\u968F\u65F6\u70B9\u51FB\u5207\u6362", displayTitle: "\u663E\u793A\u8BBE\u7F6E", switchTitle: "\u5207\u6362\u8BBE\u7F6E", show: "\u663E\u793A\u60AC\u6D6E\u7403", showDesc: "\u5728\u9875\u9762\u4E0A\u663E\u793A\u5FEB\u901F\u5207\u6362\u6309\u94AE", reset: "\u91CD\u7F6E\u4F4D\u7F6E", fixed: "\u56FA\u5B9A\u4F4D\u7F6E", fixedDesc: "\u7981\u7528\u62D6\u52A8,\u9501\u5B9A\u60AC\u6D6E\u7403\u4F4D\u7F6E", modes: "\u60AC\u6D6E\u7403\u53EF\u5207\u6362\u7684\u6A21\u5F0F", modesDesc: "\u81F3\u5C11\u4FDD\u75592\u4E2A\u9009\u9879\u4EE5\u4FBF\u5207\u6362" } }, debug: { title: "\u8C03\u8BD5", enable: "\u8C03\u8BD5\u6A21\u5F0F", allOn: "\u5168\u90E8\u5F00\u542F", allOff: "\u5168\u90E8\u5173\u95ED", categories: { site: "\u7AD9\u70B9\u8BC6\u522B", click: "\u70B9\u51FB\u8FC7\u6EE4\u539F\u56E0", link: "\u94FE\u63A5\u4FE1\u606F", rules: "\u89C4\u5219\u7EC6\u8282", final: "\u6700\u7EC8\u89C4\u5219\u4E0E\u52A8\u4F5C", bg: "\u540E\u53F0\u6253\u5F00" } } } }, en: { settings: { title: "Settings", close: "Close", theme: { light: "Light Mode", dark: "Dark Mode", auto: "Auto Mode" }, language: { zh: "\u4E2D\u6587", en: "English" }, categories: { recognition: "Forum Recognition", rules: "Navigation Rules", open: "Background Open", debug: "Debug" }, status: { title: "Current Status", domain: "Current Domain", enabled: "Enabled", disabled: "Disabled", reason: { auto: "Auto-detected", whitelist: "Whitelist", blacklist: "Blacklist", disabled: "Not a Discourse forum" } }, domain: { title: "Blacklist & Whitelist", whitelist: "Whitelist - Force Enable Script", blacklist: "Blacklist - Force Disable Script", placeholder: "Enter domain", add: "Add", addCurrent: "Add Current Domain", edit: "Edit", delete: "Delete", empty: "No domains" }, rules: { title: "Navigation Rules", topic: { title: "Topics", openNewTab: "Open topics in new tab from any page", inTopicOpenOther: "Open other links in new tab within topics", sameTopicKeepNative: "Keep native behavior for floor jumps" }, user: { title: "User Profiles", openNewTab: "Open user profiles in new tab from any page", inProfileOpenOther: "Open other links in new tab within profiles", sameProfileKeepNative: "Keep native behavior within same profile" }, attachment: { title: "Attachments", keepNative: "Keep native behavior for images and attachments" }, popup: { title: "Popups", userCard: "Open user card links in new tab", userMenu: "Open user menu links in new tab", // New: search popup results and "more" button searchMenu: "Open search box links in new tab" }, sidebar: { title: "Sidebar", nonTopicKeepNative: "Keep native behavior in non-topic pages", inTopicNewTab: "Open sidebar links in new tab within topics" } }, openMode: { title: "Open Behavior", description: "Background open means opening links in a new tab while keeping the current page active, with the new tab opened in the background", selectLabel: "Current Mode", options: { none: "Foreground", topic: "Topics Background", all: "All Background" }, optionDesc: { none: "New tab activates immediately", topic: "Topics open in background", all: "All new tabs in background" }, floatball: { title: "Float Ball Settings", tip: "If you need to frequently switch between foreground/background modes, enable the float ball to toggle anytime", displayTitle: "Display Settings", switchTitle: "Switch Settings", show: "Show Float Ball", showDesc: "Display quick toggle button on page", reset: "Reset Position", fixed: "Pin Position", fixedDesc: "Lock float ball position", modes: "Switchable Modes", modesDesc: "Keep at least 2 options for switching" } }, debug: { title: "Debug", enable: "Debug Mode", allOn: "Enable All", allOff: "Disable All", categories: { site: "Site Detection", click: "Click Filter Reasons", link: "Link Info", rules: "Rule Details", final: "Final Rule & Action", bg: "Background Open" } } } } }; } }); // src/floatball/index.ts var floatball_exports = {}; __export(floatball_exports, { __floatBall: () => __floatBall, initFloatBall: () => initFloatBall, resetFloatBallPosition: () => resetFloatBallPosition, setFloatBallFixedMode: () => setFloatBallFixedMode, setFloatBallShown: () => setFloatBallShown, syncCurrentModeFromStorage: () => syncCurrentModeFromStorage, updateAllowedModes: () => updateAllowedModes }); function ensureStyle() { if (document.getElementById("dnt-fb-style")) return; const s = document.createElement("style"); s.id = "dnt-fb-style"; s.textContent = ` .dnt-fb { position: fixed; z-index: 2147483646; width: ${SIZE}px; height: ${SIZE}px; border-radius: 50%; display: flex; flex-direction: column; align-items: center; justify-content: center; cursor: pointer; user-select: none; box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15), 0 2px 8px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); backdrop-filter: blur(12px) saturate(150%); transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.2s ease; will-change: transform; } .dnt-fb-dark { box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4), 0 2px 10px rgba(0, 0, 0, 0.3); border-color: rgba(255, 255, 255, 0.15); } .dnt-fb:hover { transform: scale(1.08); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.2), 0 3px 10px rgba(0, 0, 0, 0.12); } .dnt-fb-dark:hover { box-shadow: 0 6px 24px rgba(0, 0, 0, 0.5), 0 3px 12px rgba(0, 0, 0, 0.35); } .dnt-fb-dragging { transform: scale(1.1); box-shadow: 0 8px 24px rgba(0, 0, 0, 0.25), 0 4px 12px rgba(0, 0, 0, 0.15); cursor: grabbing !important; } .dnt-fb-dark.dnt-fb-dragging { box-shadow: 0 8px 28px rgba(0, 0, 0, 0.6), 0 4px 14px rgba(0, 0, 0, 0.4); } .dnt-fb-fixed { cursor: default; } .dnt-fb-fixed:hover { transform: scale(1.02); } .dnt-fb-drag-handle { position: absolute; top: 4px; width: 16px; height: 3px; background: rgba(255, 255, 255, 0.4); border-radius: 2px; opacity: 0.6; } .dnt-fb-fixed .dnt-fb-drag-handle { display: none; } .dnt-fb-icon { color: #fff; line-height: 0; display: flex; align-items: center; justify-content: center; } .dnt-fb-tip { position: absolute; bottom: ${SIZE + 8}px; white-space: nowrap; font-size: 12px; font-weight: 500; background: rgba(0, 0, 0, 0.85); color: #fff; padding: 6px 10px; border-radius: 6px; pointer-events: none; opacity: 0; transform: translateY(4px); transition: opacity 0.2s ease, transform 0.2s ease; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); } .dnt-fb:hover .dnt-fb-tip { opacity: 1; transform: translateY(0); } .dnt-fb-dragging .dnt-fb-tip { display: none; } `; document.head.appendChild(s); } function getThemeClass() { const theme = window.__dntThemeCache; const fallback = theme ?? (function() { try { return localStorage.getItem("dnt:ui-theme")?.replace(/"/g, "") || "auto"; } catch { return "auto"; } })(); if (fallback === "dark") return "dnt-fb-dark"; if (fallback === "auto") { return window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dnt-fb-dark" : "dnt-fb-light"; } return "dnt-fb-light"; } function pxLeftFromRatio(xRatio) { const vw = window.innerWidth; const left = Math.round(vw * xRatio - SIZE / 2); return clamp(left, MARGIN, vw - SIZE - MARGIN); } function pxTopFromRatio(yRatio) { const vh = window.innerHeight; const top = Math.round(vh * yRatio - SIZE / 2); return clamp(top, MARGIN, vh - SIZE - MARGIN); } function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); } function setElPosFromRatio(el, pos) { el.style.left = pxLeftFromRatio(pos.xRatio) + "px"; el.style.top = pxTopFromRatio(pos.yRatio) + "px"; } function getTipText(mode) { const map = { none: `\u540E\u53F0\u6253\u5F00: ${t("settings.openMode.options.none")}`, topic: `\u540E\u53F0\u6253\u5F00: ${t("settings.openMode.options.topic")}`, all: `\u540E\u53F0\u6253\u5F00: ${t("settings.openMode.options.all")}` }; return map[mode] || ""; } function applyModeVisual() { if (!rootEl || !iconEl) return; const color = ModeColor[curMode]; rootEl.style.background = color; iconEl.innerHTML = Icons[curMode]; const tip = getTipText(curMode); rootEl.title = tip; if (tipEl) tipEl.textContent = tip; } async function cycleMode() { const order = ["none", "topic", "all"]; const enabledModes = order.filter((m) => allowed[m]); if (enabledModes.length < 2) { if (!allowed.all) { allowed.all = true; await setAllowedModes(allowed); } enabledModes.push("all"); } const idx = enabledModes.indexOf(curMode); const next = enabledModes[(idx + 1) % enabledModes.length]; if (next !== curMode) { curMode = next; await setBackgroundOpenMode(curMode); applyModeVisual(); await logBgModeChange(curMode, "ball"); } } function onMouseDown(ev) { if (!rootEl) return; dragging = !fixed; const rect = rootEl.getBoundingClientRect(); dragStart = { x: ev.clientX, y: ev.clientY, left: rect.left, top: rect.top }; if (dragging) { rootEl.classList.add("dnt-fb-dragging"); rootEl.style.cursor = "grabbing"; } } function onMouseMove(ev) { if (!rootEl || !dragging || !dragStart) return; if (rafId !== null) { cancelAnimationFrame(rafId); } rafId = requestAnimationFrame(() => { if (!rootEl || !dragStart) return; const dx = ev.clientX - dragStart.x; const dy = ev.clientY - dragStart.y; const left = clamp(dragStart.left + dx, MARGIN, window.innerWidth - SIZE - MARGIN); const top = clamp(dragStart.top + dy, MARGIN, window.innerHeight - SIZE - MARGIN); rootEl.style.left = `${left}px`; rootEl.style.top = `${top}px`; rafId = null; }); } async function onMouseUp(ev) { if (!rootEl) return; if (!dragStart) return; if (rafId !== null) { cancelAnimationFrame(rafId); rafId = null; } rootEl.classList.remove("dnt-fb-dragging"); rootEl.style.cursor = fixed ? "default" : "pointer"; const dx = Math.abs(ev.clientX - dragStart.x); const dy = Math.abs(ev.clientY - dragStart.y); const wasDragging = dragging && (dx > DRAG_THRESHOLD || dy > DRAG_THRESHOLD); dragging = false; if (wasDragging) { const rect = rootEl.getBoundingClientRect(); const xRatio = (rect.left + SIZE / 2) / window.innerWidth; const yRatio = (rect.top + SIZE / 2) / window.innerHeight; await setFloatBallPos({ xRatio, yRatio }); } dragStart = null; if (!wasDragging) { await cycleMode(); } } function onWindowResize() { if (!rootEl) return; getFloatBallPos().then((pos) => setElPosFromRatio(rootEl, pos)).catch((e) => void logError("bg", "\u7A97\u53E3\u53D8\u5316\u5B9A\u4F4D\u5931\u8D25", e)); } function updateThemeClass() { if (!rootEl) return; rootEl.classList.remove("dnt-fb-light", "dnt-fb-dark"); rootEl.classList.add(getThemeClass()); } function observeTheme() { const mo = new MutationObserver(() => updateThemeClass()); mo.observe(document.documentElement, { attributes: true, attributeFilter: ["data-dnt-theme"] }); if (window.matchMedia) { try { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => updateThemeClass()); } catch { } } } function createRoot() { const el = document.createElement("div"); el.id = "dnt-float-ball"; el.className = `dnt-fb ${getThemeClass()}`; el.setAttribute("aria-label", "\u540E\u53F0\u6253\u5F00\u5207\u6362"); const dragHandle = document.createElement("div"); dragHandle.className = "dnt-fb-drag-handle"; el.appendChild(dragHandle); const icon = document.createElement("div"); icon.className = "dnt-fb-icon"; el.appendChild(icon); const tip = document.createElement("div"); tip.className = "dnt-fb-tip"; tip.textContent = ""; el.appendChild(tip); el.addEventListener("mousedown", onMouseDown); window.addEventListener("mousemove", onMouseMove); window.addEventListener("mouseup", onMouseUp); return el; } async function mount() { if (rootEl) return; await ensureDomReady(); ensureStyle(); curMode = await getBackgroundOpenMode(); fixed = await getFloatBallFixed(); allowed = await getAllowedModes(); rootEl = createRoot(); iconEl = rootEl.querySelector(".dnt-fb-icon"); tipEl = rootEl.querySelector(".dnt-fb-tip"); applyModeVisual(); if (fixed) rootEl.classList.add("dnt-fb-fixed"); const pos = await getFloatBallPos(); setElPosFromRatio(rootEl, pos); if (document.body) document.body.appendChild(rootEl); observeTheme(); window.addEventListener("resize", onWindowResize); if (unsubPos) { try { unsubPos(); } catch { } ; unsubPos = null; } unsubPos = gmOnValueChange(__keys.KEY_FB_POS, (_oldV, newV) => { if (!rootEl || !newV) return; setElPosFromRatio(rootEl, newV); }); await logBgBallVisibility(true); } async function unmount() { if (!rootEl) return; window.removeEventListener("resize", onWindowResize); window.removeEventListener("mousemove", onMouseMove); window.removeEventListener("mouseup", onMouseUp); if (unsubPos) { try { unsubPos(); } catch { } ; unsubPos = null; } rootEl.remove(); rootEl = null; iconEl = null; await logBgBallVisibility(false); } async function initFloatBall() { const enabled = await getFloatBallEnabled(); if (enabled) await mount(); } async function setFloatBallShown(on) { await setFloatBallEnabled(on); if (on) await mount(); else await unmount(); } async function setFloatBallFixedMode(on) { fixed = on; await setFloatBallFixed(on); if (rootEl) { if (on) rootEl.classList.add("dnt-fb-fixed"); else rootEl.classList.remove("dnt-fb-fixed"); } } async function resetFloatBallPosition() { const pos = await resetFloatBallPos(); if (rootEl) setElPosFromRatio(rootEl, pos); } async function updateAllowedModes(next) { allowed = await setAllowedModes(next); } async function syncCurrentModeFromStorage() { curMode = await getBackgroundOpenMode(); applyModeVisual(); } async function ensureDomReady() { if (document.head && document.body && document.readyState !== "loading") return; await new Promise((resolve) => { const check = () => { if (document.head && document.body && document.readyState !== "loading") { document.removeEventListener("DOMContentLoaded", check); document.removeEventListener("readystatechange", check); resolve(); } }; document.addEventListener("DOMContentLoaded", check, { once: true }); document.addEventListener("readystatechange", check); const timer = setInterval(() => { if (document.head && document.body && document.readyState !== "loading") { clearInterval(timer); check(); } }, 30); }); } var rootEl, iconEl, tipEl, dragging, dragStart, fixed, allowed, curMode, unsubPos, rafId, DRAG_THRESHOLD, SIZE, MARGIN, Icons, ModeColor, __floatBall; var init_floatball = __esm({ "src/floatball/index.ts"() { init_openMode(); init_floatBall(); init_gm(); init_floatBall(); init_i18n(); init_logger(); rootEl = null; iconEl = null; tipEl = null; dragging = false; dragStart = null; fixed = false; allowed = { none: true, topic: true, all: true }; curMode = "none"; unsubPos = null; rafId = null; DRAG_THRESHOLD = 5; SIZE = 44; MARGIN = 8; Icons = { none: ``, topic: ``, all: `` }; ModeColor = { none: "#909399", topic: "#409eff", all: "#67c23a" }; __floatBall = { get state() { return { mounted: !!rootEl, fixed, allowed, mode: curMode }; } }; } }); // src/ui/theme.ts async function initTheme() { currentTheme = await gmGet(KEY_THEME) || "auto"; applyTheme(); } function getTheme() { return currentTheme; } async function setTheme(theme) { currentTheme = theme; await gmSet(KEY_THEME, theme); applyTheme(); } async function toggleTheme() { const idx = THEMES.indexOf(currentTheme); const next = THEMES[(idx + 1) % THEMES.length]; await setTheme(next); } function applyTheme() { const root = document.documentElement; if (currentTheme === "auto") { const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; root.setAttribute("data-dnt-theme", prefersDark ? "dark" : "light"); } else { root.setAttribute("data-dnt-theme", currentTheme); } } var KEY_THEME, THEMES, ThemeIcon, currentTheme; var init_theme = __esm({ "src/ui/theme.ts"() { init_gm(); KEY_THEME = "ui-theme"; THEMES = ["light", "dark", "auto"]; ThemeIcon = { light: ` `, dark: ` `, auto: ` ` }; currentTheme = "auto"; if (window.matchMedia) { window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => { if (currentTheme === "auto") { applyTheme(); } }); } } }); // src/ui/sections/categories.ts var CATEGORIES; var init_categories = __esm({ "src/ui/sections/categories.ts"() { CATEGORIES = [ { id: "recognition", icon: ` `, labelKey: "settings.categories.recognition" }, { id: "rules", icon: ` `, labelKey: "settings.categories.rules" }, { id: "open", icon: ` `, labelKey: "settings.categories.open" }, { id: "debug", icon: ` `, labelKey: "settings.categories.debug" } ]; } }); // src/ui/sections/status.ts function renderStatusSection() { const section = document.createElement("div"); section.className = "dnt-section"; const title = document.createElement("h3"); title.className = "dnt-section-title"; title.textContent = t("settings.status.title"); section.appendChild(title); const content = document.createElement("div"); content.className = "dnt-status-content"; content.id = STATUS_CONTENT_ID; section.appendChild(content); updateStatusContent(content); return section; } async function updateStatusContent(content) { const host = getCurrentHostname(); const result = detectDiscourse(); const enable = await getEnablement(result.isDiscourse, host); content.innerHTML = ""; const domainRow = document.createElement("div"); domainRow.className = "dnt-status-row"; const domainLabel = document.createElement("span"); domainLabel.className = "dnt-status-label"; domainLabel.textContent = t("settings.status.domain") + ":"; const domainValue = document.createElement("span"); domainValue.className = "dnt-status-value dnt-domain-text"; domainValue.textContent = host; domainRow.appendChild(domainLabel); domainRow.appendChild(domainValue); content.appendChild(domainRow); const statusRow = document.createElement("div"); statusRow.className = "dnt-status-row"; const statusLabel = document.createElement("span"); statusLabel.className = "dnt-status-label"; statusLabel.textContent = t(enable.enabled ? "settings.status.enabled" : "settings.status.disabled"); const reasonBadge = document.createElement("span"); reasonBadge.className = `dnt-badge dnt-badge-${enable.reason}`; reasonBadge.textContent = t(`settings.status.reason.${enable.reason}`); statusRow.appendChild(statusLabel); statusRow.appendChild(reasonBadge); content.appendChild(statusRow); } async function refreshStatusSection() { const content = document.getElementById(STATUS_CONTENT_ID); if (content) { await updateStatusContent(content); } } var STATUS_CONTENT_ID; var init_status = __esm({ "src/ui/sections/status.ts"() { init_domainLists(); init_siteDetector(); init_i18n(); STATUS_CONTENT_ID = "dnt-status-content"; } }); // src/ui/sections/domain.ts function renderDomainSection() { const section = document.createElement("div"); section.className = "dnt-section"; const title = document.createElement("h3"); title.className = "dnt-section-title"; title.textContent = t("settings.domain.title"); section.appendChild(title); const content = document.createElement("div"); content.className = "dnt-domain-content"; const whitelistBlock = createListBlock("whitelist"); content.appendChild(whitelistBlock); const blacklistBlock = createListBlock("blacklist"); content.appendChild(blacklistBlock); section.appendChild(content); return section; } function createListBlock(type) { const block = document.createElement("div"); block.className = "dnt-list-block"; const subtitle = document.createElement("h4"); subtitle.className = "dnt-list-subtitle"; subtitle.textContent = t(`settings.domain.${type}`); block.appendChild(subtitle); const list = document.createElement("div"); list.className = "dnt-domain-list"; list.id = `dnt-${type}`; block.appendChild(list); const inputRow = document.createElement("div"); inputRow.className = "dnt-input-row"; const input = document.createElement("input"); input.type = "text"; input.className = "dnt-input"; input.placeholder = t("settings.domain.placeholder"); inputRow.appendChild(input); const addBtn = document.createElement("button"); addBtn.className = "dnt-btn dnt-btn-primary"; addBtn.textContent = t("settings.domain.add"); addBtn.addEventListener("click", async () => { const domain = input.value.trim(); if (domain) { await handleAdd(type, domain); input.value = ""; } }); inputRow.appendChild(addBtn); block.appendChild(inputRow); const addCurrentBtn = document.createElement("button"); addCurrentBtn.className = "dnt-btn dnt-btn-secondary"; addCurrentBtn.textContent = t("settings.domain.addCurrent"); addCurrentBtn.addEventListener("click", () => { const host = getCurrentHostname(); handleAdd(type, host); }); block.appendChild(addCurrentBtn); refreshList(type); return block; } async function refreshList(type) { const lists = await getLists(); const domains = lists[type]; const container = document.getElementById(`dnt-${type}`); if (!container) return; container.innerHTML = ""; if (domains.length === 0) { const empty = document.createElement("div"); empty.className = "dnt-empty-text"; empty.textContent = t("settings.domain.empty"); container.appendChild(empty); return; } domains.forEach((domain) => { const item = document.createElement("div"); item.className = "dnt-domain-item"; const text = document.createElement("span"); text.className = "dnt-domain-text"; text.textContent = domain; item.appendChild(text); const deleteBtn = document.createElement("button"); deleteBtn.className = "dnt-btn dnt-btn-danger dnt-btn-sm"; deleteBtn.textContent = t("settings.domain.delete"); deleteBtn.addEventListener("click", () => handleDelete(type, domain)); item.appendChild(deleteBtn); container.appendChild(item); }); } async function handleAdd(type, domain) { if (!domain) return; const fn = type === "whitelist" ? addToWhitelist : addToBlacklist; const result = await fn(domain); if (result.added) { await refreshList(type); await refreshStatusSection(); } } async function handleDelete(type, domain) { const fn = type === "whitelist" ? removeFromWhitelist : removeFromBlacklist; const result = await fn(domain); if (result.removed) { await refreshList(type); await refreshStatusSection(); } } var init_domain = __esm({ "src/ui/sections/domain.ts"() { init_domainLists(); init_i18n(); init_status(); } }); // src/ui/sections/recognition.ts function renderRecognitionCategory() { const container = document.createElement("div"); container.className = "dnt-category-content"; container.appendChild(renderStatusSection()); container.appendChild(renderDomainSection()); return container; } var init_recognition = __esm({ "src/ui/sections/recognition.ts"() { init_status(); init_domain(); } }); // src/ui/sections/rules.ts function renderRulesSection() { const section = document.createElement("div"); section.className = "dnt-section"; const title = document.createElement("h3"); title.className = "dnt-section-title"; title.textContent = t("settings.rules.title"); section.appendChild(title); const content = document.createElement("div"); content.className = "dnt-rules-content"; (async () => { const flags = await getRuleFlags(); RULE_GROUPS.forEach((group) => { const groupBlock = document.createElement("div"); groupBlock.className = "dnt-rule-group"; const groupTitle = document.createElement("h4"); groupTitle.className = "dnt-rule-group-title"; groupTitle.textContent = t(group.title); groupBlock.appendChild(groupTitle); group.rules.forEach((rule) => { const ruleItem = createRuleItem(rule.id, t(rule.label), flags[rule.id] ?? true); groupBlock.appendChild(ruleItem); }); content.appendChild(groupBlock); }); })(); section.appendChild(content); return section; } function createRuleItem(ruleId, label, enabled) { const item = document.createElement("div"); item.className = "dnt-rule-item"; const labelEl = document.createElement("label"); labelEl.className = "dnt-rule-label"; labelEl.textContent = label; const toggle = createToggle(ruleId, enabled); item.appendChild(labelEl); item.appendChild(toggle); return item; } function createToggle(ruleId, enabled) { const toggle = document.createElement("div"); toggle.className = `dnt-toggle ${enabled ? "dnt-toggle-on" : "dnt-toggle-off"}`; toggle.setAttribute("data-rule-id", ruleId); const track = document.createElement("div"); track.className = "dnt-toggle-track"; const thumb = document.createElement("div"); thumb.className = "dnt-toggle-thumb"; track.appendChild(thumb); toggle.appendChild(track); toggle.addEventListener("click", async () => { const currentState = toggle.classList.contains("dnt-toggle-on"); const newState = !currentState; await setRuleEnabled(ruleId, newState); toggle.classList.remove("dnt-toggle-on", "dnt-toggle-off"); toggle.classList.add(newState ? "dnt-toggle-on" : "dnt-toggle-off"); }); return toggle; } var RULE_GROUPS; var init_rules = __esm({ "src/ui/sections/rules.ts"() { init_settings2(); init_settings2(); init_i18n(); RULE_GROUPS = [ { title: "settings.rules.topic.title", rules: [ { id: RULE_TOPIC_OPEN_NEW_TAB, label: "settings.rules.topic.openNewTab" }, { id: RULE_TOPIC_IN_TOPIC_OPEN_OTHER, label: "settings.rules.topic.inTopicOpenOther" }, { id: RULE_TOPIC_SAME_TOPIC_KEEP_NATIVE, label: "settings.rules.topic.sameTopicKeepNative" } ] }, { title: "settings.rules.user.title", rules: [ { id: RULE_USER_OPEN_NEW_TAB, label: "settings.rules.user.openNewTab" }, { id: RULE_USER_IN_PROFILE_OPEN_OTHER, label: "settings.rules.user.inProfileOpenOther" }, { id: RULE_USER_SAME_PROFILE_KEEP_NATIVE, label: "settings.rules.user.sameProfileKeepNative" } ] }, { title: "settings.rules.attachment.title", rules: [{ id: RULE_ATTACHMENT_KEEP_NATIVE, label: "settings.rules.attachment.keepNative" }] }, { title: "settings.rules.popup.title", rules: [ { id: RULE_POPUP_USER_CARD, label: "settings.rules.popup.userCard" }, { id: RULE_POPUP_USER_MENU, label: "settings.rules.popup.userMenu" }, { id: RULE_POPUP_SEARCH_MENU, label: "settings.rules.popup.searchMenu" } ] }, { title: "settings.rules.sidebar.title", rules: [ { id: RULE_SIDEBAR_NON_TOPIC_KEEP_NATIVE, label: "settings.rules.sidebar.nonTopicKeepNative" }, { id: RULE_SIDEBAR_IN_TOPIC_NEW_TAB, label: "settings.rules.sidebar.inTopicNewTab" } ] } ]; } }); // src/ui/sections/open.ts function renderOpenSection() { const section = document.createElement("div"); section.className = "dnt-section"; const title = document.createElement("h3"); title.className = "dnt-section-title"; title.textContent = t("settings.openMode.title"); section.appendChild(title); const infoBox = createInfoBox(t("settings.openMode.description")); section.appendChild(infoBox); const modeBlock = document.createElement("div"); modeBlock.className = "dnt-list-block"; modeBlock.style.marginTop = "16px"; const modeLabel = document.createElement("div"); modeLabel.className = "dnt-list-subtitle"; modeLabel.textContent = t("settings.openMode.selectLabel"); modeBlock.appendChild(modeLabel); const segmentedControl = createSegmentedControl(); modeBlock.appendChild(segmentedControl); section.appendChild(modeBlock); const floatballTitle = document.createElement("h3"); floatballTitle.className = "dnt-section-title"; floatballTitle.textContent = t("settings.openMode.floatball.title"); floatballTitle.style.marginTop = "32px"; section.appendChild(floatballTitle); const floatballTip = createInfoBox(t("settings.openMode.floatball.tip")); section.appendChild(floatballTip); const displayBlock = document.createElement("div"); displayBlock.className = "dnt-list-block"; displayBlock.style.marginTop = "16px"; const displayTitle = document.createElement("div"); displayTitle.className = "dnt-list-subtitle"; displayTitle.textContent = t("settings.openMode.floatball.displayTitle"); displayBlock.appendChild(displayTitle); const showRow = createToggleRow( t("settings.openMode.floatball.show"), t("settings.openMode.floatball.showDesc"), false, async (on) => { await setFloatBallShown(on); } ); displayBlock.appendChild(showRow.row); const fixedRow = createToggleRow( t("settings.openMode.floatball.fixed"), t("settings.openMode.floatball.fixedDesc"), false, async (on) => { await setFloatBallFixedMode(on); } ); displayBlock.appendChild(fixedRow.row); const resetRow = document.createElement("div"); resetRow.style.marginTop = "12px"; const resetBtn = document.createElement("button"); resetBtn.className = "dnt-btn dnt-btn-secondary"; resetBtn.textContent = t("settings.openMode.floatball.reset"); resetBtn.addEventListener("click", async () => { await resetFloatBallPosition(); }); resetRow.appendChild(resetBtn); displayBlock.appendChild(resetRow); section.appendChild(displayBlock); const switchBlock = document.createElement("div"); switchBlock.className = "dnt-list-block"; switchBlock.style.marginTop = "16px"; const switchTitle = document.createElement("div"); switchTitle.className = "dnt-list-subtitle"; switchTitle.textContent = t("settings.openMode.floatball.switchTitle"); switchBlock.appendChild(switchTitle); const modesLabel = document.createElement("div"); modesLabel.className = "dnt-subsection-label"; modesLabel.textContent = t("settings.openMode.floatball.modes"); modesLabel.style.marginTop = "12px"; modesLabel.style.marginBottom = "8px"; switchBlock.appendChild(modesLabel); const modesDesc = document.createElement("div"); modesDesc.className = "dnt-hint-text"; modesDesc.textContent = t("settings.openMode.floatball.modesDesc"); modesDesc.style.marginBottom = "12px"; switchBlock.appendChild(modesDesc); const modeCards = createModeCardSelector(); switchBlock.appendChild(modeCards.container); section.appendChild(switchBlock); (async () => { const mode = await getBackgroundOpenMode(); const enabled = await getFloatBallEnabled(); const fixed2 = await getFloatBallFixed(); const allowed2 = await getAllowedModes(); setSegmentedValue(segmentedControl, mode); setToggleVisual(showRow.toggle, enabled); setToggleVisual(fixedRow.toggle, fixed2); setModeCardsValue(modeCards, allowed2); })(); return section; } function createInfoBox(text) { const box = document.createElement("div"); box.className = "dnt-info-box"; const icon = document.createElement("span"); icon.className = "dnt-info-icon"; icon.innerHTML = ` `; box.appendChild(icon); const textEl = document.createElement("span"); textEl.className = "dnt-info-text"; textEl.textContent = text; box.appendChild(textEl); return box; } function createSegmentedControl() { const container = document.createElement("div"); container.className = "dnt-segmented-control"; container.setAttribute("role", "radiogroup"); container.setAttribute("aria-label", t("settings.openMode.selectLabel")); const modes = ["none", "topic", "all"]; modes.forEach((mode, index) => { const button = document.createElement("button"); button.className = "dnt-segment-btn"; button.setAttribute("role", "radio"); button.setAttribute("aria-checked", "false"); button.setAttribute("data-mode", mode); const label = document.createElement("span"); label.className = "dnt-segment-label"; label.textContent = t(`settings.openMode.options.${mode}`); button.appendChild(label); const desc = document.createElement("span"); desc.className = "dnt-segment-desc"; desc.textContent = t(`settings.openMode.optionDesc.${mode}`); button.appendChild(desc); button.addEventListener("click", async () => { await setBackgroundOpenMode(mode); await syncCurrentModeFromStorage(); await logBgModeChange(mode, "settings"); setSegmentedValue(container, mode); }); container.appendChild(button); }); return container; } function setSegmentedValue(container, mode) { const buttons = container.querySelectorAll(".dnt-segment-btn"); buttons.forEach((btn) => { const isActive = btn.getAttribute("data-mode") === mode; if (isActive) { btn.classList.add("dnt-segment-active"); btn.setAttribute("aria-checked", "true"); } else { btn.classList.remove("dnt-segment-active"); btn.setAttribute("aria-checked", "false"); } }); } function createToggleRow(label, description, initial, onChange) { const row = document.createElement("div"); row.className = "dnt-toggle-row"; const labelWrap = document.createElement("div"); labelWrap.className = "dnt-toggle-label-wrap"; const labelEl = document.createElement("div"); labelEl.className = "dnt-toggle-label"; labelEl.textContent = label; labelWrap.appendChild(labelEl); const descEl = document.createElement("div"); descEl.className = "dnt-toggle-desc"; descEl.textContent = description; labelWrap.appendChild(descEl); row.appendChild(labelWrap); const toggle = createToggle2(initial, onChange); row.appendChild(toggle); return { row, toggle }; } function createToggle2(initial, onChange) { const toggle = document.createElement("div"); toggle.className = `dnt-toggle ${initial ? "dnt-toggle-on" : "dnt-toggle-off"}`; toggle.setAttribute("role", "switch"); toggle.setAttribute("aria-checked", initial ? "true" : "false"); const track = document.createElement("div"); track.className = "dnt-toggle-track"; const thumb = document.createElement("div"); thumb.className = "dnt-toggle-thumb"; track.appendChild(thumb); toggle.appendChild(track); toggle.addEventListener("click", async () => { const current = toggle.classList.contains("dnt-toggle-on"); const next = !current; await onChange(next); setToggleVisual(toggle, next); }); return toggle; } function setToggleVisual(el, on) { el.classList.remove("dnt-toggle-on", "dnt-toggle-off"); el.classList.add(on ? "dnt-toggle-on" : "dnt-toggle-off"); el.setAttribute("aria-checked", on ? "true" : "false"); } function createModeCardSelector() { const container = document.createElement("div"); container.className = "dnt-mode-cards"; container.setAttribute("role", "group"); container.setAttribute("aria-label", t("settings.openMode.floatball.modes")); const modes = ["none", "topic", "all"]; const cards = []; modes.forEach((mode) => { const card = document.createElement("label"); card.className = "dnt-mode-card"; card.setAttribute("data-mode", mode); const checkbox = document.createElement("input"); checkbox.type = "checkbox"; checkbox.className = "dnt-mode-card-checkbox"; checkbox.setAttribute("data-mode", mode); card.appendChild(checkbox); const content = document.createElement("div"); content.className = "dnt-mode-card-content"; const icon = document.createElement("div"); icon.className = "dnt-mode-card-icon"; icon.innerHTML = getModeIcon(mode); content.appendChild(icon); const label = document.createElement("div"); label.className = "dnt-mode-card-label"; label.textContent = t(`settings.openMode.options.${mode}`); content.appendChild(label); const desc = document.createElement("div"); desc.className = "dnt-mode-card-desc"; desc.textContent = t(`settings.openMode.optionDesc.${mode}`); content.appendChild(desc); const checkmark = document.createElement("div"); checkmark.className = "dnt-mode-card-checkmark"; checkmark.innerHTML = ` `; content.appendChild(checkmark); card.appendChild(content); container.appendChild(card); cards.push({ el: card, mode, checkbox }); }); const updateCards = async () => { const selected = cards.filter((c) => c.checkbox.checked); const count = selected.length; cards.forEach((c) => { const isLastTwo = count === 2 && c.checkbox.checked; if (isLastTwo) { c.el.classList.add("dnt-mode-card-min-required"); c.el.title = t("settings.openMode.floatball.modesDesc"); } else { c.el.classList.remove("dnt-mode-card-min-required"); c.el.title = ""; } if (c.checkbox.checked) { c.el.classList.add("dnt-mode-card-checked"); } else { c.el.classList.remove("dnt-mode-card-checked"); } }); const allowedModes = { none: cards.find((c) => c.mode === "none").checkbox.checked, topic: cards.find((c) => c.mode === "topic").checkbox.checked, all: cards.find((c) => c.mode === "all").checkbox.checked }; await setAllowedModes(allowedModes); await updateAllowedModes(allowedModes); }; cards.forEach((c) => { c.checkbox.addEventListener("change", async (e) => { const selected = cards.filter((card) => card.checkbox.checked); if (!c.checkbox.checked && selected.length < 2) { c.checkbox.checked = true; e.preventDefault(); return; } await updateCards(); }); }); return { container, cards }; } function setModeCardsValue(modeCards, allowed2) { modeCards.cards.forEach((c) => { c.checkbox.checked = allowed2[c.mode]; if (c.checkbox.checked) { c.el.classList.add("dnt-mode-card-checked"); } else { c.el.classList.remove("dnt-mode-card-checked"); } }); const selected = modeCards.cards.filter((c) => c.checkbox.checked); if (selected.length === 2) { selected.forEach((c) => { c.el.classList.add("dnt-mode-card-min-required"); c.el.title = t("settings.openMode.floatball.modesDesc"); }); } } function getModeIcon(mode) { const icons = { none: ` `, topic: ` `, all: ` ` }; return icons[mode]; } var init_open = __esm({ "src/ui/sections/open.ts"() { init_i18n(); init_openMode(); init_floatBall(); init_floatball(); init_logger(); } }); // src/ui/sections/debug.ts function renderDebugSection() { const section = document.createElement("div"); section.className = "dnt-section"; const title = document.createElement("h3"); title.className = "dnt-section-title"; title.textContent = t("settings.debug.title"); section.appendChild(title); const content = document.createElement("div"); content.className = "dnt-rules-content"; const mainRow = document.createElement("div"); mainRow.className = "dnt-rule-item"; const mainLabel = document.createElement("label"); mainLabel.className = "dnt-rule-label"; mainLabel.textContent = t("settings.debug.enable"); const mainToggle = createToggle3(false, async (on) => { await setDebugEnabled(on); detailsBlock.style.display = on ? "" : "none"; }); mainToggle.id = "dnt-debug-main-toggle"; mainRow.appendChild(mainLabel); mainRow.appendChild(mainToggle); content.appendChild(mainRow); const detailsBlock = document.createElement("div"); detailsBlock.style.marginTop = "8px"; const opsRow = document.createElement("div"); opsRow.className = "dnt-input-row"; const allOn = document.createElement("button"); allOn.className = "dnt-btn dnt-btn-secondary"; allOn.textContent = t("settings.debug.allOn"); allOn.addEventListener("click", async () => { await setAllDebugCategories(true); refreshDetailToggles(detailsBlock); }); const allOff = document.createElement("button"); allOff.className = "dnt-btn dnt-btn-secondary"; allOff.textContent = t("settings.debug.allOff"); allOff.addEventListener("click", async () => { await setAllDebugCategories(false); refreshDetailToggles(detailsBlock); }); opsRow.appendChild(allOn); opsRow.appendChild(allOff); detailsBlock.appendChild(opsRow); const cats = [ { key: "site", label: t("settings.debug.categories.site") }, { key: "click", label: t("settings.debug.categories.click") }, { key: "link", label: t("settings.debug.categories.link") }, { key: "rules", label: t("settings.debug.categories.rules") }, { key: "final", label: t("settings.debug.categories.final") }, { key: "bg", label: t("settings.debug.categories.bg") } ]; const listBlock = document.createElement("div"); listBlock.className = "dnt-rule-group"; cats.forEach((c) => { const row = document.createElement("div"); row.className = "dnt-rule-item"; const l = document.createElement("label"); l.className = "dnt-rule-label"; l.textContent = c.label; const toggle = createToggle3(true, async (on) => { await setDebugCategory(c.key, on); }); toggle.setAttribute("data-debug-cat", c.key); row.appendChild(l); row.appendChild(toggle); listBlock.appendChild(row); }); detailsBlock.appendChild(listBlock); content.appendChild(detailsBlock); (async () => { const on = await getDebugEnabled(); setToggleVisual2(mainToggle, on); detailsBlock.style.display = on ? "" : "none"; await refreshDetailToggles(detailsBlock); })(); section.appendChild(content); return section; } async function refreshDetailToggles(container) { const cats = await getDebugCategories(); container.querySelectorAll("[data-debug-cat]").forEach((el) => { const key = el.getAttribute("data-debug-cat"); const on = cats[key] ?? true; setToggleVisual2(el, on); }); } function createToggle3(initial, onChange) { const toggle = document.createElement("div"); toggle.className = `dnt-toggle ${initial ? "dnt-toggle-on" : "dnt-toggle-off"}`; const track = document.createElement("div"); track.className = "dnt-toggle-track"; const thumb = document.createElement("div"); thumb.className = "dnt-toggle-thumb"; track.appendChild(thumb); toggle.appendChild(track); toggle.addEventListener("click", async () => { const current = toggle.classList.contains("dnt-toggle-on"); const next = !current; await onChange(next); setToggleVisual2(toggle, next); }); return toggle; } function setToggleVisual2(el, on) { el.classList.remove("dnt-toggle-on", "dnt-toggle-off"); el.classList.add(on ? "dnt-toggle-on" : "dnt-toggle-off"); } var init_debug = __esm({ "src/ui/sections/debug.ts"() { init_i18n(); init_settings(); } }); // src/ui/panel.ts function createSettingsPanel() { const overlay = document.createElement("div"); overlay.id = "dnt-settings-overlay"; overlay.className = "dnt-overlay"; const dialog = document.createElement("div"); dialog.className = "dnt-dialog"; const header = createHeader(); dialog.appendChild(header); const body = document.createElement("div"); body.className = "dnt-body"; const sidebar = createSidebar(); body.appendChild(sidebar); const contentArea = document.createElement("div"); contentArea.className = "dnt-content-area"; contentArea.id = "dnt-content-area"; const defaultRenderer = categoryRenderers["recognition"]; if (defaultRenderer) { contentArea.appendChild(defaultRenderer()); } body.appendChild(contentArea); dialog.appendChild(body); overlay.appendChild(dialog); overlay.addEventListener("click", (e) => { if (e.target === overlay) { closeSettings(); } }); return overlay; } function createHeader() { const header = document.createElement("div"); header.className = "dnt-header"; const title = document.createElement("h2"); title.className = "dnt-title"; title.textContent = t("settings.title"); header.appendChild(title); const controls = document.createElement("div"); controls.className = "dnt-controls"; const themeBtn = document.createElement("button"); themeBtn.className = "dnt-icon-btn"; themeBtn.title = t(`settings.theme.${getTheme()}`); themeBtn.innerHTML = ThemeIcon[getTheme()]; themeBtn.addEventListener("click", () => { toggleTheme(); themeBtn.innerHTML = ThemeIcon[getTheme()]; themeBtn.title = t(`settings.theme.${getTheme()}`); }); controls.appendChild(themeBtn); const langBtn = document.createElement("button"); langBtn.className = "dnt-icon-btn"; langBtn.title = t(`settings.language.${getLanguage()}`); langBtn.innerHTML = LanguageIcon[getLanguage()]; langBtn.addEventListener("click", () => { toggleLanguage(); langBtn.innerHTML = LanguageIcon[getLanguage()]; closeSettings(); Promise.resolve().then(() => (init_settings3(), settings_exports)).then(({ openSettings: openSettings2 }) => openSettings2()); }); controls.appendChild(langBtn); const closeBtn = document.createElement("button"); closeBtn.className = "dnt-icon-btn"; closeBtn.title = t("settings.close"); closeBtn.innerHTML = ` `; closeBtn.addEventListener("click", closeSettings); controls.appendChild(closeBtn); header.appendChild(controls); return header; } function createSidebar() { const sidebar = document.createElement("div"); sidebar.className = "dnt-sidebar"; CATEGORIES.forEach((category, index) => { const btn = document.createElement("button"); btn.className = "dnt-category-btn"; btn.setAttribute("data-category", category.id); if (index === 0) { btn.classList.add("dnt-category-active"); } const icon = document.createElement("span"); icon.className = "dnt-category-icon"; icon.innerHTML = category.icon; btn.appendChild(icon); const label = document.createElement("span"); label.className = "dnt-category-label"; label.textContent = t(category.labelKey); btn.appendChild(label); btn.addEventListener("click", () => { switchCategory(category.id); }); sidebar.appendChild(btn); }); return sidebar; } function switchCategory(categoryId) { const buttons = document.querySelectorAll(".dnt-category-btn"); buttons.forEach((btn) => { if (btn.getAttribute("data-category") === categoryId) { btn.classList.add("dnt-category-active"); } else { btn.classList.remove("dnt-category-active"); } }); const contentArea = document.getElementById("dnt-content-area"); if (contentArea) { contentArea.innerHTML = ""; const renderer = categoryRenderers[categoryId]; if (renderer) { contentArea.appendChild(renderer()); } } } var categoryRenderers; var init_panel = __esm({ "src/ui/panel.ts"() { init_settings3(); init_theme(); init_i18n(); init_i18n(); init_categories(); init_recognition(); init_rules(); init_open(); init_debug(); categoryRenderers = { recognition: renderRecognitionCategory, rules: renderRulesSection, open: renderOpenSection, debug: renderDebugSection }; } }); // src/ui/styles.css var styles_default; var init_styles = __esm({ "src/ui/styles.css"() { styles_default = `/* \u8BBE\u7F6E\u754C\u9762\u6837\u5F0F - \u7B80\u7EA6\u8BBE\u8BA1 */\r \r /* CSS\u53D8\u91CF - \u65E5\u95F4\u4E3B\u9898 */\r :root[data-dnt-theme="light"] {\r --dnt-bg-overlay: rgba(0, 0, 0, 0.5);\r --dnt-bg-dialog: #ffffff;\r --dnt-bg-section: #f8f9fa;\r --dnt-bg-input: #ffffff;\r --dnt-bg-hover: #f0f0f0;\r \r --dnt-text-primary: #2c3e50;\r --dnt-text-secondary: #6c757d;\r --dnt-text-muted: #999999;\r \r --dnt-border: #e1e4e8;\r --dnt-border-focus: #67c23a;\r \r --dnt-primary: #67c23a;\r --dnt-primary-hover: #85ce61;\r --dnt-danger: #f56c6c;\r --dnt-danger-hover: #f78989;\r \r --dnt-badge-auto: #409eff;\r --dnt-badge-whitelist: #67c23a;\r --dnt-badge-blacklist: #f56c6c;\r --dnt-badge-disabled: #909399;\r \r --dnt-toggle-on: #67c23a;\r --dnt-toggle-off: #dcdfe6;\r --dnt-toggle-thumb: #ffffff;\r \r --dnt-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);\r }\r \r /* CSS\u53D8\u91CF - \u591C\u95F4\u4E3B\u9898 */\r :root[data-dnt-theme="dark"] {\r --dnt-bg-overlay: rgba(0, 0, 0, 0.7);\r --dnt-bg-dialog: #1e1e1e;\r --dnt-bg-section: #2a2a2a;\r --dnt-bg-input: #363636;\r --dnt-bg-hover: #3a3a3a;\r \r --dnt-text-primary: #e4e4e4;\r --dnt-text-secondary: #b0b0b0;\r --dnt-text-muted: #888888;\r \r --dnt-border: #404040;\r --dnt-border-focus: #4a9e2a;\r \r --dnt-primary: #4a9e2a;\r --dnt-primary-hover: #5fb83a;\r --dnt-danger: #d85a5a;\r --dnt-danger-hover: #e67272;\r \r --dnt-badge-auto: #3b7fb8;\r --dnt-badge-whitelist: #4a9e2a;\r --dnt-badge-blacklist: #d85a5a;\r --dnt-badge-disabled: #909399;\r \r --dnt-toggle-on: #4a9e2a;\r --dnt-toggle-off: #4a4a4a;\r --dnt-toggle-thumb: #ffffff;\r \r --dnt-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\r }\r \r /* \u91CD\u7F6E\u6837\u5F0F */\r .dnt-overlay *,\r .dnt-overlay *::before,\r .dnt-overlay *::after {\r box-sizing: border-box;\r margin: 0;\r padding: 0;\r }\r \r /* \u906E\u7F69\u5C42 */\r .dnt-overlay {\r position: fixed;\r top: 0;\r left: 0;\r right: 0;\r bottom: 0;\r background: var(--dnt-bg-overlay);\r display: flex;\r align-items: center;\r justify-content: center;\r z-index: 999999;\r font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\r font-size: 14px;\r line-height: 1.6;\r }\r \r /* \u5BF9\u8BDD\u6846 */\r .dnt-dialog {\r background: var(--dnt-bg-dialog);\r border-radius: 8px;\r box-shadow: var(--dnt-shadow);\r width: 90%;\r max-width: 920px;\r height: 85vh;\r max-height: 680px;\r display: flex;\r flex-direction: column;\r overflow: hidden;\r }\r \r /* \u5934\u90E8 */\r .dnt-header {\r display: flex;\r align-items: center;\r justify-content: space-between;\r padding: 20px 24px;\r border-bottom: 1px solid var(--dnt-border);\r }\r \r .dnt-title {\r font-size: 20px;\r font-weight: 600;\r color: var(--dnt-text-primary);\r }\r \r .dnt-controls {\r display: flex;\r gap: 8px;\r }\r \r .dnt-icon-btn {\r width: 36px;\r height: 36px;\r border: none;\r background: transparent;\r color: var(--dnt-text-secondary);\r cursor: pointer;\r border-radius: 4px;\r display: flex;\r align-items: center;\r justify-content: center;\r transition: all 0.2s;\r }\r \r .dnt-icon-btn:hover {\r background: var(--dnt-bg-hover);\r color: var(--dnt-text-primary);\r }\r \r /* \u4E3B\u4F53\u533A\u57DF - \u5DE6\u53F3\u5206\u680F */\r .dnt-body {\r flex: 1;\r display: flex;\r overflow: hidden;\r min-height: 0;\r }\r \r /* \u5DE6\u4FA7\u5BFC\u822A\u680F */\r .dnt-sidebar {\r width: 160px;\r flex-shrink: 0;\r background: var(--dnt-bg-section);\r border-right: 1px solid var(--dnt-border);\r display: flex;\r flex-direction: column;\r padding: 12px 0;\r overflow-y: auto;\r }\r \r .dnt-sidebar::-webkit-scrollbar {\r width: 4px;\r }\r \r .dnt-sidebar::-webkit-scrollbar-thumb {\r background: var(--dnt-border);\r border-radius: 2px;\r }\r \r /* \u5206\u7C7B\u6309\u94AE */\r .dnt-category-btn {\r display: flex;\r align-items: center;\r gap: 10px;\r padding: 12px 16px;\r border: none;\r background: transparent;\r color: var(--dnt-text-secondary);\r cursor: pointer;\r font-size: 14px;\r text-align: left;\r transition: all 0.2s;\r border-left: 3px solid transparent;\r }\r \r .dnt-category-btn:hover {\r background: var(--dnt-bg-hover);\r color: var(--dnt-text-primary);\r }\r \r .dnt-category-active {\r background: var(--dnt-bg-dialog);\r color: var(--dnt-primary);\r border-left-color: var(--dnt-primary);\r font-weight: 600;\r }\r \r .dnt-category-icon {\r display: flex;\r align-items: center;\r justify-content: center;\r flex-shrink: 0;\r }\r \r .dnt-category-label {\r flex: 1;\r }\r \r /* \u53F3\u4FA7\u5185\u5BB9\u533A\u57DF */\r .dnt-content-area {\r flex: 1;\r overflow-y: auto;\r padding: 20px 24px;\r min-width: 0;\r }\r \r .dnt-content-area::-webkit-scrollbar {\r width: 8px;\r }\r \r .dnt-content-area::-webkit-scrollbar-track {\r background: transparent;\r }\r \r .dnt-content-area::-webkit-scrollbar-thumb {\r background: var(--dnt-border);\r border-radius: 4px;\r }\r \r .dnt-content-area::-webkit-scrollbar-thumb:hover {\r background: var(--dnt-text-muted);\r }\r \r /* \u5206\u7C7B\u5185\u5BB9\u5BB9\u5668 */\r .dnt-category-content {\r display: flex;\r flex-direction: column;\r gap: 24px;\r }\r \r /* \u5185\u5BB9\u533A(\u65E7\u7248,\u4FDD\u7559\u517C\u5BB9) */\r .dnt-content {\r flex: 1;\r overflow-y: auto;\r padding: 20px 24px;\r }\r \r .dnt-content::-webkit-scrollbar {\r width: 8px;\r }\r \r .dnt-content::-webkit-scrollbar-track {\r background: transparent;\r }\r \r .dnt-content::-webkit-scrollbar-thumb {\r background: var(--dnt-border);\r border-radius: 4px;\r }\r \r .dnt-content::-webkit-scrollbar-thumb:hover {\r background: var(--dnt-text-muted);\r }\r \r /* \u533A\u5757 */\r .dnt-section {\r margin-bottom: 24px;\r }\r \r .dnt-section:last-child {\r margin-bottom: 0;\r }\r \r .dnt-section-title {\r font-size: 16px;\r font-weight: 600;\r color: var(--dnt-text-primary);\r margin-bottom: 12px;\r padding-bottom: 8px;\r border-bottom: 2px solid var(--dnt-primary);\r }\r \r /* \u72B6\u6001\u533A\u57DF */\r .dnt-status-content {\r background: var(--dnt-bg-section);\r border-radius: 6px;\r padding: 16px;\r }\r \r .dnt-status-row {\r display: flex;\r align-items: center;\r gap: 12px;\r margin-bottom: 8px;\r }\r \r .dnt-status-row:last-child {\r margin-bottom: 0;\r }\r \r .dnt-status-label {\r color: var(--dnt-text-primary);\r font-weight: 500;\r }\r \r .dnt-status-value {\r color: var(--dnt-text-secondary);\r }\r \r .dnt-domain-text {\r font-family: "Consolas", "Monaco", monospace;\r font-size: 13px;\r }\r \r /* \u5FBD\u7AE0 */\r .dnt-badge {\r display: inline-block;\r padding: 2px 10px;\r border-radius: 12px;\r font-size: 12px;\r font-weight: 500;\r color: #ffffff;\r }\r \r .dnt-badge-auto {\r background: var(--dnt-badge-auto);\r }\r \r .dnt-badge-whitelist {\r background: var(--dnt-badge-whitelist);\r }\r \r .dnt-badge-blacklist {\r background: var(--dnt-badge-blacklist);\r }\r \r .dnt-badge-disabled {\r background: var(--dnt-badge-disabled);\r }\r \r /* \u57DF\u540D\u7BA1\u7406\u533A\u57DF */\r .dnt-domain-content {\r display: flex;\r flex-direction: column;\r gap: 20px;\r }\r \r .dnt-list-block {\r background: var(--dnt-bg-section);\r border-radius: 6px;\r padding: 16px;\r }\r \r .dnt-list-subtitle {\r font-size: 14px;\r font-weight: 600;\r color: var(--dnt-text-primary);\r margin-bottom: 12px;\r }\r \r .dnt-domain-list {\r margin-bottom: 12px;\r min-height: 40px;\r max-height: 180px;\r overflow-y: auto;\r }\r \r .dnt-domain-list::-webkit-scrollbar {\r width: 6px;\r }\r \r .dnt-domain-list::-webkit-scrollbar-thumb {\r background: var(--dnt-border);\r border-radius: 3px;\r }\r \r .dnt-domain-item {\r display: flex;\r align-items: center;\r justify-content: space-between;\r padding: 8px 12px;\r background: var(--dnt-bg-input);\r border: 1px solid var(--dnt-border);\r border-radius: 4px;\r margin-bottom: 8px;\r }\r \r .dnt-domain-item:last-child {\r margin-bottom: 0;\r }\r \r .dnt-empty-text {\r color: var(--dnt-text-muted);\r font-size: 13px;\r text-align: center;\r padding: 20px;\r }\r \r /* \u8F93\u5165\u6846 */\r .dnt-input-row {\r display: flex;\r gap: 8px;\r margin-bottom: 8px;\r }\r \r .dnt-input {\r flex: 1;\r padding: 8px 12px;\r border: 1px solid var(--dnt-border);\r border-radius: 4px;\r background: var(--dnt-bg-input);\r color: var(--dnt-text-primary);\r font-size: 14px;\r outline: none;\r transition: border-color 0.2s;\r }\r \r .dnt-input:focus {\r border-color: var(--dnt-border-focus);\r }\r \r .dnt-input::placeholder {\r color: var(--dnt-text-muted);\r }\r \r /* \u6309\u94AE */\r .dnt-btn {\r padding: 8px 16px;\r border: none;\r border-radius: 4px;\r font-size: 14px;\r font-weight: 500;\r cursor: pointer;\r transition: all 0.2s;\r outline: none;\r }\r \r .dnt-btn-primary {\r background: var(--dnt-primary);\r color: #ffffff;\r }\r \r .dnt-btn-primary:hover {\r background: var(--dnt-primary-hover);\r }\r \r .dnt-btn-secondary {\r background: var(--dnt-bg-input);\r color: var(--dnt-text-primary);\r border: 1px solid var(--dnt-border);\r width: 100%;\r }\r \r .dnt-btn-secondary:hover {\r background: var(--dnt-bg-hover);\r }\r \r .dnt-btn-danger {\r background: var(--dnt-danger);\r color: #ffffff;\r }\r \r .dnt-btn-danger:hover {\r background: var(--dnt-danger-hover);\r }\r \r .dnt-btn-sm {\r padding: 4px 12px;\r font-size: 13px;\r }\r \r /* \u89C4\u5219\u533A\u57DF */\r .dnt-rules-content {\r display: flex;\r flex-direction: column;\r gap: 16px;\r }\r \r .dnt-rule-group {\r background: var(--dnt-bg-section);\r border-radius: 6px;\r padding: 16px;\r }\r \r .dnt-rule-group-title {\r font-size: 14px;\r font-weight: 600;\r color: var(--dnt-text-primary);\r margin-bottom: 12px;\r }\r \r .dnt-rule-item {\r display: flex;\r align-items: center;\r justify-content: space-between;\r padding: 10px 0;\r border-bottom: 1px solid var(--dnt-border);\r }\r \r .dnt-rule-item:last-child {\r border-bottom: none;\r padding-bottom: 0;\r }\r \r .dnt-rule-label {\r color: var(--dnt-text-primary);\r font-size: 14px;\r flex: 1;\r cursor: default;\r }\r \r /* \u5F00\u5173 */\r .dnt-toggle {\r width: 44px;\r height: 24px;\r border-radius: 12px;\r cursor: pointer;\r position: relative;\r transition: background-color 0.3s;\r }\r \r .dnt-toggle-on {\r background: var(--dnt-toggle-on);\r }\r \r .dnt-toggle-off {\r background: var(--dnt-toggle-off);\r }\r \r .dnt-toggle-track {\r width: 100%;\r height: 100%;\r position: relative;\r }\r \r .dnt-toggle-thumb {\r width: 20px;\r height: 20px;\r border-radius: 50%;\r background: var(--dnt-toggle-thumb);\r position: absolute;\r top: 2px;\r transition: left 0.3s;\r box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\r }\r \r .dnt-toggle-on .dnt-toggle-thumb {\r left: 22px;\r }\r \r .dnt-toggle-off .dnt-toggle-thumb {\r left: 2px;\r }\r \r /* \u54CD\u5E94\u5F0F */\r @media (max-width: 768px) {\r .dnt-dialog {\r width: 95%;\r height: 90vh;\r max-height: none;\r }\r \r .dnt-header,\r .dnt-content,\r .dnt-content-area {\r padding: 16px;\r }\r \r .dnt-title {\r font-size: 18px;\r }\r \r /* \u5C0F\u5C4F\u5E55\u4E0B\u4FA7\u8FB9\u680F\u6536\u7A84 */\r .dnt-sidebar {\r width: 120px;\r }\r \r .dnt-category-btn {\r padding: 10px 12px;\r font-size: 13px;\r }\r }\r \r @media (max-width: 560px) {\r .dnt-dialog {\r width: 100%;\r height: 100vh;\r max-height: none;\r border-radius: 0;\r }\r \r /* \u8D85\u5C0F\u5C4F\u5E55\u4E0B\u9690\u85CF\u56FE\u6807\u6587\u5B57 */\r .dnt-sidebar {\r width: 60px;\r }\r \r .dnt-category-btn {\r padding: 12px;\r justify-content: center;\r }\r \r .dnt-category-label {\r display: none;\r }\r \r .dnt-content-area {\r padding: 12px;\r }\r \r /* \u79FB\u52A8\u7AEF\u5361\u7247\u8C03\u6574 */\r .dnt-mode-cards {\r flex-direction: column;\r }\r \r .dnt-mode-card {\r width: 100%;\r }\r \r .dnt-segmented-control {\r flex-direction: column;\r }\r \r .dnt-segment-btn {\r width: 100%;\r }\r }\r \r /* ========== \u65B0\u589E: \u540E\u53F0\u6253\u5F00UI\u7EC4\u4EF6\u6837\u5F0F ========== */\r \r /* \u4FE1\u606F\u63D0\u793A\u6846 */\r .dnt-info-box {\r display: flex;\r align-items: flex-start;\r gap: 10px;\r padding: 12px 14px;\r background: var(--dnt-bg-section);\r border-left: 3px solid var(--dnt-primary);\r border-radius: 4px;\r margin-bottom: 16px;\r }\r \r .dnt-info-icon {\r flex-shrink: 0;\r color: var(--dnt-primary);\r display: flex;\r align-items: center;\r justify-content: center;\r margin-top: 2px;\r }\r \r .dnt-info-text {\r flex: 1;\r color: var(--dnt-text-secondary);\r font-size: 13px;\r line-height: 1.6;\r }\r \r /* \u5206\u6BB5\u63A7\u5236\u5668 */\r .dnt-segmented-control {\r display: flex;\r gap: 0;\r background: var(--dnt-bg-section);\r border: 1px solid var(--dnt-border);\r border-radius: 6px;\r padding: 4px;\r margin-top: 8px;\r }\r \r .dnt-segment-btn {\r flex: 1;\r display: flex;\r flex-direction: column;\r align-items: center;\r justify-content: center;\r padding: 10px 16px;\r border: none;\r background: transparent;\r color: var(--dnt-text-secondary);\r cursor: pointer;\r border-radius: 4px;\r transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\r outline: none;\r min-height: 52px;\r }\r \r .dnt-segment-btn:hover {\r background: var(--dnt-bg-hover);\r color: var(--dnt-text-primary);\r }\r \r .dnt-segment-btn:focus-visible {\r box-shadow: 0 0 0 2px var(--dnt-border-focus);\r }\r \r .dnt-segment-active {\r background: var(--dnt-primary) !important;\r color: #ffffff !important;\r box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);\r }\r \r .dnt-segment-label {\r font-size: 14px;\r font-weight: 600;\r margin-bottom: 2px;\r }\r \r .dnt-segment-desc {\r font-size: 11px;\r opacity: 0.85;\r }\r \r .dnt-segment-active .dnt-segment-desc {\r opacity: 0.95;\r }\r \r /* \u5F00\u5173\u884C\uFF08\u5E26\u8BF4\u660E\u6587\u672C\uFF09 */\r .dnt-toggle-row {\r display: flex;\r align-items: center;\r justify-content: space-between;\r padding: 12px 0;\r border-bottom: 1px solid var(--dnt-border);\r }\r \r .dnt-toggle-row:last-child {\r border-bottom: none;\r }\r \r .dnt-toggle-label-wrap {\r flex: 1;\r display: flex;\r flex-direction: column;\r gap: 4px;\r }\r \r .dnt-toggle-label {\r color: var(--dnt-text-primary);\r font-size: 14px;\r font-weight: 500;\r }\r \r .dnt-toggle-desc {\r color: var(--dnt-text-secondary);\r font-size: 12px;\r }\r \r /* \u5B50\u6807\u9898 */\r .dnt-subsection-label {\r color: var(--dnt-text-primary);\r font-size: 13px;\r font-weight: 600;\r }\r \r .dnt-hint-text {\r color: var(--dnt-text-muted);\r font-size: 12px;\r }\r \r /* \u6A21\u5F0F\u5361\u7247\u9009\u62E9\u5668 */\r .dnt-mode-cards {\r display: flex;\r gap: 12px;\r flex-wrap: wrap;\r }\r \r .dnt-mode-card {\r flex: 1;\r min-width: 140px;\r position: relative;\r cursor: pointer;\r display: block;\r }\r \r .dnt-mode-card-checkbox {\r position: absolute;\r opacity: 0;\r pointer-events: none;\r }\r \r .dnt-mode-card-content {\r display: flex;\r flex-direction: column;\r align-items: center;\r gap: 8px;\r padding: 16px 12px;\r background: var(--dnt-bg-input);\r border: 2px solid var(--dnt-border);\r border-radius: 8px;\r transition: all 0.2s ease;\r position: relative;\r }\r \r .dnt-mode-card:hover .dnt-mode-card-content {\r border-color: var(--dnt-primary);\r background: var(--dnt-bg-hover);\r transform: translateY(-2px);\r box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\r }\r \r .dnt-mode-card-checked .dnt-mode-card-content {\r border-color: var(--dnt-primary);\r background: var(--dnt-bg-dialog);\r box-shadow: 0 2px 8px rgba(103, 194, 58, 0.15);\r }\r \r .dnt-mode-card-icon {\r color: var(--dnt-text-secondary);\r display: flex;\r align-items: center;\r justify-content: center;\r transition: color 0.2s ease;\r }\r \r .dnt-mode-card-checked .dnt-mode-card-icon {\r color: var(--dnt-primary);\r }\r \r .dnt-mode-card-label {\r font-size: 14px;\r font-weight: 600;\r color: var(--dnt-text-primary);\r text-align: center;\r }\r \r .dnt-mode-card-desc {\r font-size: 11px;\r color: var(--dnt-text-secondary);\r text-align: center;\r line-height: 1.4;\r }\r \r .dnt-mode-card-checkmark {\r position: absolute;\r top: 8px;\r right: 8px;\r color: var(--dnt-primary);\r opacity: 0;\r transform: scale(0.5);\r transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);\r }\r \r .dnt-mode-card-checked .dnt-mode-card-checkmark {\r opacity: 1;\r transform: scale(1);\r }\r \r /* \u6700\u5C11\u9700\u8981\u4E24\u4E2A\u7684\u89C6\u89C9\u63D0\u793A */\r .dnt-mode-card-min-required .dnt-mode-card-content {\r cursor: not-allowed;\r opacity: 0.8;\r }\r \r .dnt-mode-card-min-required .dnt-mode-card-content::after {\r content: '';\r position: absolute;\r inset: -2px;\r border: 2px solid var(--dnt-primary);\r border-radius: 8px;\r pointer-events: none;\r animation: pulse-border 2s ease-in-out infinite;\r }\r \r @keyframes pulse-border {\r 0%, 100% {\r opacity: 0.3;\r }\r 50% {\r opacity: 0.6;\r }\r }\r `; } }); // src/ui/inject-styles.ts function injectStyles() { if (injected) return; const styleEl = document.createElement("style"); styleEl.id = "dnt-settings-styles"; styleEl.textContent = styles_default; document.head.appendChild(styleEl); injected = true; } var injected; var init_inject_styles = __esm({ "src/ui/inject-styles.ts"() { init_styles(); injected = false; } }); // src/ui/settings.ts var settings_exports = {}; __export(settings_exports, { closeSettings: () => closeSettings, openSettings: () => openSettings }); async function openSettings() { injectStyles(); await initTheme(); await initI18n(); const panel = createSettingsPanel(); document.body.appendChild(panel); } function closeSettings() { const existing = document.getElementById("dnt-settings-overlay"); if (existing) { existing.remove(); } } var init_settings3 = __esm({ "src/ui/settings.ts"() { init_panel(); init_theme(); init_i18n(); init_inject_styles(); } }); // src/main.ts init_siteDetector(); init_gm(); init_domainLists(); // src/decision/engine.ts init_settings2(); init_logger(); async function evaluateRules(rules, ctx) { let lastDecision = null; for (const rule of rules) { let match = null; match = rule.match(ctx); const enabled = await getRuleEnabled(rule.id); if (!match) { await logRuleDetail(rule, enabled, false, void 0, void 0); continue; } const action = enabled ? rule.enabledAction : rule.disabledAction; lastDecision = { action, ruleId: rule.id }; await logRuleDetail(rule, enabled, true, action, match); } if (!lastDecision) { return { action: "keep_native", ruleId: "default" }; } return lastDecision; } // src/rules/topic.ts init_url(); init_settings2(); var ruleTopicOpenNewTab = { id: RULE_TOPIC_OPEN_NEW_TAB, name: "\u4ECE\u4EFB\u610F\u9875\u9762\u6253\u5F00\u4E3B\u9898\u5E16\uFF1A\u65B0\u6807\u7B7E\u9875", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const tId = extractTopicId(ctx.targetUrl.pathname); if (tId == null) return null; return { matched: true, data: { targetTopicId: tId } }; } }; var ruleInTopicOpenOther = { id: RULE_TOPIC_IN_TOPIC_OPEN_OTHER, name: "\u4E3B\u9898\u5E16\u5185\u90E8\u70B9\u51FB\u5176\u4ED6\u94FE\u63A5\uFF1A\u65B0\u6807\u7B7E\u9875", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const currentTopicId = extractTopicId(ctx.currentUrl.pathname); if (currentTopicId == null) return null; const targetTopicId = extractTopicId(ctx.targetUrl.pathname); if (targetTopicId && targetTopicId === currentTopicId) return null; return { matched: true, data: { currentTopicId, targetTopicId: targetTopicId ?? null } }; } }; var ruleSameTopicKeepNative = { id: RULE_TOPIC_SAME_TOPIC_KEEP_NATIVE, name: "\u540C\u4E00\u4E3B\u9898\u5185\u697C\u5C42\u8DF3\u8F6C\uFF1A\u4FDD\u7559\u539F\u751F", enabledAction: "keep_native", disabledAction: "new_tab", match: (ctx) => { const currentTopicId = extractTopicId(ctx.currentUrl.pathname); const targetTopicId = extractTopicId(ctx.targetUrl.pathname); if (currentTopicId == null || targetTopicId == null) return null; if (currentTopicId !== targetTopicId) return null; return { matched: true, note: "\u540C\u4E00\u4E3B\u9898\u7F16\u53F7\uFF08\u5E38\u89C1\u4E3A\u697C\u5C42\u8DF3\u8F6C\uFF09", data: { currentTopicId, targetTopicId } }; } }; var topicRules = [ // 越靠后优先级越高(规则 3 覆盖规则 1/2) ruleTopicOpenNewTab, ruleInTopicOpenOther, ruleSameTopicKeepNative ]; // src/rules/user.ts init_url(); init_settings2(); var ruleUserOpenNewTab = { id: RULE_USER_OPEN_NEW_TAB, name: "\u4ECE\u4EFB\u610F\u9875\u9762\u6253\u5F00\u4E2A\u4EBA\u4E3B\u9875\uFF1A\u65B0\u6807\u7B7E\u9875", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const uname = extractUsername(ctx.targetUrl.pathname); if (!uname) return null; return { matched: true, data: { targetUser: uname } }; } }; var ruleInProfileOpenOther = { id: RULE_USER_IN_PROFILE_OPEN_OTHER, name: "\u4E2A\u4EBA\u4E3B\u9875\u5185\u90E8\u70B9\u51FB\u5176\u4ED6\u94FE\u63A5\uFF1A\u65B0\u6807\u7B7E\u9875", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const currentUser = extractUsername(ctx.currentUrl.pathname); if (!currentUser) return null; const targetUser = extractUsername(ctx.targetUrl.pathname); if (targetUser && targetUser === currentUser) return null; return { matched: true, data: { currentUser, targetUser: targetUser ?? null } }; } }; var ruleSameProfileKeepNative = { id: RULE_USER_SAME_PROFILE_KEEP_NATIVE, name: "\u540C\u4E00\u7528\u6237\u4E3B\u9875\uFF1A\u4FDD\u7559\u539F\u751F", enabledAction: "keep_native", disabledAction: "new_tab", match: (ctx) => { const currentUser = extractUsername(ctx.currentUrl.pathname); const targetUser = extractUsername(ctx.targetUrl.pathname); if (!currentUser || !targetUser) return null; if (currentUser !== targetUser) return null; return { matched: true, data: { currentUser, targetUser } }; } }; var userRules = [ // 越靠后优先级越高(规则 3 覆盖规则 1/2) ruleUserOpenNewTab, ruleInProfileOpenOther, ruleSameProfileKeepNative ]; // src/rules/attachment.ts init_url(); init_settings2(); var ruleAttachmentKeepNative = { id: RULE_ATTACHMENT_KEEP_NATIVE, name: "\u9644\u4EF6\u94FE\u63A5\uFF1A\u4FDD\u7559\u539F\u751F", enabledAction: "keep_native", disabledAction: "new_tab", match: (ctx) => { const p = ctx.targetUrl.pathname || ""; if (!isLikelyAttachment(p)) return null; return { matched: true, data: { pathname: p } }; } }; var attachmentRules = [ruleAttachmentKeepNative]; // src/rules/popup.ts init_settings2(); // src/utils/dom.ts init_logger(); function closestAny(el, selectors) { if (!el) return null; for (const sel of selectors) { const hit = el.closest?.(sel); if (hit) return hit; } return null; } var USER_CARD_SELECTORS = ["#user-card", ".user-card", ".user-card-container"]; var USER_MENU_SELECTORS = [ "#user-menu", ".user-menu", ".user-menu-panel", "#user-menu .quick-access-panel", ".user-menu .quick-access-panel", "#user-menu .menu-panel", ".user-menu .menu-panel" ]; var HEADER_SELECTORS = ["header", ".d-header", "#site-header"]; var USER_MENU_NAV_SELECTORS = [ ".user-menu .navigation", '.user-menu [role="tablist"]', ".user-menu .menu-tabs", ".user-menu .categories", "#user-menu .navigation" ]; function isInUserCard(el) { return !!closestAny(el, USER_CARD_SELECTORS); } function isInUserMenu(el) { return !!closestAny(el, USER_MENU_SELECTORS); } function isInHeader(el) { return !!closestAny(el, HEADER_SELECTORS); } function isInUserMenuNav(el) { return !!closestAny(el, USER_MENU_NAV_SELECTORS); } var SIDEBAR_SELECTORS = [ "#sidebar", ".sidebar", ".d-sidebar", ".sidebar-container", ".discourse-sidebar", ".sidebar-section", ".sidebar-wrapper" ]; function isInSidebar(el) { return !!closestAny(el, SIDEBAR_SELECTORS); } function isUserCardTrigger(a) { if (!a) return false; if (a.hasAttribute("data-user-card")) return true; const cls = (a.className || "").toString().toLowerCase(); if (/user-card|avatar|trigger-user-card/.test(cls) && a.pathname?.toLowerCase?.().startsWith("/u/")) { return true; } return false; } function isUserMenuTrigger(a) { if (!a) return false; if (!isInHeader(a)) return false; if (a.hasAttribute("aria-haspopup") || a.hasAttribute("aria-expanded")) return true; const cls = (a.className || "").toString().toLowerCase(); if (/current-user|header-dropdown-toggle|user-menu|avatar/.test(cls)) return true; return false; } function isActiveTab(a) { if (!a) return false; if (a.getAttribute("aria-selected") === "true") return true; const cls = (a.className || "").toString().toLowerCase(); return /active|selected/.test(cls); } var SEARCH_MENU_SELECTORS = [ "#search-menu", ".search-menu", ".header .search-menu", ".d-header .search-menu" ]; var SEARCH_RESULTS_SELECTORS = [ "#search-menu .results", ".search-menu .results", "#search-menu .search-results", ".search-menu .search-results", ".quick-access-panel .results", ".menu-panel .results", ".menu-panel .search-results" ]; function isInSearchMenu(el) { return !!closestAny(el, SEARCH_MENU_SELECTORS); } function isInSearchResults(el) { if (!isInSearchMenu(el)) return false; return !!closestAny(el, SEARCH_RESULTS_SELECTORS); } function resolveSearchResultLink(a) { if (!a) return null; if (!isInSearchResults(a)) return null; const attrNames = ["data-url", "data-href", "data-link", "data-topic-url"]; const readAttrs = (el) => { if (!el) return null; for (const k of attrNames) { const v = el.getAttribute?.(k); if (v) return v; } const topicId = el.getAttribute?.("data-topic-id") || el.getAttribute?.("data-topicid"); if (topicId && /\d+/.test(topicId)) return `/t/${topicId}`; return null; }; let node = a; for (let i = 0; i < 4 && node; i++) { const v = readAttrs(node); if (v) return v; node = node.parentElement; } const container = a.closest?.(".search-link, .search-result, .fps-result, li, article, .search-row") || a.parentElement; if (container) { const inner = container.querySelector?.("a[href]"); if (inner && inner.getAttribute("href")) return inner.getAttribute("href"); const v = readAttrs(container); if (v) return v; } return null; } // src/rules/popup.ts var ruleUserCardTriggerKeepNative = { id: RULE_POPUP_USER_CARD, name: "\u7528\u6237\u5361\u7247\uFF1A\u89E6\u53D1\u94FE\u63A5=\u4FDD\u7559\u539F\u751F", enabledAction: "keep_native", disabledAction: "keep_native", // 关闭时也保留原生 match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (isUserCardTrigger(a) && !isInUserCard(a)) { return { matched: true, note: "\u7528\u6237\u5361\u7247\u89E6\u53D1\u94FE\u63A5" }; } return null; } }; var ruleUserCardInsideNewTab = { id: RULE_POPUP_USER_CARD, name: "\u7528\u6237\u5361\u7247\uFF1A\u5361\u7247\u5185\u94FE\u63A5=\u65B0\u6807\u7B7E", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (isInUserCard(a)) { return { matched: true, note: "\u7528\u6237\u5361\u7247\u5185\u94FE\u63A5" }; } return null; } }; var ruleUserMenuTriggerKeepNative = { id: RULE_POPUP_USER_MENU, name: "\u7528\u6237\u83DC\u5355\uFF1A\u89E6\u53D1\u94FE\u63A5=\u4FDD\u7559\u539F\u751F", enabledAction: "keep_native", disabledAction: "keep_native", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (isUserMenuTrigger(a) && !isInUserMenu(a)) { return { matched: true, note: "\u7528\u6237\u83DC\u5355\u89E6\u53D1\u94FE\u63A5" }; } return null; } }; var ruleUserMenuNavKeepOrNew = { id: RULE_POPUP_USER_MENU, name: "\u7528\u6237\u83DC\u5355\uFF1A\u5BFC\u822A\u533A\u70B9\u51FB\uFF08\u6FC0\u6D3B=\u65B0\u6807\u7B7E/\u672A\u6FC0\u6D3B=\u539F\u751F\uFF09", enabledAction: "keep_native", // 默认保留原生,下面在激活情况下用后置规则覆盖 disabledAction: "keep_native", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (!isInUserMenu(a)) return null; if (!isInUserMenuNav(a)) return null; if (!isActiveTab(a)) { return { matched: true, note: "\u7528\u6237\u83DC\u5355\u5BFC\u822A\uFF08\u672A\u6FC0\u6D3B\uFF09" }; } return null; } }; var ruleUserMenuNavActiveNewTab = { id: RULE_POPUP_USER_MENU, name: "\u7528\u6237\u83DC\u5355\uFF1A\u5BFC\u822A\u533A\u6FC0\u6D3B\u9879=\u65B0\u6807\u7B7E", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (!isInUserMenu(a)) return null; if (!isInUserMenuNav(a)) return null; if (isActiveTab(a)) { return { matched: true, note: "\u7528\u6237\u83DC\u5355\u5BFC\u822A\uFF08\u6FC0\u6D3B\uFF09" }; } return null; } }; var ruleUserMenuContentNewTab = { id: RULE_POPUP_USER_MENU, name: "\u7528\u6237\u83DC\u5355\uFF1A\u5185\u5BB9\u533A\u94FE\u63A5=\u65B0\u6807\u7B7E", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (!isInUserMenu(a)) return null; if (isInUserMenuNav(a)) return null; return { matched: true, note: "\u7528\u6237\u83DC\u5355\u5185\u5BB9\u533A\u94FE\u63A5" }; } }; var popupRules = [ // 用户卡片(触发→保留;卡片内→新标签) ruleUserCardTriggerKeepNative, ruleUserCardInsideNewTab, // 用户菜单(触发→保留;导航未激活→保留;导航激活→新标签;内容区→新标签) ruleUserMenuTriggerKeepNative, ruleUserMenuNavKeepOrNew, ruleUserMenuNavActiveNewTab, ruleUserMenuContentNewTab, // 搜索弹窗(结果列表与底部“更多”按钮 → 新标签;其余保持原生) // 说明:搜索历史、建议项等(不在结果区内)一律不改写。 { id: RULE_POPUP_SEARCH_MENU, name: "\u641C\u7D22\u5F39\u7A97\uFF1A\u7ED3\u679C\u4E0E\u201C\u66F4\u591A\u201D=\u65B0\u6807\u7B7E", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (!isInSearchResults(a)) return null; const p = ctx.targetUrl?.pathname || ""; if (/\/t\//.test(p) || p.startsWith("/search")) { return { matched: true, note: "\u641C\u7D22\u5F39\u7A97\u7ED3\u679C\u6216\u66F4\u591A" }; } return null; } } ]; // src/rules/sidebar.ts init_url(); init_settings2(); var ruleSidebarNonTopicKeepNative = { id: RULE_SIDEBAR_NON_TOPIC_KEEP_NATIVE, name: "\u975E\u4E3B\u9898\u9875-\u4FA7\u8FB9\u680F\u94FE\u63A5\uFF1A\u4FDD\u7559\u539F\u751F", enabledAction: "keep_native", disabledAction: "new_tab", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (!isInSidebar(a)) return null; const currentTopicId = extractTopicId(ctx.currentUrl.pathname); if (currentTopicId != null) return null; return { matched: true, note: "\u975E\u4E3B\u9898\u9875\u7684\u4FA7\u8FB9\u680F\u94FE\u63A5" }; } }; var ruleSidebarInTopicNewTab = { id: RULE_SIDEBAR_IN_TOPIC_NEW_TAB, name: "\u4E3B\u9898\u9875-\u4FA7\u8FB9\u680F\u94FE\u63A5\uFF1A\u65B0\u6807\u7B7E\u9875", enabledAction: "new_tab", disabledAction: "keep_native", match: (ctx) => { const a = ctx.anchor; if (!a) return null; if (!isInSidebar(a)) return null; const currentTopicId = extractTopicId(ctx.currentUrl.pathname); if (currentTopicId == null) return null; return { matched: true, note: "\u4E3B\u9898\u9875\u5185\u7684\u4FA7\u8FB9\u680F\u94FE\u63A5", data: { currentTopicId } }; } }; var sidebarRules = [ // 顺序与《需求文档》一致:非主题页→保留原生;主题页→新标签 ruleSidebarNonTopicKeepNative, ruleSidebarInTopicNewTab ]; // src/rules/index.ts function getAllRules() { return [ ...topicRules, ...userRules, ...attachmentRules, ...popupRules, ...sidebarRules ]; } // src/listeners/click.ts init_url(); init_logger(); init_openMode(); // src/utils/open.ts init_logger(); function openNewTabForeground(url) { try { window.open(url, "_blank", "noopener"); } catch (err) { void logError("final", "window.open \u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A location.href", err); try { location.href = url; } catch (e2) { void logError("final", "location.href \u8DF3\u8F6C\u5931\u8D25", e2); } } } function openNewTabBackground(url) { try { const GM = globalThis.GM; if (GM?.openInTab) { GM.openInTab(url, { active: false, setParent: true }); return; } } catch (err) { void logError("final", "GM.openInTab \u8C03\u7528\u5931\u8D25", err); } try { const gmo = globalThis.GM_openInTab; if (typeof gmo === "function") { gmo(url, { active: false, setParent: true }); return; } } catch (err) { void logError("final", "GM_openInTab \u8C03\u7528\u5931\u8D25", err); } openNewTabForeground(url); } // src/listeners/click.ts init_url(); function isPlainLeftClick(ev) { return ev.button === 0 && !ev.ctrlKey && !ev.metaKey && !ev.shiftKey && !ev.altKey; } function findAnchor(el) { let node = el; while (node) { const elem = node; if (elem && elem.tagName === "A") return elem; node = elem && elem.parentElement ? elem.parentElement : null; } return null; } function attachClickListener(label = "[discourse-new-tab]") { const handler = async (ev) => { try { if (!isPlainLeftClick(ev)) { await logClickFilter("\u975E\u5DE6\u952E\u6216\u7EC4\u5408\u952E\u70B9\u51FB"); return; } const a = findAnchor(ev.target); if (!a) { await logClickFilter("\u672A\u627E\u5230 \u5143\u7D20"); return; } let rawHref = a.getAttribute("href") || a.href || ""; if (!rawHref || rawHref === "#") { if (isInSearchResults(a)) { const fallback = resolveSearchResultLink(a); if (fallback) rawHref = fallback; } } if (!rawHref) { await logClickFilter("\u94FE\u63A5\u65E0 href"); return; } if (a.hasAttribute("download")) { await logClickFilter("\u4E0B\u8F7D\u94FE\u63A5"); return; } if (a.getAttribute("data-dnt-ignore") === "1") { await logClickFilter("\u663E\u5F0F\u5FFD\u7565\u6807\u8BB0 data-dnt-ignore=1"); return; } const targetUrl = toAbsoluteUrl(rawHref, location.href); if (!targetUrl) { await logClickFilter("\u76EE\u6807 URL \u89E3\u6790\u5931\u8D25"); return; } const ctx = { anchor: a, targetUrl, currentUrl: new URL(location.href) }; await logLinkInfo(ctx); const decision = await evaluateRules(getAllRules(), ctx); if (decision.action === "new_tab") { ev.preventDefault(); ev.stopImmediatePropagation(); ev.stopPropagation(); try { const mode = await getBackgroundOpenMode(); const isTopic = extractTopicId(targetUrl.pathname) != null; const useBg = mode === "all" || mode === "topic" && isTopic; if (useBg) { openNewTabBackground(targetUrl.href); a.setAttribute("data-dnt-handled", "1"); await logFinalDecision(decision.ruleId, decision.action); await logBackgroundOpenApplied(mode === "all" ? "all" : "topic"); return; } } catch (err) { await logError("click", "\u8BFB\u53D6\u540E\u53F0\u6253\u5F00\u8BBE\u7F6E\u5931\u8D25\uFF0C\u964D\u7EA7\u4E3A\u524D\u53F0", err); } openNewTabForeground(targetUrl.href); a.setAttribute("data-dnt-handled", "1"); await logFinalDecision(decision.ruleId, decision.action); return; } else if (decision.action === "same_tab") { await logFinalDecision(decision.ruleId, decision.action); } else { await logFinalDecision(decision.ruleId, decision.action); } } catch (err) { await logError("click", "\u70B9\u51FB\u5904\u7406\u5F02\u5E38", err); } }; document.addEventListener("click", handler, true); } // src/main.ts init_logger(); (async () => { const label = "[discourse-new-tab]"; const isTop = (() => { try { return window.top === window; } catch (err) { void logError("site", "window.top \u8BBF\u95EE\u5F02\u5E38\uFF0C\u6309\u9876\u5C42\u5904\u7406", err); return true; } })(); if (!isTop) return; const result = detectDiscourse(); await logSiteDetection(result); const host = getCurrentHostname(); const enable = await getEnablement(result.isDiscourse, host); if (enable.enabled) { attachClickListener(label); try { const { initFloatBall: initFloatBall2 } = await Promise.resolve().then(() => (init_floatball(), floatball_exports)); await initFloatBall2(); } catch (err) { void logError("bg", "\u60AC\u6D6E\u7403\u521D\u59CB\u5316\u5931\u8D25", err); } } gmRegisterMenu("\u8BBE\u7F6E", async () => { const { openSettings: openSettings2 } = await Promise.resolve().then(() => (init_settings3(), settings_exports)); await openSettings2(); }); })(); })();