F95 Utility buttons

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 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();
    }
})();