您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Display Gemini response results next to Bing search results and speed up searches by eliminating intermediate URLs.
当前为
- // ==UserScript==
- // @name Bing Plus
- // @version 5.3
- // @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',
- 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 styleElement = document.createElement('style');
- styleElement.id = 'bing-plus-styles';
- styleElement.textContent = `
- #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;
- }
- #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;
- background: transparent !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;
- background: transparent !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;
- 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,
- [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_BLOCK_BG} !important;
- }
- @media (prefers-color-scheme: dark) {
- #gemini-content pre,
- #b_context #gemini-content pre,
- .b_right #gemini-content pre {
- background: ${Config.STYLES.COLORS.DARK_CODE_BLOCK_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;
- background: transparent !important;
- }
- #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;
- }
- @media (min-width: 769px) {
- #google-search-btn:hover {
- transform: scale(1.1);
- }
- #google-search-btn:hover img {
- transform: scale(1.1);
- }
- }
- #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;
- }
- }
- `;
- document.head.appendChild(styleElement);
- // 지역화 모듈
- 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 = {
- applyTheme() {
- 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(`Theme applied: ${currentTheme}`);
- }
- };
- // 유틸리티 모듈
- const Utils = {
- isDesktop() {
- return window.innerWidth > 768 && !/Mobi|Android/i.test(navigator.userAgent);
- },
- isGeminiAvailable() {
- const hasBContext = !!document.getElementById('b_context');
- const hasBRight = !!document.querySelector('.b_right');
- return this.isDesktop() && (hasBContext || hasBRight);
- },
- getQuery() {
- return new URLSearchParams(location.search).get('q');
- },
- 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 모듈
- 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.id = 'gemini-wrapper';
- wrapper.appendChild(this.createGoogleButton(query));
- wrapper.appendChild(this.createGeminiBox(query, apiKey));
- return wrapper;
- }
- };
- // Gemini API 모듈
- const GeminiAPI = {
- fetch(query, container, apiKey, force = false) {
- VersionChecker.checkMarkedJsVersion();
- const cacheKey = `${Config.CACHE.PREFIX}${query}`;
- const cached = force ? null : sessionStorage.getItem(cacheKey);
- if (cached) {
- if (container) {
- container.innerHTML = marked.parse(cached);
- }
- return;
- }
- if (container) {
- 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);
- if (container) {
- container.innerHTML = marked.parse(text);
- }
- } else {
- if (container) {
- container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.GEMINI_EMPTY);
- }
- }
- } catch (e) {
- if (container) {
- container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.PARSE_ERROR)} ${e.message}`;
- }
- }
- },
- onerror: err => {
- if (container) {
- container.textContent = `${Localization.getMessage(Config.MESSAGE_KEYS.NETWORK_ERROR)} ${err.finalUrl}`;
- }
- },
- ontimeout: () => {
- if (container) {
- container.textContent = Localization.getMessage(Config.MESSAGE_KEYS.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;
- });
- }
- };
- // 버전 확인 모듈
- 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;
- localStorage.setItem(Config.STORAGE_KEYS.LATEST_VERSION, latest);
- const lastNotified = localStorage.getItem(Config.STORAGE_KEYS.LAST_NOTIFIED);
- if (this.compareVersions(Config.VERSIONS.MARKED_VERSION, latest) < 0 &&
- (!lastNotified || this.compareVersions(lastNotified, latest) < 0)) {
- const existingPopup = document.getElementById('marked-update-popup');
- if (existingPopup) existingPopup.remove();
- 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);
- popup.remove();
- };
- document.body.appendChild(popup);
- }
- } catch (e) {
- console.warn('marked.min.js version check error:', e.message);
- }
- },
- onerror: () => console.warn('marked.min.js version check request failed')
- });
- }
- };
- // 메인 모듈
- const Main = {
- isRendering: false, // 렌더링 중복 방지 플래그
- renderGemini() {
- if (this.isRendering) return; // 이미 렌더링 중이면 중단
- this.isRendering = true;
- const query = Utils.getQuery();
- if (!query) {
- this.isRendering = false;
- return;
- }
- // 기존 요소 제거
- const existingWrapper = document.getElementById('gemini-wrapper');
- if (existingWrapper) {
- existingWrapper.remove();
- }
- const existingGoogleBtn = document.getElementById('google-search-btn');
- if (existingGoogleBtn) {
- existingGoogleBtn.remove();
- }
- if (Utils.isDesktop()) {
- if (!Utils.isGeminiAvailable()) {
- this.isRendering = false;
- return;
- }
- const apiKey = Utils.getApiKey();
- if (!apiKey) {
- this.isRendering = false;
- return;
- }
- const contextTarget = document.getElementById('b_context') || document.querySelector('.b_right');
- if (!contextTarget) {
- this.isRendering = false;
- return;
- }
- // Google 버튼 먼저 렌더링
- requestAnimationFrame(() => {
- const wrapper = UI.createGeminiUI(query, apiKey);
- contextTarget.prepend(wrapper);
- // Gemini 박스 렌더링은 유휴 시간에
- window.requestIdleCallback(() => {
- const content = wrapper.querySelector('#gemini-content');
- if (content) {
- const cache = sessionStorage.getItem(`${Config.CACHE.PREFIX}${query}`);
- if (cache) {
- content.innerHTML = marked.parse(cache);
- } else {
- window.requestIdleCallback(() => GeminiAPI.fetch(query, content, apiKey));
- }
- }
- this.isRendering = false;
- });
- });
- } else {
- const contentTarget = document.getElementById('b_content');
- if (!contentTarget) {
- this.isRendering = false;
- return;
- }
- requestAnimationFrame(() => {
- const googleBtn = UI.createGoogleButton(query);
- contentTarget.parentNode.insertBefore(googleBtn, contentTarget);
- this.isRendering = false;
- });
- }
- },
- observeUrlChange() {
- let lastUrl = location.href;
- const checkUrlChange = () => {
- if (location.href !== lastUrl) {
- lastUrl = location.href;
- this.renderGemini();
- LinkCleaner.convertLinksToReal(document);
- }
- };
- // History API 이벤트 감지
- const originalPushState = history.pushState;
- history.pushState = function (...args) {
- originalPushState.apply(this, args);
- checkUrlChange();
- };
- const originalReplaceState = history.replaceState;
- history.replaceState = function (...args) {
- originalReplaceState.apply(this, args);
- checkUrlChange();
- };
- window.addEventListener('popstate', () => {
- checkUrlChange();
- });
- // Fallback: DOM 변경 감지
- const observer = new MutationObserver(() => {
- checkUrlChange();
- });
- const targetNode = document.querySelector('head > title') || document.body;
- observer.observe(targetNode, { childList: true, subtree: true });
- },
- observeThemeChange() {
- const themeObserver = new MutationObserver(() => {
- Styles.applyTheme();
- });
- themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme', 'class'] });
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
- Styles.applyTheme();
- });
- const contextObserver = new MutationObserver(() => {
- Styles.applyTheme();
- });
- const targetElement = document.querySelector('#b_context') || document.querySelector('.b_right');
- if (targetElement) {
- contextObserver.observe(targetElement, { attributes: true, attributeFilter: ['style', 'class'] });
- }
- },
- init() {
- const initialize = () => {
- Styles.applyTheme();
- LinkCleaner.convertLinksToReal(document);
- this.renderGemini();
- this.observeUrlChange();
- this.observeThemeChange();
- };
- if (document.readyState === 'complete' || document.readyState === 'interactive') {
- setTimeout(initialize, 1);
- } else {
- document.addEventListener('DOMContentLoaded', initialize);
- }
- }
- };
- Main.init();
- })();