CS - Sort Pets by Dimensions + Ctrl Multi-Select + Drag Select

Sort pets by dimensions + Ctrl click range selection (accumulative) + drag-to-select + update log + helper text

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         CS - Sort Pets by Dimensions + Ctrl Multi-Select + Drag Select
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Sort pets by dimensions + Ctrl click range selection (accumulative) + drag-to-select + update log + helper text
// @author       lissajo
// @match        https://www.chickensmoothie.com/accounts/viewgroup.php*
// @grant        none
// @license      MIT


// ==/UserScript==

(function () {
    "use strict";

    const ENABLE_KEY = "cs_sort_pets_enabled";
    const MODE_KEY = "cs_sort_pets_mode";
    const PANEL_KEY_X = "cs_sort_pets_panel_x";
    const PANEL_KEY_Y = "cs_sort_pets_panel_y";

    let sortingEnabled = localStorage.getItem(ENABLE_KEY) === "true";
    let mode = localStorage.getItem(MODE_KEY) || "area";

    const style = document.createElement("style");
    style.textContent = `
        .cs-sort-box {
            position: fixed;
            top: 20px;
            left: 20px;
            z-index: 999999;
            background: rgba(0,0,0,0.65);
            padding: 12px;
            border-radius: 10px;
            width: 190px;
            color: white;
            font-family: Arial, sans-serif;
            cursor: move;
            user-select: none;
        }
        .cs-sort-box h3 { margin: 0 0 8px 0; font-size: 14px; text-align: center; pointer-events: none; }
        .cs-sort-box button { width: 100%; padding: 6px; margin: 4px 0; font-size: 13px; border: none; border-radius: 6px; cursor: pointer; }
        .cs-onoff { background: #4caf50; color: white; }
        .cs-off { background: #b33a3a; color: white; }
        .cs-mode-btn { background: #666; color: white; }
        .cs-mode-btn.active { background: #2196f3; }
        .cs-helper { font-size: 11px; opacity: 0.85; text-align: center; margin-top: 4px; line-height: 1.2em; }
        .cs-update-link { text-align: center; margin-top: 8px; }
        .cs-update-link a { color: #ffcc00; font-size: 12px; text-decoration: none; }
        .drag-select-box { position: absolute; border: 2px dashed #00aaff; background: rgba(0,170,255,0.15); pointer-events: none; z-index: 9999999; }
    `;
    document.head.appendChild(style);

    function createTogglePanel() {
        const panel = document.createElement("div");
        panel.className = "cs-sort-box";

        const lastX = localStorage.getItem(PANEL_KEY_X);
        const lastY = localStorage.getItem(PANEL_KEY_Y);
        if (lastX && lastY) { panel.style.left = lastX + "px"; panel.style.top = lastY + "px"; }

        panel.innerHTML = `
            <h3>Sort Pets</h3>
            <button id="cs-enable-btn" class="${sortingEnabled ? "cs-onoff" : "cs-off"}">
                ${sortingEnabled ? "Enabled" : "Disabled"}
            </button>
            <button class="cs-mode-btn ${mode === "area" ? "active" : ""}" data-mode="area">Area</button>
            <button class="cs-mode-btn ${mode === "width" ? "active" : ""}" data-mode="width">Width</button>
            <button class="cs-mode-btn ${mode === "height" ? "active" : ""}" data-mode="height">Height</button>
            <div class="cs-helper">Ctrl + Click<br>to select multiple pets in a row<br>(will not select last pet clicked)<hr>Click & drag to<br>select multiple pets</div>
            <div class="cs-update-link">
                <a href="https://www.chickensmoothie.com/Forum/viewtopic.php?f=20&t=5091368" target="_blank">update log</a>
            </div>
        `;
        document.body.appendChild(panel);

        let offsetX = 0, offsetY = 0, dragging = false;
        panel.addEventListener("mousedown", e => {
            if (e.target.tagName === "BUTTON" || e.target.tagName === "A") return;
            dragging = true;
            offsetX = e.clientX - panel.offsetLeft;
            offsetY = e.clientY - panel.offsetTop;
        });
        document.addEventListener("mousemove", e => {
            if (!dragging) return;
            panel.style.left = (e.clientX - offsetX) + "px";
            panel.style.top = (e.clientY - offsetY) + "px";
        });
        document.addEventListener("mouseup", () => {
            if (!dragging) return;
            dragging = false;
            localStorage.setItem(PANEL_KEY_X, parseInt(panel.style.left));
            localStorage.setItem(PANEL_KEY_Y, parseInt(panel.style.top));
        });

        document.querySelector("#cs-enable-btn").addEventListener("click", () => {
            sortingEnabled = !sortingEnabled;
            localStorage.setItem(ENABLE_KEY, sortingEnabled);
            const btn = document.querySelector("#cs-enable-btn");
            btn.textContent = sortingEnabled ? "Enabled" : "Disabled";
            btn.className = sortingEnabled ? "cs-onoff" : "cs-off";
            if (sortingEnabled) sortPets();
        });

        document.querySelectorAll(".cs-mode-btn").forEach(btn => {
            btn.addEventListener("click", () => {
                mode = btn.dataset.mode;
                localStorage.setItem(MODE_KEY, mode);
                document.querySelectorAll(".cs-mode-btn").forEach(b => b.classList.remove("active"));
                btn.classList.add("active");
                if (sortingEnabled) sortPets();
            });
        });
    }

    async function sortPets() {
        if (!sortingEnabled) return;
        await new Promise(res => setTimeout(res, 500));
        const pets = Array.from(document.querySelectorAll("dl.pet"));
        if (!pets.length) return;
        const container = pets[0].parentElement;
        const petData = [];
        for (let dl of pets) {
            const img = dl.querySelector("img");
            if (!img) continue;
            if (!img.complete) await new Promise(res => { img.onload = img.onerror = res; });
            const w = img.naturalWidth || img.width;
            const h = img.naturalHeight || img.height;
            petData.push({ dl, width: w, height: h, area: w * h });
        }
        petData.sort((a, b) => a[mode] - b[mode]);
        for (const p of petData) container.appendChild(p.dl);
    }

    function getPetContainer() {
        const first = document.querySelector("dl.pet");
        return first ? first.parentElement : null;
    }
    function getVisibleCheckboxes() {
        const container = getPetContainer();
        if (!container) return [];
        return Array.from(container.querySelectorAll("dl.pet input[type='checkbox']"));
    }

    /* ================= CTRL MULTI-SELECT ACCUMULATIVE ================= */
    (function enableCtrlSelect() {
        let lastAnchor = null;

        document.addEventListener("click", function(e) {
            if (!e.target.matches('dl.pet input[type="checkbox"]')) return;
            const checkboxes = getVisibleCheckboxes();
            const clicked = e.target;
            const idxClicked = checkboxes.indexOf(clicked);

            if (e.ctrlKey && lastAnchor) {
                e.preventDefault(); // stop default toggle
                e.stopPropagation();
                const idxAnchor = checkboxes.indexOf(lastAnchor);
                if (idxAnchor >= 0 && idxClicked >= 0) {
                    const start = Math.min(idxAnchor, idxClicked);
                    const end = Math.max(idxAnchor, idxClicked);
                    for (let i = start; i <= end; i++) checkboxes[i].checked = true; // include last checkbox
                }
            }
            lastAnchor = clicked; // always update last anchor
        }, true);
    })();

    /* ================= DRAG SELECT ================= */
    (function enableDragSelect() {
        let selecting = false;
        let startX = 0, startY = 0;
        let box = null;

        document.addEventListener("mousedown", function(e) {
            if (e.target.closest(".cs-sort-box")) return;
            if (e.target.closest("dl.pet")) return;
            selecting = true;
            startX = e.pageX;
            startY = e.pageY;
            box = document.createElement("div");
            box.className = "drag-select-box";
            document.body.appendChild(box);
        });

        document.addEventListener("mousemove", function(e) {
            if (!selecting || !box) return;
            const x = Math.min(e.pageX, startX);
            const y = Math.min(e.pageY, startY);
            const w = Math.abs(e.pageX - startX);
            const h = Math.abs(e.pageY - startY);
            box.style.left = x + "px";
            box.style.top = y + "px";
            box.style.width = w + "px";
            box.style.height = h + "px";
        });

        document.addEventListener("mouseup", function() {
            if (!selecting) return;
            selecting = false;
            const rect = box.getBoundingClientRect();
            const checkboxes = getVisibleCheckboxes();
            for (let cb of checkboxes) {
                const dl = cb.closest("dl.pet");
                if (!dl) continue;
                const dlRect = dl.getBoundingClientRect();
                const overlap = !(dlRect.right < rect.left || dlRect.left > rect.right || dlRect.bottom < rect.top || dlRect.top > rect.bottom);
                if (overlap) cb.checked = true;
            }
            box.remove();
            box = null;
        });
    })();

    function init() {
        createTogglePanel();
        if (sortingEnabled) sortPets();
    }
    window.addEventListener("load", init);

})();