Greasy Fork

Greasy Fork is available in English.

Chzzk 선명한 화면 업그레이드

Chzzk 방송에 선명도 필터를 적용하고 UI에서 직접 ON/OFF 및 강도 조절 기능을 제공합니다.

目前为 2025-05-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         Chzzk 선명한 화면 업그레이드
// @description  Chzzk 방송에 선명도 필터를 적용하고 UI에서 직접 ON/OFF 및 강도 조절 기능을 제공합니다.
// @namespace    http://tampermonkey.net/
// @icon         https://chzzk.naver.com/favicon.ico
// @version      2.0
// @match        https://chzzk.naver.com/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(() => {
  'use strict';

  // 상수 정의
  const STORAGE_KEY_ENABLED   = 'chzzkSharpnessEnabled';
  const STORAGE_KEY_INTENSITY = 'chzzkSharpnessIntensity';
  const FILTER_ID             = 'sharpnessFilter';
  const SVG_ID                = 'sharpnessSVGContainer';
  const STYLE_ID              = 'sharpnessStyle';
  const MENU_SELECTOR         = '.pzp-pc__settings';
  const FILTER_ITEM_SELECTOR  = '.pzp-pc-setting-intro-filter';

  class SharpnessFilter extends EventTarget {
    #enabled = false;
    #intensity = parseFloat(localStorage.getItem(STORAGE_KEY_INTENSITY)) || 1;
    #svgContainer;
    #style;

    constructor() {
      super();
      this.#svgContainer = this.#createSVG();
      this.#style        = this.#createStyle();
      this.#style.media  = 'none';
    }

    get enabled() { return this.#enabled; }
    get intensity() { return this.#intensity; }

    #createSVG() {
      const div = document.createElement('div');
      div.id = SVG_ID;
      div.innerHTML = `
        <svg xmlns="http://www.w3.org/2000/svg" style="position:absolute;width:0;height:0;">
          <filter id="${FILTER_ID}">
            <feConvolveMatrix order="3" divisor="1" kernelMatrix="" />
          </filter>
        </svg>`;
      return div;
    }

    #createStyle() {
      const style = document.createElement('style');
      style.id = STYLE_ID;
      style.textContent = `
        .pzp-pc .webplayer-internal-video { filter: url(#${FILTER_ID}) !important; }
        .sharp-slider { accent-color: var(--sharp-accent, #ccc); }
      `;
      return style;
    }

    #updateFilter() {
      const k = this.#intensity;
      const off = -((k - 1) / 4);
      const matrix = `0 ${off} 0 ${off} ${k} ${off} 0 ${off} 0`;
      this.#svgContainer.querySelector('feConvolveMatrix').setAttribute('kernelMatrix', matrix);
    }

    init() {
      document.body.append(this.#svgContainer);
      document.head.append(this.#style);
      this.#updateFilter();
      if (localStorage.getItem(STORAGE_KEY_ENABLED) === 'true') this.enable(false);
      this.dispatchEvent(new Event('initialized'));
    }

    enable(persist = true) {
      if (this.#enabled) return;
      this.#enabled = true;
      this.#style.media = 'all';
      if (persist) localStorage.setItem(STORAGE_KEY_ENABLED, 'true');
      this.dispatchEvent(new Event('enabled'));
    }

    disable(persist = true) {
      if (!this.#enabled) return;
      this.#enabled = false;
      this.#style.media = 'none';
      if (persist) localStorage.setItem(STORAGE_KEY_ENABLED, 'false');
      this.dispatchEvent(new Event('disabled'));
    }

    toggle() {
      this.enabled ? this.disable() : this.enable();
      this.dispatchEvent(new Event('toggle'));
    }

    setIntensity(value) {
      this.#intensity = value;
      this.#updateFilter();
      localStorage.setItem(STORAGE_KEY_INTENSITY, String(value));
      this.dispatchEvent(new Event('intensitychange'));
    }
  }

  const sharpness = new SharpnessFilter();
  sharpness.init();

  function addMenuControls(menu) {
    if (menu.dataset.sharpEnhanceDone) return;
    menu.dataset.sharpEnhanceDone = 'true';

    let container = menu.querySelector(FILTER_ITEM_SELECTOR);
    if (!container) {
      container = document.createElement('div');
      container.className = 'pzp-ui-setting-home-item';
      container.setAttribute('role', 'menuitem');
      container.tabIndex = 0;
      menu.append(container);
    }
    container.classList.add('pzp-ui-setting-home-item');

    container.innerHTML = `
      <div class="pzp-ui-setting-home-item__top">
        <div class="pzp-ui-setting-home-item__left">
          <span class="pzp-ui-setting-home-item__label">선명한 화면</span>
        </div>
        <div class="pzp-ui-setting-home-item__right">
          <div role="switch" class="pzp-ui-toggle sharp-toggle-wrapper"
               aria-label="샤프닝 필터 토글"
               aria-checked="${sharpness.enabled}" tabindex="0">
            <input type="checkbox" class="pzp-ui-toggle__checkbox sharp-toggle" tabindex="-1">
            <div class="pzp-ui-toggle__handle"></div>
          </div>
        </div>
      </div>
      <div class="pzp-ui-setting-home-item__bottom" style="padding:8px; display:flex; align-items:center; gap:8px;">
        <label for="sharp-slider" class="visually-hidden">강도 조절</label>
        <input id="sharp-slider" type="range" min="1" max="3" step="0.1"
               class="sharp-slider" role="slider"
               aria-valuemin="1" aria-valuemax="3"
               aria-valuenow="${sharpness.intensity.toFixed(1)}"
               aria-valuetext="강도 ${sharpness.intensity.toFixed(1)} 배">
        <span id="sharp-intensity-label">(${sharpness.intensity.toFixed(1)}x 배)</span>
      </div>`;

    const wrapper  = container.querySelector('.sharp-toggle-wrapper');
    const checkbox = container.querySelector('.sharp-toggle');
    const slider   = container.querySelector('#sharp-slider');
    const label    = container.querySelector('#sharp-intensity-label');

    // 초기 UI 반영
    checkbox.checked = sharpness.enabled;
    wrapper.setAttribute('aria-checked', sharpness.enabled);
    slider.value = sharpness.intensity;
    slider.style.accentColor = sharpness.enabled ? '#00f889' : 'gray';
    label.textContent = `(${sharpness.intensity.toFixed(1)}x 배)`;

    // mousedown 막아 메뉴 닫힘 방지
    wrapper.addEventListener('mousedown', e => e.stopPropagation());

    // 클릭/키보드로 토글
    wrapper.addEventListener('click', e => { e.stopPropagation(); sharpness.toggle(); });
    wrapper.addEventListener('keydown', e => {
      if (['Enter',' '].includes(e.key)) {
        e.preventDefault();
        e.stopPropagation();
        sharpness.toggle();
      }
    });

    // 상태 변화 시 애니메이션 트리거 & 속성 업데이트
    sharpness.addEventListener('enabled', () => {
      checkbox.checked = true;
      wrapper.setAttribute('aria-checked', 'true');
      slider.style.accentColor = '#00f889';
    });
    sharpness.addEventListener('disabled', () => {
      checkbox.checked = false;
      wrapper.setAttribute('aria-checked', 'false');
      slider.style.accentColor = 'gray';
    });
    sharpness.addEventListener('intensitychange', () => {
      label.textContent = `(${sharpness.intensity.toFixed(1)}x 배)`;
    });

    // 슬라이더 입력 처리
    slider.addEventListener('input', e => {
      const v = parseFloat(e.target.value);
      sharpness.setIntensity(v);
      slider.setAttribute('aria-valuenow', v.toFixed(1));
      slider.setAttribute('aria-valuetext', `강도 ${v.toFixed(1)} 배`);
      label.textContent = `(${v.toFixed(1)}x 배)`;
    });
    slider.addEventListener('keydown', e => {
      let v = sharpness.intensity;
      if (['ArrowRight','ArrowUp'].includes(e.key)) v = Math.min(v + 0.1, 3);
      else if (['ArrowLeft','ArrowDown'].includes(e.key)) v = Math.max(v - 0.1, 1);
      else return;
      e.preventDefault();
      sharpness.setIntensity(v);
      slider.value = v;
      slider.setAttribute('aria-valuenow', v.toFixed(1));
      slider.setAttribute('aria-valuetext', `강도 ${v.toFixed(1)} 배`);
      label.textContent = `(${v.toFixed(1)}x 배)`;
    });
  }

  const observer = new MutationObserver(muts => {
    for (const m of muts) for (const node of m.addedNodes) {
      if (!(node instanceof HTMLElement)) continue;
      const menu = node.matches(MENU_SELECTOR) ? node : node.querySelector(MENU_SELECTOR);
      menu && addMenuControls(menu);
    }
  });
  observer.observe(document.body, { childList:true, subtree:true });
  document.querySelector(MENU_SELECTOR) && addMenuControls(document.querySelector(MENU_SELECTOR));

  console.log('%c[🔧 샤프닝 필터 v2.0 준비 완료]', 'color:skyblue;font-weight:bold;');
})();