RyuuGames Multi-Platform Search Buttons

Adds a clean dropdown menu with search buttons for F95Zone, OtomiGames, and Ryuugames on Ryuugames pages. Reliable developer detection (incl. Japanese), modular config for easy expansion, robust insertion & error handling.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         RyuuGames Multi-Platform Search Buttons
// @namespace    http://tampermonkey.net/
// @version      2.17.0
// @description  Adds a clean dropdown menu with search buttons for F95Zone, OtomiGames, and Ryuugames on Ryuugames pages. Reliable developer detection (incl. Japanese), modular config for easy expansion, robust insertion & error handling.
// @author       FunkyJustin
// @license      MIT
// @match        https://www.ryuugames.com/*
// @grant        none
// @supportURL   https://greasyfork.org/en/scripts/497408-ryuugames-f95zone-search-button/feedback
// ==/UserScript==

/*
Update History:
- v2.17.0: Fixed layout shift - now cleanly pushes content down instead of up; improved spacing and flow around the inserted UI while keeping visual design identical.
- v2.16.0: Improved game title cleaning - now properly strips version tags like (v26.06.15) (v1.02) leaving only the core title (e.g. "Altair of Strife"); enhanced robustness of title extraction.
- v2.15.0: Fixed UI duplication (stronger duplicate guard using data attribute); reordered F95Zone buttons so Product ID appears on the right; kept only icon version; improved robustness against multiple triggers.
- v2.14.0: Fixed Product ID extraction to include full "RJ" prefix (e.g. RJ01623698 instead of just digits) so PID buttons now work correctly on all platforms; improved regex to handle both title and URL slugs; kept UI unchanged.
- v2.13.0: Improved developer name extraction with better fallback & trimming; made sites fully modular/configurable for easy expansion; fixed insertion order with wrapper container; added subtle version badge; enhanced robustness, error handling & MutationObserver retries; kept UI nearly identical.
- v2.12: Added clean dropdown with groups, consistent styling, reliable developer detection including Japanese characters.
*/

