Infinite Craft QOL

Create tabs to group items and color them for organization.

当前为 2025-01-06 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

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

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Infinite Craft QOL
// @version      1.0.0
// @namespace    https://github.com/ChessScholar
// @description  Create tabs to group items and color them for organization.
// @author       ChessScholar
// @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.
// ==/UserScript==

(function() {
    'use strict';

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

    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_COMBOS = new GMValue('infinitecraft_observed_combos', {});
    const VAL_PINNED_SETS = new GMValue('infinitecraft_pinned_sets', {});

    const icMain = () => unsafeWindow?.$nuxt?._route?.matched?.[0]?.instances?.default;

    async function saveCombo(lhs, rhs, result) {
        console.log(`Crafted ${lhs} + ${rhs} -> ${result}`);
        const data = await VAL_COMBOS.get();
        if (!(result in data)) data[result] = [];
        const sortedLhsRhs = [lhs, rhs].sort();
        if (!data[result].some(pair => pair[0] === sortedLhsRhs[0] && pair[1] === sortedLhsRhs[1])) {
            data[result].push(sortedLhsRhs);
            await VAL_COMBOS.set(data);
        }
    }

    async function getCombos() {
        const data = await VAL_COMBOS.get();
        return data;
    }

    function main() {
        const _getCraftResponse = icMain().getCraftResponse;
        const _selectElement = icMain().selectElement;
        const _selectInstance = icMain().selectInstance;

        icMain().getCraftResponse = async function(lhs, rhs) {
            const resp = await _getCraftResponse.apply(this, arguments);
            await saveCombo(lhs.text, rhs.text, resp.result);
            return resp;
        };

        document.documentElement.addEventListener('mousedown', e => {
            if (e.buttons === 1) {
                if (e.altKey && !e.shiftKey) {
                    e.preventDefault();
                    e.stopPropagation();
                    const elements = icMain()._data.elements;
                    const randomElement = elements[Math.floor(Math.random() * elements.length)];
                    _selectElement(e, randomElement);
                } else if (!e.altKey && e.shiftKey) {
                    e.preventDefault();
                    e.stopPropagation();
                    const instances = icMain()._data.instances;
                    const lastInstance = instances[instances.length - 1];
                    const lastInstanceElement = icMain()._data.elements.find(e => e.text === lastInstance.text);
                    _selectElement(e, lastInstanceElement);
                }
            }
        }, { capture: false });

        const cssScopeDatasetKey = Object.keys(icMain().$el.dataset)[0];

        function mkElementItem(element) {
            return el.create('div', {
                classList: ['item'],
                dataset: { [cssScopeDatasetKey]: '' },
                children: [
                    el.create('span', {
                        classList: ['item-emoji'],
                        dataset: { [cssScopeDatasetKey]: '' },
                        textContent: element.emoji,
                        style: { pointerEvents: 'none' },
                    }),
                    document.createTextNode(` ${element.text} `),
                ],
            });
        }

        async function nonBlockingChunked(chunkSize, genFn, timeout = 0) {
            return new Promise((resolve) => {
                const gen = genFn();
                (function doChunk() {
                    for (let i = 0; i < chunkSize; i++) {
                        const { done } = gen.next();
                        if (done) {
                            resolve();
                            return;
                        }
                    }
                    setTimeout(doChunk, timeout);
                })();
            });
        }

        const recipesListContainer = el.create('div');

        function clearRecipesDialog() {
            recipesListContainer.replaceChildren();
        }

        const recipesDialog = el.create('dialog', {
            parent: document.querySelector('.container'),
            children: [
                el.create('button', {
                    textContent: 'x',
                    events: { click: () => recipesDialog.close() },
                }),
                recipesListContainer,
            ],
            style: {
                background: 'var(--sidebar-bg)',
                margin: 'auto',
                color: 'unset',
            },
            events: { close: () => clearRecipesDialog() },
        });

        async function openRecipesDialog(childGenerator) {
            clearRecipesDialog();
            const container = el.create('div', { parent: recipesListContainer });
            recipesDialog.showModal();
            await nonBlockingChunked(512, function*() {
                for (const child of childGenerator()) {
                    container.appendChild(child);
                    yield;
                }
            });
        }

        function addControlsButton(label, handler) {
            el.create('div', {
                parent: document.querySelector('.side-controls'),
                textContent: label,
                style: {
                    cursor: 'pointer',
                    color: '#040404',
                },
                events: { click: handler },
            });
        }

        addControlsButton('Recipes', async () => {
            const byName = {};
            const byNameLower = {};
            icMain()._data.elements.forEach(element => {
                byName[element.text] = element;
                byNameLower[element.text.toLowerCase()] = element;
            });

            function getByName(name) {
                return byName[name] || byNameLower[name.toLowerCase()] || { emoji: "❌", text: `[Error: Element '${name}']` };
            }

            const combos = await getCombos();

            function listItemClick(evt) {
                const elementName = evt.target.dataset.comboviewerElement;
                document.querySelector(`[data-comboviewer-section="${CSS.escape(elementName)}"]`).scrollIntoView({ block: 'nearest' });
            }

            function mkLinkedElementItem(element) {
                return el.setup(mkElementItem(element), {
                    events: { click: listItemClick },
                    dataset: { comboviewerElement: element.text },
                });
            }

            openRecipesDialog(function*() {
                for (const comboResult in combos) {
                    if (comboResult === 'Nothing') continue;
                    yield el.create('div', { dataset: { comboviewerSection: comboResult } });
                    for (const [lhs, rhs] of combos[comboResult]) {
                        yield el.create('div', {
                            children: [
                                mkLinkedElementItem(getByName(comboResult)),
                                document.createTextNode(' = '),
                                mkLinkedElementItem(getByName(lhs)),
                                document.createTextNode(' + '),
                                mkLinkedElementItem(getByName(rhs)),
                            ],
                        });
                    }
                }
            });
        });

        addControlsButton('Discoveries', () => {
            openRecipesDialog(function*() {
                for (const element of icMain()._data.elements) {
                    if (element.discovered) {
                        yield mkElementItem(element);
                    }
                }
            });
        });

        const pinnedTabsContainer = el.create('div', {
            style: {
                display: 'flex',
                flexDirection: 'row',
                flexWrap: 'wrap',
                alignItems: 'flex-start',
                position: 'sticky',
                top: '0',
                background: 'var(--sidebar-bg)',
                width: 'calc(100% - 10px)',
                overflowX: 'auto',
                overflowY: 'hidden',
                borderBottom: '1px solid var(--border-color)',
                zIndex: '10',
                padding: '5px'
            },
        });

        const pinnedTabsList = el.create('div', {
            style: {
                display: 'flex',
                flexDirection: 'row',
                alignItems: 'flex-start',
                gap: '5px'
            }
        });

        pinnedTabsContainer.appendChild(pinnedTabsList);

        const pinnedItemsContainer = el.create('div', {
            style: {
                borderBottom: '1px solid var(--border-color)',
                zIndex: '9',
                padding: '5px',
                background: 'var(--sidebar-bg)',
            },
        });

        const sidebar = document.querySelector('.container > .sidebar');
        sidebar.insertBefore(pinnedItemsContainer, sidebar.firstChild);
        sidebar.insertBefore(pinnedTabsContainer, sidebar.firstChild);

        const newSetPrompt = el.create('dialog', {
            parent: document.body,
            style: {
                background: 'var(--sidebar-bg)',
                color: 'unset',
                position: 'absolute',
                top: '30px',
                left: '50%',
                transform: 'translateX(-50%)',
                zIndex: '20',
                padding: '10px',
                textAlign: 'center',
                border: '1px solid var(--border-color)',
                borderRadius: '5px'
            },
            children: [
                el.create('div', { textContent: 'Enter new set name:' }),
                el.create('input', { attrs: { type: 'text' }, dataset: { newSetNameInput: '' } }),
                el.create('button', {
                    textContent: 'Create',
                    style: { marginTop: '5px' },
                    events: {
                        click: async () => {
                            const setname = newSetPrompt.querySelector('[data-new-set-name-input]').value;
                            if (setname) {
                                const sets = await VAL_PINNED_SETS.get();
                                if (sets[setname]) {
                                    alert("A set with this name already exists.");
                                    return;
                                }
                                sets[setname] = { elements: [], color: 'var(--sidebar-bg)' };
                                await VAL_PINNED_SETS.set(sets);
                                await loadPinnedSets();
                                pinnedTabsList.querySelector(`[data-pinned-set="${CSS.escape(setname)}"]`).click();
                            }
                            newSetPrompt.close();
                        }
                    }
                })
            ]
        });

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

            if (!elementExists) {
                const elementElement = mkElementItem(element);

                el.setup(elementElement, {
                    parent: setItemContainer,
                    style: { zIndex: '10', display: 'inline-block' },
                    events: {
                        mousedown: async (e) => {
                            if (e.buttons === 4 || (e.buttons === 1 && e.altKey && !e.shiftKey)) {
                                setItemContainer.removeChild(elementElement);
                                const sets = await VAL_PINNED_SETS.get();
                                sets[setname].elements = sets[setname].elements.filter(e => e !== element.text);
                                await VAL_PINNED_SETS.set(sets);
                                return;
                            }
                            icMain().selectElement(e, element);
                        },
                    },
                });
            }
        }

        async function addPinnedElement(element, setnames) {
            const sets = await VAL_PINNED_SETS.get();
            setnames.forEach(setname => {
                if (sets[setname] && !sets[setname].elements.includes(element.text)) {
                    addPinnedElementInternal(element, setname);
                    sets[setname].elements.push(element.text);
                }
            });
            await VAL_PINNED_SETS.set(sets);
        }

        icMain().selectElement = function(mouseEvent, element) {
            if (mouseEvent.buttons === 4 || (mouseEvent.buttons === 1 && mouseEvent.altKey && !mouseEvent.shiftKey)) {
                mouseEvent.preventDefault();
                mouseEvent.stopPropagation();
                const selectedSetNames = Array.from(pinnedTabsContainer.querySelectorAll('.selected-set'))
                    .map(set => set.dataset.pinnedSet);
                if (selectedSetNames.length > 0) {
                    addPinnedElement(element, selectedSetNames);
                } else {
                    alert("No pinned set selected.");
                }

                return;
            }
            return _selectElement.apply(this, arguments);
        };

        icMain().selectInstance = function(mouseEvent, instance) {
            if (mouseEvent.buttons === 4) {
                mouseEvent.preventDefault();
                mouseEvent.stopPropagation();
                const selectedSetNames = Array.from(pinnedTabsContainer.querySelectorAll('.selected-set'))
                    .map(set => set.dataset.pinnedSet);
                if (selectedSetNames.length > 0) {
                    addPinnedElement({ text: instance.text, emoji: instance.emoji }, selectedSetNames);
                } else {
                    alert("No pinned set selected.");
                }
                return;
            }
            return _selectInstance.apply(this, arguments);
        };

        function createColorWheel(setname, setContainer) {
            const colorWheel = el.create('dialog', {
                attrs: { width: '150', height: '150' },
                style: {
                    position: 'absolute',
                    top: '50%',
                    left: '50%',
                    transform: 'translate(-50%, -50%)',
                    display: 'none',
                    zIndex: '100',
                    border: '1px solid var(--border-color)',
                    borderRadius: '50%',
                    cursor: 'crosshair',
                    padding: '0',
                    overflow: 'hidden'
                },
                parent: document.body
            });

            const colorWheelCanvas = el.create('canvas', {
                attrs: { width: '150', height: '150' },
                parent: colorWheel
            })

            const colorWheelCtx = colorWheelCanvas.getContext('2d');
            const radius = 75;
            const image = colorWheelCtx.createImageData(2 * radius, 2 * radius);
            const data = image.data;

            for (let x = -radius; x < radius; x++) {
                for (let y = -radius; y < radius; y++) {
                    const distance = Math.sqrt(x * x + y * y);
                    if (distance > radius) continue;

                    const angle = Math.atan2(y, x) * 180 / Math.PI;
                    const hue = angle < 0 ? angle + 360 : angle;
                    const saturation = distance / radius;
                    const value = 1;

                    const [red, green, blue] = hsvToRgb(hue, saturation, value);

                    const index = (x + radius + (y + radius) * (radius * 2)) * 4;
                    data[index] = red;
                    data[index + 1] = green;
                    data[index + 2] = blue;
                    data[index + 3] = 255;
                }
            }

            colorWheelCtx.putImageData(image, 0, 0);

            colorWheel.addEventListener('mousemove', async (e) => {
                if (colorWheel.style.display === 'none') return;
                const rect = colorWheelCanvas.getBoundingClientRect();
                const x = e.clientX - rect.left - radius;
                const y = e.clientY - rect.top - radius;
                const distance = Math.sqrt(x * x + y * y);

                if (distance <= radius) {
                    const angle = Math.atan2(y, x) * 180 / Math.PI;
                    const hue = angle < 0 ? angle + 360 : angle;
                    const saturation = distance / radius;
                    const value = 1;

                    const [red, green, blue] = hsvToRgb(hue, saturation, value);
                    const color = `rgb(${red}, ${green}, ${blue})`;

                    setContainer.style.background = color;
                    const setItemContainer = pinnedItemsContainer.querySelector(`[data-set-items="${setname}"]`);
                    if (setItemContainer) {
                        setItemContainer.style.background = color;
                    }
                }
            });

            colorWheel.addEventListener('click', async (e) => {
                const rect = colorWheelCanvas.getBoundingClientRect();
                const x = e.clientX - rect.left - radius;
                const y = e.clientY - rect.top - radius;
                const distance = Math.sqrt(x * x + y * y);

                if (distance <= radius) {
                    const angle = Math.atan2(y, x) * 180 / Math.PI;
                    const hue = angle < 0 ? angle + 360 : angle;
                    const saturation = distance / radius;
                    const value = 1;

                    const [red, green, blue] = hsvToRgb(hue, saturation, value);
                    const color = `rgb(${red}, ${green}, ${blue})`;

                    const sets = await VAL_PINNED_SETS.get();
                    sets[setname].color = color;
                    setContainer.style.background = color;
                    await VAL_PINNED_SETS.set(sets);
                    colorWheel.close();
                    const setItemContainer = pinnedItemsContainer.querySelector(`[data-set-items="${setname}"]`);
                    if (setItemContainer) {
                        setItemContainer.style.background = color;
                    }
                }
            });

            colorWheel.addEventListener('close', () => {
                colorWheel.style.display = 'none';
            });

            return colorWheel;
        }

        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 setDragEvents(element) {
            element.setAttribute('draggable', 'true');
            let draggedSetname = null;

            element.addEventListener('dragstart', (event) => {
                draggedSetname = event.target.dataset.pinnedSet;
                event.dataTransfer.setData('text', draggedSetname);
                event.dataTransfer.effectAllowed = 'move';
            });

            element.addEventListener('dragend', (event) => {
                draggedSetname = null;
            })
        }

        async function loadPinnedSets() {
            const sets = await VAL_PINNED_SETS.get();
            pinnedTabsList.replaceChildren();
            pinnedItemsContainer.replaceChildren();

            const buttonsContainer = el.create('div', {
                style: {
                  display: 'flex',
                  flexDirection: 'column',
                  alignItems: 'flex-start',
                  gap: '5px',
                  marginRight: '5px'
                }
              });

            const allButton = el.create('button', {
                textContent: 'All',
                style: {
                    fontWeight: 'bold',
                    cursor: 'pointer',
                    userSelect: 'none',
                    padding: '5px',
                    borderRadius: '5px',
                    border: '1px solid var(--border-color)',
                    zIndex: '11'
                },
                events: {
                    click: () => {
                        const allSetContainers = pinnedTabsList.querySelectorAll('.pinned-set');
                        allSetContainers.forEach(setContainer => {
                            setContainer.style.outline = '2px solid darkgray';
                            setContainer.classList.add('selected-set');
                            const setname = setContainer.dataset.pinnedSet;
                            const itemsContainer = pinnedItemsContainer.querySelector(`[data-set-items="${setname}"]`);
                            if (itemsContainer) {
                                itemsContainer.style.display = 'block';
                                itemsContainer.style.background = sets[setname].color;
                            }
                        });
                    }
                }
            });
            buttonsContainer.appendChild(allButton);

            const noneButton = el.create('button', {
                textContent: 'None',
                style: {
                    fontWeight: 'bold',
                    cursor: 'pointer',
                    userSelect: 'none',
                    padding: '5px',
                    borderRadius: '5px',
                    border: '1px solid var(--border-color)',
                    zIndex: '11'
                },
                events: {
                    click: () => {
                        const allSetContainers = pinnedTabsList.querySelectorAll('.pinned-set');
                        allSetContainers.forEach(setContainer => {
                            setContainer.style.outline = 'none';
                            setContainer.classList.remove('selected-set');
                            const setname = setContainer.dataset.pinnedSet;
                            const itemsContainer = pinnedItemsContainer.querySelector(`[data-set-items="${setname}"]`);
                            if (itemsContainer) {
                                itemsContainer.style.display = 'none';
                            }
                        });
                    }
                }
            });
            buttonsContainer.appendChild(noneButton);
            pinnedTabsList.appendChild(buttonsContainer);

            for (const setname in sets) {
                const setContainer = el.create('div', {
                    dataset: { pinnedSet: setname },
                    classList: ['pinned-set'],
                    style: {
                        background: sets[setname].color || 'var(--sidebar-bg)',
                        padding: '5px',
                        borderRadius: '5px',
                        display: 'flex',
                        flexDirection: 'column',
                        alignItems: 'center',
                        position: 'relative',
                        flexShrink: '0',
                        maxWidth: '200px',
                        overflow: 'hidden',
                        cursor: 'grab'
                    },
                    children: [
                        el.create('div', {
                            textContent: setname,
                            style: {
                                fontWeight: 'bold',
                                userSelect: 'none',
                                zIndex: '11',
                                flex: '1',
                                whiteSpace: 'nowrap',
                                overflow: 'hidden',
                                textOverflow: 'ellipsis',
                                textAlign: 'center'
                            },
                            events: {
                                click: (e) => {
                                    e.stopPropagation();
                                    const wasSelected = setContainer.classList.contains('selected-set');

                                    if (!wasSelected) {
                                        setContainer.style.outline = '2px solid darkgray';
                                        setContainer.classList.add('selected-set');
                                        const itemsContainer = pinnedItemsContainer.querySelector(`[data-set-items="${setname}"]`);
                                        if (itemsContainer) {
                                            itemsContainer.style.display = 'block';
                                            itemsContainer.style.background = sets[setname].color;
                                        }
                                    } else {
                                        setContainer.style.outline = 'none';
                                        setContainer.classList.remove('selected-set');
                                        const itemsContainer = pinnedItemsContainer.querySelector(`[data-set-items="${setname}"]`);
                                        if (itemsContainer) itemsContainer.style.display = 'none';
                                    }
                                }
                            }
                        }),
                        el.create('div', {
                            style: {
                                display: 'flex',
                                flexDirection: 'row',
                                alignItems: 'center',
                                marginTop: '5px'
                            },
                            children: [
                                el.create('span', {
                                    textContent: 'x',
                                    style: {
                                        cursor: 'pointer',
                                        zIndex: '12',
                                        fontSize: '12px',
                                        marginRight: '5px'
                                    },
                                    events: {
                                        click: async (e) => {
                                            e.stopPropagation();
                                            if (confirm(`Are you sure you want to delete the set "${setname}"?`)) {
                                                const sets = await VAL_PINNED_SETS.get();
                                                delete sets[setname];
                                                await VAL_PINNED_SETS.set(sets);
                                                await loadPinnedSets();
                                            }
                                        }
                                    }
                                }),
                                el.create('span', {
                                    textContent: '\u{1F4DD}',
                                    style: {
                                        cursor: 'pointer',
                                        zIndex: '12',
                                        fontSize: '12px',
                                        marginRight: '5px'
                                    },
                                    events: {
                                        click: async (e) => {
                                            e.stopPropagation();
                                            const newSetName = prompt("Enter new set name:", setname);
                                            if (newSetName) {
                                                const sets = await VAL_PINNED_SETS.get();
                                                if (sets[newSetName]) {
                                                    alert("A set with this name already exists.");
                                                    return;
                                                }
                                                sets[newSetName] = sets[setname];
                                                delete sets[setname];
                                                await VAL_PINNED_SETS.set(sets);
                                                await loadPinnedSets();
                                            }
                                        }
                                    }
                                }),
                                el.create('span', {
                                    textContent: '\u{1F3A8}',
                                    style: {
                                        cursor: 'pointer',
                                        zIndex: '12',
                                        fontSize: '12px'
                                    },
                                    events: {
                                        click: async (e) => {
                                            e.stopPropagation();
                                            colorWheel.style.display = 'block';
                                            colorWheel.showModal();
                                        }
                                    }
                                })
                            ]
                        })
                    ]
                });

                const colorWheel = createColorWheel(setname, setContainer);
                pinnedTabsList.appendChild(setContainer);
                setDragEvents(setContainer);

                const setItemContainer = el.create('div', {
                    dataset: { setItems: setname },
                    classList: ['set-items'],
                    style: {
                        display: 'none',
                        flexWrap: 'wrap',
                        background: sets[setname].color || 'var(--sidebar-bg)'
                    }
                });

                pinnedItemsContainer.appendChild(setItemContainer);

                sets[setname].elements.forEach(async (elementName) => {
                    const element = icMain()._data.elements.find(e => e.text === elementName);
                    if (element) {
                        await addPinnedElementInternal(element, setname);
                    }
                });
            }

            const addSetButton = el.create('div', {
                textContent: '+',
                style: {
                    fontWeight: 'bold',
                    cursor: 'pointer',
                    userSelect: 'none',
                    padding: '5px',
                    borderRadius: '5px',
                    border: '1px solid var(--border-color)',
                    marginLeft: '5px',
                    zIndex: '11'
                },
                events: {
                    click: () => {
                        newSetPrompt.querySelector('[data-new-set-name-input]').value = '';
                        newSetPrompt.showModal();
                    }
                }
            });

            pinnedTabsList.appendChild(addSetButton);

            let draggedIndex = -1;
            let targetIndex = -1;

            pinnedTabsList.addEventListener('dragstart', (event) => {
                const draggedSetname = event.target.dataset.pinnedSet;
                draggedIndex = Array.from(pinnedTabsList.children).indexOf(event.target);
            });

            pinnedTabsList.addEventListener('dragover', (event) => {
                event.preventDefault();
                event.dataTransfer.dropEffect = 'move';
                const target = event.target.closest('.pinned-set');
                if (target) {
                    target.style.border = '2px dashed darkgray';
                }
            });

            pinnedTabsList.addEventListener('dragleave', (event) => {
                event.preventDefault();
                const target = event.target.closest('.pinned-set');
                if (target) {
                    target.style.border = '';
                }
            });

            pinnedTabsList.addEventListener('dragenter', (event) => {
                const target = event.target.closest('.pinned-set');
                if (target) {
                  targetIndex = Array.from(pinnedTabsList.children).indexOf(target);
                }
            });

            pinnedTabsList.addEventListener('drop', async (event) => {
                event.preventDefault();
                const draggedSetname = event.dataTransfer.getData('text');
                const target = event.target.closest('.pinned-set');
                const targetSetname = target ? target.dataset.pinnedSet : null;

                if (targetSetname && draggedSetname !== targetSetname) {
                  const sets = await VAL_PINNED_SETS.get();
                  const draggedSet = sets[draggedSetname];

                  delete sets[draggedSetname];

                  const newSets = {};
                  let i = 0;
                  for (const setname in sets) {
                    if (i === targetIndex) {
                      newSets[draggedSetname] = draggedSet;
                    }
                    newSets[setname] = sets[setname];
                    i++;
                  }
                  if (i === targetIndex) {
                    newSets[draggedSetname] = draggedSet;
                  }

                  await VAL_PINNED_SETS.set(newSets);
                  await loadPinnedSets();
                } else if (!targetSetname && draggedIndex !== pinnedTabsList.children.length - 1) {
                  const sets = await VAL_PINNED_SETS.get();
                  const draggedSet = sets[draggedSetname];

                  delete sets[draggedSetname];
                  sets[draggedSetname] = draggedSet;

                  await VAL_PINNED_SETS.set(sets);
                  await loadPinnedSets();
                }

                if (target) {
                  target.style.border = '';
                }

                draggedIndex = -1;
                targetIndex = -1;
            });
        }

        (async () => {
            await loadPinnedSets();
        })();
    }

    let attempt = 0;
    function waitForReady() {
        if (icMain()) main();
        else if (attempt++ < 100) setTimeout(waitForReady, 10);
        else console.warn('Infinite Craft Tweaks failed to load: `icMain` not found after', attempt, 'attempts');
    }
    waitForReady();
})();