您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
阅读记录、键盘操作、筛选已读
// ==UserScript== // @name 素问通读助手 // @name:zh-CN 素问通读助手 // @name:en sooon-read-through-assistance // @description 阅读记录、键盘操作、筛选已读 // @description:zh-CN 阅读记录、键盘操作、筛选已读 // @description:en Reading history, keyboard operations, filtering read // @namespace https://gf.qytechs.cn/zh-CN/scripts/540273 // @version 1.0.0 // @author monkey // @icon https://sooon.ai/assets/favicon-BRntVMog.ico // @match https://sooon.ai/** // @grant GM_registerMenuCommand // @license MIT // ==/UserScript== (function () { 'use strict'; const APP_NAME = "素问通读助手"; function getTimeStr() { const d = /* @__PURE__ */ new Date(); return d.toTimeString().slice(0, 8); } function formatLog(args) { let prefix = `[${APP_NAME}]`; prefix += ` [${getTimeStr()}]`; return [prefix, ...args]; } const log = (...a) => { a.map(String).join(" "); console.log(...formatLog(a)); }; const warn = (...a) => { a.map(String).join(" "); console.warn(...formatLog(a)); }; async function waitForElement(selector, options = {}) { const { timeout = 5e3, interval = 100, maxRetries = 50, noError = false } = options; return new Promise((resolve, reject) => { let retries = 0; const startTime = Date.now(); const find = () => { const element = document.querySelector(selector); if (element) { resolve(element); return; } const elapsedTime = Date.now() - startTime; if (elapsedTime >= timeout) { if (noError) { resolve(null); } else { reject(new Error(`Element ${selector} not found after ${timeout}ms timeout`)); } return; } if (retries >= maxRetries) { if (noError) { resolve(null); } else { reject(new Error(`Element ${selector} not found after ${maxRetries} retries`)); } return; } retries++; setTimeout(find, interval); }; find(); }); } const ls = { set(key, value) { localStorage.setItem(key, JSON.stringify(value)); }, get(key) { const value = localStorage.getItem(key); try { return value === null ? null : JSON.parse(value); } catch (e) { return value; } }, remove(key) { localStorage.removeItem(key); }, clear() { localStorage.clear(); }, keys() { return Object.keys(localStorage); }, has(key) { return localStorage.getItem(key) !== null; } }; const MODAL_SELECTORS = { DIALOG: 'body > div:has(> div.fixed.inset-0.isolate.z-\\$mantine-z-index-modal) div[role="dialog"][aria-modal="true"]:not([class*="Drawer"])', CLOSE_BUTTON: 'body > div:has(> div.fixed.inset-0.isolate.z-\\$mantine-z-index-modal) div[role="dialog"][aria-modal="true"]:not([class*="Drawer"]) header button:has(svg.tabler-icon-chevron-left)', CONTENT_SCROLL: "body > div:nth-child(7) > div.fixed.inset-0.isolate.z-\\$mantine-z-index-modal.overflow-hidden > div > div > section > div > div.flex-1.flex.flex-col.overflow-hidden._mask_a535l_1 > div > div.outline-none" }; const LIST_SELECTORS = { VIEWPORT: "div[data-overlayscrollbars-viewport]", CONTENT: 'div[style*="overflow-anchor: none"]', ITEM_WRAPPER: 'div[style*="position: absolute"]', ITEM_CONTENT: "div.flex.flex-col.gap-2.p-4", ITEM_CLICKABLE: 'button.mantine-UnstyledButton-root[type="button"].flex.flex-col' }; const PAGINATION_SELECTORS = { PAGE_INPUT: 'input[name="page"]' }; const EDIT_DRAWER_SELECTORS = { DRAWER: "div.m_5df29311.mantine-Drawer-body", COLLECTION_CHECKBOX: 'button[role="checkbox"].mantine-CheckboxCard-card', EDIT_BUTTON: "button svg.tabler-icon-bookmarks", SAVE_BUTTON: 'button[type="submit"]' }; const HIGHLIGHT_CLASS = "userscript-keyboard-focused-item"; const SCROLL_CONFIG = { ARTICLE_SCROLL_AMOUNT: 300 }; let currentFocusedItem = null; function isArticleModalOpen() { const modals = document.querySelectorAll(MODAL_SELECTORS.DIALOG); for (const modal of modals) { const titleElement = modal.querySelector("header h4.select-none.text-lg"); const hasLexicalEditor = modal.querySelector('div[data-lexical-editor="true"]'); if (titleElement && titleElement.textContent.trim() === "参考源" && hasLexicalEditor) { const computedStyle = window.getComputedStyle(modal); return computedStyle.opacity === "1" && computedStyle.display !== "none"; } } return false; } function closeArticleModal() { const closeButton = document.querySelector(MODAL_SELECTORS.CLOSE_BUTTON); if (closeButton) { closeButton.click(); return true; } return false; } function getVisibleArticleItems() { const container = document.querySelector(`${LIST_SELECTORS.VIEWPORT} > ${LIST_SELECTORS.CONTENT}`); if (!container) return []; return Array.from(container.querySelectorAll(`${LIST_SELECTORS.ITEM_WRAPPER} > ${LIST_SELECTORS.ITEM_CONTENT}`)).filter((el) => el.offsetParent !== null); } function highlightItem(itemElement) { if (!itemElement) return; if (currentFocusedItem) { currentFocusedItem.classList.remove(HIGHLIGHT_CLASS); } itemElement.classList.add(HIGHLIGHT_CLASS); itemElement.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" }); currentFocusedItem = itemElement; } async function navigateToPage(targetPage) { window.location.href = `/home/read/published/${targetPage}`; return true; } function getCurrentPage() { const pageInput = document.querySelector(PAGINATION_SELECTORS.PAGE_INPUT); return pageInput ? parseInt(pageInput.placeholder, 10) || 1 : 1; } function scrollArticleContent(direction) { const scrollContainer = document.querySelector(MODAL_SELECTORS.CONTENT_SCROLL); if (!scrollContainer) return; const scrollAmount = direction * SCROLL_CONFIG.ARTICLE_SCROLL_AMOUNT; scrollContainer.scrollBy({ top: scrollAmount, behavior: "smooth" }); } async function switchArticle(direction) { const items = getVisibleArticleItems(); if (items.length === 0) return; let currentIndex = currentFocusedItem ? items.indexOf(currentFocusedItem) : -1; let nextIndex = currentIndex + direction; const wasArticleOpen = isArticleModalOpen(); if (wasArticleOpen) { closeArticleModal(); await new Promise((resolve) => setTimeout(resolve, 300)); } if (nextIndex < 0 || nextIndex >= items.length) { const currentPage = getCurrentPage(); if (direction < 0 && currentPage > 1) { await navigateToPage(currentPage - 1); setTimeout(() => { const newItems = getVisibleArticleItems(); if (newItems.length > 0) { highlightItem(newItems[newItems.length - 1]); if (wasArticleOpen) { setTimeout(() => { openFocusedArticle(); }, 100); } } }, 500); } else if (direction > 0) { await navigateToPage(currentPage + 1); setTimeout(() => { const newItems = getVisibleArticleItems(); if (newItems.length > 0) { highlightItem(newItems[0]); if (wasArticleOpen) { setTimeout(() => { openFocusedArticle(); }, 100); } } }, 500); } return; } highlightItem(items[nextIndex]); if (wasArticleOpen) { setTimeout(() => { openFocusedArticle(); }, 100); } } function openFocusedArticle() { if (!currentFocusedItem) return; const clickable = currentFocusedItem.querySelector(LIST_SELECTORS.ITEM_CLICKABLE); if (clickable) { clickable.click(); } } function isEditDrawerOpen() { const drawerBody = document.querySelector(EDIT_DRAWER_SELECTORS.DRAWER); if (!drawerBody) return false; const drawerDialog = drawerBody.closest('div[role="dialog"]'); if (!drawerDialog) return true; const computedStyle = window.getComputedStyle(drawerDialog); return computedStyle.opacity === "1" && computedStyle.display !== "none"; } function openEditDrawer() { if (isEditDrawerOpen()) return false; if (!isArticleModalOpen()) return false; const itemDetailModal = document.querySelector( 'div[role="dialog"][aria-modal="true"]:not([class*="Drawer"]) header ~ section' ); if (!itemDetailModal) return false; const tagPlusButtonIcon = itemDetailModal.querySelector(EDIT_DRAWER_SELECTORS.EDIT_BUTTON); if (tagPlusButtonIcon) { const tagPlusButton = tagPlusButtonIcon.closest("button"); if (tagPlusButton) { tagPlusButton.click(); return true; } } return false; } function getCollectionCheckboxes() { if (!isEditDrawerOpen()) return []; const drawerBody = document.querySelector(EDIT_DRAWER_SELECTORS.DRAWER); if (!drawerBody) return []; const checkboxes = drawerBody.querySelectorAll(EDIT_DRAWER_SELECTORS.COLLECTION_CHECKBOX); return Array.from(checkboxes).map((checkbox, index) => { const titleElement = checkbox.querySelector("h5.mantine-Title-root"); const title = titleElement ? titleElement.textContent.trim() : `未知标题 ${index + 1}`; const isChecked = checkbox.getAttribute("aria-checked") === "true"; return { element: checkbox, title, isChecked, index }; }); } function toggleCollectionItem(index) { const collections = getCollectionCheckboxes(); if (index >= 0 && index < collections.length) { collections[index].element.click(); return true; } return false; } function saveEditDrawer() { if (!isEditDrawerOpen()) return false; const drawerBody = document.querySelector(EDIT_DRAWER_SELECTORS.DRAWER); if (!drawerBody) return false; const saveButton = drawerBody.querySelector(EDIT_DRAWER_SELECTORS.SAVE_BUTTON); if (saveButton) { saveButton.click(); return true; } return false; } function initKeyboardNavigation() { const style = document.createElement("style"); style.textContent = ` .${HIGHLIGHT_CLASS} { outline: 3px solid #FF8C00 !important; box-shadow: 0 0 8px #FF8C00 !important; border-radius: 4px; } `; document.head.appendChild(style); document.addEventListener("keydown", async (event) => { if (event.target.tagName === "INPUT" || event.target.tagName === "TEXTAREA" || event.target.isContentEditable) { return; } const key = event.key.toLowerCase(); if (isEditDrawerOpen() && /^[1-9]$/.test(key)) { event.preventDefault(); const index = parseInt(key) - 1; toggleCollectionItem(index); return; } switch (key) { case "w": if (isArticleModalOpen()) { event.preventDefault(); scrollArticleContent(-1); } break; case "s": if (isArticleModalOpen()) { event.preventDefault(); scrollArticleContent(1); } break; case "a": event.preventDefault(); await switchArticle(-1); break; case "d": event.preventDefault(); await switchArticle(1); break; case "f": event.preventDefault(); openFocusedArticle(); break; case "q": event.preventDefault(); if (isArticleModalOpen()) { closeArticleModal(); } else { const currentPage = getCurrentPage(); if (currentPage > 1) { await navigateToPage(currentPage - 1); } } break; case "e": event.preventDefault(); await navigateToPage(getCurrentPage() + 1); break; case "r": event.preventDefault(); if (isArticleModalOpen()) { if (isEditDrawerOpen()) { saveEditDrawer(); } else { openEditDrawer(); } } break; } }); setTimeout(() => { const items = getVisibleArticleItems(); if (items.length > 0) { highlightItem(items[0]); } }, 500); } var _GM_registerMenuCommand = /* @__PURE__ */ (() => typeof GM_registerMenuCommand != "undefined" ? GM_registerMenuCommand : void 0)(); const FILTER_COUNT_KEY = "sooon_filter_count"; const STORAGE_KEY = "sooon_page_state"; const IGNORE_SET_KEY = "sooon_ignore_set"; let hasRestoredPage = false; let isProcessing = false; let lastProcessedPath = null; let saveStateTimeout = null; const SELECTORS = { PAGINATION: { CONTAINER: "#root > div > div > div > main > div.flex-1.flex.flex-col.overflow-hidden > div > div.flex-1.flex.flex-col.overflow-hidden > div > div.flex.items-center.justify-center.px-2.pt-1.pb-1 > div", SORT_BUTTON: "#root > div > div > div > main > div.flex-1.flex.flex-col.overflow-hidden > div > div.flex-1.flex.flex-col.overflow-hidden > div > div.flex.items-center.justify-center.px-2.pt-1.pb-1 > div > button:nth-child(1)" }, ARTICLE: { LIST: "#root > div > div > div > main > div.flex-1.flex.flex.flex-col.overflow-hidden > div > div.flex-1.flex.flex-col.overflow-hidden > div > div.flex-1.flex.flex-col.overflow-hidden > div > div > div > div._children_whrto_2.flex-1 > div > div:nth-child(1) > div", ITEM: "div.w-full", STATS_CONTAINER: "#root > div > div > div > div > div > div:nth-child(1)" } }; const getIgnoreSet = () => { const stored = ls.get(IGNORE_SET_KEY); return stored ? new Set(stored) : /* @__PURE__ */ new Set(); }; const saveIgnoreSet = (set) => { ls.set(IGNORE_SET_KEY, Array.from(set)); }; const addToIgnoreSet = (articleId) => { const ignoreSet = getIgnoreSet(); ignoreSet.add(articleId); saveIgnoreSet(ignoreSet); }; const getFilterCount = () => { const stored = ls.get(FILTER_COUNT_KEY); return stored !== null ? parseInt(stored, 10) : 1; }; const setFilterCount = (count) => { if (count >= 0) { ls.set(FILTER_COUNT_KEY, count); return true; } return false; }; const registerMenuCommands = () => { _GM_registerMenuCommand("🔍 设置过滤阈值 (FC)", () => { const currentCount = getFilterCount(); const newCount = prompt("请输入新的筛选阈值(0或更大的数字):\n设置为0表示暂停过滤", currentCount); if (newCount !== null) { const parsedCount = parseInt(newCount, 10); if (!isNaN(parsedCount) && parsedCount >= 0) { if (setFilterCount(parsedCount)) { filterArticlesByReadCount(); } } else { alert("请输入有效的数字(0或更大)"); } } }); }; const updateReadProgress = async (totalItems, ignoredCount) => { const container = await waitForElement(SELECTORS.ARTICLE.STATS_CONTAINER); if (!container) return; let progressButton = container.querySelector(".read-progress-button"); if (!progressButton) { progressButton = document.createElement("button"); progressButton.className = "mantine-focus-never mantine-active px-0 m_77c9d27d mantine-Button-root m_87cf2631 mantine-UnstyledButton-root read-progress-button"; progressButton.setAttribute("data-variant", "transparent"); progressButton.setAttribute("type", "button"); progressButton.style.cssText = "--button-bg: transparent; --button-hover: transparent; --button-color: var(--mantine-color-primary-light-color); --button-bd: calc(0.0625rem * var(--mantine-scale)) solid transparent;"; const percentage = (ignoredCount / totalItems * 100).toFixed(1); progressButton.innerHTML = ` <span class="m_80f1301b mantine-Button-inner"> <span class="m_811560b9 mantine-Button-label"> <div class="flex items-center gap-1 text-$mantine-primary-color-light-color"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="tabler-icon tabler-icon-eye w-6 h-6"> <path d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0 -4 0"></path> <path d="M21 12c-2.4 4 -5.4 6 -9 6c-3.6 0 -6.6 -2 -9 -6c2.4 -4 5.4 -6 9 -6c3.6 0 6.6 2 9 6"></path> </svg> <div class="font-semibold text-lg progress-text"> <span>${percentage}%</span> </div> </div> </span> </span> `; progressButton.addEventListener("mouseenter", () => { const textDiv = progressButton.querySelector(".progress-text"); if (textDiv) { textDiv.innerHTML = `<span>${ignoredCount}/${totalItems}</span>`; } }); progressButton.addEventListener("mouseleave", () => { const textDiv = progressButton.querySelector(".progress-text"); if (textDiv) { textDiv.innerHTML = `<span>${percentage}%</span>`; } }); const targetButton = container.querySelector("button:nth-child(5)"); if (targetButton) { container.insertBefore(progressButton, targetButton); } else { container.appendChild(progressButton); } } else { const percentage = (ignoredCount / totalItems * 100).toFixed(1); const textDiv = progressButton.querySelector(".progress-text"); if (textDiv) { textDiv.innerHTML = `<span>${percentage}%</span>`; } const existingEnter = progressButton._mouseenterHandler; const existingLeave = progressButton._mouseleaveHandler; if (existingEnter) { progressButton.removeEventListener("mouseenter", existingEnter); } if (existingLeave) { progressButton.removeEventListener("mouseleave", existingLeave); } const enterHandler = () => { if (textDiv) { textDiv.innerHTML = `<span>${ignoredCount}/${totalItems}</span>`; } }; const leaveHandler = () => { if (textDiv) { textDiv.innerHTML = `<span>${percentage}%</span>`; } }; progressButton.addEventListener("mouseenter", enterHandler); progressButton.addEventListener("mouseleave", leaveHandler); progressButton._mouseenterHandler = enterHandler; progressButton._mouseleaveHandler = leaveHandler; } }; const getPage = () => { const page = Number(window.location.pathname.split("/").pop()) || 1; log(`[Page Detection] Current page number: ${page}`); return page; }; const getPageParam = async () => { log("[Page Parameters] Starting to fetch page parameters..."); const bottomBarSelector = SELECTORS.PAGINATION.CONTAINER; const page = getPage(); log("[Page Parameters] Waiting for page size element..."); const pageSizeSelector = bottomBarSelector + " > button:nth-child(3) > span"; const pageSize = Number((await waitForElement(pageSizeSelector)).textContent); log(`[Page Parameters] Page size detected: ${pageSize}`); log("[Page Parameters] Checking sort order..."); const newAtFirstSelector = bottomBarSelector + " > button:nth-child(1) > span > svg"; const newAtFirst = (await waitForElement(newAtFirstSelector)).classList.contains("tabler-icon-sort-descending"); log(`[Page Parameters] Sort order - New items first: ${newAtFirst}`); log("[Page Parameters] Getting total page count..."); const allPageSelector = bottomBarSelector + " > div > div > button:nth-child(3) > div"; const allPage = Number((await waitForElement(allPageSelector)).textContent); log(`[Page Parameters] Total pages: ${allPage}`); const params = { page, pageSize, newAtFirst, allPage }; log("[Page Parameters] Complete parameters:", params); return params; }; const loadStoredPage = () => { log("[Storage] Loading stored page state..."); const defaultState = { page: 1, pageSize: 20, newAtFirst: true }; const storedState = ls.get(STORAGE_KEY); if (storedState) { log("[Storage] Found stored state:", storedState); } else { log("[Storage] No stored state found, using defaults:", defaultState); } return storedState || defaultState; }; const debouncedSavePageState = (state) => { if (saveStateTimeout) { clearTimeout(saveStateTimeout); } saveStateTimeout = setTimeout(() => { log("[Storage] Saving page state (debounced):", state); ls.set(STORAGE_KEY, state); }, 300); }; const filterArticlesByReadCount = async () => { log(`[Filter] Starting to observe articles with read count >= ${getFilterCount()}`); const articleList = await waitForElement(SELECTORS.ARTICLE.LIST); if (!articleList) { warn("[Filter] Cannot find article list container"); return false; } let visibleCount = 0; let hiddenCount = 0; let unreadCount = 0; const processEyeIcon = (icon) => { const item = icon.closest(SELECTORS.ARTICLE.ITEM); if (!item) return; const titleElement = item.querySelector(".font-semibold"); const contentElement = item.querySelector("._text_1alq7_1"); const articleContent = `${(titleElement == null ? void 0 : titleElement.textContent) || ""} ${(contentElement == null ? void 0 : contentElement.textContent) || ""}`; const iconParent = icon.closest(".flex.items-center.gap-1"); if (!iconParent) return; const readCountText = iconParent.textContent.trim(); const readCount = parseInt(readCountText, 10); if (isNaN(readCount)) { warn("[Filter] Could not find valid read count"); visibleCount++; return; } const filterCount = getFilterCount(); if (filterCount === 0) { item.style.display = ""; visibleCount++; log(`[Filter] FC=0, showing all articles`); } else if (readCount >= filterCount) { item.style.display = "none"; hiddenCount++; if (articleContent) { addToIgnoreSet(articleContent); } log(`[Filter] Removing article with read count ${readCount}`); } else { item.style.display = ""; visibleCount++; log(`[Filter] Keeping article with read count ${readCount}`); } getPageParam().then((pageParams) => { const totalItems = pageParams.pageSize * pageParams.allPage; const ignoreSet = getIgnoreSet(); updateReadProgress(totalItems, ignoreSet.size); }); }; const observer2 = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.type === "childList") { mutation.addedNodes.forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { if (node.matches && node.matches("svg.tabler-icon-eye")) { processEyeIcon(node); } const icons = node.querySelectorAll("svg.tabler-icon-eye"); icons.forEach(processEyeIcon); } }); } } }); const observerConfig = { childList: true, subtree: true, attributes: false, characterData: false }; observer2.observe(articleList, observerConfig); const existingIcons = articleList.querySelectorAll("svg.tabler-icon-eye"); existingIcons.forEach(processEyeIcon); log(`[Filter] Observer setup complete. Initial results - Visible: ${visibleCount}, Hidden: ${hiddenCount}, Unread: ${unreadCount}`); return true; }; const processPage = async () => { const currentPath = window.location.pathname; if (isProcessing && currentPath !== lastProcessedPath) { log("[Process] Path changed during processing, resetting state..."); isProcessing = false; } if (isProcessing) { log("[Process] Already processing, skipping..."); return; } isProcessing = true; lastProcessedPath = currentPath; try { if (currentPath.match(/^\/home\/read\/published\/\d+$/)) { log("[Process] Processing numbered page..."); const pageParam = await getPageParam(); debouncedSavePageState(pageParam); await filterArticlesByReadCount(); } else if (currentPath === "/home/read/published" || currentPath === "/home/read/published/") { log("[Process] Processing root path..."); const storedState = loadStoredPage(); if (!hasRestoredPage && storedState.page > 1) { log(`[Process] Restoring to stored page ${storedState.page}`); hasRestoredPage = true; window.location.href = `/home/read/published/${storedState.page}`; return; } } } catch (error) { warn("[Process] Error during processing:", error); } finally { isProcessing = false; } }; document.addEventListener("visibilitychange", () => { if (document.visibilityState === "visible") { log("[Visibility] Page becoming visible, checking state..."); const currentPath = window.location.pathname; const match = currentPath.match(/^\/home\/read\/published\/(\d+)$/); const currentPage = match ? parseInt(match[1], 10) : 1; const storedState = loadStoredPage(); if (currentPage !== storedState.page) { hasRestoredPage = false; processPage(); } } }); let lastPath = window.location.pathname; const observer = new MutationObserver(() => { const currentPath = window.location.pathname; if (currentPath !== lastPath) { log(`[Route] Path changed from ${lastPath} to ${currentPath}`); lastPath = currentPath; isProcessing = false; processPage(); } }); observer.observe(document.body, { childList: true, subtree: true }); const setupSortButtonListener = async () => { const sortButton = await waitForElement(SELECTORS.PAGINATION.SORT_BUTTON); if (!sortButton) { warn("[Sort] Sort button not found"); return; } log("[Sort] Setting up sort button click listener"); sortButton.addEventListener("click", () => { log("[Sort] Sort button clicked, triggering filter"); setTimeout(() => { filterArticlesByReadCount(); }, 500); }); }; const initialize = async () => { await setupSortButtonListener(); registerMenuCommands(); processPage(); initKeyboardNavigation(); }; getFilterCount(); initialize(); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址