// ==UserScript== // @name Chzzk_L&V: Dal.wiki Viewer // @namespace Chzzk_L&V: Dal.wiki Viewer // @version 1.0.3 // @description 치지직 합방 일정과 대회/이벤트를 확인가능한 Dal.wiki를 치지직 방송을 보면서도 확인하고자 만들었습니다. // @author DOGJIP // @match *://chzzk.naver.com/* // @grant none // @run-at document-idle // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=chzzk.naver.com // @downloadURL https://update.greasyfork.icu/scripts/536277/Chzzk_LV%3A%20Dalwiki%20Viewer.user.js // @updateURL https://update.greasyfork.icu/scripts/536277/Chzzk_LV%3A%20Dalwiki%20Viewer.meta.js // ==/UserScript== (function () { 'use strict'; // 치지직 도메인이 아니거나, iframe 내부라면 스크립트를 실행하지 않음 if (!location.hostname.includes('chzzk.naver.com')) return; if (window.top !== window.self) return; const topicPath = '/topic/%EC%B9%98%EC%A7%80%EC%A7%81%20%ED%95%A9%EB%B0%A9%2F%EB%8C%80%ED%9A%8C%2F%EC%BD%98%ED%85%90%EC%B8%A0%20%EC%9D%BC%EC%A0%95'; let buttonContainer, calendarBtn, toggleBtn, iframe, closeBtn, opacitySlider, rememberOpacityCheckbox, checkbox; let viewMode = 'agenda'; // 기본 보기 모드는 일간 (month or agenda) let currentOpacity = 1.0; const STORAGE_KEY = 'dalwiki_opacity'; const fixedPosition = { top: 16, right: 280 }; const iframeDefaultWidth = 900; const iframeDefaultHeight = 600; function applyButtonPosition() { Object.assign(buttonContainer.style, { position: 'fixed', top: `${fixedPosition.top}px`, right: `${fixedPosition.right}px`, zIndex: 2147483647, display: 'flex', gap: '4px' }); } function centerIframe() { const maxW = window.innerWidth * 0.9; const maxH = window.innerHeight * 0.9; const w = Math.min(iframeDefaultWidth, maxW); const h = Math.min(iframeDefaultHeight, maxH); const top = (window.innerHeight - h) / 2; const left = (window.innerWidth - w) / 2; Object.assign(iframe.style, { width: w + 'px', height: h + 'px', top: top + 'px', left: left + 'px', display: 'block', position: 'fixed', border: '2px solid #ccc', borderRadius: '8px', boxShadow: '0 0 12px rgba(0,0,0,0.4)', background: 'white', zIndex: 2147483646 }); Object.assign(closeBtn.style, { position: 'fixed', top: (top - 30) + 'px', left: (left + w - 30) + 'px', display: 'block', background:'crimson', color: 'white', border: 'none', padding: '4px 8px', borderRadius: '50%', cursor: 'pointer', fontSize: '12px', zIndex: 2147483647 }); // 1) 슬라이더 위치: closeBtn 왼쪽 (기존 동일) const sliderRight = parseFloat(closeBtn.style.left) - 110; opacitySlider.style.top = closeBtn.style.top; opacitySlider.style.left = `${sliderRight}px`; // 2) 체크박스 위치: 슬라이더 왼쪽 (슬라이더 너비 100px + 여백 8px) const checkboxLeft = sliderRight - 100 - 8; rememberOpacityCheckbox.style.top = closeBtn.style.top; rememberOpacityCheckbox.style.left = `${checkboxLeft}px`; } function updateToggleLabel() { // 버튼에 '월간 보기' 또는 '일간 보기' 명시 toggleBtn.textContent = viewMode === 'month' ? '월(月)' : '일(日)'; } function createUI() { buttonContainer = document.createElement('div'); document.body.appendChild(buttonContainer); // 1) 일정 보기 버튼 calendarBtn = document.createElement('button'); calendarBtn.textContent = '📅 일정 보기'; Object.assign(calendarBtn.style, { padding: '6px 10px', background: '#333333', color: 'white', border: 'none', borderRadius: '6px', cursor: 'pointer', fontSize: '14px' }); buttonContainer.appendChild(calendarBtn); // 2) 뷰 모드 토글 버튼 (크기 유지, 레이블 표시) toggleBtn = document.createElement('button'); updateToggleLabel(); Object.assign(toggleBtn.style, { padding: '6px 8px', background: '#555555', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer', fontSize: '12px', lineHeight: '1.2' }); buttonContainer.appendChild(toggleBtn); // 3) iframe iframe = document.createElement('iframe'); Object.assign(iframe.style, { display: 'none' }); document.body.appendChild(iframe); // 4) 닫기 버튼 closeBtn = document.createElement('button'); closeBtn.textContent = '✖'; Object.assign(closeBtn.style, { display: 'none' }); document.body.appendChild(closeBtn); applyButtonPosition(); // 5) 투명도 조절 슬라이더 opacitySlider = document.createElement('input'); opacitySlider.type = 'range'; opacitySlider.min = '0.3'; opacitySlider.max = '1'; opacitySlider.step = '0.01'; opacitySlider.value = '1'; Object.assign(opacitySlider.style, { display: 'none', position: 'fixed', zIndex: 2147483647, width: '100px', cursor: 'pointer' }); document.body.appendChild(opacitySlider); // 6) 투명도 기억 여부 체크박스 rememberOpacityCheckbox = document.createElement('label'); rememberOpacityCheckbox.style.position = 'fixed'; rememberOpacityCheckbox.style.zIndex = 2147483647; rememberOpacityCheckbox.style.top = '50px'; rememberOpacityCheckbox.style.transform = 'translateX(0)'; rememberOpacityCheckbox.style.fontSize = '12px'; rememberOpacityCheckbox.style.color = '#fff'; rememberOpacityCheckbox.style.background = 'rgba(34, 34, 34, 0.5)'; // 50% 투명한 검정색 rememberOpacityCheckbox.style.padding = '4px 6px'; rememberOpacityCheckbox.style.borderRadius = '4px'; checkbox = document.createElement('input'); // <-- 전역 변수 사용 checkbox.type = 'checkbox'; checkbox.style.marginRight = '4px'; rememberOpacityCheckbox.appendChild(checkbox); rememberOpacityCheckbox.appendChild(document.createTextNode('투명도 기억')); document.body.appendChild(rememberOpacityCheckbox); rememberOpacityCheckbox.style.display = 'none'; } function openIframe() { const dt = new Date(); const yyyy = dt.getFullYear(); const mm = String(dt.getMonth()+1).padStart(2,'0'); const dd = String(dt.getDate()).padStart(2,'0'); iframe.src = `https://dal.wiki${topicPath}/${viewMode}?date=${yyyy}-${mm}-${dd}`; centerIframe(); // 투명도 적용 iframe.style.opacity = currentOpacity; opacitySlider.value = String(currentOpacity); opacitySlider.style.display = 'block'; rememberOpacityCheckbox.style.display = 'block'; checkbox.checked = !!localStorage.getItem(STORAGE_KEY); } function bindEvents() { // 일정 보기 calendarBtn.addEventListener('click', () => { if (iframe.style.display === 'block') { iframe.style.display = 'none'; closeBtn.style.display = 'none'; opacitySlider.style.display = 'none'; rememberOpacityCheckbox.style.display = 'none'; } else { openIframe(); } }); // 뷰 모드 토글: iframe이 열린 상태에서만 적용 toggleBtn.addEventListener('click', () => { viewMode = viewMode === 'month' ? 'agenda' : 'month'; updateToggleLabel(); if (iframe.style.display === 'block') { // 새로 열기 openIframe(); } }); // 닫기 버튼 closeBtn.addEventListener('click', () => { iframe.style.display = 'none'; closeBtn.style.display = 'none'; opacitySlider.style.display = 'none'; rememberOpacityCheckbox.style.display = 'none'; }); // 화면 리사이즈 대응 window.addEventListener('resize', () => { if (iframe.style.display === 'block') centerIframe(); }); // 슬라이더 변경 시, 저장 옵션이 체크되어 있으면 localStorage에 저장 opacitySlider.addEventListener('input', () => { currentOpacity = parseFloat(opacitySlider.value); iframe.style.opacity = currentOpacity; if (checkbox.checked) { localStorage.setItem(STORAGE_KEY, currentOpacity.toString()); } }); // 체크박스 토글: 체크 해제 시 저장값 제거 checkbox.addEventListener('change', () => { if (!checkbox.checked) { localStorage.removeItem(STORAGE_KEY); } else { localStorage.setItem(STORAGE_KEY, currentOpacity.toString()); } }); } function onReady(fn) { if (document.body) { // 초기 투명도 로드 const saved = localStorage.getItem(STORAGE_KEY); if (saved) { currentOpacity = parseFloat(saved); } return fn(); } new MutationObserver((obs) => { if (document.body) { obs.disconnect(); const saved = localStorage.getItem(STORAGE_KEY); if (saved) { currentOpacity = parseFloat(saved); } fn(); } }).observe(document.documentElement, { childList: true }); } function observeBodyStyle() { const body = document.body; const observer = new MutationObserver(() => { const style = window.getComputedStyle(body); const wide = style.overflow === 'hidden' && style.position === 'fixed'; buttonContainer.style.display = wide ? 'none' : 'flex'; }); observer.observe(body, { attributes: true, attributeFilter: ['style'] }); // 초기 상태 확인 const initStyle = window.getComputedStyle(body); const isWide = initStyle.overflow === 'hidden' && initStyle.position === 'fixed'; buttonContainer.style.display = isWide ? 'none' : 'flex'; } onReady(() => { createUI(); bindEvents(); observeBodyStyle(); }); })();