F95 Utility buttons

Added a search button for Compressed, Update only, Walkthrough .... with Settings!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         F95 Utility buttons
// @namespace    http://tampermonkey.net/
// @description  Added a search button for Compressed, Update only, Walkthrough .... with Settings!
// @version      3.0
// @author       GGD40727
// @match        https://f95zone.to/threads/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=f95zone.to
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // --- State & Settings ---
    let openInNewTab = GM_getValue('openInNewTab', true);

    const DEFAULT_BUTTONS = [
        { label: 'Update', query: 'Update', useTitle: true },
        { label: 'New+Compressed', query: 'Compressed', useTitle: true },
        { label: 'Compressed', query: 'Compressed', useTitle: false },
        { label: 'Walkthrough', query: 'Walkthrough', useTitle: true },
        { label: 'Mod', query: 'Mod', useTitle: false },
        { label: 'Cheats', query: 'Cheats', useTitle: true }
    ];

    let customButtons = GM_getValue('f95_custom_buttons', DEFAULT_BUTTONS);

    // Register Tampermonkey Menu Command for Settings
    GM_registerMenuCommand('⚙️ Script Settings', openSettingsModal);

    const SELECTORS = {
        title: 'div.p-title h1.p-title-value',
        searchInput: '.uix_searchInput',
        searchButton: 'button.button--primary.button--icon--search'
    };

    // --- Styling ---
    function injectStyles() {
        if (document.getElementById('f95-styles')) return;
        const style = document.createElement('style');
        style.id = 'f95-styles';
        style.innerHTML = `
            #f95-btn-container {
                position: fixed;
                right: 0px;
                top: 35%;
                z-index: 9998;
                display: flex;
                flex-direction: column;
                gap: 2px;
                align-items: flex-end; /* Aligns buttons to the right edge */
            }
            .f95-quick-search-btn {
                line-height: 30px;
                font-size: 14px;
                cursor: pointer;
                padding: 3px 10px;
                border: 1px solid #444;
                border-radius: 4px 0 0 4px;
                background-color: #2d2d2d;
                color: #efefef;
                text-align: center;
                transition: background-color 0.2s;
                width: max-content; /* Ensures button fits its text exactly */
            }
            .f95-quick-search-btn:hover { background-color: #444; }

            /* Modal Styles */
            #f95-settings-overlay {
                position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
                background: rgba(0,0,0,0.75); z-index: 99999; display: flex;
                align-items: center; justify-content: center;
            }
            #f95-settings-modal {
                background: #1e1e1e; color: #eee; padding: 20px; border-radius: 8px;
                width: 600px; max-width: 90vw; max-height: 90vh; overflow-y: auto;
                border: 1px solid #444; font-family: sans-serif;
                box-shadow: 0 4px 15px rgba(0,0,0,0.5);
            }
            #f95-settings-modal h2 { margin-top: 0; color: #fff; border-bottom: 1px solid #333; padding-bottom: 10px; }
            .f95-global-settings {
                background: #222; padding: 12px; border-radius: 6px;
                margin-bottom: 15px; border: 1px solid #333;
            }
            .f95-settings-row {
                display: flex; gap: 8px; margin-bottom: 10px; align-items: center;
                background: #2a2a2a; padding: 8px; border-radius: 4px;
            }
            .f95-settings-row input[type="text"] {
                padding: 6px; background: #111; color: #fff;
                border: 1px solid #444; border-radius: 3px;
            }
            .f95-input-label { flex: 1; }
            .f95-input-query { flex: 1.2; }
            .f95-toggle-title {
                display: flex; align-items: center; gap: 4px; font-size: 12px;
                background: #111; padding: 6px 8px; border-radius: 3px; border: 1px solid #444;
                cursor: pointer; user-select: none;
            }
            .f95-toggle-title input { cursor: pointer; }
            .f95-modal-btn {
                background: #333; color: white; border: 1px solid #555;
                padding: 6px 10px; cursor: pointer; border-radius: 3px;
            }
            .f95-modal-btn:hover { background: #555; }
            .f95-btn-danger { background: #632323; }
            .f95-btn-danger:hover { background: #8a3131; }
            .f95-btn-primary { background: #23527c; }
            .f95-btn-primary:hover { background: #337ab7; }
            .f95-modal-actions { margin-top: 15px; display: flex; justify-content: space-between; border-top: 1px solid #333; padding-top: 15px;}
        `;
        document.head.appendChild(style);
    }

    // --- Core Logic ---
    function getGameTitle() {
        const titleElement = document.querySelector(SELECTORS.title);
        if (!titleElement) return null;
        const match = titleElement.textContent.match(/\[(.*?)\]/);
        return match ? match[1] : null;
    }

    function getThreadId() {
        const match = window.location.pathname.match(/threads\/[^\/]+\.(\d+)/);
        return match ? match[1] : null;
    }

    function performSearch(query, useTitle, gameTitle) {
        let searchTerm = query.trim();

        // Prepend game title if toggled on and title exists
        if (useTitle && gameTitle) {
            searchTerm = `${gameTitle} ${searchTerm}`.trim();
        }

        if (!searchTerm) return;

        if (openInNewTab) {
            const threadId = getThreadId();
            let searchUrl = `https://f95zone.to/search/1/?q=${encodeURIComponent(searchTerm)}&t=post&o=relevance`;
            if (threadId) {
                searchUrl += `&c%5Bthread%5D=${threadId}`;
            }
            window.open(searchUrl, '_blank');
        } else {
            const searchInput = document.querySelector(SELECTORS.searchInput);
            const searchButton = document.querySelector(SELECTORS.searchButton);
            if (!searchInput || !searchButton) return;

            searchInput.value = searchTerm;
            searchInput.dispatchEvent(new Event('input', { bubbles: true }));
            searchButton.click();
        }
    }

    function initButtons() {
        // Remove existing container if re-rendering
        const existing = document.getElementById('f95-btn-container');
        if (existing) existing.remove();

        const container = document.createElement('div');
        container.id = 'f95-btn-container';

        const gameTitle = getGameTitle();

        customButtons.forEach(config => {
            const button = document.createElement('input');
            button.type = 'button';
            button.value = config.label;
            button.className = 'f95-quick-search-btn';

            button.addEventListener('click', () => {
                performSearch(config.query, config.useTitle, gameTitle);
            });
            container.appendChild(button);
        });

        document.body.appendChild(container);
    }

    // --- Settings UI ---
    function openSettingsModal() {
        if (document.getElementById('f95-settings-overlay')) return;

        const overlay = document.createElement('div');
        overlay.id = 'f95-settings-overlay';

        const modal = document.createElement('div');
        modal.id = 'f95-settings-modal';

        modal.innerHTML = `
            <h2>Script Settings</h2>

            <!-- Global Settings Block -->
            <div class="f95-global-settings">
                <label style="display: flex; align-items: center; gap: 8px; cursor: pointer; font-size: 14px;">
                    <input type="checkbox" id="f95-setting-newtab" ${openInNewTab ? 'checked' : ''} style="cursor: pointer; width: 16px; height: 16px;">
                    <strong>Open searches in a New Tab</strong>
                </label>
                <p style="font-size: 12px; color: #aaa; margin: 5px 0 0 24px;">
                    If unchecked, searches will fill the forum's search bar on the current page instead.
                </p>
            </div>

            <p style="font-size: 13px; color: #bbb; margin-bottom: 15px;">
                Check <b>+Title</b> to automatically include the game's title before your search term.<br>
                <em>Example: Toggling +Title on "Update" searches for "GameName Update".</em>
            </p>

            <!-- Buttons List -->
            <div id="f95-settings-list"></div>

            <div class="f95-modal-actions">
                <button id="f95-add-row" class="f95-modal-btn">+ Add New Button</button>
                <div>
                    <button id="f95-cancel" class="f95-modal-btn">Cancel</button>
                    <button id="f95-save" class="f95-modal-btn f95-btn-primary" style="margin-left: 8px;">Save & Apply</button>
                </div>
            </div>
        `;

        overlay.appendChild(modal);
        document.body.appendChild(overlay);

        const listContainer = document.getElementById('f95-settings-list');

        // Render rows
        customButtons.forEach(btn => addRowToSettings(listContainer, btn.label, btn.query, btn.useTitle));

        // Event Listeners
        document.getElementById('f95-add-row').addEventListener('click', () => addRowToSettings(listContainer, 'New Button', 'Term', true));

        document.getElementById('f95-cancel').addEventListener('click', () => overlay.remove());

        document.getElementById('f95-save').addEventListener('click', () => {
            saveSettings(listContainer);
            overlay.remove();
            initButtons(); // Refresh buttons on page dynamically
        });
    }

    function addRowToSettings(container, labelText = '', queryText = '', useTitle = true) {
        const row = document.createElement('div');
        row.className = 'f95-settings-row';
        row.innerHTML = `
            <input type="text" class="f95-input-label" placeholder="Button Name" value="${labelText}">
            <input type="text" class="f95-input-query" placeholder="Search Term" value="${queryText}">
            <label class="f95-toggle-title" title="Append the game's title to this search?">
                <input type="checkbox" class="f95-input-title" ${useTitle ? 'checked' : ''}> +Title
            </label>
            <button class="f95-modal-btn btn-up" title="Move Up">▲</button>
            <button class="f95-modal-btn btn-down" title="Move Down">▼</button>
            <button class="f95-modal-btn f95-btn-danger btn-del" title="Delete">✖</button>
        `;

        // Movement logic
        row.querySelector('.btn-up').addEventListener('click', () => {
            if (row.previousElementSibling) row.parentNode.insertBefore(row, row.previousElementSibling);
        });
        row.querySelector('.btn-down').addEventListener('click', () => {
            if (row.nextElementSibling) row.parentNode.insertBefore(row.nextElementSibling, row);
        });
        row.querySelector('.btn-del').addEventListener('click', () => row.remove());

        container.appendChild(row);
    }

    function saveSettings(container) {
        // Save Global Settings
        const newTabCheckbox = document.getElementById('f95-setting-newtab');
        if (newTabCheckbox) {
            openInNewTab = newTabCheckbox.checked;
            GM_setValue('openInNewTab', openInNewTab);
        }

        // Save Button Array
        const rows = container.querySelectorAll('.f95-settings-row');
        const newData = [];

        rows.forEach(row => {
            const label = row.querySelector('.f95-input-label').value.trim();
            const query = row.querySelector('.f95-input-query').value.trim();
            const useTitle = row.querySelector('.f95-input-title').checked;

            if (label && query) {
                newData.push({ label, query, useTitle });
            }
        });

        if (newData.length > 0) {
            customButtons = newData;
            GM_setValue('f95_custom_buttons', customButtons);
        } else {
            alert('Settings not saved: You must have at least one button.');
        }
    }

    // --- Initialization ---
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            injectStyles();
            initButtons();
        });
    } else {
        injectStyles();
        initButtons();
    }
})();