// ==UserScript== // @name Google Plus & Bing Plus // @version 7.1 // @description Add Gemini response, improve speed to search results(Bing), add Google/Bing search buttons // @author monit8280 // @match https://www.bing.com/search* // @match https://www.google.com/search* // @grant GM_addStyle // @grant GM_xmlhttpRequest // @connect generativelanguage.googleapis.com // @require https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.12/marked.min.js // @license MIT // @namespace http://tampermonkey.net/ // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 설정 모듈: 전역 설정값 관리 (API, 스타일, 메시지 등) const Config = { API: { GEMINI_MODEL: 'gemini-2.5-flash', GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/', MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked' }, VERSIONS: { MARKED_VERSION: '15.0.12' }, CACHE: { PREFIX: 'gemini_cache_' }, STORAGE_KEYS: { CURRENT_VERSION: 'markedCurrentVersion', LATEST_VERSION: 'markedLatestVersion', LAST_NOTIFIED: 'markedLastNotifiedVersion', THEME_MODE: 'themeMode' // 테마 모드 저장을 위한 새 키 추가 }, UI: { DEFAULT_MARGIN: 8, DEFAULT_PADDING: 16, Z_INDEX: 9999 }, STYLES: { COLORS: { BACKGROUND_LIGHT: '#fff', // 라이트 모드 배경색 BACKGROUND_DARK: '#282c34', // 다크 모드 배경색 BORDER_LIGHT: '#e0e0e0', // 라이트 모드 테두리색 BORDER_DARK: '#444', // 다크 모드 테두리색 TEXT_LIGHT: '#000', // 라이트 모드 텍스트색 TEXT_DARK: '#e0e0e0', // 다크 모드 텍스트색 TITLE_LIGHT: '#000', // 라이트 모드 제목색 TITLE_DARK: '#ffffff', // 다크 모드 제목색 BUTTON_BG_LIGHT: '#f0f3ff', // 라이트 모드 버튼 배경 BUTTON_BG_DARK: '#3a3f4b', // 다크 모드 버튼 배경 BUTTON_BORDER_LIGHT: '#ccc', // 라이트 모드 버튼 테두리 BUTTON_BORDER_DARK: '#555', // 다크 모드 버튼 테두리 CODE_BLOCK_BG_LIGHT: '#f0f0f0', // 라이트 모드 코드 블록 배경 CODE_BLOCK_BG_DARK: '#3b3b3b', // 다크 모드 코드 블록 배경 }, BORDER_RADIUS: '4px', FONT_SIZE: { TEXT: '14px', TITLE: '18px' }, ICON_SIZE: '20px', LOGO_SIZE: '24px', SMALL_ICON_SIZE: '16px' }, ASSETS: { GOOGLE_LOGO: 'https://www.google.com/favicon.ico', BING_LOGO: 'https://account.microsoft.com/favicon.ico', GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png', REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.svg', LIGHT_MODE_ICON: 'https://www.svgrepo.com/show/503805/sun.svg', // 라이트 모드 아이콘 DARK_MODE_ICON: 'https://www.svgrepo.com/show/526043/moon-stars.svg' // 다크 모드 아이콘 }, MESSAGE_KEYS: { PROMPT: 'prompt', ENTER_API_KEY: 'enterApiKey', GEMINI_EMPTY: 'geminiEmpty', PARSE_ERROR: 'parseError', NETWORK_ERROR: 'networkError', TIMEOUT: 'timeout', LOADING: 'loading', UPDATE_TITLE: 'updateTitle', UPDATE_NOW: 'updateNow', SEARCH_ON_GOOGLE: 'searchongoogle', SEARCH_ON_BING: 'searchonbing' } }; // 지역화 모듈: 다국어 메시지 처리 const Localization = { MESSAGES: { [Config.MESSAGE_KEYS.PROMPT]: { ko: `"${'${query}'}"에 대한 정보를 찾아줘`, zh: `请以标记格式填写有关\"${'${query}'}\"的信息。`, default: `Please write information about \"${'${query}'}\" in markdown format` }, [Config.MESSAGE_KEYS.ENTER_API_KEY]: { ko: 'Gemini API 키를 입력하세요:', zh: '请输入 Gemini API 密钥:', default: 'Please enter your Gemini API key:' }, [Config.MESSAGE_KEYS.GEMINI_EMPTY]: { ko: '⚠️ Gemini 응답이 비어있습니다.', zh: '⚠️ Gemini 返回为空。', default: '⚠️ Gemini response is empty.' }, [Config.MESSAGE_KEYS.PARSE_ERROR]: { ko: '❌ 파싱 오류:', zh: '❌ 解析错误:', default: '❌ Parsing error:' }, [Config.MESSAGE_KEYS.NETWORK_ERROR]: { ko: '❌ 네트워크 오류:', zh: '❌ 网络错误:', default: '❌ Network error:' }, [Config.MESSAGE_KEYS.TIMEOUT]: { ko: '❌ 요청 시간이 초과되었습니다.', zh: '❌ 请求超时。', default: '❌ Request timeout' }, [Config.MESSAGE_KEYS.LOADING]: { ko: '불러오는 중...', zh: '加载中...', default: 'Loading...' }, [Config.MESSAGE_KEYS.UPDATE_TITLE]: { ko: 'marked.min.js 업데이트 필요', zh: '需要更新 marked.min.js', default: 'marked.min.js update required' }, [Config.MESSAGE_KEYS.UPDATE_NOW]: { ko: '확인', zh: '确认', default: 'OK' }, [Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE]: { ko: 'Google 에서 검색하기', zh: '在 Google 上搜索', default: 'Search on Google' }, [Config.MESSAGE_KEYS.SEARCH_ON_BING]: { ko: 'Bing 에서 검색하기', zh: '在 Bing 上搜索', default: 'Search on Bing' } }, getMessage(key, vars = {}) { const lang = navigator.language; const langKey = lang.includes('ko') ? 'ko' : lang.includes('zh') ? 'zh' : 'default'; const template = this.MESSAGES[key]?.[langKey] || this.MESSAGES[key]?.default || ''; return template.replace(/\$\{(.*?)\}/g, (_, k) => vars[k] || ''); } }; // 디바이스 감지 모듈 const DeviceDetector = { _cache: { deviceType: null, isGeminiAvailable: null }, getDeviceType() { if (this._cache.deviceType !== null) { return this._cache.deviceType; } const userAgent = navigator.userAgent; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const width = window.innerWidth; let deviceType; const isAndroid = /Android/i.test(userAgent); const isIPhone = /iPhone/i.test(userAgent); const hasMobileKeyword = /Mobile/i.test(userAgent); const isWindows = /Windows NT/i.test(userAgent); if (isWindows && !isTouchDevice && width > 1024) { deviceType = 'desktop'; } else if ((isAndroid || isIPhone) && hasMobileKeyword) { deviceType = 'mobile'; } else if (isAndroid && !hasMobileKeyword && width >= 768) { deviceType = 'tablet'; } else if (isTouchDevice && width <= 1024) { deviceType = 'mobile'; } else { deviceType = 'desktop'; } this._cache.deviceType = deviceType; console.log(`Device Type: ${deviceType.charAt(0).toUpperCase() + deviceType.slice(1)}`); return deviceType; }, isDesktop() { return this.getDeviceType() === 'desktop'; }, isMobile() { return this.getDeviceType() === 'mobile'; }, isTablet() { return this.getDeviceType() === 'tablet'; }, isGeminiAvailable() { if (this._cache.isGeminiAvailable === null) { const hasRHS = !!document.getElementById('rhs') || !!document.getElementById('b_context') || !!document.querySelector('.b_right'); this._cache.isGeminiAvailable = this.isDesktop() && hasRHS; } return this._cache.isGeminiAvailable; }, resetCache() { this._cache = { deviceType: null, isGeminiAvailable: null }; }, isGoogle() { return window.location.hostname.includes('google.com'); }, isBing() { return window.location.hostname.includes('bing.com'); } }; // 스타일 생성 모듈: CSS 스타일 정의 const StyleGenerator = { // 공통 스타일 정의 commonStyles: { '#b_results > li.b_ad a': { 'color': 'green !important' }, '#b_context, .b_context, .b_right': { 'color': 'initial !important', 'border': 'none !important', 'border-width': '0 !important', 'border-style': 'none !important', 'border-collapse': 'separate !important', 'background': 'transparent !important' }, '#rhs': { 'float': 'right', 'padding-left': '16px', 'width': '432px', 'margin-top': '20px' }, '#rhs #gemini-wrapper': { 'margin-bottom': '20px' }, // Google 모바일 페이지의 gsr 요소 배경색 추가 '.mobile-useragent #gsr': { 'background-color': '#ffffff !important' } }, // Gemini 박스 스타일 정의 geminiBoxStyles: { '#gemini-box': { 'width': '100%', 'max-width': '100%', 'border-width': '1px', 'border-style': 'solid', 'border-radius': Config.STYLES.BORDER_RADIUS, 'padding': `${Config.UI.DEFAULT_PADDING}px`, 'margin-bottom': `${Config.UI.DEFAULT_MARGIN * 2.5}px`, 'font-family': 'sans-serif', 'overflow-x': 'auto', 'position': 'relative', 'box-sizing': 'border-box', 'color': 'initial !important' } }, // 테마별 스타일 정의 themeStyles: { '#gemini-box': { 'background': `var(--gemini-background-color) !important`, 'border-color': `var(--gemini-border-color) !important` }, '#gemini-box h3': { 'color': `var(--gemini-title-color) !important` }, '#gemini-content, #gemini-content *': { 'color': `var(--gemini-text-color) !important`, 'background': 'transparent !important' }, '#gemini-divider': { 'background': `var(--gemini-border-color) !important` }, '#gemini-content pre': { 'background': `var(--gemini-code-block-bg) !important`, 'padding': `${Config.UI.DEFAULT_MARGIN + 2}px`, 'border-radius': Config.STYLES.BORDER_RADIUS, 'overflow-x': 'auto' }, '#google-search-btn, #bing-search-btn': { 'border-color': `var(--gemini-button-border)`, 'background-color': `var(--gemini-button-bg)`, 'color': `var(--gemini-title-color)`, }, '#marked-update-popup': { 'background': `var(--gemini-background-color)`, 'border-color': `var(--gemini-button-border)`, }, '#marked-update-popup button': { 'border-color': `var(--gemini-button-border)`, 'background-color': `var(--gemini-button-bg)`, 'color': `var(--gemini-title-color)`, } }, // Gemini 콘텐츠 스타일 정의 contentStyles: { '#gemini-content': { 'font-size': Config.STYLES.FONT_SIZE.TEXT, 'line-height': '1.6', 'white-space': 'pre-wrap', 'word-wrap': 'break-word', 'overflow-wrap': 'break-word', 'background': 'transparent !important' }, '#gemini-content ul, #gemini-content ol': { 'list-style-type': 'none' } }, // Gemini 헤더 스타일 정의 headerStyles: { '#gemini-header': { 'display': 'flex', 'align-items': 'center', 'justify-content': 'space-between', 'margin-bottom': `${Config.UI.DEFAULT_MARGIN}px` }, '#gemini-title-wrap': { 'display': 'flex', 'align-items': 'center' }, '#gemini-logo': { 'width': Config.STYLES.LOGO_SIZE, 'height': Config.STYLES.LOGO_SIZE, 'margin-right': `${Config.UI.DEFAULT_MARGIN}px` }, '#gemini-box h3': { 'margin': '0', 'font-size': Config.STYLES.FONT_SIZE.TITLE, 'font-weight': 'bold' }, '#gemini-refresh-btn': { 'width': Config.STYLES.ICON_SIZE, 'height': Config.STYLES.ICON_SIZE, 'cursor': 'pointer', 'opacity': '0.6', 'transition': 'transform 0.5s ease', 'margin-left': `${Config.UI.DEFAULT_MARGIN}px` }, '#gemini-theme-toggle-btn': { 'width': Config.STYLES.ICON_SIZE, 'height': Config.STYLES.ICON_SIZE, 'cursor': 'pointer', 'opacity': '0.6', 'transition': 'transform 0.5s ease' }, '#gemini-refresh-btn:hover, #gemini-theme-toggle-btn:hover': { 'opacity': '1', 'transform': 'rotate(360deg)' }, '#gemini-divider': { 'height': '1px', 'margin': `${Config.UI.DEFAULT_MARGIN}px 0` } }, // Google/Bing 검색 버튼 스타일 정의 searchButtonStyles: { '#google-search-btn, #bing-search-btn': { 'width': '100%', 'max-width': '100%', 'font-size': Config.STYLES.FONT_SIZE.TEXT, 'padding': `${Config.UI.DEFAULT_MARGIN}px`, 'margin-bottom': `${Config.UI.DEFAULT_MARGIN * 1.25}px`, 'cursor': 'pointer', 'border-width': '1px', 'border-style': 'solid', 'border-radius': Config.STYLES.BORDER_RADIUS, 'font-family': 'sans-serif', 'display': 'flex', 'align-items': 'center', 'justify-content': 'center', 'gap': `${Config.UI.DEFAULT_MARGIN}px`, 'transition': 'transform 0.2s ease' }, '#google-search-btn img, #bing-search-btn img': { 'width': Config.STYLES.SMALL_ICON_SIZE, 'height': Config.STYLES.SMALL_ICON_SIZE, 'vertical-align': 'middle', 'transition': 'transform 0.2s ease' }, '.desktop-useragent #google-search-btn:hover, .desktop-useragent #bing-search-btn:hover': { 'transform': 'scale(1.1)' }, '.desktop-useragent #google-search-btn:hover img, .desktop-useragent #bing-search-btn:hover img': { 'transform': 'scale(1.1)' } }, // 업데이트 팝업 스타일 정의 popupStyles: { '#marked-update-popup': { 'position': 'fixed', 'top': '30%', 'left': '50%', 'transform': 'translate(-50%, -50%)', 'padding': `${Config.UI.DEFAULT_PADDING * 1.25}px`, 'z-index': Config.UI.Z_INDEX, 'border-width': '1px', 'border-style': 'solid', 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)', 'text-align': 'center' }, '#marked-update-popup button': { 'margin-top': `${Config.UI.DEFAULT_MARGIN * 1.25}px`, 'padding': `${Config.UI.DEFAULT_PADDING}px ${Config.UI.DEFAULT_PADDING}px`, 'cursor': 'pointer', 'border-width': '1px', 'border-style': 'solid', 'border-radius': Config.STYLES.BORDER_RADIUS, 'font-family': 'sans-serif' } }, // 모바일 환경 스타일 정의 mobileStyles: { '.mobile-useragent #google-search-btn, .mobile-useragent #bing-search-btn': { 'max-width': '100%', 'width': 'calc(100% - 16px)', 'margin-left': `${Config.UI.DEFAULT_MARGIN}px !important`, 'margin-right': `${Config.UI.DEFAULT_MARGIN}px !important`, 'margin-top': `${Config.UI.DEFAULT_MARGIN}px`, 'margin-bottom': `${Config.UI.DEFAULT_MARGIN}px`, 'padding': `${Config.UI.DEFAULT_PADDING * 0.75}px`, 'border-radius': '16px', 'box-sizing': 'border-box' }, '.mobile-useragent #gemini-box': { 'padding': `${Config.UI.DEFAULT_PADDING * 0.75}px`, 'border-radius': '16px' }, '.mobile-useragent #b_content': { 'overflow': 'visible !important', 'position': 'relative' } }, // 모든 스타일을 하나의 CSS 문자열로 변환 generateStyles() { const styles = [ this.commonStyles, this.geminiBoxStyles, this.themeStyles, this.contentStyles, this.headerStyles, this.searchButtonStyles, this.popupStyles, this.mobileStyles ]; // CSS 변수 정의 (root에 추가하여 테마 변경에 활용) const cssVariables = ` :root { --gemini-background-color: ${Config.STYLES.COLORS.BACKGROUND_LIGHT}; --gemini-border-color: ${Config.STYLES.COLORS.BORDER_LIGHT}; --gemini-text-color: ${Config.STYLES.COLORS.TEXT_LIGHT}; --gemini-title-color: ${Config.STYLES.COLORS.TITLE_LIGHT}; --gemini-button-bg: ${Config.STYLES.COLORS.BUTTON_BG_LIGHT}; --gemini-button-border: ${Config.STYLES.COLORS.BUTTON_BORDER_LIGHT}; --gemini-code-block-bg: ${Config.STYLES.COLORS.CODE_BLOCK_BG_LIGHT}; } .dark-mode { --gemini-background-color: ${Config.STYLES.COLORS.BACKGROUND_DARK}; --gemini-border-color: ${Config.STYLES.COLORS.BORDER_DARK}; --gemini-text-color: ${Config.STYLES.COLORS.TEXT_DARK}; --gemini-title-color: ${Config.STYLES.COLORS.TITLE_DARK}; --gemini-button-bg: ${Config.STYLES.COLORS.BUTTON_BG_DARK}; --gemini-button-border: ${Config.STYLES.COLORS.BUTTON_BORDER_DARK}; --gemini-code-block-bg: ${Config.STYLES.COLORS.CODE_BLOCK_BG_DARK}; } `; return cssVariables + styles.reduce((css, styleObj) => { for (const [selector, props] of Object.entries(styleObj)) { css += `${selector} {`; for (const [prop, value] of Object.entries(props)) { css += `${prop}: ${value};`; } css += '}'; } return css; }, ''); } }; // 테마 관리 모듈: 테마 변경 감지 및 적용 const ThemeManager = { currentTheme: 'light', // 초기 테마 설정 init() { const savedTheme = localStorage.getItem(Config.STORAGE_KEYS.THEME_MODE); if (savedTheme) { this.currentTheme = savedTheme; } else { // 시스템 테마 감지 (선택 사항) if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { this.currentTheme = 'dark'; } } this.applyTheme(); }, applyTheme() { if (this.currentTheme === 'dark') { document.documentElement.classList.add('dark-mode'); } else { document.documentElement.classList.remove('dark-mode'); } }, toggleTheme() { this.currentTheme = this.currentTheme === 'light' ? 'dark' : 'light'; localStorage.setItem(Config.STORAGE_KEYS.THEME_MODE, this.currentTheme); this.applyTheme(); this.updateThemeToggleButtonIcon(); }, getThemeToggleButtonIcon() { return this.currentTheme === 'light' ? Config.ASSETS.DARK_MODE_ICON : Config.ASSETS.LIGHT_MODE_ICON; }, updateThemeToggleButtonIcon() { const themeToggleButton = document.getElementById('gemini-theme-toggle-btn'); if (themeToggleButton) { themeToggleButton.src = this.getThemeToggleButtonIcon(); themeToggleButton.title = this.currentTheme === 'light' ? 'Dark Mode' : 'Light Mode'; } }, observeThemeChange() { // 외부 테마 변경 감지 (예: 시스템 설정 변경) - 필요에 따라 추가 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { const newTheme = e.matches ? 'dark' : 'light'; if (this.currentTheme !== newTheme) { this.currentTheme = newTheme; localStorage.setItem(Config.STORAGE_KEYS.THEME_MODE, this.currentTheme); this.applyTheme(); this.updateThemeToggleButtonIcon(); } }); } }; // 스타일 관리 모듈: 스타일 초기화 및 적용 const Styles = { initStyles() { const styleElement = document.createElement('style'); styleElement.id = 'bing-plus-styles'; styleElement.textContent = StyleGenerator.generateStyles(); // CSS 문자열 생성 document.head.appendChild(styleElement); //