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);

})();