Google Plus & Bing Plus

Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons, Gemini On/Off toggle

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

(Zateb bir user-style yöneticim var, yükleyeyim!)

// ==UserScript==
// @name         Google Plus & Bing Plus
// @version      8.3
// @description  Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons, Gemini On/Off toggle
// @author       monit8280
// @match        https://www.bing.com/search*
// @match        https://www.google.com/search*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      generativelanguage.googleapis.com
// @connect      api.cdnjs.com
// @require      https://cdnjs.cloudflare.com/ajax/libs/marked/16.3.0/lib/marked.umd.js
// @license      MIT
// @namespace http://tampermonkey.net/
// ==/UserScript==

(function () {
    'use strict';

    /**
     * ==========================================================================================
     * 1. CONFIGURATION & CONSTANTS
     * ==========================================================================================
     */
    const Config = {
        API: {
            DEFAULT_MODEL: 'gemini-2.5-flash',
            BASE_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
            MARKED_CDN: 'https://api.cdnjs.com/libraries/marked'
        },
        MODELS: [
            { id: 'gemini-2.5-flash', name: '2.5 Flash' },
            { id: 'gemini-2.5-flash-lite', name: '2.5 Flash Lite' },
            { id: 'gemini-2.5-pro', name: '2.5 Pro' },
            { id: 'gemini-3-flash-preview', name: '3.0 Flash' },
            { id: 'gemini-3-pro-preview', name: '3.0 Pro' }
        ],
        STORAGE: {
            PREFIX: 'gemini_',
            API_KEY: 'geminiApiKey',
            ENABLED: 'geminiEnabled',
            THEME: 'themeMode',
            MODEL: 'selectedModel',
            LAST_NOTIFIED: 'markedLastNotifiedVersion',
            CURRENT_VERSION: 'markedCurrentVersion',
            LATEST_VERSION: 'markedLatestVersion'
        },
        UI: {
            MARGIN: 8,
            PADDING: 16,
            BORDER_RADIUS: '12px', // Rounded aesthetics
            ICON_SIZE: '20px',
            LOGO_SIZE: '24px',
            SMALL_ICON_SIZE: '16px',
            ANIMATION_SPEED: '0.2s'
        },
        ASSETS: {
            GOOGLE_LOGO: 'https://www.gstatic.com/images/branding/searchlogo/ico/favicon.ico',
            BING_LOGO: 'https://account.microsoft.com/favicon.ico',
            GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_aurora_33f86dc0c0257da337c63.svg',
            REFRESH_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik00LjA2MTg5IDEzQzQuMDIxMDQgMTIuNjcyNCA0IDEyLjMzODcgNCAxMkM0IDcuNTgxNzIgNy41ODE3MiA0IDEyIDRDMTQuNTAwNiA0IDE2LjczMzIgNS4xNDcyNyAxOC4yMDAyIDYuOTQ0MTZNMTkuOTM4MSAxMUMxOS45NzkgMTEuMzI3NiAyMCAxMS42NjEzIDIwIDEyQzIwIDE2LjQxODMgMTYuNDE4MyAyMCAxMiAyMEM5LjYxMDYxIDIwIDcuNDY1ODkgMTguOTUyNSA2IDE3LjI5MTZNOSAxN0g2VjE3LjI5MTZNMTguMjAwMiA0VjYuOTQ0MTZNMTguMjAwMiA2Ljk0NDE2VjYuOTk5OTNMMTUuMjAwMiA3TTYgMjBWMTcuMjkxNiIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPg0KPC9zdmc+',
            LIGHT_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gVXBsb2FkZWQgdG86IFNWRyBSZXBvLCB3d3cuc3ZncmVwby5jb20sIEdlbmVyYXRvcjogU1ZHIFJlcG8gTWl4ZXIgVG9vbHMgLS0+DQo8c3ZnIHdpZHRoPSI4MDBweCIgaGVpZ2h0PSI4MDBweCIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgY2xhc3M9Imljb24iICB2ZXJzaW9uPSIxLjEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTg2MSA2NTYuN2wxNDQuNi0xNDQuNkw4NjEgMzY3LjZWMTYzLjFINjU2LjZMNTEyIDE4LjYgMzY3LjQgMTYzLjFIMTYzdjIwNC41TDE4LjQgNTEyLjEgMTYzIDY1Ni43djIwNC40aDIwNC40TDUxMiAxMDA1LjdsMTQ0LjYtMTQ0LjZIODYxeiIgZmlsbD0iI0ZDRDE3MCIgLz48cGF0aCBkPSJNNTEyIDEwMTUuN2MtMi42IDAtNS4xLTEtNy4xLTIuOUwzNjMuMyA4NzEuMUgxNjNjLTUuNSAwLTEwLTQuNS0xMC0xMFY2NjAuOEwxMS40IDUxOS4yYy0xLjktMS45LTIuOS00LjQtMi45LTcuMSAwLTIuNyAxLjEtNS4yIDIuOS03LjFMMTUzIDM2My40VjE2My4xYzAtNS41IDQuNS0xMCAxMC0xMGgyMDAuM0w1MDQuOSAxMS41YzEuOS0xLjkgNC40LTIuOSA3LjEtMi45czUuMiAxLjEgNy4xIDIuOWwxNDEuNiAxNDEuNkg4NjFjNS41IDAgMTAgNC41IDEwIDEwdjIwMC4zTDEwMTIuNiA1MDVjMS45IDEuOSAyLjkgNC40IDIuOSA3LjEgMCAyLjctMS4xIDUuMi0yLjkgNy4xTDg3MSA2NjAuOHYyMDAuM2MwIDUuNS00LjUgMTAtMTAgMTBINjYwLjdsLTE0MS42IDE0MS42Yy0yIDItNC41IDMtNy4xIDN6TTE3MyA4NTEuMWgxOTQuNGMyLjcgMCA1LjIgMS4xIDcuMSAyLjlMNTEyIDk5MS42bDEzNy41LTEzNy41YzEuOS0xLjkgNC40LTIuOSA3LjEtMi45SDg1MVY2NTYuN2MwLTIuNyAxLjEtNS4yIDIuOS03LjFsMTM3LjUtMTM3LjUtMTM3LjUtMTM3LjVjLTEuOS0xLjktMi45LTQuNC0yLjktNy4xVjE3My4xSDY1Ni42Yy0yLjcgMC01LjItMS4xLTcuMS0yLjlMNTEyIDMyLjcgMzc0LjUgMTcwLjJjLTEuOSAxLjktNC40IDIuOS03LjEgMi45SDE3M3YxOTQuNGMwIDIuNy0xLjEgNS4yLTIuOSA3LjFMMzIuNiA1MTIuMWwxMzcuNSAxMzcuNWMxLjkgMS45IDIuOSA0LjQgMi45IDcuMXYxOTQuNHoiIGZpbGw9IiIgLz48cGF0aCBkPSJNNTEyIDUxMi4xbS0yNTcuOCAwYTI1Ny44IDI1Ny44IDAgMSAwIDUxNS42IDAgMjU3LjggMjU3LjggMCAxIDAtNTE1LjYgMFoiIGZpbGw9IiNGN0REQUQiIC8+PHBhdGggZD0iTTUxMiA3NzkuOWMtNzEuNSAwLTEzOC44LTI3LjktMTg5LjQtNzguNC01MC42LTUwLjYtNzguNC0xMTcuOC03OC40LTE4OS40czI3LjktMTM4LjggNzguNC0xODkuNGM1MC42LTUwLjYgMTE3LjgtNzguNCAxODkuNC03OC40IDcxLjUgMCAxMzguOCAyNy45IDE4OS40IDc4LjQgNTAuNiA1MC42IDc4LjQgMTE3LjggNzguNCAxODkuNFM3NTIgNjUwLjkgNzAxLjQgNzAxLjUgNTgzLjUgNzc5LjkgNTEyIDc3OS45eiBtMC01MTUuNmMtNjYuMiAwLTEyOC40IDI1LjgtMTc1LjIgNzIuNi00Ni44IDQ2LjgtNzIuNiAxMDktNzIuNiAxNzUuMnMyNS44IDEyOC40IDcyLjYgMTc1LjJjNDYuOCA0Ni44IDEwOSA3Mi42IDE3NS4yIDcyLjYgNjYuMiAwIDEyOC40LTI1LjggMTc1LjItNzIuNiA0Ni44LTQ2LjggNzIuNiAxMDkgNzIuNi0xNzUuMlM3MzQgMzgzLjcgNjg3LjIgMzM2LjljLTQ2LjgtNDYuOC0xMDktNzIuNi0xNzUuMi03Mi42eiIgZmlsbD0iIiAvPjwvc3ZnPg==',
            DARK_ICON: 'data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IS0tIFVwbG9hZGVkIHRvOiBTVkcgUmVwbywgd3d3LnN2Z3JlcG8uY29tLCBHZW5lcmF0b3I6IFNWRyBSZXBvIE1peGVyIFRvb2xzIC0tPg0KPHN2ZyB3aWR0aD0iODAwcHgiIGhlaWdodD0iODAwcHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4NCjxwYXRoIGQ9Ik0xOS45MDAxIDIuMzA3MTlDMTkuNzM5MiAxLjg5NzYgMTkuMTYxNiAxLjg5NzYgMTkuMDAwNyAyLjMwNzE5TDE4LjU3MDMgMy40MDI0N0MxOC41MjEyIDMuNTI3NTIgMTguNDIyNiAzLjYyNjUxIDE4LjI5OCAzLjY3NTgzTDE3LjIwNjcgNC4xMDc4QzE2Ljc5ODYgNC4yNjkzNCAxNi43OTg2IDQuODQ5IDE3LjIwNjcgNS4wMTA1NEwxOC4yOTggNS40NDI1MkMxOC40MjI2IDUuNDkxODQgMTguNTIxMiA1LjU5MDgyIDE4LjU3MDMgNS43MTU4N0wxOS4wMDA3IDYuODExMTVDMTkuMTYxNiA3LjIyMDc0IDE5LjczOTIgNy4yMjA3NCAxOS45MDAxIDYuODExMTZMMjAuMzMwNSA1LjcxNTg3QzIwLjM3OTYgNS41OTA4MiAyMC40NzgyIDUuNDkxODQgMjAuNjAyOCA1LjQ0MjUyTDIxLjY5NDEgNS4wMTA1NEMyMi4xMDIyIDQuODQ5IDIyLjEwMjIgNC4yNjkzNCAyMS42OTQxIDQuMTA3OEwyMC42MDI4IDMuNjc1ODNDMjAuNDc4MiAzLjYyNjUxIDIwLjM3OTYgMy41Mjc1MiAyMC4zMzA1IDMuNDAyNDdMMTkuOTAwMSAyLjMwNzE5WiIgZmlsbD0iIzFDMjc0QyIvPg0KPHBhdGggZD0iTTE2LjAzMjggOC4xMjk2N0MxNS44NzE4IDcuNzIwMDkgMTUuMjk0MyA3LjcyMDA5IDE1LjEzMzMgOC4xMjk2N0wxNC45NzY0IDguNTI5MDJDMTQuOTI3MyA4LjY1NDA3IDE0LjgyODcgOC43NTMwNSAxNC43MDQxIDguODAyMzdMMTQuMzA2MiA4Ljk1OTg3QzEzLjg5ODEgOS4xMjE0MSAxMy44OTgxIDkuNzAxMDcgMTQuMzA2MiA5Ljg2MjYxTDE0LjcwNDEgMTAuMDIwMUMxNC44Mjg3IDEwLjA2OTQgMTQuOTI3MyAxMC4xNjg0IDE0Ljk3NjQgMTAuMjkzNUwxNS4xMzMzIDEwLjY5MjhDMTUuMjk0MyAxMS4xMDI0IDE1Ljg3MTggMTEuMTAyNCAxNi4wMzI4IDEwLjY5MjhMMTYuMTg5NyAxMC4yOTM1QzE2LjIzODggMTAuMTY4NCAxNi4zMzc0IDEwLjA2OTQgMTYuNDYyIDEwLjAyMDFMMTYuODU5OSA5Ljg2MjYxQzE3LjI2OCA5LjcwMTA3IDE3LjI2OCA5LjEyMTQxIDE2Ljg1OTkgOC45NTk4N0wxNi40NjIgOC44MDIzN0MxNi4zMzc0IDguNzUzMDUgMTYuMjM4OCA4LjY1NDA3IDE2LjE4OTcgOC41MjkwMkwxNi4wMzI4IDguMTI5NjdaIiBmaWxsPSIjMUMyNzRDIi8+DQo8cGF0aCBkPSJNMTIgMjJDMTcuNTIyOCAyMiAyMiAxNy41MjI4IDIyIDEyQzIyIDExLjUzNzMgMjEuMzA2NSAxMS40NjA4IDIxLjA2NzIgMTEuODU2OEMxOS45Mjg5IDEzLjc0MDYgMTcuODYxNSAxNSAxNS41IDE1QzExLjkxMDEgMTUgOSAxMi4wODk5IDkgOC41QzkgNi4xMzg0NSAxMC4yNTk0IDQuMDcxMDUgMTIuMTQzMiAyLjkzMjc2QzEyLjUzOTIgMi42OTM0NyAxMi40NjI3IDIgMTIgMkM2LjQ3NzE1IDIgMiA2LjQ3NzE1IDIgMTJDMiAxNy41MjI4IDYuNDc3MTUgMjIgMTIgMjJaIiBmaWxsPSIjMUMyNzRDIi8+DQo8L3N2Zz4='
        }
    };

    /**
     * ==========================================================================================
     * 2. CORE UTILITIES
     * ==========================================================================================
     */
    const Utils = {
        Storage: {
            get: (key) => localStorage.getItem(key),
            set: (key, val) => localStorage.setItem(key, val),
            getBool: (key, def = true) => {
                const val = localStorage.getItem(key);
                return val === null ? def : val === 'true';
            }
        },
        Env: {
            isGoogle: () => window.location.hostname.includes('google.com'),
            isBing: () => window.location.hostname.includes('bing.com'),
            getQuery: () => new URLSearchParams(window.location.search).get('q'),
            isMobile: () => {
                const ua = navigator.userAgent;
                const topWidth = window.innerWidth;
                const isTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
                return (/Android|iPhone|Mobile/i.test(ua)) || (isTouch && topWidth <= 1024);
            }
        },
        I18n: {
            MESSAGES: {
                prompt: { ko: `"${'${query}'}"에 대한 정보를 찾아줘`, default: `Please write information about "${'${query}'}" in markdown format` },
                enterApiKey: { ko: 'Gemini API 키 입력:', default: 'Enter Gemini API Key:' },
                geminiEmpty: { ko: '⚠️ 응답이 비어있습니다.', default: '⚠️ Response is empty.' },
                loading: { ko: '불러오는 중...', default: 'Loading...' },
                error: { ko: '오류 발생:', default: 'Error:' },
                geminiOff: { ko: 'Gemini가 꺼져있습니다.', default: 'Gemini is OFF' },
                searchGoogle: { ko: 'Google 검색', default: 'Search on Google' },
                searchBing: { ko: 'Bing 검색', default: 'Search on Bing' }
            },
            get(key, vars = {}) {
                const lang = navigator.language.includes('ko') ? 'ko' : 'default';
                const tmpl = this.MESSAGES[key]?.[lang] || this.MESSAGES[key]?.default || '';
                return tmpl.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || '');
            }
        }
    };

    /**
     * ==========================================================================================
     * 3. STATE MANAGEMENT (Theme & Settings)
     * ==========================================================================================
     */
    const State = {
        theme: 'light',
        init() {
            const saved = Utils.Storage.get(Config.STORAGE.THEME);
            this.theme = saved || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
            this.applyTheme();
            window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
                this.theme = e.matches ? 'dark' : 'light';
                this.applyTheme();
            });
        },
        toggleTheme() {
            this.theme = this.theme === 'light' ? 'dark' : 'light';
            Utils.Storage.set(Config.STORAGE.THEME, this.theme);
            this.applyTheme();
            return this.theme;
        },
        applyTheme() {
            document.documentElement.classList.toggle('gemini-dark-mode', this.theme === 'dark');
            const btn = document.getElementById('gemini-theme-toggle-btn');
            if (btn) btn.src = this.getThemeIcon();
        },
        getThemeIcon() {
            return this.theme === 'light' ? Config.ASSETS.DARK_ICON : Config.ASSETS.LIGHT_ICON;
        },
        // Model & API Key
        getApiKey: () => Utils.Storage.get(Config.STORAGE.API_KEY),
        setApiKey: (key) => Utils.Storage.set(Config.STORAGE.API_KEY, key),
        getModel: () => Utils.Storage.get(Config.STORAGE.MODEL) || Config.API.DEFAULT_MODEL,
        setModel: (model) => Utils.Storage.set(Config.STORAGE.MODEL, model),
        isEnabled: () => Utils.Storage.getBool(Config.STORAGE.ENABLED),
        setEnabled: (v) => Utils.Storage.set(Config.STORAGE.ENABLED, v ? 'true' : 'false')
    };

    /**
     * ==========================================================================================
     * 4. STYLES
     * ==========================================================================================
     */
    const Styles = {
        inject() {
            const css = `
                :root {
                    --g-bg: #fff; --g-border: #e0e0e0; --g-text: #000; --g-title: #000;
                    --g-btn-bg: #f0f3ff; --g-btn-border: #ccc; --g-code-bg: #f0f0f0;
                    --g-icon-filter: none;
                }
                .gemini-dark-mode {
                    --g-bg: #282c34; --g-border: #444; --g-text: #e0e0e0; --g-title: #fff;
                    --g-btn-bg: #3a3f4b; --g-btn-border: #555; --g-code-bg: #3b3b3b;
                    --g-icon-filter: invert(1);
                }
                /* Layout */
                #gemini-wrapper { margin-bottom: 20px; font-family: sans-serif; }
                #gemini-top-row { display: flex; gap: 8px; margin-bottom: 12px; width: 100%; }

                /* Buttons & Selects */
                .gemini-btn, .gemini-select {
                    height: 40px; border-radius: ${Config.UI.BORDER_RADIUS}; font-size: 13px;
                    display: flex; align-items: center; justify-content: center;
                    cursor: pointer; transition: all ${Config.UI.ANIMATION_SPEED};
                    border: 1px solid var(--g-btn-border);
                    background: var(--g-btn-bg) !important; /* Bing 스타일 오버라이드 */
                    color: var(--g-title) !important;
                    box-sizing: border-box; overflow: hidden; /* 오버플로우 방지 */
                }
                .gemini-btn img {
                    width: 16px !important; height: 16px !important;
                    object-fit: contain; /* 이미지 비율 유지 */
                }
                .gemini-btn:hover, .gemini-select:hover { transform: translateY(-2px); box-shadow: 0 2px 5px rgba(0,0,0,0.15); }
                #google-search-btn, #bing-search-btn { flex: 8; gap: 8px; } /* gap으로 간격 조정 */
                #gemini-model-select-top {
                    flex: 2; appearance: none; text-align-last: center; text-align: center;
                    outline: none; padding: 0 !important; margin: 0;
                }
                #gemini-model-select-top option { text-align: center; }

                /* Main Box */
                #gemini-box {
                    width: 100%; padding: ${Config.UI.PADDING}px;
                    border: 1px solid var(--g-border); border-radius: ${Config.UI.BORDER_RADIUS};
                    background: var(--g-bg); color: var(--g-text);
                    box-sizing: border-box; overflow: hidden;
                    position: relative; /* 자식 요소 위치 기준 */
                }
                #gemini-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
                #gemini-title-wrap { display: flex; align-items: center; gap: 8px; text-decoration: none; color: inherit; }
                #gemini-title-wrap h3 { font-weight: bold; margin: 0; font-size: 16px; color: var(--g-title); }
                #gemini-time-info { font-size: 12px; color: #888; font-weight: normal; margin-left: 6px; }
                #gemini-logo { width: 24px; height: 24px; cursor: pointer; transition: transform 0.2s; }
                #gemini-logo:hover { transform: scale(1.1); }
                #gemini-actions { display: flex; align-items: center; gap: 10px; }

                /* Content & Markdown */
                #gemini-content {
                    font-size: 14px; line-height: 1.6; color: var(--g-text);
                    padding-top: 4px; padding-right: 8px; /* 스크롤바 공간 확보 */
                    max-height: 650px; overflow-y: auto; scrollbar-width: thin;
                }
                #gemini-content p { margin: 0 0 12px 0; } /* 문단 간격 */
                #gemini-content::-webkit-scrollbar { width: 6px; }
                #gemini-content::-webkit-scrollbar-thumb { background-color: #ccc; border-radius: 3px; }
                .gemini-dark-mode #gemini-content::-webkit-scrollbar-thumb { background-color: #555; }
                #gemini-content h1, #gemini-content h2, #gemini-content h3 { margin-top: 20px; margin-bottom: 10px; }
                #gemini-content ul, #gemini-content ol { padding-left: 20px; margin: 10px 0; }
                #gemini-content li { margin-bottom: 5px; }
                #gemini-content pre { background: var(--g-code-bg); padding: 12px; border-radius: 6px; overflow-x: auto; margin: 10px 0; position: relative; }
                #gemini-divider { height: 1px; border: 0; background: var(--g-border); margin: 8px 0 16px 0; }

                /* Code Copy Button */
                .code-copy-btn {
                    position: absolute; top: 8px; right: 8px;
                    background: rgba(128, 128, 128, 0.2); color: var(--g-text);
                    border: 1px solid var(--g-border); border-radius: 4px;
                    font-size: 11px; padding: 4px 8px; cursor: pointer;
                    opacity: 0; transition: opacity 0.2s;
                }
                #gemini-content pre:hover .code-copy-btn { opacity: 1; }
                .code-copy-btn:hover { background: rgba(128, 128, 128, 0.4); }

                /* Icons - Specific Animations */
                .gemini-icon-btn {
                    width: 20px; height: 20px; cursor: pointer; opacity: 0.6; transition: 0.3s ease;
                    vertical-align: middle;
                }
                .gemini-icon-btn:hover { opacity: 1; }

                #gemini-theme-toggle-btn:hover { transform: scale(1.2); }

                #gemini-refresh-btn { transition: transform 0.6s ease-in-out; filter: var(--g-icon-filter); }
                #gemini-refresh-btn:hover { transform: rotate(360deg); }

                /* Toggle Switch */
                .g-toggle { width: 44px; height: 24px; border-radius: 12px; position: relative; cursor: pointer; transition: background 0.2s; }
                .g-knob { width: 22px; height: 22px; background: #fff; border-radius: 50%; position: absolute; top: 1px; left: 1px; transition: left 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.2); }
                .g-toggle.on { background: #d1d5db; } .g-toggle.off { background: #3c4043; }
                .g-toggle.on .g-knob { left: 21px; }

                /* API Key Input */
                #gemini-api-container { display: flex; flex-direction: column; gap: 10px; padding: 10px 0; }
                #gemini-api-input { width: 95%; padding: 10px; border-radius: 8px; border: 1px solid var(--g-border); background: var(--g-bg); color: var(--g-text); }
                #gemini-api-save-btn { align-self: flex-end; padding: 8px 16px; background: #4285f4; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; }

                /* Device Specific */
                .mobile-ua #gemini-box { border-radius: 16px; }
                #b_context, .b_right { width: 432px !important; }

                /* Skeleton Loading Animation */
                @keyframes shimmer {
                    0% { background-position: 200% 0; }
                    100% { background-position: -200% 0; }
                }
                .skeleton-loader { display: flex; flex-direction: column; gap: 12px; padding: 4px 0; }
                .skeleton-line {
                    height: 14px; width: 100%; border-radius: 4px;
                    background: linear-gradient(90deg, var(--g-bg) 25%, var(--g-border) 50%, var(--g-bg) 75%);
                    background-size: 200% 100%; animation: shimmer 1.5s infinite;
                }
                .skeleton-line.short { width: 60%; }
                .skeleton-line.medium { width: 85%; }
            `;
            GM_addStyle(css);
            document.documentElement.classList.add(Utils.Env.isMobile() ? 'mobile-ua' : 'desktop-ua');
        }
    };

    /**
     * ==========================================================================================
     * 5. API & SERVICES
     * ==========================================================================================
     */
    const GeminiAPI = {
        fetch(query, container, apiKey) {
            container.innerHTML = `
                <div class="skeleton-loader">
                    <div class="skeleton-line medium"></div>
                    <div class="skeleton-line"></div>
                    <div class="skeleton-line short"></div>
                </div>
            `;

            // 시간 표시 초기화
            const timeSpan = document.getElementById('gemini-time-info');
            if (timeSpan) timeSpan.textContent = '(0.00s)';

            const model = State.getModel();
            const prompt = Utils.I18n.get('prompt', { query });
            const startTime = performance.now();
            let timerId = null;

            // 스톱워치 시작
            if (timeSpan) {
                timerId = setInterval(() => {
                    const current = ((performance.now() - startTime) / 1000).toFixed(2);
                    timeSpan.textContent = `(${current}s)`;
                }, 10);
            }

            GM_xmlhttpRequest({
                method: 'POST',
                url: `${Config.API.BASE_URL}${model}:generateContent?key=${apiKey}`,
                headers: { 'Content-Type': 'application/json' },
                data: JSON.stringify({
                    contents: [{ parts: [{ text: prompt }] }]
                }),
                onload: (res) => {
                    if (timerId) clearInterval(timerId); // 타이머 정지
                    try {
                        const endTime = performance.now();
                        const duration = ((endTime - startTime) / 1000).toFixed(2);
                        if (timeSpan) timeSpan.textContent = `(${duration}s)`;

                        const data = JSON.parse(res.responseText);
                        const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
                        if (text) {
                            sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}`, text);
                            sessionStorage.setItem(`${Config.STORAGE.PREFIX}${query}_time`, duration); // 시간 저장
                            container.innerHTML = marked.parse(text);
                            UI.addCodeCopyButtons(container);
                        } else {
                            throw new Error(data.error?.message || 'Empty response');
                        }
                    } catch (e) {
                        container.textContent = `${Utils.I18n.get('error')} ${e.message}`;
                    }
                },
                onerror: () => {
                    if (timerId) clearInterval(timerId);
                    container.textContent = Utils.I18n.get('geminiEmpty');
                }
            });
        }
    };

    const LinkCleaner = {
        clean(root = document) {
            root.querySelectorAll('a[href]').forEach(a => {
                try {
                    const url = new URL(a.href);
                    // Google
                    if (url.hostname.includes('google.com') && url.pathname === '/url') {
                        const q = url.searchParams.get('q') || url.searchParams.get('url');
                        if (q) a.href = decodeURIComponent(q);
                    }
                    // Bing
                    else if (url.hostname.includes('bing.com') && url.pathname.includes('/ck')) {
                        const u = url.searchParams.get('u');
                        if (u) {
                            // Bing uses 'a1' prefix followed by base64 encoded URL
                            const target = u.replace(/^a1/, '');
                            // Base64 decoding might fail if padding is missing or characters are invalid
                            // Also need to handle URL safe base64 if necessary (Bing usually uses standard or simple replacement)
                            // Usually just atob working is fine for standard links.
                            // Some characters might be replaced: - -> +, _ -> /
                            const b64 = target.replace(/-/g, '+').replace(/_/g, '/');
                            // Add padding if needed
                            const pad = b64.length % 4;
                            const padded = pad ? b64 + '='.repeat(4 - pad) : b64;

                            try {
                                a.href = decodeURIComponent(atob(padded));
                            } catch (e) {
                                // If base64 fails, fallback to simple request or leave it
                            }
                        }
                    }
                } catch (e) { }
            });
        }
    };

    /**
     * ==========================================================================================
     * 6. UI COMPONENTS
     * ==========================================================================================
     */
    const UI = {
        create(tag, { id, className, style, html, attributes, events } = {}) {
            const el = document.createElement(tag);
            if (id) el.id = id;
            if (className) el.className = className;
            if (style) Object.assign(el.style, style);
            if (html) el.innerHTML = html;
            if (attributes) Object.entries(attributes).forEach(([k, v]) => el.setAttribute(k, v));
            if (events) Object.entries(events).forEach(([k, v]) => el.addEventListener(k, v));
            return el;
        },

        Controls: {
            createToggle() {
                const enabled = State.isEnabled();
                const wrap = UI.create('div', { style: { display: 'flex', alignItems: 'center', cursor: 'pointer', gap: '8px' } });

                // Text Label
                const label = UI.create('span', {
                    html: enabled ? 'ON' : 'OFF',
                    style: { fontSize: '13px', fontWeight: 'bold', color: enabled ? 'var(--g-title)' : '#888' }
                });

                // Switch
                const switchEl = UI.create('div', { className: `g-toggle ${enabled ? 'on' : 'off'}` });
                switchEl.appendChild(UI.create('div', { className: 'g-knob' }));

                wrap.onclick = () => {
                    const next = !State.isEnabled();
                    State.setEnabled(next);

                    // Update UI
                    switchEl.className = `g-toggle ${next ? 'on' : 'off'}`;
                    label.textContent = next ? 'ON' : 'OFF';
                    label.style.color = next ? 'var(--g-title)' : '#888';

                    App.refresh();
                };

                wrap.append(label, switchEl);
                return wrap;
            }
        },

        createTopRow(query) {
            const row = UI.create('div', { id: 'gemini-top-row' });

            // 1. Search Switch Button
            const isGoogle = Utils.Env.isGoogle();
            const btn = UI.create('div', {
                id: isGoogle ? 'bing-search-btn' : 'google-search-btn',
                className: 'gemini-btn',
                html: `<img src="${isGoogle ? Config.ASSETS.BING_LOGO : Config.ASSETS.GOOGLE_LOGO}"> <span>${Utils.I18n.get(isGoogle ? 'searchBing' : 'searchGoogle')}</span>`,
                events: { click: () => window.open(`https://${isGoogle ? 'bing' : 'google'}.com/search?q=${encodeURIComponent(query)}`) }
            });

            // 2. Model Selector
            const select = UI.create('select', {
                id: 'gemini-model-select-top',
                className: 'gemini-select',
                events: { change: (e) => { State.setModel(e.target.value); App.refresh(true); } }
            });
            Config.MODELS.forEach(m => {
                const opt = document.createElement('option');
                opt.value = m.id; opt.textContent = m.name;
                opt.selected = m.id === State.getModel();
                select.appendChild(opt);
            });

            row.appendChild(btn);
            row.appendChild(select);
            return row;
        },

        createApiKeyInput() {
            const container = UI.create('div', { id: 'gemini-api-container' });
            container.innerHTML = `<p style="margin:0; font-size:13px; font-weight:bold;">${Utils.I18n.get('enterApiKey')}</p>`;

            const input = UI.create('input', { id: 'gemini-api-input', type: 'password', attributes: { placeholder: 'AIza...' } });
            const btn = UI.create('button', {
                id: 'gemini-api-save-btn', html: 'Save & Refresh', events: {
                    click: () => {
                        const val = input.value.trim();
                        if (val) { State.setApiKey(val); App.refresh(true); }
                    }
                }
            });

            container.appendChild(input);
            container.appendChild(btn);
            return container;
        },

        createMainBox(query) {
            const box = UI.create('div', { id: 'gemini-box' });

            // Header
            const header = UI.create('div', { id: 'gemini-header' });

            // Title Container (Wrapper)
            const titleContainer = UI.create('div', { style: { display: 'flex', alignItems: 'center', gap: '0px' } });

            // Link (Clickable)
            const titleLink = UI.create('a', {
                id: 'gemini-title-link',
                attributes: { href: 'https://gemini.google.com/', target: '_blank' },
                style: { display: 'flex', alignItems: 'center', gap: '8px', textDecoration: 'none', color: 'inherit' }
            });
            titleLink.innerHTML = `<img id="gemini-logo" src="${Config.ASSETS.GEMINI_LOGO}"> <h3 style="margin:0; font-size:16px; font-weight:bold; color:var(--g-title);">Gemini Results</h3>`;

            // Time Info (Non-clickable)
            const timeSpan = UI.create('span', { id: 'gemini-time-info' });

            titleContainer.append(titleLink, timeSpan);

            const actions = UI.create('div', { id: 'gemini-actions' });
            const toggle = this.Controls.createToggle();
            const themeBtn = UI.create('img', {
                id: 'gemini-theme-toggle-btn', className: 'gemini-icon-btn',
                attributes: { src: State.getThemeIcon(), title: 'Toggle Theme' },
                events: { click: () => { State.toggleTheme(); themeBtn.src = State.getThemeIcon(); } }
            });
            const refreshBtn = UI.create('img', {
                id: 'gemini-refresh-btn', className: 'gemini-icon-btn',
                attributes: { src: Config.ASSETS.REFRESH_ICON, title: 'Refresh' },
                events: { click: () => App.refresh(true) }
            });

            actions.append(toggle, themeBtn, refreshBtn);
            header.append(titleContainer, actions);

            // Content
            const content = UI.create('div', { id: 'gemini-content' });
            const divider = UI.create('hr', { id: 'gemini-divider' });

            box.append(header, divider, content);
            return { box, content, refreshBtn };
        },

        addCodeCopyButtons(container) {
            container.querySelectorAll('pre').forEach(pre => {
                if (pre.querySelector('.code-copy-btn')) return;

                const btn = UI.create('button', {
                    className: 'code-copy-btn',
                    html: 'Copy',
                    events: {
                        click: () => {
                            const code = pre.querySelector('code')?.innerText || pre.innerText;
                            navigator.clipboard.writeText(code).then(() => {
                                btn.textContent = 'Copied!';
                                setTimeout(() => btn.textContent = 'Copy', 2000);
                            });
                        }
                    }
                });
                pre.appendChild(btn);
            });
        }
    };

    /**
     * ==========================================================================================
     * 7. MAIN APP CONTROLLER
     * ==========================================================================================
     */
    const App = {
        elements: null,

        init() {
            State.init();
            Styles.inject();
            try { marked.setOptions({ breaks: true, gfm: true }); } catch (e) { } // 마크다운 옵션 설정
            this.handleNavigation();
            LinkCleaner.clean();

            // Delayed init to wait for page load
            const onReady = () => {
                const target = Utils.Env.isGoogle() ? document.getElementById('rhs') : (document.getElementById('b_context') || document.querySelector('.b_right'));
                // Mobile check first
                if (Utils.Env.isMobile()) {
                    this.render(null); // Container not needed for mobile logic
                } else if (target) {
                    this.render(target);
                } else if (Utils.Env.isGoogle()) {
                    // Force rhs ...
                    // Force RHS creation if missing on Google
                    const rcnt = document.getElementById('rcnt');
                    if (rcnt) {
                        const rhs = UI.create('div', { id: 'rhs', style: { marginLeft: '16px', width: '432px' } });
                        rcnt.appendChild(rhs);
                        this.render(rhs);
                    }
                }
            };

            if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', onReady);
            else onReady();
        },

        render(container) {
            const query = Utils.Env.getQuery();
            if (!query) return;

            // 1. Mobile/Tablet Mode (Search Button Only)
            if (Utils.Env.isMobile()) {
                document.getElementById('mobile-search-btn-wrapper')?.remove();
                const btn = UI.create('div', {
                    id: 'mobile-search-btn-wrapper',
                    style: { margin: '0', padding: '15px 16px', background: 'var(--g-bg)' }
                });

                // 모바일용 버튼 생성 (UI.createTopRow에서 버튼만 추출하여 사용하거나 새로 생성)
                const isGoogle = Utils.Env.isGoogle();
                const searchBtn = UI.create('div', {
                    id: isGoogle ? 'bing-search-btn' : 'google-search-btn',
                    className: 'gemini-btn',
                    html: `<img src="${isGoogle ? Config.ASSETS.BING_LOGO : Config.ASSETS.GOOGLE_LOGO}"> <span>${Utils.I18n.get(isGoogle ? 'searchBing' : 'searchGoogle')}</span>`,
                    events: { click: () => window.open(`https://${isGoogle ? 'bing' : 'google'}.com/search?q=${encodeURIComponent(query)}`) },
                    style: { width: '100%' } // 모바일은 꽉 차게
                });
                btn.appendChild(searchBtn);

                // Insert Position Logic
                if (isGoogle) {
                    const target = document.getElementById('main') || document.getElementById('rcnt');
                    if (target) target.parentNode.insertBefore(btn, target);
                } else {
                    const target = document.getElementById('b_content');
                    if (target) target.prepend(btn);
                }
                return; // Gemini 로직 중단
            }

            // 2. Desktop Mode (Existing Logic)
            if (!container) return;

            // Google Width Enforcement
            if (Utils.Env.isGoogle() && container.id === 'rhs') {
                container.style.width = '432px';
                container.style.minWidth = '432px';
            }

            // Remove existing
            document.getElementById('gemini-wrapper')?.remove();

            // Structure
            const wrapper = UI.create('div', { id: 'gemini-wrapper' });
            wrapper.appendChild(UI.createTopRow(query));

            const { box, content, refreshBtn } = UI.createMainBox(query);
            this.elements = { content, refreshBtn }; // Cache for updates
            wrapper.appendChild(box);

            container.prepend(wrapper);
            this.loadContent(query);
        },

        loadContent(query, forceRefresh = false) {
            if (!this.elements) return;
            const { content, refreshBtn } = this.elements;
            const apiKey = State.getApiKey();

            if (!apiKey) {
                content.innerHTML = '';
                content.appendChild(UI.createApiKeyInput());
                refreshBtn.style.display = 'none';
                return;
            }
            refreshBtn.style.display = 'block';

            if (!State.isEnabled()) {
                content.textContent = Utils.I18n.get('geminiOff');
                return;
            }

            const cached = !forceRefresh && sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}`);
            if (cached) {
                const cachedTime = sessionStorage.getItem(`${Config.STORAGE.PREFIX}${query}_time`);
                const timeSpan = document.getElementById('gemini-time-info');
                if (timeSpan && cachedTime) timeSpan.textContent = `(${cachedTime}s)`;

                content.innerHTML = marked.parse(cached);
                UI.addCodeCopyButtons(content);
            } else {
                GeminiAPI.fetch(query, content, apiKey);
            }
        },

        refresh(force = false) {
            if (force) {
                // 모든 Gemini 관련 캐시(sessionStorage) 삭제
                Object.keys(sessionStorage).forEach(key => {
                    if (key.startsWith(Config.STORAGE.PREFIX)) {
                        sessionStorage.removeItem(key);
                    }
                });
            }
            const query = Utils.Env.getQuery();
            if (query) this.loadContent(query, force);
        },

        handleNavigation() {
            // SPA Navigation Watcher
            let lastUrl = location.href;
            const check = () => {
                if (location.href !== lastUrl) {
                    lastUrl = location.href;
                    setTimeout(() => {
                        const target = document.getElementById('rhs') || document.querySelector('#b_context') || document.querySelector('.b_right');
                        this.render(target);
                        LinkCleaner.clean();
                    }, 500);
                }
            };
            const obs = new MutationObserver(check);
            obs.observe(document.body, { childList: true, subtree: true });
        }
    };

    // Run
    App.init();

})();