// ==UserScript==
// @name Discourse 新标签页
// @name:en Discourse New Tab
// @namespace https://github.com/selaky/discourse-new-tab
// @version 1.1.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
// @run-at document-start
// @license MIT
// ==/UserScript==
(() => {
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
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 });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// 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 {
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 {
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 {
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 {
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 {
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"() {
THRESHOLD = 3;
}
});
// 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 {
}
try {
const raw = localStorage.getItem(`dnt:${key}`);
return raw == null ? def : JSON.parse(raw);
} catch {
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 {
}
try {
localStorage.setItem(`dnt:${key}`, JSON.stringify(value));
} catch {
}
}
function gmRegisterMenu(label, cb) {
try {
const reg = globalThis.GM_registerMenuCommand;
if (typeof reg === "function") {
reg(label, cb);
return;
}
} catch {
}
}
var init_gm = __esm({
"src/storage/gm.ts"() {
}
});
// 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 {
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 {
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();
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_settings = __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/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
};
await gmSet(KEY_DEBUG_CATEGORIES, all);
}
var DEBUG_LABEL, KEY_DEBUG_ENABLED, KEY_DEBUG_CATEGORIES, DEFAULT_DEBUG_CATEGORIES;
var init_settings2 = __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
};
}
});
// 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: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>`,
dark: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>`,
auto: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M12 2 A 10 10 0 0 1 12 22 Z" fill="currentColor"></path>
</svg>`
};
currentTheme = "auto";
if (window.matchMedia) {
window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", () => {
if (currentTheme === "auto") {
applyTheme();
}
});
}
}
});
// 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: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="6" width="18" height="12" rx="2" ry="2"></rect>
<text x="12" y="16" text-anchor="middle" font-size="11" font-weight="bold" fill="currentColor" stroke="none">\u4E2D</text>
</svg>`,
en: `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="6" width="18" height="12" rx="2" ry="2"></rect>
<text x="12" y="16" text-anchor="middle" font-size="9" font-weight="bold" fill="currentColor" stroke="none">EN</text>
</svg>`
};
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"
},
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: "\u8BBA\u575B\u8BC6\u522B",
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"
}
},
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"
}
}
}
},
en: {
settings: {
title: "Settings",
close: "Close",
theme: {
light: "Light Mode",
dark: "Dark Mode",
auto: "Auto Mode"
},
language: {
zh: "\u4E2D\u6587",
en: "English"
},
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: "Forum Recognition",
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"
}
},
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"
}
}
}
}
};
}
});
// 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/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_settings();
init_settings();
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/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 = createToggle2(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") }
];
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 = createToggle2(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();
setToggleVisual(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;
setToggleVisual(el, on);
});
}
function createToggle2(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);
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");
}
var init_debug = __esm({
"src/ui/sections/debug.ts"() {
init_i18n();
init_settings2();
}
});
// 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 content = document.createElement("div");
content.className = "dnt-content";
content.appendChild(renderStatusSection());
content.appendChild(renderDomainSection());
content.appendChild(renderRulesSection());
content.appendChild(renderDebugSection());
dialog.appendChild(content);
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();
const { openSettings: openSettings2 } = (init_settings3(), __toCommonJS(settings_exports));
openSettings2();
});
controls.appendChild(langBtn);
const closeBtn = document.createElement("button");
closeBtn.className = "dnt-icon-btn";
closeBtn.title = t("settings.close");
closeBtn.innerHTML = `<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>`;
closeBtn.addEventListener("click", closeSettings);
controls.appendChild(closeBtn);
header.appendChild(controls);
return header;
}
var init_panel = __esm({
"src/ui/panel.ts"() {
init_settings3();
init_theme();
init_i18n();
init_i18n();
init_status();
init_domain();
init_rules();
init_debug();
}
});
// 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\n\r\n/* CSS\u53D8\u91CF - \u65E5\u95F4\u4E3B\u9898 */\r\n:root[data-dnt-theme="light"] {\r\n --dnt-bg-overlay: rgba(0, 0, 0, 0.5);\r\n --dnt-bg-dialog: #ffffff;\r\n --dnt-bg-section: #f8f9fa;\r\n --dnt-bg-input: #ffffff;\r\n --dnt-bg-hover: #f0f0f0;\r\n\r\n --dnt-text-primary: #2c3e50;\r\n --dnt-text-secondary: #6c757d;\r\n --dnt-text-muted: #999999;\r\n\r\n --dnt-border: #e1e4e8;\r\n --dnt-border-focus: #67c23a;\r\n\r\n --dnt-primary: #67c23a;\r\n --dnt-primary-hover: #85ce61;\r\n --dnt-danger: #f56c6c;\r\n --dnt-danger-hover: #f78989;\r\n\r\n --dnt-badge-auto: #409eff;\r\n --dnt-badge-whitelist: #67c23a;\r\n --dnt-badge-blacklist: #f56c6c;\r\n --dnt-badge-disabled: #909399;\r\n\r\n --dnt-toggle-on: #67c23a;\r\n --dnt-toggle-off: #dcdfe6;\r\n --dnt-toggle-thumb: #ffffff;\r\n\r\n --dnt-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);\r\n}\r\n\r\n/* CSS\u53D8\u91CF - \u591C\u95F4\u4E3B\u9898 */\r\n:root[data-dnt-theme="dark"] {\r\n --dnt-bg-overlay: rgba(0, 0, 0, 0.7);\r\n --dnt-bg-dialog: #1e1e1e;\r\n --dnt-bg-section: #2a2a2a;\r\n --dnt-bg-input: #363636;\r\n --dnt-bg-hover: #3a3a3a;\r\n\r\n --dnt-text-primary: #e4e4e4;\r\n --dnt-text-secondary: #b0b0b0;\r\n --dnt-text-muted: #888888;\r\n\r\n --dnt-border: #404040;\r\n --dnt-border-focus: #67c23a;\r\n\r\n --dnt-primary: #67c23a;\r\n --dnt-primary-hover: #85ce61;\r\n --dnt-danger: #f56c6c;\r\n --dnt-danger-hover: #f78989;\r\n\r\n --dnt-badge-auto: #409eff;\r\n --dnt-badge-whitelist: #67c23a;\r\n --dnt-badge-blacklist: #f56c6c;\r\n --dnt-badge-disabled: #909399;\r\n\r\n --dnt-toggle-on: #67c23a;\r\n --dnt-toggle-off: #4a4a4a;\r\n --dnt-toggle-thumb: #ffffff;\r\n\r\n --dnt-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);\r\n}\r\n\r\n/* \u91CD\u7F6E\u6837\u5F0F */\r\n.dnt-overlay *,\r\n.dnt-overlay *::before,\r\n.dnt-overlay *::after {\r\n box-sizing: border-box;\r\n margin: 0;\r\n padding: 0;\r\n}\r\n\r\n/* \u906E\u7F69\u5C42 */\r\n.dnt-overlay {\r\n position: fixed;\r\n top: 0;\r\n left: 0;\r\n right: 0;\r\n bottom: 0;\r\n background: var(--dnt-bg-overlay);\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n z-index: 999999;\r\n font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;\r\n font-size: 14px;\r\n line-height: 1.6;\r\n}\r\n\r\n/* \u5BF9\u8BDD\u6846 */\r\n.dnt-dialog {\r\n background: var(--dnt-bg-dialog);\r\n border-radius: 8px;\r\n box-shadow: var(--dnt-shadow);\r\n width: 90%;\r\n max-width: 680px;\r\n max-height: 85vh;\r\n display: flex;\r\n flex-direction: column;\r\n overflow: hidden;\r\n}\r\n\r\n/* \u5934\u90E8 */\r\n.dnt-header {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 20px 24px;\r\n border-bottom: 1px solid var(--dnt-border);\r\n}\r\n\r\n.dnt-title {\r\n font-size: 20px;\r\n font-weight: 600;\r\n color: var(--dnt-text-primary);\r\n}\r\n\r\n.dnt-controls {\r\n display: flex;\r\n gap: 8px;\r\n}\r\n\r\n.dnt-icon-btn {\r\n width: 36px;\r\n height: 36px;\r\n border: none;\r\n background: transparent;\r\n color: var(--dnt-text-secondary);\r\n cursor: pointer;\r\n border-radius: 4px;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n transition: all 0.2s;\r\n}\r\n\r\n.dnt-icon-btn:hover {\r\n background: var(--dnt-bg-hover);\r\n color: var(--dnt-text-primary);\r\n}\r\n\r\n/* \u5185\u5BB9\u533A */\r\n.dnt-content {\r\n flex: 1;\r\n overflow-y: auto;\r\n padding: 20px 24px;\r\n}\r\n\r\n.dnt-content::-webkit-scrollbar {\r\n width: 8px;\r\n}\r\n\r\n.dnt-content::-webkit-scrollbar-track {\r\n background: transparent;\r\n}\r\n\r\n.dnt-content::-webkit-scrollbar-thumb {\r\n background: var(--dnt-border);\r\n border-radius: 4px;\r\n}\r\n\r\n.dnt-content::-webkit-scrollbar-thumb:hover {\r\n background: var(--dnt-text-muted);\r\n}\r\n\r\n/* \u533A\u5757 */\r\n.dnt-section {\r\n margin-bottom: 24px;\r\n}\r\n\r\n.dnt-section:last-child {\r\n margin-bottom: 0;\r\n}\r\n\r\n.dnt-section-title {\r\n font-size: 16px;\r\n font-weight: 600;\r\n color: var(--dnt-text-primary);\r\n margin-bottom: 12px;\r\n padding-bottom: 8px;\r\n border-bottom: 2px solid var(--dnt-primary);\r\n}\r\n\r\n/* \u72B6\u6001\u533A\u57DF */\r\n.dnt-status-content {\r\n background: var(--dnt-bg-section);\r\n border-radius: 6px;\r\n padding: 16px;\r\n}\r\n\r\n.dnt-status-row {\r\n display: flex;\r\n align-items: center;\r\n gap: 12px;\r\n margin-bottom: 8px;\r\n}\r\n\r\n.dnt-status-row:last-child {\r\n margin-bottom: 0;\r\n}\r\n\r\n.dnt-status-label {\r\n color: var(--dnt-text-primary);\r\n font-weight: 500;\r\n}\r\n\r\n.dnt-status-value {\r\n color: var(--dnt-text-secondary);\r\n}\r\n\r\n.dnt-domain-text {\r\n font-family: "Consolas", "Monaco", monospace;\r\n font-size: 13px;\r\n}\r\n\r\n/* \u5FBD\u7AE0 */\r\n.dnt-badge {\r\n display: inline-block;\r\n padding: 2px 10px;\r\n border-radius: 12px;\r\n font-size: 12px;\r\n font-weight: 500;\r\n color: #ffffff;\r\n}\r\n\r\n.dnt-badge-auto {\r\n background: var(--dnt-badge-auto);\r\n}\r\n\r\n.dnt-badge-whitelist {\r\n background: var(--dnt-badge-whitelist);\r\n}\r\n\r\n.dnt-badge-blacklist {\r\n background: var(--dnt-badge-blacklist);\r\n}\r\n\r\n.dnt-badge-disabled {\r\n background: var(--dnt-badge-disabled);\r\n}\r\n\r\n/* \u57DF\u540D\u7BA1\u7406\u533A\u57DF */\r\n.dnt-domain-content {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 20px;\r\n}\r\n\r\n.dnt-list-block {\r\n background: var(--dnt-bg-section);\r\n border-radius: 6px;\r\n padding: 16px;\r\n}\r\n\r\n.dnt-list-subtitle {\r\n font-size: 14px;\r\n font-weight: 600;\r\n color: var(--dnt-text-primary);\r\n margin-bottom: 12px;\r\n}\r\n\r\n.dnt-domain-list {\r\n margin-bottom: 12px;\r\n min-height: 40px;\r\n max-height: 180px;\r\n overflow-y: auto;\r\n}\r\n\r\n.dnt-domain-list::-webkit-scrollbar {\r\n width: 6px;\r\n}\r\n\r\n.dnt-domain-list::-webkit-scrollbar-thumb {\r\n background: var(--dnt-border);\r\n border-radius: 3px;\r\n}\r\n\r\n.dnt-domain-item {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 8px 12px;\r\n background: var(--dnt-bg-input);\r\n border: 1px solid var(--dnt-border);\r\n border-radius: 4px;\r\n margin-bottom: 8px;\r\n}\r\n\r\n.dnt-domain-item:last-child {\r\n margin-bottom: 0;\r\n}\r\n\r\n.dnt-empty-text {\r\n color: var(--dnt-text-muted);\r\n font-size: 13px;\r\n text-align: center;\r\n padding: 20px;\r\n}\r\n\r\n/* \u8F93\u5165\u6846 */\r\n.dnt-input-row {\r\n display: flex;\r\n gap: 8px;\r\n margin-bottom: 8px;\r\n}\r\n\r\n.dnt-input {\r\n flex: 1;\r\n padding: 8px 12px;\r\n border: 1px solid var(--dnt-border);\r\n border-radius: 4px;\r\n background: var(--dnt-bg-input);\r\n color: var(--dnt-text-primary);\r\n font-size: 14px;\r\n outline: none;\r\n transition: border-color 0.2s;\r\n}\r\n\r\n.dnt-input:focus {\r\n border-color: var(--dnt-border-focus);\r\n}\r\n\r\n.dnt-input::placeholder {\r\n color: var(--dnt-text-muted);\r\n}\r\n\r\n/* \u6309\u94AE */\r\n.dnt-btn {\r\n padding: 8px 16px;\r\n border: none;\r\n border-radius: 4px;\r\n font-size: 14px;\r\n font-weight: 500;\r\n cursor: pointer;\r\n transition: all 0.2s;\r\n outline: none;\r\n}\r\n\r\n.dnt-btn-primary {\r\n background: var(--dnt-primary);\r\n color: #ffffff;\r\n}\r\n\r\n.dnt-btn-primary:hover {\r\n background: var(--dnt-primary-hover);\r\n}\r\n\r\n.dnt-btn-secondary {\r\n background: var(--dnt-bg-input);\r\n color: var(--dnt-text-primary);\r\n border: 1px solid var(--dnt-border);\r\n width: 100%;\r\n}\r\n\r\n.dnt-btn-secondary:hover {\r\n background: var(--dnt-bg-hover);\r\n}\r\n\r\n.dnt-btn-danger {\r\n background: var(--dnt-danger);\r\n color: #ffffff;\r\n}\r\n\r\n.dnt-btn-danger:hover {\r\n background: var(--dnt-danger-hover);\r\n}\r\n\r\n.dnt-btn-sm {\r\n padding: 4px 12px;\r\n font-size: 13px;\r\n}\r\n\r\n/* \u89C4\u5219\u533A\u57DF */\r\n.dnt-rules-content {\r\n display: flex;\r\n flex-direction: column;\r\n gap: 16px;\r\n}\r\n\r\n.dnt-rule-group {\r\n background: var(--dnt-bg-section);\r\n border-radius: 6px;\r\n padding: 16px;\r\n}\r\n\r\n.dnt-rule-group-title {\r\n font-size: 14px;\r\n font-weight: 600;\r\n color: var(--dnt-text-primary);\r\n margin-bottom: 12px;\r\n}\r\n\r\n.dnt-rule-item {\r\n display: flex;\r\n align-items: center;\r\n justify-content: space-between;\r\n padding: 10px 0;\r\n border-bottom: 1px solid var(--dnt-border);\r\n}\r\n\r\n.dnt-rule-item:last-child {\r\n border-bottom: none;\r\n padding-bottom: 0;\r\n}\r\n\r\n.dnt-rule-label {\r\n color: var(--dnt-text-primary);\r\n font-size: 14px;\r\n flex: 1;\r\n cursor: default;\r\n}\r\n\r\n/* \u5F00\u5173 */\r\n.dnt-toggle {\r\n width: 44px;\r\n height: 24px;\r\n border-radius: 12px;\r\n cursor: pointer;\r\n position: relative;\r\n transition: background-color 0.3s;\r\n}\r\n\r\n.dnt-toggle-on {\r\n background: var(--dnt-toggle-on);\r\n}\r\n\r\n.dnt-toggle-off {\r\n background: var(--dnt-toggle-off);\r\n}\r\n\r\n.dnt-toggle-track {\r\n width: 100%;\r\n height: 100%;\r\n position: relative;\r\n}\r\n\r\n.dnt-toggle-thumb {\r\n width: 20px;\r\n height: 20px;\r\n border-radius: 50%;\r\n background: var(--dnt-toggle-thumb);\r\n position: absolute;\r\n top: 2px;\r\n transition: left 0.3s;\r\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\r\n}\r\n\r\n.dnt-toggle-on .dnt-toggle-thumb {\r\n left: 22px;\r\n}\r\n\r\n.dnt-toggle-off .dnt-toggle-thumb {\r\n left: 2px;\r\n}\r\n\r\n/* \u54CD\u5E94\u5F0F */\r\n@media (max-width: 768px) {\r\n .dnt-dialog {\r\n width: 95%;\r\n max-height: 90vh;\r\n }\r\n\r\n .dnt-header,\r\n .dnt-content {\r\n padding: 16px;\r\n }\r\n\r\n .dnt-title {\r\n font-size: 18px;\r\n }\r\n}\r\n';
}
});
// 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_settings();
// src/debug/logger.ts
init_settings2();
// src/utils/url.ts
function toAbsoluteUrl(href, base) {
try {
if (!href || typeof href !== "string") return null;
return new URL(href, base);
} catch {
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 {
}
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 {
}
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;
}
// 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;
try {
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);
} catch {
}
}
async function logClickFilter(reason) {
if (!await shouldLog("click")) return;
try {
console.log(`${DEBUG_LABEL} \u70B9\u51FB\u4E8B\u4EF6\u5FFD\u7565\uFF1A${reason}`);
} catch {
}
}
async function logLinkInfo(ctx) {
if (!await shouldLog("link")) return;
try {
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}`);
} catch {
}
}
async function logRuleDetail(rule, enabled, matched, action, meta) {
if (!await shouldLog("rules")) return;
try {
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}`);
}
} catch {
}
}
async function logFinalDecision(ruleId, action) {
if (!await shouldLog("final")) return;
try {
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}`);
} catch {
}
}
function safeInline(obj) {
try {
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(", ");
} catch {
return "";
}
}
// src/decision/engine.ts
async function evaluateRules(rules, ctx) {
let lastDecision = null;
for (const rule of rules) {
let match = null;
try {
match = rule.match(ctx);
} catch {
}
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_settings();
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_settings();
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_settings();
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_settings();
// src/utils/dom.ts
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_settings();
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
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 <a> \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();
window.open(targetUrl.href, "_blank", "noopener");
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) {
}
};
document.addEventListener("click", handler, true);
}
// src/main.ts
(async () => {
const label = "[discourse-new-tab]";
const isTop = (() => {
try {
return window.top === window;
} catch {
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);
}
gmRegisterMenu("\u8BBE\u7F6E", async () => {
const { openSettings: openSettings2 } = await Promise.resolve().then(() => (init_settings3(), settings_exports));
await openSettings2();
});
})();
})();