Infinite Craft More Pins & Colored Tabs (Updated)

Create tabs to group items and color them for organization. Updated to work with "Helper: Not-so-budget Edition".

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

// ==UserScript==
// @name         Infinite Craft More Pins & Colored Tabs (Updated)
// @version      2.0.3
// @namespace    https://github.com/ChessScholar
// @description  Create tabs to group items and color them for organization. Updated to work with "Helper: Not-so-budget Edition".
// @author       ChessScholar (updated for compatibility by AI)
// @match        https://neal.fun/infinite-craft/
// @icon         https://neal.fun/favicons/infinite-craft.png
// @grant        unsafeWindow
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// @compatible   chrome
// @compatible   firefox
// @license      MIT
// @credits      adrianmgg for original "tweaks" script, Natasquare for the helper script.
// ==/UserScript==

(function() {
    'use strict';

    class GMValue {
        constructor(key, defaultValue) { this.key = key; this.defaultValue = defaultValue; }
        async set(value) { await GM_setValue(this.key, value); }
        async get() { return await GM_getValue(this.key, this.defaultValue); }
    }

    const VAL_PINNED_SETS = new GMValue('infinitecraft_pinned_sets_v2', {});
    const VAL_PINNED_SETS_ORDER = new GMValue('infinitecraft_pinned_sets_order_v1', []);
    const VAL_SETTINGS = new GMValue('infinitecraft_pinned_sets_settings_v1', {
        expandContainer: false,
        combineColors: true,
    });

    async function initialize(ICHelper, v_container, v_sidebar) {
        let settings = await VAL_SETTINGS.get();
        let zIndexCounter = 1;

        const el = {
            setup(elem, options) {
                const { style, attrs, dataset, events, classList, children, parent, insertBefore, ...props } = options;
                Object.assign(elem.style, style);
                Object.entries(style?.vars || {}).forEach(([k, v]) => elem.style.setProperty(k, v));
                Object.entries(attrs || {}).forEach(([k, v]) => elem.setAttribute(k, v));
                Object.entries(dataset || {}).forEach(([k, v]) => elem.dataset[k] = v);
                Object.entries(events || {}).forEach(([k, v]) => elem.addEventListener(k, v));
                elem.classList.add(...(classList || []));
                Object.assign(elem, props);
                (children || []).forEach(c => elem.appendChild(c));
                if (parent) { if (insertBefore) parent.insertBefore(elem, insertBefore); else parent.appendChild(elem); }
                return elem;
            },
            create(tagName, options = {}) { return this.setup(document.createElement(tagName), options); },
        };

        const css = `
            .pinned-set {
                margin-left: -40px; /* Overlap by 33% of 120px width */
                transition: transform 0.1s ease-in-out, box-shadow 0.1s ease-in-out, margin-left 0.1s ease-in-out;
            }
            .pinned-set:hover {
                transform: translateY(-3px);
                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            }
            .pinned-set.dragging { opacity: 0.4; }
            .drop-placeholder { background: rgba(128, 128, 128, 0.2); border: 1px dashed #888; border-radius: 5px; }
        `;
        el.create('style', { parent: document.head, textContent: css });

        const pinnedTabsContainer = el.create('div', {
            style: {
                display: 'flex', flexDirection: 'row', alignItems: 'flex-start',
                position: 'relative', background: 'var(--sidebar-bg)',
                width: '100%', borderBottom: '1px solid var(--border-color)',
                zIndex: '1', padding: '5px'
            },
        });

        const pinnedTabsList = el.create('div', {
            parent: pinnedTabsContainer,
            style: {
                display: 'flex', flexDirection: 'row', alignItems: 'center',
                gap: '5px', flexWrap: 'wrap',
                flex: '1 1 auto'
            }
        });

        const pinnedItemsContainer = el.create('div', {
            style: {
                borderBottom: '1px solid var(--border-color)', zIndex: '1',
                padding: '5px', background: 'var(--sidebar-bg)',
                maxHeight: '40%', overflowY: 'auto',
                transition: 'max-height 0.2s ease-in-out'
            },
        });

        const combinedItemsContainer = el.create('div', {
            parent: pinnedItemsContainer,
            style: { display: 'none', flexWrap: 'wrap' }
        });

        const sidebarInner = v_sidebar.$el.querySelector('.sidebar-inner');
        sidebarInner.insertBefore(pinnedItemsContainer, sidebarInner.firstChild);
        sidebarInner.insertBefore(pinnedTabsContainer, sidebarInner.firstChild);

        function getDialogBaseStyle() {
            const isDark = document.body.classList.contains('dark-mode');
            return {
                background: isDark ? '#2d2d2d' : '#f0f0f0', color: 'var(--text-color)',
                padding: '20px', border: '1px solid var(--border-color)',
                borderRadius: '8px', boxShadow: '0 5px 15px rgba(0,0,0,0.3)',
                margin: 'auto',
            };
        }

        function generateRandomColor() {
            const hue = Math.floor(Math.random() * 360);
            return `hsl(${hue}, 75%, 60%)`;
        }

        function addPinnedElementInternal(element, setname) {
            const setItemContainer = pinnedItemsContainer.querySelector(`[data-set-items="${CSS.escape(setname)}"]`);
            if (!setItemContainer) return;
            const elementExists = Array.from(setItemContainer.children).some(child => child.querySelector(".item")?.textContent.trim() === element.text.trim());

            if (!elementExists) {
                const elementWrapper = el.create('div', { parent: setItemContainer, classList: ['item-wrapper'], style: { display: 'inline-block' }, dataset: { originSet: setname } });
                const elementElement = ICHelper.createItemElement(element);
                elementWrapper.appendChild(elementElement);
                el.setup(elementElement, {
                    events: {
                        mousedown: async (e) => {
                            if (e.button === 1 || (e.button === 0 && e.altKey)) {
                                e.preventDefault(); e.stopPropagation();
                                elementWrapper.remove();
                                const sets = await VAL_PINNED_SETS.get();
                                sets[setname].elements = sets[setname].elements.filter(eText => eText !== element.text);
                                await VAL_PINNED_SETS.set(sets);
                            }
                        },
                    },
                });
            }
        }

        async function addElementToSelectedTabs(element) {
            const selectedSetNames = Array.from(pinnedTabsList.querySelectorAll('.selected-set')).map(set => set.dataset.pinnedSet);
            if (selectedSetNames.length === 0) { alert("No tab selected. Click a tab to select it for pinning."); return; }
            const sets = await VAL_PINNED_SETS.get();
            let changed = false;
            for (const setname of selectedSetNames) {
                if (sets[setname] && !sets[setname].elements.includes(element.text)) {
                    addPinnedElementInternal(element, setname);
                    sets[setname].elements.push(element.text);
                    changed = true;
                }
            }
            if (changed) await VAL_PINNED_SETS.set(sets);
        }

        v_sidebar.$el.addEventListener("mousedown", e => { if (e.altKey && e.button === 0) { const item = e.target.closest(".item"); if (!item || e.target.closest('.items-pinned-inner') || e.target.closest('.set-items')) return; e.preventDefault(); e.stopPropagation(); const element = { text: item.getAttribute("data-item-text"), emoji: item.getAttribute("data-item-emoji") }; addElementToSelectedTabs(element); } }, true);
        window.addEventListener("mousedown", e => { if (e.button === 1) { const instance = e.target.closest(".instance"); if (!instance) return; e.preventDefault(); e.stopPropagation(); const element = { text: instance.textContent.trim().split(" ").slice(1).join(" "), emoji: instance.querySelector(".instance-emoji")?.textContent }; addElementToSelectedTabs(element); } }, true);

        async function updatePinDisplay() {
            Array.from(combinedItemsContainer.children).forEach(itemWrapper => {
                const originSet = itemWrapper.dataset.originSet;
                if (originSet) {
                    const originalContainer = pinnedItemsContainer.querySelector(`[data-set-items="${CSS.escape(originSet)}"]`);
                    if (originalContainer) originalContainer.appendChild(itemWrapper);
                }
            });

            const selectedTabs = Array.from(pinnedTabsList.querySelectorAll('.selected-set'));
            const useCombinedView = settings.combineColors && selectedTabs.length > 1;

            if (settings.expandContainer && selectedTabs.length > 0) {
                pinnedItemsContainer.style.maxHeight = 'none';
                pinnedItemsContainer.style.overflowY = 'visible';
            } else {
                pinnedItemsContainer.style.maxHeight = '40%';
                pinnedItemsContainer.style.overflowY = 'auto';
            }

            combinedItemsContainer.style.display = 'none';
            pinnedItemsContainer.querySelectorAll('.set-items').forEach(c => c.style.display = 'none');

            if (selectedTabs.length === 0) {
                pinnedItemsContainer.style.background = 'var(--sidebar-bg)';
                return;
            }

            if (useCombinedView) {
                const displayedItems = new Set();
                combinedItemsContainer.replaceChildren();
                selectedTabs.forEach(tab => {
                    const setname = tab.dataset.pinnedSet;
                    const individualContainer = pinnedItemsContainer.querySelector(`[data-set-items="${CSS.escape(setname)}"]`);
                    if (individualContainer) {
                        Array.from(individualContainer.children).forEach(itemWrapper => {
                            const itemText = itemWrapper.querySelector('.item').textContent.trim();
                            if (!displayedItems.has(itemText)) {
                                displayedItems.add(itemText);
                                combinedItemsContainer.appendChild(itemWrapper);
                            }
                        });
                    }
                });
                const colors = selectedTabs.map(tab => tab.style.backgroundColor);
                pinnedItemsContainer.style.background = `linear-gradient(to bottom right, ${colors.join(', ')})`;
                combinedItemsContainer.style.display = 'block';
            } else {
                pinnedItemsContainer.style.background = 'var(--sidebar-bg)';
                selectedTabs.forEach(tab => {
                    const setname = tab.dataset.pinnedSet;
                    const container = pinnedItemsContainer.querySelector(`[data-set-items="${CSS.escape(setname)}"]`);
                    if (container) {
                        container.style.background = tab.style.backgroundColor;
                        container.style.display = 'block';
                    }
                });
            }
        }

        function hsvToRgb(h, s, v) { let r, g, b; const i = Math.floor(h / 60); const f = h / 60 - i; const p = v * (1 - s); const q = v * (1 - f * s); const t = v * (1 - (1 - f) * s); switch (i % 6) { case 0: r = v, g = t, b = p; break; case 1: r = q, g = v, b = p; break; case 2: r = p, g = v, b = t; break; case 3: r = p, g = q, b = v; break; case 4: r = t, g = p, b = v; break; case 5: r = v, g = p, b = q; break; } return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)]; }
        function createColorWheel(setname, setContainer) {
            const dialog = el.create('dialog', { style: { ...getDialogBaseStyle(), borderRadius: '50%', padding: '0', overflow: 'hidden' }, parent: document.body, events: { close: (e) => e.target.remove() } });
            dialog.showModal();
            const canvas = el.create('canvas', { attrs: { width: '150', height: '150' }, parent: dialog });
            const ctx = canvas.getContext('2d'), radius = 75, image = ctx.createImageData(150, 150), data = image.data;
            for (let x = -radius; x < radius; x++) for (let y = -radius; y < radius; y++) { const dist = Math.sqrt(x * x + y * y); if (dist > radius) continue; const angle = (Math.atan2(y, x) * 180 / Math.PI + 360) % 360; const [r, g, b] = hsvToRgb(angle, dist / radius, 1); const index = (x + radius + (y + radius) * 150) * 4; data[index] = r; data[index + 1] = g; data[index + 2] = b; data[index + 3] = 255; }
            ctx.putImageData(image, 0, 0);
            canvas.addEventListener('click', async (e) => {
                const rect = canvas.getBoundingClientRect(), x = e.clientX - rect.left - radius, y = e.clientY - rect.top - radius, dist = Math.sqrt(x * x + y * y);
                if (dist <= radius) {
                    const angle = (Math.atan2(y, x) * 180 / Math.PI + 360) % 360, [r, g, b] = hsvToRgb(angle, dist / radius, 1), color = `rgb(${r}, ${g}, ${b})`;
                    const sets = await VAL_PINNED_SETS.get(); sets[setname].color = color;
                    setContainer.style.backgroundColor = color;
                    await VAL_PINNED_SETS.set(sets); await updatePinDisplay();
                }
                dialog.close();
            });
        }

        function showNameDialog(title, currentName = '', onConfirm) {
            const dialog = el.create('dialog', { parent: document.body, style: getDialogBaseStyle(), events: { close: e => e.target.remove() }, children: [ el.create('h3', { textContent: title, style: { marginTop: '0', textAlign: 'center' } }), el.create('input', { attrs: { type: 'text', value: currentName, placeholder: 'Set name...' }, dataset: { nameInput: '' }, style: { width: '200px' } }), el.create('div', { style: { display: 'flex', justifyContent: 'space-around', marginTop: '15px' }, children: [ el.create('button', { textContent: 'Cancel', events: { click: () => dialog.close() } }), el.create('button', { textContent: 'Confirm', events: { click: () => { const newName = dialog.querySelector('[data-name-input]').value.trim(); onConfirm(newName); dialog.close(); } } }) ] }) ] });
            dialog.showModal();
            dialog.querySelector('input').focus();
        }

        async function loadPinnedSets() {
            const sets = await VAL_PINNED_SETS.get();
            let setsOrder = await VAL_PINNED_SETS_ORDER.get();

            const selectedBefore = Array.from(pinnedTabsList.querySelectorAll('.selected-set')).map(t => t.dataset.pinnedSet);

            pinnedTabsList.replaceChildren();
            pinnedItemsContainer.querySelectorAll('.set-items').forEach(c => c.remove());

            if (setsOrder.length !== Object.keys(sets).length) {
                const existingOrder = setsOrder.filter(name => sets[name]);
                const newKeys = Object.keys(sets).filter(name => !existingOrder.includes(name));
                setsOrder = [...existingOrder, ...newKeys];
                await VAL_PINNED_SETS_ORDER.set(setsOrder);
            }

            const btnStyle = { fontWeight: 'bold', cursor: 'pointer', userSelect: 'none', padding: '5px', borderRadius: '5px', border: '1px solid var(--border-color)', zIndex: '11', flexShrink: 0 };

            const controlsContainer = el.create('div', {style: {display: 'flex', flexDirection: 'row', gap: '5px', alignItems: 'center', marginRight: '50px', flexShrink: 0}});
            el.create('button', { parent: controlsContainer, textContent: 'All', style: btnStyle, events: { click: () => { document.querySelectorAll('.pinned-set:not(.selected-set)').forEach(sc => sc.click()); } } });
            el.create('button', { parent: controlsContainer, textContent: 'None', style: btnStyle, events: { click: () => { document.querySelectorAll('.pinned-set.selected-set').forEach(sc => sc.click()); } } });
            const addButton = el.create('div', { parent: controlsContainer, textContent: '➕', style: { ...btnStyle } });
            addButton.addEventListener('click', () => { showNameDialog("Create New Set", '', async (newName) => { if (newName) { const s = await VAL_PINNED_SETS.get(); if (s[newName]) { alert("A set with this name already exists."); return; } s[newName] = { elements: [], color: generateRandomColor() }; await VAL_PINNED_SETS.set(s); let order = await VAL_PINNED_SETS_ORDER.get(); order.push(newName); await VAL_PINNED_SETS_ORDER.set(order); await loadPinnedSets(); } }); });
            const settingsButton = el.create('div', { parent: controlsContainer, textContent: '⚙️', style: { ...btnStyle } });
            settingsButton.addEventListener('click', () => { createSettingsDialog().showModal(); });
            pinnedTabsList.appendChild(controlsContainer);

            for (const [index, setname] of setsOrder.entries()) {
                if(!sets[setname]) continue;

                const renameButton = el.create('span', { textContent: '✏️', style: { cursor: 'pointer', zIndex: '12', fontSize: '12px' } });
                renameButton.addEventListener('click', (e) => { e.stopPropagation(); showNameDialog("Rename Set", setname, async (newName) => { if (newName && newName !== setname) { const s = await VAL_PINNED_SETS.get(); if (s[newName]) { alert("A set with this name already exists."); return; } s[newName] = s[setname]; delete s[setname]; await VAL_PINNED_SETS.set(s); let order = await VAL_PINNED_SETS_ORDER.get(); const orderIndex = order.indexOf(setname); if(orderIndex > -1) order[orderIndex] = newName; await VAL_PINNED_SETS_ORDER.set(order); await loadPinnedSets(); } }); });

                const tabTitleStyle = { fontWeight: 'bold', userSelect: 'none', zIndex: '11', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', textAlign: 'center', width: '100%', textShadow: '-1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000' };
                const setContainer = el.create('div', { draggable: true, dataset: { pinnedSet: setname }, classList: ['pinned-set'], style: { backgroundColor: sets[setname].color || 'var(--sidebar-bg)', padding: '5px', borderRadius: '5px', display: 'flex', flexDirection: 'column', alignItems: 'center', position: 'relative', flexShrink: '0', width: '120px', cursor: 'grab', zIndex: index }, children: [ el.create('div', { textContent: setname, style: tabTitleStyle }), el.create('div', { style: { display: 'flex', flexDirection: 'row', alignItems: 'center', marginTop: '5px', gap: '8px' }, children: [ el.create('span', { textContent: '❌', style: { cursor: 'pointer', zIndex: '12', fontSize: '12px' }, events: { click: async (e) => { e.stopPropagation(); if (confirm(`Delete set "${setname}"?`)) { const s = await VAL_PINNED_SETS.get(); delete s[setname]; await VAL_PINNED_SETS.set(s); let order = await VAL_PINNED_SETS_ORDER.get(); await VAL_PINNED_SETS_ORDER.set(order.filter(name => name !== setname)); await loadPinnedSets(); } } } }), renameButton, el.create('span', { textContent: '🎨', style: { cursor: 'pointer', zIndex: '12', fontSize: '12px' }, events: { click: (e) => { e.stopPropagation(); createColorWheel(setname, setContainer); } } }) ] }) ] });
                if(index === 0) setContainer.style.marginLeft = '0';

                setContainer.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const isSelected = setContainer.classList.toggle('selected-set');
                    setContainer.style.outline = isSelected ? '2px solid #55aaff' : 'none';
                    if (isSelected) {
                        setContainer.style.zIndex = ++zIndexCounter + setsOrder.length;
                    } else {
                        setContainer.style.zIndex = index;
                    }
                    updatePinDisplay();
                });

                setContainer.addEventListener('dragstart', (e) => { e.dataTransfer.setData('text/plain', setname); e.dataTransfer.effectAllowed = 'move'; setTimeout(() => e.target.classList.add('dragging'), 0); });
                setContainer.addEventListener('dragend', (e) => e.target.classList.remove('dragging'));
                setContainer.addEventListener('dragover', (e) => e.preventDefault());

                let placeholder = null;
                setContainer.addEventListener('dragenter', (e) => { e.preventDefault(); if (e.target.closest('.pinned-set') && e.target.closest('.pinned-set') !== placeholder) { placeholder = e.target.closest('.pinned-set'); placeholder.classList.add('drop-placeholder'); } });
                setContainer.addEventListener('dragleave', (e) => { e.preventDefault(); if(placeholder && !placeholder.contains(e.relatedTarget)) { placeholder.classList.remove('drop-placeholder'); placeholder = null; } });

                setContainer.addEventListener('drop', async (e) => {
                    e.preventDefault();
                    if(placeholder) placeholder.classList.remove('drop-placeholder');
                    const draggedSetName = e.dataTransfer.getData('text/plain');
                    const targetSetName = e.target.closest('.pinned-set').dataset.pinnedSet;
                    if (draggedSetName !== targetSetName) {
                        let order = await VAL_PINNED_SETS_ORDER.get();
                        const draggedIndex = order.indexOf(draggedSetName);
                        order.splice(draggedIndex, 1);
                        const targetIndex = order.indexOf(targetSetName);
                        order.splice(targetIndex, 0, draggedSetName);
                        await VAL_PINNED_SETS_ORDER.set(order);
                        await loadPinnedSets();
                    }
                });

                pinnedTabsList.appendChild(setContainer);
                const setItemContainer = el.create('div', { parent: pinnedItemsContainer, dataset: { setItems: setname }, classList: ['set-items'], style: { display: 'none', flexWrap: 'wrap' } });
                sets[setname].elements.forEach((elementName) => { const element = v_container.items.find(e => e.text === elementName); if (element) addPinnedElementInternal(element, setname); });
            }

            selectedBefore.forEach(setName => {
                const tab = pinnedTabsList.querySelector(`[data-pinned-set="${CSS.escape(setName)}"]`);
                if (tab) {
                    tab.classList.add('selected-set');
                    tab.style.outline = '2px solid #55aaff';
                    tab.style.zIndex = ++zIndexCounter + setsOrder.length;
                }
            });

            updatePinDisplay();
        }

        function createSettingsDialog() {
            const createCheckbox = (id, labelText, settingKey) => {
                const checkbox = el.create('input', { attrs: { type: 'checkbox', id }, checked: settings[settingKey] });
                checkbox.addEventListener('change', async () => { settings[settingKey] = checkbox.checked; await VAL_SETTINGS.set(settings); await updatePinDisplay(); });
                const label = el.create('label', { attrs: { for: id }, children: [checkbox, document.createTextNode(` ${labelText}`)] });
                return el.create('div', { children: [label], style: { textAlign: 'left', margin: '8px 0', cursor: 'pointer', display: 'flex', alignItems: 'center' } });
            };

            const resetButton = el.create('button', {
                textContent: 'Reset All Tabs',
                style: { marginTop: '15px', color: '#fff', backgroundColor: '#d9534f', border: '1px solid #d43f3a', borderRadius: '4px', padding: '5px 10px', display: 'block', margin: '20px auto 0 auto' }
            });
            resetButton.addEventListener('click', async () => {
                if(confirm("Are you sure you want to delete ALL tabs and their contents? This action cannot be undone.")){
                    await VAL_PINNED_SETS.set({});
                    await VAL_PINNED_SETS_ORDER.set([]);
                    await loadPinnedSets();
                    dialog.close();
                }
            });

            const dialog = el.create('dialog', { parent: document.body, style: { ...getDialogBaseStyle(), minWidth: '280px' }, events: { close: e => e.target.remove() }, children: [ el.create('h3', { textContent: 'Settings', style: { marginTop: '0', textAlign: 'center' } }), createCheckbox('setting-expand-container', 'Expand container to show all items', 'expandContainer'), createCheckbox('setting-combine-colors', 'Combine selected tabs into one box', 'combineColors'), resetButton, el.create('button', { textContent: 'Close', style: { marginTop: '15px', display: 'block', margin: '0 auto' }, events: { click: () => dialog.close() } }) ] });
            return dialog;
        }

        loadPinnedSets();
    }

    function waitForReady() {
        let attempts = 0;
        const interval = setInterval(() => {
            const v_container = document.querySelector(".container")?.__vue__;
            const v_sidebar = document.querySelector("#sidebar")?.__vue__;
            const ICHelper = unsafeWindow.ICHelper;
            if (v_container && v_sidebar && ICHelper) { clearInterval(interval); initialize(ICHelper, v_container, v_sidebar); }
            else if (++attempts > 100) { clearInterval(interval); console.warn('More Pins & Colored Tabs failed to load: Helper script not found.'); }
        }, 100);
    }

    waitForReady();
})();