GameFinder

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @version      5.7
// @name         GameFinder
// @name:zh-CN   GameFinder 游戏搜索器
// @description  Game search tool directly from the Steam game page to your favourite websites search engine!
// @description:zh-CN 直接从Steam游戏页面搜索你喜欢的网站!
// @match        https://store.steampowered.com/app/*
// [v-DYNAMIC_METAS-v]
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_addStyle
// @run-at       document-end
// [^-DYNAMIC_METAS-^]
// @license      MIT
// @namespace    Name1or2-GameFinder-7x9k2m
// @author       Name1or2
// ==/UserScript==

(function() {
    'use strict';

    // === LOCALIZATION ===
    const LOCALE = {
        'en': {
            // Buttons
            hub: 'Hub',
            hubTitle: 'Open search hub',
            favTitle: 'Open all favourite sites',
            // Title area
            editTitle: 'Click to edit name',
            copyTitle: 'Click to copy',
            resetTitle: 'Click to reset name to original',
            inputPlaceholder: 'Enter game name...',
            // Toasts
            copied: 'Copied!',
            resetName: 'Reset to original name',
            // Meta
            clickCopy: 'Click to copy',
            labelId: 'ID:',
            labelDev: 'Dev:',
            labelBuild: 'Build:',
            // Search
            searchPlaceholder: 'Search sites...',
            // Favourites
            favFolder: '⭐ Favourites',
            noFav: 'No favourites yet. Click the star on any site to add it.',
            // Modal
            modalTitle: 'Open All Favourites?',
            modalMsg: 'You\'re about to open <strong>0</strong> tabs.',
            btnCancel: 'Cancel',
            btnOpenAll: 'Open All',
            // Categories
            catDirect: '📁 Direct Downloads',
            catLinux: '🐧 Linux',
            catCtrlF: '🔍 Ctrl+F',
            catTorrents: '🧲 Torrents'
        },
        'zh-CN': {
            // Buttons
            hub: '搜索',
            hubTitle: '打开搜索中心',
            favTitle: '打开所有收藏网站',
            // Title area
            editTitle: '点击编辑名称',
            copyTitle: '点击复制',
            resetTitle: '点击恢复原始名称',
            inputPlaceholder: '输入游戏名称...',
            // Toasts
            copied: '已复制!',
            resetName: '已恢复原始名称',
            // Meta
            clickCopy: '点击复制',
            labelId: 'ID:',
            labelDev: '开发商:',
            labelBuild: '版本:',
            // Search
            searchPlaceholder: '搜索网站...',
            // Favourites
            favFolder: '⭐ 收藏夹',
            noFav: '暂无收藏。点击网站旁的星标添加收藏。',
            // Modal
            modalTitle: '打开所有收藏?',
            modalMsg: '即将打开 <strong>0</strong> 个标签页。',
            btnCancel: '取消',
            btnOpenAll: '全部打开',
            // Categories
            catDirect: '📁 直接下载',
            catLinux: '🐧 Linux',
            catCtrlF: '🔍 手动搜索',
            catTorrents: '🧲 种子下载'
        }
    };

    // Detect browser locale
    function getLocale() {
        const lang = navigator.language || navigator.userLanguage || 'en';
        return LOCALE[lang] ? lang : 'en';
    }

    const LANG = getLocale();
    const i18n = LOCALE[LANG];

    // === 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>`;
    const EDIT_ICON = `<img src="https://community.fastly.steamstatic.com/public/images//sharedfiles/icons/icon_edit.png" alt="Edit" style="width:16px;height:16px;vertical-align:middle;">`;

    // === SITES ===
    const SITES = {
        [i18n.catDirect]: [
            { name: 'FitGirl Repacks', urlPattern: 'https://fitgirl-repacks.site/?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}' }
        ],
        [i18n.catLinux]: [
            { name: 'freelinuxpcgames', urlPattern: 'https://freelinuxpcgames.com/?s={query}' }
        ],
        [i18n.catCtrlF]: [
            { name: 'ElAmigos', urlPattern: 'https://elamigos.site/' },
            { name: 'Game Bounty', urlPattern: 'https://gamebounty.world/' },
            { name: 'Gnarly Repacks', urlPattern: 'https://rentry.org/gnarly_repacks' },
            { name: 'M4CKD0GE Repacks', urlPattern: 'https://m4ckd0ge-repacks.site/all-repacks.html' }
        ],
        [i18n.catTorrents]: [
            { 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 KEYS ===
    const STORAGE_KEY_FAVS = 'ngsh_favourites';
    const STORAGE_KEY_NAMES = 'ngsh_custom_names';

    // === STORAGE ===
    class Storage {
        static async load(key) {
            try { return await GM.getValue(key, {}); }
            catch { return {}; }
        }
        static async save(key, data) {
            try { await GM.setValue(key, data); }
            catch {}
        }
    }

    // === CUSTOM NAMES ===
    class CustomNames {
        constructor() { this.data = {}; }
        async init() { this.data = await Storage.load(STORAGE_KEY_NAMES); }
        async save() { await Storage.save(STORAGE_KEY_NAMES, this.data); }

        get(gameId) {
            return gameId ? this.data[gameId] || null : null;
        }

        async set(gameId, name) {
            if (gameId && name) {
                this.data[gameId] = name;
                await this.save();
            }
        }

        async remove(gameId) {
            if (gameId && this.data[gameId]) {
                delete this.data[gameId];
                await this.save();
            }
        }
    }

    // === FAVOURITES ===
    class Favourites {
        constructor() { this.list = []; }
        async init() { this.list = await Storage.load(STORAGE_KEY_FAVS) || []; }
        async save() { await Storage.save(STORAGE_KEY_FAVS, 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.customNames = new CustomNames();
            this.game = null;
            this.currentName = '';
            this.isEditing = false;
        }

        // --- 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;
    cursor: pointer;
}
#ngsh-header img {
    width: 100%;
    display: block;
}
#ngsh-header:hover {
    opacity: 0.85;
}

/* Game Info */
#ngsh-info {
    position: relative;
    padding: 14px 16px;
    z-index: 1;
    background: rgba(0,0,0,.5);
    backdrop-filter: blur(10px);
}

/* Title Container with Outline */
#ngsh-title-container {
    display: flex;
    align-items: center;
    border: 1px solid rgba(255,255,255,.14);
    border-radius: 2px;
    padding: 4px 8px;
    margin-bottom: 8px;
    background: rgba(0,0,0,.2);
    transition: border-color .2s;
}
#ngsh-title-container:focus-within {
    border-color: rgba(81,182,255,.5);
}

/* Edit Icon */
#ngsh-edit-icon {
    cursor: pointer;
    margin-right: 8px;
    opacity: 0.7;
    transition: opacity .2s;
    flex-shrink: 0;
}
#ngsh-edit-icon:hover {
    opacity: 1;
}

