OSRS Wiki Auto-Categorizer with UI, Adaptive Speed, Duplicate Checker

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

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

// ==UserScript==
// @name         OSRS Wiki Auto-Categorizer with UI, Adaptive Speed, Duplicate Checker
// @namespace     https://github.com/Nick2bad4u/UserStyles
// @version      4.4
// @description  Adds listed pages to a category 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 = "4.2";
  let categoryName = "";
  let pageLinks = [];
  let selectedLinks = [];
  let currentIndex = 0;
  let csrfToken = "";
  let isCancelled = false;
  let isRunning = false;
  let requestInterval = 500;
  const maxInterval = 5000;
  const excludedPrefixes = [
    "Template:",
    "File:",
    "Category:",
    "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 = "categorize-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 = promptCategoryName;
    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 = cancelCategorization;
    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 promptCategoryName() {
    categoryName = prompt("Enter the category name you'd like to add:");
    console.log("Category name entered:", categoryName);
    if (!categoryName) {
      alert("Category name is required.");
      return;
    }

    getPageLinks();
    if (pageLinks.length === 0) {
      alert("No pages found to categorize.");
      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 Categorize";
    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) {
        startCategorization();
      } 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 startCategorization() {
    console.log("Starting categorization 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(
        "Categorization 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}`);
    addCategoryToPage(pageTitle, () => {
      currentIndex++;
      updateProgressBar(`Processed: ${pageTitle}`);
      setTimeout(processNextPage, requestInterval);
    });
  }

  function addCategoryToPage(pageTitle, callback) {
    const categories = []; // Collects all categories from paginated responses

    // Function to standardize category names for comparison
    function standardizeCategoryName(name) {
      return name
        .replace(/^Category:/, "") // Remove prefix "Category:"
        .replace(/\s+/g, "_") // Replace spaces with underscores
        .toLowerCase(); // Convert to lowercase for case-insensitive comparison
    }

    // Recursive function to handle pagination
    function fetchCategories(clcontinue) {
      const apiUrl = `https://oldschool.runescape.wiki/api.php?action=query&prop=categories&titles=${encodeURIComponent(pageTitle)}&format=json${clcontinue ? `&clcontinue=${clcontinue}` : ""}`;
      console.log(
        `Checking categories for page: ${pageTitle}${clcontinue ? ` with clcontinue: ${clcontinue}` : ""}`,
      );

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

          // Append the categories from this response to the categories list
          if (page.categories) {
            categories.push(...page.categories.map((cat) => cat.title));
          }

          // Check if more categories need to be fetched (pagination)
          if (responseJson.continue && responseJson.continue.clcontinue) {
            fetchCategories(responseJson.continue.clcontinue); // Fetch next page
          } else {
            // All categories have been fetched
            console.log(`All categories for '${pageTitle}':`, categories);

            // Standardize target category name
            const standardizedCategoryName = standardizeCategoryName(
              `Category:${categoryName}`,
            );

            // Check if page is already categorized
            const alreadyCategorized = categories.some((cat) => {
              return standardizeCategoryName(cat) === standardizedCategoryName;
            });

            if (alreadyCategorized) {
              console.log(`Page '${pageTitle}' is already in the category.`);
              actionLog.push(
                `Skipped: '${pageTitle}' already in '${categoryName}'`,
              );
              callback();
            } else {
              const editUrl = "https://oldschool.runescape.wiki/api.php";
              const formData = new URLSearchParams();
              formData.append("action", "edit");
              formData.append("title", pageTitle);
              formData.append("appendtext", `\n[[Category:${categoryName}]]`);
              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(response) {
                  if (response.status === 200) {
                    actionLog.push(
                      `Added: '${pageTitle}' to '${categoryName}'`,
                    );
                    console.log(
                      `Successfully added '${pageTitle}' to category '${categoryName}'.`,
                    );
                    callback();
                  } else {
                    console.log(`Failed to add '${pageTitle}' to category.`);
                    callback();
                  }
                },
              });
            }
          }
        },
      });
    }

    // Start fetching categories (pagination will handle all pages)
    fetchCategories();
  }

  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 = "Categorization 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 cancelCategorization() {
    console.log("Categorization cancelled by user.");
    isCancelled = true;
  }

  addButtonAndProgressBar();
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址