Browser script to show thumbnails of owned modules on plugins page

For me it was always challenging to manage installed modules because even if I filter them by ‘Owned’ on plugins page, it a) still mixes in modules I don’t have installed, and b) I can’t see image of the module, so it’s always back and forth between library and module page to confirm I want to keep module or not.

I created a script which updates plugins page structure and injects thumbnails of modules that I own. Rest of the controls stay the same, so I can see all modules and remove them from one page.

This is how it looks:

Here’s the script:

Code
/*
 * VCV Library owned-module gallery — no-filter version
 *
 * Usage:
 *   1. Log in at https://library.vcvrack.com/plugins
 *   2. Open the browser developer console.
 *   3. Paste this entire file and press Enter.
 *
 * The script changes only the current page presentation. Individual removals
 * are still handled by VCV's original Remove buttons. Reload the page, or use
 * the "Exit gallery" button, to remove the gallery UI.
 */

(async function installVcvOwnedGalleryWithoutFilter() {
  "use strict";

  const HOST = "library.vcvrack.com";
  const GLOBAL_KEY = "__vcvOwnedGallery";
  const ROOT_CLASS = "codex-vcv-owned-gallery";
  const STYLE_ID = "codex-vcv-owned-gallery-style";
  const PANEL_ID = "codex-vcv-owned-gallery-panel";
  const COUNT_ID = "codex-vcv-owned-count";
  const CARD_CLASS = "codex-vcv-owned-card";
  const EMPTY_ROW_CLASS = "codex-vcv-empty-row";
  const EMPTY_HEADER_CLASS = "codex-vcv-empty-header";

  if (window.location.hostname !== HOST || window.location.pathname !== "/plugins") {
    throw new Error("Open https://library.vcvrack.com/plugins before running this script.");
  }

  if (window[GLOBAL_KEY] && typeof window[GLOBAL_KEY].destroy === "function") {
    window[GLOBAL_KEY].destroy();
  }

  const delay = (milliseconds) => new Promise((resolve) => {
    window.setTimeout(resolve, milliseconds);
  });

  const normalizedButtonText = (button) => button.textContent.replace(/\s+/g, " ").trim();

  const findButton = (label) => Array.from(document.querySelectorAll("button"))
    .find((button) => normalizedButtonText(button) === label);

  // The button reads "Show all" when the owned-only filter is already active.
  const showOwnedButton = findButton("Show owned");
  if (showOwnedButton) {
    showOwnedButton.click();
    await delay(400);
  }

  const expandAllButton = findButton("Expand all");
  if (expandAllButton) {
    expandAllButton.click();
    await delay(400);
  }

  const pluginTable = document.querySelector("table");
  if (!pluginTable) {
    throw new Error("The VCV plugin table was not found. Make sure the page has finished loading.");
  }

  document.documentElement.classList.add(ROOT_CLASS);

  const style = document.createElement("style");
  style.id = STYLE_ID;
  style.textContent = `
    html.${ROOT_CLASS} tr.library-plugins-modules ul {
      display: grid !important;
      grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
      align-items: stretch;
      gap: 16px;
      margin: 0;
      padding: 18px 8px 26px;
      list-style: none;
    }

    html.${ROOT_CLASS} tr.library-plugins-modules li {
      display: none !important;
      min-width: 0;
    }

    html.${ROOT_CLASS} tr.library-plugins-modules li.${CARD_CLASS} {
      display: block !important;
    }

    html.${ROOT_CLASS} tr.${EMPTY_ROW_CLASS},
    html.${ROOT_CLASS} tr.${EMPTY_HEADER_CLASS} {
      display: none !important;
    }

    html.${ROOT_CLASS} p.library-plugins-module {
      box-sizing: border-box;
      display: flex !important;
      flex-direction: column;
      gap: 8px;
      height: 100%;
      margin: 0;
      padding: 12px;
      border: 1px solid #39404c;
      border-radius: 9px;
      background: #171a20;
      box-shadow: 0 3px 12px rgba(0, 0, 0, 0.2);
    }

    html.${ROOT_CLASS} .codex-vcv-module-image-link {
      order: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      min-height: 260px;
      padding: 8px;
      border-radius: 6px;
      background: #0d0f13;
      overflow: hidden;
    }

    html.${ROOT_CLASS} .codex-vcv-module-image {
      display: block;
      width: 100%;
      height: 260px;
      object-fit: contain;
    }

    html.${ROOT_CLASS} .codex-vcv-module-image-link.codex-vcv-image-missing::after {
      color: #929baa;
      content: "Screenshot unavailable";
      text-align: center;
    }

    html.${ROOT_CLASS} .codex-vcv-module-image-link.codex-vcv-image-missing img {
      display: none;
    }

    html.${ROOT_CLASS} p.library-plugins-module > a:not(.codex-vcv-module-image-link) {
      order: 1;
      display: block;
      min-height: 2.4em;
      color: #e8edf4 !important;
      font-size: 15px;
      font-weight: 700;
      line-height: 1.2;
      text-align: center;
    }

    html.${ROOT_CLASS} p.library-plugins-module > .library-remove {
      order: 2;
      width: 100%;
      margin-top: auto;
    }

    html.${ROOT_CLASS} #${PANEL_ID} {
      position: fixed;
      top: 12px;
      right: 12px;
      z-index: 10000;
      box-sizing: border-box;
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
      width: min(410px, calc(100vw - 24px));
      padding: 12px;
      border: 1px solid #3f526a;
      border-radius: 8px;
      background: rgba(20, 25, 32, 0.97);
      box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
      color: #e8edf4;
    }

    html.${ROOT_CLASS} #${COUNT_ID} {
      color: #fff;
      font-size: 14px;
    }

    html.${ROOT_CLASS} .codex-vcv-exit-gallery {
      padding: 6px 9px;
      border: 1px solid #58677a;
      border-radius: 5px;
      background: #252b34;
      color: #e8edf4;
      cursor: pointer;
      white-space: nowrap;
    }
  `;
  document.head.appendChild(style);

  const panel = document.createElement("div");
  panel.id = PANEL_ID;

  const countLabel = document.createElement("strong");
  countLabel.id = COUNT_ID;

  const exitButton = document.createElement("button");
  exitButton.type = "button";
  exitButton.className = "codex-vcv-exit-gallery";
  exitButton.textContent = "Exit gallery";

  panel.append(countLabel, exitButton);
  document.body.appendChild(panel);

  let observer;
  let refreshTimer;

  function addModuleImage(paragraph, removeButton) {
    if (paragraph.querySelector(".codex-vcv-module-image-link")) {
      return;
    }

    const pluginSlug = removeButton.dataset.plugin || "";
    const moduleSlug = removeButton.dataset.module || "";
    const titleLink = paragraph.querySelector("a[href^='/']");
    const title = ((titleLink && titleLink.textContent) || moduleSlug).trim();

    const imageLink = document.createElement("a");
    imageLink.className = "codex-vcv-module-image-link";
    imageLink.href = (titleLink && titleLink.getAttribute("href"))
      || `/${pluginSlug}/${moduleSlug}`;
    imageLink.title = `${title} — open module page`;

    const image = document.createElement("img");
    image.className = "codex-vcv-module-image";
    image.src = `/screenshots/400/${encodeURIComponent(pluginSlug)}/${encodeURIComponent(moduleSlug)}.webp`;
    image.alt = `${title} module panel`;
    image.loading = "lazy";
    image.decoding = "async";
    image.addEventListener("error", () => {
      imageLink.classList.add("codex-vcv-image-missing");
    }, { once: true });

    imageLink.appendChild(image);
    paragraph.insertBefore(imageLink, paragraph.firstChild);
  }

  function updatePluginRows() {
    document.querySelectorAll("tr.library-plugins-modules").forEach((moduleRow) => {
      const hasOwnedCard = Boolean(moduleRow.querySelector(`li.${CARD_CLASS}`));
      moduleRow.classList.toggle(EMPTY_ROW_CLASS, !hasOwnedCard);

      const headerRow = moduleRow.previousElementSibling;
      if (headerRow && headerRow.matches("tr")) {
        headerRow.classList.toggle(EMPTY_HEADER_CLASS, !hasOwnedCard);
      }
    });
  }

  function refresh() {
    let ownedCount = 0;

    document.querySelectorAll("p.library-plugins-module").forEach((paragraph) => {
      const item = paragraph.closest("li");
      const removeButton = paragraph.querySelector("button.library-remove:not(.hidden)");

      if (!item) {
        return;
      }

      item.classList.toggle(CARD_CLASS, Boolean(removeButton));
      if (!removeButton) {
        return;
      }

      addModuleImage(paragraph, removeButton);
      ownedCount += 1;
    });

    updatePluginRows();
    countLabel.textContent = `${ownedCount} owned modules`;
  }

  function scheduleRefresh() {
    window.clearTimeout(refreshTimer);
    refreshTimer = window.setTimeout(refresh, 250);
  }

  function destroy() {
    window.clearTimeout(refreshTimer);
    if (observer) {
      observer.disconnect();
    }

    document.documentElement.classList.remove(ROOT_CLASS);
    document.getElementById(STYLE_ID)?.remove();
    document.getElementById(PANEL_ID)?.remove();
    document.querySelectorAll(".codex-vcv-module-image-link").forEach((element) => element.remove());
    document.querySelectorAll(`.${CARD_CLASS}`).forEach((element) => {
      element.classList.remove(CARD_CLASS);
    });
    document.querySelectorAll(`tr.${EMPTY_ROW_CLASS}`).forEach((element) => {
      element.classList.remove(EMPTY_ROW_CLASS);
    });
    document.querySelectorAll(`tr.${EMPTY_HEADER_CLASS}`).forEach((element) => {
      element.classList.remove(EMPTY_HEADER_CLASS);
    });

    delete window[GLOBAL_KEY];
    console.info("VCV owned-module gallery removed. Reloading the page also restores the original view.");
  }

  exitButton.addEventListener("click", destroy);
  refresh();

  observer = new MutationObserver((mutations) => {
    const ownershipChanged = mutations.some((mutation) => {
      if (mutation.type === "childList") {
        return true;
      }
      return mutation.target instanceof Element
        && mutation.target.matches("button.library-remove, button.library-add");
    });

    if (ownershipChanged) {
      scheduleRefresh();
    }
  });
  observer.observe(pluginTable, {
    subtree: true,
    childList: true,
    attributes: true,
    attributeFilter: ["class"]
  });

  window[GLOBAL_KEY] = {
    destroy,
    refresh
  };

  console.info(
    `VCV owned-module gallery enabled: ${document.querySelectorAll(`li.${CARD_CLASS}`).length} modules. `
    + "Use window.__vcvOwnedGallery.destroy() to restore the original presentation."
  );
})();

