wnacg阅读页增强:自动隐藏顶栏、悬浮球、右键设置菜单(记住阅读模式/翻页方向/动画偏好)、Dark Reader同步
// ==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);
})();