增加多功能按钮,内含多种颜色护眼模式、调整页面宽度(状态持久化,页面刷新不变)、自动/挂机阅读(增加定时模式,单双栏阅读通用)、图片复制/下载
// ==UserScript==
// @name 微信读书增强脚本
// @version 1.0.9
// @namespace http://tampermonkey.net/
// @description 增加多功能按钮,内含多种颜色护眼模式、调整页面宽度(状态持久化,页面刷新不变)、自动/挂机阅读(增加定时模式,单双栏阅读通用)、图片复制/下载
// @author Chloe
// @match https://weread.qq.com/web/reader/*
// @icon https://weread.qq.com/favicon.ico
// @require https://code.jquery.com/jquery-3.7.1.min.js
// @grant GM_log
// @grant GM_addStyle
// @grant unsafeWindow
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_openInTab
// @grant GM_download
// @grant GM_setClipboard
// @grant GM_notification
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 常量定义
const DEFAULT_WIDTH = 800;
const EYE_PROTECTION_COLORS = {
'white': {
name: '白色',
color: 'rgba(255,255,255,1)',
className: 'eye-protection-white'
},
'green': {
name: '绿色',
color: 'rgba(216,226,200,1)',
className: 'eye-protection-green'
},
'yellow': {
name: '黄色',
color: 'rgba(240,234,214,1)',
className: 'eye-protection-yellow'
},
'blue': {
name: '蓝色',
color: 'rgba(200,220,240,1)',
className: 'eye-protection-blue'
},
'pink': {
name: '粉色',
color: 'rgba(255,230,230,1)',
className: 'eye-protection-pink'
},
'purple': {
name: '紫色',
color: 'rgba(230,220,250,1)',
className: 'eye-protection-purple'
},
'gray': {
name: '灰色',
color: 'rgba(240,240,240,1)',
className: 'eye-protection-gray'
}
};
// 状态变量
let scrollInterval = null;
let timerInterval = null;
let isAutoReading = GM_getValue('weread_auto_reading', false);
let isPageTurning = false;
let pageTurnCooldown = false;
let currentScrollSpeed = GM_getValue('weread_scroll_speed', 1.0);
let remainingTime = GM_getValue('weread_remaining_time', 0);
let lastTimerValue = GM_getValue('weread_last_timer', 0);
let windowTop = 0;
let bottomReachedTimer = null;
let isWaitingForPageTurn = false;
let lastScrollPosition = 0;
let progressInterval = null;
const generateEyeProtectionStyles = () => {
let styles = '';
Object.keys(EYE_PROTECTION_COLORS).forEach(colorKey => {
const colorInfo = EYE_PROTECTION_COLORS[colorKey];
styles += `
body .app_content.eye-protection-${colorKey},
body .readerContent .app_content.eye-protection-${colorKey},
body .wr_whiteTheme .readerContent .app_content.eye-protection-${colorKey},
body .readerChapterContent.eye-protection-${colorKey},
body .readerChapterContent_container.eye-protection-${colorKey},
body .wr_horizontalReader.eye-protection-${colorKey},
body .wr_horizontalReader_app_content.eye-protection-${colorKey},
body .readerTopBar.eye-protection-${colorKey},
body .${colorInfo.className} .app_content,
body .${colorInfo.className} .readerContent .app_content,
body .${colorInfo.className} .wr_various_font_provider_wrapper,
body .${colorInfo.className} .readerChapterContent,
body .${colorInfo.className} .readerChapterContent_container,
body .${colorInfo.className} .wr_horizontalReader,
body .${colorInfo.className} .wr_horizontalReader_app_content,
body .${colorInfo.className} .wr_whiteTheme .readerContent .app_content,
body .${colorInfo.className} .readerTopBar {
background-color: ${colorInfo.color} !important;
}
.color-${colorKey} {
background-color: ${colorInfo.color} !important;
}
`;
});
return styles;
};
// 样式注入 - 更新图片预览面板样式
GM_addStyle(`
*{font-family: TsangerJinKai05 !important;}
.readerTopBar{font-family: SourceHanSerifCN-Bold !important;}
.bookInfo_title{font-family: SourceHanSerifCN-Bold !important;}
.readerTopBar_title_link{font-family: SourceHanSerifCN-Bold; !important; font-weight:bold !important;}
.readerTopBar_title_chapter{font-family: SourceHanSerifCN-Bold !important;}
.readerChapterContent{color: rgba(0,0,0,100) !important;}
.readerControls{margin-left: calc(50% - 60px) !important; margin-bottom: -28px !important;}
/* 控制面板样式 */
.control-panel {
position: fixed;
left: 60px;
top: 50%;
transform: translateY(-50%);
border: 1px solid #ddd;
border-radius: 8px;
padding: 30px 15px 15px 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 99998;
min-width: 150px;
transition: background-color 0.3s ease;
cursor: move;
user-select: none;
}
.control-panel.dragging {
opacity: 0.9;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
}
.control-panel-close {
position: absolute;
right: 8px;
top: 8px;
background: none;
border: none;
font-size: 16px;
cursor: pointer;
color: #999;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
z-index: 1;
}
.control-panel-close:hover {
background: #f0f0f0;
color: #333;
}
.control-section {
margin: 15px 0;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.control-section:last-child {
border-bottom: none;
padding-bottom: 0;
}
.control-section-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
text-align: center;
}
.control-item {
margin: 10px 0;
display: flex;
align-items: center;
justify-content: space-between;
}
.control-label {
font-size: 12px;
color: #666;
margin-right: 10px;
}
.control-slider {
width: 120px;
height: 4px;
background: #ddd;
outline: none;
opacity: 0.7;
transition: opacity .2s;
border-radius: 2px;
}
.control-slider:hover {
opacity: 1;
}
.timer-slider {
background: linear-gradient(to right,
#4CAF50 0%, #4CAF50 4.17%,
#ddd 4.17%, #ddd 12.5%,
#4CAF50 12.5%, #4CAF50 25%,
#ddd 25%, #ddd 50%,
#4CAF50 50%, #4CAF50 100%);
}
.speed-slider {
background: linear-gradient(to right,
#4CAF50 0%, #4CAF50 10%,
#ddd 10%, #ddd 20%,
#4CAF50 20%, #4CAF50 30%,
#ddd 30%, #ddd 40%,
#4CAF50 40%, #4CAF50 60%,
#ddd 60%, #ddd 100%);
}
.control-value {
font-size: 12px;
color: #333;
min-width: 40px;
text-align: center;
font-family: monospace;
}
.control-buttons {
display: flex;
gap: 5px;
margin-top: 10px;
flex-wrap: wrap;
}
/* 操作按钮样式 */
.control-btn {
flex: 1;
padding: 6px 8px;
font-size: 12px;
border: 1px solid #ddd;
background: #f5f5f5;
border-radius: 4px;
cursor: pointer;
text-align: center;
min-width: 60px;
}
.control-btn:hover {
background: #e9e9e9;
}
.control-btn.active {
background: #4CAF50;
color: white;
border-color: #4CAF50;
}
.control-btn.reset {
background: #ff9800;
color: white;
border-color: #ff9800;
}
.control-btn.reset:hover {
background: #f57c00;
}
.control-btn.disabled {
background: #cccccc;
color: #666666;
cursor: not-allowed;
border-color: #cccccc;
}
.control-btn.secondary {
background: #e0e0e0;
color: #333;
border-color: #bdbdbd;
}
.color-options {
display: flex;
gap: 10px;
margin: 10px 0;
justify-content: center;
flex-wrap: wrap;
}
.color-option-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 5px;
cursor: pointer;
}
.color-option {
width: 30px;
height: 30px;
border-radius: 50%;
border: 2px solid #ddd;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.color-option:hover {
transform: scale(1.1);
box-shadow: 0 3px 6px rgba(0,0,0,0.15);
}
.color-option.active {
border-color: #333;
transform: scale(1.1);
box-shadow: 0 3px 8px rgba(0,0,0,0.2);
}
.color-name {
font-size: 10px;
color: #666;
text-align: center;
min-width: 40px;
}
/* 动态生成的护眼模式样式 */
${generateEyeProtectionStyles()}
/* 提示框样式 */
.custom-notification {
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.9);
color: white;
padding: 12px 24px;
border-radius: 6px;
z-index: 9999999;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease-in-out;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
max-width: 80%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.custom-notification.fade-out {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
/* 定时器显示样式 */
.timer-display {
font-size: 12px;
color: #666;
text-align: center;
margin-top: 5px;
}
/* 分割条样式 */
.section-divider {
height: 1px;
background: linear-gradient(to right, transparent, #ddd, transparent);
margin: 10px 0;
}
/* 自动翻页进度条样式 */
#auto-turn-progress {
position: fixed;
bottom: 20px;
right: 20px;
background: white;
border: 1px solid #ddd;
border-radius: 8px;
padding: 10px 15px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
z-index: 99997;
min-width: 160px;
display: none;
}
.progress-text {
font-size: 12px;
color: #333;
margin-bottom: 5px;
text-align: center;
}
.progress-bar {
width: 100%;
height: 6px;
background: #f0f0f0;
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: #2196F3;
border-radius: 3px;
transition: width 0.1s linear;
width: 100%;
}
/* 设置按钮图标样式 */
.settings-icon {
display: inline-block;
width: 16px;
height: 16px;
background: currentColor;
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.22,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.22,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z'/%3E%3C/svg%3E") no-repeat center;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.22,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.22,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.68 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z'/%3E%3C/svg%3E") no-repeat center;
}
/* 图片工具栏样式 - 修复单栏模式定位问题 */
.image-toolbar-container {
position: absolute;
top: 5px;
right: 5px;
z-index: 1000;
display: none;
}
.image-toolbar {
display: flex;
gap: 3px;
background: rgba(0,0,0,0.7);
border-radius: 4px;
padding: 3px;
backdrop-filter: blur(5px);
}
.image-tool-btn {
background: none;
border: none;
color: white;
font-size: 12px;
cursor: pointer;
padding: 4px;
border-radius: 3px;
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
transition: all 0.2s ease;
}
.image-tool-btn:hover:not(.disabled) {
background: rgba(255,255,255,0.2);
transform: scale(1.1);
}
.image-tool-btn.disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.image-tool-btn.loading {
opacity: 0.7;
cursor: wait;
}
.image-tool-icon {
font-size: 12px;
line-height: 1;
}
/* 单栏模式图片工具栏特殊处理 */
.passage-content {
position: relative !important;
}
.passage-content .image-toolbar-container {
position: absolute;
top: 5px;
right: 5px;
z-index: 1001;
}
/* 双栏模式图片工具栏定位 */
.passageContent_wrapper .image-toolbar-container {
position: absolute;
top: 5px;
right: 5px;
}
/* 图片预览面板样式 - 完全重写布局 */
.image-preview-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 100000;
display: none;
}
.image-preview-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: white;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.2);
z-index: 100001;
width: 95%;
max-width: 1200px;
height: 90vh;
display: none;
flex-direction: column;
overflow: hidden;
}
/* 顶部固定区域 */
.image-preview-header {
padding: 15px 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
background: #f9f9f9;
flex-shrink: 0;
min-height: 60px;
box-sizing: border-box;
}
.image-preview-title {
font-size: 16px;
font-weight: bold;
color: #333;
}
.image-preview-close {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #666;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
}
.image-preview-close:hover {
background: #f0f0f0;
color: #333;
}
/* 中间可滚动区域 */
.image-preview-content-container {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.image-preview-stats {
padding: 12px 20px;
border-bottom: 1px solid #eee;
background: #f5f5f5;
font-size: 14px;
color: #666;
text-align: center;
flex-shrink: 0;
min-height: 44px;
box-sizing: border-box;
}
.image-preview-controls {
padding: 15px 20px;
border-bottom: 1px solid #eee;
background: #fafafa;
flex-shrink: 0;
min-height: 60px;
box-sizing: border-box;
}
.select-all-container {
display: flex;
align-items: center;
gap: 8px;
}
.select-all-checkbox {
width: 16px;
height: 16px;
cursor: pointer;
}
.select-all-label {
font-size: 14px;
color: #666;
cursor: pointer;
font-weight: 500;
}
.image-preview-content {
flex: 1;
overflow-y: auto;
padding: 0px 20px;
min-height: 68vh;
max-height: 68vh;
}
.image-preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 5px;
align-content: flex-start;
}
/* 图片项样式 */
.image-preview-item {
height: max-content;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
transition: all 0.2s ease;
position: relative;
background: white;
display: flex;
flex-direction: column;
height: fit-content;
}
.image-preview-item:hover {
border-color: #4CAF50;
box-shadow: 0 2px 8px rgba(76,175,80,0.2);
transform: translateY(-2px);
}
.image-preview-item.selected {
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76,175,80,0.5);
}
.image-preview-checkbox {
position: absolute;
top: 8px;
left: 8px;
z-index: 2;
width: 18px;
height: 18px;
cursor: pointer;
}
.image-preview-thumb {
width: 100%;
height: 140px;
object-fit: cover;
background: #f5f5f5;
display: block;
}
.image-preview-info {
padding: 12px;
font-size: 12px;
color: #333;
word-break: break-all;
border-top: 1px solid #eee;
text-align: center;
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
}
.image-preview-info div {
color: #333;
margin-bottom: 4px;
}
.image-preview-info div:first-child {
font-weight: bold;
font-size: 13px;
}
.image-action-buttons {
display: flex;
gap: 6px;
margin-top: 8px;
justify-content: center;
}
.image-action-btn {
padding: 4px 10px;
font-size: 11px;
border: 1px solid #ddd;
background: #f8f8f8;
color: #333;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
flex: 1;
max-width: 70px;
}
.image-action-btn:hover {
background: #4CAF50;
color: white;
border-color: #4CAF50;
}
.image-action-btn.loading {
background: #ccc;
color: #666;
border-color: #ccc;
cursor: not-allowed;
}
.image-action-btn.disabled {
background: #ccc;
color: #666;
border-color: #ccc;
cursor: not-allowed;
}
/* 底部固定区域 */
.image-preview-actions {
padding: 20px;
border-top: 1px solid #eee;
display: flex;
gap: 12px;
justify-content: center;
background: #f9f9f9;
flex-wrap: wrap;
flex-shrink: 0;
min-height: 80px;
box-sizing: border-box;
}
/* 滚动条样式 */
.image-preview-content::-webkit-scrollbar {
width: 8px;
}
.image-preview-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.image-preview-content::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.image-preview-content::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 空状态样式 */
.image-preview-empty {
text-align: center;
color: #666;
padding: 40px;
font-size: 14px;
}
/* 响应式设计 */
@media (max-height: 800px) {
.image-preview-panel {
height: 95vh;
top: 50%;
}
.image-preview-thumb {
height: 120px;
}
}
@media (max-width: 768px) {
.image-preview-panel {
width: 98%;
height: 95vh;
}
.image-preview-grid {
height: 60vh;
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 10px;
}
.image-preview-actions {
flex-direction: column;
gap: 8px;
}
.control-btn {
min-width: auto;
width: 100%;
}
}
`);
// 工具函数
const utils = {
// 显示通知
notificationManager: {
currentNotification: null,
timeoutId: null,
show: function (message, duration = 1000) {
this.clear();
this.currentNotification = $(`<div class="custom-notification">${message}</div>`);
$('body').append(this.currentNotification);
this.timeoutId = setTimeout(() => this.close(), duration);
},
close: function () {
this.currentNotification?.addClass('fade-out');
setTimeout(() => {
this.currentNotification?.remove();
this.currentNotification = null;
}, 300);
this.timeoutId && clearTimeout(this.timeoutId);
this.timeoutId = null;
},
clear: function () {
this.close();
$('.custom-notification').remove();
}
},
// 检测DOM元素出现
waitForElement: function (selector, maxAttempts = 80) {
return new Promise(resolve => {
let attempts = 0;
const checkInterval = setInterval(() => {
if (document.querySelectorAll(selector).length) {
clearInterval(checkInterval);
resolve(true);
}
if (attempts >= maxAttempts) {
clearInterval(checkInterval);
resolve(false);
}
attempts++;
}, 100);
});
},
// 检查当前是否是白色主题
isWhiteTheme: () => {
const isWhite = document.body.classList.contains('wr_whiteTheme');
console.log("检查当前是否是白色主题", isWhite);
GM_setValue('isWhiteTheme', document.body.classList.contains('wr_whiteTheme'));
return isWhite;
},
isThemeChanged: () => GM_getValue('isWhiteTheme') !== utils.isWhiteTheme(),
// 新增:保存护眼模式状态
saveEyeProtectionState: function (enabled, color) {
let notNullColor = color;
let colorCode = EYE_PROTECTION_COLORS[notNullColor]?.color ?? 'rgb(255, 255, 255)';
const isWhite = utils.isWhiteTheme();
if (isWhite) {
const rgbaMatch = colorCode.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
if (rgbaMatch && colorCode.startsWith('rgba')) {
const r = rgbaMatch[1];
const g = rgbaMatch[2];
const b = rgbaMatch[3];
colorCode = `rgb(${r}, ${g}, ${b})`;
}
}
console.log("保存护眼模式状态", enabled, notNullColor, colorCode);
if (enabled && isWhite) {
$('#eyeProtectionBtn').addClass('active').text('护眼模式:开')
} else {
$('#eyeProtectionBtn').removeClass('active').text('护眼模式:关')
}
GM_setValue('weread_eye_protection', enabled);
GM_setValue('weread_eye_protection_color', notNullColor);
GM_setValue('weread_eye_protection_color_code', colorCode);
},
// 新增:获取护眼模式状态
getEyeProtectionState: function () {
return {
enabled: GM_getValue('weread_eye_protection', false),
color: GM_getValue('weread_eye_protection_color', 'green'),
code: GM_getValue('weread_eye_protection_color_code', EYE_PROTECTION_COLORS['green'].color)
};
},
// 同步控制面板背景色与页面背景色
syncControlPanelBackground: function () {
const state = utils.getEyeProtectionState();
const color = state?.color ?? 'white';
const colorCode = state?.code ?? "rgb(255, 255, 255)";
const isWhite = utils.isWhiteTheme();
const isEnabled = state.enabled;
console.log("护眼模式状态", isEnabled, color, colorCode, isWhite);
// 移除所有护眼模式类名
Object.keys(EYE_PROTECTION_COLORS).forEach(colorKey => {
$('.app_content').removeClass(`eye-protection-${colorKey}`);
$('.readerChapterContent').removeClass(`eye-protection-${colorKey}`);
$('.wr_horizontalReader_app_content').removeClass(`eye-protection-${colorKey}`);
$('.readerChapterContent_container').removeClass(`eye-protection-${colorKey}`);
});
if (isWhite) {
// 清除暗色主题下的内联样式
this.resetControlPanelStyle();
if (isEnabled && color) {
// 应用当前选择的护眼模式样式
const className = EYE_PROTECTION_COLORS[color]?.className;
if (className) {
$('.app_content').addClass(className);
$('.readerChapterContent').addClass(className);
$('.readerChapterContent_container').addClass(className);
$('.wr_horizontalReader_app_content').addClass(className);
}
$('#mainControlPanel').css('background-color', colorCode);
} else {
$('#mainControlPanel').css({
'background-color': 'rgba(255, 255, 255, 1)',
'border-color': '',
'color': '',
});
}
} else {
$('#mainControlPanel').css({
'background-color': 'rgb(32, 32, 32)',
'border-color': '#3e3e3e'
});
// 在暗色主题下,提高控制面板内文字与按钮的对比度
$('#mainControlPanel').find('.control-section-title').css('color', '#e6e6e6');
$('#mainControlPanel').find('.control-btn').css({
'background': '#444',
'color': '#f5f5f5',
'border-color': '#555'
});
}
},
// 恢复控制面板内元素的默认样式(清除可能的暗色主题内联样式)
resetControlPanelStyle: function () {
$('#mainControlPanel').find('.control-section-title').css('color', '');
$('#mainControlPanel').find('.control-btn').css({
'background': '',
'color': '',
'border-color': ''
});
},
handleThemeChange: function () {
const isWhite = utils.isWhiteTheme();
const prevState = utils.getEyeProtectionState();
console.log("handleThemeChange", prevState);//黑->白 false
if (isWhite) {
// 切换回白色主题时,恢复之前的状态
$('#eyeProtectionBtn').removeClass('disabled');
if (prevState.enabled) {
utils.saveEyeProtectionState(true, prevState.color);
// utils.syncControlPanelBackground();
eyeProtection.enable(prevState.color);
}
// utils.syncControlPanelBackground();
console.log('白色主题已启用,护眼模式已恢复到之前状态');
} else {
// 切换到暗色主题时,保存当前状态但暂时禁用
$('#eyeProtectionBtn').addClass('disabled');
// utils.syncControlPanelBackground();
utils.notificationManager.show('插件提示:护眼模式仅在白色主题下可用');
console.log('白色主题已关闭,护眼模式已暂时禁用');
}
utils.syncControlPanelBackground();
},
disableConsoleWithProxy: function () {
// 使用 Proxy 来拦截所有: console 调用
window.console = new Proxy(console, {
get: function (target, prop) {
if (['log', 'warn', 'info', 'debug'].includes(prop)) {
return function () { }; // 返回空函数
}
return target[prop]; // 其他方法保持原样
}
});
}
};
// 控制面板拖拽功能
const panelDrag = {
init: function (panel) {
let isDragging = false;
let startX, startY, initialLeft, initialTop;
// 鼠标按下事件
panel.on('mousedown', function (e) {
// 排除关闭按钮和滑块
if ($(e.target).is('button, input, .color-option, .control-btn') ||
$(e.target).closest('button, input, .color-option, .control-btn').length) {
return;
}
isDragging = true;
panel.addClass('dragging');
startX = e.clientX;
startY = e.clientY;
const rect = panel[0].getBoundingClientRect();
initialLeft = rect.left;
initialTop = rect.top;
e.preventDefault();
});
// 鼠标移动事件
$(document).on('mousemove', function (e) {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
const newLeft = initialLeft + deltaX;
const newTop = initialTop + deltaY;
// 限制在窗口范围内
const maxX = window.innerWidth - panel.outerWidth();
const maxY = window.innerHeight - panel.outerHeight();
panel.css({
left: Math.max(0, Math.min(newLeft, maxX)) + 'px',
top: Math.max(0, Math.min(newTop, maxY)) + 'px',
transform: 'none'
});
});
// 鼠标释放事件
$(document).on('mouseup', function () {
if (isDragging) {
isDragging = false;
panel.removeClass('dragging');
// 保存位置
const position = {
left: parseInt(panel.css('left')),
top: parseInt(panel.css('top'))
};
GM_setValue('control_panel_position', position);
}
});
// 恢复保存的位置
const savedPosition = GM_getValue('control_panel_position');
if (savedPosition) {
panel.css({
left: savedPosition.left + 'px',
top: savedPosition.top + 'px',
transform: 'none'
});
}
}
};
// 图片预览面板功能 - 采用新的固定布局
const imagePreviewPanel = {
selectedImages: new Set(),
isInitialized: false,
init: function () {
if (this.isInitialized) return;
$('body').append(`
<div class="image-preview-overlay" id="imagePreviewOverlay"></div>
<div class="image-preview-panel" id="imagePreviewPanel">
<!-- 顶部固定区域 -->
<div class="image-preview-header">
<div class="image-preview-title">页面图片预览</div>
<span class="image-preview-stats" id="imagePreviewStats">已选择 0 张图片</span>
<button class="image-preview-close" id="closeImagePreview">×</button>
</div>
<!-- 中间可滚动区域 -->
<div class="image-preview-content-container">
<div class="image-preview-controls">
<div class="select-all-container">
<input type="checkbox" class="select-all-checkbox" id="selectAllImages">
<label class="select-all-label" for="selectAllImages">全选</label>
</div>
</div>
<div class="image-preview-content" id="imagePreviewContent">
<!-- 图片网格将通过JavaScript动态加载 -->
</div>
</div>
<!-- 底部固定区域 -->
<div class="image-preview-actions">
<button class="control-btn" id="copySelectedImageUrls">复制选中链接</button>
<button class="control-btn" id="downloadSelectedImages">下载选中图片</button>
<button class="control-btn" id="copyAllImageUrls">复制所有链接</button>
<button class="control-btn" id="downloadAllImages">下载所有图片</button>
</div>
</div>
`);
this.bindEvents();
this.isInitialized = true;
},
bindEvents: function () {
$('#closeImagePreview, #imagePreviewOverlay').click(() => this.hide());
$('#selectAllImages').change((e) => this.toggleSelectAll(e.target.checked));
$('#copySelectedImageUrls').click(() => this.copySelectedImageUrls());
$('#downloadSelectedImages').click(() => this.downloadSelectedImages());
$('#copyAllImageUrls').click(() => this.copyAllImageUrls());
$('#downloadAllImages').click(() => this.downloadAllImages());
// 阻止点击内容区域关闭
$('#imagePreviewPanel').click((e) => e.stopPropagation());
},
show: function () {
this.selectedImages.clear();
this.loadImages();
$('#imagePreviewOverlay, #imagePreviewPanel').show();
this.updateStats();
},
hide: function () {
$('#imagePreviewOverlay, #imagePreviewPanel').hide();
this.selectedImages.clear();
// 清理DOM以释放内存
$('#imagePreviewContent').empty();
},
loadImages: function () {
const content = $('#imagePreviewContent');
content.empty();
const images = $('img.wr_readerImage_opacity');
if (images.length === 0) {
content.html('<div class="image-preview-empty">当前页面没有找到图片</div>');
return;
}
const grid = $('<div class="image-preview-grid" id="imagePreviewGrid"></div>');
content.append(grid);
// 分批加载图片,避免卡顿
this.loadImagesBatch(images, 0, 20, grid);
},
loadImagesBatch: function (images, startIndex, batchSize, grid) {
const endIndex = Math.min(startIndex + batchSize, images.length);
for (let i = startIndex; i < endIndex; i++) {
const img = images[i];
const $img = $(img);
const src = $img.attr('src') || $img.attr('data-src');
if (!src) continue;
const fileName = src.split('/').pop() || `image_${i + 1}.jpg`;
const fileSize = this.getImageSizeText($img);
const item = $(`
<div class="image-preview-item" data-src="${src}" data-index="${i}">
<input type="checkbox" class="image-preview-checkbox" id="img-checkbox-${i}">
<img class="image-preview-thumb" src="${src}" alt="预览图 ${i + 1}" loading="lazy" onerror="this.style.display='none'">
<div class="image-preview-info">
<div><strong>图片 ${i + 1}</strong></div>
<div>${fileName}</div>
<div>${fileSize}</div>
<div class="image-action-buttons">
<button class="image-action-btn copy-btn" data-src="${src}" data-index="${i}">复制链接</button>
<button class="image-action-btn download-btn" data-src="${src}" data-index="${i}">下载图片</button>
</div>
</div>
</div>
`);
const checkbox = item.find('.image-preview-checkbox');
checkbox.change((e) => {
e.stopPropagation();
this.toggleImageSelection(i, src, e.target.checked);
});
// 点击项目也可以切换选择状态
item.click((e) => {
if (e.target.type !== 'checkbox' && !$(e.target).hasClass('image-action-btn')) {
checkbox.prop('checked', !checkbox.prop('checked')).trigger('change');
}
});
// 绑定操作按钮事件
item.find('.copy-btn').click((e) => {
e.stopPropagation();
this.copySingleImageUrl(src, i);
});
item.find('.download-btn').click((e) => {
e.stopPropagation();
this.downloadSingleImage(src, i);
});
grid.append(item);
}
this.updateSelectAllState();
// 如果还有更多图片,继续加载下一批
if (endIndex < images.length) {
setTimeout(() => {
this.loadImagesBatch(images, endIndex, batchSize, grid);
}, 100);
}
},
getImageSizeText: function ($img) {
const width = $img.width();
const height = $img.height();
return width && height ? `${width}×${height}` : '尺寸未知';
},
toggleImageSelection: function (index, src, selected) {
if (selected) {
this.selectedImages.add(index);
$(`#img-checkbox-${index}`).closest('.image-preview-item').addClass('selected');
} else {
this.selectedImages.delete(index);
$(`#img-checkbox-${index}`).closest('.image-preview-item').removeClass('selected');
}
this.updateStats();
this.updateSelectAllState();
},
toggleSelectAll: function (selected) {
const checkboxes = $('.image-preview-checkbox');
checkboxes.prop('checked', selected).trigger('change');
},
updateSelectAllState: function () {
const total = $('.image-preview-checkbox').length;
const selected = this.selectedImages.size;
const selectAll = $('#selectAllImages');
if (selected === 0) {
selectAll.prop('checked', false);
selectAll.prop('indeterminate', false);
} else if (selected === total) {
selectAll.prop('checked', true);
selectAll.prop('indeterminate', false);
} else {
selectAll.prop('checked', false);
selectAll.prop('indeterminate', true);
}
},
updateStats: function () {
const total = $('.image-preview-checkbox').length;
const selected = this.selectedImages.size;
$('#imagePreviewStats').text(`已选择 ${selected} 张图片,共 ${total} 张`);
},
getSelectedImageUrls: function () {
const urls = [];
this.selectedImages.forEach(index => {
const src = $(`#img-checkbox-${index}`).closest('.image-preview-item').data('src');
if (src) urls.push(src);
});
return urls;
},
getAllImageUrls: function () {
const urls = [];
$('.image-preview-item').each((index, item) => {
const src = $(item).data('src');
if (src) urls.push(src);
});
return urls;
},
// 复制选中图片链接
copySelectedImageUrls: function () {
const urls = this.getSelectedImageUrls();
if (urls.length === 0) {
utils.notificationManager.show('请先选择要复制的图片');
return;
}
const text = urls.join('\n');
const copyPromise = new Promise((resolve, reject) => {
try {
const result = GM_setClipboard(text, 'text/plain');
if (result && typeof result.then === 'function') {
result.then(resolve).catch(reject);
} else {
resolve();
}
} catch (error) {
reject(error);
}
});
copyPromise
.then(() => {
utils.notificationManager.show(`已复制 ${urls.length} 个选中图片链接到剪贴板`);
})
.catch(err => {
console.error('复制失败:', err);
this.fallbackCopyText(text);
});
},
// 下载选中图片
downloadSelectedImages: function () {
const urls = this.getSelectedImageUrls();
if (urls.length === 0) {
utils.notificationManager.show('请先选择要下载的图片');
return;
}
const downloadBtn = $('#downloadSelectedImages');
if (downloadBtn.hasClass('loading')) return;
downloadBtn.addClass('loading disabled').text('下载中...');
utils.notificationManager.show(`开始下载 ${urls.length} 张选中图片...`);
imageTools.downloadImagesByUrls(urls, 'selected', () => {
downloadBtn.removeClass('loading disabled').text('下载选中图片');
});
},
// 复制所有图片链接
copyAllImageUrls: function () {
const urls = this.getAllImageUrls();
if (urls.length === 0) {
utils.notificationManager.show('没有找到图片链接');
return;
}
const text = urls.join('\n');
const copyPromise = new Promise((resolve, reject) => {
try {
const result = GM_setClipboard(text, 'text/plain');
if (result && typeof result.then === 'function') {
result.then(resolve).catch(reject);
} else {
resolve();
}
} catch (error) {
reject(error);
}
});
copyPromise
.then(() => {
utils.notificationManager.show(`已复制 ${urls.length} 个图片链接到剪贴板`);
})
.catch(err => {
console.error('复制失败:', err);
this.fallbackCopyText(text);
});
},
// 下载所有图片
downloadAllImages: function () {
const urls = this.getAllImageUrls();
if (urls.length === 0) {
utils.notificationManager.show('当前页面没有找到图片');
return;
}
const downloadBtn = $('#downloadAllImages');
if (downloadBtn.hasClass('loading')) return;
downloadBtn.addClass('loading disabled').text('下载中...');
utils.notificationManager.show(`开始下载 ${urls.length} 张图片...`);
imageTools.downloadImagesByUrls(urls, 'all', () => {
downloadBtn.removeClass('loading disabled').text('下载所有图片');
});
},
// 复制单个图片链接
copySingleImageUrl: function (src, index) {
if (!src) {
utils.notificationManager.show('获取图片链接失败');
return;
}
const copyPromise = new Promise((resolve, reject) => {
try {
const result = GM_setClipboard(src, 'text/plain');
if (result && typeof result.then === 'function') {
result.then(resolve).catch(reject);
} else {
resolve();
}
} catch (error) {
reject(error);
}
});
copyPromise
.then(() => {
utils.notificationManager.show('图片链接已复制到剪贴板');
})
.catch(err => {
console.error('复制失败:', err);
this.fallbackCopyText(src);
});
},
// 下载单个图片
downloadSingleImage: function (src, index) {
if (!src) {
utils.notificationManager.show('获取图片链接失败');
return;
}
const downloadBtn = $(`.image-action-btn.download-btn[data-index="${index}"]`);
if (downloadBtn.hasClass('loading')) return;
downloadBtn.addClass('loading disabled').text('下载中...');
utils.notificationManager.show('开始下载单个图片...');
imageTools.downloadSingleImageByUrl(src, index, () => {
downloadBtn.removeClass('loading disabled').text('下载图片');
});
},
// 备用复制方法
fallbackCopyText: function (text) {
try {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
utils.notificationManager.show('已复制到剪贴板');
}).catch(() => {
this.fallbackCopyText2(text);
});
} else {
this.fallbackCopyText2(text);
}
} catch (error) {
console.error('备用复制方法1失败:', error);
this.fallbackCopyText2(text);
}
},
// 备用复制方法2
fallbackCopyText2: function (text) {
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
utils.notificationManager.show('已复制到剪贴板');
} else {
utils.notificationManager.show('复制失败,请手动复制');
}
} catch (error) {
console.error('备用复制方法2失败:', error);
utils.notificationManager.show('复制失败,请手动复制');
}
}
};
// 图片工具功能
const imageTools = {
init: function () {
this.observeImages();
},
// 观察图片变化
observeImages: function () {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === 1) {
this.processImageNode(node);
}
});
}
});
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// 初始处理已存在的图片
setTimeout(() => {
$('img.wr_readerImage_opacity').each((i, img) => this.addImageToolbar(img));
}, 1000);
},
// 处理图片节点
processImageNode: function (node) {
// 处理直接添加的图片
if (node.tagName === 'IMG' && node.classList.contains('wr_readerImage_opacity')) {
this.addImageToolbar(node);
}
// 处理子元素中的图片
$(node).find('img.wr_readerImage_opacity').each((i, img) => {
this.addImageToolbar(img);
});
},
// 为图片添加工具栏
addImageToolbar: function (img) {
const $img = $(img);
const src = $img.attr('src') || $img.attr('data-src');
if (!src || $img.data('toolbar-added')) return;
$img.data('toolbar-added', true);
// 创建工具栏容器
const toolbarContainer = $(`
<div class="image-toolbar-container">
<div class="image-toolbar">
<button class="image-tool-btn download-btn" title="下载图片">
<span class="image-tool-icon">⬇️</span>
</button>
<button class="image-tool-btn copy-btn" title="复制链接">
<span class="image-tool-icon">📋</span>
</button>
<button class="image-tool-btn open-btn" title="新标签页打开">
<span class="image-tool-icon">🔗</span>
</button>
</div>
</div>
`);
// 检测是双栏还是单栏模式
const isDoubleColumn = $img.closest('.passageContent_wrapper').length > 0;
const isSingleColumn = $img.closest('.passage-content').length > 0;
let parentContainer;
// 双栏模式:添加到passageContent_wrapper
if (isDoubleColumn) {
parentContainer = $img.closest('.passageContent_wrapper');
parentContainer.append(toolbarContainer);
}
// 单栏模式:添加到passage-content
else if (isSingleColumn) {
parentContainer = $img.closest('.passage-content');
// 确保passage-content有相对定位
parentContainer.css('position', 'relative');
// 在单栏模式下,需要复制图片的定位信息到工具栏
const imgRect = img.getBoundingClientRect();
// 计算相对于父容器的位置
const relativeLeft = imgRect.width
// 设置工具栏容器的位置和图片一致
toolbarContainer.css({
position: 'absolute',
left: relativeLeft + 'px',
display: 'flex',
transform: $img.css('transform')
});
parentContainer.append(toolbarContainer);
}
// 其他情况:直接添加到图片后面
else {
$img.after(toolbarContainer);
}
// 绑定事件
this.bindToolbarEvents(toolbarContainer, src);
},
// 绑定工具栏事件
bindToolbarEvents: function (toolbarContainer, src) {
const downloadBtn = toolbarContainer.find('.download-btn');
const copyBtn = toolbarContainer.find('.copy-btn');
const openBtn = toolbarContainer.find('.open-btn');
// 下载事件
downloadBtn.click(() => {
if (downloadBtn.hasClass('disabled') || downloadBtn.hasClass('loading')) {
return;
}
// 设置加载状态
downloadBtn.addClass('loading disabled')
.attr('title', '下载中...')
.find('.image-tool-icon').text('⏳');
this.downloadImage(src, () => {
// 恢复按钮状态
setTimeout(() => {
downloadBtn.removeClass('loading disabled')
.attr('title', '下载图片')
.find('.image-tool-icon').text('⬇️');
}, 1000);
});
});
// 复制事件
copyBtn.click(() => {
this.copyImageUrl(src);
});
// 打开事件
openBtn.click(() => {
this.openImage(src);
});
// 鼠标悬停显示工具栏
const $img = toolbarContainer.prev('img.wr_readerImage_opacity');
if ($img.length) {
$img.hover(
() => toolbarContainer.show(),
() => setTimeout(() => !toolbarContainer.is(':hover') && toolbarContainer.hide(), 100)
);
}
// 工具栏自身悬停
toolbarContainer.hover(
() => toolbarContainer.show(),
() => toolbarContainer.hide()
);
},
// 下载图片
downloadImage: function (src, callback) {
if (!src) {
callback && callback();
return;
}
const fileName = src.split('/').pop() || 'image.jpg';
try {
GM_download({
url: src,
name: fileName,
onload: () => {
utils.notificationManager.show('图片下载成功');
callback && callback();
},
onerror: (e) => {
utils.notificationManager.show('图片下载失败: ' + e.error);
callback && callback();
}
});
} catch (error) {
this.downloadImageFallback(src, fileName);
callback && callback();
}
},
// 备用下载方法
downloadImageFallback: function (src, fileName) {
const link = document.createElement('a');
link.href = src;
link.download = fileName;
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
utils.notificationManager.show('图片下载成功');
},
// 在新标签页打开图片
openImage: (src) => src && window.open(src, '_blank'),
// 复制图片链接
copyImageUrl: function (src) {
if (!src) return;
const copyPromise = new Promise((resolve, reject) => {
try {
const result = GM_setClipboard(src, 'text/plain');
if (result && typeof result.then === 'function') {
result.then(resolve).catch(reject);
} else {
resolve();
}
} catch (error) {
reject(error);
}
});
copyPromise
.then(() => utils.notificationManager.show('图片链接已复制到剪贴板'))
.catch(err => {
console.error('复制失败:', err);
this.fallbackCopyText(src);
});
},
// 备用复制方法
fallbackCopyText: function (text) {
try {
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).then(() => {
utils.notificationManager.show('图片链接已复制到剪贴板 (现代API)');
}).catch(() => {
this.fallbackCopyText2(text);
});
} else {
this.fallbackCopyText2(text);
}
} catch (error) {
console.error('备用复制方法1失败:', error);
this.fallbackCopyText2(text);
}
},
// 备用复制方法2
fallbackCopyText2: function (text) {
try {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.left = '-999999px';
textArea.style.top = '-999999px';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
const successful = document.execCommand('copy');
document.body.removeChild(textArea);
if (successful) {
utils.notificationManager.show('图片链接已复制到剪贴板 (传统方法)');
} else {
utils.notificationManager.show('复制失败,请手动复制链接');
}
} catch (error) {
console.error('备用复制方法2失败:', error);
utils.notificationManager.show('复制失败,请手动复制链接');
}
},
// 下载页面所有图片
downloadAllImages: function () {
const downloadBtn = $('#downloadAllImagesFromPanel');
// 防止重复点击
if (downloadBtn.hasClass('loading')) {
return;
}
const images = $('img.wr_readerImage_opacity');
if (images.length === 0) {
utils.notificationManager.show('当前页面没有找到图片');
return;
}
// 设置加载状态
downloadBtn.addClass('loading disabled').text('下载中...');
utils.notificationManager.show(`开始下载 ${images.length} 张图片...`);
this.downloadImagesBatch(images, () => {
// 恢复按钮状态
setTimeout(() => {
downloadBtn.removeClass('loading disabled').text('下载所有图片');
}, 1000);
});
},
// 通过URL列表下载图片
downloadImagesByUrls: function (urls, type = 'all', callback) {
if (urls.length === 0) return;
let downloaded = 0;
const total = urls.length;
let hasError = false;
urls.forEach((src, index) => {
setTimeout(() => {
this.downloadSingleImageByUrl(src, index, (success) => {
if (!success) {
hasError = true;
}
downloaded++;
if (downloaded === total) {
// 恢复按钮状态
if (callback) callback();
if (hasError) {
utils.notificationManager.show(`图片下载完成,部分图片下载失败 (${downloaded}/${total})`);
} else {
utils.notificationManager.show(`所有图片下载完成 (${downloaded}/${total})`);
}
}
});
}, index * 1000); // 间隔1秒下载,避免同时下载太多
});
},
// 下载单张图片通过URL
downloadSingleImageByUrl: function (src, index, callback) {
const fileName = src.split('/').pop() || `image_${index + 1}.jpg`;
try {
GM_download({
url: src,
name: fileName,
onload: () => callback && callback(true),
onerror: (e) => {
console.error('下载失败:', src, e);
callback && callback(false);
}
});
} catch (error) {
this.downloadImageFallback(src, fileName);
callback && callback(true);
}
},
// 批量下载图片
downloadImagesBatch: function (images, callback) {
let downloaded = 0;
const total = images.length;
let hasError = false;
images.each((i, img) => {
const src = $(img).attr('src') || $(img).attr('data-src');
if (src) {
setTimeout(() => {
this.downloadSingleImageByUrl(src, i, (success) => {
if (!success) {
hasError = true;
}
downloaded++;
if (downloaded === total) {
if (hasError) {
utils.notificationManager.show(`图片下载完成,部分图片下载失败 (${downloaded}/${total})`);
} else {
utils.notificationManager.show(`所有图片下载完成 (${downloaded}/${total})`);
}
callback && callback();
}
});
}, i * 1000); // 间隔1秒下载,避免同时下载太多
} else {
downloaded++;
if (downloaded === total) {
callback && callback();
}
}
});
}
};
// 宽度控制功能
const widthControl = {
init: function () {
const savedWidth = GM_getValue('weread_max_width', DEFAULT_WIDTH);
this.applyWidth(savedWidth);
return savedWidth;
},
applyWidth: function (width) {
const content = document.querySelector(".readerContent .app_content");
const topBar = document.querySelector('.readerTopBar');
if (content && topBar) {
content.style.maxWidth = width + 'px';
topBar.style.maxWidth = width + 'px';
GM_setValue('weread_max_width', width);
if ($('#widthSlider').length) {
$('#widthSlider').val(width);
$('#widthValue').text(width + 'px');
}
window.dispatchEvent(new Event('resize'));
}
},
reset: function () {
this.applyWidth(DEFAULT_WIDTH);
}
};
// 护眼模式功能
const eyeProtection = {
init: function () {
const enabled = utils.getEyeProtectionState().enabled;
const color = utils.getEyeProtectionState().color;
if (enabled) {
this.enable(color);
} else {
this.disable();
}
return enabled;
},
enable: function (color) {
console.log('改变enable', color);
// 保存状态到内存
utils.saveEyeProtectionState(true, color);
// 同步控制面板背景
utils.syncControlPanelBackground();
},
disable: function () {
// 移除所有护眼模式类名
Object.keys(EYE_PROTECTION_COLORS).forEach(colorKey => {
document.body.classList.remove(EYE_PROTECTION_COLORS[colorKey].className);
});
// 更新按钮状态
utils.saveEyeProtectionState(false, utils.getEyeProtectionState().color);
// 同步控制面板背景
utils.syncControlPanelBackground();
},
// 等待
changeColor: function (color) {
console.log('改变changeColor', color);
const enabled = utils.getEyeProtectionState().enabled;
utils.saveEyeProtectionState(enabled, color);
utils.syncControlPanelBackground();
},
// 修改:强制恢复护眼模式状态(用于页面刷新或布局切换后)
restoreState: function () {
const state = utils.getEyeProtectionState();
if (state.enabled) {
setTimeout(() => {
this.enable(state.color, true);
// const colorOptionContainers = document.querySelectorAll('.color-option-container');
// colorOptionContainers.forEach(colorOptionContainer => {
// const colorOptions = colorOptionContainer.querySelectorAll('.color-option');
// colorOptions.forEach(colorOption => {
// const colorKey = colorOption.getAttribute('data-color');
// if (colorKey === utils.getEyeProtectionState().color) {
// colorOption.classList.add('active');
// }
// });
// });
// // 遍历.color-option-container下的含有color-${colorKey}类名的元素,如果colorKey === utils.getEyeProtectionState().color,然后添加.active类名
// 获取所有颜色选项容器
const colorContainers = document.querySelectorAll('.color-option-container');
// 获取当前眼保护状态的颜色
const currentColor = utils.getEyeProtectionState().color;
// 遍历所有颜色选项容器
colorContainers.forEach(container => {
// 获取颜色选项元素
const colorOption = container.querySelector('.color-option');
// 获取该选项对应的colorKey(从data-color属性)
const colorKey = container.getAttribute('data-color');
// 如果colorKey与当前颜色匹配,则添加active类,否则移除
if (colorKey === currentColor) {
colorOption.classList.add('active');
} else {
colorOption.classList.remove('active');
}
});
console.log('护眼模式状态已恢复:', state.color);
}, 50);
}
},
syncButtonState: function () {
const state = utils.getEyeProtectionState();
if (state.enabled) {
$('#eyeProtectionBtn').removeClass('disabled').addClass('active').text('护眼模式:开');
} else {
$('#eyeProtectionBtn').removeClass('disabled active').text('护眼模式:关');
}
},
};
// 自动翻页功能
const autoPageTurn = {
trigger: function () {
if (isPageTurning || pageTurnCooldown) return;
isPageTurning = true;
pageTurnCooldown = true;
// 触发键盘右键事件
['keydown', 'keyup'].forEach(eventType =>
document.dispatchEvent(new KeyboardEvent(eventType, {
bubbles: true, cancelable: true, key: 'ArrowRight', code: 'ArrowRight', keyCode: 39
}))
);
setTimeout(() => pageTurnCooldown = false, 2000);
setTimeout(() => (isPageTurning = false, window.scrollTo(0, 100)), 1500);
}
};
// 自动翻页进度条
const progressBar = {
init: function () {
$('body').append(`
<div id="auto-turn-progress">
<div class="progress-text">0秒后自动翻页</div>
<div class="progress-bar"><div class="progress-fill"></div></div>
</div>
`);
},
show: function (waitTime) {
this.waitTime = waitTime;
this.startTime = Date.now();
$('#auto-turn-progress').show();
this.update();
progressInterval = setInterval(() => this.update(), 100);
},
update: function () {
const elapsed = (Date.now() - this.startTime) / 1000;
const remaining = Math.max(0, this.waitTime - elapsed);
const percentage = (remaining / this.waitTime) * 100;
$('.progress-text').text(`${remaining.toFixed(1)}秒后自动翻页`);
$('.progress-fill').css('width', percentage + '%');
remaining <= 0 && this.hide();
},
hide: function () {
$('#auto-turn-progress').hide();
progressInterval && (clearInterval(progressInterval), progressInterval = null);
}
};
// 自动阅读功能
const autoRead = {
calculateWaitTime: () => currentScrollSpeed <= 0.5 ? 10 :
currentScrollSpeed <= 1 ? 8 :
currentScrollSpeed <= 2 ? 6 :
currentScrollSpeed <= 3 ? 4 : 2,
start: function () {
scrollInterval && (clearInterval(scrollInterval), scrollInterval = null);
this.clearBottomTimer();
const timerMinutes = parseInt($('#timerSlider').val());
timerMinutes > 0 && (lastTimerValue = timerMinutes, GM_setValue('weread_last_timer', lastTimerValue), this.updateLastTimerButton());
const baseSpeed = 1;
const speedMultiplier = currentScrollSpeed;
let lastScrollTop = document.documentElement.scrollTop || document.body.scrollTop;
let stuckCount = 0;
scrollInterval = setInterval(() => {
if (isPageTurning) return;
const currentScrollTop = document.documentElement.scrollTop || document.body.scrollTop;
const scrollHeight = document.documentElement.scrollHeight || document.body.scrollHeight;
const clientHeight = document.documentElement.clientHeight || document.body.clientHeight;
if (currentScrollTop + clientHeight >= scrollHeight - 10) {
!isWaitingForPageTurn && this.schedulePageTurn();
return;
}
currentScrollTop === lastScrollTop ?
(stuckCount++, window.scrollBy(0, baseSpeed * speedMultiplier * (stuckCount > 5 ? 3 : 1))) :
(stuckCount = 0, window.scrollBy(0, baseSpeed * speedMultiplier));
lastScrollTop = lastScrollPosition = currentScrollTop;
}, 20);
isAutoReading = true;
this.updateButton();
this.startTimer();
this.saveState();
},
stop: function () {
scrollInterval && (clearInterval(scrollInterval), scrollInterval = null);
isAutoReading = isPageTurning = isWaitingForPageTurn = false;
this.updateButton();
this.clearBottomTimer();
progressBar.hide();
this.stopTimer();
this.saveState();
},
toggle: function () {
isAutoReading ? this.stop() : this.start();
},
schedulePageTurn: function () {
isWaitingForPageTurn = true;
const waitTime = this.calculateWaitTime();
progressBar.show(waitTime);
bottomReachedTimer = setTimeout(() => {
isWaitingForPageTurn && (autoPageTurn.trigger(), isWaitingForPageTurn = false, progressBar.hide());
setTimeout(() => isAutoReading && (lastScrollPosition = 0), 2000);
}, waitTime * 1000);
},
clearBottomTimer: function () {
bottomReachedTimer && (clearTimeout(bottomReachedTimer), bottomReachedTimer = null);
isWaitingForPageTurn = false;
progressBar.hide();
},
checkManualPageTurn: function () {
if (!isWaitingForPageTurn) return;
const currentScrollTop = document.documentElement.scrollTop || document.body.scrollTop;
Math.abs(currentScrollTop - lastScrollPosition) > 50 &&
(this.clearBottomTimer(), this.schedulePageTurn());
lastScrollPosition = currentScrollTop;
},
startTimer: function () {
const timerMinutes = parseInt($('#timerSlider').val());
if (timerMinutes > 0) {
remainingTime <= 0 && (remainingTime = timerMinutes * 60);
this.updateTimerDisplay();
timerInterval = setInterval(() => {
remainingTime--;
this.updateTimerDisplay();
GM_setValue('weread_remaining_time', remainingTime);
remainingTime <= 0 && (this.stop(), utils.notificationManager.show('定时时间到,自动阅读已停止'));
}, 1000);
}
},
stopTimer: function () {
timerInterval && (clearInterval(timerInterval), timerInterval = null);
remainingTime = 0;
GM_setValue('weread_remaining_time', 0);
this.updateTimerDisplay();
},
updateTimerDisplay: function () {
$('#timerDisplay').text(remainingTime > 0 ?
`剩余: ${Math.floor(remainingTime / 60)}:${(remainingTime % 60).toString().padStart(2, '0')}` : '');
},
updateButton: function () {
const button = $('#toggleAutoRead');
button.text(isAutoReading ? '停止阅读' : '开始阅读');
isAutoReading ? button.addClass('active') : button.removeClass('active');
},
updateLastTimerButton: function () {
const lastTimerBtn = $('#lastTimerBtn');
lastTimerValue > 0 ?
lastTimerBtn.removeClass('disabled').css('background', '#e0e0e0') :
lastTimerBtn.addClass('disabled').css('background', '#cccccc');
},
applyLastTimer: function () {
lastTimerValue > 0 ?
($('#timerSlider').val(lastTimerValue), $('#timerValue').text(lastTimerValue + '分钟'),
utils.notificationManager.show(`已设置为上次定时时间: ${lastTimerValue}分钟`)) :
utils.notificationManager.show('没有找到上次定时时间');
},
saveState: function () {
GM_setValue('weread_auto_reading', isAutoReading);
GM_setValue('weread_scroll_speed', currentScrollSpeed);
},
restoreState: function () {
if (isAutoReading) {
$('#speedSlider').val(currentScrollSpeed);
$('#speedValue').text(currentScrollSpeed.toFixed(1) + 'x');
const timerMinutes = Math.ceil(remainingTime / 60);
timerMinutes > 0 && ($('#timerSlider').val(timerMinutes), $('#timerValue').text(timerMinutes + '分钟'));
this.updateButton();
this.start();
utils.notificationManager.show('已恢复自动阅读状态');
}
}
};
// 控制面板功能
const controlPanel = {
init: function () {
const savedWidth = GM_getValue('weread_max_width', DEFAULT_WIDTH);
$("body").append(`
<div class="control-panel" style="display: none;" id="mainControlPanel">
<button class="control-panel-close" id="closeControlPanel">×</button>
<div class="control-section">
<div class="control-section-title">宽度控制</div>
<div class="control-item">
<span class="control-label">页面宽度</span>
<input type="range" class="control-slider" id="widthSlider" min="600" max="1400" value="${savedWidth}">
<span class="control-value" id="widthValue">${savedWidth}px</span>
</div>
<div class="control-buttons">
<button class="control-btn reset" id="resetWidth">恢复默认</button>
</div>
</div>
<div class="section-divider"></div>
<div class="control-section">
<div class="control-section-title">自动阅读</div>
<div class="control-item">
<span class="control-label">阅读速度</span>
<input type="range" class="control-slider speed-slider" id="speedSlider" min="0.5" max="4" step="0.1" value="${currentScrollSpeed}">
<span class="control-value" id="speedValue">${currentScrollSpeed.toFixed(1)}x</span>
</div>
<div class="control-item">
<span class="control-label">定时关闭</span>
<input type="range" class="control-slider timer-slider" id="timerSlider" min="0" max="120" step="1" value="0">
<span class="control-value" id="timerValue">0分钟</span>
</div>
<div class="timer-display" id="timerDisplay"></div>
<div class="control-buttons">
<button class="control-btn" id="toggleAutoRead">${isAutoReading ? '停止阅读' : '开始阅读'}</button>
<button class="control-btn secondary" id="lastTimerBtn">上次定时</button>
</div>
</div>
<div class="section-divider"></div>
<div class="control-section">
<div class="control-section-title">显示设置</div>
<div class="color-options" id="colorOptionsContainer"></div>
<div class="control-buttons">
<button class="control-btn" id="eyeProtectionBtn">护眼模式:关</button>
</div>
</div>
<div class="section-divider"></div>
<div class="control-section">
<div class="control-section-title">图片工具</div>
<div class="control-buttons">
<button class="control-btn" id="previewAllImages">预览页面图片</button>
<button class="control-btn" id="downloadAllImagesFromPanel" data-original-text="下载所有图片">下载所有图片</button>
</div>
</div>
</div>
`);
this.generateColorOptions();
this.addControlButton();
this.bindEvents();
// 初始化拖拽功能
panelDrag.init($('#mainControlPanel'));
},
generateColorOptions: function () {
const container = $('#colorOptionsContainer');
container.empty();
// 添加默认状态和颜色选项
utils.saveEyeProtectionState(false, 'green');
Object.keys(EYE_PROTECTION_COLORS).forEach(colorKey => {
const colorInfo = EYE_PROTECTION_COLORS[colorKey];
const isActive = colorKey === utils.getEyeProtectionState().color;
const colorOption = $(`
<div class="color-option-container" data-color="${colorKey}">
<div class="color-option color-${colorKey} ${isActive ? 'active' : ''}"></div>
<div class="color-name">${colorInfo.name}</div>
</div>
`);
container.append(colorOption);
});
},
addControlButton: function () {
$('.readerControls').append(`
<div class="wr_tooltip_container" style="--offset: 6px;">
<button class="readerControls_item" id="mainControl" style="color:#6a6c6c;cursor:pointer;">
<span class="settings-icon"></span>
</button>
<div class="wr_tooltip_item wr_tooltip_item--right" style="display: none;">设置</div>
</div>
`);
},
bindEvents: function () {
// const isWhite = utils.isWhiteTheme();// 失效
// 控制面板显示/隐藏
$('#mainControl').click(() => $('#mainControlPanel').toggle());
// 工具提示
$('#mainControl').hover(
function () { $(this).siblings('.wr_tooltip_item').show(); },
function () { $(this).siblings('.wr_tooltip_item').hide(); }
);
// 关闭按钮
$(document).on('click', '#closeControlPanel', (e) => (e.stopPropagation(), $('#mainControlPanel').hide()));
// 宽度控制
$('#widthSlider').on('input', function () {
const newWidth = parseInt($(this).val());
$('#widthValue').text(newWidth + 'px');
widthControl.applyWidth(newWidth);
});
$('#resetWidth').click(() => {
$('#widthSlider').val(DEFAULT_WIDTH);
$('#widthValue').text(DEFAULT_WIDTH + 'px');
widthControl.reset();
});
// 颜色选择
$(document).on('click', '.color-option-container', function () {
const color = $(this).data('color');
$('.color-option').removeClass('active');
$(this).find('.color-option').addClass('active');
eyeProtection.changeColor(color);
});
// 护眼模式切换
$(document).on('click', '#eyeProtectionBtn', (state, c, a) => {
const isWhite = utils.isWhiteTheme();
const isEnabled = utils.getEyeProtectionState().enabled;
if (isWhite) {
if (isEnabled) {
eyeProtection.disable();
} else {
eyeProtection.enable(utils.getEyeProtectionState().color);
}
} else {
utils.notificationManager.show('护眼模式仅在白色主题下可用');
}
});
// 自动阅读控制
$('#speedSlider').on('input', function () {
currentScrollSpeed = parseFloat($(this).val());
$('#speedValue').text(currentScrollSpeed.toFixed(1) + 'x');
GM_setValue('weread_scroll_speed', currentScrollSpeed);
isAutoReading && (autoRead.stop(), autoRead.start());
});
$('#timerSlider').on('input', function () {
$('#timerValue').text($(this).val() + '分钟');
});
$('#lastTimerBtn').click(() => autoRead.applyLastTimer());
$('#toggleAutoRead').click(() => autoRead.toggle());
// 图片工具
$('#previewAllImages').click(() => imagePreviewPanel.show());
$('#downloadAllImagesFromPanel').click(() => imageTools.downloadAllImages());
// 点击页面其他地方隐藏控制面板
$(document).on('click', (e) => {
!$(e.target).closest('.control-panel, #mainControl, #closeControlPanel').length &&
$('.control-panel').hide();
});
},
};
// 头部隐藏功能
const headerControl = {
init: function () {
$(window).scroll(function () {
const scrollS = $(this).scrollTop();
const selBtn = document.querySelector('.readerTopBar');
$('.readerControls').hover(
() => $('.readerControls').css('opacity', '1'),
() => $('.readerControls').css('opacity', '0')
);
selBtn.style.opacity = scrollS >= windowTop ? 0 : 1;
windowTop = scrollS;
isAutoReading && autoRead.checkManualPageTurn();
});
}
};
// 初始化函数
function initialize () {
// // 强制恢复护眼模式状态
const state = utils.getEyeProtectionState();
if (state.enabled && utils.isWhiteTheme()) {
eyeProtection.restoreState();
}
// 如果自动阅读状态为开启,恢复自动阅读
if (isAutoReading) {
setTimeout(() => {
autoRead.restoreState();
}, 1000);
}
console.log("1111111111111111111111初始化函数");
console.log(
'重新加载',
utils.isWhiteTheme(),
utils.getEyeProtectionState().enabled
);
progressBar.init();
imageTools.init();
imagePreviewPanel.init();
autoRead.updateLastTimerButton();
controlPanel.init();
headerControl.init();
// 初始化各模块
const currentWidth = widthControl.init();
$('#widthSlider').val(currentWidth);
$('#widthValue').text(currentWidth + 'px');
eyeProtection.syncButtonState();
utils.syncControlPanelBackground();
// 创建一个 MutationObserver 实例
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
utils.handleThemeChange();
}
});
});
// 开始观察body元素的属性变化
observer.observe(document.body, {
attributes: true, // 监听属性变化
attributeFilter: ['class'] // 只监听class属性
});
// 禁用控制台
// utils.disableConsoleWithProxy();
}
// 页面加载完成后初始化
$(window).on('load', initialize);
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址