PteroSort

Pterodactyl server sorter

当前为 2025-02-28 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         PteroSort
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  Pterodactyl server sorter
// @homepage     https://github.com/Ricman-MC/PteroSort
// @author       Ricman
// @license      Apache 2.0
// @match        https://panel.your-server.eu/
// @match        https://panel.your-second-server.com/
// @grant        none
// ==/UserScript==

// IMPORTANT
// Set your own domain for the pterodactyl panel in the @match example is provided you can add as many as you need
// Script supports only vanilla pterodactyl panel v.1.11.10 (its possible it will work on diferent versions not tested)
// IMPORTANT


// this script has some hardcoded parts i expect it will break when update comes iam fixing this in later updates
// In development second script that has also category sorting, basically create category and assaign it to server

(function () {
    'use strict';

    const STORAGE_KEY_YOURS = 'ptero_server_order_yours';
    const STORAGE_KEY_OTHERS = 'ptero_server_order_others';
    const containerSelector = 'section > div';
    const serverSelector = '.DashboardContainer___StyledServerRow-sc-1topkxf-2';
    const toggleSelector = 'input[name="show_all_servers"]';
    const buttonContainerSelector = '.DashboardContainer___StyledDiv-sc-1topkxf-0';

    let autoSaveEnabled = localStorage.getItem('autoSaveEnabled') !== 'false';
    let dragLockEnabled = localStorage.getItem('dragLockEnabled') === 'true';

    function getStorageKey() {
        return document.querySelector(toggleSelector)?.checked ? STORAGE_KEY_OTHERS : STORAGE_KEY_YOURS;
    }

    function saveOrder() {
        if (!autoSaveEnabled) return;

        const serverElements = document.querySelectorAll(serverSelector);
        const order = Array.from(serverElements).map(el => el.href.split('/').pop());
        localStorage.setItem(getStorageKey(), JSON.stringify(order));
    }

    function loadOrder() {
        const savedOrder = JSON.parse(localStorage.getItem(getStorageKey()) || '[]');
        if (!savedOrder.length) return;

        const container = document.querySelector(containerSelector);
        const servers = Array.from(document.querySelectorAll(serverSelector));
        const serverMap = new Map(servers.map(el => [el.href.split('/').pop(), el]));

        savedOrder.forEach(id => {
            if (serverMap.has(id)) {
                container.appendChild(serverMap.get(id));
            }
        });
    }

    function enableDragAndDrop() {
        const container = document.querySelector(containerSelector);
        if (!container) return;
    
        let dragged = null;
    
        // reset all drag listeners
        document.querySelectorAll(serverSelector).forEach(el => {
            el.draggable = !dragLockEnabled;
    
            el.removeEventListener('dragstart', handleDragStart);
            el.removeEventListener('dragover', handleDragOver);
            el.removeEventListener('drop', handleDrop);
    
            if (!dragLockEnabled) {
                el.addEventListener('dragstart', handleDragStart);
                el.addEventListener('dragover', handleDragOver);
                el.addEventListener('drop', handleDrop);
            }
        });
    
        function handleDragStart(e) {
            if (dragLockEnabled) return;
            dragged = e.target;
            e.dataTransfer.effectAllowed = 'move';
        }
    
        function handleDragOver(e) {
            if (dragLockEnabled) return;
            e.preventDefault();
            e.dataTransfer.dropEffect = 'move';
    
            const target = e.target.closest(serverSelector);
            if (!target || target === dragged) return;
    
            const bounding = target.getBoundingClientRect();
            const offset = e.clientY - bounding.top;
    
            if (offset > bounding.height * 0.1) {
                target.parentNode.insertBefore(dragged, target);
            } else if (offset < bounding.height * 0.9) {
                target.parentNode.insertBefore(dragged, target.nextSibling);
            }


        }
    
        function handleDrop(e) {
            if (dragLockEnabled) return;
            e.preventDefault();
            saveOrder();
            fixSpacing();
        }
    }




    function fixSpacing() {
        document.querySelectorAll(serverSelector).forEach(el => {
            el.style.marginTop = '8px';
        });
    }

    function createButtons() {
        const container = document.querySelector(buttonContainerSelector);
        const toggleSwitch = document.querySelector(toggleSelector);

        if (!container || !toggleSwitch) return; // make sure elements exist

        // prevent duplicate buttons
        if (document.getElementById('lockDragButton')) return;

        const buttonWrapper = document.createElement('div');
        buttonWrapper.style.display = 'flex';
        buttonWrapper.style.gap = '10px';
        buttonWrapper.style.paddingLeft = '15px';
        buttonWrapper.style.paddingRight = '15px';
        buttonWrapper.style.float = 'right';

        const lockButton = document.createElement('button');
        lockButton.id = 'lockDragButton';
        lockButton.title = 'Toggle drag lock';
        lockButton.style.padding = '5px 10px';
        lockButton.style.cursor = 'pointer';
        lockButton.innerText = dragLockEnabled ? '🔒' : '🔓';
        lockButton.addEventListener('click', () => {
            dragLockEnabled = !dragLockEnabled;
            localStorage.setItem('dragLockEnabled', dragLockEnabled);
            lockButton.innerText = dragLockEnabled ? '🔒' : '🔓';

            enableDragAndDrop();


        });

        const autoSaveButton = document.createElement('button');
        autoSaveButton.id = 'autoSaveButton';
        autoSaveButton.title = 'Toggle auto-saving';
        autoSaveButton.style.padding = '5px 10px';
        autoSaveButton.style.cursor = 'pointer';
        autoSaveButton.innerText = `Auto-Save: ${autoSaveEnabled ? 'ON' : 'OFF'}`;
        autoSaveButton.addEventListener('click', () => {
            autoSaveEnabled = !autoSaveEnabled;
            localStorage.setItem('autoSaveEnabled', autoSaveEnabled);
            autoSaveButton.innerText = `Auto-Save: ${autoSaveEnabled ? 'ON' : 'OFF'}`;
        });

        const clearButton = document.createElement('button');
        clearButton.id = 'clearStorageButton';
        clearButton.title = 'Delete all saved server orders';
        clearButton.style.marginRight = '10px';
        clearButton.style.padding = '5px 10px';
        clearButton.style.cursor = 'pointer';
        clearButton.style.color = 'red';
        clearButton.innerText = '🗑️';

        // click event - show confirmation overlay
        clearButton.addEventListener('click', () => {
            const overlay = document.createElement('div');
            overlay.style.position = 'fixed';
            overlay.style.top = '0';
            overlay.style.left = '0';
            overlay.style.width = '100vw';
            overlay.style.height = '100vh';
            overlay.style.background = 'rgba(0, 0, 0, 0.5)';
            overlay.style.display = 'flex';
            overlay.style.alignItems = 'center';
            overlay.style.justifyContent = 'center';
            overlay.style.zIndex = '10000';

            const confirmation = document.createElement('div');
            confirmation.style.padding = '20px';
            confirmation.style.background = 'white';
            confirmation.style.boxShadow = '0px 4px 6px rgba(0,0,0,0.1)';
            confirmation.style.borderRadius = '10px';
            confirmation.style.textAlign = 'center';

            confirmation.innerHTML = `
                <p style="margin-bottom: 15px; color: black;">Are you sure you want to delete all saved server orders?</p>
                <button id="confirmDelete" style="margin-right: 10px; padding: 5px 10px; border-radius: 5px; background: red; color: white; border: none; cursor: pointer;">Yes</button>
                <button id="cancelDelete" style="padding: 5px 10px; border-radius: 5px; background: gray; color: white; border: none; cursor: pointer;">No</button>
            `;

            overlay.appendChild(confirmation);
            document.body.appendChild(overlay);

            document.getElementById('confirmDelete').addEventListener('click', () => {
                localStorage.removeItem(STORAGE_KEY_YOURS);
                localStorage.removeItem(STORAGE_KEY_OTHERS);
                overlay.remove();
                location.reload(); // refresh the page
            });

            document.getElementById('cancelDelete').addEventListener('click', () => {
                overlay.remove();
            });

            // close overlay when clicking outside the confirmation box
            overlay.addEventListener('click', () => {
                overlay.remove();
            });

            // prevent clicking inside the box from closing the overlay
            confirmation.addEventListener('click', (e) => {
                e.stopPropagation();
            });
        });

        container.appendChild(clearButton);

        buttonWrapper.appendChild(lockButton);
        buttonWrapper.appendChild(autoSaveButton);

        const toggleContainer = toggleSwitch.closest('.Switch___StyledDiv-sc-1nxt82m-2');

        if (toggleContainer && toggleContainer.contains(toggleSwitch)) {
            toggleContainer.appendChild(buttonWrapper);
        } else {
            console.warn('Could not find valid toggle container, appending buttons at the end.');
            container.appendChild(buttonWrapper);
        }
    }




    function init() {
        loadOrder();
        enableDragAndDrop();
        fixSpacing();
        createButtons();
    }

    function observePageChanges() {
        let lastUrl = location.href;
        let lastPath = location.pathname;
    
        const observer = new MutationObserver(() => {
            if (location.href !== lastUrl) {
                lastUrl = location.href;
            }
    
            if (location.pathname !== lastPath) {
                lastPath = location.pathname;
    
                waitForElement(serverSelector, () => {
                    init()
                });
            }
        });
    
        observer.observe(document.body, { childList: true, subtree: true });
    
        // interval-based fallback
        setInterval(() => {
            if (location.pathname !== lastPath) {
                lastPath = location.pathname;
                waitForElement(serverSelector, () => {
                    // init() sometimes does not work
                    loadOrder();
                    enableDragAndDrop();
                    fixSpacing();
                    createButtons();
                });
            }
        }, 500);
    }




    function observeViewSwitch() {
        const toggleSwitch = document.querySelector(toggleSelector);
        if (toggleSwitch) {
            toggleSwitch.addEventListener('change', () => {
                setTimeout(() => { // delay to avoid using old elements
                    waitForElement(serverSelector, () => {
                        loadOrder();
                        enableDragAndDrop();
                        fixSpacing();
                    });
                }, 200);
            });
        }
    }



    function waitForElement(selector, callback) {
        const observer = new MutationObserver(() => {
            if (document.querySelector(selector)) {
                observer.disconnect();
                callback();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
    }

    waitForElement(serverSelector, () => {
        init();
        observePageChanges();
        observeViewSwitch();
    });
})();