Multi-Platform Search Buttons for Otomi Games

Adds a compact, open-by-default dropdown menu for search buttons on Otomi Games pages, with grouped buttons in a modern, pink-gradient design matching the site's logo. Product ID buttons are greyed out and unclickable if no valid DLsite ID (RJ/VJ format) is available.

// ==UserScript==
// @name         Multi-Platform Search Buttons for Otomi Games
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Adds a compact, open-by-default dropdown menu for search buttons on Otomi Games pages, with grouped buttons in a modern, pink-gradient design matching the site's logo. Product ID buttons are greyed out and unclickable if no valid DLsite ID (RJ/VJ format) is available.
// @author       FunkyJustin
// @match        https://otomi-games.com/*
// @grant        none
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // Inject CSS styles
    const style = document.createElement('style');
    style.innerHTML = `
        :root {
            --primary-pink-start: #FF6B9A; /* Pink from logo */
            --primary-pink-end: #FF8C61; /* Orange-pink gradient */
            --accent-teal: #4FD1C5; /* Otomi Games teal */
            --bg-light: #F7FAFC; /* Light background */
            --shadow-color: rgba(0, 0, 0, 0.1);
            --glass-bg: rgba(247, 250, 252, 0.9);
            --glass-border: rgba(255, 107, 154, 0.3); /* Pink-tinted border */
        }
        .search-btn {
            display: inline-flex;
            align-items: center;
            padding: 5px 10px;
            margin: 2px;
            background: linear-gradient(135deg, var(--primary-pink-start), var(--primary-pink-end));
            color: #1A202C; /* Black text to match page */
            text-decoration: none;
            border-radius: 6px;
            border: 1px solid var(--glass-border);
            box-shadow: 0 2px 4px var(--shadow-color), inset 0 0 2px var(--primary-pink-start);
            font-family: Poppins, Roboto, Arial, sans-serif;
            font-size: 16px;
            font-weight: 600;
            backdrop-filter: blur(5px);
            background-color: var(--glass-bg);
            transition: transform 0.2s, box-shadow 0.3s, opacity 0.3s;
        }
        .search-btn:hover {
            transform: scale(1.05);
            box-shadow: 0 4px 8px var(--shadow-color), 0 0 10px var(--accent-teal);
            opacity: 0.95;
        }
        .search-btn.disabled {
            opacity: 0.5;
            pointer-events: none;
            cursor: not-allowed;
            background: #E0E7FF;
            color: #1A202C; /* Black text for disabled state */
        }
        .search-btn img {
            width: 16px;
            height: 16px;
            margin-right: 6px;
        }
        .dropdown-container {
            max-width: 600px;
            margin: 10px 0;
        }
        .dropdown-toggle {
            display: flex;
            align-items: center;
            padding: 6px 12px;
            background: linear-gradient(135deg, var(--primary-pink-start), var(--primary-pink-end));
            color: #1A202C; /* Black text to match page */
            border: none;
            border-radius: 6px;
            font-family: Poppins, Roboto, Arial, sans-serif;
            font-size: 16px;
            font-weight: 500; /* Slightly thinner than buttons (600) */
            cursor: pointer;
            transition: transform 0.2s, background 0.3s;
            box-shadow: 0 2px 4px var(--shadow-color);
        }
        .dropdown-toggle:hover {
            transform: scale(1.03);
            background: linear-gradient(135deg, #FF8C61, #FF6B9A);
        }
        .dropdown-toggle:active, .dropdown-toggle:focus {
            background: linear-gradient(135deg, var(--primary-pink-start), var(--primary-pink-end));
            color: #1A202C; /* Maintain black text */
            outline: none;
            box-shadow: 0 2px 4px var(--shadow-color), inset 0 0 4px var(--primary-pink-start);
        }
        .dropdown-toggle::after {
            content: '▼';
            margin-left: 6px;
            transition: transform 0.3s ease;
        }
        .dropdown-toggle.collapsed::after {
            transform: rotate(180deg);
        }
        .dropdown-content {
            display: block; /* Open by default */
            background: var(--glass-bg);
            border-radius: 6px;
            box-shadow: 0 4px 12px var(--shadow-color);
            padding: 10px;
            margin-top: 5px;
            backdrop-filter: blur(5px);
            border: 1px solid var(--glass-border);
        }
        .dropdown-content.hidden {
            display: none;
        }
        .button-group {
            margin-bottom: 8px;
        }
        .group-label {
            font-family: Poppins, Roboto, Arial, sans-serif;
            font-size: 15px;
            font-weight: 600;
            color: #2D3748;
            margin-bottom: 4px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .button-group-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(110px, auto));
            gap: 4px;
        }
        .circle-btn-wrapper {
            display: inline-flex;
            flex-wrap: nowrap;
            align-items: center;
            margin-left: 8px;
            margin-bottom: 10px;
            margin-right: 8px;
        }
        .circle-btn-wrapper .search-btn {
            color: #1A202C !important; /* Enforce black text for developer buttons */
        }
    `;
    document.head.appendChild(style);

    // Icon URLs for each website
    const icons = {
        f95zone: 'https://f95zone.to/favicon.ico',
        ryuugames: 'https://www.ryuugames.com/wp-content/uploads/2020/05/cropped-ryuugames_logo-1-32x32.png',
        otomi: 'https://otomi-games.com/favicon.ico'
    };

    // Function to create a styled button
    function createButton(text, url, iconUrl, isDisabled = false) {
        const a = document.createElement('a');
        a.className = `search-btn ${isDisabled ? 'disabled' : ''}`;
        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;
    }

    // Main function to add buttons
    function addSearchButtons() {
        // Extract game title
        const titleEl = document.querySelector('h1.post-title.entry-title a');
        if (!titleEl) {
            console.warn('Game title element not found.');
            return;
        }
        let gameTitle = titleEl.innerText.trim();
        let productId = '';

        // Extract product ID from title (e.g., [RJ01321968] or [VJ013026])
        const idMatch = gameTitle.match(/\[([RV]J\d{6,8})\]/); // Match RJ/VJ followed by 6 or 8 digits
        if (idMatch) {
            productId = idMatch[1]; // e.g., RJ01321968 or VJ013026
            gameTitle = gameTitle.replace(/\[([RV]J\d{6,8})\]/, '').replace(/\[.*?\]/g, '').trim(); // Clean title
        } else {
            // If no ID in title, check URL slug for a valid DLsite ID
            const urlMatch = window.location.href.match(/\/([^\/]+)\/$/);
            if (urlMatch) {
                const slug = urlMatch[1]; // e.g., oceanlust
                // Check if the slug is a valid DLsite ID
                if (slug.match(/^[RV]J\d{6,8}$/)) {
                    productId = slug; // e.g., RJ01321968
                }
            }
            gameTitle = gameTitle.replace(/\[.*?\]/g, '').trim(); // Remove [Final], etc.
        }

        // If no valid DLsite ID was found, ensure productId is empty
        if (!productId.match(/^[RV]J\d{6,8}$/)) {
            productId = ''; // Invalidate if not a proper DLsite ID
        }

        // Extract developer (more flexible selector)
        const developerLi = Array.from(document.querySelectorAll('ul.wp-block-list li')).find(li => 
            li.textContent.includes('Publisher:') || li.textContent.includes('Developer:')
        );
        const developerName = developerLi ? developerLi.textContent.replace(/^(Publisher|Developer):/, '').trim() : '';
        if (!developerName) {
            console.warn('Developer element not found.');
            return;
        }

        // Define search URLs
        const urls = {
            f95zoneGame: `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/search=${encodeURIComponent(gameTitle)}`,
            f95zoneForum: `https://f95zone.to/search/?q=${encodeURIComponent(gameTitle)}&t=post&c[child_nodes]=1&c[nodes][0]=2&c[title_only]=1&o=relevance`,
            f95zonePID: `https://f95zone.to/search/?q=${encodeURIComponent(productId)}`,
            otomiGame: `https://otomi-games.com/?s=${encodeURIComponent(gameTitle)}`,
            otomiPID: `https://otomi-games.com/?s=${encodeURIComponent(productId)}`, // Updated URL format
            ryuuGame: `https://www.ryuugames.com/?s=${encodeURIComponent(gameTitle)}`,
            ryuuPID: `https://www.ryuugames.com/?s=${encodeURIComponent(productId)}`,
            f95zoneDev: `https://f95zone.to/sam/latest_alpha/#/cat=games/page=1/creator=${encodeURIComponent(developerName)}`,
            ryuuDev: `https://www.ryuugames.com/?s=${encodeURIComponent(developerName)}`,
            otomiDev: `https://otomi-games.com/?s=${encodeURIComponent(developerName)}`
        };

        // Create buttons, disabling product ID buttons if no valid ID
        const hasProductId = !!productId;
        const buttons = {
            f95Game: createButton('Search F95Zone', urls.f95zoneGame, icons.f95zone),
            f95Forum: createButton('F95 Forums', urls.f95zoneForum, icons.f95zone),
            f95PID: createButton('F95 Product ID', urls.f95zonePID, icons.f95zone, !hasProductId),
            otomiGame: createButton('Search Otomi', urls.otomiGame, icons.otomi),
            otomiPID: createButton('Otomi Product ID', urls.otomiPID, icons.otomi, !hasProductId),
            ryuuGame: createButton('Search Ryuugames', urls.ryuuGame, icons.ryuugames),
            ryuuPID: createButton('Ryuu Product ID', urls.ryuuPID, icons.ryuugames, !hasProductId),
            f95Dev: createButton('Search Developer on F95Zone', urls.f95zoneDev, icons.f95zone),
            ryuuDev: createButton('Search Developer on Ryuugames', urls.ryuuDev, icons.ryuugames),
            otomiDev: createButton('Search Developer on OtomiGames', urls.otomiDev, icons.otomi)
        };

        // Append buttons in a dropdown menu
        const titleContainer = document.querySelector('header.entry-header');
        if (titleContainer) {
            const dropdownContainer = document.createElement('div');
            dropdownContainer.className = 'dropdown-container';

            // Create toggle button
            const toggleButton = document.createElement('button');
            toggleButton.className = 'dropdown-toggle';
            toggleButton.textContent = 'Search Options';
            toggleButton.addEventListener('click', () => {
                dropdownContent.classList.toggle('hidden');
                toggleButton.classList.toggle('collapsed');
            });

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

            // F95Zone group
            const f95Group = document.createElement('div');
            f95Group.className = 'button-group';
            const f95Label = document.createElement('div');
            f95Label.className = 'group-label';
            f95Label.textContent = 'F95Zone';
            const f95Grid = document.createElement('div');
            f95Grid.className = 'button-group-grid';
            [buttons.f95Game, buttons.f95Forum, buttons.f95PID].forEach(btn => f95Grid.appendChild(btn));
            f95Group.appendChild(f95Label);
            f95Group.appendChild(f95Grid);

            // OtomiGames group
            const otomiGroup = document.createElement('div');
            otomiGroup.className = 'button-group';
            const otomiLabel = document.createElement('div');
            otomiLabel.className = 'group-label';
            otomiLabel.textContent = 'OtomiGames';
            const otomiGrid = document.createElement('div');
            otomiGrid.className = 'button-group-grid';
            [buttons.otomiGame, buttons.otomiPID].forEach(btn => otomiGrid.appendChild(btn));
            otomiGroup.appendChild(otomiLabel);
            otomiGroup.appendChild(otomiGrid);

            // Ryuugames group
            const ryuuGroup = document.createElement('div');
            ryuuGroup.className = 'button-group';
            const ryuuLabel = document.createElement('div');
            ryuuLabel.className = 'group-label';
            ryuuLabel.textContent = 'Ryuugames';
            const ryuuGrid = document.createElement('div');
            ryuuGrid.className = 'button-group-grid';
            [buttons.ryuuGame, buttons.ryuuPID].forEach(btn => ryuuGrid.appendChild(btn));
            ryuuGroup.appendChild(ryuuLabel);
            ryuuGroup.appendChild(ryuuGrid);

            // Append groups to dropdown content
            dropdownContent.appendChild(f95Group);
            dropdownContent.appendChild(otomiGroup);
            dropdownContent.appendChild(ryuuGroup);

            // Append toggle button and content to container
            dropdownContainer.appendChild(toggleButton);
            dropdownContainer.appendChild(dropdownContent);
            titleContainer.appendChild(dropdownContainer);
        }

        // Append developer buttons
        if (developerLi) {
            const devWrapper = document.createElement('span');
            devWrapper.className = 'circle-btn-wrapper';
            [buttons.f95Dev, buttons.ryuuDev, buttons.otomiDev].forEach(btn => devWrapper.appendChild(btn));
            developerLi.insertAdjacentElement('afterend', devWrapper);
        }
    }

    // Use MutationObserver to detect when required elements are available
    const observer = new MutationObserver((mutations, obs) => {
        const titleEl = document.querySelector('h1.post-title.entry-title a');
        const developerLi = Array.from(document.querySelectorAll('ul.wp-block-list li')).find(li => 
            li.textContent.includes('Publisher:') || li.textContent.includes('Developer:')
        );
        if (titleEl && developerLi) {
            addSearchButtons();
            obs.disconnect();
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // Fallback to run after a short delay for both old and new pages
    setTimeout(() => {
        const titleEl = document.querySelector('h1.post-title.entry-title a');
        const developerLi = Array.from(document.querySelectorAll('ul.wp-block-list li')).find(li => 
            li.textContent.includes('Publisher:') || li.textContent.includes('Developer:')
        );
        if (titleEl && developerLi && !document.querySelector('.dropdown-container')) {
            addSearchButtons();
        }
    }, 3000); // 3-second delay for reliability
})();