您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Avatar checklist for Grundo's Cafe, visit https://www.grundos.cafe/~Tyco
当前为
// ==UserScript== // @name [GC] Unlockables Checklist // @namespace https://www.grundos.cafe/ // @version 2.0 // @description Avatar 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* // @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 the site theme the user has selected. * * @param {Node} node The root node (default: document) * @returns {string[]} the selected site theme as an array of data-css values */ const getSelectedSiteTheme = (node = document) => { // Find the selected option in the site_theme select element const selectedOption = node.querySelector(`[name="site_theme"] option[selected]`); if (!selectedOption) return []; // Return the data-css attribute value const dataCss = selectedOption.getAttribute('data-css'); return dataCss ? [dataCss] : []; }; /** * 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 the site theme * the user has selected. * * @returns {string[]} the selected site theme as an array containing the data-css value */ const getSelectedSiteThemeAsync = () => fetch("/help/siteprefs") .then((res) => res.text()) .then(textToHTML) .then(getSelectedSiteTheme); /** * 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'); 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 if (collectedAvatars.includes(basename(img.src))) { card.classList.add('check'); foundGrid.appendChild(card); } }); }); } /** * 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`. * Collected site themes should be appended to that grid. * * @param {string[]} collectedThemes data-css values of the user's selected site theme */ function moveCollectedSiteThemes(collectedThemes) { const themesSection = document.querySelector('#themes'); if (!themesSection) return; const foundGrid = themesSection.querySelector('details .avatar-grid'); if (!foundGrid) return; const cards = themesSection.querySelectorAll(':scope > .avatar-grid > .avatar-card'); cards.forEach((card) => { const nameElement = card.querySelector('.avatar-name'); if (!nameElement) return; // Convert theme name to data-css format (lowercase with underscores) const themeName = nameElement.textContent.trim(); const themeDataCss = themeName.toLowerCase().replace(/\s+/g, '_').replace(/[^a-z0-9_]/g, ''); // Check if this theme is in the collected themes list if (collectedThemes.some(theme => themeDataCss.includes(theme) || theme.includes(themeDataCss))) { card.classList.add('check'); foundGrid.appendChild(card); } }); } // Fetch both avatar and site theme data and move the collected cards Promise.all([ getCollectedAvatarsAsync().then(moveCollectedAvatars), getSelectedSiteThemeAsync().then(moveCollectedSiteThemes) ]);