// ==UserScript==
// @name Bing Plus
// @version 5.0
// @description Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.
// @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/
// ==/UserScript==
(function () {
'use strict';
// 설정 모듈
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',
CODE_BG: '#f5f5f5',
BUTTON_BORDER: '#ccc',
DARK_BACKGROUND: '#202124',
DARK_BORDER: '#5f6368',
DARK_CODE_BG: '#2d2d2d',
DARK_TEXT: '#fff'
},
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 Styles = {
inject() {
console.log('Injecting styles...');
const currentTheme = document.documentElement.getAttribute('data-theme') ||
(document.documentElement.classList.contains('dark') ||
document.documentElement.classList.contains('b_dark')) ? 'dark' :
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
console.log(`Current theme: ${currentTheme}`);
GM_addStyle(`
#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: none !important;
}
#b_context #gemini-box,
.b_right #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;
}
[data-theme="light"] #b_context #gemini-box,
[data-theme="light"] .b_right #gemini-box,
.light #b_context #gemini-box,
.light .b_right #gemini-box {
background: ${Config.STYLES.COLORS.BACKGROUND} !important;
border: 1px solid ${Config.STYLES.COLORS.BORDER} !important;
border-style: solid !important;
border-width: 1px !important;
}
[data-theme="light"] #b_context #gemini-box h3,
[data-theme="light"] .b_right #gemini-box h3,
.light #b_context #gemini-box h3,
.light .b_right #gemini-box h3 {
color: ${Config.STYLES.COLORS.TITLE} !important;
}
[data-theme="light"] #b_context #gemini-content,
[data-theme="light"] #b_context #gemini-content *,
[data-theme="light"] .b_right #gemini-content,
[data-theme="light"] .b_right #gemini-content *,
.light #b_context #gemini-content,
.light #b_context #gemini-content *,
.light .b_right #gemini-content,
.light .b_right #gemini-content * {
color: ${Config.STYLES.COLORS.TEXT} !important;
}
[data-theme="light"] #b_context #gemini-content pre,
[data-theme="light"] .b_right #gemini-content pre,
.light #b_context #gemini-content pre,
.light .b_right #gemini-content pre {
background: ${Config.STYLES.COLORS.CODE_BG} !important;
}
[data-theme="light"] #b_context #gemini-divider,
[data-theme="light"] .b_right #gemini-divider,
.light #b_context #gemini-divider,
.light .b_right #gemini-divider {
background: ${Config.STYLES.COLORS.BORDER} !important;
}
[data-theme="dark"] #b_context #gemini-box,
[data-theme="dark"] .b_right #gemini-box,
.dark #b_context #gemini-box,
.dark .b_right #gemini-box,
.b_dark #b_context #gemini-box,
.b_dark .b_right #gemini-box {
background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
border: 1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important;
border-style: solid !important;
border-width: 1px !important;
}
@media (prefers-color-scheme: dark) {
#b_context #gemini-box,
.b_right #gemini-box {
background: ${Config.STYLES.COLORS.DARK_BACKGROUND} !important;
border: 1px solid ${Config.STYLES.COLORS.DARK_BORDER} !important;
border-style: solid !important;
border-width: 1px !important;
}
}
[data-theme="dark"] #b_context #gemini-box h3,
[data-theme="dark"] .b_right #gemini-box h3,
.dark #b_context #gemini-box h3,
.dark .b_right #gemini-box h3,
.b_dark #b_context #gemini-box h3,
.b_dark .b_right #gemini-box h3 {
color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
}
@media (prefers-color-scheme: dark) {
#b_context #gemini-box h3,
.b_right #gemini-box h3 {
color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
}
}
[data-theme="dark"] #b_context #gemini-content,
[data-theme="dark"] #b_context #gemini-content *,
[data-theme="dark"] .b_right #gemini-content,
[data-theme="dark"] .b_right #gemini-content *,
.dark #b_context #gemini-content,
.dark #b_context #gemini-content *,
.dark .b_right #gemini-content,
.dark .b_right #gemini-content *,
.b_dark #b_context #gemini-content,
.b_dark #b_context #gemini-content *,
.b_dark .b_right #gemini-content,
.b_dark .b_right #gemini-content * {
color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
}
@media (prefers-color-scheme: dark) {
#b_context #gemini-content,
#b_context #gemini-content *,
.b_right #gemini-content,
.b_right #gemini-content * {
color: ${Config.STYLES.COLORS.DARK_TEXT} !important;
}
}
[data-theme="dark"] #b_context #gemini-content pre,
[data-theme="dark"] .b_right #gemini-content pre,
.dark #b_context #gemini-content pre,
.dark .b_right #gemini-content pre,
.b_dark #b_context #gemini-content pre,
.b_dark .b_right #gemini-content pre {
background: ${Config.STYLES.COLORS.DARK_CODE_BG} !important;
}
@media (prefers-color-scheme: dark) {
#b_context #gemini-content pre,
.b_right #gemini-content pre {
background: ${Config.STYLES.COLORS.DARK_CODE_BG} !important;
}
}
[data-theme="dark"] #b_context #gemini-divider,
[data-theme="dark"] .b_right #gemini-divider,
.dark #b_context #gemini-divider,
.dark .b_right #gemini-divider,
.b_dark #b_context #gemini-divider,
.b_dark .b_right #gemini-divider {
background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
}
@media (prefers-color-scheme: dark) {
#b_context #gemini-divider,
.b_right #gemini-divider {
background: ${Config.STYLES.COLORS.DARK_BORDER} !important;
}
}
#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;
}
#gemini-content {
font-size: ${Config.STYLES.FONT_SIZE.TEXT};
line-height: 1.6;
white-space: pre-wrap;
word-wrap: break-word;
}
#gemini-content pre {
padding: ${Config.UI.DEFAULT_MARGIN + 2}px;
border-radius: ${Config.STYLES.BORDER_RADIUS};
overflow-x: auto;
}
#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;
}
#google-search-btn img {
width: ${Config.STYLES.SMALL_ICON_SIZE};
height: ${Config.STYLES.SMALL_ICON_SIZE};
vertical-align: middle;
}
#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;
}
@media (prefers-color-scheme: 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;
}
@media (max-width: 768px) {
#google-search-btn {
max-width: 96%;
margin: ${Config.UI.DEFAULT_MARGIN}px auto;
padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
border-radius: 16px;
}
#gemini-box {
padding: ${Config.UI.DEFAULT_PADDING * 0.75}px;
border-radius: 16px;
}
}
`);
console.log('Styles injected', {
light: {
background: Config.STYLES.COLORS.BACKGROUND,
text: Config.STYLES.COLORS.TEXT,
title: Config.STYLES.COLORS.TITLE,
border: Config.STYLES.COLORS.BORDER
},
dark: {
background: Config.STYLES.COLORS.DARK_BACKGROUND,
text: Config.STYLES.COLORS.DARK_TEXT,
border: Config.STYLES.COLORS.DARK_BORDER
}
});
// 계산된 스타일 디버깅
setTimeout(() => {
const geminiBox = document.querySelector('#b_context #gemini-box') ||
document.querySelector('.b_right #gemini-box');
const content = document.querySelector('#b_context #gemini-content') ||
document.querySelector('.b_right #gemini-content');
const bContext = document.querySelector('#b_context');
const bContextParent = document.querySelector('.b_context');
const bRight = document.querySelector('.b_right');
if (geminiBox && content && (bContext || bRight)) {
const computedBoxStyle = window.getComputedStyle(geminiBox);
const computedContentStyle = window.getComputedStyle(content);
const computedBContextStyle = bContext ? window.getComputedStyle(bContext) : null;
const computedBContextParentStyle = bContextParent ? window.getComputedStyle(bContextParent) : null;
const computedBRightStyle = bRight ? window.getComputedStyle(bRight) : null;
console.log('Computed styles:', {
geminiBox: {
background: computedBoxStyle.backgroundColor,
border: computedBoxStyle.border,
borderStyle: computedBoxStyle.borderStyle,
borderWidth: computedBoxStyle.borderWidth,
borderColor: computedBoxStyle.borderColor
},
geminiContent: {
color: computedContentStyle.color,
children: Array.from(content.children).map(child => ({
tag: child.tagName,
color: window.getComputedStyle(child).color
}))
},
bContext: bContext ? {
color: computedBContextStyle.color,
border: computedBContextStyle.border,
borderStyle: computedBContextStyle.borderStyle,
borderWidth: computedBContextStyle.borderWidth,
borderColor: computedBContextStyle.borderColor
} : null,
bContextParent: bContextParent ? {
color: computedBContextParentStyle.color,
border: computedBContextParentStyle.border,
borderStyle: computedBContextParentStyle.borderStyle,
borderWidth: computedBContextParentStyle.borderWidth,
borderColor: computedBContextParentStyle.borderColor
} : null,
bRight: bRight ? {
color: computedBRightStyle.color,
border: computedBRightStyle.border,
borderStyle: computedBRightStyle.borderStyle,
borderWidth: computedBRightStyle.borderWidth,
borderColor: computedBRightStyle.borderColor
} : null
});
} else {
console.log('Elements not found for computed style check', {
geminiBox: !!geminiBox,
content: !!content,
bContext: !!bContext,
bContextParent: !!bContextParent,
bRight: !!bRight
});
}
}, 2000); // 2초 지연으로 DOM 로드 대기
}
};
// 유틸리티 모듈
const Utils = {
isDesktop() {
const isDesktop = window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent);
console.log('isDesktop:', { width: window.innerWidth, userAgent: navigator.userAgent, result: isDesktop });
return isDesktop;
},
isGeminiAvailable() {
const hasBContext = !!document.getElementById('b_context');
const hasBRight = !!document.querySelector('.b_right');
console.log('Bing isGeminiAvailable:', { isDesktop: this.isDesktop(), hasBContext, hasBRight });
return this.isDesktop() && (hasBContext || hasBRight);
},
getQuery() {
const query = new URLSearchParams(location.search).get('q');
console.log('getQuery:', { query, search: location.search });
return query;
},
getApiKey() {
let key = localStorage.getItem('geminiApiKey');
if (!key) {
key = prompt(Localization.getMessage(Config.MESSAGE_KEYS.ENTER_API_KEY));
if (key) localStorage.setItem('geminiApiKey', key);
console.log('API key:', key ? 'stored' : 'prompt failed');
} else {
console.log('API key retrieved');
}
return key;
}
};
// UI 모듈
const UI = {
createGoogleButton(query) {
const btn = document.createElement('button');
btn.id = 'google-search-btn';
btn.innerHTML = `
<img src="${Config.ASSETS.GOOGLE_LOGO}" alt="Google Logo">
${Localization.getMessage(Config.MESSAGE_KEYS.SEARCH_ON_GOOGLE)}
`;
btn.onclick = () => window.open(`https://www.google.com/search?q=${encodeURIComponent(query)}`, '_blank');
return btn;
},
createGeminiBox(query, apiKey) {
const box = document.createElement('div');
box.id = 'gemini-box';
box.innerHTML = `
<div id="gemini-header">
<div id="gemini-title-wrap">
<img id="gemini-logo" src="${Config.ASSETS.GEMINI_LOGO}" alt="Gemini Logo">
<h3>Gemini Search Results</h3>
</div>
<img id="gemini-refresh-btn" title="Refresh" src="${Config.ASSETS.REFRESH_ICON}" />
</div>
<hr id="gemini-divider">
<div id="gemini-content">${Localization.getMessage(Config.MESSAGE_KEYS.LOADING)}</div>
`;
box.querySelector('#gemini-refresh-btn').onclick = () => GeminiAPI.fetch(query, box.querySelector('#gemini-content'), apiKey, true);
return box;
},
createGeminiUI(query, apiKey) {
const wrapper = document.createElement('div');
wrapper.appendChild(this.createGoogleButton(query));
wrapper.appendChild(this.createGeminiBox(query, apiKey));
console.log('Gemini UI created:', { query, hasApiKey: !!apiKey });
return wrapper;
}
};
// Gemini API 모듈
const GeminiAPI = {
fetch(query, container, apiKey, force = false) {
console.log('Fetching Gemini API:', { query, force });
VersionChecker.checkMarkedJsVersion();
const cacheKey = `${Config.CACHE.PREFIX}${query}`;
if (!force) {
const cached = sessionStorage.getItem(cacheKey);
if (cached) {
container.innerHTML = marked.parse(cached);
console.log('Loaded from cache:', { query });
return;
}
}
container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.LOADING);
GM_xmlhttpRequest({
method: 'POST',
url: `${Config.API.GEMINI_URL}${Config.API.GEMINI_MODEL}:generateContent?key=${apiKey}`,
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({
contents: [{
parts: [{ text: Localization.getMessage(Config.MESSAGE_KEYS.PROMPT, { query }) }]
}]
}),
onload({ responseText }) {
try {
const text = JSON.parse(responseText)?.candidates?.[0]?.content?.parts?.[0]?.text;
if (text) {
sessionStorage.setItem(cacheKey, text);
container.innerHTML = marked.parse(text);
console.log('Gemini API success:', { query });
} else {
container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY);
console.log('Gemini API empty response');
}
} catch (e) {
container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`;
console.error('Gemini API parse error:', e.message);
}
},
onerror: err => {
container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl}`;
console.error('Gemini API network error:', err);
},
ontimeout: () => {
container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.TIMEOUT);
console.error('Gemini API timeout');
}
});
}
};
// 링크 정리 모듈
const LinkCleaner = {
decodeRealUrl(url, key) {
const param = new URL(url).searchParams.get(key)?.replace(/^a1/, '');
if (!param) return null;
try {
const decoded = decodeURIComponent(atob(param.replace(/_/g, '/').replace(/-/g, '+')));
return decoded.startsWith('/') ? location.origin + decoded : decoded;
} catch {
return null;
}
},
resolveRealUrl(url) {
const rules = [
{ pattern: /bing\.com\/(ck\/a|aclick)/, key: 'u' },
{ pattern: /so\.com\/search\/eclk/, key: 'aurl' }
];
for (const { pattern, key } of rules) {
if (pattern.test(url)) {
const real = this.decodeRealUrl(url, key);
if (real && real !== url) return real;
}
}
return url;
},
convertLinksToReal(root) {
root.querySelectorAll('a[href]').forEach(a => {
const realUrl = this.resolveRealUrl(a.href);
if (realUrl && realUrl !== a.href) a.href = realUrl;
});
console.log('Links converted');
}
};
// 버전 확인 모듈
const VersionChecker = {
compareVersions(current, latest) {
const currentParts = current.split('.').map(Number);
const latestParts = latest.split('.').map(Number);
for (let i = 0; i < Math.max(currentParts.length, latestParts.length); i++) {
const c = currentParts[i] || 0;
const l = latestParts[i] || 0;
if (c < l) return -1;
if (c > l) return 1;
}
return 0;
},
checkMarkedJsVersion() {
localStorage.setItem(Config.STORAGE_KEYS.CURRENT_VERSION, Config.VERSIONS.MARKED_VERSION);
GM_xmlhttpRequest({
method: 'GET',
url: Config.API.MARKED_CDN_URL,
onload({ responseText }) {
try {
const latest = JSON.parse(responseText).version;
console.log(`marked.js version: current=${Config.VERSIONS.MARKED_VERSION}, latest=${latest}`);
localStorage.setItem(Config.STORAGE_KEYS.LATEST_VERSION, latest);
const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED);
console.log(`Last notified version: ${lastNotified || 'none'}`);
if (this.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 &&
(!lastNotified || this.compareVersions(lastNotified, latest) < 0)) {
console.log('Popup display condition met');
const existingPopup = document.getElementById('marked-update-popup');
if (existingPopup) {
existingPopup.remove();
console.log('Existing popup removed');
}
const popup = document.createElement('div');
popup.id = 'marked-update-popup';
popup.innerHTML = `
<p><b>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_TITLE)}</b></p>
<p>Current: ${Config.VERSIONS.MARKED_VERSION}<br>Latest: ${latest}</p>
<button>${Localization.getMessage(Config.MESSAGE_KEYS.UPDATE_NOW)}</button>
`;
popup.querySelector('button').onclick = () => {
localStorage.setItem(Config.STORAGE_KEYS.LAST_NOTIFIED, latest);
console.log(`Notified version recorded: ${latest}`);
popup.remove();
};
document.body.appendChild(popup);
console.log('New popup displayed');
} else {
console.log('Popup display condition not met');
}
} catch (e) {
console.warn('marked.min.js version check error:', e.message);
}
},
onerror: () => console.warn('marked.min.js version check request failed')
});
}
};
// 메인 모듈
const Main = {
renderGemini() {
console.log('renderGemini called');
const query = Utils.getQuery();
if (!query || document.getElementById('google-search-btn')) {
console.log('Skipped:', { queryExists: !!query, googleBtnExists: !!document.getElementById('google-search-btn') });
return;
}
if (Utils.isDesktop()) {
if (!Utils.isGeminiAvailable()) {
console.log('Skipped PC: isGeminiAvailable false');
return;
}
const apiKey = Utils.getApiKey();
if (!apiKey) {
console.log('Skipped PC: No API key');
return;
}
const contextTarget = document.getElementById('b_context') ||
document.querySelector('.b_right');
if (!contextTarget) {
console.error('Target element (#b_context or .b_right) not found for PC UI insertion');
return;
}
const ui = UI.createGeminiUI(query, apiKey);
contextTarget.prepend(ui);
console.log('PC: Gemini UI (with Google button) inserted into target element');
const content = ui.querySelector('#gemini-content');
const cache = sessionStorage.getItem(`${Config.CACHE.PREFIX}${query}`);
content.innerHTML = cache ? marked.parse(cache) : Localization.getMessage(Config.MESSAGE_KEYS.LOADING);
if (!cache) GeminiAPI.fetch(query, content, apiKey);
// Gemini 박스 삽입 여부 확인
const geminiBox = document.querySelector('#gemini-box');
console.log('Gemini box inserted:', !!geminiBox);
} else {
const contentTarget = document.getElementById('b_content');
if (!contentTarget) {
console.error('b_content not found for mobile Google button insertion');
return;
}
const googleBtn = UI.createGoogleButton(query);
contentTarget.parentNode.insertBefore(googleBtn, contentTarget);
console.log('Mobile: Google search button inserted before b_content');
}
},
observeUrlChange() {
let lastUrl = location.href;
const observer = new MutationObserver(() => {
if (location.href !== lastUrl) {
lastUrl = location.href;
console.log('MutationObserver triggered: URL changed');
this.renderGemini();
LinkCleaner.convertLinksToReal(document);
}
});
observer.observe(document.body, { childList: true, subtree: true });
console.log('Observing URL changes on document.body');
},
observeThemeChange() {
const themeObserver = new MutationObserver(() => {
const newTheme = document.documentElement.getAttribute('data-theme') ||
(document.documentElement.classList.contains('dark') ||
document.documentElement.classList.contains('b_dark')) ? 'dark' :
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
console.log(`Theme changed: ${newTheme}`);
Styles.inject();
});
themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
// 시스템 테마 변경 감지
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
const newTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
console.log(`System theme changed: ${newTheme}`);
Styles.inject();
});
// 타겟 요소 스타일 변경 감지
const contextObserver = new MutationObserver(() => {
console.log('Target element style changed, reapplying styles');
Styles.inject();
});
const targetElement = document.querySelector('#b_context') ||
document.querySelector('.b_right');
if (targetElement) {
contextObserver.observe(targetElement, { attributes: true, attributeFilter: ['style', 'class'] });
}
console.log('Observing theme and style changes');
},
waitForElement(selector, callback, maxAttempts = 20, interval = 500) {
let attempts = 0;
const checkElement = () => {
const element = document.querySelector(selector);
if (element) {
console.log(`Element found: ${selector}`);
callback(element);
} else if (attempts < maxAttempts) {
attempts++;
console.log(`Waiting for element: ${selector}, attempt ${attempts}/${maxAttempts}`);
setTimeout(checkElement, interval);
} else {
console.error(`Element not found after ${maxAttempts} attempts: ${selector}`);
}
};
checkElement();
},
init() {
console.log('Bing Plus init:', { hostname: location.hostname, url: location.href });
try {
// 페이지 로드 완료 후 타겟 요소 대기
this.waitForElement('#b_context, .b_right, #b_content', () => {
Styles.inject();
LinkCleaner.convertLinksToReal(document);
this.renderGemini();
this.observeUrlChange();
this.observeThemeChange();
// DOM 구조 디버깅
const bContext = document.getElementById('b_context');
const bContextParent = document.querySelector('.b_context');
const bRight = document.querySelector('.b_right');
const bContent = document.getElementById('b_content');
console.log('DOM structure debugging:', {
bContextExists: !!bContext,
bContextParentExists: !!bContextParent,
bRightExists: !!bRight,
bContentExists: !!bContent
});
});
} catch (e) {
console.error('Init error:', e.message);
}
}
};
console.log('Bing Plus script loaded');
Main.init();
})();