GameFinder

Game search tool directly from the Steam game page to your favourite websites search engine!

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         GameFinder
// @namespace    https://violentmonkey.github.io/
// @version      5.4
// @description  Game search tool directly from the Steam game page to your favourite websites search engine!
// @author       Okagame
// @license      MIT
// @match        https://store.steampowered.com/app/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    // === ICONS ===
    const STAR = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 14 14" fill="none"><path fill="currentColor" d="M7 0L4.88269 4.68067L0 5.348L3.5688 8.918L2.67216 14L7 11.536L11.3278 14L10.4268 8.918L14 5.348L9.11731 4.68067L7 0Z"/></svg>`;
    const GRID = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none"><rect x="1" y="1" width="6" height="6" rx="1" fill="currentColor"/><rect x="9" y="1" width="6" height="6" rx="1" fill="currentColor"/><rect x="1" y="9" width="6" height="6" rx="1" fill="currentColor"/><rect x="9" y="9" width="6" height="6" rx="1" fill="currentColor"/></svg>`;
    const SEARCH = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18" fill="none"><path fill="currentColor" d="M13.8296 12.0786C14.8347 10.6321 15.2623 8.86133 15.0284 7.11496C14.7945 5.36859 13.9159 3.77313 12.5656 2.64269C11.2153 1.51224 9.49114 0.928708 7.73254 1.00696C5.97394 1.08522 4.30831 1.8196 3.06357 3.06552C1.81882 4.31144 1.08514 5.97864 1.00696 7.7389C0.928776 9.49916 1.51176 11.2249 2.64114 12.5765C3.77052 13.9281 5.36446 14.8075 7.10919 15.0417C8.85391 15.2758 10.623 14.8477 12.0682 13.8417L15.2185 17L15.3997 16.8187L16.8188 15.3982L17 15.2168L13.8296 12.0786ZM8.04222 12.5824C7.14643 12.5824 6.27075 12.3165 5.52593 11.8183C4.7811 11.3202 4.20058 10.6122 3.85777 9.78376C3.51497 8.95538 3.42528 8.04384 3.60004 7.16443C3.7748 6.28502 4.20616 5.47723 4.83958 4.84321C5.47301 4.20919 6.28004 3.77742 7.15862 3.60249C8.0372 3.42757 8.94787 3.51734 9.77548 3.86047C10.6031 4.2036 11.3104 4.78467 11.8081 5.5302C12.3058 6.27573 12.5714 7.15223 12.5714 8.04887C12.5714 9.25123 12.0943 10.4043 11.2449 11.2545C10.3955 12.1047 9.24344 12.5824 8.04222 12.5824V12.5824Z"/></svg>`;

    // === SITES ===
    const SITES = {
        '📁 Direct Downloads': [
            { name: 'FitGirl Repacks', urlPattern: 'https://fitgirl-repacks.site/?s={query}' },
            { name: 'Game Bounty', urlPattern: 'https://gamebounty.world/?s={query}' },
            { name: 'GOG Games', urlPattern: 'https://gog-games.to/?search={query}' },
            { name: 'STEAMRIP', urlPattern: 'https://steamrip.com/?s={query}' },
            { name: 'SteamUnderground', urlPattern: 'https://steamunderground.net/?s={query}' },
            { name: 'AbandonwareGames', urlPattern: 'https://abandonwaregames.net/search.php?q={query}' },
            { name: 'AstralGames', urlPattern: 'https://astral-games.xyz/search?q={query}' },
            { name: 'CG-GamesPC', urlPattern: 'https://www.cg-gamespc.com/games?game={query}' },
            { name: '🇪🇸 ElEnemigos', urlPattern: 'https://elenemigos.com/?g_name={query}' },
            { name: 'games 4 u', urlPattern: 'https://g4u.to/en/search/?str={query}' },
            { name: 'Games4U', urlPattern: 'https://games4u.org/?s={query}' },
            { name: 'Gamedie', urlPattern: 'https://gamdie.com/?s={query}' },
            { name: 'scene.cat', urlPattern: 'https://scene.cat/?q={query}' },
            { name: 'GamePCFull', urlPattern: 'https://gamepcfull.com/?s={query}' },
            { name: 'GamesPack', urlPattern: 'https://gamespack.net/?s={query}' },
            { name: 'GetFreeGames', urlPattern: 'https://getfreegames.net/?s={query}' },
            { name: '🇩🇪 GLOAD', urlPattern: 'https://gload.to/?s={query}' },
            { name: 'MyAbandonware', urlPattern: 'https://www.myabandonware.com/search/q/{query}' },
            { name: 'OldGamesDownload', urlPattern: 'https://oldgamesdownload.com/?s={query}' },
            { name: '🇷🇺 Old-Games.RU', urlPattern: 'https://www.old-games.ru/catalog/?gamename={query}' },
            { name: 'OvaGames', urlPattern: 'https://www.ovagames.com/?s={query}' },
            { name: '🇪🇸 PiviGames', urlPattern: 'https://pivigames.blog/?s={query}' },
            { name: 'ReloadedSteam', urlPattern: 'https://reloadedsteam.com/?s={query}' },
            { name: 'Repack-Games', urlPattern: 'https://repack-games.com/?s={query}' },
            { name: '⭕ RepackLab', urlPattern: 'https://repacklab.com/?s={query}' },
            { name: 'Rexa Games', urlPattern: 'https://rexagames.com/search/?q={query}' },
            { name: 'Steam-Cracked', urlPattern: 'https://steam-cracked.com/?s={query}' },
            { name: 'SteamGG', urlPattern: 'https://steamgg.net/?s={query}' },
            { name: 'SteamOra', urlPattern: 'https://steamora.net/?s={query}' },
            { name: 'The Collection Chamber', urlPattern: 'https://collectionchamber.blogspot.com/search?q={query}' },
            { name: 'The Dark Games', urlPattern: 'https://the-dark-games.com/?s={query}' },
            { name: '🇮🇩 Triah Games', urlPattern: 'https://triahgames.com/search/?q={query}' },
            { name: 'UnionCrax', urlPattern: 'https://union-crax.xyz/search?q={query}' },
            { name: 'WorldofPCGames', urlPattern: 'https://worldofpcgames.com/?s={query}' },
            { name: 'Stevv Game', urlPattern: 'https://www.stevvgame.com/search?q={query}' }
        ],
        '🐧 Linux': [
            { name: 'freelinuxpcgames', urlPattern: 'https://freelinuxpcgames.com/?s={query}' }
        ],
        '🔍 Ctrl+F': [
            { name: 'ElAmigos', urlPattern: 'https://elamigos.site/' },
            { name: 'Gnarly Repacks', urlPattern: 'https://rentry.org/gnarly_repacks' },
            { name: 'M4CKD0GE Repacks', urlPattern: 'https://m4ckd0ge-repacks.site/all-repacks.html' }
        ],
        '🧲 Torrents': [
            { name: 'FitGirl Repacks', urlPattern: 'https://fitgirl-repacks.site/?s={query}' },
            { name: '🇷🇺 Appnetica', urlPattern: 'https://appnetica.com/search?term={query}' },
            { name: '🇷🇺 ByXatab', urlPattern: 'https://byxatab.com/search/{query}' },
            { name: 'DODI Repacks', urlPattern: 'https://dodi-repacks.site/?s={query}' },
            { name: 'DODI Repacks-Alt', urlPattern: 'https://dodi-repacks.download/?s={query}' },
            { name: 'KaosKrew', urlPattern: 'https://kaoskrew.org/search.php?keywords={query}' },
            { name: '🇷🇺 Torrent Games', urlPattern: 'https://torrent-games.games/search/{query}' },
            { name: '🇷🇺 Torrent-Games-Alt', urlPattern: 'https://torrent-games.net/search/{query}' },
            { name: '🇷🇺 RuTor', urlPattern: 'https://rutor.info/search/{query}' }
        ]
    };

    // === STORAGE (key: ngsh_favourites - DO NOT CHANGE) ===
    const STORAGE_KEY = 'ngsh_favourites';

    class Storage {
        static async load() {
            try { return await GM.getValue(STORAGE_KEY, []); }
            catch { return []; }
        }
        static async save(data) {
            try { await GM.setValue(STORAGE_KEY, data); }
            catch {}
        }
    }

    // === FAVOURITES ===
    class Favourites {
        constructor() { this.list = []; }
        async init() { this.list = await Storage.load(); }
        async save() { await Storage.save(this.list); }

        has(site) {
            return this.list.some(f => f.name === site.name);
        }

        async toggle(site) {
            const idx = this.list.findIndex(f => f.name === site.name);
            if (idx >= 0) {
                this.list.splice(idx, 1);
            } else if (!this.has(site)) {
                this.list.push(site);
            }
            await this.save();
        }

        getAll() {
            const all = Object.values(SITES).flat();
            const unique = [...new Map(all.map(s => [s.name, s])).values()];
            return unique.filter(s => this.has(s));
        }
    }

    // === GAME DATA ===
    class Game {
        static get id() {
            const m = location.href.match(/app\/(\d+)/);
            return m ? m[1] : null;
        }

        static get name() {
            const el = document.getElementById('appHubAppName');
            return el ? el.textContent.replace(/[™®©]/g, '').replace(/\s+/g, ' ').trim() : '';
        }

        static get developer() {
            const el = document.querySelector('.dev_row a');
            return el ? el.textContent.trim() : '';
        }

        static get buildId() {
            for (const s of document.querySelectorAll('script')) {
                const m = s.textContent.match(/"buildid":\s*"?(\d+)"?/i);
                if (m) return m[1];
            }
            return null;
        }

        static get headerImg() {
            const el = document.querySelector('.game_header_image_full');
            return el ? el.src : null;
        }

        static get bgImg() {
            const el = document.querySelector('img.gameColor');
            return el ? el.src : null;
        }

        static get data() {
            return {
                id: this.id,
                name: this.name,
                dev: this.developer,
                build: this.buildId,
                header: this.headerImg,
                bg: this.bgImg
            };
        }
    }

    // === UI ===
    class UI {
        constructor() {
            this.el = {};
            this.visible = false;
            this.fav = new Favourites();
            this.game = null;
        }

        // --- Styles ---
        styles() {
            GM_addStyle(`
/* Main Buttons */
#ngsh-hub, #ngsh-fav {
    position: fixed;
    bottom: 40px;
    border-radius: 2px;
    height: 30px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    cursor: pointer;
    z-index: 100000;
    border: none;
    font-family: "Motiva Sans", Arial, sans-serif;
    font-size: 13px;
    white-space: nowrap;
    transition: all .2s;
}
#ngsh-hub {
    right: 20px;
    padding: 0 12px;
    background: rgba(103,193,245,.2);
    color: #67c1f5;
    box-shadow: 0 0 3px rgba(0,0,0,.2);
}
#ngsh-hub:hover {
    background: linear-gradient(135deg, rgba(103,193,245,.4), rgba(103,193,245,.3));
    color: #fff;
}
#ngsh-hub svg {
    width: 16px;
    height: 16px;
    margin-right: 6px;
}
#ngsh-fav {
    right: 98px;
    width: 30px;
    padding: 0;
    background: rgba(103,193,245,.2);
    color: #67c1f5;
    box-shadow: 0 0 3px rgba(0,0,0,.2);
}
#ngsh-fav:hover {
    background: linear-gradient(135deg, rgba(103,193,245,.4), rgba(103,193,245,.3));
    color: #fff;
}
#ngsh-fav svg {
    width: 14px;
    height: 14px;
}

/* Menu Container */
#ngsh-menu {
    position: fixed;
    right: 10px;
    bottom: 80px;
    width: 312px;
    max-height: 85vh;
    background: rgba(27,40,56,.95);
    border-radius: 4px;
    box-shadow: 0 0 30px rgba(0,0,0,.5);
    z-index: 99999;
    display: none;
    overflow: hidden;
    font-family: "Motiva Sans", Arial, sans-serif;
}
#ngsh-menu.show { display: block; }

/* Background Image */
#ngsh-bg {
    position: absolute;
    inset: 0;
    background-size: cover;
    background-position: center top;
    z-index: 0;
    -webkit-mask-image: linear-gradient(180deg, #000 150px, transparent 350px);
    mask-image: linear-gradient(180deg, #000 150px, transparent 350px);
}

/* Header Image */
#ngsh-header {
    position: relative;
    width: 100%;
    z-index: 1;
}
#ngsh-header img {
    width: 100%;
    display: block;
}

/* Game Info */
#ngsh-info {
    position: relative;
    padding: 14px 16px;
    z-index: 1;
    background: rgba(0,0,0,.5);
    backdrop-filter: blur(10px);
}
#ngsh-title {
    color: #fff;
    font-size: 18px;
    font-weight: 500;
    text-shadow: 1px 1px 2px rgba(0,0,0,.7);
    margin-bottom: 8px;
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
#ngsh-title:hover { color: #51b6ff; }
#ngsh-meta {
    display: flex;
    flex-wrap: wrap;
    gap: 12px;
    color: #8f98a0;
    font-size: 13px;
    text-shadow: 1px 1px 1px rgba(0,0,0,.5);
}
#ngsh-meta span { cursor: pointer; }
#ngsh-meta span:hover { color: #51b6ff; }
#ngsh-meta label { margin-right: 4px; }

/* Separator */
#ngsh-sep {
    height: 1px;
    background: rgba(255,255,255,.1);
    position: relative;
    z-index: 1;
}

/* Site List */
#ngsh-list {
    position: relative;
    z-index: 1;
    flex: 1;
    overflow-y: auto;
    overscroll-behavior: contain;
    scrollbar-width: thin;
    scrollbar-color: rgba(255,255,255,.2) transparent;
}
#ngsh-list::-webkit-scrollbar { width: 6px; }
#ngsh-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.2); border-radius: 3px; }

/* Search Box */
#ngsh-search {
    position: relative;
    z-index: 2;
    padding: 10px 12px;
    background: rgba(0,0,0,.4);
    display: flex;
}
#ngsh-search input {
    flex: 1;
    color: #fff;
    background: transparent;
    padding: 6px 10px;
    border: 1px solid rgba(255,255,255,.14);
    border-radius: 2px 0 0 2px;
    font-size: 12px;
    outline: none;
}
#ngsh-search input::placeholder { color: #8f98a0; }
#ngsh-search input:focus { border-color: rgba(255,255,255,.2); }
#ngsh-search button {
    width: 32px;
    display: flex;
    align-items: center;
    justify-content: center;
    background: #51b6ff;
    border: none;
    border-radius: 0 2px 2px 0;
    color: #fff;
    cursor: pointer;
}
#ngsh-search button:hover { background: #28aaff; }
#ngsh-search svg { width: 14px; height: 14px; }

/* Folder */
.ngsh-folder {
    border-bottom: 1px solid rgba(255,255,255,.1);
}
.ngsh-folder-head {
    display: flex;
    align-items: center;
    padding: 10px 16px;
    cursor: pointer;
    color: #fff;
    font-size: 13px;
    font-weight: 500;
    text-shadow: 1px 1px 2px rgba(0,0,0,.7);
    background: rgba(62,126,167,.2);
    border-left: 3px solid transparent;
}
.ngsh-folder-head:hover,
.ngsh-folder.open > .ngsh-folder-head {
    background: rgba(81,182,255,.2);
    color: #51b6ff;
    border-left-color: #51b6ff;
}
.ngsh-folder-icon {
    margin-right: 8px;
    transition: transform .15s;
    font-size: 10px;
    color: #8f98a0;
}
.ngsh-folder-head:hover .ngsh-folder-icon { color: #51b6ff; }
.ngsh-folder.open > .ngsh-folder-head > .ngsh-folder-icon { transform: rotate(90deg); }
.ngsh-folder-title { flex: 1; }
.ngsh-folder-count { color: #BEEE11; font-size: 14px; }
.ngsh-folder-body {
    max-height: 0;
    overflow: hidden;
    background: rgba(0,0,0,.15);
}
.ngsh-folder.open > .ngsh-folder-body {
    max-height: 250px;
    overflow-y: auto;
    scrollbar-width: thin;
    scrollbar-color: rgba(255,255,255,.15) transparent;
}
.ngsh-folder-body::-webkit-scrollbar { width: 5px; }
.ngsh-folder-body::-webkit-scrollbar-thumb { background: rgba(255,255,255,.15); border-radius: 3px; }

/* Site Item */
.ngsh-site {
    display: flex;
    align-items: center;
    padding: 8px 16px;
    cursor: pointer;
    color: #fff;
    font-size: 12px;
    text-shadow: 1px 1px 1px rgba(0,0,0,.5);
    border-bottom: 1px solid rgba(255,255,255,.03);
}
.ngsh-site:hover { background: rgba(255,255,255,.05); }
.ngsh-site img {
    width: 14px;
    height: 14px;
    margin-right: 10px;
    border-radius: 2px;
    flex-shrink: 0;
    background: rgba(255,255,255,.1);
}
.ngsh-site-name {
    flex: 1;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Favourite Star */
.ngsh-star {
    width: 24px;
    height: 24px;
    cursor: pointer;
    transition: transform .15s, color .15s;
    flex-shrink: 0;
    margin-left: auto;
    margin-right: -6px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 4px;
}
.ngsh-star:hover { transform: scale(1.15); background: rgba(255,255,255,.1); }
.ngsh-star.on { color: #fff; }
.ngsh-star.off { color: rgba(255,255,255,.3); }
.ngsh-star svg { width: 12px; height: 12px; }

/* No Favourites */
.ngsh-no-fav {
    padding: 12px 16px;
    color: #8f98a0;
    font-size: 11px;
    text-align: center;
    font-style: italic;
}

/* Toast */
#ngsh-toast {
    position: fixed;
    bottom: 20px;
    left: 50%;
    transform: translateX(-50%);
    background: #51b6ff;
    color: #fff;
    padding: 6px 12px;
    border-radius: 4px;
    font-size: 12px;
    z-index: 100001;
    opacity: 0;
    transition: opacity .2s;
}
#ngsh-toast.show { opacity: 1; }

/* Confirm Modal */
#ngsh-modal-overlay {
    position: fixed;
    inset: 0;
    background: transparent;
    z-index: 100010;
    display: none;
    align-items: flex-end;
    justify-content: flex-end;
    padding: 0 138px 80px 0;
}
#ngsh-modal-overlay.show { display: flex; }
#ngsh-modal {
    background: #1D2B3C;
    border-radius: 4px;
    box-shadow: 0 0 30px rgba(0,0,0,.6);
    padding: 16px 20px;
    min-width: 240px;
    max-width: 280px;
    font-family: "Motiva Sans", Arial, sans-serif;
    animation: ngsh-modal-in .15s ease-out;
}
@keyframes ngsh-modal-in {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}
#ngsh-modal-icon {
    text-align: center;
    margin-bottom: 12px;
}
#ngsh-modal-icon svg {
    width: 32px;
    height: 32px;
    color: #51b6ff;
}
#ngsh-modal-title {
    color: #fff;
    font-size: 18px;
    font-weight: 500;
    text-align: center;
    margin-bottom: 8px;
}
#ngsh-modal-msg {
    color: #c6d4df;
    font-size: 13px;
    text-align: center;
    margin-bottom: 20px;
}
#ngsh-modal-msg strong {
    color: #BEEE11;
    font-weight: 500;
}
#ngsh-modal-btns {
    display: flex;
    gap: 8px;
    justify-content: center;
}
#ngsh-modal-btns button {
    border-radius: 2px;
    border: none;
    cursor: pointer;
    color: #67c1f5;
    background: rgba(103,193,245,.2);
    font-size: 13px;
    font-family: inherit;
    line-height: 30px;
    padding: 0 15px;
    transition: all .2s;
}
#ngsh-modal-btns button:hover {
    background: linear-gradient(135deg, rgba(103,193,245,.4), rgba(103,193,245,.3));
    color: #fff;
}
            `);
        }

        // --- Helpers ---
        make(tag, cls = '', html = '') {
            const el = document.createElement(tag);
            if (cls) el.className = cls;
            if (html) el.innerHTML = html;
            return el;
        }

        favicon(url) {
            try {
                const host = new URL(url.replace('{query}', 'x')).hostname;
                return `https://www.google.com/s2/favicons?domain=${host}&sz=32`;
            } catch { return ''; }
        }

        copy(text) {
            navigator.clipboard.writeText(text).then(() => {
                this.el.toast.classList.add('show');
                setTimeout(() => this.el.toast.classList.remove('show'), 1500);
            }).catch(() => {});
        }

        openUrl(url) {
            const tab = window.open(url, '_blank');
            if (tab) { tab.blur(); window.focus(); }
        }

        // --- Buttons ---
        createButtons() {
            this.el.hub = this.make('button', '', GRID + '<span>Hub</span>');
            this.el.hub.id = 'ngsh-hub';
            this.el.hub.title = 'Open search hub';

            this.el.favBtn = this.make('button', '', STAR);
            this.el.favBtn.id = 'ngsh-fav';
            this.el.favBtn.title = 'Open all favourite sites';

            document.body.append(this.el.hub, this.el.favBtn);

            this.el.hub.onclick = e => { e.stopPropagation(); this.toggle(); };
            this.el.favBtn.onclick = e => { e.stopPropagation(); this.openAllFav(); };
            document.onclick = e => { if (this.visible && !this.el.menu.contains(e.target)) this.close(); };
        }

        // --- Menu ---
        createMenu() {
            this.el.menu = this.make('div');
            this.el.menu.id = 'ngsh-menu';

            this.el.bg = this.make('div');
            this.el.bg.id = 'ngsh-bg';
            this.el.header = this.make('div');
            this.el.header.id = 'ngsh-header';
            this.el.info = this.make('div');
            this.el.info.id = 'ngsh-info';
            this.el.sep = this.make('div');
            this.el.sep.id = 'ngsh-sep';
            this.el.list = this.make('div');
            this.el.list.id = 'ngsh-list';
            this.el.search = this.make('div');
            this.el.search.id = 'ngsh-search';
            this.el.toast = this.make('div');
            this.el.toast.id = 'ngsh-toast';
            this.el.toast.textContent = 'Copied!';

            const searchInput = this.make('input');
            searchInput.type = 'text';
            searchInput.placeholder = 'Search sites...';

            const searchBtn = this.make('button', '', SEARCH);
            this.el.search.append(searchInput, searchBtn);

            this.el.menu.append(this.el.bg, this.el.header, this.el.info, this.el.sep, this.el.list, this.el.search);
            this.createModal();
            document.body.append(this.el.menu, this.el.toast);

            searchInput.oninput = () => this.filter(searchInput.value.toLowerCase());
            searchBtn.onclick = () => { searchInput.value = ''; this.filter(''); searchInput.focus(); };
            searchInput.onkeypress = e => { if (e.key === 'Enter') searchInput.blur(); };

            this.el.searchInput = searchInput;
        }

        createModal() {
            this.el.modalOverlay = this.make('div');
            this.el.modalOverlay.id = 'ngsh-modal-overlay';

            this.el.modal = this.make('div');
            this.el.modal.id = 'ngsh-modal';
            this.el.modal.innerHTML = `
                <div id="ngsh-modal-icon">${STAR}</div>
                <div id="ngsh-modal-title">Open All Favourites?</div>
                <div id="ngsh-modal-msg">You're about to open <strong>0</strong> tabs.</div>
                <div id="ngsh-modal-btns">
                    <button>Cancel</button>
                    <button>Open All</button>
                </div>
            `;

            this.el.modalOverlay.appendChild(this.el.modal);
            document.body.appendChild(this.el.modalOverlay);

            this.el.modalMsg = this.el.modal.querySelector('#ngsh-modal-msg strong');
            const [cancelBtn, okBtn] = this.el.modal.querySelectorAll('#ngsh-modal-btns button');

            cancelBtn.onclick = () => this.closeModal();
            okBtn.onclick = () => { this.closeModal(); this.doOpenAllFav(); };
            this.el.modalOverlay.onclick = e => { if (e.target === this.el.modalOverlay) this.closeModal(); };
        }

        showModal(count) {
            this.el.modalMsg.textContent = count;
            this.el.modalOverlay.classList.add('show');
        }

        closeModal() {
            this.el.modalOverlay.classList.remove('show');
        }

        // --- Menu State ---
        toggle() {
            this.visible ? this.close() : this.open();
        }

        async open() {
            this.visible = true;
            this.el.menu.classList.add('show');
            if (!this.el.menu.built) {
                this.game = Game.data;
                this.buildMenu();
                this.el.menu.built = true;
            }
            setTimeout(() => this.el.searchInput.focus(), 100);
        }

        close() {
            this.visible = false;
            this.el.menu.classList.remove('show');
            this.el.searchInput.value = '';
            this.filter('');
            this.el.list.querySelectorAll('.ngsh-folder').forEach(f => f.classList.remove('open'));
        }

        filter(q) {
            this.el.list.querySelectorAll('.ngsh-folder').forEach(f => {
                if (f.dataset.fav === 'true') { f.style.display = 'block'; return; }
                let show = false;
                f.querySelectorAll('.ngsh-site').forEach(s => {
                    const name = s.querySelector('.ngsh-site-name').textContent.toLowerCase();
                    const vis = !q || name.includes(q);
                    s.style.display = vis ? 'flex' : 'none';
                    if (vis) show = true;
                });
                f.style.display = !q || show ? 'block' : 'none';
                if (q && show) f.classList.add('open');
            });
        }

        // --- Build Menu ---
        buildMenu() {
            if (this.game.bg) this.el.bg.style.backgroundImage = `url('${this.game.bg}')`;

            if (this.game.header) {
                const img = this.make('img');
                img.src = this.game.header;
                img.alt = this.game.name;
                this.el.header.appendChild(img);
            }

            this.el.info.innerHTML = '';
            if (this.game.name) {
                const t = this.make('div');
                t.id = 'ngsh-title';
                t.textContent = this.game.name;
                t.title = 'Click to copy';
                t.onclick = e => { e.stopPropagation(); this.copy(this.game.name); };
                this.el.info.appendChild(t);
            }

            const meta = this.make('div');
            meta.id = 'ngsh-meta';
            if (this.game.id) meta.innerHTML += `<span title="Click to copy"><label>ID:</label>${this.game.id}</span>`;
            if (this.game.dev) meta.innerHTML += `<span title="Click to copy"><label>Dev:</label>${this.game.dev}</span>`;
            if (this.game.build) meta.innerHTML += `<span title="Click to copy"><label>Build:</label>${this.game.build}</span>`;
            meta.querySelectorAll('span').forEach(s => {
                s.onclick = e => { e.stopPropagation(); this.copy(s.textContent.slice(s.textContent.indexOf(':') + 1)); };
            });
            if (meta.innerHTML) this.el.info.appendChild(meta);

            this.el.list.innerHTML = '';
            this.el.list.appendChild(this.createFolder('⭐ Favourites', this.fav.getAll(), true));
            for (const [cat, sites] of Object.entries(SITES)) {
                this.el.list.appendChild(this.createFolder(cat, sites));
            }
        }

        createFolder(title, sites, isFav = false) {
            const f = this.make('div', 'ngsh-folder');
            if (isFav) f.dataset.fav = 'true';

            const head = this.make('div', 'ngsh-folder-head');
            head.innerHTML = `<span class="ngsh-folder-icon">▶</span><span class="ngsh-folder-title">${title}</span><span class="ngsh-folder-count">${sites.length}</span>`;

            const body = this.make('div', 'ngsh-folder-body');
            if (sites.length) {
                sites.forEach(s => body.appendChild(this.createSite(s)));
            } else if (isFav) {
                body.innerHTML = '<div class="ngsh-no-fav">No favourites yet. Click the star on any site to add it.</div>';
            }

            head.onclick = e => { e.stopPropagation(); f.classList.toggle('open'); };
            f.append(head, body);
            return f;
        }

        createSite(s) {
            const el = this.make('div', 'ngsh-site');

            const img = this.make('img');
            img.src = this.favicon(s.urlPattern);
            img.onerror = () => img.style.display = 'none';

            const name = this.make('span', 'ngsh-site-name');
            name.textContent = s.name;

            const star = this.make('span', 'ngsh-star');
            star.innerHTML = STAR;
            star.classList.add(this.fav.has(s) ? 'on' : 'off');

            el.onclick = e => {
                e.stopPropagation();
                const url = s.urlPattern.replace('{query}', encodeURIComponent(this.game.name));
                this.openUrl(url);
            };

            star.onclick = async e => {
                e.stopPropagation();
                await this.fav.toggle(s);
                star.classList.toggle('on', this.fav.has(s));
                star.classList.toggle('off', !this.fav.has(s));
                this.refreshFav();
            };

            el.append(img, name, star);
            return el;
        }

        refreshFav() {
            const old = this.el.list.querySelector('[data-fav="true"]');
            if (old) old.replaceWith(this.createFolder('⭐ Favourites', this.fav.getAll(), true));
        }

        // --- Favourites Actions ---
        openAllFav() {
            const sites = this.fav.getAll();
            if (!sites.length) return;
            this.showModal(sites.length);
        }

        doOpenAllFav() {
            const sites = this.fav.getAll();
            const name = Game.name;
            sites.forEach(s => {
                const url = s.urlPattern.replace('{query}', encodeURIComponent(name));
                this.openUrl(url);
            });
        }

        // --- Init ---
        async init() {
            if (document.querySelector('#ngsh-hub')) return;
            await this.fav.init();
            this.styles();
            this.createButtons();
            this.createMenu();
        }
    }

    // === START ===
    (async () => {
        const ui = new UI();
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', () => ui.init());
        } else {
            await ui.init();
        }
    })();

    console.log('⚡ Neon Game Hub v5.3 loaded ⚡');
})();