您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a draggable floating AI bubble to all webpages with an updated list of AI sites appearing above it on hover, with a delay and fade-out on mouse leave. Prevents image dragging. The bubble will not appear in the opened AI popup windows.
当前为
// ==UserScript== // @name AI Floating Bubble // @version 1.0 // @description Adds a draggable floating AI bubble to all webpages with an updated list of AI sites appearing above it on hover, with a delay and fade-out on mouse leave. Prevents image dragging. The bubble will not appear in the opened AI popup windows. // @author monit8280 // @match *://*/* // @grant GM_addStyle // @license MIT // @namespace http://tampermonkey.net/ // ==/UserScript== (function() { 'use strict'; // 현재 창이 AI 팝업 창인지 확인합니다. // URL에 'bubble_popup' 쿼리 파라미터가 있는지 검사하여 팝업 여부를 판단합니다. const urlParams = new URLSearchParams(window.location.search); const isAIPopup = urlParams.has('bubble_popup'); // 'ai_popup'에서 'bubble_popup'으로 변경됨 // 현재 창이 AI 팝업 창으로 감지되면 버블을 초기화하지 않고 스크립트 실행을 종료합니다. if (isAIPopup) { console.log("AI 플로팅 버블: AI 팝업 창으로 감지되어 버블을 초기화하지 않습니다."); return; // 스크립트 실행을 중단하여 팝업 창에 버블이 나타나지 않도록 합니다. } /** * @class AIIcons * AI 사이트 아이콘 URL을 관리하는 클래스입니다. * 각 AI 서비스에 사용될 아이콘 이미지의 URL을 정의합니다. */ class AIIcons { // 메인 버블 버튼에 사용될 아이콘 이미지 URL static get BUBBLE() { return "https://i.namu.wiki/i/LrJz7uHTAdFkV7Q0Cl4L8HPntexp6KUcqrZErhUrl-41Vk-IJ6n4K5TUQ_9WP0cNWECdZdegYID1KNnhHE7jX-xmjFFtpgozb7hTVlhwONvWu5lD_lF2hTF6Z0sktRBakk2-a-UCpeOn1Kx8Dn_Lg.webp"; } // Gemini AI 서비스 아이콘 URL static get GEMINI() { return "https://www.gstatic.com/lamda/images/gemini_sparkle_v002_d4735304ff6292a690345.svg"; } // ChatGPT AI 서비스 아이콘 URL static get CHATGPT() { return "https://chatgpt.com/favicon.ico"; } // Claude AI 서비스 아이콘 URL static get CLAUDE() { return "https://claude.ai/favicon.ico"; } // Copilot AI 서비스 아이콘 URL static get COPILOT() { return "https://copilot.microsoft.com/favicon.ico"; } // Grok AI 서비스 아이콘 URL static get GROK() { return "https://grok.com/favicon.ico"; } // Perplexity AI 서비스 아이콘 URL static get PERPLEXITY() { return "https://www.perplexity.ai/favicon.ico"; } // 아이콘 로드 실패 시 표시될 대체 이미지 (Placeholder) static get PLACEHOLDER() { return "https://placehold.co/16x16/cccccc/000000?text=AI"; } } /** * @class AISites * AI 사이트 이름과 URL 목록을 관리하는 클래스입니다. * 이 목록은 플로팅 버블 메뉴에 표시될 AI 서비스들의 정보를 담고 있습니다. * 목록 순서는 AI 목록 표시 순서와 동일하게 유지됩니다. */ class AISites { static get LIST() { return [ { name: "Perplexity", url: "https://www.perplexity.ai/", icon: AIIcons.PERPLEXITY }, { name: "Grok", url: "https://grok.com/", icon: AIIcons.GROK }, { name: "Gemini", url: "https://gemini.google.com/", icon: AIIcons.GEMINI }, { name: "Copilot", url: "https://copilot.microsoft.com/", icon: AIIcons.COPILOT }, { name: "Claude", url: "https://claude.ai/", icon: AIIcons.CLAUDE }, { name: "ChatGPT", url: "https://chatgpt.com/", icon: AIIcons.CHATGPT } ]; } } /** * @class BubbleConfig * 버블의 크기, 간격, 애니메이션 타이밍 등 모든 숫자형 설정 값을 관리하는 클래스입니다. * 모든 단위는 픽셀(px) 또는 밀리초(ms)입니다. * 이 값을 조정하여 버블의 외형과 동작을 커스터마이징할 수 있습니다. */ class BubbleConfig { static get BUBBLE_SIZE() { return 50; } // 버블 버튼의 너비와 높이 (px) static get OPTION_ICON_SIZE() { return 16; } // AI 목록 각 항목의 아이콘 크기 (px) static get OPTION_MENU_GAP() { return 10; } // 버블 버튼과 AI 목록 메뉴 사이의 간격 (px) static get OPTION_ITEM_PADDING_VERTICAL() { return 10; } // 각 AI 목록 항목의 상하 패딩 (px) static get OPTION_ITEM_PADDING_HORIZONTAL() { return 15; } // 각 AI 목록 항목의 좌우 패딩 (px) static get OPTION_ITEM_ICON_MARGIN_RIGHT() { return 10; } // AI 목록 항목의 아이콘과 텍스트 사이 간격 (px) static get OPTION_MENU_WIDTH() { return 150; } // AI 목록 메뉴의 고정 너비 (px) static get MENU_TRANSITION_DURATION() { return 0.3; } // 메뉴가 나타나고 사라지는 애니메이션 시간 (초) static get MENU_HIDE_DELAY() { return 200; } // 마우스가 메뉴에서 벗어난 후 메뉴가 숨겨지기까지의 지연 시간 (밀리초) static get POPUP_WINDOW_WIDTH() { return 800; } // 새 창으로 열릴 AI 사이트 팝업의 기본 너비 (px) static get POPUP_WINDOW_HEIGHT() { return 600; } // 새 창으로 열릴 AI 사이트 팝업의 기본 높이 (px) } /** * @class AIFloatingBubble * AI 플로팅 버블을 관리하는 메인 클래스입니다. * 이 클래스는 버블의 생성, 드래그 기능, AI 목록 메뉴 표시/숨김, * AI 사이트 클릭 시 새 창 열기 등의 모든 기능을 담당합니다. */ class AIFloatingBubble { constructor() { // DOM 요소 참조 변수 초기화 this.bubbleContainer = null; // 전체 버블 컨테이너 (드래그 가능 영역) this.bubbleButton = null; // 버블 아이콘이 표시되는 버튼 영역 this.siteOptions = null; // AI 사이트 목록 메뉴 영역 this.hideTimeout = null; // 메뉴 숨김 지연을 위한 타이머 ID this.isDragging = false; // 버블 드래그 중인지 여부 this.offsetX = 0; // 드래그 시작 시 마우스 X 오프셋 this.offsetY = 0; // 드래그 시작 시 마우스 Y 오프셋 this._init(); // 클래스 초기화 메서드 호출 } /** * @private * 초기화 메서드: DOM 요소 생성, 스타일 적용, 이벤트 리스너 설정. * 스크립트가 로드될 때 가장 먼저 호출됩니다. */ _init() { this._createElements(); // 필요한 HTML 요소들을 생성하고 문서에 추가합니다. this._applyStyles(); // 생성된 요소들에 CSS 스타일을 적용합니다. this._setupEventListeners(); // 드래그, 호버, 클릭 등의 이벤트 리스너를 설정합니다. } /** * @private * DOM 요소를 생성하고 문서에 추가합니다. * 플로팅 버블의 구조 (컨테이너, 버튼, 옵션 메뉴)를 만듭니다. */ _createElements() { // AI 플로팅 버블 컨테이너 요소 생성 this.bubbleContainer = document.createElement('div'); this.bubbleContainer.id = 'aiFloatingBubbleContainer'; // 초기 위치 설정 (화면 우측 하단에 배치) this.bubbleContainer.style.bottom = `${BubbleConfig.OPTION_ITEM_PADDING_VERTICAL * 2}px`; // 하단 패딩 확보 this.bubbleContainer.style.right = `${BubbleConfig.OPTION_ITEM_PADDING_HORIZONTAL}px`; // 우측 패딩 확보 document.body.appendChild(this.bubbleContainer); // body에 컨테이너 추가 // 플로팅 버블 버튼 요소 (메인 아이콘) 생성 this.bubbleButton = document.createElement('div'); this.bubbleButton.id = 'aiFloatingBubbleButton'; // 버블 아이콘 이미지를 설정합니다. this.bubbleButton.innerHTML = ` <img src="${AIIcons.BUBBLE}" alt="AI 아이콘" style="width: ${BubbleConfig.BUBBLE_SIZE * (2/3)}px; height: ${BubbleConfig.BUBBLE_SIZE * (2/3)}px;"> `; this.bubbleContainer.appendChild(this.bubbleButton); // 컨테이너 안에 버튼 추가 // AI 사이트 선택지 메뉴 요소 생성 this.siteOptions = document.createElement('div'); this.siteOptions.id = 'aiSiteOptions'; // AISites 클래스에서 AI 목록을 가져와 메뉴 HTML을 동적으로 생성합니다. let optionsHtml = ''; AISites.LIST.forEach(site => { optionsHtml += ` <div class="ai-option" data-url="${site.url}"> <img src="${site.icon}" alt="${site.name} 아이콘" class="option-icon" onerror="this.onerror=null;this.src='${AIIcons.PLACEHOLDER}';"> <span>${site.name}</span> </div> `; }); this.siteOptions.innerHTML = optionsHtml; // 생성된 HTML을 메뉴에 삽입 this.bubbleContainer.appendChild(this.siteOptions); // 컨테이너 안에 메뉴 추가 } /** * @private * 필요한 CSS 스타일을 문서에 동적으로 추가합니다. * Tampermonkey의 GM_addStyle 함수를 사용하여 전역 스타일을 적용합니다. * 모든 크기 관련 값은 BubbleConfig 클래스에서 가져옵니다. */ _applyStyles() { GM_addStyle(` /* 플로팅 버블 전체 컨테이너 스타일 */ #aiFloatingBubbleContainer { position: fixed; /* 화면에 고정 */ z-index: 9999; /* 다른 요소 위에 표시 */ width: ${BubbleConfig.BUBBLE_SIZE}px; height: ${BubbleConfig.BUBBLE_SIZE}px; cursor: grab; /* 드래그 가능함을 나타내는 커서 */ } /* 드래그 중일 때의 커서 스타일 */ #aiFloatingBubbleContainer.grabbing { cursor: grabbing; } /* 플로팅 버블 버튼 (원형 아이콘) 스타일 */ #aiFloatingBubbleButton { position: absolute; /* 컨테이너 내에서 절대 위치 */ bottom: 0; right: 0; background-color: #fff; /* 흰색 배경 */ border: 1px solid #ccc; /* 옅은 회색 테두리 */ border-radius: 50%; /* 원형 모양 */ width: ${BubbleConfig.BUBBLE_SIZE}px; height: ${BubbleConfig.BUBBLE_SIZE}px; display: flex; /* 내부 아이콘 중앙 정렬 */ justify-content: center; align-items: center; cursor: pointer; /* 클릭 가능함을 나타내는 커서 */ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); /* 그림자 효과 */ transition: transform 0.2s; /* 호버 시 변형 애니메이션 */ } /* 버블 버튼 내부 이미지 스타일 */ #aiFloatingBubbleButton img { user-select: none; /* 드래그 방지 */ -webkit-user-drag: none; /* 드래그 방지 (웹킷 브라우저) */ pointer-events: none; /* 이미지 클릭 시 버튼 이벤트 발생 */ } /* 버블 버튼 호버 시 확대 효과 */ #aiFloatingBubbleButton:hover { transform: scale(1.1); /* 10% 확대 */ } /* AI 사이트 옵션 메뉴 스타일 */ #aiSiteOptions { position: absolute; /* 컨테이너 내에서 절대 위치 */ bottom: ${BubbleConfig.BUBBLE_SIZE + BubbleConfig.OPTION_MENU_GAP}px; /* 버블 위에 배치 */ right: 0; flex-direction: column; /* 세로 정렬 */ background-color: #fff; /* 흰색 배경 */ border: 1px solid #eee; /* 옅은 회색 테두리 */ border-radius: 8px; /* 둥근 모서리 */ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15); /* 그림자 효과 */ overflow: hidden; /* 내용이 넘칠 경우 숨김 */ white-space: nowrap; /* 줄바꿈 방지 */ max-height: 0; /* 기본적으로 숨김 (높이 0) */ opacity: 0; /* 투명도 0 */ pointer-events: none; /* 클릭 이벤트 비활성화 */ transition: max-height ${BubbleConfig.MENU_TRANSITION_DURATION}s ease-out, opacity ${BubbleConfig.MENU_TRANSITION_DURATION}s ease-out; /* 나타나고 사라지는 애니메이션 */ } /* AI 사이트 옵션 메뉴가 보일 때의 스타일 */ #aiSiteOptions.visible { max-height: 500px; /* 메뉴 내용이 모두 보이도록 충분히 큰 값 설정 */ opacity: 1; /* 완전히 불투명하게 */ pointer-events: auto; /* 클릭 이벤트 활성화 */ transition: max-height ${BubbleConfig.MENU_TRANSITION_DURATION}s ease-out, opacity ${BubbleConfig.MENU_TRANSITION_DURATION}s ease-out; /* 나타나고 사라지는 애니메이션 */ } /* 각 AI 사이트 옵션 항목 스타일 */ .ai-option { display: flex; /* 내부 아이콘과 텍스트 정렬 */ align-items: center; padding: ${BubbleConfig.OPTION_ITEM_PADDING_VERTICAL}px ${BubbleConfig.OPTION_ITEM_PADDING_HORIZONTAL}px; /* 패딩 */ cursor: pointer; /* 클릭 가능함을 나타내는 커서 */ border-bottom: 1px solid #f0f0f0; /* 하단 구분선 */ transition: background-color 0.2s; /* 호버 시 배경색 변경 애니메이션 */ width: ${BubbleConfig.OPTION_MENU_WIDTH}px; /* 메뉴 항목의 고정 너비 */ } /* 마지막 메뉴 항목의 하단 구분선 제거 */ .ai-option:last-child { border-bottom: none; } /* 메뉴 항목 호버 시 배경색 변경 */ .ai-option:hover { background-color: #f5f5f5; } /* 메뉴 항목 내부 아이콘 스타일 */ .ai-option .option-icon { width: ${BubbleConfig.OPTION_ICON_SIZE}px; height: ${BubbleConfig.OPTION_ICON_SIZE}px; margin-right: ${BubbleConfig.OPTION_ITEM_ICON_MARGIN_RIGHT}px; /* 아이콘과 텍스트 사이 간격 */ border-radius: 2px; /* 살짝 둥근 모서리 */ vertical-align: middle; user-select: none; -webkit-user-drag: none; pointer-events: none; } /* 메뉴 항목 내부 텍스트 (AI 이름) 스타일 */ .ai-option span { font-family: 'Inter', sans-serif; /* Inter 폰트 사용 (폴백: sans-serif) */ font-size: 14px; /* 폰트 크기 */ color: #333; /* 어두운 회색 폰트 색상 */ font-weight: normal; /* 보통 굵기 */ } `); } /** * @private * 모든 이벤트 리스너를 설정합니다. * 드래그, 호버, 클릭 관련 이벤트를 등록합니다. */ _setupEventListeners() { this._setupDrag(); // 버블 드래그 기능 설정 this._setupHover(); // AI 목록 표시/숨김 호버 기능 설정 this._setupClick(); // AI 사이트 옵션 클릭 기능 설정 } /** * @private * 버블 드래그 기능을 설정합니다. * 마우스 다운, 이동, 업 이벤트를 사용하여 버블을 드래그 가능하게 합니다. */ _setupDrag() { this.bubbleContainer.addEventListener('mousedown', (e) => { // AI 옵션 메뉴 자체를 드래그하는 것은 방지합니다. // 만약 클릭된 요소가 '.ai-option' 클래스를 포함한다면 드래그를 시작하지 않습니다. if (e.target.closest('.ai-option')) { return; } this.isDragging = true; // 드래그 시작 플래그 설정 this.bubbleContainer.classList.add('grabbing'); // 드래그 중임을 나타내는 클래스 추가 // 마우스 포인터와 버블 컨테이너의 좌상단 모서리 간의 오프셋을 계산합니다. // 이는 드래그 시작 시 마우스 위치와 버블 위치의 차이를 기억하여 자연스러운 드래그를 가능하게 합니다. this.offsetX = e.clientX - this.bubbleContainer.getBoundingClientRect().left; this.offsetY = e.clientY - this.bubbleContainer.getBoundingClientRect().top; }); document.addEventListener('mousemove', (e) => { // 드래그 중이 아니면 함수를 종료합니다. if (!this.isDragging) return; // 새로운 버블 위치 계산 let newLeft = e.clientX - this.offsetX; let newTop = e.clientY - this.offsetY; // 화면 경계를 벗어나지 않도록 버블 위치를 제한합니다. const maxX = window.innerWidth - this.bubbleContainer.offsetWidth; // 최대 X 좌표 const maxY = window.innerHeight - this.bubbleContainer.offsetHeight; // 최대 Y 좌표 newLeft = Math.max(0, Math.min(newLeft, maxX)); // X 좌표를 0과 maxX 사이로 제한 newTop = Math.max(0, Math.min(newTop, maxY)); // Y 좌표를 0과 maxY 사이로 제한 // 계산된 위치를 버블 컨테이너에 적용합니다. this.bubbleContainer.style.left = `${newLeft}px`; this.bubbleContainer.style.top = `${newTop}px`; // left/top을 사용할 때는 기존 right/bottom 속성을 초기화하여 충돌을 방지합니다. this.bubbleContainer.style.right = 'auto'; this.bubbleContainer.style.bottom = 'auto'; }); document.addEventListener('mouseup', () => { this.isDragging = false; // 드래그 종료 플래그 설정 this.bubbleContainer.classList.remove('grabbing'); // 드래그 중임을 나타내는 클래스 제거 }); } /** * @private * AI 목록 표시/숨김 호버 기능을 설정합니다. * 마우스가 버블 위에 있을 때 메뉴를 표시하고, 벗어났을 때 지연 후 숨깁니다. */ _setupHover() { this.bubbleContainer.addEventListener('mouseenter', () => { clearTimeout(this.hideTimeout); // 숨김 타이머가 설정되어 있다면 취소합니다. this.siteOptions.classList.add('visible'); // AI 목록 메뉴를 표시합니다. }); this.bubbleContainer.addEventListener('mouseleave', () => { // 마우스가 벗어난 후 일정 지연 시간(MENU_HIDE_DELAY) 후에 메뉴를 숨깁니다. // 이 지연 시간 동안 마우스가 다시 들어오면 숨김이 취소됩니다. this.hideTimeout = setTimeout(() => { this.siteOptions.classList.remove('visible'); // AI 목록 메뉴를 숨깁니다. }, BubbleConfig.MENU_HIDE_DELAY); // BubbleConfig에서 지연 시간 가져옴 }); } /** * @private * AI 사이트 옵션 클릭 기능을 설정합니다. * AI 목록에서 특정 AI 사이트를 클릭하면 새 팝업 창으로 해당 사이트를 엽니다. */ _setupClick() { this.siteOptions.addEventListener('click', (event) => { // 클릭된 요소가 '.ai-option' 클래스를 가진 가장 가까운 부모 요소를 찾습니다. const option = event.target.closest('.ai-option'); if (option) { let url = option.dataset.url; // 클릭된 옵션의 'data-url' 속성에서 URL을 가져옵니다. if (url) { // 새 팝업 창임을 나타내는 쿼리 파라미터 'bubble_popup=true'를 URL에 추가합니다. // 이 파라미터는 팝업 창에서 버블이 나타나지 않도록 하는 데 사용됩니다. url += (url.includes('?') ? '&' : '?') + 'bubble_popup=true'; const windowName = 'AIFloatingWindow'; // 팝업 창의 이름 설정 (동일한 이름으로 열면 기존 창 재활용) // 팝업 창의 특징(너비, 높이, 메뉴바, 툴바 등)을 BubbleConfig에서 가져와 설정합니다. const features = `width=${BubbleConfig.POPUP_WINDOW_WIDTH},height=${BubbleConfig.POPUP_WINDOW_HEIGHT},menubar=no,toolbar=no,location=no,status=no,resizable=yes,scrollbars=yes`; window.open(url, windowName, features); // 새 팝업 창을 엽니다. } } }); } } // 스크립트 실행 시 AI 플로팅 버블 인스턴스를 생성하여 모든 기능을 시작합니다. new AIFloatingBubble(); })();