// ==UserScript==
// @name E-Hentai 实用增强:回顶/到底 + [ ] 翻页 + 自动主题切换
// @name:en E-Hentai Tweaks: Scroll Buttons + [ ] Paging + Auto Theme Switching
// @namespace https://greasyfork.org/users/1508871-vesper233
// @version 4.5
// @description 悬浮回顶/到底;全站 [ 与 ] 快捷翻页;自动主题切换
// @description:en Scroll to Top/Bottom buttons; [ and ] for Prev/Next page; Auto Theme Switching
// @author Vesper233
// @match *://e-hentai.org/*
// @match *://exhentai.org/*
// @match *://*.e-hentai.org/*
// @match *://upld.e-hentai.org/*
// @match *://upload.e-hentai.org/*
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
/* =========================
* 偏好:跨子域同步
* ========================= */
const EH_DOMAIN =
location.hostname.endsWith('.e-hentai.org') ? '.e-hentai.org' :
location.hostname === 'e-hentai.org' ? '.e-hentai.org' :
location.hostname === 'exhentai.org' ? 'exhentai.org' :
null;
const LS_KEY = 'eh-dark-mode-enabled';
const CK_KEY = 'eh_dark';
const MODE_KEY = 'eh-dark-mode-pref';
const MODE_AUTO = 'auto';
const MODE_DARK = 'dark';
const MODE_LIGHT = 'light';
const MODE_SEQUENCE = [MODE_AUTO, MODE_DARK, MODE_LIGHT];
const mediaQuery = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
const LIGHT_BG = '#E2E0D2';
const LIGHT_TEXT = '#1f1f1f';
const DARK_BG = '#34353A';
const DARK_TEXT = '#f1f1f1';
let currentMode;
let systemListenerAttached = false;
let darkToggleBtn;
const readCookie = (k) =>
document.cookie.split('; ').find(s => s.startsWith(k + '='))?.split('=')[1];
const writeCookie = (k, v, days = 365, domain = EH_DOMAIN) => {
const d = new Date();
d.setTime(d.getTime() + days * 864e5);
document.cookie = `${k}=${v}; expires=${d.toUTCString()}; path=/` + (domain ? `; domain=${domain}` : '');
};
const applyDark = (on) => document.documentElement.classList.toggle('eh-dark', !!on);
const setPref = (on) => {
localStorage.setItem(LS_KEY, on ? '1' : '0');
// 只能在 *.e-hentai.org 同步 cookie;exhentai 独立域名用本地存储
if (EH_DOMAIN && EH_DOMAIN.includes('e-hentai.org')) writeCookie(CK_KEY, on ? '1' : '0');
};
const getPref = () => {
const ls = localStorage.getItem(LS_KEY);
if (ls !== null) return ls === '1';
const ck = (EH_DOMAIN && EH_DOMAIN.includes('e-hentai.org')) ? readCookie(CK_KEY) : null;
if (ck !== undefined && ck !== null) return ck === '1';
return null;
};
const resolveSystemDark = () => mediaQuery ? mediaQuery.matches : true;
const resolveEffectiveMode = (mode) => {
if (mode === MODE_AUTO) return resolveSystemDark() ? MODE_DARK : MODE_LIGHT;
return mode === MODE_LIGHT ? MODE_LIGHT : MODE_DARK;
};
const updateSystemListener = () => {
if (!mediaQuery) return;
const handler = (event) => {
if (currentMode === MODE_AUTO) applyMode(MODE_AUTO, { persist: false });
};
if (currentMode === MODE_AUTO && !systemListenerAttached) {
if (mediaQuery.addEventListener) mediaQuery.addEventListener('change', handler);
else mediaQuery.addListener(handler);
systemListenerAttached = handler;
}
if (currentMode !== MODE_AUTO && systemListenerAttached) {
if (mediaQuery.removeEventListener) mediaQuery.removeEventListener('change', systemListenerAttached);
else mediaQuery.removeListener(systemListenerAttached);
systemListenerAttached = false;
}
};
const updateToggleVisual = (mode, effective) => {
if (!darkToggleBtn) return;
darkToggleBtn.style.border = 'none';
let shadow = '0 0 0 1px rgba(0,0,0,0.45)';
if (mode === MODE_AUTO) {
darkToggleBtn.style.background = `linear-gradient(90deg, ${LIGHT_BG} 0 50%, ${DARK_BG} 50% 100%)`;
darkToggleBtn.style.color = DARK_TEXT;
shadow = '0 0 0 1px rgba(0,0,0,0.35)';
darkToggleBtn.style.textShadow = '0 0 4px rgba(0,0,0,0.35)';
} else if (mode === MODE_LIGHT) {
darkToggleBtn.style.background = LIGHT_BG;
darkToggleBtn.style.color = LIGHT_TEXT;
darkToggleBtn.style.textShadow = 'none';
shadow = '0 0 0 1px rgba(0,0,0,0.25)';
} else {
darkToggleBtn.style.background = DARK_BG;
darkToggleBtn.style.color = DARK_TEXT;
darkToggleBtn.style.textShadow = 'none';
shadow = '0 0 0 1px rgba(255,255,255,0.25)';
}
darkToggleBtn.style.boxShadow = shadow;
};
const updateToggleTooltip = (mode, effective) => {
if (!darkToggleBtn) return;
const labels = {
[MODE_AUTO]: '系统偏好',
[MODE_DARK]: '固定暗色',
[MODE_LIGHT]: '固定亮色'
};
const effectiveLabel = effective === MODE_DARK ? '暗色' : '亮色';
darkToggleBtn.title = `当前:${labels[mode]} (实际:${effectiveLabel})\n点击切换模式`;
};
const applyMode = (mode, { persist = true } = {}) => {
currentMode = mode;
const effective = resolveEffectiveMode(mode);
const isDark = effective === MODE_DARK;
applyDark(isDark);
setPref(isDark);
if (persist) localStorage.setItem(MODE_KEY, mode);
updateSystemListener();
updateToggleTooltip(mode, effective);
updateToggleVisual(mode, effective);
fixMonsterBox();
fixFavoritesUI();
};
const readInitialMode = () => {
const stored = localStorage.getItem(MODE_KEY);
if (stored === MODE_AUTO || stored === MODE_DARK || stored === MODE_LIGHT) return stored;
return MODE_AUTO;
};
const initDarkPref = () => {
const initialMode = readInitialMode();
applyMode(initialMode, { persist: true });
};
/* =========================
* 样式
* ========================= */
const styles = `
/* —— 悬浮按钮 —— */
.eh-scroll-btn{
position:fixed; width:45px; height:45px;
background-color:#3e3e3e; color:#dcdcdc;
border:none; border-radius:50%;
cursor:pointer; display:none; justify-content:center; align-items:center;
font-size:20px; font-weight:bold; z-index:9999; opacity:.85;
transition:opacity .2s ease, background-color .2s ease, transform .1s ease;
user-select:none; backdrop-filter:saturate(120%) blur(2px);
box-shadow:0 0 0 1px rgba(255,255,255,0.08);
}
.eh-scroll-btn:hover{ opacity:1; background-color:#575757; transform:translateY(-1px); }
#eh-to-top-btn{ right:25px; bottom:130px; }
#eh-to-bottom-btn{ right:25px; bottom:75px; }
#eh-dark-toggle-btn{ right:25px; top:20px; display:flex !important; font-size:18px; background-color:#2f2f2f; }
/* —— 暗色主题变量 —— */
html.eh-dark{
--eh-fg:#f1f1f1; --eh-bg:#34353b; --eh-panel:#4f535b;
--eh-panel-2:#3c414b; --eh-panel-3:#43464e;
--eh-border:#000000; --eh-link:#dddddd; --eh-link-hover:#eeeeee; --eh-muted:#8a8a8a;
}
html.eh-dark body{ color:var(--eh-fg) !important; background:var(--eh-bg) !important; }
html.eh-dark a{ color:var(--eh-link) !important; } html.eh-dark a:hover{ color:var(--eh-link-hover) !important; }
html.eh-dark input, html.eh-dark select, html.eh-dark option, html.eh-dark optgroup, html.eh-dark textarea{
color:var(--eh-fg) !important; background-color:var(--eh-bg) !important; border:2px solid #8d8d8d !important;
}
html.eh-dark ::placeholder{ color:var(--eh-muted) !important; -webkit-text-fill-color:var(--eh-muted) !important; }
/* ===== 通用容器 / 表格 / 列表 ===== */
html.eh-dark .stuffbox, html.eh-dark .ido, html.eh-dark .gm, html.eh-dark #gdt,
html.eh-dark .gt, html.eh-dark .gtl, html.eh-dark .gtw{
background:var(--eh-panel) !important; border:1px solid var(--eh-border) !important;
}
html.eh-dark table.itg{ border:2px ridge #3c3c3c !important; }
html.eh-dark table.itg > tbody > tr:nth-child(2n+1){ background:#363940 !important; }
html.eh-dark table.itg > tbody > tr:nth-child(2n+2){ background:#3c414b !important; }
html.eh-dark div.itg.gld,
html.eh-dark .itg.gld{
border-left:none !important;
background:var(--eh-panel) !important;
}
html.eh-dark table.mt{ border:1px solid var(--eh-border) !important; background:#40454b !important; }
html.eh-dark table.mt > tbody > tr:nth-child(2n+1){ background:#363940 !important; }
html.eh-dark table.mt > tbody > tr:nth-child(2n+2){ background:#3c414b !important; }
html.eh-dark td.itd{ border-right:1px solid #6f6f6f4d !important; }
html.eh-dark img.th, html.eh-dark .glthumb{ border:1px solid var(--eh-border) !important; }
/* ===== 分页条 ===== */
html.eh-dark table.ptt td, html.eh-dark table.ptb td{
background:var(--eh-bg) !important; border:1px solid var(--eh-border) !important; color:var(--eh-fg) !important;
}
html.eh-dark td.ptds{ background:var(--eh-panel-3) !important; color:#000 !important; }
/* ===== 首页/搜索区 ===== */
html.eh-dark #searchbox, html.eh-dark .searchnav, html.eh-dark .searchstuff, html.eh-dark .searchform, html.eh-dark .searchpane{
background:var(--eh-panel) !important; border:1px solid var(--eh-border) !important; color:var(--eh-fg) !important;
}
html.eh-dark hr{ border-color:var(--eh-border) !important; background:var(--eh-border) !important; }
html.eh-dark .glname, html.eh-dark .glname a{ color:var(--eh-fg) !important; }
html.eh-dark .gl1t{ border:none !important; }
html.eh-dark .gl1t:nth-child(2n+1){ background:#363940 !important; }
html.eh-dark .gl1t:nth-child(2n+2){ background:#3c414b !important; }
html.eh-dark .gl3t,
html.eh-dark .gl4t,
html.eh-dark .gl5t,
html.eh-dark .gl6t,
html.eh-dark .gl7t,
html.eh-dark .glthumb > div[style*="position:absolute"],
html.eh-dark .glthumb > div.gl3t{
background:var(--eh-bg) !important;
color:var(--eh-fg) !important;
border:1px solid var(--eh-border) !important;
box-shadow:none !important;
}
html.eh-dark .lc > span,
html.eh-dark .lr > span{
background-color:var(--eh-bg) !important;
border:2px solid #8d8d8d !important;
}
html.eh-dark .lc > span:after{
border:solid var(--eh-fg) !important;
}
html.eh-dark .lr > span:after{
background:var(--eh-fg) !important;
}
html.eh-dark .lc:hover input:enabled ~ span,
html.eh-dark .lr:hover input:enabled ~ span,
html.eh-dark .lc input:enabled:focus ~ span,
html.eh-dark .lr input:enabled:focus ~ span{
background-color:var(--eh-panel-3) !important;
border-color:#aeaeae !important;
}
html.eh-dark .lc input:disabled ~ span,
html.eh-dark .lr input:disabled ~ span{
border-color:#5c5c5c !important;
background-color:#2c2d32 !important;
}
/* ===== 画廊详情/缩略图区/信息条 ===== */
html.eh-dark #gmid, html.eh-dark #gd2, html.eh-dark #gdt, html.eh-dark .sni{ background:var(--eh-panel) !important; }
html.eh-dark #gd1 div, html.eh-dark #gdt img{ border:1px solid var(--eh-border) !important; }
html.eh-dark h1#gj{ color:#b8b8b8 !important; border-bottom:1px solid var(--eh-border) !important; }
/* ===== 评论区 ===== */
html.eh-dark #cdiv{
background:var(--eh-panel) !important;
color:var(--eh-fg) !important;
}
html.eh-dark #cdiv .c1{
background:var(--eh-panel) !important;
color:var(--eh-fg) !important;
border:none !important;
}
html.eh-dark #cdiv .c2{
background:var(--eh-bg) !important;
color:var(--eh-fg) !important;
border:none !important;
padding:6px 14px !important;
border-radius:4px !important;
}
html.eh-dark #cdiv .c3,
html.eh-dark #cdiv .c4,
html.eh-dark #cdiv .c5{
background:transparent !important;
color:var(--eh-fg) !important;
}
html.eh-dark #cdiv .c5 span{ color:var(--eh-fg) !important; }
html.eh-dark #cdiv .c6,
html.eh-dark #cdiv .c7{
background:var(--eh-panel) !important;
color:var(--eh-fg) !important;
}
html.eh-dark #postnewcomment,
html.eh-dark #formdiv,
html.eh-dark #formdiv textarea,
html.eh-dark #formdiv input[type="submit"]{
background:var(--eh-panel) !important;
color:var(--eh-fg) !important;
}
/* ===== torrents(gallerytorrents.php & torrents.php)===== */
html.eh-dark table#ett, html.eh-dark div#etd{ background:#43464e !important; border:1px solid #34353b !important; }
html.eh-dark #torrentinfo > div + div{ border-top-color:var(--eh-border) !important; }
html.eh-dark .torrent, html.eh-dark .tl, html.eh-dark .tr{ background:var(--eh-panel) !important; }
/* ===== 统计 stats.php(含 gid 子页)===== */
html.eh-dark .stuffbox table{ background:var(--eh-panel) !important; border-color:var(--eh-border) !important; }
html.eh-dark tr > td.stdk, html.eh-dark tr > td.stdv{ border-color:var(--eh-border) !important; }
html.eh-dark body.stats table th{ border-bottom-color:var(--eh-border) !important; }
/* ===== 上传管理(manage / managefolders / managegallery / act=new)===== */
html.eh-dark td.l{ border-bottom:1px solid #f1f1f1 !important; border-right:1px dashed #f1f1f1 !important; }
html.eh-dark td.r{ border-bottom:1px solid #f1f1f1 !important; }
html.eh-dark td#d{ border-right:1px dashed #f1f1f1 !important; }
html.eh-dark [id^="cell_"]{ background:#5f636b !important; border:1px solid #34353b !important; }
html.eh-dark .gb, html.eh-dark .gl, html.eh-dark .gf{ background:var(--eh-panel) !important; }
html.eh-dark .m_btn, html.eh-dark .uf_btn{ background:var(--eh-bg) !important; color:var(--eh-fg) !important; border:1px solid #8d8d8d !important; }
html.eh-dark tr.gtr td.l,
html.eh-dark tr.gtr td.r{ border-right:none !important; }
/* ===== 归档/下载(archiver.php)===== */
html.eh-dark #hathdl_form + table td{ border-color:var(--eh-fg) !important; }
html.eh-dark div#db{ border:1px solid var(--eh-border) !important; background:var(--eh-panel) !important; }
/* ===== 日常事件 / 遭遇提示面板 ===== */
html.eh-dark #eventpane{
background:var(--eh-panel) !important;
border:1px solid var(--eh-border) !important;
color:var(--eh-fg) !important;
}
html.eh-dark #eventpane p,
html.eh-dark #eventpane strong{
color:var(--eh-fg) !important;
}
/* ====== 遭遇怪物(Monster Encounter)===== */
html.eh-dark .eh-dark-monbox,
html.eh-dark .eh-dark-moonbox{
background:var(--eh-panel) !important;
color:var(--eh-fg) !important;
border:none !important;
box-shadow:none !important;
}
html.eh-dark .eh-dark-monbox a{ color:var(--eh-link) !important; text-decoration:underline; }
html.eh-dark #monsterpane,
html.eh-dark .monster,
html.eh-dark .monsterbox { background:var(--eh-panel) !important; color:var(--eh-fg) !important; border:1px solid var(--eh-border) !important; }
/* ====== 收藏页 favorites.php:分类 pill / Show All ====== */
/* 直接适配原生结构:div.fp / div.fp.fps */
html.eh-dark .fp{
background:transparent !important;
color:var(--eh-fg) !important;
border:none !important;
border-radius:16px !important;
}
html.eh-dark .fp:hover{
background:var(--eh-panel-3) !important;
}
html.eh-dark .fps{
background:transparent !important;
border:none !important;
font-weight:600 !important;
}
/* 兼容你此前的自定义类(两套选择器都可用) */
html.eh-dark .eh-dark-favpill{
background:transparent !important;
color:var(--eh-fg) !important;
border:none !important;
box-shadow:none !important;
border-radius:16px !important;
}
html.eh-dark .eh-dark-favpill a{ color:var(--eh-fg) !important; }
html.eh-dark .eh-dark-favpill[disabled],
html.eh-dark .eh-dark-favpill.disabled{
color:var(--eh-muted) !important; opacity:.65 !important;
}
`;
const style = document.createElement('style');
style.textContent = styles;
document.head.appendChild(style);
/* =========================
* 悬浮:顶 / 底 / 暗色开关
* ========================= */
const makeBtn = (id, text, title, pos) => {
const el = document.createElement('div');
el.id = id; el.className = 'eh-scroll-btn';
el.textContent = text; el.title = title;
Object.assign(el.style, pos || {});
document.body.appendChild(el);
return el;
};
const toTopBtn = makeBtn('eh-to-top-btn', '▲', '回到顶部');
const toBottomBtn = makeBtn('eh-to-bottom-btn', '▼', '直达底部');
darkToggleBtn = makeBtn('eh-dark-toggle-btn', '🌓', '主题模式:系统/暗色/亮色(快捷键:d)', {display:'flex'});
toTopBtn.addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
toBottomBtn.addEventListener('click', () => window.scrollTo({ top: document.documentElement.scrollHeight, behavior: 'smooth' }));
const onScroll = () => {
const h = document.documentElement.scrollHeight;
const ch = document.documentElement.clientHeight;
const t = window.scrollY || document.documentElement.scrollTop;
toTopBtn.style.display = t > 200 ? 'flex' : 'none';
toBottomBtn.style.display = (t + ch >= h - 5) ? 'none' : 'flex';
};
window.addEventListener('scroll', onScroll, { passive: true });
/* =========================
* 暗色开关(快捷键 d)
* ========================= */
const cycleMode = (direction = 1) => {
const step = typeof direction === 'number' ? direction : 1;
const idx = MODE_SEQUENCE.indexOf(currentMode);
const base = idx === -1 ? 0 : idx;
const nextMode = MODE_SEQUENCE[(base + step + MODE_SEQUENCE.length) % MODE_SEQUENCE.length];
applyMode(nextMode);
};
darkToggleBtn.addEventListener('click', () => cycleMode());
initDarkPref();
onScroll();
/* =========================
* Monster Encounter 适配
* ========================= */
const MONSTER_RE = /You have encountered a monster!/i;
function markAsMonsterBox(el){
el.classList.add('eh-dark-monbox');
const parentBox = el.closest('div, table, td, center, p');
if (parentBox) parentBox.classList.add('eh-dark-monbox');
}
function scanMonsterBox(root=document){
if (!document.documentElement.classList.contains('eh-dark')) return;
const nodes = root.querySelectorAll('div, td, p, center');
for (const n of nodes){
const t = (n.textContent || '').trim();
if (!t) continue;
if (MONSTER_RE.test(t)){
markAsMonsterBox(n);
const link = n.querySelector('a[href*="hentaiverse"]') || n.nextElementSibling?.querySelector?.('a[href*="hentaiverse"]');
if (link) markAsMonsterBox(link.closest('div, td, p, center') || n);
break;
}
}
}
function fixMonsterBox(){ scanMonsterBox(); }
document.addEventListener('DOMContentLoaded', fixMonsterBox);
window.addEventListener('load', fixMonsterBox);
const moMonster = new MutationObserver((muts)=>{
for (const m of muts){ if (m.addedNodes?.length) fixMonsterBox(); }
});
moMonster.observe(document.documentElement, { childList:true, subtree:true });
/* =========================
* Favorites 页面适配:分类 pill / Show All
* ========================= */
function fixFavoritesUI(root = document){
// 未开启暗色或不在收藏页,直接返回
if (!document.documentElement.classList.contains('eh-dark')) return;
if (!/\/favorites\.php(?:\?|$)/.test(location.pathname + location.search)) return;
// 1) 直接命中原生分类 pill:<div class="fp"> 和 <div class="fp fps">
const pills = root.querySelectorAll('div.fp');
pills.forEach(el => el.classList.add('eh-dark-favpill')); // 兼容自定义样式选择器
// 2) 兼容极少数旧布局里 “Show All Favorites” 是单独 <a> 的情况
const showAllCandidates = root.querySelectorAll('a[href$="favorites.php"]:not([href*="favcat="])');
showAllCandidates.forEach(a => {
const box = a.closest('div, span, td, button, a') || a;
box.classList.add('eh-dark-favpill');
});
}
document.addEventListener('DOMContentLoaded', fixFavoritesUI);
window.addEventListener('load', fixFavoritesUI);
const moFav = new MutationObserver((muts)=>{
for (const m of muts){ if (m.addedNodes?.length) fixFavoritesUI(m.target instanceof Document ? m.target : document); }
});
moFav.observe(document.documentElement, { childList:true, subtree:true });
/* =========================
* [ / ] 快捷键翻页
* ========================= */
const isTyping = (el) =>
!!el && (['INPUT','TEXTAREA','SELECT'].includes(el.tagName) || el.isContentEditable);
const triggerArrow = (key) => {
const ev = new KeyboardEvent('keydown', {
key,
code: key,
keyCode: key === 'ArrowLeft' ? 37 : 39,
which: key === 'ArrowLeft' ? 37 : 39,
bubbles:true, cancelable:true
});
document.dispatchEvent(ev); window.dispatchEvent(ev);
};
const followNavElement = (el) => {
if (!el) return false;
// If the element itself is a link-like node, try to act on it directly.
if (el.matches?.('td[onclick*="document.location"]')) {
const anchor = el.querySelector?.('a[href]');
if (anchor?.href) {
location.href = anchor.href;
} else if (typeof el.click === 'function') {
el.click();
}
return true;
}
if (el.matches?.('a[href]')) {
const href = el.getAttribute('href');
if (!href) return false;
const onclickAttr = el.getAttribute('onclick');
if (onclickAttr && /return\s+false/i.test(onclickAttr)) {
location.href = href;
return true;
}
el.click();
return true;
}
// Some wrappers (like #dprev/#dnext TD/Div) contain an anchor child.
const anchorChild = el.querySelector?.('a[href]');
if (anchorChild) return followNavElement(anchorChild);
const href = el.getAttribute?.('href');
if (href) {
location.href = href;
return true;
}
return false;
};
const gotoPrevNextPage = (isNext) => {
const followFirst = (selectors) => {
for (const selector of selectors) {
const candidate = document.querySelector(selector);
if (candidate && followNavElement(candidate)) return true;
}
return false;
};
const directSelectors = isNext
? ['#dnext', '.searchnav #dnext', '.searchnav .dnext', '.dnext']
: ['#dprev', '.searchnav #dprev', '.searchnav .dprev', '.dprev'];
if (followFirst(directSelectors)) return true;
const pagers = Array.from(document.querySelectorAll('table.ptt, table.ptb, .searchnav, #dprev, #dnext, .dprev, .dnext, td.ptdd'));
const wantNext = isNext;
const nextRegex = /(next|>>|»|>)/i;
const prevRegex = /(prev|<<|«|<)/i;
for (const pager of pagers) {
const links = [];
if (pager.matches?.('a[href], span[id^="u"], td[onclick*="document.location"]')) links.push(pager);
links.push(...pager.querySelectorAll?.('a[href], span[id^="u"], td[onclick*="document.location"]') || []);
if (!links.length) continue;
const primary = links.find(a => (wantNext ? /next/i.test(a.textContent) : /prev/i.test(a.textContent)));
if (primary) {
if (followNavElement(primary)) return true;
}
// 兼容 favorites 上方的 span#uprev/#unext(需要触发它的 href)
if (!primary) {
const alt = pager.querySelector(wantNext ? '#unext' : '#uprev');
if (alt && alt.getAttribute('href')) { location.href = alt.getAttribute('href'); return true; }
}
const fallback = links.find(a => (wantNext ? nextRegex.test(a.textContent) : prevRegex.test(a.textContent)));
if (fallback) {
if (followNavElement(fallback)) return true;
}
}
// Direct fallbacks for dprev/dnext outside of the pagers list
const direct = document.querySelector(isNext ? '#dnext a[href], #dnext' : '#dprev a[href], #dprev');
if (direct && followNavElement(direct)) return true;
return false;
};
const KEYDOWN_MARK = '__ehKeyHandled';
const keyListenerOptions = { capture:true, passive:false };
const onKeyDown = (e) => {
if (e[KEYDOWN_MARK]) return;
e[KEYDOWN_MARK] = true;
if (isTyping(document.activeElement)) return;
// d 键:在 系统 / 暗色 / 亮色 之间循环
const keyLower = typeof e.key === 'string' ? e.key.toLowerCase() : '';
if (keyLower === 'd' && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
e.preventDefault();
cycleMode();
return;
}
// [ 与 ]:兼容 e.code
const isBracketLeft = (e.key === '[') || (e.code === 'BracketLeft');
const isBracketRight = (e.key === ']') || (e.code === 'BracketRight');
if ((isBracketLeft || isBracketRight) && !e.ctrlKey && !e.metaKey && !e.altKey && !e.shiftKey) {
e.preventDefault();
const isImageView = /\/s\//.test(location.pathname) || /\/mpv\//.test(location.pathname);
if (isImageView) {
if (isBracketLeft) triggerArrow('ArrowLeft'); else triggerArrow('ArrowRight');
} else {
const ok = gotoPrevNextPage(isBracketRight);
if (!ok) triggerArrow(isBracketRight ? 'ArrowRight' : 'ArrowLeft');
}
}
};
window.addEventListener('keydown', onKeyDown, keyListenerOptions);
document.addEventListener('keydown', onKeyDown, keyListenerOptions);
})();