页面灰度调节器

点击屏幕左侧按钮中部按钮,可展开灰度调节面板

// ==UserScript==
// @name         页面灰度调节器
// @name:zh-CN   页面灰度调节器
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  点击屏幕左侧按钮中部按钮,可展开灰度调节面板
// @description:zh-CN  点击屏幕左侧按钮中部按钮,可展开灰度调节面板
// @author       yonlin zhu
// @match        *://*/*
// @license      MIT
// @run-at       document-end
// @grant        none
// ==/UserScript==

(function () {
    const KEY_PREFIX = '__tm_grayscale_level__:';
    const CONTROL_ID = 'tm-gray-control';

    if (document.getElementById(CONTROL_ID)) return;

    const host = location.hostname;
    const baseDomain = getBaseDomain(host);
    const STORAGE_KEY = KEY_PREFIX + baseDomain;

    const stored = localStorage.getItem(STORAGE_KEY);
    let grayLevel = stored !== null ? clamp(parseInt(stored, 10), 0, 100) : 0;
    applyGrayscale(grayLevel);

    const root = document.createElement('div');
    root.id = CONTROL_ID;
    root.innerHTML = `
      <div class="tg-panel">
        <div class="tg-btn" title="点击调节灰度">
          <span class="tg-btn-text">灰</span>
        </div>
        <div class="tg-slider-wrap" style="display:none;">
          <div class="tg-row">
            <input type="range" min="0" max="100" value="${grayLevel}" class="tg-range"/>
            <span class="tg-val">${grayLevel}%</span>
          </div>
          <div class="tg-tip">主域: ${baseDomain}<br/>离开 1 秒收起</div>
        </div>
      </div>
    `;
    document.documentElement.appendChild(root);

    const style = document.createElement('style');
    style.textContent = `
#${CONTROL_ID} {
  position: fixed;
  top: 40%;
  left: 0;
  z-index: 2147483647;
  font-family: system-ui, sans-serif;
  pointer-events: none;
}
#${CONTROL_ID} * { box-sizing: border-box; }

#${CONTROL_ID} .tg-panel {
  position: relative;
  width: 280px;
  height: 78px;
  pointer-events: auto;
}
#${CONTROL_ID} .tg-panel:not(.expanded) {
  width: 56px;
  height: 56px;
  transform: translateX(-28px);
  transition: transform .25s ease;
}
#${CONTROL_ID} .tg-panel.peek:not(.expanded) {
  transform: translateX(0);
}
#${CONTROL_ID} .tg-panel.expanded {
  transform: translateX(8px);
  width: 260px;
  height: 78px;
  transition: transform .25s ease;
}

#${CONTROL_ID} .tg-btn {
  position: relative;
  width: 40px;
  height: 40px;
  cursor: pointer;
  background: transparent;
}
#${CONTROL_ID} .tg-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: radial-gradient(circle at 30% 30%, #ffffffdd, #d0d0d0);
  box-shadow: 0 2px 6px rgba(0,0,0,.28);
  backdrop-filter: blur(4px);
  -webkit-backdrop-filter: blur(4px);
}
#${CONTROL_ID} .tg-btn-text {
  position: relative;
  width: 100%; height: 100%;
  display:flex;align-items:center;justify-content:center;
  font-size: 12px; font-weight: 600; color:#333; letter-spacing:2px;
  pointer-events:none; text-shadow:0 1px 2px #fff;
}

#${CONTROL_ID} .tg-panel.expanded .tg-btn { display:none; }

#${CONTROL_ID} .tg-slider-wrap {
  position: absolute;
  inset: 0;
  background: rgba(255,255,255,0.9);
  border-radius: 14px;
  padding: 10px 14px 8px 66px;
  box-shadow: 0 4px 14px rgba(0,0,0,.25);
  display: flex;
  flex-direction: column;
  gap: 4px;
  animation: fadeIn .18s ease;
}

#${CONTROL_ID} .tg-row {
  display:flex;
  align-items:center;
  gap:8px;
}
#${CONTROL_ID} .tg-range {
  -webkit-appearance: none;
  appearance: none;
  width: 160px;
  height: 6px;
  border-radius: 3px;
  background: linear-gradient(90deg,#666,#bbb);
  outline: none;
  cursor: pointer;
}
#${CONTROL_ID} .tg-range::-webkit-slider-thumb {
  -webkit-appearance: none;
  width:16px; height:16px;
  border-radius:50%;
  background:#fff;
  border:2px solid #444;
  box-shadow:0 0 0 2px #ffffff99;
  cursor:pointer;
  transition: transform .15s;
}
#${CONTROL_ID} .tg-range:active::-webkit-slider-thumb { transform: scale(1.1); }

#${CONTROL_ID} .tg-val {
  min-width: 52px;
  font-size:13px;
  font-weight:600;
  text-align:right;
  color:#222;
}

#${CONTROL_ID} .tg-tip {
  font-size:10px;
  line-height:1.2;
  color:#555;
  text-align:right;
  padding-right:2px;
  word-break: break-all;
}

@keyframes fadeIn {
  from { opacity:0; transform:translateY(4px);}
  to   { opacity:1; transform:translateY(0);}
}
`;
    document.head.appendChild(style);

    const panel      = root.querySelector('.tg-panel');
    const btn        = root.querySelector('.tg-btn');
    const sliderWrap = root.querySelector('.tg-slider-wrap');
    const range      = root.querySelector('.tg-range');
    const valEl      = root.querySelector('.tg-val');

    let expanded = false;
    let collapseTimer = null;
    let peekOutTimer = null;
    let peekRetractTimer = null;

    function expand() {
        if (expanded) return;
        expanded = true;
        panel.classList.add('expanded');
        sliderWrap.style.display = 'flex';
        clearTimers();
    }

    function collapse() {
        if (!expanded) return;
        expanded = false;
        panel.classList.remove('expanded');
        sliderWrap.style.display = 'none';
        // 折叠后若鼠标不在面板上,安排半隐藏
        schedulePeekOff(160);
    }

    function clearTimers() {
        clearTimeout(collapseTimer);
        clearTimeout(peekOutTimer);
        clearTimeout(peekRetractTimer);
    }

    // 进入:立即展示按钮完整(添加 peek)
    panel.addEventListener('pointerenter', () => {
        clearTimers();
        if (!expanded) panel.classList.add('peek');
    });

    // 离开:展开状态 → 1 秒后折叠;非展开则轻微延迟恢复半隐藏
    panel.addEventListener('pointerleave', () => {
        if (expanded) {
            collapseTimer = setTimeout(() => {
                if (!panel.matches(':hover')) collapse();
            }, 1000);
        } else {
            // 非展开状态下离开,安排收回半隐藏
            schedulePeekOff(140);
        }
    });

    btn.addEventListener('click', (e) => {
        e.stopPropagation();
        expand();
    });

    range.addEventListener('input', () => {
        grayLevel = clamp(parseInt(range.value, 10), 0, 100);
        valEl.textContent = grayLevel + '%';
        applyGrayscale(grayLevel);
        localStorage.setItem(STORAGE_KEY, String(grayLevel));
    });

    document.addEventListener('click', (e) => {
        if (!panel.contains(e.target) && expanded) {
            collapse();
        }
    });

    function schedulePeekOff(delay) {
        clearTimeout(peekRetractTimer);
        // 如果此刻还在 hover,就不收回
        if (panel.matches(':hover') || expanded) return;
        peekRetractTimer = setTimeout(() => {
            if (!panel.matches(':hover') && !expanded) {
                panel.classList.remove('peek');
            }
        }, delay);
    }

    function applyGrayscale(level) {
        document.documentElement.style.filter = level ? `grayscale(${level}%)` : '';
    }

    function clamp(n, min, max) {
        return n < min ? min : (n > max ? max : n);
    }

    function getBaseDomain(h) {
        if (!h) return 'unknown';
        const lower = h.toLowerCase();
        if (lower === 'localhost') return lower;
        if (/^(\d{1,3}\.){3}\d{1,3}$/.test(lower)) return lower;
        if (/^\[?[a-f0-9:]+\]?$/.test(lower)) return lower;
        const parts = lower.split('.');
        if (parts.length <= 2) return lower;

        const multiSuffixes = [
            'com.cn','net.cn','org.cn','gov.cn','edu.cn','ac.cn',
            'co.uk','org.uk','gov.uk','ltd.uk','plc.uk',
            'com.hk','com.tw'
        ];
        const last2 = parts.slice(-2).join('.');
        const last3 = parts.slice(-3).join('.');
        if (multiSuffixes.includes(last2)) {
            return parts.slice(-3).join('.');
        }
        if (multiSuffixes.includes(last3)) {
            return parts.slice(-4).join('.');
        }
        return last2;
    }
})();