// ==UserScript==
// @name Bing Plus
// @version 6.2
// @description Add Gemini response, improve speed to search results, add Google search buttons
// @author lanpod
// @match https://www.bing.com/search*
// @grant GM_addStyle
// @grant GM_xmlhttpRequest
// @require https://cdnjs.cloudflare.com/ajax/libs/marked/15.0.7/marked.min.js
// @license MIT
// @namespace http://tampermonkey.net/
// @downloadURL https://update.greasyfork.icu/scripts/530097/Bing%20Plus.user.js
// @updateURL https://update.greasyfork.icu/scripts/530097/Bing%20Plus.meta.js
// ==/UserScript==
(function () {
'use strict';
// 설정 모듈: 전역 설정값 관리 (API, 스타일, 메시지 등)
const Config = {
API: {
GEMINI_MODEL: 'gemini-2.0-flash',
GEMINI_URL: 'https://generativelanguage.googleapis.com/v1beta/models/',
MARKED_CDN_URL: 'https://api.cdnjs.com/libraries/marked'
},
VERSIONS: {
MARKED_VERSION: '15.0.7'
},
CACHE: {
PREFIX: 'gemini_cache_'
},
STORAGE_KEYS: {
CURRENT_VERSION: 'markedCurrentVersion',
LATEST_VERSION: 'markedLatestVersion',
LAST_NOTIFIED: 'markedLastNotifiedVersion'
},
UI: {
DEFAULT_MARGIN: 8,
DEFAULT_PADDING: 16,
Z_INDEX: 9999
},
STYLES: {
COLORS: {
BACKGROUND: '#fff',
BORDER: '#e0e0e0',
TEXT: '#000',
TITLE: '#000',
BUTTON_BG: '#f0f3ff',
BUTTON_BORDER: '#ccc',
DARK_BACKGROUND: '#202124',
DARK_BORDER: '#5f6368',
DARK_TEXT: '#fff',
CODE_BLOCK_BG: '#f0f0f0',
DARK_CODE_BLOCK_BG: '#555'
},
BORDER: '1px solid #e0e0e0',
BORDER_RADIUS: '4px',
FONT_SIZE: {
TEXT: '14px',
TITLE: '18px'
},
ICON_SIZE: '20px',
LOGO_SIZE: '24px',
SMALL_ICON_SIZE: '16px'
},
ASSETS: {
GOOGLE_LOGO: 'https://www.gstatic.com/marketing-cms/assets/images/bc/1a/a310779347afa1927672dc66a98d/g.png=s48-fcrop64=1,00000000ffffffff-rw',
GEMINI_LOGO: 'https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg',
REFRESH_ICON: 'https://www.svgrepo.com/show/533704/refresh-cw-alt-3.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'
}
};
// 지역화 모듈: 다국어 메시지 처리
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'
}
},
// 주어진 키와 변수를 사용하여 지역화된 메시지 반환
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;
// 1. UserAgent 기반으로 기본 판별
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);
// 2. 디바이스 타입 판별 로직
if (isWindows && !isTouchDevice && width > 1024) {
// Windows 기반이고 터치 디바이스가 아니며 화면이 넓으면 데스크톱으로 판별
deviceType = 'desktop';
} else if ((isAndroid || isIPhone) && hasMobileKeyword) {
// Android 또는 iPhone이고 "Mobile" 키워드가 있으면 모바일
deviceType = 'mobile';
} else if (isAndroid && !hasMobileKeyword && width >= 768) {
// Android이지만 "Mobile" 키워드가 없고 화면 크기가 태블릿 수준이면 태블릿
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';
},
// Gemini UI를 표시할 수 있는 환경인지 확인 (데스크톱 환경에서만 가능)
isGeminiAvailable() {
if (this._cache.isGeminiAvailable === null) {
const hasBContext = !!document.getElementById('b_context');
const hasBRight = !!document.querySelector('.b_right');
this._cache.isGeminiAvailable = this.isDesktop() && (hasBContext || hasBRight);
}
return this._cache.isGeminiAvailable;
},
// 캐시 초기화
resetCache() {
this._cache = {
deviceType: null,
isGeminiAvailable: null
};
}
};
// 스타일 생성 모듈: 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'
}
},
// Gemini 박스 스타일 정의
geminiBoxStyles: {
'#gemini-box': {
'width': '100%',
'max-width': '100%',
'background': `${Config.STYLES.COLORS.BACKGROUND} !important`,
'border': `${Config.STYLES.BORDER} !important`,
'border-style': 'solid !important',
'border-width': '1px !important',
'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: {
'[data-theme="light"] #gemini-box, .light #gemini-box': {
'background': `${Config.STYLES.COLORS.BACKGROUND} !important`,
'border': `1px solid ${Config.STYLES.COLORS.BORDER} !important`
},
'[data-theme="light"] #gemini-box h3, .light #gemini-box h3': {
'color': `${Config.STYLES.COLORS.TITLE} !important`
},
'[data-theme="light"] #gemini-content, [data-theme="light"] #gemini-content *, .light #gemini-content, .light #gemini-content *': {
'color': `${Config.STYLES.COLORS.TEXT} !important`,
'background': 'transparent !important'
},
'[data-theme="light"] #gemini-divider, .light #gemini-divider': {
'background': `${Config.STYLES.COLORS.BORDER} !important`
},
'[data-theme="dark"] #gemini-box, .dark #gemini-box, .b_dark #gemini-box': {
'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
'border': `1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important`
},
'@media (prefers-color-scheme: dark)': {
'#gemini-box': {
'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
'border': `1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important`
},
'#gemini-box h3': {
'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`
},
'#gemini-content, #gemini-content *': {
'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`,
'background': 'transparent !important'
},
'#gemini-content pre': {
'background': `${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important`
},
'#gemini-divider': {
'background': `${Config.STYLES.COLORS.DARK_BORDER} !important`
},
'#marked-update-popup': {
'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`
}
}
},
// Gemini 콘텐츠 스타일 정의
contentStyles: {
'#gemini-content': {
'font-size': Config.STYLES.FONT_SIZE.TEXT,
'line-height': '1.6',
'white-space': 'pre-wrap',
'word-wrap': 'break-word',
'background': 'transparent !important'
},
'#gemini-content pre': {
'background': `${Config.STYLES.COLORS.CODE_BLOCK_BG} !important`,
'padding': `${Config.UI.DEFAULT_MARGIN + 2}px`,
'border-radius': Config.STYLES.BORDER_RADIUS,
'overflow-x': 'auto'
},
'[data-theme="dark"] #gemini-content pre, .dark #gemini-content pre, .b_dark #gemini-content pre': {
'background': `${Config.STYLES.COLORS.DARK_CODE_BLOCK_BG} !important`
}
},
// 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'
},
'#gemini-refresh-btn:hover': {
'opacity': '1',
'transform': 'rotate(360deg)'
},
'#gemini-divider': {
'height': '1px',
'margin': `${Config.UI.DEFAULT_MARGIN}px 0`
}
},
// Google 검색 버튼 스타일 정의
googleButtonStyles: {
'#google-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': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`,
'border-radius': Config.STYLES.BORDER_RADIUS,
'background-color': Config.STYLES.COLORS.BUTTON_BG,
'color': Config.STYLES.COLORS.TITLE,
'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': {
'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': {
'transform': 'scale(1.1)'
},
'.desktop-useragent #google-search-btn:hover img': {
'transform': 'scale(1.1)'
}
},
// 업데이트 팝업 스타일 정의
popupStyles: {
'#marked-update-popup': {
'position': 'fixed',
'top': '30%',
'left': '50%',
'transform': 'translate(-50%, -50%)',
'background': Config.STYLES.COLORS.BACKGROUND,
'padding': `${Config.UI.DEFAULT_PADDING * 1.25}px`,
'z-index': Config.UI.Z_INDEX,
'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`,
'box-shadow': '0 2px 10px rgba(0,0,0,0.1)',
'text-align': 'center'
},
'[data-theme="dark"] #marked-update-popup, .dark #marked-update-popup, .b_dark #marked-update-popup': {
'background': `${Config.STYLES.COLORS.DARK_BACKGROUND} !important`,
'color': `${Config.STYLES.COLORS.DARK_TEXT} !important`
},
'#marked-update-popup button': {
'margin-top': `${Config.UI.DEFAULT_MARGIN * 1.25}px`,
'padding': `${Config.UI.DEFAULT_MARGIN}px ${Config.UI.DEFAULT_PADDING}px`,
'cursor': 'pointer',
'border': `1px solid ${Config.STYLES.COLORS.BUTTON_BORDER}`,
'border-radius': Config.STYLES.BORDER_RADIUS,
'background-color': Config.STYLES.COLORS.BUTTON_BG,
'color': Config.STYLES.COLORS.TITLE,
'font-family': 'sans-serif'
}
},
// 모바일 환경 스타일 정의
mobileStyles: {
'.mobile-useragent #google-search-btn': {
'max-width': '100%',
'width': 'calc(100% - 16px)', // 좌우 마진 8px씩을 고려한 너비 계산
'margin-left': `${Config.UI.DEFAULT_MARGIN}px !important`, // 좌측 마진 8px
'margin-right': `${Config.UI.DEFAULT_MARGIN}px !important`, // 우측 마진 8px
'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.googleButtonStyles,
this.popupStyles,
this.mobileStyles
];
return 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 = {
// 테마 변경 감지 및 스타일 적용
applyTheme() {
const isDarkTheme = document.documentElement.getAttribute('data-theme') === 'dark' ||
document.documentElement.classList.contains('dark') ||
document.documentElement.classList.contains('b_dark') ||
window.matchMedia('(prefers-color-scheme: dark)').matches;
const geminiBox = document.querySelector('#gemini-box');
if (geminiBox) {
geminiBox.style.background = isDarkTheme
? Config.STYLES.COLORS.DARK_BACKGROUND
: Config.STYLES.COLORS.BACKGROUND;
geminiBox.style.borderColor = isDarkTheme
? Config.STYLES.COLORS.DARK_BORDER
: Config.STYLES.COLORS.BORDER;
}
},
// 테마 변경 감지 설정
observeThemeChange() {
const observer = new MutationObserver(() => this.applyTheme());
const targetElement = document.querySelector('#b_context') || document.querySelector('.b_right') || document.documentElement;
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
if (targetElement !== document.documentElement) {
observer.observe(targetElement, { attributes: true, attributeFilter: ['style', 'class'] });
}
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => this.applyTheme());
}
};
// 스타일 관리 모듈: 스타일 초기화 및 적용
const Styles = {
// 스타일 초기화
initStyles() {
const styleElement = document.createElement('style');
styleElement.id = 'bing-plus-styles';
styleElement.textContent = StyleGenerator.generateStyles();
document.head.appendChild(styleElement);
this.applyMobileStyles();
},
// 모바일 환경 스타일 적용
applyMobileStyles() {
if (DeviceDetector.isMobile()) {
document.documentElement.classList.add('mobile-useragent');
} else if (DeviceDetector.isDesktop()) {
document.documentElement.classList.add('desktop-useragent');
}
}
};
// 유틸리티 모듈: 공통 유틸리티 함수
const Utils = {
// 검색 쿼리 추출
getQuery() {
return new URLSearchParams(location.search).get('q');
},
// Gemini API 키 가져오기 또는 입력받기
getApiKey() {
let key = localStorage.getItem('geminiApiKey');
if (!key) {
key = prompt(Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY));
if (key) localStorage.setItem('geminiApiKey', key);
}
return key;
}
};
// UI 생성 모듈: DOM 요소 생성
const UI = {
// Google 검색 버튼 생성
createGoogleButton(query) {
const btn = document.createElement('button');
btn.id = 'google-search-btn';
btn.innerHTML = `
${Localization.getMessage(Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE)}
`;
btn.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(query)}`, '_blank');
return btn;
},
// Gemini 박스 생성
createGeminiBox(query, apiKey) {
const box = document.createElement('div');
box.id = 'gemini-box';
box.innerHTML = `
${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_TITLE)}
Current: ${Config.VERSIONS.MARKED_VERSION}
Latest: ${latest}