Avatar, Site Themes, and Relics checklist for Grundo's Cafe, visit https://www.grundos.cafe/~Tyco
当前为
// ==UserScript==
// @name [GC] Avatar Checklist
// @namespace https://www.grundos.cafe/
// @version 3.0.0
// @description Avatar, Site Themes, and Relics checklist for Grundo's Cafe, visit https://www.grundos.cafe/~Tyco
// @author soupfaerie, supercow64, arithmancer
// @match https://www.grundos.cafe/~Tyco*
// @match https://www.grundos.cafe/~tyco*
// @match https://www.grundos.cafe/~TYCO*
// @grant none
// @license MIT
// ==/UserScript==
const textToHTML = (text) => new DOMParser().parseFromString(text, "text/html");
/**
* Analyse the HTML select element for a list of avatars the user has collected.
*
* @param {Node} node The root node (default: document)
* @returns {string[]} the list of avatars as an array of basenames
*/
const getCollectedAvatars = (node = document) => {
// The list of avatars is partitioned into default avatars
// and collected secret avatars. The option with the text ---
// (6 dashes) is the inclusive cutoff. All avatars at and below
// the cutoff are collected secret avatars
const allAvatars = Array.from(
node.querySelectorAll(`[name="new_avatar"] option`)
);
const i = allAvatars.findIndex((e) => e.textContent.includes("---"));
return allAvatars.slice(i).map((e) => e.value);
};
/**
* Analyse the HTML select element for all site themes available to the user.
*
* @param {Node} node The root node (default: document)
* @returns {string[]} all site themes as an array of theme names
*/
const getAllSiteThemes = (node = document) => {
// Find all options in the site_theme select element
const themeOptions = Array.from(node.querySelectorAll(`[name="site_theme"] option`));
if (!themeOptions.length) return [];
// Return the text content of each option
return themeOptions
.map(option => option.textContent.trim())
.filter(themeName => themeName); // Filter out any empty theme names
};
/**
* Returns a Promise that resolves to a list of avatars
* the user has collected.
*
* @returns {string[]} list of collected avatars
*/
const getCollectedAvatarsAsync = () =>
fetch("/neoboards/preferences/")
.then((res) => res.text())
.then(textToHTML)
.then(getCollectedAvatars);
/**
* Returns a Promise that resolves to all site themes
* available to the user.
*
* @returns {string[]} all site themes as an array of theme names
*/
const getAllSiteThemesAsync = () =>
fetch("/help/siteprefs")
.then((res) => res.text())
.then(textToHTML)
.then(getAllSiteThemes);
/**
* Analyse the HTML response to identify logged relics.
*
* @param {Node} node The root node (default: document)
* @returns {string[]} the list of logged relics as an array of relic names
*/
const getLoggedRelics = (node = document) => {
// Find all divs with id="reliclogged"
const loggedRelics = Array.from(
node.querySelectorAll(`[id="reliclogged"]`)
);
if (!loggedRelics.length) return [];
// Extract the relic names from the logged relics
return loggedRelics
.map(relic => {
const img = relic.querySelector('img');
// Use the title attribute of the image which contains the relic name
if (img && img.title) {
return img.title;
}
// If title is not available, try to get the name from the span element
const span = relic.querySelector('span.medfont strong');
if (span) {
// Extract just the relic name without any child elements
return span.childNodes[0].textContent.trim();
}
return null;
})
.filter(name => name); // Filter out any null values
};
/**
* Returns a Promise that resolves to a list of logged relics.
*
* @returns {string[]} list of logged relics as an array of relic names
*/
const getLoggedRelicsAsync = () =>
fetch("/space/warehouse/relics/")
.then((res) => res.text())
.then(textToHTML)
.then(getLoggedRelics);
/**
* For static assets, returns the basename of the asset indicated
* in the url.
*
* ```js
* basename("https://example.com/foo/bar/baz.gif") == "baz.gif"
* ```
*
* @param {string} url path to the file with slashes
* @returns {string} the basename
*/
const basename = (url) => url.split("/").slice(-1)[0];
/**
* Move collected avatar cards into their section's <details> element.
*
* The tracker page groups avatars by section. Each section is a <div>
* directly under the #avatars container. Within each section there are one or
* more `.avatar-grid` containers followed by a <details> element containing an
* empty `.avatar-grid`. Collected avatars should be appended to that grid.
*
* @param {string[]} collectedAvatars basenames of the user's collected avatars
*/
function moveCollectedAvatars(collectedAvatars) {
const sections = document.querySelectorAll('#avatars > div');
// Create a set to track which collected avatars are found on the page
const foundAvatars = new Set();
sections.forEach((section) => {
const foundGrid = section.querySelector('details .avatar-grid');
if (!foundGrid) return;
const cards = section.querySelectorAll(':scope > .avatar-grid > .avatar-card');
cards.forEach((card) => {
const img = card.querySelector('img');
if (!img) return; // site theme cards currently lack images
const avatarName = basename(img.src);
if (collectedAvatars.includes(avatarName)) {
card.classList.add('check');
foundGrid.appendChild(card);
foundAvatars.add(avatarName);
}
});
});
// Log any collected avatars that weren't found on the page
collectedAvatars.forEach(avatar => {
if (!foundAvatars.has(avatar)) {
console.log(`Collected avatar not found on page: ${avatar}`);
}
});
}
/**
* Move collected site theme cards into the themes section's <details> element.
*
* The site themes section has a <details> element containing an empty `.avatar-grid`.
* All site themes from the site_theme select element should be appended to that grid.
*
* @param {string[]} collectedThemes names of all available site themes
*/
function moveCollectedSiteThemes(collectedThemes) {
const themesSection = document.querySelector('#themes');
if (!themesSection) return;
const foundGrid = themesSection.querySelector('details .avatar-grid');
if (!foundGrid) return;
// Create a set to track which collected themes are found on the page
const foundThemes = new Set();
const cards = themesSection.querySelectorAll(':scope > .avatar-grid > .avatar-card');
cards.forEach((card) => {
const nameElement = card.querySelector('.avatar-name');
if (!nameElement) return;
// Get the theme name directly from the card
const themeName = nameElement.textContent.trim();
// Check if this theme is in the collected themes list
if (collectedThemes.includes(themeName)) {
card.classList.add('done');
foundGrid.appendChild(card);
foundThemes.add(themeName);
}
});
// Log any collected themes that weren't found on the page
collectedThemes.forEach(theme => {
if (!foundThemes.has(theme)) {
console.log(`Collected site theme not found on page: ${theme}`);
}
});
}
/**
* Move logged relic cards into their section's <details> element.
*
* The relics section has a <details> element containing an empty `.avatar-grid`.
* Logged relics should be appended to that grid and marked with the "redeemed" class.
*
* @param {string[]} loggedRelics names of logged relics
*/
function moveLoggedRelics(loggedRelics) {
const relicsSection = document.querySelector('#relics');
if (!relicsSection) return;
const foundGrid = relicsSection.querySelector('details .avatar-grid');
if (!foundGrid) return;
// Create a set to track which logged relics are found on the page
const foundRelics = new Set();
const cards = relicsSection.querySelectorAll(':scope > .avatar-grid > .avatar-card');
cards.forEach((card) => {
const nameElement = card.querySelector('.avatar-name');
if (!nameElement) return;
// Get the relic name directly from the card
const relicName = nameElement.textContent.trim();
// Check if this relic is in the logged relics list
if (loggedRelics.includes(relicName)) {
card.classList.add('redeemed');
foundGrid.appendChild(card);
foundRelics.add(relicName);
}
});
// Log any logged relics that weren't found on the page
loggedRelics.forEach(relic => {
if (!foundRelics.has(relic)) {
console.log(`Logged relic not found on page: ${relic}`);
}
});
}
// Fetch avatar, site theme, and relic data and move the collected cards
Promise.all([
getCollectedAvatarsAsync().then(moveCollectedAvatars),
getAllSiteThemesAsync().then(moveCollectedSiteThemes),
getLoggedRelicsAsync().then(moveLoggedRelics)
]);