/* Title Display */
#ngsh-title {
    color: #fff;
    font-size: 18px;
    font-weight: 500;
    text-shadow: 1px 1px 2px rgba(0,0,0,.7);
    cursor: pointer;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    flex: 1;
}
#ngsh-title:hover { color: #51b6ff; }

/* Title Input (Edit Mode) */
#ngsh-title-input {
    display: none;
    color: #fff;
    font-size: 18px;
    font-weight: 500;
    font-family: inherit;
    background: transparent;
    border: none;
    outline: none;
    flex: 1;
    text-shadow: 1px 1px 2px rgba(0,0,0,.7);
    min-width: 0;
}
#ngsh-title-input::placeholder { color: #8f98a0; }

/* Editing State */
#ngsh-title-container.editing #ngsh-title { display: none; }
#ngsh-title-container.editing #ngsh-title-input { display: block; }

#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;
    font-family: "Motiva Sans", Arial, sans-serif;
}
#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.showToast(i18n.copied);
            }).catch(() => {});
        }

        showToast(msg) {
            this.el.toast.textContent = msg;
            this.el.toast.classList.add('show');
            setTimeout(() => this.el.toast.classList.remove('show'), 1500);
        }

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

        // --- Buttons ---
        createButtons() {
            this.el.hub = this.make('button', '', GRID + `<span>${i18n.hub}</span>`);
            this.el.hub.id = 'ngsh-hub';
            this.el.hub.title = i18n.hubTitle;

            this.el.favBtn = this.make('button', '', STAR);
            this.el.favBtn.id = 'ngsh-fav';
            this.el.favBtn.title = i18n.favTitle;

            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 = i18n.copied;

            const searchInput = this.make('input');
            searchInput.type = 'text';
            searchInput.placeholder = i18n.searchPlaceholder;

            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">${i18n.modalTitle}</div>
                <div id="ngsh-modal-msg">${i18n.modalMsg}</div>
                <div id="ngsh-modal-btns">
                    <button>${i18n.btnCancel}</button>
                    <button>${i18n.btnOpenAll}</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() {
            if (this.isEditing) {
                this.saveEdit();
            }
            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}')`;

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

            this.el.header.onclick = e => {
                e.stopPropagation();
                this.resetName();
            };

            this.el.info.innerHTML = '';

            const titleContainer = this.make('div');
            titleContainer.id = 'ngsh-title-container';

            const editIcon = this.make('span');
            editIcon.id = 'ngsh-edit-icon';
            editIcon.innerHTML = EDIT_ICON;
            editIcon.title = i18n.editTitle;

            const titleDisplay = this.make('span');
            titleDisplay.id = 'ngsh-title';
            titleDisplay.title = i18n.copyTitle;

            const titleInput = this.make('input');
            titleInput.id = 'ngsh-title-input';
            titleInput.type = 'text';
            titleInput.placeholder = i18n.inputPlaceholder;

            titleContainer.append(editIcon, titleDisplay, titleInput);
            this.el.info.appendChild(titleContainer);

            const savedName = this.customNames.get(this.game.id);
            this.currentName = savedName || this.game.name || '';
            titleDisplay.textContent = this.currentName;
            titleInput.value = this.currentName;

            editIcon.onclick = e => {
                e.stopPropagation();
                this.enterEditMode();
            };

            titleDisplay.onclick = e => {
                e.stopPropagation();
                if (!this.isEditing) {
                    this.copy(this.currentName);
                }
            };

            titleInput.onclick = e => e.stopPropagation();

            titleInput.onkeydown = e => {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    this.saveEdit();
                } else if (e.key === 'Escape') {
                    this.cancelEdit();
                }
            };

            titleInput.onblur = e => {
                setTimeout(() => {
                    if (this.isEditing) {
                        this.saveEdit();
                    }
                }, 100);
            };

            const meta = this.make('div');
            meta.id = 'ngsh-meta';
            if (this.game.id) meta.innerHTML += `<span title="${i18n.clickCopy}"><label>${i18n.labelId}</label>${this.game.id}</span>`;
            if (this.game.dev) meta.innerHTML += `<span title="${i18n.clickCopy}"><label>${i18n.labelDev}</label>${this.game.dev}</span>`;
            if (this.game.build) meta.innerHTML += `<span title="${i18n.clickCopy}"><label>${i18n.labelBuild}</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(i18n.favFolder, this.fav.getAll(), true));
            for (const [cat, sites] of Object.entries(SITES)) {
                this.el.list.appendChild(this.createFolder(cat, sites));
            }

            this.el.titleContainer = titleContainer;
            this.el.editIcon = editIcon;
            this.el.titleDisplay = titleDisplay;
            this.el.titleInput = titleInput;
        }

        // --- Edit Mode ---
        enterEditMode() {
            this.isEditing = true;
            this.el.titleContainer.classList.add('editing');
            this.el.titleInput.value = this.currentName;
            this.el.titleInput.focus();
            this.el.titleInput.select();
        }

        saveEdit() {
            if (!this.isEditing) return;

            const newName = this.el.titleInput.value.trim();
            if (newName && newName !== this.game.name) {
                this.currentName = newName;
                this.customNames.set(this.game.id, newName);
            } else if (newName === this.game.name) {
                this.currentName = newName;
                this.customNames.remove(this.game.id);
            } else if (!newName) {
                this.el.titleInput.value = this.currentName;
            }

            this.el.titleDisplay.textContent = this.currentName;
            this.exitEditMode();
        }

        cancelEdit() {
            this.el.titleInput.value = this.currentName;
            this.exitEditMode();
        }

        exitEditMode() {
            this.isEditing = false;
            this.el.titleContainer.classList.remove('editing');
        }

        async resetName() {
            this.currentName = this.game.name;
            this.el.titleDisplay.textContent = this.currentName;
            this.el.titleInput.value = this.currentName;

            await this.customNames.remove(this.game.id);

            this.exitEditMode();

            this.showToast(i18n.resetName);
        }

        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">${i18n.noFav}</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.dataset.name = s.name;
            star.classList.add(this.fav.has(s) ? 'on' : 'off');

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

            star.onclick = async e => {
                e.stopPropagation();
                await this.fav.toggle(s);
                this.syncStars(s.name);
                this.refreshFav();
            };

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

        syncStars(siteName) {
            const isFav = this.fav.list.some(f => f.name === siteName);
            this.el.list.querySelectorAll(`.ngsh-star[data-name="${siteName}"]`).forEach(star => {
                star.classList.toggle('on', isFav);
                star.classList.toggle('off', !isFav);
            });
        }

        refreshFav() {
            const old = this.el.list.querySelector('[data-fav="true"]');
            if (old) old.replaceWith(this.createFolder(i18n.favFolder, 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();
            sites.forEach(s => {
                const url = s.urlPattern.replace('{query}', encodeURIComponent(this.currentName));
                this.openUrl(url);
            });
        }

        // --- Init ---
        async init() {
            if (document.querySelector('#ngsh-hub')) return;
            await this.fav.init();
            await this.customNames.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('⚡ GameFinder loaded ⚡');
})();