// ==UserScript==
// @name 📖小窗净读器Plus(通用适配/目录分页/多页拼合/可拖可调/记忆进度/全局替换/黑暗模式/字体大小/行高设置/可扩展)
// @namespace https://github.com/jx-j-x/Greasemonkey-script
// @version 0.10.1
// @description Ctrl+Alt+L 输入链接→抽正文;Alt+T 目录(每页50条,可跳页);Ctrl+Alt+R 续读上次;←/→ 翻页/跳章;↑/↓ 平滑滚动;Ctrl+Alt+X 显示/隐藏;Alt+Q打开文本替换;Alt+1刷新当前章节。多站点适配可扩展,跨域抓取(含 GBK/Big5),小窗可拖动/调整大小,自动记忆目录与最后章节链接。
// @match *://*/*
// @grant GM_xmlhttpRequest
// @grant GM_addStyle
// @connect *
// @connect 3wwd.com
// @connect m.3wwd.com
// @connect biquge.tw
// @connect www.biquge.tw
// @connect m.biquge.tw
// @run-at document-idle
// ==/UserScript==
(function () {
'use strict';
// ========== 样式 ==========
GM_addStyle(`
#cr-panel{
position: fixed; left: 16px; bottom: 16px; width: 300px; height: 300px;
background: #fff; color:#222; border:1px solid #ddd; border-radius:10px;
box-shadow:0 6px 24px rgba(0,0,0,.15); z-index: 2147483646;
display:none; overflow:hidden; font:14px/1.7 -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,"Noto Sans","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
}
#cr-drag{ position:absolute; top:0; left:0; right:0; height:10px; cursor:move; z-index:3; }
#cr-resize{
position:absolute; right:2px; bottom:2px; width:14px; height:14px; cursor:nwse-resize; z-index:3; opacity:.7;
background:linear-gradient(135deg, rgba(0,0,0,0) 0, rgba(0,0,0,0) 50%, #cfcfcf 50%, #cfcfcf 100%);
border-radius:3px;
}
#cr-content{ height:100%; overflow:auto; padding:12px 14px; position:relative; z-index:1; }
#cr-content p{ margin:0 0 6px 0; }
/* URL 输入弹窗 & 目录弹窗 */
#cr-modal, #cr-toc{
position: fixed; inset: 0; background: rgba(0,0,0,.35); display:none;
align-items: center; justify-content: center; z-index: 2147483647;
}
#cr-modal .cr-box, #cr-toc .cr-box{
width: min(800px, 96vw); background:#fff; border-radius:12px;
box-shadow: 0 10px 30px rgba(0,0,0,.25); padding: 16px;
}
#cr-modal h3, #cr-toc h3{ margin:0 0 10px 0; font-size:16px; }
#cr-url{
width:100%; box-sizing:border-box; padding:10px 12px; font-size:14px;
border:1px solid #ddd; border-radius:8px; outline:none;
}
#cr-modal .ops{ margin-top:12px; display:flex; gap:8px; justify-content:flex-end; }
#cr-modal button{ border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 12px; cursor:pointer; }
#cr-modal button.primary{ background:#111; color:#fff; border-color:#111; }
/* 目录弹窗 */
#cr-toc .cr-box{ padding:12px 12px 8px;}
.cr-toc-head{ display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; }
.cr-toc-head .title{ font-weight:600; }
.cr-toc-head .close{ border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 10px; cursor:pointer; }
.toc-list{
max-height: 65vh; overflow:auto; border:1px solid #eee; border-radius:8px;
padding: 6px;
}
.toc-item{
padding:6px 8px; border-radius:6px; cursor:pointer; user-select:none;
display:flex; gap:10px; align-items:flex-start;
}
.toc-item:hover{ background:#f6f6f6; }
.toc-item.active{ background:#111; color:#fff; }
.toc-idx{ min-width: 56px; opacity:.7; font-variant-numeric: tabular-nums; }
.toc-title{ flex:1; word-break: break-all; }
.cr-toc-foot{
display:flex; align-items:center; justify-content:space-between;
margin-top:8px; gap:12px; flex-wrap:wrap;
}
.range{ font-size:12px; color:#666; }
.pager{ display:flex; align-items:center; gap:8px; }
.pager button{
border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 10px; cursor:pointer;
}
.pager input{
width:90px; padding:6px 8px; border:1px solid #ddd; border-radius:8px; outline:none; font-size:14px;
}
//以下均为AI增加
/* 黑暗模式样式 */
#cr-panel.cr-theme-dark {
background: #1a1a1a !important;
color: #e6e6e6 !important;
border-color: #444 !important;
box-shadow: 0 6px 24px rgba(0,0,0,0.4);
}
#cr-panel.cr-theme-dark #cr-content {
color: #e6e6e6;
}
#cr-panel.cr-theme-dark #cr-content a {
color: #88c0ff;
}
/* 黑暗模式滚动条样式 */
#cr-panel.cr-theme-dark #cr-content::-webkit-scrollbar {
width: 8px;
}
#cr-panel.cr-theme-dark #cr-content::-webkit-scrollbar-track {
background: #2a2a2a;
border-radius: 4px;
}
#cr-panel.cr-theme-dark #cr-content::-webkit-scrollbar-thumb {
background: #555;
border-radius: 4px;
}
#cr-panel.cr-theme-dark #cr-content::-webkit-scrollbar-thumb:hover {
background: #666;
}
/* Firefox 滚动条样式 */
#cr-panel.cr-theme-dark #cr-content {
scrollbar-width: thin;
scrollbar-color: #555 #2a2a2a;
}
/* 控制按钮样式 */
.cr-theme-controls {
position: absolute;
top: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 1000;
}
.cr-theme-btn {
width: 28px;
height: 28px;
border-radius: 4px;
border: 1px solid #ddd;
background: #fff;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
#cr-panel.cr-theme-dark .cr-theme-btn {
background: #000;
border-color: #555;
color: #000;
}
/* 文本替换弹窗样式 ,AI增加*/
#cr-replace-modal {
position: fixed; inset: 0; background: rgba(0,0,0,.35); display:none;
align-items: center; justify-content: center; z-index: 2147483647; /* 和其他弹窗同层级 */
}
#cr-replace-modal .cr-box {
width: min(600px, 96vw); background:#fff; border-radius:12px;
box-shadow: 0 10px 30px rgba(0,0,0,.25); padding: 16px;
}
#cr-replace-modal h3 { margin:0 0 12px 0; font-size:16px; }
#cr-replace-inputs { display:flex; flex-direction:column; gap:8px; margin-bottom:12px; }
#cr-replace-from, #cr-replace-to {
width:100%; box-sizing:border-box; padding:10px 12px; font-size:14px;
border:1px solid #ddd; border-radius:8px; outline:none;
}
#cr-replace-modal .ops { margin-top:8px; display:flex; gap:8px; justify-content:flex-end; }
#cr-replace-modal button { border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 12px; cursor:pointer; }
#cr-replace-modal button.primary { background:#111; color:#fff; border-color:#111; }
/* 黑暗模式下弹窗适配 */
#cr-panel.cr-theme-dark #cr-replace-modal .cr-box { background:#1a1a1a; color:#e6e6e6; }
#cr-panel.cr-theme-dark #cr-replace-from,
#cr-panel.cr-theme-dark #cr-replace-to { background:#2a2a2a; border-color:#444; color:#e6e6e6; }
#cr-panel.cr-theme-dark #cr-replace-modal button { background:#2a2a2a; border-color:#444; color:#e6e6e6; }
#cr-panel.cr-theme-dark #cr-replace-modal button.primary { background:#333; color:#e6e6e6; }
/* 新增:替换规则列表样式 */
#cr-replace-rules {
max-height: 180px; overflow-y: auto; margin: 10px 0;
border: 1px solid #eee; border-radius: 8px; padding: 8px;
}
.cr-replace-rule-item {
display: flex; align-items: center; gap: 8px; padding: 6px;
border-radius: 4px; margin-bottom: 4px; background: #f9f9f9;
}
.cr-replace-rule-text {
flex: 1; font-size: 13px; color: #333;
}
.cr-replace-rule-del {
border: none; background: #ff4444; color: #fff;
border-radius: 4px; padding: 2px 6px; cursor: pointer;
font-size: 12px;
}
/* 黑暗模式适配规则列表 */
#cr-panel.cr-theme-dark #cr-replace-rules {
border-color: #444;
}
#cr-panel.cr-theme-dark .cr-replace-rule-item {
background: #2a2a2a;
}
#cr-panel.cr-theme-dark .cr-replace-rule-text {
color: #e6e6e6;
}
/* 右键刷新菜单样式 */
#cr-refresh-menu {
position: fixed; background: #fff; border: 1px solid #ddd;
border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,.15);
padding: 4px 0; z-index: 99999; display: none;
}
#cr-refresh-menu .menu-item {
padding: 6px 16px; font-size: 14px; cursor: pointer;
white-space: nowrap; color: #333;
}
#cr-refresh-menu .menu-item:hover {
background: #f5f5f5;
}
/* 黑暗模式适配 */
#cr-panel.cr-theme-dark #cr-refresh-menu {
background: #1a1a1a; border-color: #444;
}
#cr-panel.cr-theme-dark #cr-refresh-menu .menu-item {
color: #e6e6e6;
}
#cr-panel.cr-theme-dark #cr-refresh-menu .menu-item:hover {
background: #2a2a2a;
}
/* ========== 新增:设置面板样式 ========== */
#cr-settings-modal {
position: fixed; inset: 0; background: rgba(0,0,0,.35); display:none;
align-items: center; justify-content: center; z-index: 2147483647; /* 和其他弹窗同层级 */
}
#cr-settings-modal .cr-box {
width: min(400px, 96vw); background:#fff; border-radius:12px;
box-shadow: 0 10px 30px rgba(0,0,0,.25); padding: 16px;
}
#cr-settings-modal h3 { margin:0 0 16px 0; font-size:16px; }
.cr-settings-item {
display: flex; flex-direction: column; gap: 6px; margin-bottom: 16px;
}
.cr-settings-item label {
font-size: 14px; color: #666;
}
.cr-settings-input {
width:100%; box-sizing:border-box; padding:10px 12px; font-size:14px;
border:1px solid #ddd; border-radius:8px; outline:none;
}
#cr-settings-modal .ops {
margin-top: 8px; display:flex; gap:8px; justify-content:flex-end;
}
#cr-settings-modal button {
border:1px solid #ddd; background:#fff; border-radius:8px; padding:6px 12px; cursor:pointer;
}
#cr-settings-modal button.primary {
background:#111; color:#fff; border-color:#111;
}
/* 黑暗模式适配设置面板 */
#cr-panel.cr-theme-dark #cr-settings-modal .cr-box {
background:#1a1a1a; color:#e6e6e6;
}
#cr-panel.cr-theme-dark .cr-settings-item label {
color:#bbb;
}
#cr-panel.cr-theme-dark .cr-settings-input {
background:#2a2a2a; border-color:#444; color:#e6e6e6;
}
#cr-panel.cr-theme-dark #cr-settings-modal button {
background:#2a2a2a; border-color:#444; color:#e6e6e6;
}
#cr-panel.cr-theme-dark #cr-settings-modal button.primary {
background:#333; color:#e6e6e6;
}
`);
// ========== DOM ==========
const panel = document.createElement('div');
panel.id = 'cr-panel';
panel.innerHTML = `
Ctrl+Alt+L 输入链接;Alt+T 目录;Ctrl+Alt+R 续读上次;←/→ 翻页或跳章;↑/↓ 平滑滚动;Ctrl+Alt+X 显示/隐藏。可拖动小窗,右下角可调大小。
`;
document.documentElement.appendChild(panel);
const modal = document.createElement('div');
modal.id = 'cr-modal';
modal.innerHTML = `
`;
document.documentElement.appendChild(modal);
const toc = document.createElement('div');
toc.id = 'cr-toc';
toc.innerHTML = `
`;
document.documentElement.appendChild(toc);
const $ = (sel, root = document) => root.querySelector(sel);
const contentEl = $('#cr-content', panel);
const urlInput = $('#cr-url', modal);
const tocListEl = $('#cr-toc-list', toc);
const tocRangeEl = $('#cr-toc-range', toc);
const tocGotoEl = $('#cr-toc-goto', toc);
const dragEl = $('#cr-drag', panel);
const resizeEl = $('#cr-resize', panel);
//AI增加刷新
// 2. 核心:刷新当前章节函数(复用现有抓取逻辑)
function refreshCurrentChapter() {
// 校验:当前有章节内容且未加载中
if (state.loading || !state.pages || state.pages.length === 0) {
alert('当前无章节可刷新或正在加载中!');
return;
}
// 获取当前章节的 URL(从已加载的页面中取)
const currentChapterUrl = state.pages[state.pageIndex].url;
// 调用现有函数重新抓取(自动覆盖旧内容+应用替换规则)
fetchChapterSeries(currentChapterUrl);
// 隐藏右键菜单(如果显示中)
// refreshMenu.style.display = 'none';
}
//AI增加替换窗口
// 文本替换弹窗(新增)
// 文本替换弹窗(重写:加规则列表)
const replaceModal = document.createElement('div');
replaceModal.id = 'cr-replace-modal';
replaceModal.innerHTML = `
`;
document.documentElement.appendChild(replaceModal);
// 替换弹窗元素获取(新增规则列表元素)
const replaceFrom = $('#cr-replace-from', replaceModal);
const replaceTo = $('#cr-replace-to', replaceModal);
const replaceRulesList = $('#cr-replace-rules', replaceModal); // 规则列表容器
//新弹窗事件绑定代码
// 【这是新代码,粘贴到旧代码的位置】
// 改造:替换弹窗事件绑定(用新的 addReplaceRule 逻辑)
$('#cr-replace-cancel', replaceModal).addEventListener('click', closeReplaceModal);
$('#cr-replace-confirm', replaceModal).addEventListener('click', addReplaceRule);
// 输入框快捷键:Enter 确认添加规则,Esc 关闭弹窗
replaceFrom.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); replaceTo.focus(); } // 先切到目标文本框
if (e.key === 'Escape') { e.preventDefault(); closeReplaceModal(); }
});
replaceTo.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); addReplaceRule(); } // 按 Enter 保存规则
if (e.key === 'Escape') { e.preventDefault(); closeReplaceModal(); }
});
// 替换弹窗全局 Esc 关闭(和其他弹窗保持一致)
replaceModal.addEventListener('keydown', (e) => {
if (!state1.modalOpen) return;
if (e.key === 'Escape') { e.preventDefault(); closeReplaceModal(); }
});
//AI增加结束
// ========== 新增:设置面板DOM ==========
// 修改设置面板DOM结构,改为左右分栏布局
const settingsModal = document.createElement('div');
settingsModal.id = 'cr-settings-modal';
settingsModal.innerHTML = `
阅读设置(Alt+A 打开)
`;
document.documentElement.appendChild(settingsModal);
// 新增:获取设置面板输入框元素(和其他元素选择器放一起)
const settingsFontSizeEl = $('#cr-settings-fontsize', settingsModal);
const settingsLineHeightEl = $('#cr-settings-lineheight', settingsModal);
// 添加新的样式
GM_addStyle(`
.cr-settings-container {
display: flex;
gap: 16px;
margin-bottom: 16px;
min-height: 200px;
}
.cr-settings-sidebar {
width: 140px;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
}
.cr-settings-item-option {
padding: 12px 16px;
cursor: pointer;
font-size: 14px;
border-bottom: 1px solid #eee;
transition: background-color 0.2s;
}
.cr-settings-item-option:last-child {
border-bottom: none;
}
.cr-settings-item-option:hover {
background-color: #f5f5f5;
}
.cr-settings-item-option.active {
background-color: #f0f0f0;
font-weight: 600;
border-left: 3px solid #111;
}
.cr-settings-content {
flex: 1;
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
}
.cr-settings-panel {
display: none;
}
.cr-settings-panel.active {
display: block;
}
.cr-settings-placeholder {
color: #666;
padding: 30px 0;
text-align: center;
font-size: 14px;
}
/* 黑暗模式适配 */
#cr-panel.cr-theme-dark .cr-settings-sidebar,
#cr-panel.cr-theme-dark .cr-settings-content {
border-color: #444;
}
#cr-panel.cr-theme-dark .cr-settings-item-option {
border-bottom-color: #444;
color: #e6e6e6;
}
#cr-panel.cr-theme-dark .cr-settings-item-option:hover {
background-color: #2a2a2a;
}
#cr-panel.cr-theme-dark .cr-settings-item-option.active {
background-color: #333;
border-left-color: #ccc;
}
#cr-panel.cr-theme-dark .cr-settings-placeholder {
color: #999;
}
`);
// 修改设置面板事件绑定,添加选项切换功能
// 获取新的元素
const settingsOptions = document.querySelectorAll('.cr-settings-item-option');
const settingsPanels = document.querySelectorAll('.cr-settings-panel');
// 切换设置面板
function switchSettingsPanel(tabId) {
// 更新选项激活状态
settingsOptions.forEach(option => {
option.classList.toggle('active', option.dataset.tab === tabId);
});
// 更新面板显示状态
settingsPanels.forEach(panel => {
panel.classList.toggle('active', panel.id === tabId);
});
}
// 绑定选项点击事件
settingsOptions.forEach(option => {
option.addEventListener('click', () => {
switchSettingsPanel(option.dataset.tab);
});
});
// ========== 新增:设置面板事件绑定 ==========
// 打开设置面板
// 修改打开设置面板函数,确保默认显示第一个面板
function openSettingsModal() {
state1.settingsModalOpen = true;
showPanel(); // 显示阅读小窗
settingsModal.style.display = 'flex';
// 同步当前设置到输入框
settingsFontSizeEl.value = state1.fontSize;
settingsLineHeightEl.value = state1.lineHeight;
// 确保默认显示第一个面板
switchSettingsPanel('font-settings');
settingsFontSizeEl.focus(); // 自动聚焦到字号输入框
}
// 关闭设置面板
function closeSettingsModal() {
state1.settingsModalOpen = false;
settingsModal.style.display = 'none';
}
// ========== 修改:保存设置处理函数 ==========
function saveSettingsHandler() {
const inputFontSize = parseInt(settingsFontSizeEl.value, 10);
const inputLineHeight = parseFloat(settingsLineHeightEl.value);
// 校验输入合法性
if (isNaN(inputFontSize) || inputFontSize <12 || inputFontSize >24) {
alert('请输入12-24之间的字号!');
settingsFontSizeEl.focus();
return;
}
if (isNaN(inputLineHeight) || inputLineHeight <1.2 || inputLineHeight >2.5) {
alert('请输入1.2-2.5之间的行高!');
settingsLineHeightEl.focus();
return;
}
// 更新状态并保存
state1.fontSize = inputFontSize;
state1.lineHeight = inputLineHeight;
saveSettings();
applySettings(); // 立即应用新设置
closeSettingsModal();
alert('设置已保存并应用!');
}
// 绑定按钮点击事件
$('#cr-settings-cancel', settingsModal).addEventListener('click', closeSettingsModal);
$('#cr-settings-confirm', settingsModal).addEventListener('click', saveSettingsHandler);
// 输入框快捷键:Enter保存,Esc关闭
settingsFontSizeEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); settingsLineHeightEl.focus(); }
if (e.key === 'Escape') { e.preventDefault(); closeSettingsModal(); }
});
settingsLineHeightEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); saveSettingsHandler(); }
if (e.key === 'Escape') { e.preventDefault(); closeSettingsModal(); }
});
// 面板全局Esc关闭
settingsModal.addEventListener('keydown', (e) => {
if (!state1.settingsModalOpen) return;
if (e.key === 'Escape') { e.preventDefault(); closeSettingsModal(); }
});
// ========== 状态 ==========
const state = {
visible: false,
modalOpen: false,
seriesId: null,
pages: [],
pageIndex: 0,
nextChapterUrl: null,
prevChapterUrl: null,
loading: false,
profile: null, // 当前命中的站点 profile
bookBase: null, // 当前书籍基准路径
tocUrl: null,
tocItems: [],
tocPage: 0,
tocPageSize: 50,
dragging: false, dragDX: 0, dragDY: 0,
resizing: false, startW: 0, startH: 0, startX: 0, startY: 0,
};
//AI增加
const state1 = {
visible: false,
modalOpen: false,
// ... 原有其他状态 ...
// 新增:替换规则存储(数组,存多组 {from, to})
replaceRules: [],
// 新增:标记是否已加载本地存储的规则(避免重复加载)
replaceRulesLoaded: false,
// ========== 新增:设置面板状态 + 字号/行高配置 ==========
settingsModalOpen: false, // 标记设置面板是否打开
fontSize: 14, // 默认字号(12-24px)
lineHeight: 1.7 // 默认行高(1.2-2.5)
};
// ========== 黑暗模式功能 ==========
function addDarkModeFeature() {
// 创建主题切换按钮容器
const themeControls = document.createElement('div');
themeControls.className = 'cr-theme-controls';
themeControls.style.cssText = `
position: absolute;
top: 10px;
right: 18px;
display: flex;
gap: 6px;
z-index: 1000;
`;
// 主题按钮(🌞亮色 / 🌙黑暗)
themeControls.innerHTML = `
`;
panel.appendChild(themeControls);
// 主题切换逻辑(修改后)
function setTheme(theme) {
// 1. 强制添加/移除黑暗模式类名
panel.classList.toggle('cr-theme-dark', theme === 'dark');
// 2. 内联样式兜底(防止 CSS 失效)
if (theme === 'dark') {
panel.style.background = '#1a1a1a';
panel.style.color = '#e6e6e6';
panel.style.borderColor = '#444';
} else {
panel.style.background = '#fff';
panel.style.color = '#222';
panel.style.borderColor = '#ddd';
}
// 3. 保存主题到本地
localStorage.setItem('cr_theme', theme);
// 4. 更新按钮状态(修复文字颜色)
themeControls.querySelectorAll('.cr-theme-btn').forEach(btn => {
const isActive = btn.dataset.theme === theme;
btn.style.opacity = isActive ? '1' : '0.6';
btn.style.fontWeight = isActive ? 'bold' : 'normal';
btn.style.color = theme === 'dark' ? '#e6e6e6' : '#000';
});
}
// 绑定按钮点击事件
themeControls.querySelectorAll('.cr-theme-btn').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation(); // 防止事件冒泡影响其他功能
setTheme(btn.dataset.theme);
});
});
// 恢复本地保存的主题(原逻辑保留)
const savedTheme = localStorage.getItem('cr_theme') || 'light';
setTheme(savedTheme);
}
//AI增加截止
// ========== Profiles(站点适配表) ==========
const PROFILES = [
// --- 3wwd.com ---
{
id: '3wwd',
test: (url) => /(^|\.)3wwd\.com$/i.test(new URL(url).hostname),
deriveBookBase: (url) => {
const u = new URL(url);
const m = u.pathname.match(/^(.*?\/book_\d+\/)/);
if (m) return new URL(m[1], u.origin).href;
return generic.deriveBookBase(url);
},
tocContainers: ['#list', '.box_con #list'],
isChapterLink: (abs, bookBase) => sameBook(abs, bookBase) && /\/\d+(?:_\d+)?\.html(?:[#?].*)?$/i.test(abs),
extractContent: (doc, baseUrl) => preferFirst(doc, [
'#content', '#chaptercontent', '#chapterContent', '.content', '.read-content', '#contentTxt', '#BookText', '#txtContent'
], baseUrl),
findInfoUrl: (doc, baseUrl, entry) => {
const el = doc.querySelector('#info_url');
if (el) return absolutize(baseUrl, el.getAttribute('href') || '');
return generic.findInfoUrl(doc, baseUrl, entry);
},
findNav: (doc, baseUrl) => generic.findNav(doc, baseUrl),
},
// --- biquge.tw / www.biquge.tw / m.biquge.tw ---
{
id: 'biquge-tw',
test: (url) => /(^|\.)(biquge\.tw)$/i.test(new URL(url).hostname.replace(/^www\./,'')),
deriveBookBase: (url) => {
const u = new URL(url);
const m = u.pathname.match(/^(.*?\/book\/\d+\/)/);
if (m) return new URL(m[1], u.origin).href; // 形如 https://www.biquge.tw/book/2319336/
return generic.deriveBookBase(url);
},
tocContainers: ['#list', '.listmain', '#chapterlist', '.chapterlist', '#listmain', '#chapters', '.chapters'],
isChapterLink: (abs, bookBase) => sameBook(abs, bookBase) && /\/book\/\d+\/\d+(?:_\d+)?\.html(?:[#?].*)?$/i.test(abs),
extractContent: (doc, baseUrl) => preferFirst(doc, [
'#content', '#chaptercontent', '#chapterContent', '.content', '.read-content', '#contentTxt', '#BookText', '#txtContent'
], baseUrl),
findInfoUrl: (doc, baseUrl, entry) => {
// biquge.tw 正文页一般没有明确“目录”按钮,直接回落到书籍根
return generic.deriveBookBase(entry);
},
findNav: (doc, baseUrl) => generic.findNav(doc, baseUrl),
tocCandidates: (base) => {
// biquge 常见目录页:/book//、/book//index.html、也有 all.html
const out = [];
const b = base.replace(/\/?$/,'/');
out.push(b);
out.push(b + 'index.html');
out.push(b + 'all.html');
try {
const u = new URL(b);
if (u.hostname.startsWith('www.')) {
const mu = new URL(b); mu.hostname = 'm.' + u.hostname.slice(4); out.push(mu.href); out.push(mu.href + 'index.html');
} else if (u.hostname.startsWith('m.')) {
const wu = new URL(b); wu.hostname = 'www.' + u.hostname.slice(2); out.push(wu.href); out.push(wu.href + 'index.html');
}
} catch {}
return Array.from(new Set(out));
}
},
// --- 通用兜底(最后一项) ---
{
id: 'generic',
test: (_url) => true,
deriveBookBase: (url) => generic.deriveBookBase(url),
tocContainers: ['#list', '.listmain', '#listmain', '#chapterlist', '.chapterlist', '#chapters', '.chapters', '.volume', '.mulu'],
isChapterLink: (abs, bookBase) => sameBook(abs, bookBase) && /\/\d+(?:_\d+)?\.html(?:[#?].*)?$/i.test(abs),
extractContent: (doc, baseUrl) => preferFirst(doc, [
'#content', '#chaptercontent', '#chapterContent', '.content', '.read-content', '#contentTxt', '#BookText', '#txtContent'
], baseUrl),
findInfoUrl: (doc, baseUrl, entry) => generic.findInfoUrl(doc, baseUrl, entry),
findNav: (doc, baseUrl) => generic.findNav(doc, baseUrl)
}
];
// ========== 通用实现 ==========
const generic = {
deriveBookBase(url) {
try {
const u = new URL(url);
const m1 = u.pathname.match(/^(.*?\/book_\d+\/)/);
if (m1) return new URL(m1[1], u.origin).href;
const m2 = u.pathname.match(/^(.*?\/book\/\d+\/)/);
if (m2) return new URL(m2[1], u.origin).href;
const p = u.pathname.endsWith('/') ? u.pathname : u.pathname.replace(/[^/]+$/, '');
return new URL(p, u.origin).href;
} catch { return null; }
},
findInfoUrl(doc, baseUrl, entryUrl) {
const el = doc.querySelector('#info_url');
if (el) return absolutize(baseUrl, el.getAttribute('href') || '');
const hint = Array.from(doc.querySelectorAll('a')).find(a => /(目录|章节目录|返回书页|返回目录)/.test((a.textContent || '').trim()));
if (hint) return absolutize(baseUrl, hint.getAttribute('href') || '');
return generic.deriveBookBase(entryUrl);
},
findNav(doc, baseUrl) {
const norm = (u)=>u ? absolutize(baseUrl, u) : null;
let prev = safeHref(doc.querySelector('#prev_url')?.getAttribute('href') || '');
let next = safeHref(doc.querySelector('#next_url')?.getAttribute('href') || '');
if (!prev) { const c = Array.from(doc.querySelectorAll('a')).find(a => /上[一页一章]/.test((a.textContent || '').trim())); if (c) prev = safeHref(c.getAttribute('href') || ''); }
if (!next) { const anchors = Array.from(doc.querySelectorAll('a')); const c = anchors.reverse().find(a => /下[一页一章]/.test((a.textContent || '').trim())); if (c) next = safeHref(c.getAttribute('href') || ''); }
return { prev: prev ? norm(prev) : null, next: next ? norm(next) : null };
}
};
// ========== 工具 ==========
const sleep = (ms)=>new Promise(r=>setTimeout(r,ms));
function absolutize(base, href) { try { return new URL(href, base).href; } catch { return href; } }
function safeHref(href) {
if (!href) return null;
if (/^\s*javascript:/i.test(href)) return null;
if (href.trim() === '#') return null;
return href;
}
function getSeriesIdFromUrl(url) { try { const m = new URL(url).pathname.match(/(\d+)(?:_(\d+))?\.html$/); return m ? m[1] : null; } catch { return null; } }
function isSameChapterPage(u1, u2) { const a = getSeriesIdFromUrl(u1), b = getSeriesIdFromUrl(u2); return a && b && a === b; }
function sameBook(hrefAbs, bookBase) {
try {
const u = new URL(hrefAbs), b = new URL(bookBase);
return u.origin === b.origin && u.pathname.startsWith(b.pathname);
} catch { return false; }
}
function chapterIdFromHref(href) {
try { const m = href.match(/\/(\d+)(?:_\d+)?\.html/); return m ? m[1] : null; } catch { return null; }
}
function chooseProfile(url) {
for (const p of PROFILES) { try { if (p.test(url)) return p; } catch {} }
return PROFILES[PROFILES.length-1];
}
function preferFirst(doc, selList, baseUrl) {
for (const sel of selList) {
const node = doc.querySelector(sel);
if (node) return cleanContentNode(node, baseUrl);
}
const body = doc.body.cloneNode(true);
try { body.querySelectorAll('script,style,ins,.adsbygoogle,.ad,[class*="ad-"],.advert,[id^="hm_t_"],.recommend,.toolbar').forEach(e=>e.remove()); } catch {}
const txt = (body.textContent || '').trim().replace(/\n{2,}/g,'');
return txt ? `
${txt}
` : '(未找到正文容器)
';
}
function cleanContentNode(node, baseUrl) {
const n = node.cloneNode(true);
try { n.querySelectorAll('script,style,ins,.adsbygoogle,.ad,[class*="ad-"],.advert,[id^="hm_t_"],.recommend,.toolbar').forEach(e=>e.remove()); } catch {}
n.querySelectorAll('img').forEach(img => {
const src = img.getAttribute('src') || '';
try { img.src = new URL(src, baseUrl).href; } catch { img.src = src; }
img.style.maxWidth = '100%';
});
n.querySelectorAll('a').forEach(a => {
const href = safeHref(a.getAttribute('href') || '');
if (!href) { a.removeAttribute('href'); return; }
try { a.href = new URL(href, baseUrl).href; } catch { a.href = href; }
a.rel = 'noreferrer noopener';
});
return n.innerHTML || '(正文为空)
';
}
// ========== 存储 ==========
//AI增加
// 新增:保存替换规则到本地存储
function saveReplaceRules() {
try {
localStorage.setItem('cr_replace_rules', JSON.stringify(state1.replaceRules));
} catch (e) {
console.error('保存替换规则失败:', e);
}
}
// 新增:从本地存储加载替换规则
function loadReplaceRules() {
if (state1.replaceRulesLoaded) return; // 避免重复加载
try {
const raw = localStorage.getItem('cr_replace_rules');
if (raw) {
state1.replaceRules = JSON.parse(raw).filter(rule =>
rule.from && rule.from.trim() // 过滤空规则
);
}
state1.replaceRulesLoaded = true;
renderReplaceRulesList(); // 加载后更新弹窗规则列表
} catch (e) {
console.error('加载替换规则失败:', e);
state1.replaceRules = [];
state1.replaceRulesLoaded = true;
}
}
// 新增:渲染替换规则列表(弹窗中显示已添加的规则)
function renderReplaceRulesList() {
if (!replaceRulesList) return;
// 无规则时显示提示
if (state1.replaceRules.length === 0) {
replaceRulesList.innerHTML = `
暂无替换规则,添加后将对所有章节生效
`;
return;
}
// 渲染已有的规则(带删除按钮)
replaceRulesList.innerHTML = state1.replaceRules.map((rule, index) => `
「${escapeHTML(rule.from)}」→「${escapeHTML(rule.to)}」
`).join('');
// 绑定删除规则事件
// 绑定删除规则事件(完整修复版)
replaceRulesList.querySelectorAll('.cr-replace-rule-del').forEach(btn => {
btn.addEventListener('click', () => {
const index = parseInt(btn.dataset.index);
state1.replaceRules.splice(index, 1); // 删除对应规则
saveReplaceRules(); // 保存到本地
renderReplaceRulesList(); // 刷新列表
applyAllReplaceRules(); // 立即对当前章生效(删除后更新内容)
});
});
}
//AI增加完成
// ========== 新增:设置存储(字号/行高) ==========
const LS_KEY_SETTINGS = 'cr_reader_settings'; // 本地存储键名
// 保存设置到localStorage
function saveSettings() {
try {
const settings = {
fontSize: state1.fontSize,
lineHeight: state1.lineHeight
};
localStorage.setItem(LS_KEY_SETTINGS, JSON.stringify(settings));
} catch (e) {
console.error('保存阅读设置失败:', e);
}
}
// 从localStorage加载设置
function loadSettings() {
try {
const raw = localStorage.getItem(LS_KEY_SETTINGS);
if (raw) {
const saved = JSON.parse(raw);
// 校验数值合法性(防止异常值)
if (saved.fontSize && saved.fontSize >=12 && saved.fontSize <=24) {
state1.fontSize = saved.fontSize;
}
if (saved.lineHeight && saved.lineHeight >=1.2 && saved.lineHeight <=2.5) {
state1.lineHeight = saved.lineHeight;
}
}
// 同步设置到输入框
settingsFontSizeEl.value = state1.fontSize;
settingsLineHeightEl.value = state1.lineHeight;
// 立即应用设置到正文
applySettings();
} catch (e) {
console.error('加载阅读设置失败:', e);
// 加载失败用默认值
state1.fontSize = 14;
state1.lineHeight = 1.7;
}
}
// ========== 修正:应用设置到“网页正文内容”(而非脚本UI) ==========
// ========== 修正:应用设置到正文内容 ==========
function applySettings() {
if (!contentEl) return;
// 清除可能影响正文样式的容器级设置
contentEl.style.fontSize = '';
contentEl.style.lineHeight = '';
// 获取所有正文段落和文本元素
const contentParagraphs = contentEl.querySelectorAll('p, div, span, h1, h2, h3, h4, h5, h6');
// 应用设置到每个正文元素
contentParagraphs.forEach(element => {
// 跳过脚本自身的UI元素
if (element.closest('div[style*="color:#666"], div[style*="color:#888"]')) return;
// 强制应用字体大小和行高(使用!important)
element.style.setProperty('font-size', `${state1.fontSize}px`, 'important');
element.style.setProperty('line-height', `${state1.lineHeight}`, 'important');
});
// 同时设置容器的基础样式(作为后备)
contentEl.style.fontSize = `${state1.fontSize}px`;
contentEl.style.lineHeight = state1.lineHeight;
}
const LS_KEY_PANEL = 'cr_reader_panel_state';
const LS_KEY_PROGRESS = 'cr_reader_progress';
function savePanelState() {
const rect = panel.getBoundingClientRect();
const data = { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
try { localStorage.setItem(LS_KEY_PANEL, JSON.stringify(data)); } catch {}
}
function restorePanelState() {
try {
const raw = localStorage.getItem(LS_KEY_PANEL);
if (!raw) return;
const { x, y, w, h } = JSON.parse(raw);
if (Number.isFinite(x) && Number.isFinite(y)) {
panel.style.top = Math.max(2, Math.min(y, window.innerHeight - 50)) + 'px';
panel.style.left = Math.max(2, Math.min(x, window.innerWidth - 50)) + 'px';
panel.style.bottom = ''; panel.style.right = '';
}
if (Number.isFinite(w) && Number.isFinite(h)) {
const cw = Math.max(220, Math.min(w, window.innerWidth - 10));
const ch = Math.max(160, Math.min(h, window.innerHeight - 10));
panel.style.width = cw + 'px';
panel.style.height = ch + 'px';
}
} catch {}
}
function clampIntoViewport() {
const rect = panel.getBoundingClientRect();
let x = rect.left, y = rect.top, w = rect.width, h = rect.height;
const maxX = window.innerWidth - w - 2;
const maxY = window.innerHeight - h - 2;
x = Math.max(2, Math.min(x, Math.max(2, maxX)));
y = Math.max(2, Math.min(y, Math.max(2, maxY)));
panel.style.left = x + 'px';
panel.style.top = y + 'px';
}
window.addEventListener('resize', () => { clampIntoViewport(); savePanelState(); });
function saveProgress({ tocUrl, chapterUrl, seriesId }) {
const bookBase = (state.profile?.deriveBookBase?.(tocUrl || chapterUrl)) || generic.deriveBookBase(tocUrl || chapterUrl) || '';
const payload = {
tocUrl: tocUrl || null, chapterUrl: chapterUrl || null, seriesId: seriesId || null,
bookBase, updatedAt: Date.now()
};
try { localStorage.setItem(LS_KEY_PROGRESS, JSON.stringify(payload)); } catch {}
}
function getSavedProgress() {
try {
const raw = localStorage.getItem(LS_KEY_PROGRESS);
if (!raw) return null;
const o = JSON.parse(raw);
if (!o || (!o.tocUrl && !o.chapterUrl)) return null;
return o;
} catch { return null; }
}
// ========== 抓取 ==========
function decodeText(arrayBuffer, headersStr) {
const lower = (headersStr || '').toLowerCase();
const m = lower.match(/charset\s*=\s*([^\s;]+)/);
const fromHeader = m && m[1] ? m[1].replace(/["']/g,'').toLowerCase() : '';
const tryDec = enc => { try { return new TextDecoder(enc).decode(arrayBuffer); } catch { return null; } };
let text = null;
if (/big5/.test(fromHeader)) text = tryDec('big5') || tryDec('utf-8');
else if (/gbk|gb18030|gb2312/.test(fromHeader)) text = tryDec('gbk') || tryDec('gb18030') || tryDec('utf-8');
else text = tryDec('utf-8') || tryDec('gbk') || tryDec('gb18030') || tryDec('big5');
if (!text) text = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer));
const hint = (text.match(/]+charset\s*=\s*["']?\s*([a-z0-9-]+)/i) || [])[1];
if (hint) {
const h = hint.toLowerCase();
if (/big5/.test(h)) text = tryDec('big5') || text;
else if (/gb/.test(h)) text = tryDec('gbk') || tryDec('gb18030') || text;
}
return text;
}
function gmFetch(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'GET',
url,
responseType: 'arraybuffer',
headers: { 'Accept': 'text/html,application/xhtml+xml' },
timeout: 30000,
onload: (res) => {
try {
const html = decodeText(res.response, res.responseHeaders || '');
resolve({ html, finalUrl: url, headers: res.responseHeaders || '' });
} catch (e) { reject(e); }
},
onerror: reject,
ontimeout: () => reject(new Error('请求超时')),
});
});
}
const parseHTML = html => new DOMParser().parseFromString(html, 'text/html');
// ========== 正文抽取(走 profile,可回落通用) ==========
function extractMain(doc, baseUrl) {
console.log('profile:', state.profile);
if (state.profile?.extractContent) {
try { return state.profile.extractContent(doc, baseUrl); } catch {}
}
return preferFirst(doc, [
'#content', '#chaptercontent', '#chapterContent', '.content', '.read-content', '#contentTxt', '#BookText', '#txtContent'
], baseUrl);
}
// ========== 上下页 & 目录链接 ==========
function getNavUrls(doc, baseUrl) {
if (state.profile?.findNav) {
try { return state.profile.findNav(doc, baseUrl); } catch {}
}
return generic.findNav(doc, baseUrl);
}
function getInfoUrl(doc, baseUrl, entryUrl) {
if (state.profile?.findInfoUrl) {
try { return state.profile.findInfoUrl(doc, baseUrl, entryUrl); } catch {}
}
return generic.findInfoUrl(doc, baseUrl, entryUrl);
}
// ========== 抓取章节(多页拼合) ==========
async function fetchChapterSeries(entryUrl) {
const visited = new Set();
state.loading = true;
state.pages = []; state.pageIndex = 0;
state.seriesId = getSeriesIdFromUrl(entryUrl);
state.nextChapterUrl = null; state.prevChapterUrl = null;
state.profile = chooseProfile(entryUrl);
const newBase = state.profile.deriveBookBase?.(entryUrl) || generic.deriveBookBase(entryUrl) || null;
if (state.bookBase && newBase && state.bookBase !== newBase) {
state.tocItems = []; // 切书:清空老目录缓存
}
state.bookBase = newBase;
renderInfo('正在抓取章节…');
try {
const first = await gmFetch(entryUrl);
const firstDoc = parseHTML(first.html);
state.tocUrl = getInfoUrl(firstDoc, entryUrl, entryUrl);
saveProgress({ tocUrl: state.tocUrl, chapterUrl: entryUrl, seriesId: state.seriesId });
const { prev: prev0, next: next0 } = getNavUrls(firstDoc, entryUrl);
state.prevChapterUrl = (prev0 && !isSameChapterPage(prev0, entryUrl)) ? prev0 : null;
state.pages.push({ url: entryUrl, html: extractMain(firstDoc, entryUrl) });
visited.add(new URL(entryUrl, location.href).href);
// 连抓分页
let cursor = next0, step = 0;
while (cursor && isSameChapterPage(cursor, entryUrl) && step < 50) {
const abs = new URL(cursor, entryUrl).href;
if (visited.has(abs)) break;
visited.add(abs);
const pg = await gmFetch(abs);
const d = parseHTML(pg.html);
state.pages.push({ url: abs, html: extractMain(d, abs) });
const nav = getNavUrls(d, abs);
cursor = nav.next;
step++;
await sleep(60);
}
if (cursor && !isSameChapterPage(cursor, entryUrl)) state.nextChapterUrl = cursor;
if (!state.pages.length) throw new Error('未抓到正文');
showCurrentPage();
} catch (err) {
console.error('[clean-reader] 抓取失败:', err);
renderInfo('抓取失败:' + (err && err.message ? err.message : '未知错误'));
} finally {
state.loading = false;
}
}
// ========== 目录抓取与渲染 ==========
async function openTOC() {
state.modalOpen = true;
// 切书校验:目录缓存属于别的书则清空
const saved = getSavedProgress?.() || null;
const desiredBase = (state.tocUrl ? (state.profile?.deriveBookBase?.(state.tocUrl) || generic.deriveBookBase(state.tocUrl))
: (saved?.tocUrl ? (state.profile?.deriveBookBase?.(saved.tocUrl) || generic.deriveBookBase(saved.tocUrl))
: generic.deriveBookBase(location.href))) || null;
const currBase = state.tocItems.length ? generic.deriveBookBase(state.tocItems[0].href) : null;
if (currBase && desiredBase && currBase !== desiredBase) state.tocItems = [];
toc.style.display = 'flex';
$('#cr-toc-goto').value = '';
if (!state.tocUrl) {
const sp = getSavedProgress();
if (sp && sp.tocUrl) state.tocUrl = sp.tocUrl;
else state.tocUrl = state.bookBase || generic.deriveBookBase(location.href);
}
if (!state.tocItems.length) {
tocListEl.innerHTML = `正在加载目录…
`;
try {
const ok = await tryFetchTOC(state.tocUrl);
if (!ok) throw new Error('未找到目录链接');
state.tocPage = clampTocPageToCurrent(state.tocItems);
renderTOC();
} catch (e) {
console.error('[clean-reader] 目录抓取失败:', e);
tocListEl.innerHTML = `目录加载失败:${e && e.message ? e.message : '未知错误'}
`;
tocRangeEl.textContent = '—';
}
} else {
state.tocPage = clampTocPageToCurrent(state.tocItems);
renderTOC();
}
}
async function tryFetchTOC(tocUrl) {
const base = (state.profile?.deriveBookBase?.(tocUrl) || generic.deriveBookBase(tocUrl) || tocUrl).replace(/\/?$/,'/');
const candidates = (state.profile?.tocCandidates?.(base)) || [base, base + 'index.html'];
for (const u of candidates) {
try {
const res = await gmFetch(u);
const doc = parseHTML(res.html);
const items = collectTOCItems(doc, u);
if (items && items.length) {
state.tocUrl = (state.profile?.deriveBookBase?.(u) || generic.deriveBookBase(u) || u);
state.tocItems = items;
return true;
}
} catch {}
}
return false;
}
function clampTocPageToCurrent(items) {
const idx = items.findIndex(it => it.id && state.seriesId && it.id === state.seriesId);
if (idx < 0) return state.tocPage || 0;
return Math.floor(idx / state.tocPageSize);
}
function closeTOC() { state.modalOpen = false; toc.style.display = 'none'; }
function collectTOCItems(doc, baseUrl) {
const bookBase = (state.profile?.deriveBookBase?.(baseUrl) || generic.deriveBookBase(baseUrl) || '').replace(/\/?$/,'/');
const isChapterLink = (abs) => (state.profile?.isChapterLink?.(abs, bookBase)) ?? (sameBook(abs, bookBase) && /\/\d+(?:_\d+)?\.html(?:[#?].*)?$/i.test(abs));
// 1) 优先从 profile 指定容器搜
const containersSel = state.profile?.tocContainers || [];
const containerEls = containersSel.map(sel => doc.querySelector(sel)).filter(Boolean);
let anchors = [];
for (const c of containerEls) anchors.push(...c.querySelectorAll('a'));
// 2) 容器里没拿到就全局兜底
if (anchors.length === 0) anchors = Array.from(doc.querySelectorAll('a'));
const out = [];
const seen = new Set();
for (const a of anchors) {
const raw = safeHref(a.getAttribute('href') || '');
if (!raw) continue;
const abs = absolutize(baseUrl, raw);
if (!isChapterLink(abs)) continue;
if (seen.has(abs)) continue;
seen.add(abs);
const title = (a.textContent || a.getAttribute('title') || '').trim().replace(/\s+/g, ' ');
const id = chapterIdFromHref(abs);
out.push({ title: title || (id ? `章节 ${id}` : abs), href: abs, id });
}
// 3) 目录由脚本写入或结构非常规时,正则兜底(biquge.tw 常见)
if (out.length < 5) {
const html = doc.documentElement.innerHTML || '';
const re = /]+href=["']([^"']*\/book\/\d+\/\d+(?:_\d+)?\.html)["'][^>]*>([^<]*)<\/a>/ig;
let m;
while ((m = re.exec(html))) {
const abs = absolutize(baseUrl, m[1]);
if (!isChapterLink(abs) || seen.has(abs)) continue;
seen.add(abs);
const id = chapterIdFromHref(abs);
const title = (m[2] || '').trim().replace(/\s+/g, ' ');
out.push({ title: title || (id ? `章节 ${id}` : abs), href: abs, id });
}
}
// 4) 过滤噪声项
const blacklist = /(上一[页章]|下一[页章]|返回|顶|底|最新|目录)/;
return out.filter(it => !blacklist.test(it.title));
}
// ========== 渲染 ==========
function renderInfo(msg) { contentEl.innerHTML = `${msg}
`; }
// ========== 修改:显示当前页面函数 ==========
function showCurrentPage() {
const idx = state.pageIndex;
if (!state.pages[idx]) return;
const total = state.pages.length;
const currentUrl = state.pages[idx].url;
// 渲染章节内容
contentEl.innerHTML = `
第 ${idx+1}/${total} 页 · ←/→ 翻页,↑/↓ 滚动,Alt+1刷新
${state.pages[idx].html}
`;
contentEl.scrollTop = 0;
applyAllReplaceRules();
// 确保设置应用 - 添加短暂延迟以确保DOM完全加载
setTimeout(applySettings, 50);
}
// ========== 面板显示/隐藏 ==========
function showPanel(){ state.visible = true; panel.style.display = 'block'; restorePanelState(); clampIntoViewport(); }
function hidePanel(){ state.visible = false; panel.style.display = 'none'; }
function togglePanel(){ state.visible ? hidePanel() : showPanel(); }
// ========== URL 弹窗 ==========
function openUrlModal(defaultUrl) {
state.modalOpen = true; showPanel(); modal.style.display = 'flex';
const saved = getSavedProgress();
urlInput.value = (saved && saved.chapterUrl) || defaultUrl || location.href;
urlInput.focus(); urlInput.select();
}
function closeUrlModal() { state.modalOpen = false; modal.style.display = 'none'; }
$('#cr-cancel', modal).addEventListener('click', closeUrlModal);
$('#cr-confirm', modal).addEventListener('click', () => {
const u = urlInput.value.trim();
if (!u) return;
closeUrlModal();
fetchChapterSeries(u);
});
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); $('#cr-confirm', modal).click(); }
if (e.key === 'Escape') { e.preventDefault(); closeUrlModal(); }
});
// ========== 目录弹窗事件 ==========
$('#cr-toc-close', toc).addEventListener('click', closeTOC);
$('#cr-toc-prev', toc).addEventListener('click', () => { state.tocPage = Math.max(0, state.tocPage - 1); renderTOC(); });
$('#cr-toc-next', toc).addEventListener('click', () => {
const total = state.tocItems.length;
const maxPage = Math.max(0, Math.ceil(total / state.tocPageSize) - 1);
state.tocPage = Math.min(maxPage, state.tocPage + 1);
renderTOC();
});
$('#cr-toc-go', toc).addEventListener('click', () => {
const total = state.tocItems.length;
const maxPage = Math.max(1, Math.ceil(total / state.tocPageSize));
let p = parseInt(tocGotoEl.value, 10);
if (!isFinite(p) || p < 1) p = 1;
if (p > maxPage) p = maxPage;
state.tocPage = p - 1;
renderTOC();
});
tocGotoEl.addEventListener('keydown', (e) => {
if (e.key === 'Enter') { e.preventDefault(); $('#cr-toc-go', toc).click(); }
if (e.key === 'Escape') { e.preventDefault(); closeTOC(); }
});
toc.addEventListener('keydown', (e) => {
if (!state.modalOpen) return;
if (e.key === 'PageDown') { e.preventDefault(); $('#cr-toc-next', toc).click(); }
if (e.key === 'PageUp') { e.preventDefault(); $('#cr-toc-prev', toc).click(); }
if (e.key === 'Escape') { e.preventDefault(); closeTOC(); }
});
function renderTOC() {
const total = state.tocItems.length;
const size = state.tocPageSize;
const page = Math.max(0, Math.min(state.tocPage, Math.floor((total-1)/size) || 0));
const start = page * size;
const end = Math.min(start + size, total);
const slice = state.tocItems.slice(start, end);
tocListEl.innerHTML = slice.map((it, i) => {
const idx = start + i + 1;
const active = (it.id && state.seriesId && it.id === state.seriesId) ? ' active' : '';
return `
${idx}.
${escapeHTML(it.title)}
`;
}).join('') || `目录为空
`;
tocRangeEl.textContent = total ? `${start+1}-${end}` : '—';
const maxPage = Math.max(1, Math.ceil(total / size));
tocGotoEl.setAttribute('max', String(maxPage));
tocGotoEl.setAttribute('placeholder', `1~${maxPage}`);
tocListEl.querySelectorAll('.toc-item').forEach(el => {
el.addEventListener('click', () => {
const href = el.getAttribute('data-href');
if (href) {
closeTOC(); showPanel(); fetchChapterSeries(href);
}
});
el.addEventListener('dblclick', () => {
const href = el.getAttribute('data-href');
if (href) {
closeTOC(); showPanel(); fetchChapterSeries(href);
}
});
});
}
function escapeHTML(s) {
return (s || '').replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
}
// ========== 拖动/大小 ==========
function startDrag(e){
state.dragging = true;
const rect = panel.getBoundingClientRect();
state.dragDX = e.clientX - rect.left;
state.dragDY = e.clientY - rect.top;
panel.style.top = rect.top + 'px';
panel.style.left = rect.left + 'px';
panel.style.bottom = ''; panel.style.right = '';
document.addEventListener('mousemove', onDragMove, true);
document.addEventListener('mouseup', endDrag, true);
e.preventDefault(); e.stopPropagation();
}
function onDragMove(e){
if (!state.dragging) return;
const w = panel.offsetWidth, h = panel.offsetHeight;
let nx = e.clientX - state.dragDX;
let ny = e.clientY - state.dragDY;
const maxX = window.innerWidth - w - 2;
const maxY = window.innerHeight - h - 2;
nx = Math.max(2, Math.min(nx, maxX));
ny = Math.max(2, Math.min(ny, maxY));
panel.style.left = nx + 'px';
panel.style.top = ny + 'px';
}
function endDrag(){
state.dragging = false;
document.removeEventListener('mousemove', onDragMove, true);
document.removeEventListener('mouseup', endDrag, true);
savePanelState();
}
dragEl.addEventListener('mousedown', startDrag, true);
function startResize(e){
state.resizing = true;
const rect = panel.getBoundingClientRect();
state.startW = rect.width;
state.startH = rect.height;
state.startX = e.clientX;
state.startY = e.clientY;
panel.style.top = rect.top + 'px';
panel.style.left = rect.left + 'px';
panel.style.bottom = ''; panel.style.right = '';
document.addEventListener('mousemove', onResizeMove, true);
document.addEventListener('mouseup', endResize, true);
e.preventDefault(); e.stopPropagation();
}
function onResizeMove(e){
if (!state.resizing) return;
const dx = e.clientX - state.startX;
const dy = e.clientY - state.startY;
let w = state.startW + dx;
let h = state.startH + dy;
const minW = 220, minH = 160;
const maxW = Math.min(window.innerWidth - 10, 900);
const maxH = Math.min(window.innerHeight - 10, 900);
w = Math.max(minW, Math.min(w, maxW));
h = Math.max(minH, Math.min(h, maxH));
panel.style.width = w + 'px';
panel.style.height = h + 'px';
clampIntoViewport();
}
function endResize(){
state.resizing = false;
document.removeEventListener('mousemove', onResizeMove, true);
document.removeEventListener('mouseup', endResize, true);
savePanelState();
}
resizeEl.addEventListener('mousedown', startResize, true);
// ========== 键盘捕获 ==========
const SCROLL_STEP = 80;
function handleKey(e) {
//此处为AI增加
// ========== 新增:Alt+A 打开设置面板 ==========
if (e.altKey && (e.key === 'a' || e.key === 'A')) {
e.preventDefault();
e.stopPropagation();
openSettingsModal();
return;
}
// 新增:Alt+1 刷新当前章节
if (e.altKey && e.key === '1') {
e.preventDefault();
e.stopPropagation();
refreshCurrentChapter();
return;
}
// 新增:Alt+Q 打开文本替换弹窗
if (e.altKey && (e.key === 'q' || e.key === 'Q')) {
e.preventDefault();
e.stopPropagation();
openReplaceModal();
return;
}
//AI增加结束
if (e.ctrlKey && e.altKey && (e.key === 'l' || e.key === 'L')) { e.preventDefault(); e.stopPropagation(); openUrlModal(location.href); return; }
if (e.altKey && (e.key === 't' || e.key === 'T')) { e.preventDefault(); e.stopPropagation(); openTOC(); return; }
if (e.ctrlKey && e.altKey && (e.key === 'r' || e.key === 'R')) {
e.preventDefault(); e.stopPropagation();
const saved = getSavedProgress();
if (saved && saved.chapterUrl) { showPanel(); fetchChapterSeries(saved.chapterUrl); }
else { openUrlModal(location.href); }
return;
}
if (e.ctrlKey && e.altKey && (e.key === 'x' || e.key === 'X')) { e.preventDefault(); e.stopPropagation(); togglePanel(); return; }
if (state.modalOpen) return;
if (!state.visible) return;
if (!['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)) return;
e.preventDefault(); e.stopImmediatePropagation(); e.stopPropagation();
if (e.key === 'ArrowUp') {
if (contentEl.scrollTop <= 0) {
if (state.pageIndex > 0) { state.pageIndex--; showCurrentPage(); }
else if (state.prevChapterUrl && !state.loading) { fetchChapterSeries(state.prevChapterUrl); }
else contentEl.scrollTop = 0;
} else {
contentEl.scrollBy({ top: -SCROLL_STEP, behavior: 'smooth' });
}
}
if (e.key === 'ArrowDown') {
const atBottom = contentEl.scrollTop + contentEl.clientHeight >= contentEl.scrollHeight - 2;
if (atBottom) {
if (state.pageIndex < state.pages.length - 1) { state.pageIndex++; showCurrentPage(); }
else if (state.nextChapterUrl && !state.loading) { fetchChapterSeries(state.nextChapterUrl); }
} else {
contentEl.scrollBy({ top: SCROLL_STEP, behavior: 'smooth' });
}
}
if (e.key === 'ArrowLeft') {
if (state.pageIndex > 0) { state.pageIndex--; showCurrentPage(); }
else if (state.prevChapterUrl && !state.loading) { fetchChapterSeries(state.prevChapterUrl); }
}
if (e.key === 'ArrowRight') {
if (state.pageIndex < state.pages.length - 1) { state.pageIndex++; showCurrentPage(); }
else if (state.nextChapterUrl && !state.loading) { fetchChapterSeries(state.nextChapterUrl); }
}
}
window.addEventListener('keydown', handleKey, true);
// ========== 辅助 ==========
// 新增:应用所有替换规则到当前内容(核心函数)
function applyAllReplaceRules() {
const contentEl = $('#cr-content', panel);
if (!contentEl || state1.replaceRules.length === 0) return;
let currentHtml = contentEl.innerHTML;
let totalReplaceCount = 0;
// 遍历所有规则,逐个替换
state1.replaceRules.forEach(rule => {
const from = rule.from.trim();
if (!from) return;
// 计算当前规则替换次数
const count = (currentHtml.split(from).length - 1);
totalReplaceCount += count;
// 执行替换(全局替换所有匹配)
currentHtml = currentHtml.replaceAll(from, rule.to);
});
// 更新内容(只有有替换时才更新,避免无意义DOM操作)
if (totalReplaceCount > 0) {
contentEl.innerHTML = currentHtml;
}
}
// ========== 文本替换功能(新增) ==========
// 打开替换弹窗
function openReplaceModal() {
state1.modalOpen = true;
showPanel(); // 显示小窗
replaceModal.style.display = 'flex';
replaceFrom.focus(); // 自动聚焦到原文本输入框
}
// 关闭替换弹窗
function closeReplaceModal() {
state1.modalOpen = false;
replaceModal.style.display = 'none';
replaceFrom.value = ''; // 清空输入
replaceTo.value = '';
}
// 执行文本替换
// 改造:添加替换规则(替换之前的 replaceText 函数)
function addReplaceRule() {
const fromText = replaceFrom.value.trim();
const toText = replaceTo.value;
// 校验:原文本不能为空
if (!fromText) {
alert('请输入要替换的原文本!');
return;
}
// 校验:避免重复添加相同原文本的规则(可选,可根据需求删除)
const isDuplicate = state1.replaceRules.some(rule =>
rule.from.trim() === fromText
);
if (isDuplicate) {
if (!confirm(`已存在「${fromText}」的替换规则,是否覆盖?`)) {
return;
}
// 覆盖旧规则
state1.replaceRules = state1.replaceRules.filter(rule =>
rule.from.trim() !== fromText
);
}
// 添加/更新规则
state1.replaceRules.push({ from: fromText, to: toText });
saveReplaceRules(); // 保存到本地
renderReplaceRulesList(); // 刷新弹窗规则列表
applyAllReplaceRules(); // 立即对当前章生效
// 清空输入框,提示成功
replaceFrom.value = '';
replaceTo.value = '';
alert(`添加成功!当前共 ${state1.replaceRules.length} 组规则(所有章节自动生效)`);
}
//function renderInfo(msg) { contentEl.innerHTML = `${msg}
`; }
restorePanelState();
// 初始化黑暗模式功能,AI增加
// 初始化功能
setTimeout(() => {
addDarkModeFeature();
}, 500);
// AI新增:初始化加载替换规则(脚本启动时加载,立即对当前章生效)
setTimeout(() => {
loadReplaceRules();
applyAllReplaceRules();
}, 600);
// ========== 新增:初始化加载阅读设置 ==========
setTimeout(() => {
loadSettings(); // 脚本启动时加载保存的字号/行高
}, 700);
})();