Warhammer Kill Team Rules Sorter

Fan-made sorting tool for Kill Team rules page. Adds alphabetical and date sorting functionality with configurable date formats. This is an unofficial tool not affiliated with, endorsed, or sponsored by Games Workshop. Warhammer Kill Team is a trademark of Games Workshop Limited.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name Warhammer Kill Team Rules Sorter
// @namespace https://greasyfork.org/en/users/Kardiff
// @version 1.1.1
// @description Fan-made sorting tool for Kill Team rules page. Adds alphabetical and date sorting functionality with configurable date formats. This is an unofficial tool not affiliated with, endorsed, or sponsored by Games Workshop. Warhammer Kill Team is a trademark of Games Workshop Limited.
// @author Kardiff
// @match https://www.warhammer-community.com/*/downloads/kill-team/
// @license MIT
// @run-at document-idle
// @supportURL https://github.com/Kardiff-Kill-Team/page_sorter/issues
// @homepageURL https://github.com/Kardiff-Kill-Team/page_sorter
// ==/UserScript==

(function() {
    'use strict';

    const styles = `
        .sort-controls {
            position: sticky;
            top: 0;
            background: white;
            padding: 10px;
            z-index: 1000;
            box-shadow: 0 2px 5px rgba(0,0,0,0.1);
            margin-bottom: 20px;
            display: flex;
            justify-content: center;
            gap: 10px;
        }
        .sort-button {
            padding: 8px 15px;
            border: none;
            border-radius: 4px;
            background: #234;
            color: white;
            cursor: pointer;
            font-weight: bold;
            min-width: 150px;
        }
        .sort-button:hover {
            background: #345;
        }
    `;

    // Date format configurations
    const dateFormats = {
        US: {
            label: 'US (MM/DD/YYYY)',
            format: (day, month, year) => `${month}/${day}/${year}`,
            parse: (text) => {
                const [month, day, year] = text.split('/');
                return new Date(`${year}-${month}-${day}`);
            }
        },
        EU: {
            label: 'EU (DD/MM/YYYY)',
            format: (day, month, year) => `${day}/${month}/${year}`,
            parse: (text) => {
                const [day, month, year] = text.split('/');
                return new Date(`${year}-${month}-${day}`);
            }
        },
        ISO: {
            label: 'ISO (YYYY-MM-DD)',
            format: (day, month, year) => `${year}-${month}-${day}`,
            parse: (text) => new Date(text)
        }
    };

    // Add styles
    const styleSheet = document.createElement("style");
    styleSheet.textContent = styles;
    document.head.appendChild(styleSheet);

    function initializeSorter(container) {
        // Create control buttons
        const controlsDiv = document.createElement('div');
        controlsDiv.className = 'sort-controls';
        
        const alphabeticalButton = document.createElement('button');
        alphabeticalButton.textContent = 'Sort A-Z';
        alphabeticalButton.className = 'sort-button';
        
        const dateButton = document.createElement('button');
        dateButton.textContent = 'Sort by Latest Update';
        dateButton.className = 'sort-button';

        const formatButton = document.createElement('button');
        formatButton.className = 'sort-button';
        let currentFormat = 'US'; // Default format

        // Store original dates to prevent conversion errors
        const originalDates = new Map();

        function updateFormatButtonText() {
            formatButton.textContent = `Format: ${dateFormats[currentFormat].label}`;
        }
        updateFormatButtonText();

        controlsDiv.appendChild(alphabeticalButton);
        controlsDiv.appendChild(dateButton);
        controlsDiv.appendChild(formatButton);
        container.parentElement.insertBefore(controlsDiv, container);

        // Store original dates when first encountering them
        container.querySelectorAll('.shared-downloadCard').forEach(card => {
            const dateSpan = card.querySelector('.border-t span.ml-5');
            if (dateSpan && !originalDates.has(card)) {
                const dateParts = dateSpan.textContent.trim().split('/');
                // Store as day, month, year
                originalDates.set(card, {
                    day: dateParts[0],
                    month: dateParts[1],
                    year: dateParts[2]
                });
            }
        });

        function updateAllDates() {
            container.querySelectorAll('.shared-downloadCard').forEach(card => {
                const dateSpan = card.querySelector('.border-t span.ml-5');
                if (dateSpan && originalDates.has(card)) {
                    const { day, month, year } = originalDates.get(card);
                    if (currentFormat === 'ISO') {
                        dateSpan.textContent = dateFormats.ISO.format(day, month, year);
                    } else if (currentFormat === 'US') {
                        dateSpan.textContent = dateFormats.US.format(day, month, year);
                    } else {
                        dateSpan.textContent = dateFormats.EU.format(day, month, year);
                    }
                }
            });
        }

        // Initial date conversion
        updateAllDates();

        function getTeamBoxes() {
            return Array.from(container.querySelectorAll('.shared-downloadCard'));
        }

        function getTeamName(box) {
            const link = box.querySelector('a[download]');
            return link ? link.textContent.trim() : '';
        }

        function getLastUpdated(box) {
            if (originalDates.has(box)) {
                const { day, month, year } = originalDates.get(box);
                // Always use ISO format for date comparison
                return new Date(`${year}-${month}-${day}`);
            }
            return new Date(0);
        }

        function sortBoxes(sortFunction) {
            const boxes = getTeamBoxes();
            if (boxes.length === 0) return;

            const sortedBoxes = [...boxes].sort(sortFunction);
            sortedBoxes.forEach(box => container.appendChild(box));
        }

        function sortAlphabetically() {
            sortBoxes((a, b) => getTeamName(a).localeCompare(getTeamName(b)));
        }

        function sortByDate() {
            sortBoxes((a, b) => {
                const dateCompare = getLastUpdated(b) - getLastUpdated(a);
                return dateCompare === 0 ? getTeamName(a).localeCompare(getTeamName(b)) : dateCompare;
            });
        }

        alphabeticalButton.addEventListener('click', sortAlphabetically);
        dateButton.addEventListener('click', sortByDate);
        formatButton.addEventListener('click', () => {
            // Cycle through formats: US -> EU -> ISO -> US
            const formats = Object.keys(dateFormats);
            const currentIndex = formats.indexOf(currentFormat);
            currentFormat = formats[(currentIndex + 1) % formats.length];
            updateFormatButtonText();
            updateAllDates();
        });

        // Initial sort
        setTimeout(sortAlphabetically, 1000);
    }

    // Watch for content to load
    const observer = new MutationObserver((mutations, obs) => {
        const downloadCards = document.querySelectorAll('.shared-downloadCard');
        if (downloadCards.length > 0) {
            const container = downloadCards[0].parentElement;
            if (container && !document.querySelector('.sort-controls')) {
                obs.disconnect();
                initializeSorter(container);
            }
        }
    });

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

    // Fallback timeout
    setTimeout(() => {
        observer.disconnect();
        const downloadCards = document.querySelectorAll('.shared-downloadCard');
        if (downloadCards.length > 0) {
            const container = downloadCards[0].parentElement;
            if (container && !document.querySelector('.sort-controls')) {
                initializeSorter(container);
            }
        }
    }, 10000);
})();