User:Stumblean/common.js

Revision as of 01:04, 27 June 2025 by Stumblean (talk | contribs)

Note: After publishing, you may have to bypass your browser's cache to see the changes.

  • Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
  • Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
  • Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
(function () {
  const iconURL = "https://files.catbox.moe/wk78nl.jpg";

  // Create menu
  const menu = document.createElement("div");
  menu.id = "mahitoMenu";
  Object.assign(menu.style, {
    position: "fixed",
    top: "20px",
    right: "20px",
    padding: "10px",
    backgroundColor: "#111",
    color: "#fff",
    border: "2px solid #666",
    zIndex: "9999",
    fontFamily: "monospace",
    borderRadius: "10px",
    boxShadow: "0 0 10px rgba(0,0,0,0.5)",
    userSelect: "none",
    touchAction: "none",
  });

  // Header
  const header = document.createElement("div");
  Object.assign(header.style, {
    cursor: "grab",
    display: "flex",
    justifyContent: "center",
    alignItems: "center",
    marginBottom: "8px",
    gap: "6px",
  });

  // Left icon
  const leftIcon = document.createElement("img");
  leftIcon.src = iconURL;
  leftIcon.width = 15;
  leftIcon.height = 15;
  leftIcon.alt = "Mahito icon";
  leftIcon.style.borderRadius = "3px";

  // Title text
  const title = document.createElement("div");
  title.textContent = "Mahito Menu";
  title.style.fontWeight = "bold";

  // Right icon
  const rightIcon = document.createElement("img");
  rightIcon.src = iconURL;
  rightIcon.width = 15;
  rightIcon.height = 15;
  rightIcon.alt = "Mahito icon";
  rightIcon.style.borderRadius = "3px";

  header.appendChild(leftIcon);
  header.appendChild(title);
  header.appendChild(rightIcon);

  menu.appendChild(header);

  // Content area
  const content = document.createElement("div");

  // Mass edit button
  const massEditBtn = document.createElement("button");
  massEditBtn.textContent = "Mass edit";
  massEditBtn.title = "It allows you to edit any line on any page(s)";
  Object.assign(massEditBtn.style, {
    background: "#555",
    color: "#fff",
    border: "none",
    padding: "5px 10px",
    cursor: "pointer",
    marginBottom: "6px",
    width: "100%",
    textAlign: "left",
  });

  // Open Explanation button
  const openBtn = document.createElement("button");
  openBtn.textContent = "Open Explanation";
  Object.assign(openBtn.style, {
    background: "#444",
    color: "#fff",
    border: "none",
    padding: "5px 10px",
    cursor: "pointer",
    width: "100%",
    textAlign: "left",
  });
  openBtn.onclick = () => console.log("Open Explanation clicked");

  content.appendChild(massEditBtn);
  content.appendChild(openBtn);
  menu.appendChild(content);
  document.body.appendChild(menu);

  // Modal overlay & form for mass edit
  const modal = document.createElement("div");
  Object.assign(modal.style, {
    display: "none",
    position: "fixed",
    top: 0,
    left: 0,
    width: "100vw",
    height: "100vh",
    backgroundColor: "rgba(0,0,0,0.8)",
    zIndex: "10000",
    justifyContent: "center",
    alignItems: "center",
  });

  const form = document.createElement("form");
  Object.assign(form.style, {
    backgroundColor: "#222",
    padding: "20px",
    borderRadius: "8px",
    color: "#fff",
    maxWidth: "400px",
    width: "90%",
    display: "flex",
    flexDirection: "column",
    gap: "10px",
  });

  form.innerHTML = `
    <label>Pages to edit (one per line):</label>
    <textarea id="massEditPages" rows="5" style="width:100%;"></textarea>

    <label>Text 1:</label>
    <textarea id="massEditText1" rows="3" style="width:100%;"></textarea>

    <label>Text 2:</label>
    <textarea id="massEditText2" rows="3" style="width:100%;"></textarea>

    <label>Edit type:</label>
    <select id="massEditType" style="width:100%;">
      <option value="prepend">Prepend text 1</option>
      <option value="append">Append text 1</option>
      <option value="bothpend">Prepend text 1 and append text 2</option>
      <option value="replacetext">Replace first instance of text 1 with text 2</option>
      <option value="replacetextg">Replace all instances of text 1 with text 2</option>
      <option value="replacepage">Replace page with text 1</option>
    </select>

    <label>Edit summary:</label>
    <input id="massEditSummary" type="text" style="width:100%;" />

    <label><input id="massEditMinor" type="checkbox" /> Mark edit as minor</label>

    <button type="submit" style="padding:8px; background:#555; border:none; color:#fff; cursor:pointer;">
      Submit Mass Edit
    </button>
    <button type="button" id="massEditCancel" style="padding:8px; background:#333; border:none; color:#fff; cursor:pointer;">
      Cancel
    </button>

    <div id="massEditResult" style="margin-top:10px;"></div>
  `;

  modal.appendChild(form);
  document.body.appendChild(modal);

  massEditBtn.onclick = () => {
    modal.style.display = "flex";
  };

  document.getElementById("massEditCancel").onclick = (e) => {
    e.preventDefault();
    modal.style.display = "none";
    document.getElementById("massEditResult").textContent = "";
  };

  // Mass Edit function (your code slightly adapted for async)
  async function doMassEdit(pages, text1, text2, editType, summary, minor) {
    let edited = 0;
    const failed = [];
    const errors = [];

    function getPageText(title) {
      return fetch(
        mw.config.get("wgScriptPath") +
          "/api.php?action=query&prop=revisions&rvprop=content&format=json&indexpageids=1&titles=" +
          encodeURIComponent(title)
      )
        .then((res) => res.json())
        .then((response) => {
          const pageid = response.query.pageids[0];
          if (pageid === "-1") return "";
          return response.query.pages[pageid].revisions[0]["*"];
        });
    }

    for (let article of pages) {
      if (!article.trim()) continue;

      try {
        // Get token
        const tokenResp = await fetch(
          mw.config.get("wgScriptPath") +
            "/api.php?format=json&action=query&prop=info&meta=tokens&type=csrf&titles=" +
            encodeURIComponent(article)
        );
        const tokenJson = await tokenResp.json();
        const edittoken = tokenJson.query.tokens.csrftoken;

        // Get original page text if needed
        let newPageText = "";
        if (
          editType === "replacetext" ||
          editType === "replacetextg" ||
          editType === "bothpend"
        ) {
          let pagetext = await getPageText(article);
          if (editType === "replacetextg") {
            const escapedText1 = text1.replace(new RegExp("[.*+?|(){}\\\\\", "g"), "\\$&");
            const reg = new RegExp(escapedText1, "g");
            pagetext = pagetext.replace(reg, text2);
          } else if (editType === "replacetext") {
            pagetext = pagetext.replace(text1, text2);
          } else {
            pagetext = text1 + pagetext + text2;
          }
          newPageText = pagetext;
        }

        let postData = new URLSearchParams();
        postData.append("format", "json");
        postData.append("action", "edit");
        postData.append("watchlist", "nochange");
        postData.append("title", article);
        postData.append("summary", summary);
        postData.append("token", edittoken);
        if (minor) postData.append("minor", "1");
        else postData.append("notminor", "1");

        if (editType === "prepend") {
          postData.append("prependtext", text1 + "\n");
        } else if (editType === "append") {
          postData.append("appendtext", "\n" + text1);
        } else if (editType === "replacepage") {
          postData.append("text", text1);
        } else if (
          editType === "replacetext" ||
          editType === "replacetextg" ||
          editType === "bothpend"
        ) {
          postData.append("text", newPageText);
        }

        const editResp = await fetch(mw.config.get("wgScriptPath") + "/api.php", {
          method: "POST",
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
          body: postData.toString(),
        });

        const editJson = await editResp.json();

        if (editJson.edit) {
          edited++;
          document.getElementById("massEditResult").textContent =
            `Edited ${edited} pages...`;
        } else {
          failed.push(article);
          errors.push(editJson.error?.info || "Unknown error");
        }
      } catch (err) {
        failed.push(article);
        errors.push(err.message);
      }
    }

    // Show summary
    if (failed.length > 0) {
      document.getElementById("massEditResult").innerHTML =
        `<b>Done with errors.</b><br>Edited: ${edited}<br>Failed:` +
        failed
          .map(
            (page, i) =>
              `<div>${page}: ${errors[i]}</div>`
          )
          .join("");
    } else {
      document.getElementById("massEditResult").innerHTML =
        `<b>All done!</b><br>Edited: ${edited}`;
    }
  }

  // Handle form submit
  form.onsubmit = async (e) => {
    e.preventDefault();
    document.getElementById("massEditResult").textContent = "Working...";
    const pages = document
      .getElementById("massEditPages")
      .value.split("\n")
      .map((x) => x.trim())
      .filter((x) => x);
    const text1 = document.getElementById("massEditText1").value;
    const text2 = document.getElementById("massEditText2").value;
    const editType = document.getElementById("massEditType").value;
    const summary = document.getElementById("massEditSummary").value;
    const minor = document.getElementById("massEditMinor").checked;

    await doMassEdit(pages, text1, text2, editType, summary, minor);
  };

  // Drag logic
  let isDragging = false,
    offsetX = 0,
    offsetY = 0;

  function startDrag(x, y) {
    isDragging = true;
    const rect = menu.getBoundingClientRect();
    offsetX = x - rect.left;
    offsetY = y - rect.top;
    menu.style.right = "auto";
  }

  function doDrag(x, y) {
    if (isDragging) {
      menu.style.left = x - offsetX + "px";
      menu.style.top = y - offsetY + "px";
    }
  }

  function stopDrag() {
    isDragging = false;
  }

  header.addEventListener("mousedown", function (e) {
    startDrag(e.clientX, e.clientY);
    e.preventDefault();
  });

  document.addEventListener("mousemove", function (e) {
    doDrag(e.clientX, e.clientY);
  });

  document.addEventListener("mouseup", stopDrag);

  header.addEventListener("touchstart", function (e) {
    const touch = e.touches[0];
    startDrag(touch.clientX, touch.clientY);
    e.preventDefault();
  });

  document.addEventListener("touchmove", function (e) {
    if (!isDragging) return;
    const touch = e.touches[0];
    doDrag(touch.clientX, touch.clientY);
  });

  document.addEventListener("touchend", stopDrag);
})();