OSRS Wiki Auto-Navbox with UI, Adaptive Speed, Duplicate Checker (Slow Version)

Adds listed pages to a Navbox upon request with UI, CSRF token, adaptive speed, duplicate checker, and highlighted links option.

当前为 2024-11-04 提交的版本,查看 最新版本

// ==UserScript==
// @name         OSRS Wiki Auto-Navbox with UI, Adaptive Speed, Duplicate Checker (Slow Version)
// @namespace    https://github.com/Nick2bad4u/UserStyles
// @version      5.2
// @description  Adds listed pages to a Navbox upon request with UI, CSRF token, adaptive speed, duplicate checker, and highlighted links option.
// @author       Nick2bad4u
// @match        https://oldschool.runescape.wiki/*
// @match        https://runescape.wiki/*
// @match        https://*.runescape.wiki/*
// @match        https://api.runescape.wiki/*
// @match        https://classic.runescape.wiki/*
// @match        *://*.runescape.wiki/*
// @grant        GM_xmlhttpRequest
// @icon         https://www.google.com/s2/favicons?sz=64&domain=oldschool.runescape.wiki
// @license      UnLicense
// ==/UserScript==

(function () {
  "use strict";
  const versionNumber = "5.1";
  let navboxName = "";
  let pageLinks = [];
  let selectedLinks = [];
  let currentIndex = 0;
  let csrfToken = "";
  let isCancelled = false;
  let isRunning = false;
  let requestInterval = 10000;
  const maxInterval = 20000;
  const excludedPrefixes = [
    "Template:",
    "File:",
    "Navbox:",
    "Module:",
    "RuneScape:",
    "Update:",
    "Exchange:",
    "RuneScape:",
    "User:",
    "Help:",
  ];
  let actionLog = []; // Track actions for summary

  function addButtonAndProgressBar() {
    console.log("Adding button and progress bar to the UI.");
    const container = document.createElement("div");
    container.id = "Navbox-ui";
    container.style = `position: fixed; bottom: 20px; right: 20px; z-index: 1000;
            background-color: #2b2b2b; color: #ffffff; padding: 12px;
            border: 1px solid #595959; border-radius: 8px; font-family: Arial, sans-serif; width: 250px;`;

    const startButton = document.createElement("button");
    startButton.textContent = "Start Categorizing";
    startButton.style = `background-color: #4caf50; color: #fff; border: none;
            padding: 6px 12px; border-radius: 5px; cursor: pointer;`;
    startButton.onclick = promptnavboxName;
    container.appendChild(startButton);

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "Cancel";
    cancelButton.style = `background-color: #d9534f; color: #fff; border: none;
            padding: 6px 12px; border-radius: 5px; cursor: pointer; margin-left: 10px;`;
    cancelButton.onclick = cancelNavbox;
    container.appendChild(cancelButton);

    const progressBarContainer = document.createElement("div");
    progressBarContainer.style = `width: 100%; margin-top: 10px; background-color: #3d3d3d;
            height: 20px; border-radius: 5px; position: relative;`;
    progressBarContainer.id = "progress-bar-container";

    const progressBar = document.createElement("div");
    progressBar.style = `width: 0%; height: 100%; background-color: #4caf50; border-radius: 5px;`;
    progressBar.id = "progress-bar";
    progressBarContainer.appendChild(progressBar);

    const progressText = document.createElement("span");
    progressText.id = "progress-text";
    progressText.style = `position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
            font-size: 12px; color: #ffffff; white-space: nowrap; overflow: visible; text-align: center;`;
    progressBarContainer.appendChild(progressText);

    container.appendChild(progressBarContainer);
    document.body.appendChild(container);
  }

  function promptnavboxName() {
    navboxName = prompt("Enter the Navbox name you'd like to add:");
    console.log("Navbox name entered:", navboxName);
    if (!navboxName) {
      alert("Navbox name is required.");
      return;
    }

    getPageLinks();
    if (pageLinks.length === 0) {
      alert("No pages found to Navbox.");
      console.log("No pages found after filtering.");
      return;
    }

    displayPageSelectionPopup();
  }

  // Function to check for highlighted text
  function getHighlightedText() {
    const selection = globalThis.getSelection();
    if (selection.rangeCount > 0) {
      const container = document.createElement("div");
      for (let i = 0; i < selection.rangeCount; i++) {
        container.appendChild(selection.getRangeAt(i).cloneContents());
      }
      return container.innerHTML;
    }
    return "";
  }

  // Modify getPageLinks to consider highlighted text
  function getPageLinks() {
    let contextElement = document.querySelector("#mw-content-text");
    const highlightedText = getHighlightedText();

    if (highlightedText) {
      // Create a temporary container to process the highlighted text
      const tempContainer = document.createElement("div");
      tempContainer.innerHTML = highlightedText;
      contextElement = tempContainer;
    }

    pageLinks = Array.from(
      new Set(
        Array.from(contextElement.querySelectorAll("a"))
          .map((link) => link.getAttribute("href"))
          .filter((href) => href && href.startsWith("/w/"))
          .map((href) => decodeURIComponent(href.replace("/w/", "")))
          .filter(
            (page) =>
              !excludedPrefixes.some((prefix) => page.startsWith(prefix)) &&
              !page.includes("?") &&
              !page.includes("/") &&
              !page.includes("#"),
          ),
      ),
    );

    console.log("Filtered unique page links:", pageLinks);
  }

  function displayPageSelectionPopup() {
    console.log("Displaying page selection popup.");
    const popup = document.createElement("div");
    popup.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
    z-index: 1001; background-color: #2b2b2b; padding: 15px; color: white;
    border-radius: 8px; max-height: 80vh; overflow-y: auto; border: 1px solid #595959;`;

    const title = document.createElement("h3");
    title.textContent = "Select Pages to Navbox";
    title.style = `margin: 0 0 10px; font-family: Arial, sans-serif;`;
    popup.appendChild(title);

    const listContainer = document.createElement("div");
    listContainer.style = `max-height: 300px; overflow-y: auto;`;

    // Declare lastChecked outside the event listener to keep track of the last clicked checkbox
    let lastChecked = null;

    pageLinks.forEach((link) => {
      const listItem = document.createElement("div");
      const checkbox = document.createElement("input");
      checkbox.type = "checkbox";
      checkbox.checked = true;
      checkbox.value = link;
      listItem.appendChild(checkbox);
      listItem.appendChild(document.createTextNode(` ${link}`));
      listContainer.appendChild(listItem);

      checkbox.addEventListener("click", function (e) {
        if (e.shiftKey && lastChecked) {
          let inBetween = false;
          listContainer
            .querySelectorAll('input[type="checkbox"]')
            .forEach((checkbox) => {
              if (checkbox === this || checkbox === lastChecked) {
                inBetween = !inBetween;
              }
              if (inBetween) {
                checkbox.checked = this.checked;
              }
            });
        }
        lastChecked = this;
      });
    });
    popup.appendChild(listContainer);

    const buttonContainer = document.createElement("div");
    buttonContainer.style = `margin-top: 10px; display: flex; justify-content: space-between;`;

    let allSelected = true;
    const selectAllButton = document.createElement("button");
    selectAllButton.textContent = "Select All";
    selectAllButton.style = `padding: 5px 10px; background-color: #5bc0de; border: none;
        color: white; cursor: pointer; border-radius: 5px;`;
    selectAllButton.onclick = () => {
      listContainer
        .querySelectorAll('input[type="checkbox"]')
        .forEach((checkbox) => {
          checkbox.checked = allSelected;
        });
      selectAllButton.textContent = allSelected ? "Deselect All" : "Select All";
      allSelected = !allSelected;
      console.log(
        allSelected
          ? "Select All clicked: all checkboxes selected."
          : "Deselect All clicked: all checkboxes deselected.",
      );
    };
    buttonContainer.appendChild(selectAllButton);

    const confirmButton = document.createElement("button");
    confirmButton.textContent = "Confirm Selection";
    confirmButton.style = `padding: 5px 10px; background-color: #4caf50;
            border: none; color: white; cursor: pointer; border-radius: 5px;`;
    confirmButton.onclick = () => {
      selectedLinks = Array.from(
        listContainer.querySelectorAll("input:checked"),
      ).map((input) => input.value);
      console.log("Confirmed selected links:", selectedLinks);
      document.body.removeChild(popup);
      if (selectedLinks.length > 0) {
        startNavbox();
      } else {
        alert("No pages selected.");
      }
    };

    const cancelPopupButton = document.createElement("button");
    cancelPopupButton.textContent = "Cancel";
    cancelPopupButton.style = `padding: 5px 10px; background-color: #d9534f;
            border: none; color: white; cursor: pointer; border-radius: 5px;`;
    cancelPopupButton.onclick = () => {
      console.log("Popup canceled.");
      document.body.removeChild(popup);
    };

    buttonContainer.appendChild(confirmButton);
    buttonContainer.appendChild(cancelPopupButton);
    popup.appendChild(buttonContainer);

    document.body.appendChild(popup);
  }

  function startNavbox() {
    console.log("Starting Navbox process.");
    isCancelled = false;
    isRunning = true;
    currentIndex = 0;
    document.getElementById("progress-bar-container").style.display = "block";
    fetchCsrfToken(() => processNextPage());
  }

  function processNextPage() {
    if (isCancelled || currentIndex >= selectedLinks.length) {
      console.log(
        "Navbox ended. Reason:",
        isCancelled ? "Cancelled" : "Completed",
      );
      isRunning = false;
      if (!isCancelled) {
        displayCompletionSummary(); // Show summary popup
      }
      resetUI();
      return;
    }

    const pageTitle = selectedLinks[currentIndex];
    updateProgressBar(`Processing: ${pageTitle}`);
    console.log(`Processing page: ${pageTitle}`);
    addNavboxToPage(pageTitle, () => {
      currentIndex++;
      updateProgressBar(`Processed: ${pageTitle}`);
      setTimeout(processNextPage, requestInterval);
    });
  }

  function addNavboxToPage(pageTitle, callback) {
    const navboxTemplate = `{{${navboxName}}}`;

    // Fetch page content to check for duplicate
    const apiUrl = `https://oldschool.runescape.wiki/api.php?action=query&prop=revisions&titles=${encodeURIComponent(pageTitle)}&rvprop=content&format=json`;

    GM_xmlhttpRequest({
      method: "GET",
      url: apiUrl,
      onload(response) {
        const responseJson = JSON.parse(response.responseText);
        const pageId = Object.keys(responseJson.query.pages)[0];
        const page = responseJson.query.pages[pageId];

        // If page content is undefined, skip processing this page
        if (!page.revisions || !page.revisions[0]) {
          console.log(`Page content not found for '${pageTitle}', skipping.`);
          actionLog.push(`Skipped: '${pageTitle}' - page content not found.`);
          callback();
          return;
        }

        const pageContent = page.revisions[0]["*"];

        // Check if the navbox already exists in the content
        if (pageContent.includes(navboxTemplate)) {
          console.log(
            `Page '${pageTitle}' already contains the navbox '${navboxName}', skipping.`,
          );
          actionLog.push(
            `Skipped: '${pageTitle}' already contains '${navboxName}'`,
          );
          callback();
          return;
        }

        // Find the index of the first category
        const firstCategoryIndex = pageContent.indexOf("[[Category:");

        // Add the navbox above the first category, or append if no categories
        const newContent =
          firstCategoryIndex === -1
            ? pageContent + "\n" + navboxTemplate // Append if no categories
            : pageContent.slice(0, firstCategoryIndex) +
              navboxTemplate +
              "\n" +
              pageContent.slice(firstCategoryIndex);

        const editUrl = "https://oldschool.runescape.wiki/api.php";
        const formData = new URLSearchParams();
        formData.append("action", "edit");
        formData.append("title", pageTitle);
        formData.append("text", newContent);
        formData.append("token", csrfToken);
        formData.append("format", "json");

        GM_xmlhttpRequest({
          method: "POST",
          url: editUrl,
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
          data: formData.toString(),
          onload(editResponse) {
            if (editResponse.status === 200) {
              actionLog.push(`Added: '${pageTitle}' to '${navboxName}'`);
              console.log(
                `Successfully added '${pageTitle}' to Navbox '${navboxName}'.`,
              );
              callback();
            } else {
              console.log(`Failed to add '${pageTitle}' to Navbox.`);
              callback();
            }
          },
        });
      },
    });
  }

  function fetchCsrfToken(callback) {
    const apiUrl =
      "https://oldschool.runescape.wiki/api.php?action=query&meta=tokens&type=csrf&format=json";
    GM_xmlhttpRequest({
      method: "GET",
      url: apiUrl,
      onload(response) {
        const responseJson = JSON.parse(response.responseText);
        csrfToken = responseJson.query.tokens.csrftoken;
        console.log("CSRF token fetched:", csrfToken);
        callback();
      },
    });
  }

  function updateProgressBar(status) {
    const progressBar = document.getElementById("progress-bar");
    const progressText = document.getElementById("progress-text");
    const progress = (currentIndex / selectedLinks.length) * 100;
    progressBar.style.width = `${progress}%`;
    progressText.textContent = `${Math.round(progress)}% - ${status}`;
  }

  function displayCompletionSummary() {
    console.log("Displaying completion summary.");
    const summaryPopup = document.createElement("div");
    summaryPopup.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
        z-index: 1001; background-color: #2b2b2b; padding: 15px; color: white;
        border-radius: 8px; max-height: 80vh; overflow-y: auto; border: 1px solid #595959;`;

    const title = document.createElement("h3");
    title.textContent = "Navbox Summary";
    title.style = `margin: 0 0 10px; font-family: Arial, sans-serif;`;
    summaryPopup.appendChild(title);

    const logList = document.createElement("ul");
    logList.style = "max-height: 300px; overflow-y: auto;";

    actionLog.forEach((entry) => {
      const listItem = document.createElement("li");
      listItem.textContent = entry;
      logList.appendChild(listItem);
    });

    summaryPopup.appendChild(logList);

    const closeButton = document.createElement("button");
    closeButton.textContent = "Close";
    closeButton.style = `margin-top: 10px; padding: 5px 10px; background-color: #4caf50;
            border: none; color: white; cursor: pointer; border-radius: 5px;`;
    closeButton.onclick = () => {
      document.body.removeChild(summaryPopup);
      actionLog = [];
    };

    summaryPopup.appendChild(closeButton);
    document.body.appendChild(summaryPopup);
  }

  function resetUI() {
    document.getElementById("progress-bar").style.width = "0%";
    document.getElementById("progress-text").textContent = "";
    document.getElementById("progress-bar-container").style.display = "none";
    isRunning = false;
  }

  function cancelNavbox() {
    console.log("Navbox cancelled by user.");
    isCancelled = true;
  }

  addButtonAndProgressBar();
})();