(function() {
    'use strict';

    // ====================== CONFIGURATION (EASY TO EXTEND) ======================
    const CONFIG = {
        version: '2.17.0',
        sites: {
            f95zone: {
                name: 'F95Zone',
                icon: 'https://f95zone.to/favicon.ico',
                searchUrl: (q) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/search=${encodeURIComponent(q)}`,
                forumUrl: (q) => `https://f95zone.to/search/?q=${encodeURIComponent(q)}&t=post&c[child_nodes]=1&c[nodes][0]=2&c[title_only]=1&o=relevance`,
                pidUrl: (pid) => `https://f95zone.to/search/?q=${encodeURIComponent(pid)}`,
                devUrl: (dev) => `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/creator=${encodeURIComponent(dev)}`
            },
            otomi: {
                name: 'OtomiGames',
                icon: 'https://otomi-games.com/favicon.ico',
                searchUrl: (q) => `https://otomi-games.com/?s=${encodeURIComponent(q)}`,
                pidUrl: (pid) => `https://otomi-games.com/?s=${encodeURIComponent(pid)}`,
                devUrl: (dev) => `https://otomi-games.com/?s=${encodeURIComponent(dev)}`
            },
            ryuugames: {
                name: 'Ryuugames',
                icon: 'https://www.ryuugames.com/wp-content/uploads/2020/05/cropped-ryuugames_logo-150x150.png',
                searchUrl: (q) => `https://www.ryuugames.com/?s=${encodeURIComponent(q)}`,
                pidUrl: (pid) => `https://www.ryuugames.com/?s=${encodeURIComponent(pid)}`,
                devUrl: (dev) => `https://www.ryuugames.com/?s=${encodeURIComponent(dev)}`
            }
        }
    };

    // ====================== STYLES (UNCHANGED VISUALLY) ======================
    const style = document.createElement('style');
    style.innerHTML = `
        :root {
            --button-bg: linear-gradient(135deg, #1e73be, #155a8a);
            --button-text: #ffffff;
            --button-hover: #155a8a;
            --dropdown-bg: rgba(255, 255, 255, 0.95);
            --dropdown-border: #e0e0e0;
            --text-color: #ffffff;
            --container-bg: #1a1a1a;
            --shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
        }
        .search-btn {
            display: inline-flex;
            align-items: center;
            padding: 8px 16px;
            margin: 4px;
            background: var(--button-bg);
            color: var(--button-text);
            text-decoration: none;
            border-radius: 10px;
            border: none;
            box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
            font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            font-size: 14px;
            font-weight: 500;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .search-btn:hover {
            background: var(--button-hover);
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
            transform: scale(1.05);
        }
        .search-btn img {
            width: 16px;
            height: 16px;
            margin-right: 8px;
        }
        .developer-btn-wrapper .search-btn {
            padding: 6px 12px;
            font-size: 13px;
        }
        .dropdown-container {
            max-width: 600px;
            margin: 16px 0 20px 0;
        }
        .dropdown-toggle {
            display: inline-flex;
            align-items: center;
            padding: 8px 16px;
            background: var(--button-bg);
            color: var(--button-text);
            border: none;
            border-radius: 10px;
            font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            transition: transform 0.2s, box-shadow 0.2s;
            position: relative;
        }
        .dropdown-toggle:hover {
            background: var(--button-hover);
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.15);
            transform: scale(1.05);
        }
        .dropdown-toggle::after {
            content: '▼';
            margin-left: 8px;
            transition: transform 0.2s;
        }
        .dropdown-toggle.collapsed::after {
            transform: rotate(180deg);
        }
        .version-badge {
            position: absolute;
            top: -6px;
            right: -8px;
            background: #ff9800;
            color: #000;
            font-size: 10px;
            padding: 1px 5px;
            border-radius: 8px;
            font-weight: bold;
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
        }
        .dropdown-content {
            display: block;
            background: var(--dropdown-bg);
            border: 1px solid var(--dropdown-border);
            border-radius: 12px;
            box-shadow: var(--shadow);
            padding: 16px;
            margin-top: 8px;
        }
        .dropdown-content.hidden {
            display: none;
        }
        .button-group {
            margin-bottom: 12px;
        }
        .group-label {
            font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            font-size: 14px;
            font-weight: 700;
            color: #222;
            margin-bottom: 8px;
        }
        .button-group-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
            gap: 10px;
        }
        .developer-btn-wrapper {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            margin-bottom: 24px;
            padding: 12px;
            background: var(--container-bg);
            border-radius: 12px;
            box-shadow: var(--shadow);
        }
        .developer-label {
            font-family: 'Roboto', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
            font-size: 14px;
            color: var(--text-color);
            margin-bottom: 8px;
        }
        .developer-btn-row {
            display: flex;
            flex-direction: row;
            flex-wrap: wrap;
            gap: 10px;
        }

        /* Ensure clean downward flow */
        .ryuugames-search-ui {
            clear: both;
            width: 100%;
        }
    `;
    document.head.appendChild(style);

    // ====================== HELPER FUNCTIONS ======================
    function createButton(text, url, iconUrl) {
        const a = document.createElement('a');
        a.className = 'search-btn';
        a.href = url;
        a.target = '_blank';
        a.title = text;
        if (iconUrl) {
            const img = document.createElement('img');
            img.src = iconUrl;
            img.alt = '';
            a.appendChild(img);
        }
        a.appendChild(document.createTextNode(text));
        return a;
    }

    function extractGameInfo() {
        const header = document.querySelector('header.td-post-title');
        if (!header) return null;

        const h1 = header.querySelector('h1.entry-title');
        if (!h1) return null;

        let titleText = h1.textContent.trim();

        let gameTitle = titleText
            .replace(/^\[ENG\]\s*/i, '')
            .replace(/\s*\(v[\d\.]+\)/gi, '')
            .replace(/\s*\(v[\d\.]+\)/gi, '')
            .replace(/\s*\(RJ\d+\)\s*$/i, '')
            .replace(/\s*\(\d+\.\d+\)/g, '')
            .trim();

        if (gameTitle.includes('(')) {
            gameTitle = gameTitle.split('(')[0].trim();
        }

        let productId = '';
        const titlePidMatch = titleText.match(/\(?([Rr][Jj]\d{6,})\)?/);
        const urlPidMatch = window.location.href.match(/([Rr][Jj]\d{6,})/);

        if (titlePidMatch) {
            productId = titlePidMatch[1].toUpperCase();
        } else if (urlPidMatch) {
            productId = urlPidMatch[1].toUpperCase();
        }

        return { gameTitle, productId, h1 };
    }

    function extractDeveloperName() {
        let developerName = 'Unknown';
        const contentDiv = document.querySelector('.td-post-content');
        if (!contentDiv) return developerName;

        const textContent = contentDiv.textContent || '';
        const paragraphs = contentDiv.getElementsByTagName('p');

        for (let p of paragraphs) {
            const lines = p.textContent.split(/\n|\r/);
            for (let line of lines) {
                line = line.trim();
                const match = line.match(/^(developer|開発者)\s*:+\s*(.+)$/i);
                if (match && match[2]) {
                    developerName = match[2].trim().split(/(?=\s*(released|language|version|18\+|censored|uncensored))/i)[0].trim();
                    if (developerName) return developerName;
                }
            }
        }

        const lines = textContent.split(/\n|\r/);
        for (let line of lines) {
            line = line.trim();
            const match = line.match(/(developer|開発者)\s*:+\s*([^\n\r]+)/i);
            if (match && match[2]) {
                developerName = match[2].trim().split(/(?=\s*(released|language|version|18\+))/i)[0].trim();
                if (developerName) return developerName;
            }
        }

        return developerName || 'Unknown';
    }

    // ====================== MAIN UI CREATION ======================
    function createSearchUI() {
        const info = extractGameInfo();
        if (!info) return false;

        const { gameTitle, productId, h1 } = info;
        const developerName = extractDeveloperName();

        if (document.querySelector('.ryuugames-search-ui')) {
            console.log('[RyuuGames Search] UI already present, skipping.');
            return true;
        }

        const mainContainer = document.createElement('div');
        mainContainer.className = 'ryuugames-search-ui';

        // Dropdown container
        const dropdownContainer = document.createElement('div');
        dropdownContainer.className = 'dropdown-container';

        const toggleButton = document.createElement('button');
        toggleButton.className = 'dropdown-toggle';
        toggleButton.innerHTML = `Search Options <span class="version-badge">${CONFIG.version}</span>`;
        toggleButton.title = `Multi-Platform Search v${CONFIG.version}`;

        const dropdownContent = document.createElement('div');
        dropdownContent.className = 'dropdown-content';

        toggleButton.addEventListener('click', () => {
            dropdownContent.classList.toggle('hidden');
            toggleButton.classList.toggle('collapsed');
        });

        Object.keys(CONFIG.sites).forEach(siteKey => {
            const site = CONFIG.sites[siteKey];
            const group = document.createElement('div');
            group.className = 'button-group';

            const label = document.createElement('div');
            label.className = 'group-label';
            label.textContent = site.name;
            group.appendChild(label);

            const grid = document.createElement('div');
            grid.className = 'button-group-grid';

            const buttonsInGroup = [];

            if (site.searchUrl) {
                buttonsInGroup.push(createButton(`Search ${site.name}`, site.searchUrl(gameTitle), site.icon));
            }

            if (siteKey === 'f95zone') {
                if (site.forumUrl) {
                    buttonsInGroup.push(createButton('F95Zone Forum', site.forumUrl(gameTitle), site.icon));
                }
                if (productId && site.pidUrl) {
                    buttonsInGroup.push(createButton('F95Zone Product ID', site.pidUrl(productId), site.icon));
                }
            } else {
                if (productId && site.pidUrl) {
                    buttonsInGroup.push(createButton(`${site.name} Product ID`, site.pidUrl(productId), site.icon));
                }
            }

            buttonsInGroup.forEach(btn => grid.appendChild(btn));
            group.appendChild(grid);
            dropdownContent.appendChild(group);
        });

        dropdownContainer.appendChild(toggleButton);
        dropdownContainer.appendChild(dropdownContent);

        // Developer section
        const developerWrapper = document.createElement('div');
        developerWrapper.className = 'developer-btn-wrapper';

        const devLabel = document.createElement('div');
        devLabel.className = 'developer-label';
        devLabel.textContent = `Developer: ${developerName}`;
        developerWrapper.appendChild(devLabel);

        const devRow = document.createElement('div');
        devRow.className = 'developer-btn-row';

        Object.keys(CONFIG.sites).forEach(siteKey => {
            const site = CONFIG.sites[siteKey];
            if (site.devUrl) {
                const btn = createButton(`${site.name} Developer`, site.devUrl(developerName), site.icon);
                devRow.appendChild(btn);
            }
        });

        developerWrapper.appendChild(devRow);

        mainContainer.appendChild(dropdownContainer);
        mainContainer.appendChild(developerWrapper);

        // Insert cleanly after the header to push content DOWN
        h1.insertAdjacentElement('afterend', mainContainer);

        console.log(`%c[RyuuGames Search] UI added successfully - v${CONFIG.version} | Title: "${gameTitle}" | PID: ${productId} | Developer: ${developerName}`, 'color: #1e73be; font-weight: bold');
        return true;
    }

    // ====================== OBSERVER & ROBUST INIT ======================
    let attempts = 0;
    const maxAttempts = 12;

    const observer = new MutationObserver(() => {
        try {
            const header = document.querySelector('header.td-post-title');
            if (header && document.querySelector('.td-post-content')) {
                if (!document.querySelector('.ryuugames-search-ui')) {
                    createSearchUI();
                } else {
                    observer.disconnect();
                }
            }
        } catch (err) {
            console.error('[RyuuGames Search] Error in observer:', err);
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // Initial + safety calls
    setTimeout(() => { try { createSearchUI(); } catch (e) {} }, 600);
    setTimeout(() => { try { createSearchUI(); } catch (e) {} }, 1800);
    setTimeout(() => { try { if (!document.querySelector('.ryuugames-search-ui')) createSearchUI(); } catch (e) {} }, 3500);

})();