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-25 提交的版本。查看 最新版本

// ==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();
})();

QingJ © 2025

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