yoharnu / Twitch Game Search (Steam, ITAD, Nintendo)

// ==UserScript==
// @name         Twitch Game Search (Steam, ITAD, Nintendo)
// @namespace    https://openuserjs.org/users/yoharnu
// @version      2.4.1
// @description  Adds Steam, IsThereAnyDeal, and Nintendo eShop search buttons to Twitch stream pages.
// @author       yoharnu
// @copyright    2026, yoharnu (https://openuserjs.org/users/yoharnu)
// @license      MIT
// @updateURL    https://openuserjs.org/meta/yoharnu/Twitch_Game_Search_(Steam,_ITAD,_Nintendo).meta.js
// @downloadURL  https://openuserjs.org/install/yoharnu/Twitch_Game_Search_(Steam,_ITAD,_Nintendo).user.js
// @match        https://www.twitch.tv/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    const CONTAINER_ID = 'twitch-game-search-container';

    // Base64 SVGs for Fallback Protection
    const FALLBACKS = {
        steam: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2M3ZDVlMCI+PHBhdGggZD0iTTEyIDJhMTAgMTAgMCAwIDAtOS42IDYuNkwgMiA4LjFWMTRhMiAyIDAgMCAwIDIgMmgybDQuNCA4LjhBmMTAgMTAgMCAwIDAgMTIgMjJhMTAgMTAgMCAwIDAgMTAtMTBBMTAgMTAgMCAwIDAgMTIgMnpmbTQuNiA1LjdjMS41IDAgMi43IDEuMiAyLjcgMi43cy0xLjIgMi43LTIuNyAyLjctMi43LTEuMi0yLjctMi43IDEuMi0yLjcgMi43LTIuN3oiLz48L3N2Zz4=',
        // Rocket shape to match ITAD v2 branding
        itad: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZmZmZiI+PHBhdGggZD0iTTE4LjUgMmgtNmMtLjggMC0xLjUuMy0yLjEgOWwtNi40IDYuNGMtMS4yIDEuMi0xLjIgMy4xIDAgNC4ycyTMuMSAxLjIgNC4yIDBsNi40LTYuNGMuNi0uNi45LTEuMy45LTIuMXYtNmMwLTEuMS0uOS0yLTItMnptLTMuNSA1LjVjLS44IDAtMS41LS43LTEuNS0xLjVzLjctMS41IDEuNS0xLjUgMS41LjcgMS41IDEuNS0uNyAxLjUtMS41IDEuNXoiLz48L3N2Zz4=',
        nintendo: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0iI2ZmZmZmZiI+PHBhdGggZD0iTTIgNmg0djEySDJWMnptMTYgMHYxMmg0VjZoLTR6TTggNnY4bDQtOFY2SDh6Ii8+PC9zdmc+'
    };

    const PROVIDERS = [
        {
            id: 'btn-steam',
            label: 'Steam',
            color: '#171a21', // Steam Dark Blue/Black
            textColor: '#c7d5e0',
            icon: 'https://store.steampowered.com/favicon.ico',
            fallback: FALLBACKS.steam,
            urlGen: (game) => `https://store.steampowered.com/search/?term=${encodeURIComponent(game)}`
        },
        {
            id: 'btn-itad',
            label: 'ITAD',
            // ITAD v2 Brand Blue
            color: '#01b2f1', 
            textColor: '#ffffff',
            // Official v2 Icon URL
            icon: 'https://isthereanydeal.com/public/icons/favicon-83577ccc.png',
            fallback: FALLBACKS.itad,
            urlGen: (game) => `https://isthereanydeal.com/search/?q=${encodeURIComponent(game)}`
        },
        {
            id: 'btn-nintendo',
            label: 'eShop',
            color: '#e60012', // Nintendo Red
            textColor: '#ffffff',
            icon: 'https://www.nintendo.com/favicon.ico',
            fallback: FALLBACKS.nintendo,
            // Added &cat=gme to filter strictly for Games
            urlGen: (game) => `https://www.nintendo.com/search/?q=${encodeURIComponent(game)}&cat=gme`
        }
    ];

    function createButton(provider, gameTitle) {
        const btn = document.createElement('a');
        btn.href = provider.urlGen(gameTitle);
        btn.target = '_blank';
        btn.title = `Search ${gameTitle} on ${provider.label}`;

        // Button Styles
        Object.assign(btn.style, {
            display: 'inline-flex',
            alignItems: 'center',
            marginLeft: '6px',
            textDecoration: 'none',
            backgroundColor: provider.color,
            color: provider.textColor,
            padding: '4px 8px',
            borderRadius: '4px',
            fontSize: '11px',
            fontWeight: '600',
            fontFamily: 'Helvetica Neue, Helvetica, Arial, sans-serif',
            lineHeight: '1',
            border: '1px solid rgba(255,255,255,0.1)',
            transition: 'all 0.2s ease',
            height: '22px',
            boxSizing: 'border-box',
            cursor: 'pointer'
        });

        // Hover Effect
        btn.onmouseover = () => {
            btn.style.opacity = '0.9';
            btn.style.transform = 'translateY(-1px)';
        };
        btn.onmouseout = () => {
            btn.style.opacity = '1';
            btn.style.transform = 'translateY(0)';
        };

        // Icon Image
        const icon = document.createElement('img');
        icon.src = provider.icon;
        icon.alt = '';
        Object.assign(icon.style, {
            width: '14px',
            height: '14px',
            marginRight: '5px',
            verticalAlign: 'middle',
            display: 'block'
        });

        // Fallback Protection
        icon.onerror = function() {
            console.log(`[TwitchSearch] Icon failed for ${provider.label}, using fallback.`);
            this.onerror = null;
            this.src = provider.fallback;
        };

        // Text Span
        const text = document.createElement('span');
        text.textContent = provider.label;
        text.style.verticalAlign = 'middle';

        btn.appendChild(icon);
        btn.appendChild(text);

        return btn;
    }

    // Helper to find the ACTUAL game link, ignoring "Streaming Together" dashboard links
    function getRealGameLink() {
        const links = document.querySelectorAll('a[data-a-target="stream-game-link"]');
        for (const link of links) {
            const href = link.getAttribute('href');
            // We only want links that point to a Category or Game directory
            if (href && (href.includes('/directory/category/') || href.includes('/directory/game/'))) {
                return link;
            }
        }
        return null;
    }

    function updateLinks() {
        // 1. Locate the correct link
        const gameLink = getRealGameLink();
        if (!gameLink) return;

        const gameTitle = gameLink.textContent.trim();
        if (!gameTitle) return;

        // 2. Manage Container
        let container = document.getElementById(CONTAINER_ID);

        if (container) {
            // Check if we need to update
            if (container.getAttribute('data-game') !== gameTitle) {
                container.innerHTML = '';
                container.setAttribute('data-game', gameTitle);
            } else {
                // Ensure the container is still in the correct place
                if (gameLink.nextSibling !== container) {
                    gameLink.insertAdjacentElement('afterend', container);
                }
                return;
            }
        } else {
            // Create new container
            container = document.createElement('div');
            container.id = CONTAINER_ID;
            container.setAttribute('data-game', gameTitle);
            Object.assign(container.style, {
                display: 'inline-flex',
                alignItems: 'center',
                marginLeft: '10px'
            });
            
            // 3. Insert specificially AFTER the game link
            gameLink.insertAdjacentElement('afterend', container);
        }

        // 4. Populate Buttons
        if (container.children.length === 0) {
            PROVIDERS.forEach(provider => {
                container.appendChild(createButton(provider, gameTitle));
            });
        }
    }

    // Observer
    const observer = new MutationObserver(() => {
        updateLinks();
    });

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

})();