// ==UserScript==
// @name 微信读书笔记列表跟随当前章节滚动
// @namespace http://tampermonkey.net/
// @version 0.4.9
// @description 笔记列表跟随当前章节滚动
// @author XQH
// @match https://weread.qq.com/web/reader/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=qq.com
// @grant none
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// 等待页面加载完毕
setTimeout(function () {
// 保存点击的上一个笔记
let lastNote = null;
let lastPos = -1;
// 双击
let clickCount = 0;
let lastClickTime = 0;
const doubleClickThreshold = 300; // ms
// 要注入的底部栏
let invoke = document.querySelector(".readerBottomBar.showShadow");
// 笔记面板
let notePanel = document.getElementsByClassName("readerNotePanel")[0];
if (notePanel) {
notePanel.style.display = "none";
}
let title = document.querySelector(".readerTopBar_title_chapter");
let noteList = document.getElementsByClassName("readerNoteList")[0];
let noteTexts = noteList.getElementsByClassName("sectionListItem_title");
let noteItems = document.getElementsByClassName("sectionListItem_content noteItem_content clickable");
// 获取网页标题 css:.readerTopBar_title_chapter
function jumpNote() {
// 获取title文字,查找noteList中div(class:noteItemClz)子标签文字对应标签
let titleText = title.innerText;
for (let i = 0; i < noteTexts.length; i++) {
if (noteTexts[i].innerText == titleText) {
noteList.scrollTop = noteTexts[i].offsetTop - 100;
break;
}
}
// 如果保存了上一个笔记
if (lastNote) {
// 滚动到笔记位置
noteList.scrollTop = lastNote.offsetTop - 100;
}
}
function getClipboardText() {
return new Promise(function (resolve, reject) {
if (navigator.clipboard && navigator.clipboard.readText) {
// 使用新的 navigator.clipboard API 获取剪贴板内容
navigator.clipboard.readText().then(function (text) {
resolve(text);
}).catch(function (err) {
reject(err);
});
} else if (window.clipboardData && window.clipboardData.getData) {
// 使用旧的 window.clipboardData 获取剪贴板内容
let clipboardText = window.clipboardData.getData('Text');
resolve(clipboardText);
} else if (event.clipboardData && event.clipboardData.getData) {
// 使用事件对象的 clipboardData 获取剪贴板内容
let clipboardText = event.clipboardData.getData('text/plain');
resolve(clipboardText);
} else {
reject(new Error('Clipboard API not supported'));
}
});
}
// 定时隐藏推广按钮
setInterval(function () {
// 隐藏推广按钮
let appDownloadBtn = document.querySelector(".readerControls_item.download");
if (appDownloadBtn) {
appDownloadBtn.style.display = "none";
}
// 移动端UA下会显示打开app阅读
let toAppBtn = document.querySelector(".readerFooter_button.blue");
if (toAppBtn) {
// 直接删除该元素
toAppBtn.parentNode.removeChild(toAppBtn);
}
// 笔记面板底部栏移除
let notePanelFooter = document.querySelector(".readerNotePanelBottomBar");
if (notePanelFooter) {
notePanelFooter.parentNode.removeChild(notePanelFooter);
}
// 如果底部栏未显示,同步隐藏笔记面板
if (invoke && !invoke.classList.contains("active")) {
// 检测是否宽屏显示侧栏(clz readerControls readerControls)
let readerControls = document.querySelector(".readerControls.readerControls");
if (readerControls && readerControls.style.display == "none") {
notePanel.style.display = "none";
}
}
// 获取所有 clz:wr_underline_wrapper, 设置点击事件
let underlines = document.getElementsByClassName("wr_underline_wrapper");
for (let i = 0; i < underlines.length; i++) {
// 判断是否已添加点击事件
if (underlines[i].getAttribute("data-clicked")) {
continue;
}
underlines[i].setAttribute("data-clicked", true);
underlines[i].addEventListener("click", function () {
// 调用复制按钮点击事件 toolbarItem copy
let copyBtn = document.querySelector(".toolbarItem.copy");
copyBtn.dispatchEvent(
new MouseEvent("click", {
clientX: 1,
clientY: 1,
})
);
// 设置当前lastNode 为 剪切板内容比较noteTexts结果
setTimeout(function () {
let toasts = document.querySelectorAll(".toast.toast_Show");
for (let i = 0; i < toasts.length; i++) {
toasts[i].parentNode.removeChild(toasts[i]); // 移除dom
}
}, 50);
getClipboardText().then(function (text) {
for (let i = 0; i < noteTexts.length; i++) {
if (noteTexts[i].innerText == text) {
lastNote = noteItems[i];
lastPos = i;
break;
}
}
// 隐藏复制成功提示
}).catch(function (err) {
console.error('Failed to get clipboard text:', err);
});
// 双击判断
const currentTime = new Date().getTime();
if (currentTime - lastClickTime < doubleClickThreshold && clickCount == 1) {
clickCount = 0;
// 获取当前 note 的下一条
let index = lastPos + 1;
if (index >= noteItems.length) {
index = 0;
}
lastPos = index;
let nextNote = noteItems[lastPos];
nextNote.click(); // 触发点击事件跳转到下一条笔记
lastNote = nextNote;
setTimeout(function () {
jumpNote();
}, 50);
} else {
clickCount++;
setTimeout(function () {
if (clickCount == 1) {
// 设置显示 clz:reader_toolbar_container
let toolbar = document.querySelector(".reader_toolbar_container");
toolbar.style.display = "";
setTimeout(function () {
toolbar.style.display = "none";
}, 2000);
}
clickCount = 0;
}, doubleClickThreshold);
}
lastClickTime = currentTime;
});
}
// 持续替换笔记操作栏中
// 移除波浪线,直线item,替换为上下一条笔记
let hand = document.querySelector(".toolbarItem.underlineHandWrite");
let straight = document.querySelector(".toolbarItem.underlineStraight");
let lastPage = document.querySelector(".toolbarItem.lastPage");
let nextPage = document.querySelector(".toolbarItem.nextPage");
// 检测到删除划线按钮(clz:toolbarItem removeUnderline)时,添加上下一条笔记按钮
if (!document.querySelector(".toolbarItem.removeUnderline")) {
// 移除上下一条笔记按钮
if (lastPage) {
lastPage.parentNode.removeChild(lastPage);
}
if (nextPage) {
nextPage.parentNode.removeChild(nextPage);
}
return;
} else if (hand && straight){
let par = hand.parentElement;
par.removeChild(hand);
par.removeChild(straight);
}
if (!lastPage || !nextPage) {
let toolInvoke = document.querySelector(".toolbarItem.copy");
let lastPage = document.createElement("button");
lastPage.className = "toolbarItem lastPage";
lastPage.title = "上一条笔记";
lastPage.innerHTML = '<span class="toolbarItem_text">后退</span>';
let nextPage = document.createElement("button");
nextPage.className = "toolbarItem nextPage";
nextPage.title = "下一条笔记";
nextPage.innerHTML = '<span class="toolbarItem_text">前进</span>';
toolInvoke.parentNode.insertBefore(lastPage, toolInvoke);
toolInvoke.parentNode.insertBefore(nextPage, toolInvoke);
// lastPage触发当前note的上一条的点击事件,nextPage触发当前note的下一条的点击事件
lastPage.addEventListener("click", function () {
// 获取当前note的上一条
let index = lastPos - 1;
if (index < 0) {
index = noteItems.length - 1;
}
lastPos = index;
let prevNote = noteItems[lastPos];
prevNote.dispatchEvent(
new MouseEvent("click", {
clientX: 1,
clientY: 1,
})
);
lastNote = prevNote;
// 跳转到上一个笔记位置
setTimeout(function () {
jumpNote();
}, 100);
});
nextPage.addEventListener("click", function () {
// 获取当前note的下一条
let index = lastPos - 1;
if (index < 0) {
index = noteItems.length - 1;
}
lastPos = index;
let nextNote = noteItems[lastPos];
nextNote.dispatchEvent(
new MouseEvent("click", {
clientX: 1,
clientY: 1,
})
);
lastNote = nextNote;
// 跳转到上一个笔记位置
setTimeout(function () {
jumpNote();
}, 100);
}
);
}
}, 800)
// 检测移动端左右滑动手势,左滑显示笔记面板,右滑隐藏笔记面板
var start_x = 0; // 记录起始位置
document.addEventListener('touchstart', function (e) {
// 获取当前手指的横向位置
start_x = e.touches[0].clientX;
});
document.addEventListener('touchmove', function (e) {
// 计算手指的横向偏移量
var offset_x = e.touches[0].clientX - start_x;
// 如果偏移量大于一定值,则认为是左右滑手势
if (Math.abs(offset_x) > 20) {
e.preventDefault(); // 阻止默认事件
}
});
document.addEventListener('touchend', function (e) {
// 计算手指的横向偏移量
var offset_x = e.changedTouches[0].clientX - start_x;
// 如果偏移量大于一定值,则认为是左右滑手势
if (offset_x > 20) {
// 右滑处理逻辑
notePanel.style.width = "100%";
notePanel.style.display = "none";
// 清除过渡动画
notePanel.style.transition = "";
} else if (offset_x < -20) {
// 注入过渡动画
notePanel.style.transition = "width 0.5s";
// 左滑处理逻辑
notePanel.style.display = "";
// 设置宽度占3/4
notePanel.style.width = "75%";
notePanel.style.height = "100%";
// 设置间隔
notePanel.style.marginLeft = "25%";
// 置顶
notePanel.style.top = "0px";
// 显示invoke (添加acitve)
invoke.classList.add("active");
}
});
// 为笔记列表添加点击事件
// 为所有(sectionListItem_content noteItem_content clickable)设置点击隐藏notePanel
for (let i = 0; i < noteItems.length; i++) {
noteItems[i].addEventListener("click", function () {
// 保存到上一个笔记
lastNote = noteItems[i];
lastPos = i;
notePanel.style.display = "none";
// 获取笔记文字
let noteText = noteItems[i].querySelector(".text").innerText;
// 调用大声朗读api
setTimeout(function () {
// 获取当前.app_content(div) 离顶部的距离
let appContent = document.querySelector(".app_content");
let appContentTop = Math.round(appContent.getBoundingClientRect().top);
// 如果top 为-75(PC) -11(MOBILE),这个高度可能受屏幕高度影响(?),需检测是否有上一页,可能需要调用翻页并重新调用跳转笔记
console.log("appContentTop:" + appContentTop);
// TODO 考虑重构代码理清逻辑
if (appContentTop == -75 || appContentTop == -11 || appContentTop == -12) {
// 则判断为上一页,调用翻页并重新调用跳转笔记
console.log("start to judge if it is prev page");
// 可以考虑每个笔记跳转时都直接切换章节再跳转避免触发翻页bug
setTimeout(function () {
// 高度无变化,判断为需要切换上一页
if (appContentTop == -75 || appContentTop == -11 || appContentTop == -12) {
// 获取 ul(clz:readerCatalog_list),
// 下的div(chapterItem_link chapterItem_level1)比较div中span文字
// 选择不与当前标题相同的item调用点击事件,再重新调用切换笔记
let catalogList = document.querySelector(".readerCatalog_list");
let catalogItems = catalogList.getElementsByClassName("chapterItem_link chapterItem_level1");
let titleText = title.innerText;
for (let i = 0; i < catalogItems.length; i++) {
if (catalogItems[i].querySelector("span").innerText != titleText) {
console.log("jump another chapter");
catalogItems[i].dispatchEvent(
new MouseEvent("click", {
clientX: 1,
clientY: 1,
})
);
break;
}
}
setTimeout(function () {
lastNote.dispatchEvent(
new MouseEvent("click", {
clientX: 1,
clientY: 1,
})
);
// 跳转到上一个笔记位置
setTimeout(function () {
jumpNote();
}, 100);
}, 600);
// console.log("start to jumpNote");
// localStorage.setItem("lastNotePos", i);
// localStorage.setItem("need_jump", true);
}
}, 1600);
}
}, 100);
});
}
// 如果localStorage中保存了上一个笔记
// if (localStorage.getItem("lastNotePos")) {
// // 获取上一个笔记
// let lastPos = localStorage.getItem("lastNotePos");
// if (lastPos) {
// lastNote = noteItems[lastPos];
// // 获取是否需要跳转
// let need_jump = localStorage.getItem("need_jump");
// if (need_jump == "true") {
// lastNote.dispatchEvent(
// new MouseEvent("click", {
// clientX: 1,
// clientY: 1,
// })
// );
// // 跳转到上一个笔记位置
// setTimeout(function () {
// jumpNote();
// // 清除localStorage
// localStorage.removeItem("lastNotePos");
// localStorage.removeItem("need_jump");
// }, 100);
// }
// }
// }
// 底部添加笔记按钮
// 获取button.rbb_item
if (invoke) {
// 获取笔记图标
let noteIcon = document.querySelector('.readerControls_item.note').querySelector('.icon');
let backgroundImg = getComputedStyle(noteIcon).getPropertyValue("background-image");
// 在readerBottomBar 的 rbb_item.setting(bar内第三个button) 后添加一个button
let newInvoke = document.createElement("button");
newInvoke.className = "rbb_item note";
newInvoke.title = "笔记";
newInvoke.innerHTML = '<span class="icon""></span><span class="txt">笔记</span>';
invoke.insertBefore(newInvoke, invoke.children[3]);
newInvoke.querySelector('.icon').style.backgroundImage = backgroundImg;
// 设置点击事件为切换readerNotePanel的display
newInvoke.addEventListener("click", function () {
// 不隐藏readerBottomBar, 为readerBottomBar添加active class
setTimeout(function () {
invoke.classList.add("active");
}, 100);
let notePanel = document.getElementsByClassName("readerNotePanel")[0];
if (notePanel.style.display == "none") {
// 注入过渡动画
notePanel.style.transition = "width 0.5s";
// 左滑处理逻辑
notePanel.style.display = "";
// 设置宽度占3/4
notePanel.style.width = "75%";
notePanel.style.height = "100%";
// 设置间隔
notePanel.style.marginLeft = "25%";
// 置顶
notePanel.style.top = "0px";
} else {
notePanel.style.width = "100%";
notePanel.style.display = "none";
}
// 延迟100ms
setTimeout(function () {
jumpNote();
}, 100);
});
}
// 监听点击事件(readerControls_item note)
document.querySelector(".readerControls_item.note").addEventListener("click", function () {
jumpNote();
});
}, 1500);
})();