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.

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 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);
})();