您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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(); } })();