替换文本

可影响输入框中的内容,支持自定义设置

// ==UserScript==
// @name         替换文本
// @license      MIT
// @namespace    https://github.com/laiyoi/GM_scripts
// @version      1.0.4
// @description  可影响输入框中的内容,支持自定义设置
// @author       laiyoi
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @grant        GM_openInTab
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/sweetalert2.all.min.js
// ==/UserScript==

// 默认字典,如果没有保存过,则使用这个
let dictionary = GM_getValue("dictionary", {});

// 是否影响输入框
let affectInput = GM_getValue('setting_affect_input', true);

// 统计字典替换成功的次数
let settingSuccessTimes = GM_getValue('setting_success_times', 0);

// 显示设置框
function showSettingBox() {
  let html = `
    <div style="font-size: 1em;">
      <label class="panai-setting-label">
        影响输入框的替换
        <input type="checkbox" id="S-Affect-Input" ${affectInput ? 'checked' : ''} class="panai-setting-checkbox">
      </label>
      <h3>自定义替换词典</h3>
      <div>
        <label>关键词:</label>
        <input type="text" id="key" placeholder="输入关键词" />
      </div>
      <div>
        <label>替换文本:</label>
        <input type="text" id="value" placeholder="输入替换文本" />
      </div>
      <button id="addEntry">添加替换</button>
      <div>
        <h4>当前替换项</h4>
        <ul id="dictionaryList"></ul>
      </div>
      <div>
        <button id="importSettings">导入设置</button>
        <button id="exportSettings">导出设置</button>
      </div>
    </div>
  `;

  Swal.fire({
    title: '字典替换配置',
    html,
    icon: 'info',
    showCloseButton: true,
    confirmButtonText: '保存',
    footer: '<div style="text-align: center;font-size: 1em;">助手免费开源,Powered by <a href="https://www.example.com">example</a></div>',
    customClass: 'panai-setting-box'
  }).then((res) => {
    if (res.isConfirmed) {
      // 保存字典设置
      GM_setValue('setting_affect_input', document.getElementById('S-Affect-Input').checked);
      GM_setValue("dictionary", dictionary);
      res.isConfirmed && history.go(0);
    }
  });

  const keyInput = document.getElementById("key");
  const valueInput = document.getElementById("value");
  const dictionaryList = document.getElementById("dictionaryList");
  const addButton = document.getElementById("addEntry");
  const affectInputCheckbox = document.getElementById("S-Affect-Input");

  // 更新显示的字典列表
  function updateDictionaryList() {
    dictionaryList.innerHTML = "";
  
    // Create a wrapper for the dictionary list to make it scrollable
    const scrollWrapper = document.createElement("div");
    scrollWrapper.style.maxHeight = "300px"; // Limit the height of the dictionary list
    scrollWrapper.style.overflowY = "auto"; // Enable vertical scrolling if content overflows
    scrollWrapper.style.paddingRight = "5px"; // Add some space for scrollbar
  
    for (const [key, value] of Object.entries(dictionary)) {
      const listItem = document.createElement("li");
      
      // Compact display: use a shorter format
      listItem.textContent = `${key} → ${value}`;
  
      // Create delete button
      const deleteButton = document.createElement("button");
      deleteButton.textContent = "删除";
      deleteButton.style.marginLeft = "10px";
      deleteButton.style.fontSize = "0.8em"; // Reduce button size
      deleteButton.addEventListener("click", () => {
        delete dictionary[key];
        updateDictionaryList(); // 更新显示的字典列表
      });
  
      // Append delete button and the list item
      listItem.appendChild(deleteButton);
  
      // Style list items for more compact display
      listItem.style.display = "flex"; // Use flexbox for compact layout
      listItem.style.justifyContent = "space-between"; // Space between text and delete button
      listItem.style.marginBottom = "5px"; // Reduce spacing between items
  
      scrollWrapper.appendChild(listItem); // Add list item to the scrollable container
    }
  
    dictionaryList.appendChild(scrollWrapper); // Add the scrollable wrapper to the dictionary list container
  }

  // 添加替换项
  addButton.addEventListener("click", () => {
    const key = keyInput.value.trim();
    const value = valueInput.value.trim();

    if (key && value) {
      dictionary[key] = value;
      keyInput.value = "";
      valueInput.value = "";
      updateDictionaryList();
    }
  });

  // 导出设置为JSON(不包含 affectInput)
  document.getElementById("exportSettings").addEventListener("click", () => {
    const settingsJSON = JSON.stringify({ dictionary }); // 只导出字典
    const blob = new Blob([settingsJSON], { type: "application/json" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = "settings.json";
    a.click();
  });

  // 导入设置(不影响 affectInput)
  document.getElementById("importSettings").addEventListener("click", () => {
    Swal.fire({
      title: '选择导入文件',
      input: 'file',
      inputAttributes: {
        accept: '.json',
        'aria-label': 'Upload your settings'
      },
      showCancelButton: true,
    }).then((result) => {
      if (result.isConfirmed && result.value) {
        const file = result.value;
        const reader = new FileReader();
        
        reader.onload = function(event) {
          try {
            const importedSettings = JSON.parse(event.target.result);

            // 校验导入内容是否包含字典
            if (importedSettings.hasOwnProperty('dictionary')) {
              // 合并导入的字典到现有字典中
              dictionary = { ...dictionary, ...importedSettings.dictionary };

              updateDictionaryList(); // 更新显示的字典列表
              Swal.fire('设置已成功导入!');
            } else {
              throw new Error('导入的文件格式不正确');
            }
          } catch (error) {
            Swal.fire('导入失败', `错误信息:${error.message}`, 'error');
          }
        };

        reader.onerror = function() {
          Swal.fire('导入失败', '文件读取错误,请确保文件格式正确', 'error');
        };

        reader.readAsText(file);
      }
    });
  });

  // 初始化页面显示字典
  updateDictionaryList();
  affectInputCheckbox.checked = GM_getValue('setting_affect_input', true);
}

// 替换页面中的文本
function replacer(str) {
  const dictionary_ = {
    '湖人': '科比',
    '豆包': '董斌',
    '超越': '超载',
  }
  // prereplace
  for (const [key_, value_] of Object.entries(dictionary_)) {
    const regex_ = new RegExp(key_, 'g');
    str = str.replace(regex_, value_);
  }

  for (const [key, value] of Object.entries(dictionary)) {
    const regex = new RegExp(key, 'g');
    str = str.replace(regex, value);
  }
  return str;
}

const elementToMatch = [
  "title",
  "h1",
  "h2",
  "h3",
  "h4",
  "h5",
  "h6",
  "p",
  "article",
  "section",
  "blockquote",
  "li",
  "a",
  "CC",
  "span",
];

// 替换页面中的文本内容
function replace(root) {
  requestIdleCallback(() => {
    root
      .querySelectorAll(
        elementToMatch
          .concat(elementToMatch.map((name) => name + " *"))
          .concat(affectInput ? ["input"] : [])
          .join(",")
    ).forEach((candidate) => {
      if (!candidate.closest('.panai-setting-box')) { // 排除设置页面的内容
        if (candidate.nodeName === "INPUT" && affectInput) {
          candidate.value = replacer(candidate.value);
        } else if (candidate.textContent && candidate.textContent == candidate.innerHTML.trim()) {
          candidate.textContent = replacer(candidate.textContent);
        } else if (Array.from(candidate.childNodes).filter((c) => c.nodeName == "BR")) {
          Array.from(candidate.childNodes).forEach((maybeText) => {
            if (maybeText.nodeType === Node.TEXT_NODE) {
              maybeText.textContent = replacer(maybeText.textContent);
            }
          });
        }
      }
    });
  });
}

/**
 * @param {Element} root
 */
async function afterDomLoaded(root) {
  if (!root) return;

  const fn = () => {
    replace(root);
    root.querySelectorAll("*").forEach(async (node) => {
      if (node.shadowRoot) {
        await afterDomLoaded(node.shadowRoot);
      }
    });
  };

  while (document.readyState === "loading") {
    await new Promise((r) => setTimeout(r, 1000));
  }
  fn();
}

// 初始执行
afterDomLoaded(document);
setInterval(() => afterDomLoaded(document), 2500);

// 注册菜单命令
GM_registerMenuCommand('⚙️ 设置', () => {
  showSettingBox();
});