How to use:

  1. Go to VCV Library - Plugins
  2. Open developers tools or javascript console, paste code into it and press enter

Or create bookmark with following url, and click on it while being on plugins page:

Bookmarklet
javascript:void%20(async%20function%20installVcvOwnedGalleryWithoutFilter()%20%7B%0A%20%20%22use%20strict%22%3B%0A%0A%20%20const%20HOST%20%3D%20%22library.vcvrack.com%22%3B%0A%20%20const%20GLOBAL_KEY%20%3D%20%22__vcvOwnedGallery%22%3B%0A%20%20const%20ROOT_CLASS%20%3D%20%22codex-vcv-owned-gallery%22%3B%0A%20%20const%20STYLE_ID%20%3D%20%22codex-vcv-owned-gallery-style%22%3B%0A%20%20const%20PANEL_ID%20%3D%20%22codex-vcv-owned-gallery-panel%22%3B%0A%20%20const%20COUNT_ID%20%3D%20%22codex-vcv-owned-count%22%3B%0A%20%20const%20CARD_CLASS%20%3D%20%22codex-vcv-owned-card%22%3B%0A%20%20const%20EMPTY_ROW_CLASS%20%3D%20%22codex-vcv-empty-row%22%3B%0A%20%20const%20EMPTY_HEADER_CLASS%20%3D%20%22codex-vcv-empty-header%22%3B%0A%0A%20%20if%20(window.location.hostname%20!%3D%3D%20HOST%20%7C%7C%20window.location.pathname%20!%3D%3D%20%22%2Fplugins%22)%20%7B%0A%20%20%20%20throw%20new%20Error(%22Open%20https%3A%2F%2Flibrary.vcvrack.com%2Fplugins%20before%20running%20this%20script.%22)%3B%0A%20%20%7D%0A%0A%20%20if%20(window%5BGLOBAL_KEY%5D%20%26%26%20typeof%20window%5BGLOBAL_KEY%5D.destroy%20%3D%3D%3D%20%22function%22)%20%7B%0A%20%20%20%20window%5BGLOBAL_KEY%5D.destroy()%3B%0A%20%20%7D%0A%0A%20%20const%20delay%20%3D%20(milliseconds)%20%3D%3E%20new%20Promise((resolve)%20%3D%3E%20%7B%0A%20%20%20%20window.setTimeout(resolve%2C%20milliseconds)%3B%0A%20%20%7D)%3B%0A%0A%20%20const%20normalizedButtonText%20%3D%20(button)%20%3D%3E%20button.textContent.replace(%2F%5Cs%2B%2Fg%2C%20%22%20%22).trim()%3B%0A%0A%20%20const%20findButton%20%3D%20(label)%20%3D%3E%20Array.from(document.querySelectorAll(%22button%22))%0A%20%20%20%20.find((button)%20%3D%3E%20normalizedButtonText(button)%20%3D%3D%3D%20label)%3B%0A%0A%20%20%2F%2F%20The%20button%20reads%20%22Show%20all%22%20when%20the%20owned-only%20filter%20is%20already%20active.%0A%20%20const%20showOwnedButton%20%3D%20findButton(%22Show%20owned%22)%3B%0A%20%20if%20(showOwnedButton)%20%7B%0A%20%20%20%20showOwnedButton.click()%3B%0A%20%20%20%20await%20delay(400)%3B%0A%20%20%7D%0A%0A%20%20const%20expandAllButton%20%3D%20findButton(%22Expand%20all%22)%3B%0A%20%20if%20(expandAllButton)%20%7B%0A%20%20%20%20expandAllButton.click()%3B%0A%20%20%20%20await%20delay(400)%3B%0A%20%20%7D%0A%0A%20%20const%20pluginTable%20%3D%20document.querySelector(%22table%22)%3B%0A%20%20if%20(!pluginTable)%20%7B%0A%20%20%20%20throw%20new%20Error(%22The%20VCV%20plugin%20table%20was%20not%20found.%20Make%20sure%20the%20page%20has%20finished%20loading.%22)%3B%0A%20%20%7D%0A%0A%20%20document.documentElement.classList.add(ROOT_CLASS)%3B%0A%0A%20%20const%20style%20%3D%20document.createElement(%22style%22)%3B%0A%20%20style.id%20%3D%20STYLE_ID%3B%0A%20%20style.textContent%20%3D%20%60%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20tr.library-plugins-modules%20ul%20%7B%0A%20%20%20%20%20%20display%3A%20grid%20!important%3B%0A%20%20%20%20%20%20grid-template-columns%3A%20repeat(auto-fill%2C%20minmax(170px%2C%201fr))%3B%0A%20%20%20%20%20%20align-items%3A%20stretch%3B%0A%20%20%20%20%20%20gap%3A%2016px%3B%0A%20%20%20%20%20%20margin%3A%200%3B%0A%20%20%20%20%20%20padding%3A%2018px%208px%2026px%3B%0A%20%20%20%20%20%20list-style%3A%20none%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20tr.library-plugins-modules%20li%20%7B%0A%20%20%20%20%20%20display%3A%20none%20!important%3B%0A%20%20%20%20%20%20min-width%3A%200%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20tr.library-plugins-modules%20li.%24%7BCARD_CLASS%7D%20%7B%0A%20%20%20%20%20%20display%3A%20block%20!important%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20tr.%24%7BEMPTY_ROW_CLASS%7D%2C%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20tr.%24%7BEMPTY_HEADER_CLASS%7D%20%7B%0A%20%20%20%20%20%20display%3A%20none%20!important%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20p.library-plugins-module%20%7B%0A%20%20%20%20%20%20box-sizing%3A%20border-box%3B%0A%20%20%20%20%20%20display%3A%20flex%20!important%3B%0A%20%20%20%20%20%20flex-direction%3A%20column%3B%0A%20%20%20%20%20%20gap%3A%208px%3B%0A%20%20%20%20%20%20height%3A%20100%25%3B%0A%20%20%20%20%20%20margin%3A%200%3B%0A%20%20%20%20%20%20padding%3A%2012px%3B%0A%20%20%20%20%20%20border%3A%201px%20solid%20%2339404c%3B%0A%20%20%20%20%20%20border-radius%3A%209px%3B%0A%20%20%20%20%20%20background%3A%20%23171a20%3B%0A%20%20%20%20%20%20box-shadow%3A%200%203px%2012px%20rgba(0%2C%200%2C%200%2C%200.2)%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20.codex-vcv-module-image-link%20%7B%0A%20%20%20%20%20%20order%3A%200%3B%0A%20%20%20%20%20%20display%3A%20flex%3B%0A%20%20%20%20%20%20align-items%3A%20center%3B%0A%20%20%20%20%20%20justify-content%3A%20center%3B%0A%20%20%20%20%20%20min-height%3A%20260px%3B%0A%20%20%20%20%20%20padding%3A%208px%3B%0A%20%20%20%20%20%20border-radius%3A%206px%3B%0A%20%20%20%20%20%20background%3A%20%230d0f13%3B%0A%20%20%20%20%20%20overflow%3A%20hidden%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20.codex-vcv-module-image%20%7B%0A%20%20%20%20%20%20display%3A%20block%3B%0A%20%20%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20%20%20height%3A%20260px%3B%0A%20%20%20%20%20%20object-fit%3A%20contain%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20.codex-vcv-module-image-link.codex-vcv-image-missing%3A%3Aafter%20%7B%0A%20%20%20%20%20%20color%3A%20%23929baa%3B%0A%20%20%20%20%20%20content%3A%20%22Screenshot%20unavailable%22%3B%0A%20%20%20%20%20%20text-align%3A%20center%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20.codex-vcv-module-image-link.codex-vcv-image-missing%20img%20%7B%0A%20%20%20%20%20%20display%3A%20none%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20p.library-plugins-module%20%3E%20a%3Anot(.codex-vcv-module-image-link)%20%7B%0A%20%20%20%20%20%20order%3A%201%3B%0A%20%20%20%20%20%20display%3A%20block%3B%0A%20%20%20%20%20%20min-height%3A%202.4em%3B%0A%20%20%20%20%20%20color%3A%20%23e8edf4%20!important%3B%0A%20%20%20%20%20%20font-size%3A%2015px%3B%0A%20%20%20%20%20%20font-weight%3A%20700%3B%0A%20%20%20%20%20%20line-height%3A%201.2%3B%0A%20%20%20%20%20%20text-align%3A%20center%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20p.library-plugins-module%20%3E%20.library-remove%20%7B%0A%20%20%20%20%20%20order%3A%202%3B%0A%20%20%20%20%20%20width%3A%20100%25%3B%0A%20%20%20%20%20%20margin-top%3A%20auto%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20%23%24%7BPANEL_ID%7D%20%7B%0A%20%20%20%20%20%20position%3A%20fixed%3B%0A%20%20%20%20%20%20top%3A%2012px%3B%0A%20%20%20%20%20%20right%3A%2012px%3B%0A%20%20%20%20%20%20z-index%3A%2010000%3B%0A%20%20%20%20%20%20box-sizing%3A%20border-box%3B%0A%20%20%20%20%20%20display%3A%20flex%3B%0A%20%20%20%20%20%20align-items%3A%20center%3B%0A%20%20%20%20%20%20justify-content%3A%20space-between%3B%0A%20%20%20%20%20%20gap%3A%2012px%3B%0A%20%20%20%20%20%20width%3A%20min(410px%2C%20calc(100vw%20-%2024px))%3B%0A%20%20%20%20%20%20padding%3A%2012px%3B%0A%20%20%20%20%20%20border%3A%201px%20solid%20%233f526a%3B%0A%20%20%20%20%20%20border-radius%3A%208px%3B%0A%20%20%20%20%20%20background%3A%20rgba(20%2C%2025%2C%2032%2C%200.97)%3B%0A%20%20%20%20%20%20box-shadow%3A%200%204px%2016px%20rgba(0%2C%200%2C%200%2C%200.35)%3B%0A%20%20%20%20%20%20color%3A%20%23e8edf4%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20%23%24%7BCOUNT_ID%7D%20%7B%0A%20%20%20%20%20%20color%3A%20%23fff%3B%0A%20%20%20%20%20%20font-size%3A%2014px%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20html.%24%7BROOT_CLASS%7D%20.codex-vcv-exit-gallery%20%7B%0A%20%20%20%20%20%20padding%3A%206px%209px%3B%0A%20%20%20%20%20%20border%3A%201px%20solid%20%2358677a%3B%0A%20%20%20%20%20%20border-radius%3A%205px%3B%0A%20%20%20%20%20%20background%3A%20%23252b34%3B%0A%20%20%20%20%20%20color%3A%20%23e8edf4%3B%0A%20%20%20%20%20%20cursor%3A%20pointer%3B%0A%20%20%20%20%20%20white-space%3A%20nowrap%3B%0A%20%20%20%20%7D%0A%20%20%60%3B%0A%20%20document.head.appendChild(style)%3B%0A%0A%20%20const%20panel%20%3D%20document.createElement(%22div%22)%3B%0A%20%20panel.id%20%3D%20PANEL_ID%3B%0A%0A%20%20const%20countLabel%20%3D%20document.createElement(%22strong%22)%3B%0A%20%20countLabel.id%20%3D%20COUNT_ID%3B%0A%0A%20%20const%20exitButton%20%3D%20document.createElement(%22button%22)%3B%0A%20%20exitButton.type%20%3D%20%22button%22%3B%0A%20%20exitButton.className%20%3D%20%22codex-vcv-exit-gallery%22%3B%0A%20%20exitButton.textContent%20%3D%20%22Exit%20gallery%22%3B%0A%0A%20%20panel.append(countLabel%2C%20exitButton)%3B%0A%20%20document.body.appendChild(panel)%3B%0A%0A%20%20let%20observer%3B%0A%20%20let%20refreshTimer%3B%0A%0A%20%20function%20addModuleImage(paragraph%2C%20removeButton)%20%7B%0A%20%20%20%20if%20(paragraph.querySelector(%22.codex-vcv-module-image-link%22))%20%7B%0A%20%20%20%20%20%20return%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20const%20pluginSlug%20%3D%20removeButton.dataset.plugin%20%7C%7C%20%22%22%3B%0A%20%20%20%20const%20moduleSlug%20%3D%20removeButton.dataset.module%20%7C%7C%20%22%22%3B%0A%20%20%20%20const%20titleLink%20%3D%20paragraph.querySelector(%22a%5Bhref%5E%3D'%2F'%5D%22)%3B%0A%20%20%20%20const%20title%20%3D%20((titleLink%20%26%26%20titleLink.textContent)%20%7C%7C%20moduleSlug).trim()%3B%0A%0A%20%20%20%20const%20imageLink%20%3D%20document.createElement(%22a%22)%3B%0A%20%20%20%20imageLink.className%20%3D%20%22codex-vcv-module-image-link%22%3B%0A%20%20%20%20imageLink.href%20%3D%20(titleLink%20%26%26%20titleLink.getAttribute(%22href%22))%0A%20%20%20%20%20%20%7C%7C%20%60%2F%24%7BpluginSlug%7D%2F%24%7BmoduleSlug%7D%60%3B%0A%20%20%20%20imageLink.title%20%3D%20%60%24%7Btitle%7D%20%E2%80%94%20open%20module%20page%60%3B%0A%0A%20%20%20%20const%20image%20%3D%20document.createElement(%22img%22)%3B%0A%20%20%20%20image.className%20%3D%20%22codex-vcv-module-image%22%3B%0A%20%20%20%20image.src%20%3D%20%60%2Fscreenshots%2F400%2F%24%7BencodeURIComponent(pluginSlug)%7D%2F%24%7BencodeURIComponent(moduleSlug)%7D.webp%60%3B%0A%20%20%20%20image.alt%20%3D%20%60%24%7Btitle%7D%20module%20panel%60%3B%0A%20%20%20%20image.loading%20%3D%20%22lazy%22%3B%0A%20%20%20%20image.decoding%20%3D%20%22async%22%3B%0A%20%20%20%20image.addEventListener(%22error%22%2C%20()%20%3D%3E%20%7B%0A%20%20%20%20%20%20imageLink.classList.add(%22codex-vcv-image-missing%22)%3B%0A%20%20%20%20%7D%2C%20%7Bonce%3A%20true%7D)%3B%0A%0A%20%20%20%20imageLink.appendChild(image)%3B%0A%20%20%20%20paragraph.insertBefore(imageLink%2C%20paragraph.firstChild)%3B%0A%20%20%7D%0A%0A%20%20function%20updatePluginRows()%20%7B%0A%20%20%20%20document.querySelectorAll(%22tr.library-plugins-modules%22).forEach((moduleRow)%20%3D%3E%20%7B%0A%20%20%20%20%20%20const%20hasOwnedCard%20%3D%20Boolean(moduleRow.querySelector(%60li.%24%7BCARD_CLASS%7D%60))%3B%0A%20%20%20%20%20%20moduleRow.classList.toggle(EMPTY_ROW_CLASS%2C%20!hasOwnedCard)%3B%0A%0A%20%20%20%20%20%20const%20headerRow%20%3D%20moduleRow.previousElementSibling%3B%0A%20%20%20%20%20%20if%20(headerRow%20%26%26%20headerRow.matches(%22tr%22))%20%7B%0A%20%20%20%20%20%20%20%20headerRow.classList.toggle(EMPTY_HEADER_CLASS%2C%20!hasOwnedCard)%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%7D)%3B%0A%20%20%7D%0A%0A%20%20function%20refresh()%20%7B%0A%20%20%20%20let%20ownedCount%20%3D%200%3B%0A%0A%20%20%20%20document.querySelectorAll(%22p.library-plugins-module%22).forEach((paragraph)%20%3D%3E%20%7B%0A%20%20%20%20%20%20const%20item%20%3D%20paragraph.closest(%22li%22)%3B%0A%20%20%20%20%20%20const%20removeButton%20%3D%20paragraph.querySelector(%22button.library-remove%3Anot(.hidden)%22)%3B%0A%0A%20%20%20%20%20%20if%20(!item)%20%7B%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20item.classList.toggle(CARD_CLASS%2C%20Boolean(removeButton))%3B%0A%20%20%20%20%20%20if%20(!removeButton)%20%7B%0A%20%20%20%20%20%20%20%20return%3B%0A%20%20%20%20%20%20%7D%0A%0A%20%20%20%20%20%20addModuleImage(paragraph%2C%20removeButton)%3B%0A%20%20%20%20%20%20ownedCount%20%2B%3D%201%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20updatePluginRows()%3B%0A%20%20%20%20countLabel.textContent%20%3D%20%60%24%7BownedCount%7D%20owned%20modules%20with%20screenshots%60%3B%0A%20%20%7D%0A%0A%20%20function%20scheduleRefresh()%20%7B%0A%20%20%20%20window.clearTimeout(refreshTimer)%3B%0A%20%20%20%20refreshTimer%20%3D%20window.setTimeout(refresh%2C%20250)%3B%0A%20%20%7D%0A%0A%20%20function%20destroy()%20%7B%0A%20%20%20%20window.clearTimeout(refreshTimer)%3B%0A%20%20%20%20if%20(observer)%20%7B%0A%20%20%20%20%20%20observer.disconnect()%3B%0A%20%20%20%20%7D%0A%0A%20%20%20%20document.documentElement.classList.remove(ROOT_CLASS)%3B%0A%20%20%20%20document.getElementById(STYLE_ID)%3F.remove()%3B%0A%20%20%20%20document.getElementById(PANEL_ID)%3F.remove()%3B%0A%20%20%20%20document.querySelectorAll(%22.codex-vcv-module-image-link%22).forEach((element)%20%3D%3E%20element.remove())%3B%0A%20%20%20%20document.querySelectorAll(%60.%24%7BCARD_CLASS%7D%60).forEach((element)%20%3D%3E%20%7B%0A%20%20%20%20%20%20element.classList.remove(CARD_CLASS)%3B%0A%20%20%20%20%7D)%3B%0A%20%20%20%20document.querySelectorAll(%60tr.%24%7BEMPTY_ROW_CLASS%7D%60).forEach((element)%20%3D%3E%20%7B%0A%20%20%20%20%20%20element.classList.remove(EMPTY_ROW_CLASS)%3B%0A%20%20%20%20%7D)%3B%0A%20%20%20%20document.querySelectorAll(%60tr.%24%7BEMPTY_HEADER_CLASS%7D%60).forEach((element)%20%3D%3E%20%7B%0A%20%20%20%20%20%20element.classList.remove(EMPTY_HEADER_CLASS)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20delete%20window%5BGLOBAL_KEY%5D%3B%0A%20%20%20%20console.info(%22VCV%20owned-module%20gallery%20removed.%20Reloading%20the%20page%20also%20restores%20the%20original%20view.%22)%3B%0A%20%20%7D%0A%0A%20%20exitButton.addEventListener(%22click%22%2C%20destroy)%3B%0A%20%20refresh()%3B%0A%0A%20%20observer%20%3D%20new%20MutationObserver((mutations)%20%3D%3E%20%7B%0A%20%20%20%20const%20ownershipChanged%20%3D%20mutations.some((mutation)%20%3D%3E%20%7B%0A%20%20%20%20%20%20if%20(mutation.type%20%3D%3D%3D%20%22childList%22)%20%7B%0A%20%20%20%20%20%20%20%20return%20true%3B%0A%20%20%20%20%20%20%7D%0A%20%20%20%20%20%20return%20mutation.target%20instanceof%20Element%0A%20%20%20%20%20%20%20%20%26%26%20mutation.target.matches(%22button.library-remove%2C%20button.library-add%22)%3B%0A%20%20%20%20%7D)%3B%0A%0A%20%20%20%20if%20(ownershipChanged)%20%7B%0A%20%20%20%20%20%20scheduleRefresh()%3B%0A%20%20%20%20%7D%0A%20%20%7D)%3B%0A%20%20observer.observe(pluginTable%2C%20%7B%0A%20%20%20%20subtree%3A%20true%2C%0A%20%20%20%20childList%3A%20true%2C%0A%20%20%20%20attributes%3A%20true%2C%0A%20%20%20%20attributeFilter%3A%20%5B%22class%22%5D%0A%20%20%7D)%3B%0A%0A%20%20window%5BGLOBAL_KEY%5D%20%3D%20%7B%0A%20%20%20%20destroy%2C%0A%20%20%20%20refresh%0A%20%20%7D%3B%0A%0A%20%20console.info(%0A%20%20%20%20%60VCV%20owned-module%20gallery%20enabled%3A%20%24%7Bdocument.querySelectorAll(%60li.%24%7BCARD_CLASS%7D%60).length%7D%20modules.%20%60%0A%20%20%20%20%20%20%2B%20%22Use%20window.__vcvOwnedGallery.destroy()%20to%20restore%20the%20original%20presentation.%22%0A%20%20)%3B%0A%7D)()%3B
How bookmarklet works

Hope you’ll find it helpful.

3 Likes