WNACG Read Helper

wnacg阅读页增强:自动隐藏顶栏、悬浮球、右键设置菜单(记住阅读模式/翻页方向/动画偏好)、Dark Reader同步

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         WNACG Read Helper
// @namespace    https://wnacg.com/
// @version      1.2.1
// @license      MIT
// @description  wnacg阅读页增强:自动隐藏顶栏、悬浮球、右键设置菜单(记住阅读模式/翻页方向/动画偏好)、Dark Reader同步
// @author       You
// @match        https://wnacg.com/photos-slide-*
// @match        https://*.wnacg.com/photos-slide-*
// @grant        GM_addStyle
// @run-at       document-idle
// ==/UserScript==

(function () {
  'use strict';

  const HOVER_ZONE_HEIGHT = 30;
  const HIDE_DELAY = 800;
  const FAB_SIZE = 40;
  const SETTINGS_KEY = 'wnacg_autohide_settings';

  const topBar = document.getElementById('top-bar');
  if (!topBar) return;

  // --- 设置存储 ---
  function loadSettings() {
    try {
      return JSON.parse(localStorage.getItem(SETTINGS_KEY)) || {};
    } catch { return {}; }
  }

  function saveSettingsData(data) {
    localStorage.setItem(SETTINGS_KEY, JSON.stringify(data));
  }

  // --- 等待网站 reader 对象就绪 ---
  function waitForReader(callback, maxWait = 5000) {
    const start = Date.now();
    const check = () => {
      if (typeof reader !== 'undefined' && reader.setMode) {
        callback(reader);
      } else if (Date.now() - start < maxWait) {
        setTimeout(check, 100);
      }
    };
    check();
  }

  // --- 注入样式 ---
  GM_addStyle(`
    #top-bar.autohide-hidden {
      transform: translateY(-100%) !important;
      pointer-events: none;
    }
    #top-bar.autohide-visible {
      transform: translateY(0) !important;
      pointer-events: auto;
    }

    #autohide-hover-zone {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: ${HOVER_ZONE_HEIGHT}px;
      z-index: 99998;
    }

    #autohide-fab {
      position: fixed;
      bottom: 20px;
      right: 20px;
      width: ${FAB_SIZE}px;
      height: ${FAB_SIZE}px;
      border-radius: 50%;
      background: rgba(0, 0, 0, 0.45);
      backdrop-filter: blur(8px);
      border: 1px solid rgba(255, 255, 255, 0.15);
      color: #fff;
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      z-index: 100000;
      user-select: none;
      -webkit-user-select: none;
      touch-action: none;
      transition: background 0.2s, transform 0.15s;
      box-shadow: 0 2px 10px rgba(0,0,0,0.3);
    }
    #autohide-fab:active {
      transform: scale(0.9);
    }
    #autohide-fab.bar-visible {
      background: rgba(255, 255, 255, 0.25);
    }

    /* --- 设置菜单样式 --- */
    #autohide-settings {
      position: fixed;
      z-index: 100001;
      background: rgba(30, 30, 30, 0.9);
      backdrop-filter: blur(12px);
      border: 1px solid rgba(255, 255, 255, 0.15);
      border-radius: 12px;
      padding: 14px 16px;
      color: #eee;
      font-size: 13px;
      min-width: 180px;
      box-shadow: 0 4px 20px rgba(0,0,0,0.4);
      display: none;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
    }
    #autohide-settings.show {
      display: block;
    }
    #autohide-settings .menu-title {
      font-size: 11px;
      color: rgba(255,255,255,0.5);
      margin-bottom: 8px;
      letter-spacing: 0.5px;
      text-transform: uppercase;
    }
    #autohide-settings .menu-group {
      margin-bottom: 12px;
    }
    #autohide-settings .menu-group:last-child {
      margin-bottom: 0;
    }
    #autohide-settings .menu-item {
      display: flex;
      align-items: center;
      padding: 6px 8px;
      border-radius: 6px;
      cursor: pointer;
      transition: background 0.15s;
      gap: 8px;
    }
    #autohide-settings .menu-item:hover {
      background: rgba(255,255,255,0.1);
    }
    #autohide-settings .menu-item.active {
      background: rgba(255,255,255,0.15);
    }
    #autohide-settings .menu-item.disabled {
      opacity: 0.4;
      cursor: default;
    }
    #autohide-settings .menu-item.disabled:hover {
      background: none;
    }
    #autohide-settings .radio {
      width: 14px;
      height: 14px;
      border-radius: 50%;
      border: 2px solid rgba(255,255,255,0.5);
      flex-shrink: 0;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    #autohide-settings .radio.checked {
      border-color: #6cb4ee;
    }
    #autohide-settings .radio.checked::after {
      content: '';
      width: 6px;
      height: 6px;
      border-radius: 50%;
      background: #6cb4ee;
    }
    #autohide-settings .toggle-track {
      width: 34px;
      height: 18px;
      border-radius: 9px;
      background: rgba(255,255,255,0.2);
      position: relative;
      flex-shrink: 0;
      transition: background 0.2s;
    }
    #autohide-settings .toggle-track.on {
      background: #6cb4ee;
    }
    #autohide-settings .toggle-thumb {
      width: 14px;
      height: 14px;
      border-radius: 50%;
      background: #fff;
      position: absolute;
      top: 2px;
      left: 2px;
      transition: left 0.2s;
    }
    #autohide-settings .toggle-track.on .toggle-thumb {
      left: 18px;
    }
    #autohide-settings .menu-sep {
      height: 1px;
      background: rgba(255,255,255,0.1);
      margin: 8px 0;
    }
    #autohide-settings .hint {
      font-size: 11px;
      color: rgba(255,255,255,0.4);
      margin-top: 2px;
      padding-left: 22px;
    }
  `);

  let barVisible = true;
  let hideTimer = null;
  const isMobile = /Android|iPhone|iPad|iPod|Mobile/i.test(navigator.userAgent);

  function hideBar() {
    topBar.classList.remove('autohide-visible');
    topBar.classList.add('autohide-hidden');
    barVisible = false;
    updateFab();
  }

  function showBar() {
    topBar.classList.remove('autohide-hidden');
    topBar.classList.add('autohide-visible');
    barVisible = true;
    updateFab();
  }

  function toggleBar() {
    barVisible ? hideBar() : showBar();
  }

  // --- 悬浮球(PC + 移动端) ---
  const fab = document.createElement('div');
  fab.id = 'autohide-fab';
  const SVG_CHECKED = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 19.5C16.1421 19.5 19.5 16.1421 19.5 12C19.5 7.85786 16.1421 4.5 12 4.5C7.85786 4.5 4.5 7.85786 4.5 12C4.5 16.1421 7.85786 19.5 12 19.5ZM12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" fill="#fff"/><circle cx="12" cy="12" r="5.25" fill="#fff"/></svg>`;
  const SVG_UNCHECKED = `<svg width="22" height="22" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M12 19.5C16.1421 19.5 19.5 16.1421 19.5 12C19.5 7.85786 16.1421 4.5 12 4.5C7.85786 4.5 4.5 7.85786 4.5 12C4.5 16.1421 7.85786 19.5 12 19.5ZM12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21Z" fill="#fff"/></svg>`;

  fab.innerHTML = SVG_CHECKED;
  document.body.appendChild(fab);

  // 恢复上次悬浮球位置
  (function restoreFabPosition() {
    const settings = loadSettings();
    if (settings.fabX != null && settings.fabY != null) {
      const x = Math.max(0, Math.min(window.innerWidth - FAB_SIZE, settings.fabX));
      const y = Math.max(0, Math.min(window.innerHeight - FAB_SIZE, settings.fabY));
      fab.style.left = x + 'px';
      fab.style.top = y + 'px';
      fab.style.right = 'auto';
      fab.style.bottom = 'auto';
    }
  })();

  function updateFab() {
    fab.innerHTML = barVisible ? SVG_CHECKED : SVG_UNCHECKED;
    fab.classList.toggle('bar-visible', barVisible);
  }

  // --- 设置菜单 ---
  const MODE_OPTIONS = [
    { value: null, label: '跟随网站' },
    { value: 'vertical', label: '下拉模式' },
    { value: 'single', label: '单页模式' },
    { value: 'double', label: '双页模式' },
  ];

  const DIR_OPTIONS = [
    { value: null, label: '跟随网站' },
    { value: 'rtl', label: '日漫 (←)' },
    { value: 'ltr', label: '美漫 (→)' },
  ];

  const ANIM_OPTIONS = [
    { value: null, label: '跟随网站' },
    { value: 'slide', label: '滑动' },
    { value: 'fade', label: '淡入' },
  ];

  const settingsMenu = document.createElement('div');
  settingsMenu.id = 'autohide-settings';
  document.body.appendChild(settingsMenu);

  function isDarkReaderActive() {
    return !!(document.querySelector('meta[name="darkreader"]') ||
              document.querySelector('.darkreader') ||
              document.querySelector('style.darkreader'));
  }

  function renderRadioGroup(title, options, currentValue, actionName) {
    let html = '<div class="menu-group">';
    html += `<div class="menu-title">${title}</div>`;
    options.forEach(opt => {
      const isActive = currentValue === opt.value;
      html += `<div class="menu-item${isActive ? ' active' : ''}" data-action="${actionName}" data-value="${opt.value}">`;
      html += `<div class="radio${isActive ? ' checked' : ''}"></div>`;
      html += `<span>${opt.label}</span>`;
      html += '</div>';
    });
    html += '</div>';
    return html;
  }

  function renderMenu() {
    const settings = loadSettings();
    const drSync = settings.darkReaderSync || false;
    const drDetected = isDarkReaderActive();

    let html = '';

    html += renderRadioGroup('默认阅读模式', MODE_OPTIONS, settings.defaultMode || null, 'mode');
    html += '<div class="menu-sep"></div>';
    html += renderRadioGroup('翻页方向(单页/双页)', DIR_OPTIONS, settings.defaultDirection || null, 'direction');
    html += '<div class="menu-sep"></div>';
    html += renderRadioGroup('翻页动画(单页/双页)', ANIM_OPTIONS, settings.defaultAnim || null, 'anim');
    html += '<div class="menu-sep"></div>';

    // Dark Reader 同步
    html += '<div class="menu-group">';
    html += '<div class="menu-title">Dark Reader 同步</div>';
    html += `<div class="menu-item" data-action="dr-sync">`;
    html += `<span style="flex:1">自动同步主题</span>`;
    html += `<div class="toggle-track${drSync ? ' on' : ''}">`;
    html += `<div class="toggle-thumb"></div>`;
    html += '</div></div>';
    html += `<div class="hint">${drDetected ? 'Dark Reader 已激活' : '未检测到 Dark Reader(启用后自动生效)'}</div>`;
    html += '</div>';

    settingsMenu.innerHTML = html;
  }

  function positionMenu() {
    const fabRect = fab.getBoundingClientRect();
    const menuW = 210;
    const menuH = settingsMenu.offsetHeight || 250;

    let left = fabRect.left - menuW - 10;
    let top = fabRect.top - menuH + FAB_SIZE;

    if (left < 10) left = fabRect.right + 10;
    if (top < 10) top = 10;
    if (top + menuH > window.innerHeight - 10) top = window.innerHeight - menuH - 10;

    settingsMenu.style.left = left + 'px';
    settingsMenu.style.top = top + 'px';
  }

  function openMenu() {
    renderMenu();
    settingsMenu.classList.add('show');
    positionMenu();
  }

  function closeMenu() {
    settingsMenu.classList.remove('show');
  }

  function isMenuOpen() {
    return settingsMenu.classList.contains('show');
  }

  // 菜单点击处理
  const ACTION_TO_KEY = {
    mode: 'defaultMode',
    direction: 'defaultDirection',
    anim: 'defaultAnim',
  };

  settingsMenu.addEventListener('click', (e) => {
    const item = e.target.closest('.menu-item');
    if (!item || item.classList.contains('disabled')) return;

    const action = item.dataset.action;
    const settings = loadSettings();

    if (ACTION_TO_KEY[action]) {
      const val = item.dataset.value === 'null' ? null : item.dataset.value;
      settings[ACTION_TO_KEY[action]] = val;
      saveSettingsData(settings);
      renderMenu();
    } else if (action === 'dr-sync') {
      settings.darkReaderSync = !settings.darkReaderSync;
      saveSettingsData(settings);
      renderMenu();
      if (settings.darkReaderSync) {
        syncThemeWithDarkReader();
      }
    }
  });

  // 右键打开设置菜单
  fab.addEventListener('contextmenu', (e) => {
    e.preventDefault();
    e.stopPropagation();
    if (isMenuOpen()) {
      closeMenu();
    } else {
      openMenu();
    }
  });

  // 长按也可打开设置菜单(移动端)
  let longPressTimer = null;
  fab.addEventListener('touchstart', (e) => {
    longPressTimer = setTimeout(() => {
      longPressTimer = null;
      if (!hasMoved) {
        e.preventDefault();
        if (isMenuOpen()) closeMenu(); else openMenu();
        hasMoved = true; // 防止 touchend 触发 toggleBar
      }
    }, 500);
  }, { passive: false });

  fab.addEventListener('touchmove', () => {
    if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
  });

  fab.addEventListener('touchend', () => {
    if (longPressTimer) { clearTimeout(longPressTimer); longPressTimer = null; }
  });

  // 点击外部关闭菜单
  document.addEventListener('mousedown', (e) => {
    if (isMenuOpen() && !settingsMenu.contains(e.target) && !fab.contains(e.target)) {
      closeMenu();
    }
  });
  document.addEventListener('touchstart', (e) => {
    if (isMenuOpen() && !settingsMenu.contains(e.target) && !fab.contains(e.target)) {
      closeMenu();
    }
  }, { passive: true });

  // 悬浮球拖拽支持
  let isDragging = false;
  let dragStartX, dragStartY, fabStartX, fabStartY;
  let hasMoved = false;

  function onDragStart(e) {
    isDragging = true;
    hasMoved = false;
    const touch = e.touches ? e.touches[0] : e;
    dragStartX = touch.clientX;
    dragStartY = touch.clientY;
    const rect = fab.getBoundingClientRect();
    fabStartX = rect.left;
    fabStartY = rect.top;
    fab.style.transition = 'none';
  }

  function onDragMove(e) {
    if (!isDragging) return;
    const touch = e.touches ? e.touches[0] : e;
    const dx = touch.clientX - dragStartX;
    const dy = touch.clientY - dragStartY;
    if (Math.abs(dx) > 3 || Math.abs(dy) > 3) hasMoved = true;
    if (!hasMoved) return;
    e.preventDefault();
    const x = Math.max(0, Math.min(window.innerWidth - FAB_SIZE, fabStartX + dx));
    const y = Math.max(0, Math.min(window.innerHeight - FAB_SIZE, fabStartY + dy));
    fab.style.left = x + 'px';
    fab.style.top = y + 'px';
    fab.style.right = 'auto';
    fab.style.bottom = 'auto';
  }

  function onDragEnd() {
    if (!isDragging) return;
    isDragging = false;
    fab.style.transition = '';
    if (!hasMoved) {
      toggleBar();
    } else {
      const settings = loadSettings();
      settings.fabX = parseInt(fab.style.left);
      settings.fabY = parseInt(fab.style.top);
      saveSettingsData(settings);
    }
  }

  fab.addEventListener('mousedown', onDragStart);
  document.addEventListener('mousemove', onDragMove);
  document.addEventListener('mouseup', onDragEnd);
  fab.addEventListener('touchstart', onDragStart, { passive: false });
  document.addEventListener('touchmove', onDragMove, { passive: false });
  document.addEventListener('touchend', onDragEnd);

  // --- PC端:鼠标悬停顶部唤醒 ---
  if (!isMobile) {
    const hoverZone = document.createElement('div');
    hoverZone.id = 'autohide-hover-zone';
    document.body.appendChild(hoverZone);

    hoverZone.addEventListener('mouseenter', () => {
      clearTimeout(hideTimer);
      showBar();
    });

    topBar.addEventListener('mouseenter', () => {
      clearTimeout(hideTimer);
    });

    topBar.addEventListener('mouseleave', () => {
      hideTimer = setTimeout(hideBar, HIDE_DELAY);
    });

    hoverZone.addEventListener('mouseleave', () => {
      hideTimer = setTimeout(hideBar, HIDE_DELAY);
    });
  }

  // --- Dark Reader 同步 ---
  function getCurrentSiteTheme() {
    return document.documentElement.dataset.theme || 'light';
  }

  function syncThemeWithDarkReader() {
    const settings = loadSettings();
    if (!settings.darkReaderSync) return;

    const drActive = isDarkReaderActive();
    const siteTheme = getCurrentSiteTheme();

    // Dark Reader 激活 → 网站应为 dark;Dark Reader 未激活 → 网站应为 light
    if (drActive && siteTheme === 'light') {
      waitForReader(r => r.toggleTheme());
    } else if (!drActive && siteTheme === 'dark') {
      waitForReader(r => r.toggleTheme());
    }
  }

  // 始终监听 <head> 变化,这样即使 Dark Reader 之后才安装/启用也能捕获到
  // Dark Reader 开关时会在 <head> 中增删 style.darkreader 和 meta[name="darkreader"]
  let drSyncDebounce = null;
  const headObserver = new MutationObserver(() => {
    clearTimeout(drSyncDebounce);
    drSyncDebounce = setTimeout(syncThemeWithDarkReader, 50);
  });

  headObserver.observe(document.head, {
    childList: true,
    subtree: false,
  });

  // --- 读取网站当前设置 ---
  function getSiteSettings() {
    try {
      return JSON.parse(localStorage.getItem('wnacg_reader_settings') || '{}');
    } catch { return {}; }
  }

  // --- 初始化:应用所有默认设置 ---
  const userSettings = loadSettings();

  waitForReader(r => {
    const site = getSiteSettings();

    // 阅读模式
    if (userSettings.defaultMode && site.mode !== userSettings.defaultMode) {
      r.setMode(userSettings.defaultMode);
    }

    // 翻页方向(toggle 式,需要比较后决定是否切换)
    if (userSettings.defaultDirection && site.direction !== userSettings.defaultDirection) {
      r.toggleDir();
    }

    // 翻页动画
    if (userSettings.defaultAnim && site.anim !== userSettings.defaultAnim) {
      r.toggleAnim();
    }
  });

  // 初始 Dark Reader 同步
  syncThemeWithDarkReader();

  // --- 初始自动隐藏(延迟一点让页面加载完) ---
  setTimeout(hideBar, 1000);
})();