Chzzk_L&V: Dal.wiki & WWME Viewer

치지직에서 Dal.wiki 일정 확인 및 추가기능 + WWME 뷰어 기능 추가 (기본 시작페이지 WWME)

Install this script?
Author's suggested script

You may also like Chzzk_L&V: Chatting Plus.

Install this script
// ==UserScript==
// @name         Chzzk_L&V: Dal.wiki & WWME Viewer
// @namespace    Chzzk_L&V: Third Party Iframe Viewer
// @version      1.0.8.1
// @description  치지직에서 Dal.wiki 일정 확인 및 추가기능 + WWME 뷰어 기능 추가 (기본 시작페이지 WWME)
// @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 && !location.pathname.includes('/chat')) {
    let buttonContainer, MainBtn, streamerBtn, dayBtn, wwmeBtn;
    let iframe, closeBtn, opacitySlider, rememberOpacityCheckbox, checkbox;
    let viewMode = 'wwme'; // 'streamer' | 'daily' | 'wwme'
    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;
      dayBtn.style.left = `${parseFloat(iframe.style.left) + streamerWidth + 8}px`;
      dayBtn.style.display = 'block';

      wwmeBtn.style.top = closeBtn.style.top;
      wwmeBtn.style.left = `${parseFloat(dayBtn.style.left) + dayBtn.offsetWidth + 4}px`;
      wwmeBtn.style.display = 'block';
    }

    function updateMainBtnText(mode) {
      switch (mode) {
        case 'streamer':
          MainBtn.textContent = '🎥 스트리머 일정';
          break;
        case 'daily':
          MainBtn.textContent = '📅 치지직 일간 일정';
          break;
        case 'wwme':
          MainBtn.textContent = '🌐 WWME';
          break;
      }
    }

    function createUI() {
      buttonContainer = document.createElement('div');
      document.body.appendChild(buttonContainer);

      MainBtn = document.createElement('button');
      Object.assign(MainBtn.style, {
        padding: '6px 8px',
        zIndex: 2147483647,
        background: '#333',
        color: 'white',
        borderRadius: '6px',
        border: 'none',
        fontSize: '12px'
      });
      buttonContainer.appendChild(MainBtn);

      streamerBtn = document.createElement('button');
      streamerBtn.textContent = '🎥 스트리머 일정';
      dayBtn = document.createElement('button');
      [streamerBtn, dayBtn].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 = '📅 치지직 일간 일정';

      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);

      // WWME 버튼 생성
      wwmeBtn = document.createElement('button');
      wwmeBtn.textContent = '🌐 WWME';
      Object.assign(wwmeBtn.style, {
        display: 'none',
        position: 'fixed',
        zIndex: '2147483647',
        padding: '4px 8px',
        background: '#555',
        color: 'white',
        borderRadius: '4px',
        border: 'none',
        cursor: 'pointer',
        fontSize: '12px'
      });
      document.body.appendChild(wwmeBtn);

      applyButtonPosition();
    }

    function openIframe(mode = viewMode) {
      if (mode === 'wwme') {
        // WWME 모드: 사전 로딩된 iframe이 아니더라도 URL 설정
        iframe.src = 'https://wwme.kr/';
        centerIframe();
        // 투명도 기억 적용
        iframe.style.opacity = currentOpacity;
        opacitySlider.value = currentOpacity.toString();
        checkbox.checked = !!localStorage.getItem(STORAGE_KEY);

        // 표시
        iframe.style.display = 'block';
        closeBtn.style.display = 'block';
        opacitySlider.style.display = 'block';
        rememberOpacityCheckbox.style.display = 'block';

        // 버튼 위치
        streamerBtn.style.top = closeBtn.style.top;
        streamerBtn.style.left = iframe.style.left;
        dayBtn.style.top = closeBtn.style.top;
        dayBtn.style.left = `${parseFloat(streamerBtn.style.left) + streamerBtn.offsetWidth + 8}px`;
        wwmeBtn.style.top = closeBtn.style.top;
        wwmeBtn.style.left = `${parseFloat(dayBtn.style.left) + dayBtn.offsetWidth + 4}px`;
        return;
      }

      // Dal.wiki 모드: daily 또는 streamer
      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 'daily':
          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 '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();
      checkbox.checked = !!localStorage.getItem(STORAGE_KEY);

      // 표시
      iframe.style.display = 'block';
      closeBtn.style.display = 'block';
      opacitySlider.style.display = 'block';
      rememberOpacityCheckbox.style.display = 'block';
    }

    function bindEvents() {
      MainBtn.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 = wwmeBtn.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 = wwmeBtn.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 = () => {
        if (checkbox.checked) localStorage.setItem(STORAGE_KEY, currentOpacity.toString());
        else localStorage.removeItem(STORAGE_KEY);
      };
      streamerBtn.onclick = () => {
        viewMode = 'streamer';
        openIframe(viewMode);
      };
      dayBtn.onclick = () => {
        viewMode = 'daily';
        openIframe(viewMode);
      };
      wwmeBtn.onclick = () => {
        viewMode = 'wwme';
        openIframe(viewMode);
      };
    }

    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();
      updateMainBtnText(viewMode);
      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 });
  }
})();