// ==UserScript==
// @name 鼠标悬停图片自动放大预览
// @namespace https://github.com/YDGG123
// @version 4.3.6
// @description 一款好用的网页图片放大工具,鼠标悬停即可自动放大图片,适配所有网页~
// @author 益达哥哥
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_xmlhttpRequest
// @grant GM_info
// @connect damp-woodpecker-4867.ydgg123.deno.net
// @run-at document-end
// @noframes
// @license MIT
// @homepageURL https://github.com/YDGG123/hover-image-zoom
// @supportURL https://github.com/YDGG123/hover-image-zoom/issues
// ==/UserScript==
(function() {
'use strict';
// ================================
// 图片放大功能模块
// ================================
let wheelManager = null;
const imageZoomModule = (function() {
const defaultConfig = {
delay: 500,
scale: 3,
maxWidth: 1200,
maxHeight: 980,
minScale: 1.4,
portraitRatio: 1.3,
zoomZIndex: 9999,
transition: 'all 0.3s ease',
scrollSpeed: 50,
smallImgThreshold: 280,
smallImgWidth: 500,
smallImgHeight: 430,
avoidClickConflict: true,
zoomMode: 'adaptive',
minOriginalSize: 30
};
// 参数合法范围(唯一来源,validateConfig 与配置面板 defs 都与此对齐)
const CONFIG_LIMITS = {
delay: [0, 2000],
scale: [1, 5],
maxWidth: [300, 3000],
maxHeight: [300, 3000],
minScale: [1, 3],
portraitRatio: [1, 3],
scrollSpeed: [5, 50],
smallImgThreshold: [100, 500],
smallImgWidth: [300, 1000],
smallImgHeight: [300, 1000],
minOriginalSize: [0, 500]
};
const states = new WeakMap();
let isEnabled = true;
let config = { ...defaultConfig };
const currentDomain = getDomain();
const lastMouse = { x: -1, y: -1 };
let currentZoomContainer = null;
let toggleButton = null;
let gearButton = null;
let dockZone = null;
let dockTip = null;
let settingsTip = null;
let zoomObserver = null;
let lightboxObserver = null;
let styleElement = null;
let dockStyleElement = null; // 悬浮按钮 + 配置面板样式(永不随 cleanup 移除)
const DEBUG_IMAGE_CHECK = false;
const COMMON_SELECTORS = [
'[onclick*="zoom"]', '[onclick*="lightbox"]', '[onclick*="gallery"]',
'[onclick*="preview"]', '[data-action*="zoom"]', '[data-lightbox]',
'[data-gallery]', '[data-fancybox]', '.zoomable', '.lightbox',
'.gallery-item', '.fancybox', '.stretched-link'
];
const SITE_HOVER_PROXY_RULES = [
{
domains: ['jd.com'],
imgMode: 'img',
itemSelector: '.more2_img, .img-wrapper',
cardSelector: 'li, .jd-pick-content-item',
pollInterval: 300
},
{
domains: ['taobao.com', 'tmall.com'],
imgMode: 'background',
itemSelector: '.img-wrapper',
cardSelector: '.tb-pick-content-item, li',
pollInterval: 300
},
{ domains: ['58pic.com', 'qiantu.com'], imgMode: 'img', itemSelector: '.image-container', cardSelector: '.qtd-theme', pollInterval: 300 }
];
// ===== 用户自定义站点规则 =====
function getCustomRules() {
try { return GM_getValue('image_zoom_custom_rules', []); } catch (e) { return []; }
}
function saveCustomRules(rules) {
GM_setValue('image_zoom_custom_rules', rules);
}
function escapeHtml(s) {
return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
function getDomain() {
try {
return new URL(window.location.href).hostname;
} catch (e) {
return window.location.hostname || 'unknown';
}
}
// 主页禁用:域名级开关,仅影响当前网站的主页
function isHomepageDisabled() {
return GM_getValue(`image_zoom_homepage_disabled_${currentDomain}`, false);
}
function refreshPanelHomepageSection() {
const overlay = document.getElementById('izModalOverlay');
if (!overlay) return;
const disabled = isHomepageDisabled();
const hpDot = overlay.querySelector('#izHpDot');
const hpText = overlay.querySelector('#izHpText');
const hpBtn = overlay.querySelector('#izHpToggleBtn');
if (hpDot) hpDot.className = disabled ? 'iz-dot off' : 'iz-dot on';
if (hpText) hpText.textContent = disabled ? '当前主页已禁用图片放大' : '当前主页已启用图片放大';
if (hpBtn) {
hpBtn.textContent = disabled ? '启用主页图片放大功能' : '禁用主页图片放大功能';
hpBtn.className = disabled ? 'iz-btn-sm primary' : 'iz-btn-sm warning';
}
}
function toggleHomepageDisabled() {
const disabled = !isHomepageDisabled();
GM_setValue(`image_zoom_homepage_disabled_${currentDomain}`, disabled);
if (disabled && isHomepage() && isEnabled) {
cleanup();
}
showToast(disabled ? '已禁用当前网站主页的图片放大功能' : '已启用当前网站主页的图片放大功能');
updateButtonState();
refreshPanelHomepageSection();
}
function isHomepage() {
const path = window.location.pathname;
return path === '/' || path === '/index.html' || path === '/index.php' || path === '';
}
function isHomepageZoomDisabled() {
return isHomepage() && isHomepageDisabled();
}
function showToast(message) {
let toast = document.getElementById('image-zoom-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'image-zoom-toast';
toast.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(0, 0, 0, 0.8); color: white; padding: 12px 20px;
border-radius: 6px; z-index: 1000000; font-size: 14px;
font-family: Arial, sans-serif; opacity: 0; transition: opacity 0.3s ease;
pointer-events: none;
`;
document.body.appendChild(toast);
}
toast.textContent = message;
toast.style.opacity = '1';
if (toast.timeoutId) clearTimeout(toast.timeoutId);
toast.timeoutId = setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
if (toast && toast.parentNode) {
toast.parentNode.removeChild(toast);
toast = null;
}
}, 300);
}, 2000);
}
let saveToastTimeout = null;
let saveToastEl = null;
const debouncedSaveToast = debounce(function(message) {
showSaveToast(message);
}, 600);
function showSaveToast(message) {
if (!saveToastEl || !saveToastEl.parentNode) {
saveToastEl = document.createElement('div');
saveToastEl.id = 'image-zoom-save-toast';
saveToastEl.style.cssText = `
position: fixed; bottom: 30px; left: 50%; transform: translateX(-50%);
background: rgba(15, 23, 42, 0.88); color: #fff; padding: 12px 24px;
border-radius: 16px; font-size: 14px; font-weight: 500; font-family: Arial, sans-serif;
box-shadow: 0 8px 32px rgba(0,0,0,0.25); border: 1px solid rgba(255,255,255,0.08);
opacity: 0; transform: translateX(-50%) translateY(16px);
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
pointer-events: none; z-index: 1000001; max-width: 300px; text-align: center;
`;
document.body.appendChild(saveToastEl);
}
saveToastEl.textContent = '✅ ' + message;
void saveToastEl.offsetWidth;
saveToastEl.style.opacity = '1';
saveToastEl.style.transform = 'translateX(-50%) translateY(0)';
clearTimeout(saveToastTimeout);
saveToastTimeout = setTimeout(() => {
if (saveToastEl) {
saveToastEl.style.opacity = '0';
saveToastEl.style.transform = 'translateX(-50%) translateY(16px)';
}
}, 1500);
}
function notifyConfigSaved(key, value, label) {
debouncedSaveToast(`已保存:${label || key} = ${value}`);
}
function debounce(func, wait) {
let timeout;
return function(...args) {
const context = this;
const later = () => {
clearTimeout(timeout);
func.apply(context, args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function validateConfig(cfg) {
const validated = { ...cfg };
for (const [key, [min, max]] of Object.entries(CONFIG_LIMITS)) {
const val = parseFloat(validated[key]);
validated[key] = isNaN(val) ? defaultConfig[key] : Math.max(min, Math.min(max, val));
}
validated.zoomMode = validated.zoomMode === 'fixed' ? 'fixed' : 'adaptive';
return validated;
}
function loadConfig() {
const savedConfig = GM_getValue(`image_zoom_config_${currentDomain}`);
if (savedConfig) {
const validatedConfig = validateConfig(savedConfig);
config = { ...defaultConfig, ...validatedConfig };
}
}
function saveConfig() {
GM_setValue(`image_zoom_config_${currentDomain}`, config);
}
function loadState() {
const savedState = GM_getValue(`image_zoom_enabled_${currentDomain}`);
isEnabled = savedState !== false;
if (isHomepageZoomDisabled()) {
isEnabled = false;
}
}
function injectStyles() {
// 悬浮按钮 + 配置面板样式放在独立样式表中,不随 cleanup 移除
if (!dockStyleElement) {
const dockStyle = document.createElement('style');
dockStyle.textContent = `
#zoomDockZone { position: fixed; right: 0; top: 50%; transform: translateY(-50%); width: 110px; height: 120px; z-index: 100000; pointer-events: none; }
#zoomDockZone.open { pointer-events: auto; }
#zoomDock { position: absolute; right: -20px; top: 10px; width: 44px; height: 44px; border-radius: 22px 0 0 22px; background: rgba(52, 211, 153, 0.20) !important; backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border: 1px solid rgba(52, 211, 153, 0.32) !important; border-right: none; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18) !important; display: flex; align-items: center; justify-content: center; padding-left: 2px; transition: right 0.35s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s ease, background 0.3s ease; opacity: 0.6; pointer-events: auto; cursor: pointer; z-index: 100001; }
#zoomDockZone.open #zoomDock { right: 0; opacity: 1; }
#zoomDock.off { background: rgba(239, 68, 68, 0.20) !important; border-color: rgba(239, 68, 68, 0.32) !important; }
#zoomDock.off .icon-svg { opacity: 0.7; }
#zoomDock.hp { background: rgba(255, 152, 0, 0.22) !important; border-color: rgba(255, 152, 0, 0.35) !important; }
#zoomDockZone.open #zoomDock:hover { filter: brightness(1.25); }
.icon-svg { width: 20px; height: 20px; fill: none; stroke: rgba(255, 255, 255, 0.92); stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.2)); pointer-events: none; transition: transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1); transform: translateX(-8px); }
#zoomDockZone.open .icon-svg { transform: translateX(0); }
#statusDot { position: absolute; right: 6px; top: 50%; transform: translateY(-50%); width: 6px; height: 6px; border-radius: 50%; background: #34d399; box-shadow: 0 0 12px rgba(52, 211, 153, 0.5); transition: background 0.3s, box-shadow 0.3s; opacity: 0.7; pointer-events: none; }
#zoomDock.off #statusDot { background: #f87171; box-shadow: 0 0 12px rgba(248, 113, 113, 0.5); }
#zoomDock.hp #statusDot { background: #ffb74d; box-shadow: 0 0 12px rgba(255, 152, 0, 0.5); }
#zoomDockZone.open #statusDot { opacity: 1; }
#zoomSettings { position: absolute; right: 0; top: 62px; width: 32px; height: 32px; border-radius: 16px 0 0 16px; background: rgba(20, 24, 44, 0.75) !important; backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.14) !important; border-right: none; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.35); display: flex; align-items: center; justify-content: center; cursor: pointer; opacity: 0; transform: translateX(18px) scale(0.9); pointer-events: none; transition: all 0.35s cubic-bezier(0.34, 1.56, 0.64, 1) 0.06s; z-index: 100002; }
#zoomDockZone.open #zoomSettings { opacity: 1; transform: translateX(0) scale(1); pointer-events: auto; }
#zoomSettings:hover { background: rgba(30, 36, 64, 0.85) !important; border-color: rgba(251, 191, 36, 0.35) !important; box-shadow: 0 4px 28px rgba(251, 191, 36, 0.18); }
#zoomSettings .icon-svg--gear { width: 16px; height: 16px; stroke: rgba(255, 255, 255, 0.9); stroke-width: 2; fill: none; transition: stroke 0.3s, transform 0.6s ease; pointer-events: none; }
#zoomSettings:hover .icon-svg--gear { stroke: #fbbf24; transform: rotate(60deg); }
.zoom-bubble-tip { position: fixed; background: rgba(20, 20, 40, 0.80); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.08); color: rgba(255, 255, 255, 0.90); padding: 6px 16px; border-radius: 10px; font-size: 12px; font-weight: 450; letter-spacing: 0.3px; font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; white-space: nowrap; opacity: 0; pointer-events: none; transition: opacity 0.25s ease 0.2s; z-index: 100003; box-shadow: 0 8px 30px rgba(0, 0, 0, 0.4); }
.zoom-bubble-tip::after { content: ''; position: absolute; right: -6px; top: 50%; transform: translateY(-50%); border: 6px solid transparent; border-left-color: rgba(20, 20, 40, 0.80); border-right: 0; }
.zoom-bubble-tip.visible { opacity: 1; }
body.zoom-dock-dragging, body.zoom-dock-dragging * { transition: none !important; cursor: grabbing !important; user-select: none !important; }
body.zoom-dock-dragging #zoomDock { cursor: grabbing !important; }
#izModalOverlay { position: fixed; inset: 0; z-index: 99999; display: none; align-items: center; justify-content: center; padding: 24px; background: rgba(15, 23, 42, 0.45); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); }
#izModalOverlay.anim-in { animation: izOverlayFade 0.35s ease; }
#izModalOverlay.anim-out { animation: izOverlayFadeOut 0.3s ease forwards; }
@keyframes izOverlayFade { from { opacity: 0; } to { opacity: 1; } }
@keyframes izOverlayFadeOut { from { opacity: 1; } to { opacity: 0; } }
#izConfigPanel { width: 100%; max-width: 560px; max-height: 90vh; background: rgba(255, 255, 255, 0.88); backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px); border-radius: 28px; box-shadow: 0 25px 60px -12px rgba(0, 0, 0, 0.35), 0 0 0 1px rgba(255, 255, 255, 0.6) inset; overflow: hidden; animation: izPanelSlide 0.40s cubic-bezier(0.16, 1, 0.3, 1); display: flex; flex-direction: column; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; box-sizing: border-box; }
@keyframes izPanelSlide { from { opacity: 0; transform: translateY(28px) scale(0.96); } to { opacity: 1; transform: translateY(0) scale(1); } }
.iz-panel-scroll { flex: 1; overflow-y: auto; padding: 0 28px 12px 28px; scroll-behavior: smooth; }
.iz-panel-scroll::-webkit-scrollbar { width: 4px; }
.iz-panel-scroll::-webkit-scrollbar-track { background: transparent; }
.iz-panel-scroll::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 8px; }
.iz-panel-header { display: flex; align-items: center; justify-content: space-between; padding: 20px 28px 0 28px; flex-shrink: 0; }
.iz-panel-header-left { display: flex; align-items: center; gap: 12px; }
.iz-panel-icon { width: 38px; height: 38px; background: linear-gradient(135deg, #4F46E5, #7C3AED); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: white; font-size: 20px; flex-shrink: 0; box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); }
.iz-panel-title { font-size: 20px; font-weight: 600; color: #0F172A; letter-spacing: -0.3px; }
.iz-panel-title span { font-weight: 400; color: #64748B; font-size: 14px; margin-left: 6px; }
.iz-close-btn { width: 36px; height: 36px; border: none; background: rgba(203, 213, 225, 0.4); border-radius: 50%; cursor: pointer; font-size: 18px; color: #64748B; display: flex; align-items: center; justify-content: center; transition: all 0.2s; flex-shrink: 0; line-height: 1; }
.iz-close-btn:hover { background: rgba(239, 68, 68, 0.12); color: #EF4444; transform: rotate(90deg); }
.iz-section { margin-top: 20px; background: rgba(255, 255, 255, 0.5); border-radius: 18px; padding: 18px 20px 20px 20px; border: 1px solid rgba(226, 232, 240, 0.7); }
.iz-section-title { font-size: 13px; font-weight: 600; color: #64748B; letter-spacing: 0.6px; margin-bottom: 14px; display: flex; align-items: center; gap: 8px; }
.iz-badge { background: #4F46E5; color: white; font-size: 10px; font-weight: 600; padding: 0 8px; border-radius: 20px; line-height: 18px; }
.iz-row { display: flex; align-items: center; gap: 14px; margin-bottom: 14px; }
.iz-row:last-child { margin-bottom: 0; }
.iz-row-label { font-size: 14px; font-weight: 500; color: #1E293B; flex-shrink: 0; min-width: 100px; }
.iz-row-label .iz-hint { font-weight: 400; font-size: 12px; color: #94A3B8; display: block; margin-top: 1px; }
.iz-row-control { flex: 1; min-width: 0; }
.iz-select { width: 100%; padding: 8px 36px 8px 14px; font-size: 14px; font-weight: 500; color: #0F172A; background: white url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2364748b' d='M6 8L1 3h10z'/%3E%3C/svg%3E") no-repeat right 14px center; background-size: 12px; border: 1.5px solid #E2E8F0; border-radius: 12px; appearance: none; -webkit-appearance: none; transition: all 0.2s; cursor: pointer; outline: none; height: 42px; }
.iz-select:hover { border-color: #A5B4FC; }
.iz-select:focus { border-color: #4F46E5; box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); }
.iz-input-group { display: flex; align-items: center; background: white; border: 1.5px solid #E2E8F0; border-radius: 12px; overflow: hidden; transition: all 0.2s; height: 42px; }
.iz-input-group:focus-within { border-color: #4F46E5; box-shadow: 0 0 0 3px rgba(79, 70, 229, 0.15); }
.iz-input-group input[type="number"] { flex: 1; border: none; padding: 0 12px; font-size: 14px; font-weight: 500; color: #0F172A; background: transparent; outline: none; min-width: 0; height: 100%; width: 100%; -moz-appearance: textfield; }
.iz-input-group input[type="number"]::-webkit-inner-spin-button, .iz-input-group input[type="number"]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
.iz-input-group .iz-unit { padding: 0 14px 0 4px; font-size: 13px; color: #94A3B8; font-weight: 500; flex-shrink: 0; }
.iz-input-group.disabled-group { opacity: 0.6; background-color: #f8fafc; border-color: #e2e8f0; cursor: not-allowed; }
.iz-input-group.disabled-group input { cursor: not-allowed; background-color: #f8fafc; }
.iz-checkbox-wrap { display: flex; align-items: center; gap: 12px; cursor: pointer; user-select: none; }
.iz-checkbox-custom { width: 20px; height: 20px; flex-shrink: 0; border: 2px solid #CBD5E1; border-radius: 6px; background: white; transition: all 0.2s; display: flex; align-items: center; justify-content: center; }
.iz-checkbox-custom.checked { background: #4F46E5; border-color: #4F46E5; }
.iz-checkbox-custom.checked::after { content: "✓"; color: white; font-size: 14px; font-weight: 700; line-height: 1; }
.iz-checkbox-label { font-size: 14px; font-weight: 500; color: #1E293B; }
.iz-checkbox-label .iz-sub { font-weight: 400; font-size: 12px; color: #94A3B8; display: block; margin-top: 1px; }
.iz-toggle-wrap { display: flex; align-items: center; gap: 12px; cursor: pointer; user-select: none; }
.iz-toggle { position: relative; width: 46px; height: 28px; flex-shrink: 0; background: #CBD5E1; border-radius: 20px; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1); }
.iz-toggle.active { background: linear-gradient(135deg, #4F46E5, #7C3AED); }
.iz-toggle .iz-knob { position: absolute; top: 3px; left: 3px; width: 22px; height: 22px; background: white; border-radius: 50%; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); box-shadow: 0 2px 6px rgba(0, 0, 0, 0.18); }
.iz-toggle.active .iz-knob { left: 21px; }
.iz-exclusion-box { background: rgba(241, 245, 249, 0.7); border-radius: 14px; padding: 14px 16px; border: 1px solid rgba(226, 232, 240, 0.5); }
.iz-exclusion-box .iz-status-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; flex-wrap: wrap; }
.iz-exclusion-box .iz-status-text { font-size: 14px; font-weight: 500; display: flex; align-items: center; gap: 8px; color: #1E293B; }
.iz-exclusion-box .iz-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.iz-exclusion-box .iz-dot.on { background: #10B981; }
.iz-exclusion-box .iz-dot.off { background: #F59E0B; }
.iz-exclusion-note { margin-top: 8px; font-size: 12px; color: #64748B; }
.iz-btn-sm { padding: 6px 16px; border: none; border-radius: 10px; font-size: 13px; font-weight: 600; cursor: pointer; transition: all 0.2s; flex-shrink: 0; height: 34px; }
.iz-btn-sm.primary { background: #4F46E5; color: white; }
.iz-btn-sm.primary:hover { background: #4338CA; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3); }
.iz-btn-sm.warning { background: #F59E0B; color: white; }
.iz-btn-sm.warning:hover { background: #D97706; transform: translateY(-1px); box-shadow: 0 4px 12px rgba(245, 158, 11, 0.3); }
.iz-collapse-header { display: flex; align-items: center; justify-content: space-between; padding: 10px 0 6px 0; cursor: pointer; user-select: none; border-top: 1px solid rgba(226, 232, 240, 0.5); margin-top: 4px; transition: opacity 0.2s; }
.iz-collapse-header .iz-left { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 600; color: #1E293B; }
.iz-collapse-header .iz-arrow { transition: transform 0.3s ease; font-size: 12px; color: #94A3B8; }
.iz-collapse-header .iz-arrow.open { transform: rotate(90deg); }
.iz-badge-params { font-size: 11px; font-weight: 500; color: #64748B; background: #F1F5F9; padding: 2px 10px; border-radius: 20px; }
.iz-collapse-body { overflow: hidden; max-height: 0; opacity: 0; transition: all 0.35s cubic-bezier(0.16, 1, 0.3, 1); }
.iz-collapse-body.open { max-height: 800px; opacity: 1; padding-top: 12px; }
.iz-param-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.iz-param-item { display: flex; flex-direction: column; gap: 4px; }
.iz-param-item label { font-size: 12px; font-weight: 500; color: #64748B; letter-spacing: 0.2px; display: flex; align-items: center; gap: 5px; }
.iz-tip-icon { display: inline-flex; align-items: center; justify-content: center; width: 14px; height: 14px; flex-shrink: 0; border-radius: 50%; background: #E2E8F0; color: #64748B; font-size: 10px; font-weight: 700; line-height: 1; cursor: help; position: relative; transition: all 0.2s; }
.iz-tip-icon:hover { background: #4F46E5; color: white; }
#izTipBubble { position: fixed; width: 240px; background: rgba(15, 23, 42, 0.95); color: #F1F5F9; font-size: 12px; font-weight: 400; line-height: 1.6; padding: 10px 13px; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); opacity: 0; pointer-events: none; transition: opacity 0.15s ease; z-index: 100005; white-space: normal; text-align: left; }
.iz-param-item .iz-input-group { height: 36px; }
.iz-param-item .iz-input-group input[type="number"] { font-size: 13px; padding: 0 10px; }
.iz-param-item .iz-input-group .iz-unit { font-size: 12px; padding: 0 10px 0 2px; }
.iz-panel-footer { padding: 14px 28px 20px 28px; border-top: 1px solid rgba(226, 232, 240, 0.5); display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; background: rgba(255, 255, 255, 0.4); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px); }
.iz-btn-ghost { background: none; border: none; padding: 8px 14px; font-size: 13px; font-weight: 500; color: #64748B; cursor: pointer; border-radius: 10px; transition: all 0.2s; }
.iz-btn-ghost:hover { background: rgba(239, 68, 68, 0.08); color: #EF4444; }
.iz-btn-ghost:active { transform: scale(0.96); }
.iz-btn-primary-solid { padding: 10px 28px; background: linear-gradient(135deg, #4F46E5, #7C3AED); border: none; border-radius: 14px; font-size: 14px; font-weight: 600; color: white; cursor: pointer; transition: all 0.25s; box-shadow: 0 4px 16px rgba(79, 70, 229, 0.3); }
.iz-btn-primary-solid:hover { transform: translateY(-2px); box-shadow: 0 8px 28px rgba(79, 70, 229, 0.4); }
.iz-btn-primary-solid:active { transform: scale(0.96); }
@media (max-width: 600px) {
#izConfigPanel { border-radius: 20px; max-height: 95vh; }
.iz-panel-scroll { padding: 0 18px 8px 18px; }
.iz-panel-header { padding: 16px 18px 0 18px; }
.iz-panel-footer { padding: 12px 18px 16px 18px; flex-wrap: wrap; gap: 10px; }
.iz-row { flex-direction: column; align-items: stretch; gap: 6px; }
.iz-param-grid { grid-template-columns: 1fr; }
.iz-panel-title { font-size: 17px; }
.iz-panel-icon { width: 34px; height: 34px; font-size: 17px; }
}
`;
document.head.appendChild(dockStyle);
dockStyleElement = dockStyle;
}
// 图片放大相关样式:随 cleanup 一起移除
if (styleElement) return;
const style = document.createElement('style');
style.textContent = `
.image-zoom-wrapper * { box-sizing: border-box; }
.image-zoom-container img { object-fit: contain; }
.image-zoom-hover { cursor: zoom-in !important; }
a.image-zoom-hover, .cover-container.image-zoom-hover, .card.image-zoom-hover { cursor: zoom-in !important; }
a.stretched-link.image-zoom-hover { cursor: zoom-in !important; }
`;
document.head.appendChild(style);
styleElement = style;
}
function createDockButton() {
dockZone = document.createElement('div');
dockZone.id = 'zoomDockZone';
toggleButton = document.createElement('div');
toggleButton.id = 'zoomDock';
toggleButton.innerHTML = `
`;
toggleButton.title = '点击:切换图片放大';
gearButton = document.createElement('div');
gearButton.id = 'zoomSettings';
gearButton.innerHTML = `
`;
gearButton.title = '打开配置面板';
dockZone.appendChild(toggleButton);
dockZone.appendChild(gearButton);
dockTip = document.createElement('div');
dockTip.className = 'zoom-bubble-tip';
settingsTip = document.createElement('div');
settingsTip.className = 'zoom-bubble-tip';
settingsTip.textContent = '配置面板';
document.body.appendChild(dockZone);
document.body.appendChild(dockTip);
document.body.appendChild(settingsTip);
const savedTop = GM_getValue(`image_zoom_dock_top_${currentDomain}`);
if (savedTop !== undefined && savedTop !== null) {
dockZone.style.transform = 'none';
dockZone.style.top = savedTop + 'px';
}
let leaveTimer = null;
function positionTips() {
if (!dockTip || !settingsTip) return;
const dockRect = toggleButton.getBoundingClientRect();
const gearRect = gearButton.getBoundingClientRect();
dockTip.style.top = (dockRect.top + dockRect.height / 2) + 'px';
dockTip.style.right = (window.innerWidth - dockRect.left + 8) + 'px';
dockTip.style.transform = 'translateY(-50%)';
dockTip.classList.add('visible');
settingsTip.style.top = (gearRect.top + gearRect.height / 2) + 'px';
settingsTip.style.right = (window.innerWidth - gearRect.left + 8) + 'px';
settingsTip.style.transform = 'translateY(-50%)';
settingsTip.classList.add('visible');
}
function hideTips() {
dockTip.classList.remove('visible');
settingsTip.classList.remove('visible');
}
function updateDockTipText() {
if (isHomepageZoomDisabled()) {
dockTip.innerHTML = '主页已禁用图片放大 设置中可开启';
} else if (!isEnabled) {
dockTip.innerHTML = '图片放大已关闭 点击开启';
} else {
dockTip.innerHTML = '图片放大已开启 点击关闭';
}
}
toggleButton.addEventListener('mouseenter', () => {
clearTimeout(leaveTimer);
dockZone.classList.add('open');
updateDockTipText();
positionTips();
setTimeout(positionTips, 320);
});
dockZone.addEventListener('mouseenter', () => clearTimeout(leaveTimer));
dockZone.addEventListener('mouseleave', () => {
leaveTimer = setTimeout(() => {
if (dockZone.matches(':hover')) return;
dockZone.classList.remove('open');
hideTips();
}, 200);
});
toggleButton.addEventListener('click', (e) => {
e.stopPropagation();
if (isHomepageZoomDisabled()) {
showToast('当前网站主页已禁用图片放大,可在设置面板中开启');
return;
}
toggleEnabled();
updateDockTipText();
if (dockZone.classList.contains('open')) positionTips();
});
gearButton.addEventListener('click', (e) => {
e.stopPropagation();
toggleConfigPanel();
});
// 拖拽:区分点击与拖动(THRESHOLD),dragJustEnded 用于吞掉拖拽结束后紧随的 click
(function() {
let isDragging = false;
let hasMoved = false;
let startY = 0;
let startTop = 0;
let dragJustEnded = false;
const THRESHOLD = 3;
function getCurTop(el) {
return el.getBoundingClientRect().top;
}
function clamp(val, min, max) {
return Math.max(min, Math.min(max, val));
}
function syncY(topPx) {
const h = toggleButton.offsetHeight;
const safeTop = clamp(topPx, 0, window.innerHeight - h);
dockZone.style.top = safeTop + 'px';
if (dockZone.classList.contains('open')) positionTips();
}
toggleButton.addEventListener('click', function(e) {
if (dragJustEnded) {
e.stopImmediatePropagation();
e.preventDefault();
dragJustEnded = false;
}
}, true);
toggleButton.addEventListener('mousedown', function(e) {
if (e.button !== 0) return;
isDragging = true;
hasMoved = false;
startY = e.clientY;
startTop = getCurTop(dockZone);
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
// v4.2.1:全局记录鼠标坐标(供站点悬停代理轮询使用)
lastMouse.x = e.clientX;
lastMouse.y = e.clientY;
if (!isDragging) return;
const delta = e.clientY - startY;
if (!hasMoved && Math.abs(delta) > THRESHOLD) {
hasMoved = true;
document.body.classList.add('zoom-dock-dragging');
}
if (hasMoved) syncY(startTop + delta);
});
document.addEventListener('mouseup', function() {
if (!isDragging) return;
isDragging = false;
document.body.classList.remove('zoom-dock-dragging');
if (hasMoved) {
dockZone.style.transform = 'none';
GM_setValue(`image_zoom_dock_top_${currentDomain}`, parseFloat(dockZone.style.top) || 0);
dragJustEnded = true;
requestAnimationFrame(() => {
setTimeout(() => { dragJustEnded = false; }, 50);
});
}
});
let touchStartY = 0, touchStartTop = 0, isTouching = false;
toggleButton.addEventListener('touchstart', function(e) {
const touch = e.touches[0];
if (!touch) return;
isTouching = true;
hasMoved = false;
touchStartY = touch.clientY;
touchStartTop = getCurTop(dockZone);
}, { passive: true });
document.addEventListener('touchmove', function(e) {
if (!isTouching) return;
const touch = e.touches[0];
if (!touch) return;
const delta = touch.clientY - touchStartY;
if (!hasMoved && Math.abs(delta) > THRESHOLD) {
hasMoved = true;
document.body.classList.add('zoom-dock-dragging');
}
if (hasMoved) {
syncY(touchStartTop + delta);
e.preventDefault();
}
}, { passive: false });
document.addEventListener('touchend', function() {
if (!isTouching) return;
isTouching = false;
document.body.classList.remove('zoom-dock-dragging');
if (hasMoved) {
dockZone.style.transform = 'none';
GM_setValue(`image_zoom_dock_top_${currentDomain}`, parseFloat(dockZone.style.top) || 0);
dragJustEnded = true;
setTimeout(() => { dragJustEnded = false; }, 50);
}
}, { passive: true });
window.addEventListener('resize', function() {
const curTop = getCurTop(dockZone);
if (curTop + dockZone.offsetHeight > window.innerHeight) {
syncY(Math.min(curTop, window.innerHeight - dockZone.offsetHeight));
}
if (dockZone.classList.contains('open')) positionTips();
});
})();
updateButtonState();
}
function updateButtonState() {
if (!toggleButton) return;
if (isHomepageZoomDisabled()) {
toggleButton.classList.remove('off');
toggleButton.classList.add('hp');
} else if (!isEnabled) {
toggleButton.classList.remove('hp');
toggleButton.classList.add('off');
} else {
toggleButton.classList.remove('off');
toggleButton.classList.remove('hp');
}
}
function toggleEnabled() {
isEnabled = !isEnabled;
GM_setValue(`image_zoom_enabled_${currentDomain}`, isEnabled);
updateButtonState();
if (isEnabled) {
injectStyles();
initImages();
} else {
cleanup();
}
}
function cleanup() {
// 先回收 blob URL(必须在容器移除前查,移除后就查不到了)
document.querySelectorAll('.image-zoom-container img').forEach(im => {
if (im.__zoomBlobUrl) { URL.revokeObjectURL(im.__zoomBlobUrl); im.__zoomBlobUrl = null; }
});
if (zoomObserver) { zoomObserver.disconnect(); zoomObserver = null; }
if (lazyImageObserver) {
lazyImageObserver.disconnect();
lazyImageObserver = null;
}
if (currentZoomContainer) { currentZoomContainer.remove(); currentZoomContainer = null; }
document.querySelectorAll('.image-zoom-container').forEach(el => el.remove());
document.querySelectorAll('.image-zoom-wrapper').forEach(wrapper => {
const parent = wrapper.parentNode;
while (wrapper.firstChild) parent.insertBefore(wrapper.firstChild, wrapper);
parent.removeChild(wrapper);
});
document.querySelectorAll('img.image-zoom-processed').forEach(img => {
const state = states.get(img);
if (state) {
if (state.timer) clearTimeout(state.timer);
if (state.handlers) {
img.removeEventListener('mouseenter', state.handlers.mouseenter);
img.removeEventListener('mouseleave', state.handlers.mouseleave);
img.removeEventListener('click', state.handlers.click, true);
if (state.handlers.parent && state.handlers.parentMouseenter) {
state.handlers.parent.removeEventListener('mouseenter', state.handlers.parentMouseenter);
state.handlers.parent.removeEventListener('mouseleave', state.handlers.parentMouseleave);
}
}
if (state.containers) {
state.containers.forEach(ctn => {
ctn.classList.remove('image-zoom-hover');
ctn.removeAttribute('data-zoom-proxy');
ctn.removeAttribute('data-stretch-fixed');
});
}
}
img.classList.remove('image-zoom-processed');
img.classList.remove('image-zoom-hover');
states.delete(img);
});
if (styleElement && styleElement.parentNode) {
styleElement.parentNode.removeChild(styleElement);
styleElement = null;
}
}
function checkImageClickBehavior(img) {
for (const selector of COMMON_SELECTORS) {
if (img.matches(selector)) return true;
}
let parent = img.parentElement;
while (parent && parent !== document.body) {
const parentClass = (typeof parent.className === 'string' ? parent.className : parent.getAttribute('class')) || '';
const parentId = parent.id || '';
if (parentClass.includes('zoom') || parentClass.includes('lightbox') ||
parentClass.includes('gallery') || parentClass.includes('fancybox') ||
parentClass.includes('stretched-link') || parentId.includes('zoom') ||
parentId.includes('lightbox') || parentId.includes('gallery') ||
parentId.includes('fancybox')) {
return true;
}
parent = parent.parentElement;
}
return false;
}
function isImageInLightboxMode() {
const commonLightboxSelectors = ['.lightbox-open', '.fancybox-open', '.modal-open', '.zoom-overlay-open'];
for (const selector of commonLightboxSelectors) {
if (document.body.classList.contains(selector.replace('.', '')) ||
document.documentElement.classList.contains(selector.replace('.', ''))) {
return true;
}
}
return false;
}
function isValidImage(img) {
if (!img || !img.parentNode) return false;
if (img.tagName !== 'IMG') return false;
if (img.getClientRects().length === 0) {
if (DEBUG_IMAGE_CHECK) console.log('[图片放大] 跳过(不可见):', img.src);
return false;
}
const style = window.getComputedStyle(img);
if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) {
if (DEBUG_IMAGE_CHECK) console.log('[图片放大] 暂时跳过(隐藏/淡出中):', img.src);
return false;
}
const src = img.src || img.currentSrc;
if (!src || src.trim() === '' || src.startsWith('data:') || src.includes('placeholder')) {
if (DEBUG_IMAGE_CHECK) console.log('[图片放大] 跳过(无效src):', src);
return false;
}
if (style.backgroundImage && style.backgroundImage !== 'none') return false;
if (!img.complete || img.naturalWidth === 0) {
if (!img.dataset.zoomLoadBound) {
img.dataset.zoomLoadBound = 'true';
img.addEventListener('load', () => processImage(img), { once: true });
}
return false;
}
const rect = img.getBoundingClientRect();
if (rect.width < 10 || rect.height < 10) return false;
return true;
}
// ================================
// 📮 问题反馈模块
// ================================
const FEEDBACK_API = 'https://damp-woodpecker-4867.ydgg123.deno.net';
function submitFeedback(text) {
const payload = JSON.stringify({
text: text,
page: location.hostname + location.pathname
+ ' | 脚本 v' + (typeof GM_info !== 'undefined' && GM_info.script ? GM_info.script.version : 'unknown')
});
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: FEEDBACK_API,
headers: { 'Content-Type': 'application/json' },
data: payload,
timeout: 15000,
onload: (r) => {
if (r.status >= 200 && r.status < 400) {
resolve();
} else {
reject(new Error('提交失败(' + r.status + ')'));
}
},
onerror: () => reject(new Error('网络错误,请稍后重试')),
ontimeout: () => reject(new Error('提交超时,请检查网络'))
});
});
}
function injectFeedbackSection(overlay) {
const scroll = overlay.querySelector('.iz-panel-scroll');
if (!scroll || scroll.querySelector('#izFeedbackSection')) return;
const section = document.createElement('div');
section.className = 'iz-section';
section.id = 'izFeedbackSection';
section.innerHTML = `
📮 问题反馈
`;
scroll.appendChild(section);
const textarea = section.querySelector('#izFeedbackText');
const status = section.querySelector('#izFeedbackStatus');
const btn = section.querySelector('#izFeedbackBtn');
textarea.addEventListener('focus', () => {
textarea.style.borderColor = '#4F46E5';
textarea.style.boxShadow = '0 0 0 3px rgba(79, 70, 229, 0.15)';
});
textarea.addEventListener('blur', () => {
textarea.style.borderColor = '#E2E8F0';
textarea.style.boxShadow = 'none';
});
btn.addEventListener('mouseenter', () => {
btn.style.transform = 'translateY(-2px)';
btn.style.boxShadow = '0 8px 24px rgba(79, 70, 229, 0.4)';
});
btn.addEventListener('mouseleave', () => {
btn.style.transform = '';
btn.style.boxShadow = '0 4px 12px rgba(79, 70, 229, 0.3)';
});
btn.addEventListener('click', () => {
const text = textarea.value.trim();
if (!text) {
status.textContent = '⚠️ 请先填写反馈内容';
status.style.color = '#F59E0B';
return;
}
btn.disabled = true;
btn.textContent = '提交中…';
btn.style.opacity = '0.7';
status.textContent = '';
submitFeedback(text).then(() => {
status.textContent = '✅ 反馈已提交,感谢你的支持!';
status.style.color = '#10B981';
textarea.value = '';
}).catch((err) => {
status.textContent = '❌ ' + (err.message || '提交失败');
status.style.color = '#EF4444';
}).finally(() => {
btn.disabled = false;
btn.textContent = '📮 提交反馈';
btn.style.opacity = '';
setTimeout(() => { status.textContent = ''; }, 4000);
});
});
}
// ================================
// 🎯 自定义站点规则模块
// ================================
const RULE_SHARE_API = FEEDBACK_API;
function submitSiteRule(rule, note) {
const payload = JSON.stringify({
type: 'site_rule',
rule: {
name: rule.name,
domains: rule.domains,
imgMode: rule.imgMode,
itemSelector: rule.itemSelector,
cardSelector: rule.cardSelector,
pollInterval: rule.pollInterval
},
note: note || '',
context: {
page: location.hostname + location.pathname,
scriptVersion: (typeof GM_info !== 'undefined' && GM_info.script) ? GM_info.script.version : 'unknown',
userAgent: navigator.userAgent,
timestamp: Date.now()
}
});
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: RULE_SHARE_API,
headers: { 'Content-Type': 'application/json' },
data: payload,
timeout: 15000,
onload: (r) => (r.status >= 200 && r.status < 400) ? resolve() : reject(new Error('状态码 ' + r.status)),
onerror: () => reject(new Error('网络错误')),
ontimeout: () => reject(new Error('超时'))
});
});
}
function injectCustomRulesSection(overlay) {
const scroll = overlay.querySelector('.iz-panel-scroll');
if (!scroll || scroll.querySelector('#izCustomRulesSection')) return;
const section = document.createElement('div');
section.className = 'iz-section';
section.id = 'izCustomRulesSection';
const renderList = () => {
const rules = getCustomRules();
if (!rules.length) {
return '暂无自定义规则。小技巧:在目标页面按 F12 检查图片及其容器,把 class 选择器填进来即可。
';
}
return rules.map(r => `
`).join('');
};
section.innerHTML = `
🎯 自定义站点规则 进阶-不会添加用⬆️反馈
为当前网站添加悬停放大规则,解决遮罩层挡住鼠标、背景图无法放大等问题。保存后刷新页面生效。
${renderList()}
`;
scroll.appendChild(section);
const listEl = section.querySelector('#izRuleList');
const form = section.querySelector('#izRuleForm');
let lastSavedRule = null;
section.querySelector('#izRuleAddBtn').addEventListener('click', () => {
form.style.display = 'block';
section.querySelector('#izRuleDomains').value = currentDomain;
});
// ===== 拾取模式 v2:以 img 为锚点,自动推导图片容器和卡片 =====
section.querySelector('#izRulePickBtn').addEventListener('click', () => {
form.style.display = 'block';
const overlay = document.getElementById('izModalOverlay');
if (overlay) overlay.style.display = 'none';
const tips = document.createElement('div');
tips.style.cssText = 'position:fixed;z-index:2147483647;background:rgba(15,23,42,.92);color:#fff;padding:8px 12px;border-radius:8px;font:12px monospace;pointer-events:none;max-width:420px;display:none;white-space:nowrap;';
document.body.appendChild(tips);
const bar = document.createElement('div');
bar.style.cssText = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);z-index:2147483647;background:#4F46E5;color:#fff;padding:8px 20px;border-radius:20px;font-size:13px;font-weight:600;';
bar.textContent = '🎯 点击图片附近任意位置(遮罩/标题也行),自动定位图片容器,按 ESC 取消';
document.body.appendChild(bar);
// 生成"短选择器":优先 class,不带层级
const shortSel = (el) => {
if (!el || el === document.body || el === document.documentElement) return null;
if (el.tagName === 'IMG') return 'img';
if (el.id) { const s = '#' + CSS.escape(el.id); if (document.querySelectorAll(s).length <= 60) return s; }
for (const c of (el.classList || [])) {
const s = '.' + CSS.escape(c);
const n = document.querySelectorAll(s).length;
if (n > 0) return s;
}
return el.tagName.toLowerCase();
};
const deriveSelectors = (hitEl) => {
let card = null, node = hitEl;
while (node && node !== document.body) {
if (node.querySelector && node.querySelector('img')) {
const r = node.getBoundingClientRect();
if (r.width < window.innerWidth * 0.9 && r.height < window.innerHeight * 0.9) {
card = node;
break;
}
}
node = node.parentElement;
}
if (!card) return null;
const img = card.querySelector('img');
const item = img ? (img.parentElement && img.parentElement !== document.body ? img.parentElement : img) : null;
const itemSel = item ? shortSel(item) : shortSel(card);
const cardSel = (item && item !== card) ? shortSel(card) : '';
let ok = false, count = 0;
try {
count = document.querySelectorAll(itemSel + ' img').length;
ok = count > 0;
} catch (e) { }
return ok ? { itemSel, cardSel, count } : null;
};
const cleanup = () => {
document.removeEventListener('mousemove', move, true);
document.removeEventListener('click', pick, true);
document.removeEventListener('keydown', esc, true);
tips.remove(); bar.remove();
document.body.style.cursor = '';
};
const move = (e) => {
e.preventDefault(); e.stopPropagation();
const el = document.elementFromPoint(e.clientX, e.clientY);
if (!el) return;
const d = deriveSelectors(el);
if (!d) { tips.style.display = 'none'; return; }
tips.style.display = 'block';
tips.style.left = Math.min(e.clientX + 16, window.innerWidth - 440) + 'px';
tips.style.top = (e.clientY + 16) + 'px';
tips.textContent = '图片容器: ' + d.itemSel + '(含图 ' + d.count + ' 张)' + (d.cardSel ? ' | 卡片: ' + d.cardSel : '');
tips.dataset.item = d.itemSel;
tips.dataset.card = d.cardSel || '';
};
const esc = (e) => { if (e.key === 'Escape') cleanup(); };
const pick = (e) => {
e.preventDefault(); e.stopPropagation();
const itemSel = tips.dataset.item, cardSel = tips.dataset.card;
cleanup();
if (!itemSel) { showToast('未能定位到图片容器,请换个位置点击'); return; }
section.querySelector('#izRuleItem').value = itemSel;
section.querySelector('#izRuleCard').value = cardSel;
showSaveToast('已拾取:' + itemSel + ',确认后保存');
const ov = document.getElementById('izModalOverlay');
if (ov) { ov.classList.add('anim-in'); ov.style.display = 'flex'; setTimeout(() => ov.classList.remove('anim-in'), 400); }
};
document.body.style.cursor = 'crosshair';
document.addEventListener('mousemove', move, true);
document.addEventListener('click', pick, true);
document.addEventListener('keydown', esc, true);
});
section.querySelector('#izRuleCancel').addEventListener('click', () => {
form.style.display = 'none';
const shareBar = section.querySelector('.iz-rule-share-bar');
if (shareBar) shareBar.remove();
});
section.querySelector('#izRuleSave').addEventListener('click', () => {
const itemSelector = section.querySelector('#izRuleItem').value.trim();
if (!itemSelector) { showToast('请填写图片容器选择器'); return; }
try { document.querySelector(itemSelector); } catch (e) { showToast('选择器语法有误,请检查'); return; }
const oldShareBar = section.querySelector('.iz-rule-share-bar');
if (oldShareBar) oldShareBar.remove();
const rules = getCustomRules();
const newRule = {
id: 'r' + Date.now(),
name: section.querySelector('#izRuleName').value.trim() || currentDomain,
domains: section.querySelector('#izRuleDomains').value.trim() || currentDomain,
imgMode: section.querySelector('#izRuleMode').value,
itemSelector,
cardSelector: section.querySelector('#izRuleCard').value.trim(),
pollInterval: 300,
enabled: true
};
rules.push(newRule);
saveCustomRules(rules);
lastSavedRule = newRule;
listEl.innerHTML = renderList();
form.style.display = 'none';
showSaveToast('规则已保存,刷新页面后生效');
const shareBar = document.createElement('div');
shareBar.className = 'iz-rule-share-bar';
shareBar.style.cssText = 'margin-top:10px;padding:10px 14px;background:#F0FDF4;border-radius:12px;display:flex;align-items:center;justify-content:space-between;gap:10px;font-size:12px;color:#166534;';
shareBar.innerHTML = `
是否把这条规则分享给作者,帮助更多用户?(仅发送域名和选择器)
`;
form.parentNode.insertBefore(shareBar, form.nextSibling);
shareBar.querySelector('.iz-rule-share-btn').addEventListener('click', function() {
const btn = this;
btn.disabled = true; btn.textContent = '发送中…';
submitSiteRule(lastSavedRule).then(() => {
btn.textContent = '✓ 已分享,感谢!';
btn.style.background = '#64748B';
GM_setValue(`rule_shared_${lastSavedRule.id}`, true);
setTimeout(() => shareBar.remove(), 3000);
}).catch((err) => {
btn.disabled = false; btn.textContent = '重试';
showToast('分享失败:' + err.message);
});
});
});
listEl.addEventListener('click', (e) => {
const item = e.target.closest('.iz-rule-item');
if (!item) return;
const rules = getCustomRules();
const idx = rules.findIndex(r => r.id === item.dataset.id);
if (idx < 0) return;
if (e.target.classList.contains('iz-rule-del')) {
rules.splice(idx, 1);
saveCustomRules(rules);
listEl.innerHTML = renderList();
showSaveToast('规则已删除');
} else if (e.target.classList.contains('iz-rule-enabled')) {
rules[idx].enabled = e.target.checked;
saveCustomRules(rules);
}
});
}
const COMMON_PARAM_DEFS = [
{ key: 'delay', label: '悬停延迟', unit: 'ms', min: 0, max: 2000, step: 100, tip: '鼠标停在图片上多久后才放大。数值越小响应越快,越大越不容易误触发。建议 300~800ms' },
{ key: 'minOriginalSize', label: '最小放大尺寸', unit: 'px', min: 0, max: 500, step: 5, tip: '原图宽或高小于此值时不放大,用来过滤网站里的小图标、表情、按钮图标等。设为 0 表示全部放大' },
{ key: 'maxWidth', label: '大图最大宽度', unit: 'px', min: 300, max: 3000, step: 100, tip: '放大后的大图宽度上限。自适应模式下它决定了放大画布的宽度上限,调小后大图整体变小;固定模式下大图最多放大到这个宽度' },
{ key: 'maxHeight', label: '大图最大高度', unit: 'px', min: 300, max: 3000, step: 100, tip: '放大后的大图高度上限。自适应模式下它决定了放大画布的高度上限,调小后大图整体变小;固定模式下大图最多放大到这个高度' },
{ key: 'scrollSpeed', label: '滚轮移动速度', unit: 'px', min: 5, max: 50, step: 1, tip: '大图超出屏幕时,滚动鼠标滚轮查看图片其余部分,每次滚动的距离。数值越大滚得越快' }
];
const FIXED_PARAM_DEFS = [
{ key: 'scale', label: '大图放大倍数', unit: '×', min: 1, max: 5, step: 0.1, tip: '固定倍数模式下,大图相对原图的放大倍数' },
{ key: 'minScale', label: '大图最小倍数', unit: '×', min: 1, max: 3, step: 0.1, tip: '固定倍数模式下,大图至少要放大到的倍数下限,避免小图放大后依然看不清' },
{ key: 'portraitRatio', label: '竖屏判定比例', unit: '×', min: 1, max: 3, step: 0.1, tip: '图片高÷宽超过这个值就判定为竖长图(如手机截图、漫画长图),固定模式下会按高度优先铺满放大' },
{ key: 'smallImgThreshold', label: '小图判定阈值', unit: 'px', min: 100, max: 500, step: 10, tip: '固定模式下,原图宽或高小于此值会被当作「小图」,改用下方两个小图专用尺寸放大,而不是套用大图规则' },
{ key: 'smallImgWidth', label: '小图强制宽度', unit: 'px', min: 300, max: 1000, step: 10, tip: '固定模式下,判定为小图的图片放大后的宽度基准(高度按原图比例自动计算)' },
{ key: 'smallImgHeight', label: '小图强制高度', unit: 'px', min: 300, max: 1000, step: 10, tip: '固定模式下,判定为小图的图片放大后的高度基准(宽度按原图比例自动计算)' }
];
function createConfigPanel() {
const overlay = document.createElement('div');
overlay.id = 'izModalOverlay';
const renderParams = (defs) => defs.map(p => `
`).join('');
overlay.innerHTML = `
`;
document.body.appendChild(overlay);
injectFeedbackSection(overlay);
injectCustomRulesSection(overlay);
const $ = (id) => overlay.querySelector('#' + id);
const modeSelect = $('izModeSelect');
const conflictCheck = $('izConflictCheck');
const commonHeader = $('izCommonHeader');
const commonBody = $('izCommonBody');
const commonArrow = $('izCommonArrow');
const commonHint = $('izCommonHint');
const fixedHeader = $('izFixedHeader');
const fixedBody = $('izFixedBody');
const fixedArrow = $('izFixedArrow');
const fixedHint = $('izFixedHint');
const modeBadge = $('izModeBadge');
const biliToggle = $('izBiliToggle');
// ===== 参数说明气泡:全局唯一,挂在 body 上,fixed 定位不受折叠区裁剪 =====
const oldTip = document.getElementById('izTipBubble');
if (oldTip) oldTip.remove();
const tipBubble = document.createElement('div');
tipBubble.id = 'izTipBubble';
document.body.appendChild(tipBubble);
const showTip = (icon) => {
tipBubble.textContent = icon.dataset.tip;
const r = icon.getBoundingClientRect();
tipBubble.style.opacity = '0';
tipBubble.style.display = 'block';
const bw = tipBubble.offsetWidth;
const bh = tipBubble.offsetHeight;
let top = r.top - bh - 10;
if (top < 8) top = r.bottom + 10;
let left = r.left + r.width / 2 - bw / 2;
left = Math.max(8, Math.min(left, window.innerWidth - bw - 8));
tipBubble.style.top = top + 'px';
tipBubble.style.left = left + 'px';
tipBubble.style.opacity = '1';
};
const hideTip = () => { tipBubble.style.opacity = '0'; };
overlay.addEventListener('mouseover', (e) => {
const icon = e.target.closest('.iz-tip-icon');
if (icon) showTip(icon);
});
overlay.addEventListener('mouseout', (e) => {
if (e.target.closest('.iz-tip-icon')) hideTip();
});
function updateDetailState() {
const isFixed = config.zoomMode === 'fixed';
modeBadge.textContent = isFixed ? '固定倍数模式' : '智能自适应模式';
if (isFixed) {
fixedHeader.style.cursor = 'pointer';
fixedHeader.style.opacity = '1';
fixedHint.textContent = '点击展开 · 悬停问号查看参数说明';
} else {
fixedBody.classList.remove('open');
fixedArrow.classList.remove('open');
fixedHeader.style.cursor = 'not-allowed';
fixedHeader.style.opacity = '0.55';
fixedHint.textContent = '自适应模式下不可用';
}
overlay.querySelectorAll('.iz-param-input').forEach(input => {
const isFixedParam = FIXED_PARAM_DEFS.some(p => p.key === input.dataset.param);
input.disabled = isFixedParam && !isFixed;
const group = input.closest('.iz-input-group');
if (group) group.classList.toggle('disabled-group', isFixedParam && !isFixed);
});
}
$('izHpToggleBtn').addEventListener('click', (e) => {
e.stopPropagation();
toggleHomepageDisabled();
});
modeSelect.addEventListener('change', () => {
config.zoomMode = modeSelect.value;
saveConfig();
updateDetailState();
showSaveToast(`已切换至 ${config.zoomMode === 'fixed' ? '固定倍数' : '智能自适应'} 模式`);
});
$('izConflictWrap').addEventListener('click', (e) => {
if (e.target.closest('.iz-checkbox-custom') || e.target.closest('.iz-checkbox-label')) {
config.avoidClickConflict = !config.avoidClickConflict;
conflictCheck.classList.toggle('checked', config.avoidClickConflict);
saveConfig();
showSaveToast(`避免与点击放大功能冲突 ${config.avoidClickConflict ? '已开启' : '已关闭'}`);
if (isEnabled) {
cleanup();
initImages();
}
}
});
commonHeader.addEventListener('click', () => {
const isOpen = commonBody.classList.contains('open');
commonBody.classList.toggle('open');
commonArrow.classList.toggle('open');
commonHint.textContent = isOpen ? '点击展开 · 悬停问号查看参数说明' : '点击收起';
});
fixedHeader.addEventListener('click', () => {
if (config.zoomMode !== 'fixed') {
showSaveToast('切换到固定倍数模式后才能调整这些参数');
return;
}
const isOpen = fixedBody.classList.contains('open');
fixedBody.classList.toggle('open');
fixedArrow.classList.toggle('open');
fixedHint.textContent = isOpen ? '点击展开 · 悬停问号查看参数说明' : '点击收起';
});
overlay.querySelectorAll('.iz-param-input').forEach(input => {
const key = input.dataset.param;
const def = COMMON_PARAM_DEFS.concat(FIXED_PARAM_DEFS).find(p => p.key === key);
input.addEventListener('input', () => {
let val = parseFloat(input.value);
if (isNaN(val)) val = defaultConfig[key];
config[key] = val;
saveConfig();
notifyConfigSaved(key, val, def ? def.label : key);
});
});
$('izBiliWrap').addEventListener('click', (e) => {
if (e.target.closest('.iz-toggle')) {
e.stopPropagation();
const newState = !bilibiliVolumeModule.isEnabled;
bilibiliVolumeModule.setEnabled(newState);
biliToggle.classList.toggle('active', newState);
showSaveToast(`B站播放器辅助 ${newState ? '已启用' : '已禁用'}`);
}
});
$('izResetBtn').addEventListener('click', () => {
if (!confirm('确定要恢复所有设置为默认值吗?')) return;
config = { ...defaultConfig };
saveConfig();
modeSelect.value = config.zoomMode;
conflictCheck.classList.toggle('checked', config.avoidClickConflict);
overlay.querySelectorAll('.iz-param-input').forEach(input => {
input.value = config[input.dataset.param];
});
updateDetailState();
showToast('已恢复默认设置 🎉');
});
function closePanel() {
overlay.classList.add('anim-out');
setTimeout(() => {
overlay.style.display = 'none';
overlay.classList.remove('anim-out');
}, 300);
}
$('izCloseBtn').addEventListener('click', closePanel);
$('izSaveBtn').addEventListener('click', () => {
showSaveToast('设置已保存');
setTimeout(closePanel, 350);
});
overlay.addEventListener('click', (e) => {
if (e.target === overlay) closePanel();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && overlay.style.display !== 'none' && overlay.style.display !== '') {
closePanel();
}
});
updateDetailState();
return overlay;
}
function toggleConfigPanel() {
let overlay = document.getElementById('izModalOverlay');
if (!overlay) {
overlay = createConfigPanel();
}
if (overlay.style.display === 'flex') {
overlay.style.display = 'none';
} else {
overlay.querySelector('#izModeSelect').value = config.zoomMode;
overlay.querySelector('#izConflictCheck').classList.toggle('checked', config.avoidClickConflict);
overlay.querySelectorAll('.iz-param-input').forEach(input => {
input.value = config[input.dataset.param];
});
overlay.querySelector('#izBiliToggle').classList.toggle('active', bilibiliVolumeModule.isEnabled);
refreshPanelHomepageSection();
overlay.classList.add('anim-in');
overlay.style.display = 'flex';
setTimeout(() => overlay.classList.remove('anim-in'), 400);
}
}
function onWheel(e) {
if (currentZoomContainer && isEnabled) {
e.preventDefault();
e.stopPropagation();
debounceMove(e);
return true;
}
return false;
}
const debounceMove = debounce(function(e) {
if (!currentZoomContainer || !isEnabled) return;
const zoomedImg = currentZoomContainer.querySelector('img');
if (!zoomedImg) return;
const moveDistance = e.deltaY > 0 ? -config.scrollSpeed : config.scrollSpeed;
const currentMarginTop = parseFloat(zoomedImg.style.marginTop) || 0;
zoomedImg.style.marginTop = (currentMarginTop + moveDistance) + 'px';
}, 16);
function handleResize() {
if (!isEnabled) return;
// 尺寸变化后旧放大图位置已失效,直接统一收起,避免残留透明的占位容器
document.querySelectorAll('.image-zoom-container').forEach(container => fadeOutAndRemove(container));
currentZoomContainer = null;
}
function addKeyboardSupport() {
document.addEventListener('keydown', (e) => {
if (e.altKey && e.key === 'z') {
e.preventDefault();
if (!isHomepageZoomDisabled()) {
toggleEnabled();
}
}
});
}
// 智能自适应:按小图面积放大到视口可用尺寸,超出可用宽高时等比收缩
function computeAdaptiveSize(img, rect) {
const availW = Math.min(window.innerWidth - 60, config.maxWidth);
const availH = Math.min(window.innerHeight - 60, config.maxHeight);
const size = Math.sqrt(rect.width * rect.height);
const availSize = Math.sqrt(availW * availH);
const TARGET_MIN = Math.min(1200, availSize);
let target = Math.min(Math.max(size * 5, TARGET_MIN), availSize);
let scale = target / size;
scale = Math.min(scale, 10);
scale = Math.max(scale, 1);
let w = Math.round(rect.width * scale);
let h = Math.round(rect.height * scale);
if (w > availW || h > availH) {
const fit = Math.min(availW / w, availH / h);
w = Math.round(w * fit);
h = Math.round(h * fit);
}
return { w, h };
}
function createZoomedImage(img, hasClickFunctionality = false) {
if (!isEnabled) return null;
try {
if (currentZoomContainer) {
try { currentZoomContainer.remove(); } catch (e) { }
currentZoomContainer = null;
}
const rect = img.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return null;
const isSmallImg = rect.width < config.smallImgThreshold || rect.height < config.smallImgThreshold;
const isPortrait = rect.height / rect.width > config.portraitRatio;
let targetWidth, targetHeight;
if (config.zoomMode === 'adaptive') {
const size = computeAdaptiveSize(img, rect);
targetWidth = size.w;
targetHeight = size.h;
} else if (isSmallImg) {
targetWidth = config.smallImgWidth;
targetHeight = config.smallImgHeight;
const imgRatio = rect.width / rect.height;
if (isPortrait) {
targetWidth = Math.round(targetHeight * imgRatio);
} else {
targetHeight = Math.round(targetWidth / imgRatio);
}
targetWidth = Math.min(targetWidth, config.maxWidth);
targetHeight = Math.min(targetHeight, config.maxHeight);
} else {
if (isPortrait) {
const heightScale = config.maxHeight / rect.height;
targetWidth = Math.round(rect.width * heightScale);
targetHeight = Math.round(rect.height * heightScale);
} else {
const widthScale = config.maxWidth / rect.width;
targetWidth = Math.round(rect.width * widthScale);
targetHeight = Math.round(rect.height * widthScale);
}
targetWidth = Math.min(targetWidth, config.maxWidth);
targetHeight = Math.min(targetHeight, config.maxHeight);
}
// 按原图比例校正画布,避免 contain 留空导致容器比图片大
// 注意:图片未加载完时 naturalWidth/Height 为 0,会算出 NaN 导致容器塌缩,必须跳过
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
const natRatio = img.naturalWidth / img.naturalHeight;
const boxRatio = targetWidth / targetHeight;
if (natRatio > boxRatio) {
targetHeight = Math.round(targetWidth / natRatio);
} else {
targetWidth = Math.round(targetHeight * natRatio);
}
}
const zoomedImgStyle = `
width: ${targetWidth}px;
height: ${targetHeight}px;
max-width: ${config.maxWidth}px;
max-height: ${config.maxHeight}px;
object-fit: contain;
transition: ${config.transition}, transform 0.35s cubic-bezier(0.34, 1.56, 0.64, 1);
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
margin-top: 0;
display: block;
border-radius: 10px;
transform: scale(0.6);
opacity: 0;
`;
const zoomZIndex = hasClickFunctionality ? config.zoomZIndex - 1 : config.zoomZIndex;
const zoomContainer = document.createElement('div');
zoomContainer.className = 'image-zoom-container';
zoomContainer.style.cssText = `
position: fixed;
z-index: ${zoomZIndex};
opacity: 0;
transition: ${config.transition};
pointer-events: none;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: ${targetWidth}px;
height: ${targetHeight}px;
box-sizing: border-box;
border-radius: 10px;
overflow: hidden;
`;
const zoomedImg = document.createElement('img');
const fallbackSrc = img.src || img.currentSrc;
let hiResSrc;
if (currentDomain === 'jd.com' || currentDomain.endsWith('.jd.com')) {
hiResSrc = fallbackSrc;
} else {
hiResSrc = upgradeImgUrl((img.src || img.currentSrc || '')
.replace(/\/s\d+x\d+_/g, '/')
.replace(/\.avif$/i, '')) || fallbackSrc;
}
let needsCors = true;
try { needsCors = new URL(hiResSrc, location.href).origin !== location.origin; } catch (e) { }
if (needsCors) zoomedImg.crossOrigin = 'anonymous';
zoomedImg.src = hiResSrc;
let noCorsTried = false;
zoomedImg.onerror = () => {
if (zoomedImg.crossOrigin && !noCorsTried) {
noCorsTried = true;
zoomedImg.removeAttribute('crossOrigin');
zoomedImg.src = hiResSrc;
return;
}
// 原图彻底失败 → 回退缩略图
if (zoomedImg.src !== fallbackSrc) zoomedImg.src = fallbackSrc;
};
zoomedImg.onload = () => cropBlackBars(zoomedImg);
zoomedImg.alt = img.alt;
zoomedImg.style.cssText = zoomedImgStyle;
zoomContainer.appendChild(zoomedImg);
document.body.appendChild(zoomContainer);
if (wheelManager) wheelManager.sync();
currentZoomContainer = zoomContainer;
setTimeout(() => {
zoomContainer.style.opacity = '1';
zoomedImg.style.transform = 'scale(1)';
zoomedImg.style.opacity = '1';
}, 10);
return zoomContainer;
} catch (error) {
console.warn('创建放大图失败:', error);
return null;
}
}
function showZoom(img) {
if (!isEnabled) return;
if (config.avoidClickConflict && isImageInLightboxMode()) {
return;
}
const state = states.get(img);
if (!state || state.isZoomed) return;
const zoomContainer = createZoomedImage(img, state.hasClickFunctionality);
if (!zoomContainer) return;
states.set(img, { ...state, isZoomed: true, zoomContainer });
}
// ===== 统一的"淡出并移除"封装(img 传入时才会重置对应 state,且仅在 state 仍指向该容器时重置,避免竞态)=====
function fadeOutAndRemove(container, img) {
if (!container) return;
container.style.opacity = '0';
const zImg = container.querySelector('img');
if (zImg) {
zImg.style.transform = 'scale(0.6)';
zImg.style.opacity = '0';
if (zImg.__zoomBlobUrl) { URL.revokeObjectURL(zImg.__zoomBlobUrl); zImg.__zoomBlobUrl = null; }
}
setTimeout(() => {
if (container && container.parentNode) container.parentNode.removeChild(container);
if (currentZoomContainer === container) {
currentZoomContainer = null;
if (wheelManager) wheelManager.sync();
}
if (img) {
const state = states.get(img);
// 关键:只有 state 仍指向这个旧容器时才重置,
// 防止快速"移出→再移入"时旧定时器把新容器状态覆盖掉
if (state && state.zoomContainer === container) {
states.set(img, { ...state, isZoomed: false, zoomContainer: null, timer: null });
}
}
}, 300);
}
function hideZoom(img) {
const state = states.get(img);
if (!state || !state.isZoomed || !state.zoomContainer) return;
fadeOutAndRemove(state.zoomContainer, img);
}
function handleStretchedLink(img) {
const card = img.closest('.card');
if (!card) return;
const link = card.querySelector('a.stretched-link');
if (!link || link.dataset.stretchFixed) return;
link.dataset.stretchFixed = '1';
link.addEventListener('mouseenter', () => {
if (img.classList.contains('image-zoom-processed')) {
img.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
}
});
link.addEventListener('mouseleave', () => {
if (img.classList.contains('image-zoom-processed')) {
img.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
}
});
}
// ===== 自动裁剪放大图的黑边(只保留有内容的区域)=====
// 优化:从四边向内逐行/列扫描,而不是全像素遍历;输出用 toBlob(jpeg)替代 dataURL
function cropBlackBars(imgEl) {
try {
if (imgEl.dataset.zoomCropped) return; // 防止替换 src 后再次触发 onload 重复裁剪
const w = imgEl.naturalWidth, h = imgEl.naturalHeight;
if (!w || !h) return;
const canvas = document.createElement('canvas');
canvas.width = w; canvas.height = h;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
ctx.drawImage(imgEl, 0, 0);
let data;
try { data = ctx.getImageData(0, 0, w, h).data; }
catch (e) { return; } // 图片跨域受保护,读不了像素 → 放弃裁剪,按原图显示
// 透明 PNG 检测:抽样检查是否有明显透明区域,有则跳过裁剪
// (透明像素会被黑边逻辑误判,且 JPEG 输出会把透明变黑)
try {
const sampleData = ctx.getImageData(0, 0, w, h).data;
let transparentCount = 0;
const total = w * h;
const step = Math.max(1, Math.floor(total / 20000)); // 抽样约2万个点,够快
for (let i = 0; i < total; i += step) {
if (sampleData[i * 4 + 3] < 10) transparentCount++;
}
if (transparentCount / (total / step) > 0.05) return; // 超过5%透明像素,不裁
} catch (e) { }
const threshold = 24; // 亮度低于此值视为黑边
const isContent = (i) => data[i + 3] > 10 &&
(data[i] > threshold || data[i + 1] > threshold || data[i + 2] > threshold);
const rowHasContent = (y) => {
for (let x = 0; x < w; x++) if (isContent((y * w + x) * 4)) return true;
return false;
};
const colHasContent = (x) => {
for (let y = 0; y < h; y++) if (isContent((y * w + x) * 4)) return true;
return false;
};
let top = 0; while (top < h && !rowHasContent(top)) top++;
if (top === h) return; // 整张全黑,不裁
let bottom = h - 1; while (bottom > top && !rowHasContent(bottom)) bottom--;
let left = 0; while (left < w && !colHasContent(left)) left++;
let right = w - 1; while (right > left && !colHasContent(right)) right--;
// 防误判:深色图片容易被整片误判为黑边。
// 每边最多只允许裁掉 25%,超出的部分强制保留
const maxCutX = Math.floor(w * 0.25);
const maxCutY = Math.floor(h * 0.25);
top = Math.min(top, maxCutY);
bottom = Math.max(bottom, h - 1 - maxCutY);
left = Math.min(left, maxCutX);
right = Math.max(right, w - 1 - maxCutX);
const cw = right - left + 1, ch = bottom - top + 1;
if (cw >= w * 0.9 && ch >= h * 0.9) return; // 几乎没有黑边,不裁
if (cw < 20 || ch < 20) return; // 裁出来太小,疑似误判,不裁
const out = document.createElement('canvas');
out.width = cw; out.height = ch;
out.getContext('2d').drawImage(imgEl, left, top, cw, ch, 0, 0, cw, ch);
imgEl.dataset.zoomCropped = '1';
if (typeof out.toBlob === 'function') {
out.toBlob((blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
if (imgEl.__zoomBlobUrl) URL.revokeObjectURL(imgEl.__zoomBlobUrl);
imgEl.__zoomBlobUrl = url;
imgEl.src = url;
// 同步容器尺寸:按裁剪比例缩放"当前显示尺寸",而不是直接用源像素尺寸
const box = imgEl.parentNode;
if (box && box.classList.contains('image-zoom-container')) {
const dispW = parseFloat(imgEl.style.width) || w;
const dispH = parseFloat(imgEl.style.height) || h;
// 裁剪后不缩小显示尺寸,而是按新比例重新撑满可用空间
const availW = Math.min(window.innerWidth - 60, config.maxWidth);
const availH = Math.min(window.innerHeight - 60, config.maxHeight);
const cropRatio = cw / ch;
let newW = availW, newH = Math.round(availW / cropRatio);
if (newH > availH) {
newH = availH;
newW = Math.round(availH * cropRatio);
}
box.style.width = newW + 'px';
box.style.height = newH + 'px';
imgEl.style.width = newW + 'px';
imgEl.style.height = newH + 'px';
}
}, 'image/jpeg', 0.92);
} else {
imgEl.src = out.toDataURL('image/jpeg', 0.92);
}
} catch (e) { }
}
// ===== 图片 URL 升级:缩略图 → 原图 =====
function upgradeImgUrl(url) {
if (!url) return url;
// Discuz 论坛远程缩略图:/remote/thumb/宽x高/原路径 → 原图
url = url.replace(/\/remote\/thumb\/\d+x\d+\//, '/');
return url;
}
// ===== 背景图 URL 提取与清洗=====
function extractBgUrl(el) {
if (!el || el.nodeType !== 1) return null;
let url = null;
let m = null;
// 先看内联 style
const inline = el.getAttribute('style') || '';
m = inline.match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/i);
if (m) url = m[1];
// 再看计算样式(有些站点背景图是 class 控制的)
if (!url) {
try {
const bg = getComputedStyle(el).backgroundImage;
if (bg && bg !== 'none') {
m = bg.match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/i);
if (m) url = m[1];
}
} catch (e) { }
}
return url ? cleanBgUrl(url) : null;
}
function cleanBgUrl(url) {
let u = url.trim().replace(/^['"]|['"]$/g, '');
if (/alicdn\.com/i.test(u)) {
let prev;
do {
prev = u;
u = u.replace(/(_!![\w\-.,]+?\.(?:jpg|jpeg|png|webp))_[\w.\-]+$/i, '$1') // xxx_!!123.jpg_400x400q90.jpg → xxx_!!123.jpg
.replace(/\.(jpg|jpeg|png|webp)_[\w.]+$/i, '.$1') // xxx.jpg_.webp → xxx.jpg
.replace(/_\.(webp|jpg|jpeg|png)$/i, ''); // xxx_.webp → xxx
} while (u !== prev);
return u;
}
// 其他站点:保留原有通用清洗规则
let prev;
do {
prev = u;
u = u.replace(/![\w\-]+\.(jpg|jpeg|png|webp)$/i, function (s) {
return s;
}).replace(/![\w\-]+$/i, '')
.replace(/_\d+x\d+(q\d+)?\.(jpg|jpeg|png|webp)(\.\w+)?$/i, '')
.replace(/\.(jpg|jpeg|png|webp)_[\w.]+$/i, '.$1')
.replace(/_\.(webp|jpg|jpeg|png)$/i, '')
.replace(/\.webp$/i, '.jpg');
} while (u !== prev);
return u;
}
function setupHoverProxy() {
const customRules = getCustomRules().filter(r => r.enabled).map(r => ({
domains: String(r.domains).split(',').map(s => s.trim()).filter(Boolean),
imgMode: r.imgMode === 'background' ? 'background' : 'img',
itemSelector: r.itemSelector,
cardSelector: r.cardSelector,
pollInterval: Math.max(100, parseInt(r.pollInterval) || 300)
}));
const domainMatch = (r) => r.domains.some(d => currentDomain === d || currentDomain.endsWith('.' + d));
const rule = customRules.find(domainMatch) || SITE_HOVER_PROXY_RULES.find(domainMatch);
if (!rule) return;
const itemSelector = rule.itemSelector;
const cardSelector = rule.cardSelector;
let lastPolledX = -1, lastPolledY = -1;
// —— 背景图模式专用:创建/移除放大层 ——
let bgZoomContainer = null;
let bgZoomUrl = null; // 记录当前显示的图,避免每个轮询周期都重建
const removeBgZoom = () => {
bgZoomUrl = null;
if (!bgZoomContainer) return;
const c = bgZoomContainer;
bgZoomContainer = null;
// 退出动画:图片缩小 + 整体渐隐,和进入动画对称
const img = c.querySelector('img');
if (img) img.style.transform = 'scale(0.6)';
c.style.opacity = '0';
setTimeout(() => c.remove(), 300);
};
const showBgZoom = (wrapperEl) => {
const rawUrl = extractBgUrl(wrapperEl);
if (!rawUrl) return;
// 同一张图已经在显示中 → 直接返回,不重建(防闪烁)
if (bgZoomContainer && bgZoomUrl === rawUrl) return;
removeBgZoom();
// 清掉可能还在退出动画中的旧容器,避免重叠
document.querySelectorAll('.image-zoom-container').forEach(el => el.remove());
// 提取"未经清洗"的原始 URL,作为加载失败的兜底
let originalUrl = null;
const inline = wrapperEl.getAttribute('style') || '';
const m = inline.match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/i);
if (m) originalUrl = m[1].trim().replace(/^['"]|['"]$/g, '');
if (!originalUrl) {
try {
const bg = getComputedStyle(wrapperEl).backgroundImage;
if (bg && bg !== 'none') {
const m2 = bg.match(/url\(\s*['"]?([^'")]+)['"]?\s*\)/i);
if (m2) originalUrl = m2[1].trim().replace(/^['"]|['"]$/g, '');
}
} catch (e) { }
}
// 调试:按 F12 打开控制台查看实际请求的地址
console.log('[图片放大] 清洗后URL:', rawUrl, '| 原始URL:', originalUrl);
const container = document.createElement('div');
container.className = 'image-zoom-container';
container.style.cssText = `
position: fixed; inset: 0;
z-index: ${config.zoomZIndex - 1};
opacity: 0; transition: all 0.3s ease;
pointer-events: none;
display: flex; justify-content: center; align-items: center;
padding: 20px; box-sizing: border-box;
background-color: rgba(0,0,0,0.1);
`;
const bigImg = document.createElement('img');
bigImg.style.cssText = `
max-width: ${Math.min(window.innerWidth - 60, config.maxWidth)}px;
max-height: ${Math.min(window.innerHeight - 60, config.maxHeight)}px;
object-fit: contain; border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
transform: scale(0.6);
transition: transform 0.35s cubic-bezier(0.34,1.56,0.64,1);
`;
// 三重保险:清洗URL → 失败回退原始URL → 再失败销毁放大层
let triedFallback = false;
bigImg.onerror = () => {
if (!triedFallback && originalUrl && originalUrl !== bigImg.src) {
triedFallback = true;
console.warn('[图片放大] 清洗URL加载失败,回退原始URL:', originalUrl);
bigImg.src = originalUrl;
return;
}
console.error('[图片放大] 图片加载彻底失败:', bigImg.src);
bgZoomUrl = null;
removeBgZoom();
};
bigImg.onload = () => {
requestAnimationFrame(() => {
container.style.opacity = '1';
bigImg.style.transform = 'scale(1)';
});
};
bigImg.src = rawUrl;
container.appendChild(bigImg);
document.body.appendChild(container);
bgZoomContainer = container;
bgZoomUrl = rawUrl;
};
setInterval(() => {
if (document.hidden) return;
if (!isEnabled || isHomepageZoomDisabled()) return;
const x = lastMouse.x;
const y = lastMouse.y;
if (x < 0) return;
if (rule.imgMode === 'img' && x === lastPolledX && y === lastPolledY) return;
lastPolledX = x; lastPolledY = y;
const el = document.elementFromPoint(x, y);
if (!el || !el.closest) return;
// ===== 背景图模式=====
if (rule.imgMode === 'background') {
let wrapper = el.closest ? el.closest(itemSelector) : null;
// 2) 兜底:命中了 .hover-border / .item-appear 等覆盖在图片上的遮罩层,
// closest 找不到容器 → 改用“坐标是否落在图片矩形内”判断
if (!wrapper) {
const card = el.closest ? el.closest(cardSelector) : null;
if (card) {
const w = card.querySelector(itemSelector);
if (w) {
const r = w.getBoundingClientRect();
if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
wrapper = w;
}
}
}
}
if (wrapper) {
showBgZoom(wrapper);
} else {
// 鼠标在卡片内但不在图上(如文字区)时保留放大层,完全离开卡片才收起(防闪烁)
const card = el.closest ? el.closest(cardSelector) : null;
if (!card || !card.querySelector(itemSelector)) removeBgZoom();
}
return;
}
let scope = null;
if (el.tagName === 'IMG') {
scope = el;
} else {
scope = el.closest?.(itemSelector) || el.closest?.(cardSelector) || null;
// 自定义规则兜底:itemSelector 直接是 img 标签选择器时,命中元素本身就是 img
if (!scope || (scope !== el && scope.tagName !== 'IMG' && !scope.querySelector('img'))) {
const direct = document.elementFromPoint(x, y);
if (direct && direct.tagName !== 'IMG') {
try {
const hit = direct.closest(itemSelector);
if (hit && hit.tagName === 'IMG') scope = hit;
} catch (e) { }
}
}
}
// 正确地把每个 item 选择器都拼上 " img",避免 .more2_img 裸命中容器 div
const imgSel = itemSelector.split(',').map(s => s.trim() + ' img').join(',');
const img = scope ? (scope.tagName === 'IMG' ? scope : (scope.querySelector(imgSel) || scope.querySelector('img'))) : null;
const hoveredImg = (img && img.closest(itemSelector)) ? img : null;
const prevImg = document.querySelector(`img[data-__zoom-hovering="1"]`);
if (hoveredImg && hoveredImg !== prevImg) {
if (prevImg) {
prevImg.removeAttribute('data-__zoom-hovering');
prevImg.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));
}
if (!hoveredImg.classList.contains('image-zoom-processed')) {
processImage(hoveredImg);
}
if (hoveredImg.classList.contains('image-zoom-processed')) {
hoveredImg.dataset.__zoomHovering = '1';
hoveredImg.dispatchEvent(new MouseEvent('mouseenter', { bubbles: false, clientX: x, clientY: y }));
}
} else if (!hoveredImg && prevImg) {
prevImg.removeAttribute('data-__zoom-hovering');
prevImg.dispatchEvent(new MouseEvent('mouseleave', { bubbles: false }));
}
}, rule.pollInterval);
}
function processImage(img) {
if (!isEnabled) return;
try {
if (!img || !img.parentNode) return;
if (!isValidImage(img)) return;
let containers = [];
let current = img.parentNode;
while (current && current !== document.body) {
containers.push(current);
if (current.tagName === 'A' || current.classList.contains('card') ||
current.classList.contains('col') || current.classList.contains('card-img-container')) {
break;
}
current = current.parentNode;
}
const container = containers.length > 0 ? containers[containers.length - 1] : img.parentNode;
const hasClickFunctionality = config.avoidClickConflict ?
(checkImageClickBehavior(img) || container.tagName === 'A' || container.classList.contains('stretched-link')) : false;
if (img.classList.contains('image-zoom-processed')) return;
img.classList.add('image-zoom-processed');
if (!hasClickFunctionality) {
img.classList.add('image-zoom-hover');
if (img.parentNode && img.parentNode !== document.body) {
img.parentNode.classList.add('image-zoom-hover');
}
}
handleStretchedLink(img);
const directParent = img.parentNode;
const handleParentMouseEnter = (e) => {
const rect = directParent.getBoundingClientRect();
if (e.clientX >= rect.left && e.clientX <= rect.right &&
e.clientY >= rect.top && e.clientY <= rect.bottom) {
img.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true, cancelable: true }));
}
};
let isDispatchingLeave = false;
const handleParentMouseLeave = (e) => {
if (isDispatchingLeave) return;
if (!directParent.contains(e.relatedTarget)) {
isDispatchingLeave = true;
try {
img.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true, cancelable: true }));
} finally {
isDispatchingLeave = false;
}
}
};
if (directParent && directParent !== document.body) {
if (!directParent.dataset.zoomProxy) {
directParent.dataset.zoomProxy = "true";
directParent.addEventListener('mouseenter', handleParentMouseEnter);
directParent.addEventListener('mouseleave', handleParentMouseLeave);
}
}
let timer = null;
let isZoomed = false;
let zoomContainer = null;
const handleMouseEnter = (e) => {
if (!isEnabled || isZoomed) return;
if (config.avoidClickConflict && isImageInLightboxMode()) {
return;
}
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
const rect = img.getBoundingClientRect();
if (rect.width < config.minOriginalSize || rect.height < config.minOriginalSize) return;
if (!isZoomed) {
zoomContainer = createZoomedImage(img, hasClickFunctionality);
if (zoomContainer) {
isZoomed = true;
states.set(img, { ...states.get(img), isZoomed: true, zoomContainer });
}
}
}, config.delay);
};
const handleMouseLeave = (e) => {
const owner = img.closest('a, li, .more2_img') || img.parentNode;
if (e.relatedTarget && owner && owner.contains(e.relatedTarget)) {
return;
}
if (timer) {
clearTimeout(timer);
timer = null;
}
if (isZoomed && zoomContainer) {
fadeOutAndRemove(zoomContainer, img);
isZoomed = false;
zoomContainer = null;
}
};
const handleClick = (e) => {
if (isZoomed && zoomContainer) {
hideZoom(img);
isZoomed = false;
zoomContainer = null;
}
};
img.addEventListener('mouseenter', handleMouseEnter);
img.addEventListener('mouseleave', handleMouseLeave);
img.addEventListener('click', handleClick, true);
states.set(img, {
isZoomed, timer, zoomContainer, hasClickFunctionality,
containers: [directParent],
handlers: {
mouseenter: handleMouseEnter,
mouseleave: handleMouseLeave,
click: handleClick,
parent: directParent,
parentMouseenter: handleParentMouseEnter,
parentMouseleave: handleParentMouseLeave
}
});
} catch (error) {
console.warn('图片处理失败:', error);
}
}
function setupLazyHoverProcessor() {
document.addEventListener('mouseover', debounce((e) => {
lastMouse.x = e.clientX;
lastMouse.y = e.clientY;
if (!isEnabled || isHomepageZoomDisabled()) return;
let img = null;
if (e.target.tagName === 'IMG') img = e.target;
else if (e.target.closest) img = e.target.closest('img');
if (img && !img.classList.contains('image-zoom-processed')) {
processImage(img);
if (img.classList.contains('image-zoom-processed')) {
const rect = img.getBoundingClientRect();
if (rect.width >= config.minOriginalSize && rect.height >= config.minOriginalSize) {
img.dispatchEvent(new MouseEvent('mouseenter'));
}
}
}
}, 80), true);
}
function setupLightboxObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'attributes' &&
(mutation.target === document.body || mutation.target === document.documentElement)) {
if (isImageInLightboxMode()) {
document.querySelectorAll('.image-zoom-container').forEach(container => fadeOutAndRemove(container));
currentZoomContainer = null;
document.querySelectorAll('img.image-zoom-processed').forEach(img => {
const state = states.get(img);
if (state) {
states.set(img, { ...state, isZoomed: false, zoomContainer: null });
}
});
}
}
});
});
observer.observe(document.body, { attributes: true, attributeFilter: ['class'] });
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
lightboxObserver = observer;
return observer;
}
let lazyImageObserver = null;
// 视口外图片先观察,进入视口(含 300px 预加载缓冲)才真正处理,省 CPU/内存
function observeImage(img) {
if (!lazyImageObserver) {
lazyImageObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
lazyImageObserver.unobserve(entry.target);
processImage(entry.target);
}
});
}, { rootMargin: '300px 0px' });
}
lazyImageObserver.observe(img);
}
function initImages() {
if (isHomepageZoomDisabled()) return;
const imgs = document.querySelectorAll('img:not(.image-zoom-processed)');
if ('IntersectionObserver' in window) {
imgs.forEach(observeImage);
return;
}
// 旧浏览器回退:空闲时间分批处理
const queue = Array.from(imgs);
const processBatch = (deadline) => {
while (queue.length > 0 && (deadline.timeRemaining ? deadline.timeRemaining() > 0 : true)) {
processImage(queue.shift());
}
if (queue.length > 0) {
if ('requestIdleCallback' in window) {
requestIdleCallback(processBatch, { timeout: 1000 });
} else {
setTimeout(() => processBatch({ timeRemaining: () => 5 }), 10);
}
}
};
if ('requestIdleCallback' in window) {
requestIdleCallback(processBatch, { timeout: 1000 });
} else {
processBatch({ timeRemaining: () => 5 });
}
}
function startObserver() {
let processingQueue = false;
const observer = new MutationObserver(mutations => {
if (!isEnabled || isHomepageZoomDisabled()) return;
if (processingQueue) return;
processingQueue = true;
requestAnimationFrame(() => {
const nodesToProcess = new Set();
mutations.forEach(mutation => {
if (mutation.type === 'attributes' && mutation.target.tagName === 'IMG' &&
!mutation.target.classList.contains('image-zoom-processed')) {
nodesToProcess.add(mutation.target);
} else if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType !== Node.ELEMENT_NODE) return;
if (node.tagName === 'IMG' && !node.classList.contains('image-zoom-processed')) {
nodesToProcess.add(node);
} else if (node.tagName === 'A' && node.classList.contains('stretched-link')) {
const img = node.closest('.card')?.querySelector('img.image-zoom-processed');
if (img) handleStretchedLink(img);
} else if (node.tagName === 'A' && node.querySelector('img')) {
const img = node.querySelector('img');
if (img && !img.classList.contains('image-zoom-processed')) nodesToProcess.add(img);
} else if (node.querySelectorAll) {
node.querySelectorAll('img:not(.image-zoom-processed)').forEach(img => nodesToProcess.add(img));
node.querySelectorAll('a.stretched-link').forEach(link => {
const img = link.closest('.card')?.querySelector('img.image-zoom-processed');
if (img) handleStretchedLink(img);
});
}
});
}
});
nodesToProcess.forEach(img => {
if (lazyImageObserver) observeImage(img);
else processImage(img);
});
processingQueue = false;
});
});
observer.observe(document.body, {
childList: true, subtree: true, attributes: true,
attributeFilter: ['src', 'data-src', 'srcset']
});
zoomObserver = observer;
return observer;
}
function init() {
loadConfig();
loadState();
injectStyles();
createDockButton();
addKeyboardSupport();
window.addEventListener('resize', debounce(handleResize, 250));
setupLightboxObserver();
if (isEnabled) initImages();
setupLazyHoverProcessor();
setupHoverProxy();
startObserver();
}
return { init, cleanup, onWheel, hasActiveZoom: () => !!currentZoomContainer };
})();
// ================================
// B站播放器辅助模块
// ================================
const bilibiliVolumeModule = (function() {
let isEnabled = GM_getValue('bilibili_volume_enabled', true);
let toast = null;
function isInFullscreenMode() {
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
if (fullscreenElement) {
return true;
}
if (document.body.classList.contains('player-mode-webfullscreen')) {
return true;
}
const player = document.querySelector('.bpx-player-container');
if (player && player.classList.contains('state-fullscreen')) {
return true;
}
return false;
}
function findVideoElement() {
const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement || document.mozFullScreenElement;
if (fullscreenElement) {
const video = fullscreenElement.querySelector('video');
if (video) return video;
}
return document.querySelector('.bpx-player-container video, video');
}
function applyVolume(video, newVolume) {
const clamped = Math.max(0, Math.min(1, newVolume));
try {
const player = window.player;
if (player && typeof player.setVolume === 'function') {
player.setVolume(Math.round(clamped * 100));
if (clamped > 0) {
if (typeof player.setMute === 'function') player.setMute(false);
else video.muted = false;
}
return;
}
} catch (err) { }
video.volume = clamped;
video.muted = false;
}
function getVolume(video) {
try {
const player = window.player;
if (player && typeof player.getVolume === 'function') {
return player.getVolume() / 100;
}
} catch (err) {}
return video.volume;
}
function getVolumeIcon(volume) {
if (volume === 0) {
return ``;
}
return ``;
}
function showVolumeToast(volume) {
if (!toast) {
toast = document.createElement('div');
toast.id = 'bilibili-volume-toast';
toast.style.cssText = `
position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9); color: #333; padding: 8px 16px;
border-radius: 8px; z-index: 2147483647; font-size: 26px; font-weight: 300;
font-family: 'Segoe UI', Arial, sans-serif; opacity: 0; transition: opacity 0.3s ease;
pointer-events: none; box-shadow: 0 4px 20px rgba(0,0,0,0.2);
border: 1px solid rgba(0,0,0,0.1); min-width: 90px; text-align: center;
backdrop-filter: blur(10px); display: flex; align-items: center; justify-content: center; gap: 8px;
`;
}
const parentElement = document.fullscreenElement || document.webkitFullscreenElement || document.body;
if (!toast.parentNode || toast.parentNode !== parentElement) {
if (toast.parentNode) toast.parentNode.removeChild(toast);
parentElement.appendChild(toast);
}
toast.innerHTML = getVolumeIcon(volume) + `${volume === 0 ? '静音' : Math.round(volume * 100) + '%'}`;
toast.style.opacity = '1';
if (toast.timeoutId) clearTimeout(toast.timeoutId);
toast.timeoutId = setTimeout(() => {
toast.style.opacity = '0';
setTimeout(() => {
if (toast && toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 300);
}, 2000);
}
function onWheel(e) {
if (!isEnabled || !isInFullscreenMode()) return false;
const video = findVideoElement();
if (!video) return false;
e.stopPropagation();
e.preventDefault();
const target = Math.max(0, Math.min(1, getVolume(video) + (e.deltaY > 0 ? -0.02 : 0.02)));
applyVolume(video, target);
showVolumeToast(target);
return true;
}
function handleKeydown(e) {
if (!isEnabled) return;
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable) return;
if ((e.code === 'ArrowUp' || e.code === 'ArrowDown' || e.code === 'ArrowLeft' || e.code === 'ArrowRight') && isInFullscreenMode()) {
e.preventDefault();
}
}
function handleVolumeChange(e) {
if (!isEnabled) return;
if (e.target.tagName !== 'VIDEO') return;
if (!isInFullscreenMode()) return;
const video = e.target;
showVolumeToast(video.muted ? 0 : video.volume);
}
function init() {
if (!window.location.hostname.includes('bilibili.com')) return;
isInFullscreenMode();
setEnabled(isEnabled);
}
function setEnabled(enabled) {
isEnabled = enabled;
GM_setValue('bilibili_volume_enabled', enabled);
if (enabled) {
window.addEventListener('keydown', handleKeydown);
document.addEventListener('volumechange', handleVolumeChange, { capture: true });
} else {
window.removeEventListener('keydown', handleKeydown);
document.removeEventListener('volumechange', handleVolumeChange, { capture: true });
}
if (wheelManager) wheelManager.sync();
}
return {
init, setEnabled, onWheel,
isFullscreenActive: isInFullscreenMode,
get isEnabled() { return isEnabled; }
};
})();
wheelManager = (function () {
let attached = false;
function handler(e) {
if (imageZoomModule.onWheel(e)) return;
bilibiliVolumeModule.onWheel(e);
}
function sync() {
const need = imageZoomModule.hasActiveZoom() || bilibiliVolumeModule.isFullscreenActive();
if (need && !attached) {
document.addEventListener('wheel', handler, { capture: true, passive: false });
attached = true;
} else if (!need && attached) {
document.removeEventListener('wheel', handler, { capture: true });
attached = false;
}
}
document.addEventListener('fullscreenchange', sync);
document.addEventListener('webkitfullscreenchange', sync);
return { sync };
})();
// ================================
// 主初始化函数
// ================================
function mainInit() {
imageZoomModule.init();
bilibiliVolumeModule.init();
wheelManager.sync();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mainInit);
} else {
mainInit();
}
})();