// ==UserScript==
// @name Chzzk_L&V: Dal.wiki Viewer
// @namespace Chzzk_L&V: Dal.wiki Viewer
// @version 1.0.6
// @description 치지직에서 Dal.wiki 일정 확인 및 추가기능
// @author DOGJIP
// @match *://chzzk.naver.com/*
// @match https://dal.wiki/*
// @grant none
// @run-at document-idle
// @license MIT
// @icon https://www.google.com/s2/favicons?sz=64&domain=chzzk.naver.com
// ==/UserScript==
(function () {
'use strict';
const isChzzk = location.hostname.includes('chzzk.naver.com');
const isDalWiki = location.hostname.includes('dal.wiki');
// ========================= CHZZK: 일정 뷰어 =========================
if (isChzzk && window.top === window.self) {
let buttonContainer, calendarBtn, streamerBtn, dayBtn, monthBtn, iframe, closeBtn, opacitySlider, rememberOpacityCheckbox, checkbox;
let viewMode = 'agenda';
let currentOpacity = 1.0;
const STORAGE_KEY = 'dalwiki_opacity';
const fixedPosition = { top: 16, left: 150 };
const iframeDefaultWidth = 1000;
const iframeDefaultHeight = 800;
function applyButtonPosition() {
Object.assign(buttonContainer.style, {
position: 'fixed',
top: `${fixedPosition.top}px`,
left: `${fixedPosition.left}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
});
const sliderRight = parseFloat(closeBtn.style.left) - 110;
opacitySlider.style.top = closeBtn.style.top;
opacitySlider.style.left = `${sliderRight}px`;
const checkboxLeft = sliderRight - 100 - 8;
rememberOpacityCheckbox.style.top = closeBtn.style.top;
rememberOpacityCheckbox.style.left = `${checkboxLeft}px`;
streamerBtn.style.top = closeBtn.style.top;
streamerBtn.style.left = iframe.style.left;
streamerBtn.style.display = 'block';
dayBtn.style.top = closeBtn.style.top;
const streamerWidth = streamerBtn.offsetWidth;
const gap = 8;
dayBtn.style.left = `${parseFloat(iframe.style.left) + streamerWidth + gap}px`;
dayBtn.style.display = 'block';
const dayWidth = dayBtn.offsetWidth;
monthBtn.style.top = closeBtn.style.top;
monthBtn.style.left = `${parseFloat(dayBtn.style.left) + dayWidth + 4}px`;
monthBtn.style.display = 'block';
}
function createUI() {
buttonContainer = document.createElement('div');
document.body.appendChild(buttonContainer);
calendarBtn = document.createElement('button');
calendarBtn.textContent = '📅 일정 보기';
Object.assign(calendarBtn.style, { padding: '6px 8px', zIndex: 2147483647, background: '#333', color: 'white', borderRadius: '6px', border: 'none', fontSize: '12px' });
buttonContainer.appendChild(calendarBtn);
streamerBtn = document.createElement('button');
streamerBtn.textContent = '🎥 스트리머 일정';
dayBtn = document.createElement('button');
monthBtn = document.createElement('button');
[streamerBtn, dayBtn, monthBtn].forEach(btn => {
btn.style.display = 'none';
btn.style.position = 'fixed';
btn.style.zIndex = '2147483647';
btn.style.padding = '4px 8px';
btn.style.background = '#555';
btn.style.color = 'white';
btn.style.borderRadius = '4px';
btn.style.border = 'none';
btn.style.cursor = 'pointer';
btn.style.fontSize = '12px';
document.body.appendChild(btn);
});
dayBtn.textContent = '일(日)';
monthBtn.textContent = '월(月)';
iframe = document.createElement('iframe');
Object.assign(iframe.style, { display: 'none' });
document.body.appendChild(iframe);
closeBtn = document.createElement('button');
closeBtn.textContent = '✖';
Object.assign(closeBtn.style, { display: 'none' });
document.body.appendChild(closeBtn);
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' });
document.body.appendChild(opacitySlider);
rememberOpacityCheckbox = document.createElement('label');
checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.marginRight = '4px';
rememberOpacityCheckbox.appendChild(checkbox);
rememberOpacityCheckbox.appendChild(document.createTextNode('투명도 기억'));
Object.assign(rememberOpacityCheckbox.style, { display: 'none', position: 'fixed', zIndex: 2147483647, fontSize: '12px', color: '#fff', background: 'rgba(34,34,34,0.5)', padding: '4px 6px', borderRadius: '4px' });
document.body.appendChild(rememberOpacityCheckbox);
applyButtonPosition();
}
function openIframe(mode = viewMode) {
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');
let topicPath = '';
let page = '';
switch (mode) {
case 'agenda':
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';
page = 'agenda';
break;
case 'month':
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';
page = 'month';
break;
case 'streamer':
topicPath = '/topic/%EC%B9%98%EC%A7%80%EC%A7%81%20%EC%8A%A4%ED%8A%B8%EB%A6%AC%EB%A8%B8%20%EC%9D%BC%EC%A0%95';
page = 'agenda';
break;
default:
return;
}
iframe.src = `https://dal.wiki${topicPath}/${page}?date=${yyyy}-${mm}-${dd}`;
centerIframe();
iframe.style.opacity = currentOpacity;
opacitySlider.value = currentOpacity.toString();
opacitySlider.style.display = 'block';
rememberOpacityCheckbox.style.display = 'block';
checkbox.checked = !!localStorage.getItem(STORAGE_KEY);
}
function bindEvents() {
calendarBtn.onclick = () => {
if (iframe.style.display === 'block') {
iframe.style.display = closeBtn.style.display = opacitySlider.style.display = rememberOpacityCheckbox.style.display = 'none';
streamerBtn.style.display = dayBtn.style.display = monthBtn.style.display = 'none';
} else {
openIframe();
}
};
closeBtn.onclick = () => {
iframe.style.display = closeBtn.style.display = opacitySlider.style.display = rememberOpacityCheckbox.style.display = 'none';
streamerBtn.style.display = dayBtn.style.display = monthBtn.style.display = 'none';
};
window.addEventListener('resize', () => iframe.style.display === 'block' && centerIframe());
opacitySlider.oninput = () => {
currentOpacity = parseFloat(opacitySlider.value);
iframe.style.opacity = currentOpacity;
if (checkbox.checked) localStorage.setItem(STORAGE_KEY, currentOpacity.toString());
};
checkbox.onchange = () => !checkbox.checked ? localStorage.removeItem(STORAGE_KEY) : localStorage.setItem(STORAGE_KEY, currentOpacity.toString());
streamerBtn.onclick = () => openIframe('streamer');
dayBtn.onclick = () => openIframe('agenda');
monthBtn.onclick = () => openIframe('month');
}
function observeBodyStyle() {
const observer = new MutationObserver(() => {
const style = window.getComputedStyle(document.body);
const hidden = style.overflow === 'hidden' && style.position === 'fixed';
buttonContainer.style.display = hidden ? 'none' : 'flex';
});
observer.observe(document.body, { attributes: true, attributeFilter: ['style'] });
}
function init() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) currentOpacity = parseFloat(saved);
createUI();
bindEvents();
observeBodyStyle();
}
if (document.body) init();
else new MutationObserver((obs) => document.body && (obs.disconnect(), init())).observe(document.documentElement, { childList: true });
}
// ========================= DAL.WIKI 내 고정 레이아웃 =========================
if (isDalWiki) {
const observer = new MutationObserver(() => {
const aside = document.querySelector('aside.md\\:w-\\[176px\\]');
if (!aside || aside.dataset.stickyApplied === 'true') return;
const addButtonAnchor = aside.querySelector('a[href*="/editor"]');
const scrollWrapper = aside.querySelector('[style*="--radix-scroll-area-corner-width"]');
const viewport = scrollWrapper?.querySelector('[data-radix-scroll-area-viewport]');
if (!addButtonAnchor || !scrollWrapper || !viewport) return;
aside.dataset.stickyApplied = 'true';
viewport.style.minHeight = viewport.offsetHeight + 'px';
scrollWrapper.style.maxHeight = '50vh';
scrollWrapper.style.overflowY = 'auto';
const stickyContainer = document.createElement('div');
stickyContainer.style.position = 'sticky';
stickyContainer.style.top = '0';
stickyContainer.style.background = 'white';
stickyContainer.style.zIndex = '50';
stickyContainer.style.paddingBottom = '12px';
const config = { attributes: true, childList: false, subtree: false };
observer.disconnect();
stickyContainer.appendChild(addButtonAnchor);
stickyContainer.appendChild(scrollWrapper);
aside.insertBefore(stickyContainer, aside.firstChild);
observer.observe(document.body, config);
});
observer.observe(document.body, { childList: true, subtree: true });
}
})();