Moxfield Tag Highlighter

Highlights cards in the Moxfield deck builder that have multiple shared tags, showing which cards in your deck have the most synergy.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Moxfield Tag Highlighter
// @namespace    https://moxfield.com/
// @version      3.5.1
// @description  Highlights cards in the Moxfield deck builder that have multiple shared tags, showing which cards in your deck have the most synergy.
// @match        https://moxfield.com/*
// @match        https://www.moxfield.com/*
// @run-at       document-idle
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
    'use strict';

    const ROOT_SELECTOR = 'section.deckview';
    const ROW_SELECTOR = 'li[data-hash]';
    const ATTR_ID = 'data-hash';
    const MIN_COUNT = 2;

    const COLORS = {
        2: 'rgba(59, 130, 246, 0.4)',
        3: 'rgba(168, 85, 247, 0.5)',
        4: 'rgba(6, 182, 212, 0.6)'
    };

    let isProcessing = false;

    const isDeckPage = () => {
        const path = window.location.pathname;
        return path.startsWith('/decks/') &&
              !path.startsWith('/decks/public') &&
              !path.startsWith('/decks/personal');
    };

    const isGroupedByTags = () => {
        const groupSelect = document.getElementById('groupBy');
        return groupSelect && groupSelect.value === 'tag';
    };

    function shouldShow() {
        return isDeckPage() && isGroupedByTags();
    }

    function updateVisibility() {
        let container = document.getElementById('mh-legend');

        if (shouldShow() && !container) {
            injectLegend();
            container = document.getElementById('mh-legend');
        }

        if (!container) return;

        const active = shouldShow();
        container.style.display = active ? 'flex' : 'none';

        if (active) {
            highlightRows();
        } else {
            clearHighlights();
        }
    }

    function clearHighlights() {
        const root = document.querySelector(ROOT_SELECTOR);
        if (!root) return;
        root.querySelectorAll(ROW_SELECTOR).forEach(row => {
            const target = row.querySelector('.w-100') || row;
            target.style.backgroundColor = '';
            target.style.paddingLeft = '';
            target.style.borderRadius = '';
        });
    }

    function injectLegend() {
        if (document.getElementById('mh-legend')) return;

        const isEnabled = localStorage.getItem('mh-highlighter-enabled') !== 'false';

        const container = document.createElement('div');
        container.id = 'mh-legend';
        container.style.cssText = `
            position: fixed; bottom: 112px; right: 30px;
            font-family: sans-serif; font-size: 11px; color: #eee;
            z-index: 9999; display: flex; flex-direction: column-reverse;
            filter: drop-shadow(0 4px 12px rgba(0,0,0,0.5));
            width: 143.5px;
        `;

        const headerPill = document.createElement('div');
        headerPill.style.cssText = `
            background: #444; display: flex; align-items: center;
            border-radius: 30px; border: 1px solid #555; overflow: hidden;
            height: 35px; width: 100%;
        `;

        const toggleZone = document.createElement('div');
        toggleZone.style.cssText = `
            padding: 0 12px; height: 100%; display: flex; align-items: center;
            cursor: pointer; border-right: 1px solid #555; transition: background 0.2s;
            background: ${isEnabled ? 'transparent' : '#333'};
        `;

        const burstIcon = document.createElement('div');
        burstIcon.style.cssText = `
            display: flex; flex-direction: column; gap: 2px;
            width: 16px; transition: filter 0.3s;
            filter: ${isEnabled ? 'saturate(1)' : 'grayscale(1) opacity(0.5)'};
        `;

        [ {w:'100%', c:COLORS[4]}, {w:'70%', c:COLORS[3]}, {w:'85%', c:COLORS[2]} ].forEach(row => {
            const r = document.createElement('div');
            r.style.cssText = `width: ${row.w}; height: 2.5px; background: ${row.c}; border-radius: 1px;`;
            burstIcon.appendChild(r);
        });

        const legendZone = document.createElement('div');
        legendZone.style.cssText = `
            flex-grow: 1; height: 100%; display: flex; align-items: center;
            justify-content: center; cursor: pointer; user-select: none;
            font-weight: bold; color: #fff; padding-right: 8px;
        `;

        const title = document.createElement('span');
        title.innerText = 'Tag Legend';
        const toggleIcon = document.createElement('span');
        toggleIcon.style.cssText = 'margin-left: 6px; font-size: 12px; width: 10px; text-align: center;';
        toggleIcon.innerText = '+';

        toggleZone.appendChild(burstIcon);
        legendZone.appendChild(title);
        legendZone.appendChild(toggleIcon);
        headerPill.appendChild(toggleZone);
        headerPill.appendChild(legendZone);

        const content = document.createElement('div');
        content.id = 'mh-legend-content';
        content.style.cssText = `
            padding: 12px 14px; background: #2a2a2a; border: 1px solid #444;
            border-radius: 12px; margin-bottom: 5px; display: none; width: 100%;
        `;

        const createItem = (color, text) => {
            const item = document.createElement('div');
            item.style.cssText = 'display:flex; align-items:center; margin-bottom:6px;';
            item.innerHTML = `<div style="width:12px; height:12px; background:${color}; border:1px solid rgba(255,255,255,0.2); margin-right:10px; border-radius:2px;"></div> ${text}`;
            return item;
        };

        [2, 3, 4].forEach(n => content.appendChild(createItem(COLORS[n], `${n}${n===4?'+':''} Tags`)));

        container.appendChild(headerPill);
        container.appendChild(content);

        toggleZone.addEventListener('click', (e) => {
            e.stopPropagation();
            const newState = !(localStorage.getItem('mh-highlighter-enabled') !== 'false');
            localStorage.setItem('mh-highlighter-enabled', newState);
            burstIcon.style.filter = newState ? 'saturate(1)' : 'grayscale(1) opacity(0.5)';
            toggleZone.style.background = newState ? 'transparent' : '#333';
            updateVisibility();
        });

        legendZone.addEventListener('click', () => {
            const isHidden = content.style.display === 'none';
            content.style.display = isHidden ? 'block' : 'none';
            toggleIcon.innerText = isHidden ? '−' : '+';
        });

        document.body.appendChild(container);
    }

    function highlightRows() {
        if (isProcessing || !shouldShow()) return;
        isProcessing = true;
        const isEnabled = localStorage.getItem('mh-highlighter-enabled') !== 'false';
        const root = document.querySelector(ROOT_SELECTOR);
        if (!root) { isProcessing = false; return; }

        const allRows = Array.from(root.querySelectorAll(ROW_SELECTOR));
        const counts = {};
        allRows.forEach(row => {
            const hash = row.getAttribute(ATTR_ID);
            if (hash) counts[hash] = (counts[hash] || 0) + 1;
        });

        allRows.forEach(row => {
            const hash = row.getAttribute(ATTR_ID);
            const count = counts[hash] || 0;
            const target = row.querySelector('.w-100') || row;

            if (isEnabled && count >= MIN_COUNT) {
                target.style.setProperty('background-color', COLORS[Math.min(count, 4)], 'important');
                target.style.setProperty('padding-left', '4px', 'important');
                target.style.setProperty('border-radius', '7px');
            } else {
                target.style.backgroundColor = '';
                target.style.paddingLeft = '';
                target.style.borderRadius = '';
            }
        });
        setTimeout(() => { isProcessing = false; }, 300);
    }

    // --- Listeners ---
    document.addEventListener('change', (e) => {
        if (e.target && e.target.id === 'groupBy') updateVisibility();
    });

    document.addEventListener('click', (e) => {
        if (e.target.closest('.modal-footer .btn-primary')) setTimeout(highlightRows, 500);
    });

    document.addEventListener('keydown', (e) => {
        if (e.shiftKey && e.key.toLowerCase() === 't') setTimeout(highlightRows, 800);
    });

    let lastUrl = location.href;
    new MutationObserver(() => {
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            updateVisibility();
        }
    }).observe(document, { subtree: true, childList: true });

    const observer = new MutationObserver((mutations) => {
        if (mutations.some(m => m.addedNodes.length > 0)) updateVisibility();
    });

    const init = () => {
        injectLegend();
        updateVisibility();
        observer.observe(document.body, { childList: true, subtree: true });
    };

    // Ensure the script waits for the page to be ready but fires reliably
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();