Last active
April 18, 2026 10:03
-
-
Save shelllee/88d6d48ba671101a63e5177fe127ffb4 to your computer and use it in GitHub Desktop.
Unity Asset Store Wishlist Tag Filter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Unity Asset Store Wishlist Tag Filter | |
| // @namespace http://tampermonkey.net/ | |
| // @version 1.0 | |
| // @description Add search box to Unity Asset Store collection dialog for quick filtering | |
| // @author You | |
| // @match https://assetstore.unity.com/* | |
| // @grant none | |
| // @run-at document-idle | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict'; | |
| const STYLE_ID = 'ua-collection-search-style'; | |
| const INPUT_ID = 'ua-collection-search-input'; | |
| function injectStyle() { | |
| if (document.getElementById(STYLE_ID)) return; | |
| const style = document.createElement('style'); | |
| style.id = STYLE_ID; | |
| style.textContent = ` | |
| .ua-search-wrap { | |
| padding: 8px 16px; | |
| position: relative; | |
| } | |
| .ua-search-wrap input { | |
| width: 100%; | |
| box-sizing: border-box; | |
| padding: 8px 12px 8px 32px; | |
| border: 1px solid #555; | |
| border-radius: 6px; | |
| background: #2a2a2a; | |
| color: #eee; | |
| font-size: 14px; | |
| outline: none; | |
| transition: border-color 0.2s; | |
| } | |
| .ua-search-wrap input:focus { | |
| border-color: #4a90d9; | |
| } | |
| .ua-search-wrap input::placeholder { | |
| color: #888; | |
| } | |
| .ua-search-wrap .ua-search-icon { | |
| position: absolute; | |
| left: 26px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: #888; | |
| font-size: 14px; | |
| pointer-events: none; | |
| } | |
| .ua-search-count { | |
| padding: 4px 16px 0; | |
| font-size: 12px; | |
| color: #888; | |
| display: none; | |
| } | |
| .ua-search-count.visible { | |
| display: block; | |
| } | |
| .ua-collection-hidden { | |
| display: none !important; | |
| } | |
| `; | |
| document.head.appendChild(style); | |
| } | |
| function createSearchUI(container) { | |
| // Already injected | |
| if (document.getElementById(INPUT_ID)) return; | |
| // Find the scrollable collection list | |
| const scrollable = container.querySelector('.modal-inner-scrollable'); | |
| if (!scrollable) return; | |
| // Build search input wrapper | |
| const wrap = document.createElement('div'); | |
| wrap.className = 'ua-search-wrap'; | |
| const icon = document.createElement('span'); | |
| icon.className = 'ua-search-icon'; | |
| icon.textContent = '\u{1F50D}'; | |
| const input = document.createElement('input'); | |
| input.id = INPUT_ID; | |
| input.type = 'text'; | |
| input.placeholder = 'Search collections...'; | |
| // Match count display | |
| const countEl = document.createElement('div'); | |
| countEl.className = 'ua-search-count'; | |
| wrap.appendChild(icon); | |
| wrap.appendChild(input); | |
| scrollable.parentElement.insertBefore(wrap, scrollable); | |
| scrollable.parentElement.insertBefore(countEl, scrollable); | |
| // Filter collections on input | |
| input.addEventListener('input', () => { | |
| const keyword = input.value.trim().toLowerCase(); | |
| const items = scrollable.querySelectorAll('._3bUE8'); | |
| let visibleCount = 0; | |
| let totalCount = 0; | |
| items.forEach(item => { | |
| totalCount++; | |
| const nameEl = item.querySelector('._1irTB'); | |
| const name = nameEl ? nameEl.textContent.toLowerCase() : ''; | |
| if (!keyword || name.includes(keyword)) { | |
| item.classList.remove('ua-collection-hidden'); | |
| visibleCount++; | |
| } else { | |
| item.classList.add('ua-collection-hidden'); | |
| } | |
| }); | |
| if (keyword) { | |
| countEl.textContent = `${visibleCount} / ${totalCount} collections`; | |
| countEl.classList.add('visible'); | |
| } else { | |
| countEl.classList.remove('visible'); | |
| } | |
| }); | |
| // Auto focus the search input | |
| setTimeout(() => input.focus(), 100); | |
| } | |
| function cleanup() { | |
| const existing = document.getElementById(INPUT_ID); | |
| if (existing) { | |
| existing.closest('.ua-search-wrap')?.remove(); | |
| } | |
| const countEl = document.querySelector('.ua-search-count'); | |
| if (countEl) countEl.remove(); | |
| // Restore all hidden collection items | |
| document.querySelectorAll('.ua-collection-hidden').forEach(el => { | |
| el.classList.remove('ua-collection-hidden'); | |
| }); | |
| } | |
| // Watch for the "add to list" dialog appearing | |
| function init() { | |
| injectStyle(); | |
| const observer = new MutationObserver(mutations => { | |
| for (const mutation of mutations) { | |
| for (const node of mutation.addedNodes) { | |
| if (!(node instanceof HTMLElement)) continue; | |
| // Check if the "add to list" dialog appeared | |
| if ( | |
| node.querySelector?.('[aria-labelledby="add-to-list-title"]') || | |
| node.querySelector?.('.modal-inner-scrollable') | |
| ) { | |
| setTimeout(() => createSearchUI(node), 150); | |
| return; | |
| } | |
| // Check if the node itself is the dialog | |
| if ( | |
| node.getAttribute?.('aria-labelledby') === 'add-to-list-title' || | |
| node.classList?.contains('modal-inner-scrollable') | |
| ) { | |
| setTimeout(() => createSearchUI(node.closest('[role="dialog"]') || node), 150); | |
| return; | |
| } | |
| } | |
| // Cleanup when dialog is removed | |
| for (const node of mutation.removedNodes) { | |
| if (!(node instanceof HTMLElement)) continue; | |
| if (node.querySelector?.('.modal-inner-scrollable')) { | |
| cleanup(); | |
| return; | |
| } | |
| } | |
| } | |
| }); | |
| observer.observe(document.body, { childList: true, subtree: true }); | |
| } | |
| // Handle case where dialog is already open when script loads | |
| function checkExisting() { | |
| const dialog = document.querySelector('[aria-labelledby="add-to-list-title"]'); | |
| if (dialog && !document.getElementById(INPUT_ID)) { | |
| createSearchUI(dialog); | |
| } | |
| } | |
| if (document.readyState === 'complete') { | |
| init(); | |
| checkExisting(); | |
| } else { | |
| window.addEventListener('load', () => { | |
| init(); | |
| checkExisting(); | |
| }); | |
| } | |
| })(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment