[GC] Avatar Checklist

Avatar and Site Themes checklist for Grundo's Cafe, visit https://www.grundos.cafe/~Tyco

当前为 2025-07-20 提交的版本,查看 最新版本

// ==UserScript==
// @name         [GC] Avatar Checklist
// @namespace    https://www.grundos.cafe/
// @version      2.0.1
// @description  Avatar and Site Themes 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 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);

/**
 * 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`.
 * 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;

  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);
    }
  });
}

// Fetch both avatar and site theme data and move the collected cards
Promise.all([
  getCollectedAvatarsAsync().then(moveCollectedAvatars),
  getAllSiteThemesAsync().then(moveCollectedSiteThemes)
]);