微信读书移动端笔记列表增强

添加打开/隐藏笔记面板按钮、笔记列表自动匹配当前章节进度、双击笔记跳转下一个笔记

// ==UserScript==
// @name         微信读书移动端笔记列表增强
// @namespace    http://tampermonkey.net/
// @version      0.7.2
// @description  添加打开/隐藏笔记面板按钮、笔记列表自动匹配当前章节进度、双击笔记跳转下一个笔记
// @author       XQH
// @match        https://weread.qq.com/web/reader/*
// @icon         https://weread.qq.com/favicon.ico
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  "use strict";
  // variable
  // 上一个点击的笔记列表项
  let lastNote = null;
  let doubleClickThreshold = 700;
  const DEBUG = false;

  console.log("[微信读书移动端笔记列表增强] js加载成功");

  // 注入一个pre显示日志
  if (DEBUG) {
    let pre = document.createElement("pre");
    pre.style.position = "fixed";
    pre.style.top = "0";
    pre.style.left = "0";
    pre.style.zIndex = "10000";
    pre.style.background = "rgba(255,255,255,0.9)";
    pre.style.padding = "10px";
    pre.style.border = "1px solid #ccc";
    pre.style.maxHeight = "200px";
    pre.style.width = "200px";
    pre.style.color = "#000";
    pre.style.height = "100px";
    pre.style.overflow = "auto";
    document.body.appendChild(pre);
    window.log = function (msg) {
      pre.innerText = msg + "\n" + pre.innerText;
    };
    window.console.log = function (msg) {
      pre.innerText = msg + "\n" + pre.innerText;
    };
    // 拦截报错log
    window.console.error = function (msg) {
      pre.innerText = msg + "\n" + pre.innerText;
    };
  }
  console.log("脚本启动v5");

  initStyle();
  setTimeout(async() => {
    await waitPageLoaded();
    injectNoteButton();
    // 为页面高亮文本添加点击事件监听
    observeDOMChanges();
  }, 700);

  // 主循环,检查是否跨页笔记,跨页笔记体现为可视范围内存在上一页按钮,
  // 且当前页面未出现文本与 lastNote 相同的高亮文本
  // lastNote (最后操作的笔记项,即当前笔记项) 赋值来源
  // 1. 点击笔记列表项
  // 2. 双击高亮文本
  // setInterval(() => {
  //   // checkNeedBackPage
  //   if (lastNote) {
  //     lastNote
  //   }
  // }, 1000);

  console.log("脚本注入组件完毕");

  // side effect method
  function initStyle() {
    addStyle(`
      .wr_reader_note_panel_footer_button,
      .wr_btn.wr_btn_Big.rbb_addShelf,
      .readerFooter_button.blue,
      .reader_toolbar_color_container,
      .toolbarItem.underlineHandWrite,
      .toolbarItem.underlineStraight,
      .toolbarItem.review,
      .toolbarItem.query,
      .wr_reader_note_panel_header_wrapper,
      .toast.toast_Show {
          display: none !important;
      }
      .readerNotePanel {
          overflow-y: auto;
          position: fixed;
          bottom: 0;
          left: 25%;
          width: 80%;
          margin-left: 0;
          display: none;
          z-index: 10000;
          background: white;
      }
      .readerBottomBar {
          z-index: 9999;
      }
      `);
  }

  // 在底部栏注入笔记按钮
  function injectNoteButton() {
    let note_btn = `
      <button title="笔记" class="rbb_item wr_note">
          <span class="icon"></span>
          <span class="txt">笔记</span>
      </button>`;
    try {
      let note_btn_container = document.querySelector(
        ".readerBottomBar_content"
      );
      if (note_btn_container) {
        if (note_btn_container.querySelector(".rbb_item.wr_note")) {
          return;
        }
        note_btn_container.insertAdjacentHTML("afterbegin", note_btn);
        document
          .querySelector(".rbb_item.wr_note")
          .addEventListener("click", function (event) {
            event.stopPropagation(); // 防止点击事件冒泡到外部
            let reader_note_panel = document.querySelector(".readerNotePanel");
            if (
              reader_note_panel.style.display === "none" ||
              reader_note_panel.style.display === ""
            ) {
              reader_note_panel.style.display = "block";
              scrollNotePanelToProgress();
              setTimeout(() => {
                document.addEventListener("click", outsideClickListener);
              }, 0);
              // 为笔记面板中的笔记项添加点击事件监听,点击跳转后关闭笔记面板
              let noteItems = document.querySelectorAll(
                ".wr_reader_note_panel_item_cell_wrapper.clickable"
              );
              for (let i = 0; i < noteItems.length; i++) {
                noteItems[i].addEventListener("click", function () {
                  reader_note_panel.style.display = "none";
                  lastNote = noteItems[i];
                  checkNeedSwitchPage();
                });
              }
            } else {
              reader_note_panel.style.display = "none";
              document.removeEventListener("click", outsideClickListener);
            }
          });
      }
    } catch (error) {
      console.log("注入笔记按钮失败", error);
    }
  }

  function outsideClickListener(event) {
    let reader_note_panel = document.querySelector(".readerNotePanel");
    let btn = document.querySelector(".rbb_item.wr_note");

    if (
      !reader_note_panel.contains(event.target) &&
      !btn.contains(event.target)
    ) {
      reader_note_panel.style.display = "none";
      document.removeEventListener("click", outsideClickListener);
    }
  }

  function scrollNotePanelToProgress() {
    let inViewUnderlines = getInViewUnderline();
    if (inViewUnderlines.length > 0) {
      let jumpText = inViewUnderlines[0];
      scrollNotePanelToText(jumpText);
    } else if (lastNote) {
      lastNote.scrollIntoView({ block: "center" });
    } else {
      scrollNotePanelToText(getChapterTitle());
    }
  }

  function scrollNotePanelToText(text) {
    // 在笔记面板中查找匹配的章节
    let noteChapters = document.querySelectorAll(
      ".wr_reader_note_panel_chapter_title"
    );
    for (let i = 0; i < noteChapters.length; i++) {
      if (noteChapters[i].innerText.trim() === text) {
        console.log("Found chapter in notes: " + text);
        // 滚动到笔记面板中的该章节
        noteChapters[i].scrollIntoView({ block: "center" });
        // 如果之前有选中的笔记,滚动到该笔记
        break;
      }
    }
  }

  function getInViewUnderline(targetText) {
    let underlines = document.getElementsByClassName("wr_underline_wrapper");
    let viewportHeight =
      window.innerHeight || document.documentElement.clientHeight;
    let viewportWidth =
      window.innerWidth || document.documentElement.clientWidth;
    let visibleUnderlines = [];
    for (let i = 0; i < underlines.length; i++) {
      let rect = underlines[i].getBoundingClientRect();
      if (
        rect.bottom >= 0 &&
        rect.top <= viewportHeight &&
        rect.right >= 0 &&
        rect.left <= viewportWidth
      ) {
        visibleUnderlines.push(underlines[i]);
      }
    }
    console.log("visibleUnderlines", visibleUnderlines);
    let textArr = [];
    for (let i = 0; i < visibleUnderlines.length; i++) {
      visibleUnderlines[i].click();
      setTimeout(() => {
        let copyButton = document.querySelector(".toolbarItem.wr_copy");
        if (copyButton) {
          window.isCustomCopy = true;
          function onCopy(e) {
            if (window.isCustomCopy) {
              let selectionText = e.target.value;
              e.preventDefault();
              e.stopPropagation();
              window.isCustomCopy = false;
              document.removeEventListener("copy", onCopy, true);
              textArr.push(selectionText);
              if (targetText && selectionText === targetText) {
                return textArr;
              }
            }
          }
          document.addEventListener("copy", onCopy, true);
          copyButton.click();
          // 拦截可能出现的复制提示框
          let toast = document.querySelector(".toast.toast_Show");
          if (toast) {
            toast.style.display = "none";
          }
        }
      }, 100);
    }
    return textArr;
  }

  async function lastPageInView() {
    let readerHeaderButton = await whenElementExist(".readerHeaderButton");
    if (readerHeaderButton && readerHeaderButton.innerText === "上一页") {
      // 判断是否在可见范围
      let rect = readerHeaderButton.getBoundingClientRect();
      let viewportHeight =
        window.innerHeight || document.documentElement.clientHeight;
      if (rect.top >= 0 && rect.bottom <= viewportHeight) {
        return true;
      }
    }
    return false;
  }

  async function checkNeedSwitchPage() {
    // 如果为上一页
    await waitPageLoaded();
    console.log("[检查是否需要切换页面]");
    setTimeout(async () => {
      let inViewUnderlines = getInViewUnderline();
      // 如果当前页面没有找到与上一个笔记相同的高亮文本,则跳转到上一页

      if (lastNote && !inViewUnderlines.includes(lastNote.innerText)) {
        console.log("当前页面没有与上一个笔记相同的高亮文本, 细分判断");
        // 如果存在上一页
        let isLastPageInView = await lastPageInView();
        if (isLastPageInView) {
          console.log("当前页面存在上一页按钮, 点击上一页按钮");
          elClick(document.querySelector(".readerHeaderButton"));
          // 等待页面加载完成
          await waitPageLoaded();
          setTimeout(() => {
            if (lastNote) {
              elClick(lastNote);
              checkNeedSwitchPage();
            }
          }, 500);
        } else {
          console.log("当前页面不存在上一页按钮, 尝试切换章节实现定位");
          // 再检查一遍高亮文本
          let checkInViewUnderlines = getInViewUnderline();
          if (!checkInViewUnderlines.includes(lastNote.innerText)) {
            console.log(
              "当前页面没有与上一个笔记相同的高亮文本, 尝试切换章节实现定位"
            );
            let contentList = getContentList();
            let chapterTitle = getChapterTitle();
            contentList.forEach((content, index) => {
              // chapterTitle include content
              if (chapterTitle.includes(content)) {
                // 边界判断加载下一章节再跳转笔记
                let idx = index + 1;
                if (idx >= contentList.length) {
                  idx = 0;
                }
                // 加载
                let nextChapter = document.querySelectorAll(
                  ".readerCatalog_list_item"
                )[idx];
                elClick(nextChapter);
                setTimeout(() => {
                  console.log("已加载下一章节, 尝试回到目标笔记进度");

                  elClick(lastNote);
                  // checkNeedSwitchPage();
                }, 500);
              }
            });
          }
        }
      } else {
        console.log("当前页面有与上一个笔记相同的高亮文本, 释放 lastNote");
        lastNote = null;
      }
    }, 200);
  }

  function getContentList() {
    let contentList = document.querySelectorAll(".readerCatalog_list_item");
    let contentArr = [];
    for (let i = 0; i < contentList.length; i++) {
      let content = contentList[i].innerText;
      contentArr.push(content);
    }
    return contentArr;
  }

  // 获取当前章节标题
  function getChapterTitle() {
    let chapter_title = document.querySelector(".readerTopBar_title_chapter");
    if (chapter_title) {
      return chapter_title.innerText.trim();
    }
    return "";
  }

  async function findAndClickNextNoteItem(jumpText) {
    console.log("[跳转下一个划线笔记]", "当前笔记文本 " + jumpText);

    let noteItems = document.querySelectorAll(
      ".wr_reader_note_panel_item_cell_wrapper.clickable"
    );
    let foundIndex = -1;
    for (let j = 0; j < noteItems.length; j++) {
      let noteText = noteItems[j].innerText.replace(/\s/g, "");
      console.log("[笔记文本]", noteText);

      // 移除空格和换行符
      noteText = noteText.replace(/\s/g, "");
      if (noteText === jumpText) {
        foundIndex = j;
        break;
      }
    }

    if (foundIndex >= 0) {
      let nextIndex = foundIndex + 1;
      if (nextIndex >= noteItems.length) {
        nextIndex = 0;
      }
      elClick(noteItems[nextIndex]);
      lastNote = noteItems[nextIndex];
      console.log("[下一个笔记]", lastNote.innerText);
      checkNeedSwitchPage();
    } else {
      console.log("查找下一个划线笔记文本失败: " + noteText);
    }
  }

  // 通过高亮文本的点击后的工具栏来获取高亮文本
  async function getHighlightText(element) {
    element.click();
    let copyBtn = await whenElementExist(".toolbarItem.wr_copy");
    return new Promise((resolve) => {
      function onCopy(e) {
        if (window.isCustomCopy) {
          let copyText = e.target.value;
          e.preventDefault();
          e.stopPropagation();
          window.isCustomCopy = false;
          document.removeEventListener("copy", onCopy, true);
          resolve(copyText);
        }
      }
      document.addEventListener("copy", onCopy, true);
      window.isCustomCopy = true;
      copyBtn.click();
    });
  }

  function elClick(el) {
    el.dispatchEvent(
      new MouseEvent("click", {
        clientX: 1,
        clientY: 1,
      })
    );
  }

  // 为高亮文本添加点击事件监听
  function addUnderlineClickListeners(dbClickThreshold) {
    let underlines = document.getElementsByClassName("wr_underline_wrapper");
    for (let i = 0; i < underlines.length; i++) {
      if (underlines[i].getAttribute("data-listener-added")) {
        continue;
      }
      underlines[i].setAttribute("data-listener-added", "true");

      let clickCount = 0;
      let lastClickTime = 0;
      underlines[i].addEventListener("click", function (e) {
        const currentTime = new Date().getTime();
        clickCount++;
        if (clickCount === 1) {
          setTimeout(async function () {
            if (clickCount === 1) {
              // 单击:可添加显示工具栏的逻辑
            } else if (clickCount === 2) {
              const copyText = await getHighlightText(underlines[i]);
              findAndClickNextNoteItem(copyText);
            }
            clickCount = 0;
          }, dbClickThreshold);
        }
        lastClickTime = currentTime;
      });
    }
  }

  function observeDOMChanges() {
    let targetNode = document.querySelector(".readerContent");
    if (!targetNode) {
      console.log("Reader content not found for observing DOM changes.");
      return;
    }
    let config = { childList: true, subtree: true };

    let callback = function (mutationsList, observer) {
      for (let mutation of mutationsList) {
        if (mutation.addedNodes.length > 0) {
          addUnderlineClickListeners(doubleClickThreshold);
        }
      }
    };

    let observer = new MutationObserver(callback);
    observer.observe(targetNode, config);
  }
  // base method
  async function waitPageLoaded() {
    await whenElementExist(".readerCatalog_list_item");
  }
  function addStyle(cssRules) {
    const styleElement = document.createElement("style");
    styleElement.innerHTML = cssRules;
    document.head.appendChild(styleElement);
  }
  function whenElementExist(selector) {
    return new Promise((resolve) => {
      const checkForElement = () => {
        let element = null;
        if (typeof selector === "function") {
          element = selector();
        } else {
          element = document.querySelector(selector);
        }
        if (element) {
          resolve(element);
        } else {
          requestAnimationFrame(checkForElement);
        }
      };
      checkForElement();
    });
  }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址