// ==UserScript==
// @name OSRS Wiki Auto-Categorizer with UI, Adaptive Speed, Duplicate Checker (Slow Version)
// @namespace typpi.online
// @version 4.9
// @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.6';
let categoryName = '';
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:',
'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,
);
// Exit if the page has no categories
if (categories.length === 0) {
console.log(
`Skipped: '${pageTitle}' has no existing categories.`,
);
actionLog.push(
`Skipped: '${pageTitle}' has no existing categories.`,
);
callback();
return;
}
// 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();
})();