Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save lunamoth/27717e92ded97d0f5e820d8897ade053 to your computer and use it in GitHub Desktop.
Save lunamoth/27717e92ded97d0f5e820d8897ade053 to your computer and use it in GitHub Desktop.
// ==UserScript==
// @name Google AI Studio - 사이드바 자동 숨김 (v2.3 - 사용자 설정)
// @namespace http://tampermonkey.net/
// @version 2.4
// @description Google AI Studio 자동 숨김. 사용자가 직접 열었을 때 유지 시간 설정 가능.
// @author Gemini
// @match https://aistudio.google.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
// ========================================================================
// ★★★ 사용자 설정 영역 시작 ★★★
// ========================================================================
const USER_SETTINGS = {
USER_ACTION_GRACE_PERIOD_MS: 10000, // 오른쪽 패널을 직접 열었을 때 유지 시간 (밀리초)
// 예: 5초 = 5000, 10초 = 10000, 30초 = 30000
CHECK_INTERVAL_MS: 250, // (고급) 핵심 확인 주기 (밀리초)
INITIAL_CHECK_MAX_DURATION_MS: 5000, // (고급) 초기 로딩 시 확인 최대 지속 시간 (밀리초)
NAV_CLOSE_DELAY_MS: 50, // (고급) 페이지 이동 후 닫기 시도까지 지연 시간 (밀리초)
};
// ========================================================================
// ★★★ 사용자 설정 영역 끝 ★★★
// ========================================================================
const SELECTORS = {
LEFT_NAVBAR: 'ms-navbar .layout-navbar',
LEFT_COLLAPSE_BUTTON: 'ms-navbar button[aria-label="Expand or collapse navigation menu"], ms-navbar button[aria-label*="탐색 메뉴"]',
RIGHT_PANEL: 'ms-right-side-panel',
// 아래 선택자들은 RIGHT_PANEL 요소 하위에서 검색됩니다.
RIGHT_PANEL_CONTENT: '.content-container',
RIGHT_PANEL_CLOSE_BUTTON: 'ms-run-settings button[aria-label="Close run settings panel"], ms-run-settings button[aria-label*="닫기"]',
// 이 선택자는 페이지 전역에서 검색됩니다.
RIGHT_PANEL_OPEN_BUTTON: 'button[aria-label="Run settings"], button[aria-label*="실행 설정"]',
};
const STATE = {
lastUserOpenTimestamp: 0,
checkIntervalId: null,
isInitialCheckPhase: true,
initialCheckStartTime: 0,
currentHref: document.location.href,
isClosingRightPanel: false,
isInitialized: false,
};
const SCRIPT_NAME = 'AI Studio 자동 숨김';
const SCRIPT_VERSION = 'v2.3'; // UserScript 메타데이터와 일치
// --- 유틸리티 함수 ---
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func.apply(this, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
// --- 로깅 함수 ---
function logImportant(message) {
console.log(`%c${SCRIPT_NAME} (${SCRIPT_VERSION}): ${message}`, 'color: green; font-weight: bold;');
}
function logError(message, error) {
console.error(`${SCRIPT_NAME} (${SCRIPT_VERSION}): ${message}`, error || '');
}
// 디버그용 로그 (릴리스 시 주석 처리 상태 유지)
// function logDebug(message) { /* console.log(`${SCRIPT_NAME}: ${message}`); */ }
// --- DOM 요소 가져오기 헬퍼 ---
function getElement(selector, parent = document) {
try {
return parent.querySelector(selector);
} catch (e) {
logError(`요소(${selector}) 검색 중 오류 발생:`, e);
return null;
}
}
// --- 오른쪽 패널 열기 버튼 리스너 관리 ---
function attachRightPanelOpenButtonListener() {
try {
const openButton = getElement(SELECTORS.RIGHT_PANEL_OPEN_BUTTON); // 전역에서 검색
if (openButton && openButton.getAttribute('data-autohide-listener') !== 'true') {
openButton.removeEventListener('click', handleRightPanelOpenButtonClick); // 중복 방지
openButton.addEventListener('click', handleRightPanelOpenButtonClick);
openButton.setAttribute('data-autohide-listener', 'true');
// logDebug('오른쪽 패널 열기 버튼 리스너 추가됨.');
return true;
}
} catch (error) {
logError('오른쪽 패널 열기 버튼 리스너 부착 중 오류 발생:', error);
}
return false;
}
function handleRightPanelOpenButtonClick(event) {
if (event && event.isTrusted) { // 실제 사용자 클릭인지 확인
// logDebug('[사용자 액션] 오른쪽 패널 열림 버튼 클릭됨. 타임스탬프 업데이트.');
STATE.lastUserOpenTimestamp = Date.now();
}
}
// --- 패널 제어 함수 ---
function closeLeftPanel() {
try {
const leftNavbar = getElement(SELECTORS.LEFT_NAVBAR);
const leftCollapseButton = getElement(SELECTORS.LEFT_COLLAPSE_BUTTON);
if (leftNavbar && leftNavbar.classList.contains('expanded') && leftCollapseButton) {
leftCollapseButton.click();
// logDebug('왼쪽 패널 닫기 실행됨.');
return true;
}
} catch (error) {
logError('왼쪽 패널 닫기 중 오류 발생:', error);
}
return false;
}
function isRightPanelRenderedAndOpen() {
const rightPanelElement = getElement(SELECTORS.RIGHT_PANEL);
if (!rightPanelElement) return false; // 패널 자체가 없으면 닫힌 상태
return !!getElement(SELECTORS.RIGHT_PANEL_CONTENT, rightPanelElement); // 패널 내 컨텐츠 유무로 열림 상태 판단
}
function tryCloseRightPanel(reason = "자동 숨김") {
if (STATE.isClosingRightPanel) return false; // 이미 닫기 진행 중이면 중복 실행 방지
const rightPanelElement = getElement(SELECTORS.RIGHT_PANEL);
// 패널이 없거나, 패널은 있으나 컨텐츠가 없어 이미 닫힌 것으로 간주되면 실행 안함
if (!rightPanelElement || !getElement(SELECTORS.RIGHT_PANEL_CONTENT, rightPanelElement)) {
return false;
}
const closeButton = getElement(SELECTORS.RIGHT_PANEL_CLOSE_BUTTON, rightPanelElement);
if (!closeButton) {
// logDebug('오른쪽 패널 닫기 버튼을 찾을 수 없습니다.');
return false; // 닫기 버튼이 없으면 실행 불가
}
STATE.isClosingRightPanel = true;
// logDebug(`오른쪽 패널 닫기 시도 (사유: ${reason})`);
try {
if (document.body.contains(closeButton)) { // 버튼이 실제로 DOM에 존재하는지 최종 확인
closeButton.click();
} else {
// 이 로그는 버튼이 사라지는 예외적인 상황을 파악하는 데 도움이 될 수 있습니다.
logError('오른쪽 패널 닫기 버튼이 DOM에서 사라져 클릭할 수 없습니다.', `사유: ${reason}`);
}
} catch (clickError) {
logError('오른쪽 패널 닫기 버튼 클릭 중 오류 발생:', clickError);
} finally {
// 클릭 후 실제 DOM 변경까지 시간이 걸릴 수 있으므로, 플래그 해제에 약간의 지연을 줌
setTimeout(() => { STATE.isClosingRightPanel = false; }, 100);
}
return true;
}
function closeRightPanelIfNeeded(reason = "주기적 확인") {
if (STATE.isClosingRightPanel) return false;
const timeSinceLastUserOpen = Date.now() - STATE.lastUserOpenTimestamp;
// 사용자가 패널을 열었고, 설정된 유예 시간 이내인 경우 닫지 않음
if (STATE.lastUserOpenTimestamp > 0 && timeSinceLastUserOpen < USER_SETTINGS.USER_ACTION_GRACE_PERIOD_MS) {
// logDebug(`오른쪽 패널 닫기 건너뜀 (사용자 유예 기간 ${Math.round((USER_SETTINGS.USER_ACTION_GRACE_PERIOD_MS - timeSinceLastUserOpen)/1000)}초 남음).`);
return false;
}
return tryCloseRightPanel(reason);
}
// --- 핵심 로직 ---
function periodicCheck() {
try {
closeLeftPanel();
closeRightPanelIfNeeded("주기적 확인");
attachRightPanelOpenButtonListener(); // 버튼이 동적으로 생성될 수 있으므로 주기적 리스너 부착 시도
if (STATE.isInitialCheckPhase) {
const leftNavbar = getElement(SELECTORS.LEFT_NAVBAR);
const isLeftPanelClosed = !(leftNavbar && leftNavbar.classList.contains('expanded'));
const isRightEffectivelyClosed = !isRightPanelRenderedAndOpen(); // 오른쪽 패널 상태 확인
const elapsedTime = Date.now() - STATE.initialCheckStartTime;
// 초기 0.5초 이후 양쪽 패널이 모두 닫혔거나, 최대 지속 시간을 초과하면 초기 단계 종료
if ((isLeftPanelClosed && isRightEffectivelyClosed && elapsedTime > 500) ||
(elapsedTime > USER_SETTINGS.INITIAL_CHECK_MAX_DURATION_MS)) {
STATE.isInitialCheckPhase = false;
logImportant(`초기 확인 단계 종료 (경과: ${elapsedTime}ms).`);
}
}
} catch (error) {
logError('주기적 확인 작업 중 오류 발생:', error);
}
}
// --- URL 변경 처리 ---
const debouncedHandleUrlChange = debounce(() => {
// logDebug(`[URL 변경 감지] 새 URL: ${document.location.href}. 즉시 닫기 시도.`);
STATE.currentHref = document.location.href;
STATE.lastUserOpenTimestamp = 0; // URL 변경 시 사용자 액션으로 인한 유예 시간 초기화
STATE.isClosingRightPanel = false; // 닫기 플래그 초기화
// URL 변경 후 DOM이 안정화될 시간을 약간 줌
setTimeout(() => {
closeLeftPanel(); // 왼쪽 패널 즉시 닫기
tryCloseRightPanel("URL 변경으로 인한 강제 닫기"); // 오른쪽 패널은 사용자 유예 시간 무시하고 닫기
attachRightPanelOpenButtonListener(); // 새 페이지 요소에 리스너 재부착
STATE.isInitialCheckPhase = true; // 초기화 확인 단계 재시작
STATE.initialCheckStartTime = Date.now();
}, USER_SETTINGS.NAV_CLOSE_DELAY_MS);
}, 150); // URL 변경 감지 디바운스 시간
function setupUrlChangeListener() {
const checkForUrlChange = () => {
if (document.location.href !== STATE.currentHref) {
debouncedHandleUrlChange();
}
};
window.addEventListener('popstate', checkForUrlChange);
const originalPushState = history.pushState;
history.pushState = function(...args) {
originalPushState.apply(history, args);
checkForUrlChange();
};
const originalReplaceState = history.replaceState;
history.replaceState = function(...args) {
originalReplaceState.apply(history, args);
checkForUrlChange();
};
// logDebug('URL 변경 리스너 설정됨.');
}
// --- 스크립트 초기화 ---
function initialize() {
if (STATE.isInitialized) {
// logDebug('스크립트가 이미 초기화되었습니다. 중복 실행 방지.');
return;
}
STATE.isInitialized = true; // 초기화 플래그 설정
logImportant(`스크립트 초기화 완료. 사용자 설정 유지 시간: ${USER_SETTINGS.USER_ACTION_GRACE_PERIOD_MS / 1000}초.`);
STATE.initialCheckStartTime = Date.now();
STATE.isInitialCheckPhase = true;
STATE.lastUserOpenTimestamp = 0;
STATE.isClosingRightPanel = false;
STATE.currentHref = document.location.href; // 초기화 시 현재 href 정확히 설정
if (STATE.checkIntervalId) clearInterval(STATE.checkIntervalId); // 만약을 위한 이전 인터벌 정리
STATE.checkIntervalId = setInterval(periodicCheck, USER_SETTINGS.CHECK_INTERVAL_MS);
// logDebug(`핵심 확인 인터벌 시작 (간격: ${USER_SETTINGS.CHECK_INTERVAL_MS}ms).`);
setupUrlChangeListener();
setTimeout(periodicCheck, 50); // 첫 확인은 DOM 렌더링 후 빠르게 시도
}
// --- 스크립트 실행 시작점 ---
function main() {
// DOM 로드 상태에 따라 초기화 실행
if (document.readyState === 'complete') {
setTimeout(initialize, 200); // 페이지 로드 완료 후 약간의 지연을 두어 안정성 확보
} else {
const onReadyStateChangeCallback = () => {
if (document.readyState === 'complete') {
document.removeEventListener('readystatechange', onReadyStateChangeCallback);
if (!STATE.isInitialized) setTimeout(initialize, 200);
}
};
document.addEventListener('readystatechange', onReadyStateChangeCallback);
// 안전 장치: readystatechange 이벤트가 예상대로 동작하지 않을 경우를 대비
setTimeout(() => {
if (document.readyState === 'complete' && !STATE.isInitialized) {
logError('안전 장치 발동: 강제 초기화 시도.');
initialize();
}
}, 2000); // 안전 장치 발동 시간
}
// 페이지를 떠나기 전 인터벌 정리
window.addEventListener('beforeunload', () => {
if (STATE.checkIntervalId) {
clearInterval(STATE.checkIntervalId);
// logDebug('페이지 언로드. 인터벌 정리됨.');
}
});
}
// 스크립트 실행
main();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment