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.

  1. // ==UserScript==
  2. // @name OSRS Wiki Auto-Navbox with UI, Adaptive Speed, Duplicate Checker (Slow Version)
  3. // @namespace typpi.online
  4. // @version 5.3
  5. // @description Adds listed pages to a Navbox upon request with UI, CSRF token, adaptive speed, duplicate checker, and highlighted links option.
  6. // @author Nick2bad4u
  7. // @match https://oldschool.runescape.wiki/*
  8. // @match https://runescape.wiki/*
  9. // @match https://*.runescape.wiki/*
  10. // @match https://api.runescape.wiki/*
  11. // @match https://classic.runescape.wiki/*
  12. // @match *://*.runescape.wiki/*
  13. // @grant GM_xmlhttpRequest
  14. // @icon https://www.google.com/s2/favicons?sz=64&domain=oldschool.runescape.wiki
  15. // @license UnLicense
  16. // ==/UserScript==
  17.  
  18. (function () {
  19. 'use strict';
  20. const versionNumber = '5.1';
  21. let navboxName = '';
  22. let pageLinks = [];
  23. let selectedLinks = [];
  24. let currentIndex = 0;
  25. let csrfToken = '';
  26. let isCancelled = false;
  27. let isRunning = false;
  28. let requestInterval = 10000;
  29. const maxInterval = 20000;
  30. const excludedPrefixes = [
  31. 'Template:',
  32. 'File:',
  33. 'Navbox:',
  34. 'Module:',
  35. 'RuneScape:',
  36. 'Update:',
  37. 'Exchange:',
  38. 'RuneScape:',
  39. 'User:',
  40. 'Help:',
  41. ];
  42. let actionLog = []; // Track actions for summary
  43.  
  44. function addButtonAndProgressBar() {
  45. console.log(
  46. 'Adding button and progress bar to the UI.',
  47. );
  48. const container =
  49. document.createElement('div');
  50. container.id = 'Navbox-ui';
  51. container.style = `position: fixed; bottom: 20px; right: 20px; z-index: 1000;
  52. background-color: #2b2b2b; color: #ffffff; padding: 12px;
  53. border: 1px solid #595959; border-radius: 8px; font-family: Arial, sans-serif; width: 250px;`;
  54.  
  55. const startButton =
  56. document.createElement('button');
  57. startButton.textContent =
  58. 'Start Categorizing';
  59. startButton.style = `background-color: #4caf50; color: #fff; border: none;
  60. padding: 6px 12px; border-radius: 5px; cursor: pointer;`;
  61. startButton.onclick = promptnavboxName;
  62. container.appendChild(startButton);
  63.  
  64. const cancelButton =
  65. document.createElement('button');
  66. cancelButton.textContent = 'Cancel';
  67. cancelButton.style = `background-color: #d9534f; color: #fff; border: none;
  68. padding: 6px 12px; border-radius: 5px; cursor: pointer; margin-left: 10px;`;
  69. cancelButton.onclick = cancelNavbox;
  70. container.appendChild(cancelButton);
  71.  
  72. const progressBarContainer =
  73. document.createElement('div');
  74. progressBarContainer.style = `width: 100%; margin-top: 10px; background-color: #3d3d3d;
  75. height: 20px; border-radius: 5px; position: relative;`;
  76. progressBarContainer.id =
  77. 'progress-bar-container';
  78.  
  79. const progressBar =
  80. document.createElement('div');
  81. progressBar.style = `width: 0%; height: 100%; background-color: #4caf50; border-radius: 5px;`;
  82. progressBar.id = 'progress-bar';
  83. progressBarContainer.appendChild(progressBar);
  84.  
  85. const progressText =
  86. document.createElement('span');
  87. progressText.id = 'progress-text';
  88. progressText.style = `position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
  89. font-size: 12px; color: #ffffff; white-space: nowrap; overflow: visible; text-align: center;`;
  90. progressBarContainer.appendChild(
  91. progressText,
  92. );
  93.  
  94. container.appendChild(progressBarContainer);
  95. document.body.appendChild(container);
  96. }
  97.  
  98. function promptnavboxName() {
  99. navboxName = prompt(
  100. "Enter the Navbox name you'd like to add:",
  101. );
  102. console.log(
  103. 'Navbox name entered:',
  104. navboxName,
  105. );
  106. if (!navboxName) {
  107. alert('Navbox name is required.');
  108. return;
  109. }
  110.  
  111. getPageLinks();
  112. if (pageLinks.length === 0) {
  113. alert('No pages found to Navbox.');
  114. console.log(
  115. 'No pages found after filtering.',
  116. );
  117. return;
  118. }
  119.  
  120. displayPageSelectionPopup();
  121. }
  122.  
  123. // Function to check for highlighted text
  124. function getHighlightedText() {
  125. const selection = globalThis.getSelection();
  126. if (selection.rangeCount > 0) {
  127. const container =
  128. document.createElement('div');
  129. for (
  130. let i = 0;
  131. i < selection.rangeCount;
  132. i++
  133. ) {
  134. container.appendChild(
  135. selection.getRangeAt(i).cloneContents(),
  136. );
  137. }
  138. return container.innerHTML;
  139. }
  140. return '';
  141. }
  142.  
  143. // Modify getPageLinks to consider highlighted text
  144. function getPageLinks() {
  145. let contextElement = document.querySelector(
  146. '#mw-content-text',
  147. );
  148. const highlightedText = getHighlightedText();
  149.  
  150. if (highlightedText) {
  151. // Create a temporary container to process the highlighted text
  152. const tempContainer =
  153. document.createElement('div');
  154. tempContainer.innerHTML = highlightedText;
  155. contextElement = tempContainer;
  156. }
  157.  
  158. pageLinks = Array.from(
  159. new Set(
  160. Array.from(
  161. contextElement.querySelectorAll('a'),
  162. )
  163. .map((link) =>
  164. link.getAttribute('href'),
  165. )
  166. .filter(
  167. (href) =>
  168. href && href.startsWith('/w/'),
  169. )
  170. .map((href) =>
  171. decodeURIComponent(
  172. href.replace('/w/', ''),
  173. ),
  174. )
  175. .filter(
  176. (page) =>
  177. !excludedPrefixes.some((prefix) =>
  178. page.startsWith(prefix),
  179. ) &&
  180. !page.includes('?') &&
  181. !page.includes('/') &&
  182. !page.includes('#'),
  183. ),
  184. ),
  185. );
  186.  
  187. console.log(
  188. 'Filtered unique page links:',
  189. pageLinks,
  190. );
  191. }
  192.  
  193. function displayPageSelectionPopup() {
  194. console.log(
  195. 'Displaying page selection popup.',
  196. );
  197. const popup = document.createElement('div');
  198. popup.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  199. z-index: 1001; background-color: #2b2b2b; padding: 15px; color: white;
  200. border-radius: 8px; max-height: 80vh; overflow-y: auto; border: 1px solid #595959;`;
  201.  
  202. const title = document.createElement('h3');
  203. title.textContent = 'Select Pages to Navbox';
  204. title.style = `margin: 0 0 10px; font-family: Arial, sans-serif;`;
  205. popup.appendChild(title);
  206.  
  207. const listContainer =
  208. document.createElement('div');
  209. listContainer.style = `max-height: 300px; overflow-y: auto;`;
  210.  
  211. // Declare lastChecked outside the event listener to keep track of the last clicked checkbox
  212. let lastChecked = null;
  213.  
  214. pageLinks.forEach((link) => {
  215. const listItem =
  216. document.createElement('div');
  217. const checkbox =
  218. document.createElement('input');
  219. checkbox.type = 'checkbox';
  220. checkbox.checked = true;
  221. checkbox.value = link;
  222. listItem.appendChild(checkbox);
  223. listItem.appendChild(
  224. document.createTextNode(` ${link}`),
  225. );
  226. listContainer.appendChild(listItem);
  227.  
  228. checkbox.addEventListener(
  229. 'click',
  230. function (e) {
  231. if (e.shiftKey && lastChecked) {
  232. let inBetween = false;
  233. listContainer
  234. .querySelectorAll(
  235. 'input[type="checkbox"]',
  236. )
  237. .forEach((checkbox) => {
  238. if (
  239. checkbox === this ||
  240. checkbox === lastChecked
  241. ) {
  242. inBetween = !inBetween;
  243. }
  244. if (inBetween) {
  245. checkbox.checked = this.checked;
  246. }
  247. });
  248. }
  249. lastChecked = this;
  250. },
  251. );
  252. });
  253. popup.appendChild(listContainer);
  254.  
  255. const buttonContainer =
  256. document.createElement('div');
  257. buttonContainer.style = `margin-top: 10px; display: flex; justify-content: space-between;`;
  258.  
  259. let allSelected = true;
  260. const selectAllButton =
  261. document.createElement('button');
  262. selectAllButton.textContent = 'Select All';
  263. selectAllButton.style = `padding: 5px 10px; background-color: #5bc0de; border: none;
  264. color: white; cursor: pointer; border-radius: 5px;`;
  265. selectAllButton.onclick = () => {
  266. listContainer
  267. .querySelectorAll(
  268. 'input[type="checkbox"]',
  269. )
  270. .forEach((checkbox) => {
  271. checkbox.checked = allSelected;
  272. });
  273. selectAllButton.textContent = allSelected
  274. ? 'Deselect All'
  275. : 'Select All';
  276. allSelected = !allSelected;
  277. console.log(
  278. allSelected
  279. ? 'Select All clicked: all checkboxes selected.'
  280. : 'Deselect All clicked: all checkboxes deselected.',
  281. );
  282. };
  283. buttonContainer.appendChild(selectAllButton);
  284.  
  285. const confirmButton =
  286. document.createElement('button');
  287. confirmButton.textContent =
  288. 'Confirm Selection';
  289. confirmButton.style = `padding: 5px 10px; background-color: #4caf50;
  290. border: none; color: white; cursor: pointer; border-radius: 5px;`;
  291. confirmButton.onclick = () => {
  292. selectedLinks = Array.from(
  293. listContainer.querySelectorAll(
  294. 'input:checked',
  295. ),
  296. ).map((input) => input.value);
  297. console.log(
  298. 'Confirmed selected links:',
  299. selectedLinks,
  300. );
  301. document.body.removeChild(popup);
  302. if (selectedLinks.length > 0) {
  303. startNavbox();
  304. } else {
  305. alert('No pages selected.');
  306. }
  307. };
  308.  
  309. const cancelPopupButton =
  310. document.createElement('button');
  311. cancelPopupButton.textContent = 'Cancel';
  312. cancelPopupButton.style = `padding: 5px 10px; background-color: #d9534f;
  313. border: none; color: white; cursor: pointer; border-radius: 5px;`;
  314. cancelPopupButton.onclick = () => {
  315. console.log('Popup canceled.');
  316. document.body.removeChild(popup);
  317. };
  318.  
  319. buttonContainer.appendChild(confirmButton);
  320. buttonContainer.appendChild(
  321. cancelPopupButton,
  322. );
  323. popup.appendChild(buttonContainer);
  324.  
  325. document.body.appendChild(popup);
  326. }
  327.  
  328. function startNavbox() {
  329. console.log('Starting Navbox process.');
  330. isCancelled = false;
  331. isRunning = true;
  332. currentIndex = 0;
  333. document.getElementById(
  334. 'progress-bar-container',
  335. ).style.display = 'block';
  336. fetchCsrfToken(() => processNextPage());
  337. }
  338.  
  339. function processNextPage() {
  340. if (
  341. isCancelled ||
  342. currentIndex >= selectedLinks.length
  343. ) {
  344. console.log(
  345. 'Navbox ended. Reason:',
  346. isCancelled ? 'Cancelled' : 'Completed',
  347. );
  348. isRunning = false;
  349. if (!isCancelled) {
  350. displayCompletionSummary(); // Show summary popup
  351. }
  352. resetUI();
  353. return;
  354. }
  355.  
  356. const pageTitle = selectedLinks[currentIndex];
  357. updateProgressBar(`Processing: ${pageTitle}`);
  358. console.log(`Processing page: ${pageTitle}`);
  359. addNavboxToPage(pageTitle, () => {
  360. currentIndex++;
  361. updateProgressBar(
  362. `Processed: ${pageTitle}`,
  363. );
  364. setTimeout(
  365. processNextPage,
  366. requestInterval,
  367. );
  368. });
  369. }
  370.  
  371. function addNavboxToPage(pageTitle, callback) {
  372. const navboxTemplate = `{{${navboxName}}}`;
  373.  
  374. // Fetch page content to check for duplicate
  375. const apiUrl = `https://oldschool.runescape.wiki/api.php?action=query&prop=revisions&titles=${encodeURIComponent(pageTitle)}&rvprop=content&format=json`;
  376.  
  377. GM_xmlhttpRequest({
  378. method: 'GET',
  379. url: apiUrl,
  380. onload(response) {
  381. const responseJson = JSON.parse(
  382. response.responseText,
  383. );
  384. const pageId = Object.keys(
  385. responseJson.query.pages,
  386. )[0];
  387. const page =
  388. responseJson.query.pages[pageId];
  389.  
  390. // If page content is undefined, skip processing this page
  391. if (
  392. !page.revisions ||
  393. !page.revisions[0]
  394. ) {
  395. console.log(
  396. `Page content not found for '${pageTitle}', skipping.`,
  397. );
  398. actionLog.push(
  399. `Skipped: '${pageTitle}' - page content not found.`,
  400. );
  401. callback();
  402. return;
  403. }
  404.  
  405. const pageContent =
  406. page.revisions[0]['*'];
  407.  
  408. // Check if the navbox already exists in the content
  409. if (
  410. pageContent.includes(navboxTemplate)
  411. ) {
  412. console.log(
  413. `Page '${pageTitle}' already contains the navbox '${navboxName}', skipping.`,
  414. );
  415. actionLog.push(
  416. `Skipped: '${pageTitle}' already contains '${navboxName}'`,
  417. );
  418. callback();
  419. return;
  420. }
  421.  
  422. // Find the index of the first category
  423. const firstCategoryIndex =
  424. pageContent.indexOf('[[Category:');
  425.  
  426. // Add the navbox above the first category, or append if no categories
  427. const newContent =
  428. firstCategoryIndex === -1
  429. ? pageContent + '\n' + navboxTemplate // Append if no categories
  430. : pageContent.slice(
  431. 0,
  432. firstCategoryIndex,
  433. ) +
  434. navboxTemplate +
  435. '\n' +
  436. pageContent.slice(
  437. firstCategoryIndex,
  438. );
  439.  
  440. const editUrl =
  441. 'https://oldschool.runescape.wiki/api.php';
  442. const formData = new URLSearchParams();
  443. formData.append('action', 'edit');
  444. formData.append('title', pageTitle);
  445. formData.append('text', newContent);
  446. formData.append('token', csrfToken);
  447. formData.append('format', 'json');
  448.  
  449. GM_xmlhttpRequest({
  450. method: 'POST',
  451. url: editUrl,
  452. headers: {
  453. 'Content-Type':
  454. 'application/x-www-form-urlencoded',
  455. },
  456. data: formData.toString(),
  457. onload(editResponse) {
  458. if (editResponse.status === 200) {
  459. actionLog.push(
  460. `Added: '${pageTitle}' to '${navboxName}'`,
  461. );
  462. console.log(
  463. `Successfully added '${pageTitle}' to Navbox '${navboxName}'.`,
  464. );
  465. callback();
  466. } else {
  467. console.log(
  468. `Failed to add '${pageTitle}' to Navbox.`,
  469. );
  470. callback();
  471. }
  472. },
  473. });
  474. },
  475. });
  476. }
  477.  
  478. function fetchCsrfToken(callback) {
  479. const apiUrl =
  480. 'https://oldschool.runescape.wiki/api.php?action=query&meta=tokens&type=csrf&format=json';
  481. GM_xmlhttpRequest({
  482. method: 'GET',
  483. url: apiUrl,
  484. onload(response) {
  485. const responseJson = JSON.parse(
  486. response.responseText,
  487. );
  488. csrfToken =
  489. responseJson.query.tokens.csrftoken;
  490. console.log(
  491. 'CSRF token fetched:',
  492. csrfToken,
  493. );
  494. callback();
  495. },
  496. });
  497. }
  498.  
  499. function updateProgressBar(status) {
  500. const progressBar = document.getElementById(
  501. 'progress-bar',
  502. );
  503. const progressText = document.getElementById(
  504. 'progress-text',
  505. );
  506. const progress =
  507. (currentIndex / selectedLinks.length) * 100;
  508. progressBar.style.width = `${progress}%`;
  509. progressText.textContent = `${Math.round(progress)}% - ${status}`;
  510. }
  511.  
  512. function displayCompletionSummary() {
  513. console.log('Displaying completion summary.');
  514. const summaryPopup =
  515. document.createElement('div');
  516. summaryPopup.style = `position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
  517. z-index: 1001; background-color: #2b2b2b; padding: 15px; color: white;
  518. border-radius: 8px; max-height: 80vh; overflow-y: auto; border: 1px solid #595959;`;
  519.  
  520. const title = document.createElement('h3');
  521. title.textContent = 'Navbox Summary';
  522. title.style = `margin: 0 0 10px; font-family: Arial, sans-serif;`;
  523. summaryPopup.appendChild(title);
  524.  
  525. const logList = document.createElement('ul');
  526. logList.style =
  527. 'max-height: 300px; overflow-y: auto;';
  528.  
  529. actionLog.forEach((entry) => {
  530. const listItem =
  531. document.createElement('li');
  532. listItem.textContent = entry;
  533. logList.appendChild(listItem);
  534. });
  535.  
  536. summaryPopup.appendChild(logList);
  537.  
  538. const closeButton =
  539. document.createElement('button');
  540. closeButton.textContent = 'Close';
  541. closeButton.style = `margin-top: 10px; padding: 5px 10px; background-color: #4caf50;
  542. border: none; color: white; cursor: pointer; border-radius: 5px;`;
  543. closeButton.onclick = () => {
  544. document.body.removeChild(summaryPopup);
  545. actionLog = [];
  546. };
  547.  
  548. summaryPopup.appendChild(closeButton);
  549. document.body.appendChild(summaryPopup);
  550. }
  551.  
  552. function resetUI() {
  553. document.getElementById(
  554. 'progress-bar',
  555. ).style.width = '0%';
  556. document.getElementById(
  557. 'progress-text',
  558. ).textContent = '';
  559. document.getElementById(
  560. 'progress-bar-container',
  561. ).style.display = 'none';
  562. isRunning = false;
  563. }
  564.  
  565. function cancelNavbox() {
  566. console.log('Navbox cancelled by user.');
  567. isCancelled = true;
  568. }
  569.  
  570. addButtonAndProgressBar();
  571. })();

QingJ © 2025

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