Infinite Craft QOL

Create tabs to group items and color them for organization.

目前為 2025-01-06 提交的版本,檢視 最新版本

// ==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();
})();