// ==UserScript== // @name Fixed Site Name Replacer2 // @namespace local.codex.fixed-site-replacer // @version 0.6.1 // @description 本地替换 Microsoft 登录页、Outlook 和 Bandai Parks 页面上的显示文字。(iOS Safari Userscripts / 油猴双兼容;触发:长按 2 秒或三击顶部区域) // @match https://login.microsoftonline.com/* // @match https://outlook.live.com/* // @match https://parks2.bandainamco-am.co.jp/* // @grant none // @run-at document-start // ==/UserScript== (() => { "use strict"; const STORAGE_KEY = "codex.fixedSite.nameReplacer.config.v1"; const SUPPORTED_HOSTS = [ "login.microsoftonline.com", "outlook.live.com", "parks2.bandainamco-am.co.jp", ]; const PANEL_ID = "codex-msmail-name-panel"; const STYLE_ID = "codex-msmail-name-style"; const SESSION_DURATION_MS = 4 * 60 * 60 * 1000; const PANEL_HOLD_MS = 2000; const PANEL_HOLD_ZONE_PX = 100; const TRIPLE_CLICK_WINDOW_MS = 500; const TRIPLE_CLICK_MAX_SPREAD = 40; const FAST_SCAN_WINDOW_MS = 4000; const FAST_SCAN_INTERVAL_MS = 120; const DEFAULT_RULES = [ { enabled: true, original: "", replacement: "", mode: "normal" }, { enabled: true, original: "", replacement: "", mode: "normal" }, ]; let originalTextMap = new WeakMap(); let originalValueMap = new WeakMap(); const touchedTextNodes = new Set(); const touchedElements = new Set(); let observer = null; let applying = false; let statusUpdater = null; let holdTimer = null; let topHoldStartY = 0; let threeFingerHold = false; let tripleClickTimes = []; let fastScanInterval = null; let fastScanStopTimer = null; const defaultConfig = { enabled: false, fontAdjust: false, panelVisible: false, bodyCollapsed: false, sessionStartedAt: null, rules: DEFAULT_RULES, }; const cloneRules = (rules) => (Array.isArray(rules) ? rules : DEFAULT_RULES).map((rule) => ({ enabled: rule.enabled !== false, original: String(rule.original || ""), replacement: String(rule.replacement || ""), mode: rule.mode === "regex" ? "regex" : "normal", })); const readStoredConfig = () => { if (typeof GM_getValue === "function") { return GM_getValue(STORAGE_KEY, "{}"); } return localStorage.getItem(STORAGE_KEY) || "{}"; }; const writeStoredConfig = (value) => { if (typeof GM_setValue === "function") { GM_setValue(STORAGE_KEY, value); return; } localStorage.setItem(STORAGE_KEY, value); }; const loadConfig = () => { try { const saved = JSON.parse(readStoredConfig()); return { ...defaultConfig, ...saved, panelVisible: false, sessionStartedAt: typeof saved.sessionStartedAt === "number" && saved.sessionStartedAt > 0 ? saved.sessionStartedAt : null, rules: cloneRules(saved.rules), }; } catch { return { ...defaultConfig, rules: cloneRules(defaultConfig.rules), }; } }; let config = loadConfig(); config.panelVisible = false; const saveConfig = () => { writeStoredConfig( JSON.stringify({ ...config, rules: cloneRules(config.rules), }) ); }; const normalize = (value) => value.replace(/\s+/g, " ").trim(); const siteAuthorized = () => { return SUPPORTED_HOSTS.includes(location.hostname); }; const isOutlookPage = () => location.hostname === "outlook.live.com"; const shouldSkipNode = (node) => { const element = node instanceof Element ? node : node?.parentElement instanceof Element ? node.parentElement : null; if (!element) return false; if (element.closest(`#${PANEL_ID}`)) return true; if (["SCRIPT", "STYLE", "NOSCRIPT"].includes(element.tagName)) return true; return Boolean(element.closest('iframe, [contenteditable="true"]')); }; const getReplacementRoots = () => { if (!document.body) return []; return [document.body]; }; const hasValidSession = () => typeof config.sessionStartedAt === "number" && config.sessionStartedAt > 0; const getSessionExpiresAt = () => hasValidSession() ? config.sessionStartedAt + SESSION_DURATION_MS : 0; const getRemainingMs = () => hasValidSession() ? Math.max(0, getSessionExpiresAt() - Date.now()) : 0; const formatRemaining = (ms) => { const totalSeconds = Math.ceil(ms / 1000); const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`; }; const hasExpired = () => hasValidSession() && getRemainingMs() <= 0; const isReplacementActive = () => config.enabled && siteAuthorized() && hasValidSession() && !hasExpired(); const replaceByRule = (input, rule) => { if (!rule.enabled || !rule.original || !rule.replacement) { return input; } if (rule.mode === "regex") { try { return input.replace(new RegExp(rule.original, "g"), rule.replacement); } catch { return input; } } return input.split(rule.original).join(rule.replacement); }; const replaceText = (text) => { if (!isReplacementActive()) return text; return config.rules.reduce((next, rule) => replaceByRule(next, rule), text); }; const rememberOriginalText = (node, value) => { touchedTextNodes.add(node); if (!originalTextMap.has(node)) { originalTextMap.set(node, value); } }; const rememberOriginalValue = (element, key, value) => { touchedElements.add(element); let item = originalValueMap.get(element); if (!item) { item = {}; originalValueMap.set(element, item); } if (!(key in item)) { item[key] = value; } }; const restoreTouchedContent = () => { touchedTextNodes.forEach((node) => { if (node.isConnected) { restoreNode(node); } }); touchedElements.forEach((element) => { if (element.isConnected) { restoreElement(element); } }); touchedTextNodes.clear(); touchedElements.clear(); originalTextMap = new WeakMap(); originalValueMap = new WeakMap(); }; const restoreNode = (node) => { if (!originalTextMap.has(node)) return; const original = originalTextMap.get(node); if (node.nodeValue !== original) { node.nodeValue = original; } }; const restoreElement = (element) => { const item = originalValueMap.get(element); if (!item) return; if ("value" in item && typeof element.value === "string" && element.value !== item.value) { element.value = item.value; } if ( "placeholder" in item && typeof element.placeholder === "string" && element.placeholder !== item.placeholder ) { element.placeholder = item.placeholder; } }; const updateTextNode = (node) => { const current = node.nodeValue; if (!current || !normalize(current)) return; if (!isReplacementActive()) return; rememberOriginalText(node, current); const base = originalTextMap.get(node) || current; const next = replaceText(base); if (next !== current) { node.nodeValue = next; } }; const updateElementValue = (element) => { if (!(element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement)) { return; } if (shouldSkipNode(element)) return; if (!isReplacementActive()) return; if (typeof element.placeholder === "string") { rememberOriginalValue(element, "placeholder", element.placeholder); const nextPlaceholder = replaceText( originalValueMap.get(element)?.placeholder || element.placeholder ); if (nextPlaceholder !== element.placeholder) { element.placeholder = nextPlaceholder; } } }; const walkAndReplace = (root) => { if (!root) return; const walker = document.createTreeWalker( root, NodeFilter.SHOW_TEXT, { acceptNode(node) { if (!node.parentElement) return NodeFilter.FILTER_REJECT; if (shouldSkipNode(node)) return NodeFilter.FILTER_REJECT; return NodeFilter.FILTER_ACCEPT; }, } ); let textNode = walker.nextNode(); while (textNode) { updateTextNode(textNode); textNode = walker.nextNode(); } if (root instanceof Element) { updateElementValue(root); root.querySelectorAll("input, textarea").forEach(updateElementValue); } }; const applyFontAdjust = () => { const active = Boolean(config.fontAdjust && isReplacementActive()); document.documentElement.classList.toggle("codex-msmail-font-adjust", active); }; const updateStatusText = (message) => { const statusNode = document.querySelector(`#${PANEL_ID} [data-role="status"]`); if (!statusNode) return; if (message) { statusNode.textContent = message; return; } if (!config.enabled) { statusNode.textContent = "替换功能已关闭,页面正常显示。"; return; } if (!hasValidSession()) { statusNode.textContent = "尚未开始计时,点击保存并应用后开始 4 小时倒计时。"; return; } if (hasExpired()) { statusNode.textContent = "已超过 4 小时,替换功能自动失效。"; return; } statusNode.textContent = `替换功能开启中,剩余时间: ${formatRemaining(getRemainingMs())}`; }; const syncStatusLoop = () => { if (statusUpdater) { clearInterval(statusUpdater); } statusUpdater = window.setInterval(() => { if (config.enabled && hasExpired()) { disableReplacement("已超过 4 小时,替换功能自动失效。"); return; } updateStatusText(); }, 1000); }; const applyReplacements = (statusMessage = "") => { if (applying) return; applying = true; try { applyFontAdjust(); if (isReplacementActive()) { getReplacementRoots().forEach(walkAndReplace); } updateStatusText(statusMessage); } finally { applying = false; } }; const stopFastScanLoop = () => { if (fastScanInterval) { clearInterval(fastScanInterval); fastScanInterval = null; } if (fastScanStopTimer) { clearTimeout(fastScanStopTimer); fastScanStopTimer = null; } }; const startFastScanLoop = () => { stopFastScanLoop(); const tick = () => { if (document.body) { applyReplacements(); } }; tick(); fastScanInterval = window.setInterval(tick, FAST_SCAN_INTERVAL_MS); fastScanStopTimer = window.setTimeout(() => { stopFastScanLoop(); }, FAST_SCAN_WINDOW_MS); }; const startObserver = () => { if (observer) observer.disconnect(); observer = new MutationObserver((mutations) => { if (applying) return; for (const mutation of mutations) { if (mutation.type === "characterData") { updateTextNode(mutation.target); continue; } mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.TEXT_NODE) { updateTextNode(node); } else if (node.nodeType === Node.ELEMENT_NODE) { walkAndReplace(node); } }); } }); observer.observe(document.documentElement, { childList: true, subtree: true, characterData: true, }); }; const ensureStyles = () => { if (document.getElementById(STYLE_ID)) return; const style = document.createElement("style"); style.id = STYLE_ID; style.textContent = ` .codex-msmail-font-adjust body, .codex-msmail-font-adjust input, .codex-msmail-font-adjust button, .codex-msmail-font-adjust textarea, .codex-msmail-font-adjust select { letter-spacing: 0.02em !important; } #${PANEL_ID} { position: fixed; top: max(12px, env(safe-area-inset-top)); left: 12px; right: 12px; z-index: 2147483647; background: rgba(247, 244, 237, 0.98); color: #2e2a26; border: 1px solid rgba(60, 49, 38, 0.16); border-radius: 10px; box-shadow: 0 18px 40px rgba(27, 22, 18, 0.18); padding: 12px; font: 13px/1.35 -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); max-height: calc(100vh - max(24px, env(safe-area-inset-top)) - max(24px, env(safe-area-inset-bottom))); overflow: hidden; } #${PANEL_ID}[hidden] { display: none !important; } #${PANEL_ID}.is-collapsed .codex-body { display: none; } #${PANEL_ID} .codex-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; } #${PANEL_ID} .codex-header-main { min-width: 0; } #${PANEL_ID} .codex-title { font-size: 18px; font-weight: 700; border: 0; background: transparent; color: #2e2a26; padding: 0; text-align: left; } #${PANEL_ID} .codex-close { border: 0; background: transparent; color: #4b433b; font-size: 14px; padding: 2px 4px; } #${PANEL_ID} .codex-meta { margin-top: 4px; color: #6d6256; display: grid; gap: 2px; word-break: break-all; } #${PANEL_ID} .codex-body { margin-top: 12px; display: grid; gap: 10px; max-height: calc(100vh - 180px); overflow-y: auto; overflow-x: hidden; padding-right: 2px; -webkit-overflow-scrolling: touch; } #${PANEL_ID} button, #${PANEL_ID} input, #${PANEL_ID} textarea, #${PANEL_ID} select { font: inherit; } #${PANEL_ID} .codex-action-row { display: flex; gap: 8px; flex-wrap: wrap; } #${PANEL_ID} .codex-btn { border: 0; border-radius: 999px; padding: 8px 12px; background: #ddd4c7; color: #352f29; } #${PANEL_ID} .codex-btn.primary { background: #c6b091; color: #201915; } #${PANEL_ID} .codex-input { width: 100%; box-sizing: border-box; border: 1px solid #d5c8b8; border-radius: 8px; padding: 9px 10px; background: rgba(255, 255, 255, 0.9); color: #2f2924; } #${PANEL_ID} .codex-check-row { display: flex; gap: 12px; flex-wrap: wrap; color: #433b34; } #${PANEL_ID} .codex-check { display: inline-flex; align-items: center; gap: 6px; padding: 6px 10px; border-radius: 999px; background: rgba(255, 255, 255, 0.68); } #${PANEL_ID} .codex-table { display: grid; gap: 8px; } #${PANEL_ID} .codex-rules-scroll { overflow-x: auto; overflow-y: visible; -webkit-overflow-scrolling: touch; padding-bottom: 4px; } #${PANEL_ID} .codex-rule-guide { display: grid; gap: 6px; padding: 10px 12px; border-radius: 8px; background: rgba(255, 255, 255, 0.62); color: #5c5248; } #${PANEL_ID} .codex-guide-strong { font-weight: 700; color: #2f2924; } #${PANEL_ID} .codex-table-head, #${PANEL_ID} .codex-rule-row { display: grid; grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1fr) 58px; gap: 8px; align-items: center; } #${PANEL_ID}.is-advanced .codex-table-head, #${PANEL_ID}.is-advanced .codex-rule-row { grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1fr) 112px 58px; } #${PANEL_ID} .codex-col-mode, #${PANEL_ID} .codex-mode-block { display: none; } #${PANEL_ID}.is-advanced .codex-col-mode, #${PANEL_ID}.is-advanced .codex-mode-block { display: block; } #${PANEL_ID} .codex-table-head { color: #5d5348; font-weight: 600; } #${PANEL_ID} .codex-rule-row { background: rgba(255, 255, 255, 0.48); border-radius: 14px; padding: 12px; border: 1px solid rgba(190, 176, 157, 0.5); } #${PANEL_ID} .codex-field-block { display: grid; gap: 4px; min-width: 0; } #${PANEL_ID} .codex-field-label { font-size: 11px; font-weight: 700; letter-spacing: 0.01em; } #${PANEL_ID} .codex-field-label.original { color: #8a5a26; } #${PANEL_ID} .codex-field-label.replacement { color: #1f6b45; } #${PANEL_ID} .codex-rule-row .codex-input.original { border-color: #d9bf9a; background: rgba(255, 248, 240, 0.95); } #${PANEL_ID} .codex-rule-row .codex-input.replacement { border-color: #9fc7ae; background: rgba(244, 255, 248, 0.95); } #${PANEL_ID} .codex-small-btn { border: 0; border-radius: 8px; background: #e4d9cb; color: #443b32; padding: 11px 8px; } #${PANEL_ID} .codex-status { color: #75685a; font-size: 12px; } #${PANEL_ID} .codex-rule-toggle { display: flex; align-items: center; justify-content: center; } #${PANEL_ID} .codex-rule-toggle input { width: 22px; height: 22px; } #${PANEL_ID} .codex-mode-block { display: grid; gap: 4px; } #${PANEL_ID} .codex-mode-label { font-size: 11px; font-weight: 700; color: #555048; } @media (max-width: 520px) { #${PANEL_ID} .codex-table-head, #${PANEL_ID} .codex-rule-row { grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1fr) 58px; } #${PANEL_ID}.is-advanced .codex-table-head, #${PANEL_ID}.is-advanced .codex-rule-row { grid-template-columns: 40px minmax(0, 1fr) minmax(0, 1fr) 104px 52px; } #${PANEL_ID} .codex-rule-row { gap: 10px; } } `; document.head.appendChild(style); }; const escapeHtml = (value) => value .replace(/&/g, "&") .replace(//g, ">") .replace(/"/g, """); const buildMetaText = () => [ `当前网站: ${location.hostname}`, ]; // 从会员页面 HTML 中提取用户信息字段(通用解析器) const extractMemberInfo = (html) => { const doc = new DOMParser().parseFromString(html, "text/html"); const fields = []; const push = (label, value) => { const v = String(value || "").replace(/\s+/g, " ").trim(); if (v && v.length <= 80 && !fields.some((f) => f.value === v)) { fields.push({ label: String(label || "").replace(/\s+/g, " ").trim(), value: v }); } }; // 1) dl > dt + dd 结构 doc.querySelectorAll("dl").forEach((dl) => { const dt = dl.querySelector("dt"); const dd = dl.querySelector("dd"); if (dt && dd) push(dt.textContent, dd.textContent); }); // 2) table 中 th + td 结构 doc.querySelectorAll("tr").forEach((tr) => { const th = tr.querySelector("th"); const td = tr.querySelector("td"); if (th && td) push(th.textContent, td.textContent); }); // 3) 输入框 value(文本类) doc.querySelectorAll("input").forEach((inp) => { const t = (inp.type || "text").toLowerCase(); if (["text", "email", "tel", "search"].includes(t) && inp.value) { const label = inp.getAttribute("aria-label") || inp.getAttribute("title") || inp.name || "入力値"; push(label, inp.value); } }); // 4) 常见日文会员字段名:找到标签元素,取其相邻值 const knownLabels = ["会員番号", "会員No", "会員ID", "氏名", "お名前", "フリガナ", "ニックネーム", "メールアドレス", "電話番号", "生年月日", "性別", "住所"]; const labelCandidates = doc.querySelectorAll("div, span, p, label, th, dt"); knownLabels.forEach((lb) => { const el = Array.from(labelCandidates).find((e) => e.textContent.replace(/\s+/g, "").trim() === lb); if (!el) return; const sibling = el.nextElementSibling; if (sibling) push(lb, sibling.textContent); }); // 5) 页面内嵌脚本变量 member_data(会員番号 / 生年月日 / 性別) const memberDataMatch = html.match(/var\s+member_data\s*=\s*(\{[\s\S]*?\})(?:\s*;)?\s*(?:<\/script>|$)/i); if (memberDataMatch) { try { const data = JSON.parse(memberDataMatch[1]); if (data.member_id) push("会員番号", data.member_id); if (data.birth) push("生年月日", data.birth); if (data.sex) push("性別", data.sex); } catch (e) { // 忽略解析失败 } } // 6) ポイント(現在のポイント:Nポイント,值前后可能夹着标签) const pointMatch = html.match(/現在のポイント[::][^0-9]*([\d,]+)[^0-9]*ポイント/); if (pointMatch) push("現在のポイント", pointMatch[1]); // 7) 页面标题 h1「XXX さんのマイページ」→ 昵称 const titleMatch = html.match(/]*>([^<]*?)\s*さんのマイページ<\/h1>/); if (titleMatch && titleMatch[1]) push("ニックネーム(页面标题)", titleMatch[1]); return fields; }; const createRuleRowHtml = (rule, index) => `
网页当前显示的原文字
你想显示的新文字
模式
`; const getPanel = () => document.getElementById(PANEL_ID); const syncPanelVisibility = () => { const panel = getPanel(); if (!panel) return; panel.hidden = !config.panelVisible; panel.classList.toggle("is-collapsed", Boolean(config.bodyCollapsed)); }; const showPanel = () => { config.panelVisible = true; saveConfig(); syncPanelVisibility(); updateStatusText("设置面板已打开。"); }; const hidePanel = () => { config.panelVisible = false; saveConfig(); syncPanelVisibility(); }; const togglePanelBody = () => { config.bodyCollapsed = !config.bodyCollapsed; saveConfig(); syncPanelVisibility(); }; const startSessionNow = () => { config.sessionStartedAt = Date.now(); }; const disableReplacement = (message = "替换功能已关闭,页面正常显示。") => { config.enabled = false; config.sessionStartedAt = null; saveConfig(); hidePanel(); }; const clearHoldTimer = () => { if (holdTimer) { clearTimeout(holdTimer); holdTimer = null; } threeFingerHold = false; }; const clearSelection = () => { const sel = window.getSelection && window.getSelection(); if (sel && typeof sel.removeAllRanges === "function") { sel.removeAllRanges(); } }; const recordTripleClick = (clientY) => { if (config.panelVisible) return; if (clientY > PANEL_HOLD_ZONE_PX) return; const now = Date.now(); tripleClickTimes = tripleClickTimes.filter( (t) => now - t.time <= TRIPLE_CLICK_WINDOW_MS ); tripleClickTimes.push({ time: now, y: clientY }); if (tripleClickTimes.length < 3) return; const ys = tripleClickTimes.map((t) => t.y); const spread = Math.max(...ys) - Math.min(...ys); tripleClickTimes = []; if (spread <= TRIPLE_CLICK_MAX_SPREAD) { showPanel(); setTimeout(clearSelection, 0); } }; const startTopHoldDetector = () => { const beginHold = (clientY, isThreeFinger = false) => { if (config.panelVisible) return; if (!isThreeFinger && clientY > PANEL_HOLD_ZONE_PX) return; clearHoldTimer(); topHoldStartY = clientY; threeFingerHold = isThreeFinger; holdTimer = window.setTimeout(() => { holdTimer = null; threeFingerHold = false; showPanel(); }, PANEL_HOLD_MS); }; const moveHold = (clientY, touchesLength = 1) => { if (!holdTimer) return; if (threeFingerHold && touchesLength !== 3) { clearHoldTimer(); return; } if ( Math.abs(clientY - topHoldStartY) > 14 || (!threeFingerHold && clientY > PANEL_HOLD_ZONE_PX + 20) ) { clearHoldTimer(); } }; document.addEventListener( "touchstart", (event) => { if (event.touches.length === 1) { beginHold(event.touches[0].clientY, false); return; } if (event.touches.length === 3) { beginHold(event.touches[0].clientY, true); return; } clearHoldTimer(); }, { passive: true } ); document.addEventListener( "touchmove", (event) => { if (event.touches.length < 1) { clearHoldTimer(); return; } moveHold(event.touches[0].clientY, event.touches.length); }, { passive: true } ); document.addEventListener("touchend", clearHoldTimer, { passive: true }); document.addEventListener("touchcancel", clearHoldTimer, { passive: true }); document.addEventListener("mousedown", (event) => { beginHold(event.clientY); // 顶部区域内、且已有近期点击(正在形成三击的后续点击)→ 阻止浏览器选中文本 const isInZone = event.clientY <= PANEL_HOLD_ZONE_PX; const lastTap = tripleClickTimes.length > 0 ? tripleClickTimes[tripleClickTimes.length - 1] : null; const isChainTap = lastTap !== null && Date.now() - lastTap.time <= TRIPLE_CLICK_WINDOW_MS; if (isInZone && isChainTap && event.defaultPrevented === false) { event.preventDefault(); } }); document.addEventListener("mousemove", (event) => { moveHold(event.clientY); }); document.addEventListener("mouseup", (event) => { clearHoldTimer(); recordTripleClick(event.clientY); }); document.addEventListener("mouseleave", clearHoldTimer); }; const buildPanel = () => { if (document.getElementById(PANEL_ID)) return; ensureStyles(); const panel = document.createElement("section"); panel.id = PANEL_ID; panel.innerHTML = `
${buildMetaText() .map((line) => `
${escapeHtml(line)}
`) .join("")}
左边填网页当前显示的内容,右边填你想显示的新内容。
规则会替换页面里匹配到的可见文字;登录框里手动输入的内容不会被修改。
示例: 原文字 user@example.com -> 新文字 alias@example.com
原文字
新文字
模式
`; const rulesContainer = panel.querySelector(".codex-rules"); const advancedToggleBtn = panel.querySelector('[data-action="toggle-advanced"]'); const syncAdvancedMode = () => { const hasRegex = (config.rules || []).some((r) => r.mode === "regex"); panel.classList.toggle("is-advanced", hasRegex); if (advancedToggleBtn) { advancedToggleBtn.textContent = hasRegex ? "↩ 返回简单替换" : "⚙ 高级替换(正则)"; } }; const renderRules = () => { if (!rulesContainer) return; rulesContainer.innerHTML = config.rules.map((rule, index) => createRuleRowHtml(rule, index)).join(""); syncAdvancedMode(); }; const syncFields = () => { panel.querySelectorAll("[data-field]").forEach((input) => { const field = input.getAttribute("data-field"); if (!field) return; if (input instanceof HTMLInputElement && input.type === "checkbox") { input.checked = Boolean(config[field]); } else if (input instanceof HTMLInputElement) { input.value = String(config[field] || ""); } }); }; const readFields = () => { panel.querySelectorAll("[data-field]").forEach((input) => { const field = input.getAttribute("data-field"); if (!field) return; if (input instanceof HTMLInputElement && input.type === "checkbox") { config[field] = input.checked; } else if (input instanceof HTMLInputElement) { config[field] = input.value.trim(); } }); }; const readRules = () => { const nextRules = []; panel.querySelectorAll(".codex-rule-row").forEach((row) => { const enabled = row.querySelector('[data-rule-field="enabled"]'); const original = row.querySelector('[data-rule-field="original"]'); const replacement = row.querySelector('[data-rule-field="replacement"]'); const mode = row.querySelector('[data-rule-field="mode"]'); nextRules.push({ enabled: enabled instanceof HTMLInputElement ? enabled.checked : true, original: original instanceof HTMLInputElement ? original.value.trim() : "", replacement: replacement instanceof HTMLInputElement ? replacement.value.trim() : "", mode: mode instanceof HTMLSelectElement && mode.value === "regex" ? "regex" : "normal", }); }); config.rules = nextRules.length ? nextRules : cloneRules(DEFAULT_RULES); }; const handleFetchMember = async () => { if (location.hostname !== "parks2.bandainamco-am.co.jp") { updateStatusText("⚠️ 请先在 Bandai Parks 网站(parks2.bandainamco-am.co.jp)上使用此功能。"); return; } updateStatusText("⏳ 正在获取会员信息…"); try { const resp = await fetch("https://parks2.bandainamco-am.co.jp/member_mypage.html", { credentials: "include", }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const html = await resp.text(); const fields = extractMemberInfo(html); if (!fields.length) { updateStatusText("⚠️ 未获取到用户信息(可能未登录或页面结构变化)。"); return; } readRules(); // 清掉空行,避免和抓取到的信息混在一起 config.rules = config.rules.filter((r) => r.original.trim() !== ""); // 每个字段值作为一条规则的「原文字」,新文字留空由用户填写 fields.forEach((f) => { config.rules.push({ enabled: true, original: f.value, replacement: "", mode: "normal" }); }); if (!config.rules.length) config.rules = cloneRules(DEFAULT_RULES); renderRules(); updateStatusText(`✅ 已获取 ${fields.length} 项用户信息,请在「新文字」中填写要显示的内容。`); } catch (e) { updateStatusText("⚠️ 获取失败: " + (e && e.message ? e.message : "未知错误")); } }; panel.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; const action = target.getAttribute("data-action"); if (!action) return; if (action === "toggle-advanced") { const isAdvanced = panel.classList.toggle("is-advanced"); target.textContent = isAdvanced ? "↩ 返回简单替换" : "⚙ 高级替换(正则)"; updateStatusText(isAdvanced ? "已开启高级替换,支持正则匹配。" : "已关闭高级替换,仅普通匹配。"); return; } if (action === "toggle-body") { togglePanelBody(); return; } if (action === "hide-panel") { hidePanel(); return; } if (action === "fetch-member") { handleFetchMember(); return; } if (action === "add-rule") { readRules(); config.rules.push({ enabled: true, original: "", replacement: "", mode: "normal" }); renderRules(); updateStatusText("已添加一条规则。"); return; } if (action === "remove-rule") { const row = target.closest(".codex-rule-row"); if (!row) return; const index = Number(row.getAttribute("data-index")); readRules(); config.rules.splice(index, 1); if (!config.rules.length) { config.rules = cloneRules(DEFAULT_RULES); } renderRules(); updateStatusText("规则已删除。"); return; } if (action === "save") { readFields(); readRules(); config.enabled = true; startSessionNow(); saveConfig(); applyReplacements( `已应用 ${config.rules.filter((rule) => rule.enabled && rule.original && rule.replacement).length} 条规则,4 小时后自动失效。` ); return; } if (action === "disable") { disableReplacement(); return; } if (action === "restart-hour") { readFields(); readRules(); config.enabled = true; startSessionNow(); saveConfig(); applyReplacements("已重新开始计时,4 小时后自动失效。"); } }); syncFields(); renderRules(); document.body.appendChild(panel); syncPanelVisibility(); updateStatusText("长按顶部 2 秒,或快速三击顶部区域可再次打开设置面板。"); }; const boot = () => { if (config.enabled && hasExpired()) { config.enabled = false; config.sessionStartedAt = null; saveConfig(); } config.panelVisible = false; startObserver(); startTopHoldDetector(); syncStatusLoop(); const mountUi = () => { if (!document.body) { requestAnimationFrame(mountUi); return; } buildPanel(); applyReplacements(); startFastScanLoop(); }; mountUi(); document.addEventListener("readystatechange", () => { applyReplacements(); }); window.addEventListener("load", () => { applyReplacements(); startFastScanLoop(); }); }; boot(); })();