diff --git a/code-v0.6.1.user.js b/code-v0.6.1.user.js
new file mode 100644
index 0000000..d0474f6
--- /dev/null
+++ b/code-v0.6.1.user.js
@@ -0,0 +1,1244 @@
+// ==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 = `
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
左边填网页当前显示的内容,右边填你想显示的新内容。
+
规则会替换页面里匹配到的可见文字;登录框里手动输入的内容不会被修改。
+
示例: 原文字 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();
+})();
+
diff --git a/code-v1.6.1.user.js b/code-v1.6.1.user.js
new file mode 100644
index 0000000..3c9057b
--- /dev/null
+++ b/code-v1.6.1.user.js
@@ -0,0 +1,1166 @@
+// ==UserScript==
+// @name NAMCO Parks 改个人信息 *(通用版本)
+// @namespace https://parks2.bandainamco-am.co.jp/
+// @version 1.6.0
+// @description 改会员资料姓名/生日/性别;可隐藏按钮与券面强制显示;支持 Excel 复制快速填充
+// @grant unsafeWindow
+// @author park-tools
+// @match https://parks2.bandainamco-am.co.jp/*
+// @icon https://parks2.bandainamco-am.co.jp/client_info/BNAM_LBC_EC/view/userweb/favicon.ico
+// @run-at document-end
+// @grant GM_setValue
+// @grant GM_getValue
+// @grant GM_deleteValue
+// ==/UserScript==
+
+(function () {
+ 'use strict';
+
+ const ORIGIN = 'https://parks2.bandainamco-am.co.jp';
+ const LS_KEY = 'namco_rename_draft_v1';
+ const LS_OVERLAY = 'namco_ticket_overlay_v1';
+ const LS_HIDE_UI = 'namco_hide_plugin_ui_v1';
+
+ const TICKET_PATH_RE = /\/admission_(use_)?ticket\.html/i;
+ const PAGE = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
+
+ function isLoggedInFromDom() {
+ if (document.querySelector('a[href*="logoff"], a[href*="request=logoff"]')) return true;
+ const html = document.documentElement.innerHTML;
+ if (html.includes('ログアウト')) return true;
+ return !!parseMemberData(html).member_id;
+ }
+
+ function htmlLooksLoggedIn(html) {
+ if (!html) return false;
+ if (html.includes('ログアウト')) return true;
+ if (parseMemberData(html).member_id) return true;
+ if (parseInput(html, 'PC_MAIL') && (parseInput(html, 'TEL') || parseInput(html, 'L_NAME'))) return true;
+ return false;
+ }
+
+ /** iOS Tampermonkey 沙箱 fetch 不带 Cookie;结果放页面 window,避免把整页 HTML 塞进 DOM 属性被截断 */
+ function pageFetch(url, options) {
+ return new Promise((resolve, reject) => {
+ const id = '__npFetch_' + Date.now() + '_' + Math.random().toString(36).slice(2);
+ const opt = {
+ method: (options && options.method) || 'GET',
+ credentials: 'same-origin',
+ redirect: 'follow',
+ headers: (options && options.headers) || {},
+ };
+ if (options && options.body) opt.body = options.body;
+ const win = PAGE;
+ const script = document.createElement('script');
+ script.textContent =
+ '(function(){var id=' +
+ JSON.stringify(id) +
+ ';window[id]={p:1};fetch(' +
+ JSON.stringify(url) +
+ ',' +
+ JSON.stringify(opt) +
+ ').then(function(r){return r.text().then(function(t){window[id]={s:r.status,u:r.url,t:t};});}).catch(function(e){window[id]={e:String(e&&e.message||e)};});})();';
+ document.documentElement.appendChild(script);
+ script.remove();
+
+ const start = Date.now();
+ const timer = setInterval(() => {
+ const box = (win && win[id]) || window[id];
+ if (box && box.e) {
+ clearInterval(timer);
+ try { delete win[id]; } catch (e) { /* ignore */ }
+ reject(new Error(box.e));
+ return;
+ }
+ if (box && typeof box.t === 'string') {
+ clearInterval(timer);
+ const out = { status: Number(box.s || 0), url: box.u || url, text: box.t };
+ try { delete win[id]; } catch (e) { /* ignore */ }
+ resolve(out);
+ return;
+ }
+ if (Date.now() - start > 90000) {
+ clearInterval(timer);
+ try { delete win[id]; } catch (e) { /* ignore */ }
+ reject(new Error('请求超时'));
+ }
+ }, 40);
+ });
+ }
+
+ async function httpGet(path, referer) {
+ const url = path.startsWith('http') ? path : ORIGIN + path;
+ const headers = { Referer: referer || ORIGIN + '/member_mypage.html' };
+ try {
+ return await pageFetch(url, { method: 'GET', headers });
+ } catch (e1) {
+ try {
+ const r = await PAGE.fetch(url, { method: 'GET', credentials: 'include', headers });
+ return { status: r.status, text: await r.text(), url: r.url };
+ } catch (e2) {
+ throw e1;
+ }
+ }
+ }
+
+ async function httpPost(path, body, referer) {
+ const url = path.startsWith('http') ? path : ORIGIN + path;
+ const headers = {
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
+ Origin: ORIGIN,
+ Referer: referer || ORIGIN + '/member_regist.html?request=edit',
+ };
+ const bodyStr = new URLSearchParams(body).toString();
+ try {
+ return await pageFetch(url, { method: 'POST', headers, body: bodyStr });
+ } catch (e1) {
+ try {
+ const r = await PAGE.fetch(url, { method: 'POST', credentials: 'include', headers, body: bodyStr });
+ return { status: r.status, text: await r.text(), url: r.url };
+ } catch (e2) {
+ throw e1;
+ }
+ }
+ }
+
+ const store = {
+ get(k, def) {
+ try {
+ if (typeof GM_getValue === 'function') return GM_getValue(k, def);
+ } catch (e) { /* ignore */ }
+ try {
+ const raw = localStorage.getItem(k);
+ return raw == null ? def : JSON.parse(raw);
+ } catch (e2) {
+ return def;
+ }
+ },
+ set(k, v) {
+ try {
+ if (typeof GM_setValue === 'function') GM_setValue(k, v);
+ } catch (e) { /* ignore */ }
+ try {
+ localStorage.setItem(k, JSON.stringify(v));
+ } catch (e2) { /* ignore */ }
+ },
+ };
+
+ function $(sel, root) {
+ return (root || document).querySelector(sel);
+ }
+
+ function escapeRe(name) {
+ return String(name).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+ }
+
+ function parseInput(html, name) {
+ const re = new RegExp(
+ ']*\\bname=["\']' + escapeRe(name) + '["\'][^>]*>',
+ 'i'
+ );
+ const m = html.match(re);
+ if (!m) return '';
+ const v = m[0].match(/\bvalue=["']([^"']*)["']/i);
+ return v ? v[1].trim() : '';
+ }
+
+ function parseSelected(html, name) {
+ const sm = html.match(
+ new RegExp('', 'i')
+ );
+ if (!sm) return '';
+ const opt = sm[1].match(/