TF Games Search Enhancer

Combines collapsible subcategories with a Save Search Config button and adds table sorter buttons (by likes, last update, name, author).

// ==UserScript==
// @name         TF Games Search Enhancer
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Combines collapsible subcategories with a Save Search Config button and adds table sorter buttons (by likes, last update, name, author).
// @match        https://tfgames.site/*
// @grant        none
// @run-at       document-end
// @license      Apache 2.0
// ==/UserScript==

(function() {
    'use strict';

    // --- Collapsible Subcategories & Save Search Config ---

    function makeHeadersCollapsible() {
        const headers = document.querySelectorAll('.searchheader');
        headers.forEach(header => {
            // Indicate clickable
            header.style.cursor = 'pointer';

            // Create an icon element for toggle state (▼ for expanded, ► for collapsed)
            const icon = document.createElement('span');
            icon.innerHTML = '▼'; // ▼ initially (expanded)
            icon.style.marginRight = '5px';
            icon.style.userSelect = 'none';
            // Insert the icon at the beginning of the header
            header.insertBefore(icon, header.firstChild);

            header.addEventListener('click', () => {
                const content = header.nextElementSibling;
                if (!content) return;
                if (content.style.display === 'none') {
                    // Expand
                    content.style.display = '';
                    icon.innerHTML = '▼'; // ▼
                } else {
                    // Collapse
                    content.style.display = 'none';
                    icon.innerHTML = '▶'; // ►
                }
            });
        });
    }

    function saveSearchConfig() {
        const config = {};
        const checkboxes = document.querySelectorAll('input[type="checkbox"]');
        checkboxes.forEach(cb => {
            // Create a key using the checkbox's name and value
            const key = cb.name + '::' + cb.value;
            config[key] = cb.checked;
        });
        localStorage.setItem('savedSearchConfig', JSON.stringify(config));
        alert('Search configuration saved!');
    }

    function loadSearchConfig() {
        const configJson = localStorage.getItem('savedSearchConfig');
        if (!configJson) return;
        let config;
        try {
            config = JSON.parse(configJson);
        } catch(e) {
            console.error('Failed to parse search config:', e);
            return;
        }
        const checkboxes = document.querySelectorAll('input[type="checkbox"]');
        checkboxes.forEach(cb => {
            const key = cb.name + '::' + cb.value;
            if (config.hasOwnProperty(key)) {
                cb.checked = config[key];
            }
        });
    }

    // The button is appended to the container with id "includexcludeother"
    function addSaveButton() {
        const btn = document.createElement('button');
        btn.textContent = 'Save Search Config';
        // Minimal styling: block-level with modest margin and padding.
        btn.style.display = 'block';
        btn.style.margin = '10px auto';
        btn.style.padding = '3px 6px';
        // Append the button to the main panel if it exists.
        const panel = document.getElementById('includexcludeother');
        if (panel) {
            panel.appendChild(btn);
        } else {
            document.body.appendChild(btn);
        }
        btn.addEventListener('click', saveSearchConfig);
    }

    function initCollapsibleAndConfig() {
        makeHeadersCollapsible();
        loadSearchConfig();
        addSaveButton();
    }


    // --- TF Games Site Table Sorter ---

    // Inject CSS for the sort buttons and arrow indicators.
    const style = document.createElement("style");
    style.innerHTML = `
        .sort-button {
            cursor: pointer;
            padding: 5px 10px;
            font-size: 14px;
            border: 1px solid #ccc;
            background: #f9f9f9;
        }
        .sort-button[data-sort-order="asc"]::after {
            content: " \\2193";  /* Down arrow for ascending */
        }
        .sort-button[data-sort-order="desc"]::after {
            content: " \\2191";  /* Up arrow for descending */
        }
    `;
    document.head.appendChild(style);

    function initTableSorter() {
        // Look for the searchdetails element.
        const searchDetails = document.querySelector('.searchdetails');
        if (!searchDetails) return;

        // Create a container for the buttons.
        const container = document.createElement('div');
        container.style.display = 'flex';
        container.style.gap = '10px';
        container.style.marginTop = '10px';

        // Helper: Creates a button with a default sort order.
        function createButton(text, defaultOrder, onClick) {
            const btn = document.createElement('button');
            btn.classList.add('sort-button');
            btn.textContent = text;
            // Store the default order and current sort order in the dataset.
            btn.dataset.defaultOrder = defaultOrder;
            btn.dataset.sortOrder = defaultOrder;
            btn.addEventListener('click', onClick);
            return btn;
        }

        // Generic sorting function.
        function sortRows(getValue, compareFn, reverse = false) {
            const tbody = document.querySelector('tbody');
            if (!tbody) return;
            const rows = Array.from(tbody.querySelectorAll('tr'));
            rows.sort((a, b) => {
                const aVal = getValue(a);
                const bVal = getValue(b);
                const cmp = compareFn(aVal, bVal);
                return reverse ? -cmp : cmp;
            });
            // Append sorted rows back into the tbody.
            rows.forEach(row => tbody.appendChild(row));
        }

        // Helper to handle toggling sort order for a button and performing the sort.
        function handleSort(button, getValue, defaultOrder, compareFn) {
            // Reset all other buttons to their default state.
            document.querySelectorAll('.sort-button').forEach(btn => {
                if (btn !== button) {
                    btn.dataset.sortOrder = btn.dataset.defaultOrder;
                }
            });

            // Toggle the clicked button.
            const currentOrder = button.dataset.sortOrder;
            const newOrder = currentOrder === defaultOrder
                ? (defaultOrder === "asc" ? "desc" : "asc")
                : defaultOrder;
            button.dataset.sortOrder = newOrder;

            // Determine if we need to reverse the natural sort order.
            let reverse = false;
            if (defaultOrder === "asc" && newOrder === "desc") {
                reverse = true;
            } else if (defaultOrder === "desc" && newOrder === "asc") {
                reverse = true;
            }
            sortRows(getValue, compareFn, reverse);
        }

        // Sorting functions for each criteria.

        // Sort by Likes (assumes likes are in the 5th cell, index 4). Default is descending.
        function sortByLikes(button) {
            handleSort(button,
                row => parseInt(row.children[4].innerText, 10) || 0,
                "desc",
                (a, b) => a - b
            );
        }

        // Sort by Last Update (assumes last update is in the 3rd cell, index 2). Default is descending.
        function sortByLastUpdate(button) {
            handleSort(button,
                row => {
                    const time = Date.parse(row.children[2].innerText.trim());
                    return isNaN(time) ? 0 : time;
                },
                "desc",
                (a, b) => a - b
            );
        }

        // Sort by Name (assumes game name is in the 1st cell, index 0). Default is ascending.
        function sortByName(button) {
            handleSort(button,
                row => row.children[0].innerText.trim().toLowerCase(),
                "asc",
                (a, b) => a.localeCompare(b)
            );
        }

        // Sort by Author (assumes author is in the 2nd cell, index 1). Default is ascending.
        function sortByAuthor(button) {
            handleSort(button,
                row => row.children[1].innerText.trim().toLowerCase(),
                "asc",
                (a, b) => a.localeCompare(b)
            );
        }

        // Create the buttons with appropriate default orders.
        const btnLikes = createButton('Sort by Likes', 'desc', function() { sortByLikes(this); });
        const btnLastUpdate = createButton('Sort by Last Update', 'desc', function() { sortByLastUpdate(this); });
        const btnName = createButton('Sort by Name', 'desc', function() { sortByName(this); });
        const btnAuthor = createButton('Sort by Author', 'desc', function() { sortByAuthor(this); });

        // Append buttons to the container.
        container.appendChild(btnLikes);
        container.appendChild(btnLastUpdate);
        container.appendChild(btnName);
        container.appendChild(btnAuthor);

        // Insert the container right after the searchdetails element.
        searchDetails.parentNode.insertBefore(container, searchDetails.nextSibling);
    }

    // --- Initialization ---
    function init() {
        initCollapsibleAndConfig();
        initTableSorter();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();