// ==UserScript== // @name PKU-treeHole优化脚本 // @author PKUer // @namespace http://tampermonkey.net/ // @version 2.0.2 // @license GPL-3.0 License // @description 优化PKU-treeHole的使用体验,具体功能见https://greasyfork.org/zh-CN/scripts/464053-pku-treehole优化脚本 // @match https://treehole.pku.edu.cn/web/* // @icon https://www.google.com/s2/favicons?sz=64&domain=pku.edu.cn // @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js // @grant unsafeWindow // @grant GM_setClipboard // @grant GM_getValue // @grant GM_setValue // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_notification // @grant GM_openInTab // @grant GM_xmlhttpRequest // @grant GM_addStyle // @connect cn.bing.com // @connect wallhaven.cc // @run-at document-end // @downloadURL https://update.greasyfork.icu/scripts/464053/PKU-treeHole%E4%BC%98%E5%8C%96%E8%84%9A%E6%9C%AC.user.js // @updateURL https://update.greasyfork.icu/scripts/464053/PKU-treeHole%E4%BC%98%E5%8C%96%E8%84%9A%E6%9C%AC.meta.js // ==/UserScript== // 部分代码借鉴了项目https://greasyfork.org/zh-CN/scripts/419081-知乎增强 ,因此基于GPL-3.0 License进行开源 // 壁纸刷新小风车图片来自 https://infinityicon.infinitynewtab.com/assets/windmill.svg console.log('PKUtreeHole优化脚本来啦!'); //================================================================================================================ // 显示脚本菜单 start (前800行) //================================================================================================================ /** 配置 */ const Config = { copyFullText: { ID: 'menu_copyFullText', text: '复制全文', value: GM_getValue('menu_copyFullText', true), shownInMainMenu: true, hasSetPage: true }, showCopyFullTextAlert: { ID: 'menu_showCopyFullTextAlert', text: '复制长文成功时显示提醒框', value: GM_getValue('menu_showCopyFullTextAlert', true) }, cancelMaxHeight: { ID: 'menu_cancelMaxHeight', text: '取消长文限制', value: GM_getValue('menu_cancelMaxHeight', true), shownInMainMenu: true, hasSetPage: false }, configureWallPaperHub: { ID: 'menu_configureWallPaperHub', text: '配置壁纸库', value: GM_getValue('menu_configureWallPaperHub', true), shownInMainMenu: true, hasSetPage: true }, wallPaperHubSite: { ID: 'menu_wallPaperHubSite', text: '壁纸仓库', value: GM_getValue('menu_wallPaperHubSite', 'Bing') }, WallHavenUrl: { ID: 'menu_WallHavenUrl', text: 'WallHaven壁纸分类地址', value: GM_getValue('menu_WallHavenUrl', 'https://wallhaven.cc/search?categories=100&purity=100&sorting=toplist&order=desc&ai_art_filter=1'), tips: '填写WallHaven对应类别的网址' }, addSiteButton: { ID: 'menu_addSiteButton', text: '添加网站图标', value: GM_getValue('menu_addSiteButton', true), shownInMainMenu: true, hasSetPage: true }, addCourseEvaluationSiteButton: { ID: 'menu_addCourseEvaluationSiteButton', text: '添加课程评估网站图标', value: GM_getValue('menu_addCourseEvaluationSiteButton', true) }, addBBSSiteButton: { ID: 'menu_addBBSSiteButton', text: '添加未名BBS网站按钮', value: GM_getValue('menu_addBBSSiteButton', true) }, recordAlias: { ID: 'menu_recordAlias', text: '收藏树洞', value: GM_getValue('menu_recordAlias', true), shownInMainMenu: true, hasSetPage: true }, showAliasSuggestions: { ID: 'menu_showAliasSuggestions', text: '在搜索时触发收藏建议', value: GM_getValue('menu_showAliasSuggestions', true) }, recordHistory: { ID: 'menu_recordHistory', text: '保存搜索记录(关闭该功能会同时清除搜索记录)', value: GM_getValue('menu_recordHistory', true), shownInMainMenu: true, hasSetPage: true }, showHistorySuggestions: { ID: 'menu_showHistorySuggestions', text: '在搜索时触发历史记录建议', value: GM_getValue('menu_showHistorySuggestions', true) }, maxHistorySize: { ID: 'menu_maxHistorySize', text: '最大历史记录数量', value: parseInt(GM_getValue('menu_maxHistorySize', '200')), tips: '默认200,建议100~500' }, maxShownHistorySize: { ID: 'menu_maxShownHistorySize', text: '最大展示历史记录数量', value: parseInt(GM_getValue('menu_maxShownHistorySize', '10')), tips: '默认10,建议5~20' }, blockKeywords: { ID: 'menu_blockKeywords', text: '屏蔽指定关键词', value: GM_getValue('menu_blockKeywords', true), shownInMainMenu: true, hasSetPage: true }, customBlockKeywords: { ID: 'menu_customBlockKeywords', text: '自定义屏蔽关键词', value: GM_getValue('menu_customBlockKeywords', []), tips: "关键词不分大小写,使用 '|' 分隔" }, showLastestReplyTime: { ID: 'menu_showLastestReplyTime', text: '显示最近回复时间', value: GM_getValue('menu_showLastestReplyTime', true), shownInMainMenu: true, hasSetPage: false }, clickAgreeServiceAgreement: { ID: 'menu_clickAgreeServiceAgreement', text: '点击“同意北大树洞服务协议”选中框', value: GM_getValue('menu_clickAgreeServiceAgreement', false), shownInMainMenu: true, hasSetPage: false }, openTheFuckingURLRightNow: { ID: 'menu_openTheFuckingURLRightNow', text: '点击其它链接时,关闭提醒,自动跳转外链', value: GM_getValue('menu_openTheFuckingURLRightNow', false), shownInMainMenu: true, hasSetPage: false }, } const wallPaperHub = { Bing: "https://cn.bing.com", WallHaven: "https://wallhaven.cc/" } // 注册脚本菜单 function registerMenuCommand() { GM_registerMenuCommand('功能菜单', raiseMenu) GM_registerMenuCommand('💬 反馈 & 建议', function () { window.GM_openInTab('https://greasyfork.org/zh-CN/scripts/460666-pku-treehole%E4%BC%98%E5%8C%96%E8%84%9A%E6%9C%AC/feedback', { active: true, insert: true, setParent: true }) }) } /** 唤起设置UI */ function raiseMenu() { if (document.querySelector("#zhmMenu")) return /* Safari兼容 */ GM_addStyle(menuSetStyle) const root_body = document.querySelector('body') const zhmMenu = document.createElement('div') zhmMenu.id = 'zhmMenu' zhmMenu.innerHTML = sidebar_menu_html + main_menu_html root_body.appendChild(zhmMenu) addcustomEvents() } /** 给UI绑定各种点击事件 */ function addcustomEvents() { /* 给所有开关型的标签绑定事件,包括主页面和侧边页面(侧边栏) */ const circular = document.querySelectorAll('.circular') circular.forEach(function (item) { item.addEventListener('click', function (_e) { const buttonStyle = item.children[0].style // 判断按钮开关情况 let left = buttonStyle.left left = parseInt(left) let listLeftValue if (left == 0) { // 点击之前按钮是关闭状态,改成打开 buttonStyle.left = '22px' buttonStyle.background = '#fe6d73' item.style.background = '#ffE5E5' if (item.nextElementSibling && item.nextElementSibling.getAttribute('data')) { // 如果是有右侧栏的按钮,右侧栏开放 item.nextElementSibling.setAttribute('style', 'border: solid #ccc;border-width: 0 3px 3px 0;') } listLeftValue = true } else { // 点击之前按钮是打开状态,改成关闭 buttonStyle.left = '0px' buttonStyle.background = '#fff' item.style.background = '#fff' if (item.nextElementSibling) { // 如果有右侧栏,则关闭右侧栏 item.nextElementSibling.setAttribute('style', 'border: solid #EEE;border-width: 0 3px 3px 0;') } listLeftValue = false } const setListID = item.id // 去除Enter后缀 GM_setValue(setListID, listLeftValue) // 将开关状态回写 }) }) /* 给所有的右箭头标签绑定事件 */ const toRight = document.querySelectorAll('.to-right') toRight.forEach(function (item) { item.addEventListener('click', function (e) { let left = item.previousElementSibling.children[0].style.left // 忽略文本节点,找到.circular节点 left = parseInt(left) // 根据按钮开关情况选择是否给右箭头绑定事件 if (left != 22) return const setPageID = item.getAttribute('data') const pageId = document.getElementById(setPageID) if (pageId) pageId.className = 'iconSetPage toLeftMove' // 进入右侧侧边栏 }) }) /* 给所有的左上角返回键标签绑定事件 */ const toBack = document.querySelectorAll('.back') toBack.forEach(function (item) { item.addEventListener('click', function (e) { const parentDom = item.parentNode.parentNode.parentNode parentDom.className = 'iconSetPage toRightMove' // document.querySelector('#zhmTakePlace').style = 'height:40px;' }) }) /* 给所有的关闭按钮标签绑定事件 */ const setSave = document.querySelectorAll('.iconSetSave') setSave.forEach(function (item) { item.addEventListener('click', () => { let _customBlockKeywords = document.getElementById(Config.customBlockKeywords.ID).value _customBlockKeywords = _customBlockKeywords.length ? _customBlockKeywords.split("|") : [] // 判断屏蔽关键词是否为空 let _wallPaperHubSite = document.getElementById(Config.wallPaperHubSite.ID).value let _wallHavenUrl = document.getElementById(Config.WallHavenUrl.ID).value let _maxHistorySize = document.getElementById(Config.maxHistorySize.ID).value let _maxShownHistorySize = document.getElementById(Config.maxShownHistorySize.ID).value GM_setValue(Config.customBlockKeywords.ID, _customBlockKeywords) // 保存屏蔽的关键词 GM_setValue(Config.wallPaperHubSite.ID, _wallPaperHubSite) // 保存壁纸选项 GM_setValue(Config.WallHavenUrl.ID, _wallHavenUrl) // 保存壁纸类别URL GM_setValue(Config.maxHistorySize.ID, _maxHistorySize) // 保存最大历史记录数量 GM_setValue(Config.maxShownHistorySize.ID, _maxShownHistorySize) // 保存最大显示历史记录数量 history.go(0) }) }) } /**侧边栏HTML */ let sidebar_menu_html = `
` /** 主界面UI的HTML */ let main_menu_html = `
` /** UI的CSS样式 */ var menuSetStyle = ` .zhmMask{ z-index:999999999; background-color:#000; position: fixed;top: 0;right: 0;bottom: 0;left: 0; opacity:0.8; } .wrap-box{ z-index:1000000000; position:fixed;;top: 40%;left: 50%;transform: translate(-50%, -200px); width: 300px; color: #555; background-color: #fff; border-radius: 5px; overflow:hidden; font:16px numFont,PingFangSC-Regular,Tahoma,Microsoft Yahei,sans-serif !important; font-weight:400 !important; } .setWrapHead{ background-color:#f24443;height:40px;color:#fff;text-align:center;line-height:40px; } .setWrapLi{ margin:0px;padding:0px; } .setWrapLi li{ background-color: #fff; border-bottom:1px solid #eee; margin:0px !important; padding:12px 20px; display: flex; justify-content: space-between;align-items: center; list-style: none; } .setWrapLiContent{ display: flex;justify-content: space-between;align-items: center; } .setWrapSave{ position:absolute;top:-2px;right:10px;font-size:24px;cursor:pointer } .iconSetFoot{ position:absolute;bottom:0px;padding:10px 20px;width:100%; z-index:1000000009;background:#fef9ef; } .iconSetFootLi{ margin:0px;padding:0px; } .iconSetFootLi li{ display: inline-flex; padding:0px 2px; justify-content: space-between;align-items: center; font-size: 12px; } .iconSetFootLi li a{ color:#555; } .iconSetFootLi a:hover { color:#fe6d73; } .iconSetPage{ z-index:1000000001; position:absolute;top:0px;left:300px; background:#fff; width:300px; height:100%; } .iconSetUlHead{ padding:0px; margin:0px; } .iconSetPageHead{ border-bottom:1px solid #ccc; height:40px; line-height:40px; display: flex; justify-content: space-between; align-items: center; background-color:#fe6d73; color:#fff; font-size: 15px; } .iconSetPageLi{ margin:0px;padding:0px; } .iconSetPageLi li{ list-style: none; padding:8px 20px; border-bottom:1px solid #eee; } .zhihuSetPage{ z-index:1000000002;position:absolute;top:0px;left:300px;background:#fff;width:300px;height:100%; } .iconSetPageInput{ display: flex !important;justify-content: space-between;align-items: center; } .zhihuSetPageLi{ margin:0px;padding:0px; height:258px; overflow-y: scroll; } .zhihuSetPageContent{ display: flex !important;justify-content: space-between;align-items: center; } .circular{ width: 40px;height: 20px;border-radius: 16px;transition: .3s;cursor: pointer;box-shadow: 0 0 3px #999 inset; } .round-button{ width: 20px;height: 20px;;border-radius: 50%;box-shadow: 0 1px 5px rgba(0,0,0,.5);transition: .3s;position: relative; } .back{ border: solid #FFF; border-width: 0 3px 3px 0; display: inline-block; padding: 3px;transform: rotate(135deg); -webkit-transform: rotate(135deg);margin-left:10px;cursor:pointer; } .to-right{ margin-left:20px; display: inline-block; padding: 3px;transform: rotate(-45deg); -webkit-transform: rotate(-45deg);cursor:pointer; } .iconSetSave{ font-size:24px;cursor:pointer;margin-right:5px;margin-bottom:4px;color:#FFF; } .zhm_set_page{ z-index:1000000003; position:absolute; top:0px;left:300px; background:#fff; width:300px; height:100%; } .zhm_set_page_header{ border-bottom:1px solid #ccc; height:40px; line-height:40px; display: flex; justify-content: space-between; align-items: center; background-color:#fe6d73; color:#fff; font-size: 15px; } .zhm_set_page_content{ display: flex !important;justify-content: space-between;align-items: center; } .zhm_set_page_list{ margin:0px;padding:0px; height: 220px; overflow-y: scroll; } .zhm_set_page_list::-webkit-scrollbar { /*滚动条整体样式*/ width : 0px; /*高宽分别对应横竖滚动条的尺寸*/ height: 1px; } .zhm_set_page_list::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ border-radius : 2px; background-color: #fe6d73; } .zhm_set_page_list::-webkit-scrollbar-track { /*滚动条里面轨道*/ box-shadow : inset 0 0 5px rgba(0, 0, 0, 0.2); background : #ededed; border-radius: 10px; } .zhm_set_page_list li{ /*border-bottom:1px solid #ccc;*/ padding:12px 20px; display:block; border-bottom:1px solid #eee; } li:last-child{ border-bottom:none; } .zhm_scroll{ overflow-y: scroll !important; } .zhm_scroll::-webkit-scrollbar { /*滚动条整体样式*/ width : 0px; /*高宽分别对应横竖滚动条的尺寸*/ height: 1px; } .zhm_scroll::-webkit-scrollbar-thumb { /*滚动条里面小方块*/ border-radius : 2px; background-color: #fe6d73; } .zhm_scroll::-webkit-scrollbar-track { /*滚动条里面轨道*/ box-shadow : inset 0 0 5px rgba(0, 0, 0, 0.2); background : #ededed; border-radius: 10px; } /*-form-*/ :root { --base-color: #434a56; --white-color-primary: #f7f8f8; --white-color-secondary: #fefefe; --gray-color-primary: #c2c2c2; --gray-color-secondary: #c2c2c2; --gray-color-tertiary: #676f79; --active-color: #227c9d; --valid-color: #c2c2c2; --invalid-color: #f72f47; --invalid-icon: url("data:image/svg+xml;charset=utf8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%20viewBox%3D%220%200%2024%2024%22%3E%20%3Cpath%20d%3D%22M13.41%2012l4.3-4.29a1%201%200%201%200-1.42-1.42L12%2010.59l-4.29-4.3a1%201%200%200%200-1.42%201.42l4.3%204.29-4.3%204.29a1%201%200%200%200%200%201.42%201%201%200%200%200%201.42%200l4.29-4.3%204.29%204.3a1%201%200%200%200%201.42%200%201%201%200%200%200%200-1.42z%22%20fill%3D%22%23f72f47%22%20%2F%3E%3C%2Fsvg%3E"); } .text-input { font-size: 16px; position: relative; right:0px; z-index: 0; } .text-input__body { -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: transparent; border: 1px solid var(--gray-color-primary); border-radius: 3px; height: 1.7em; line-height: 1.7; overflow: hidden; padding: 2px 1em; text-overflow: ellipsis; transition: background-color 0.3s; width:55%; font-size:14px; } .text-input__body:-ms-input-placeholder { color: var(--gray-color-secondary); } .text-input__body::-moz-placeholder { color: var(--gray-color-secondary); } .text-input__body::placeholder { color: var(--gray-color-secondary); } *, ::after, ::before { box-sizing: initial !important; } .text-input__body[data-is-valid] { padding-right: 1em; } .text-input__body[data-is-valid=true] { border-color: var(--valid-color); } .text-input__body[data-is-valid=false] { border-color: var(--invalid-color); box-shadow: inset 0 0 0 1px var(--invalid-color); } .text-input__body:focus { border-color: var(--active-color); box-shadow: inset 0 0 0 1px var(--active-color); outline: none; } .text-input__body:-webkit-autofill { transition-delay: 9999s; -webkit-transition-property: background-color; transition-property: background-color; } .text-input__validator { background-position: right 0.5em center; background-repeat: no-repeat; background-size: 1.5em; display: inline-block; height: 100%; left: 0; position: absolute; top: 0; width: 100%; z-index: -1; } .text-input__body[data-is-valid=false] + .text-input__validator { background-image: var(--invalid-icon); } .select-box { box-sizing: inherit; font-size: 16px; position: relative; transition: background-color 0.5s ease-out; width:90px; } .select-box::after { border-color: var(--gray-color-secondary) transparent transparent transparent; border-style: solid; border-width: 6px 4px 0; bottom: 0; content: ""; display: inline-block; height: 0; margin: auto 0; pointer-events: none; position: absolute; right: -72px; top: 0; width: 0; z-index: 1; } .select-box__body { box-sizing: inherit; -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: transparent; border: 1px solid var(--gray-color-primary); border-radius: 3px; cursor: pointer; height: 1.7em; line-height: 1.7; padding-left: 1em; padding-right: calc(1em + 16px); width: 140%; font-size:14px; padding-top:2px; padding-bottom:2px; } .select-box__body[data-is-valid=true] { border-color: var(--valid-color); box-shadow: inset 0 0 0 1px var(--valid-color); } .select-box__body[data-is-valid=false] { border-color: var(--invalid-color); box-shadow: inset 0 0 0 1px var(--invalid-color); } .select-box__body.focus-visible { border-color: var(--active-color); box-shadow: inset 0 0 0 1px var(--active-color); outline: none; } .select-box__body:-webkit-autofill { transition-delay: 9999s; -webkit-transition-property: background-color; transition-property: background-color; } .textarea__body { -webkit-appearance: none; -moz-appearance: none; appearance: none; background-color: transparent; border: 1px solid var(--gray-color-primary); border-radius: 0; box-sizing: border-box; font: inherit; left: 0; letter-spacing: inherit; overflow: hidden; padding: 1em; position: absolute; resize: none; top: 0; transition: background-color 0.5s ease-out; width: 100%; } .textarea__body:only-child { position: relative; resize: vertical; } .textarea__body:focus { border-color: var(--active-color); box-shadow: inset 0 0 0 1px var(--active-color); outline: none; } .textarea__body[data-is-valid=true] { border-color: var(--valid-color); box-shadow: inset 0 0 0 1px var(--valid-color); } .textarea__body[data-is-valid=false] { border-color: var(--invalid-color); box-shadow: inset 0 0 0 1px var(--invalid-color); } .textarea ._dummy-box { border: 1px solid; box-sizing: border-box; min-height: 240px; overflow: hidden; overflow-wrap: break-word; padding: 1em; visibility: hidden; white-space: pre-wrap; word-wrap: break-word; } .toLeftMove{ nimation:moveToLeft 0.5s infinite; -webkit-animation:moveToLeft 0.5s infinite; /*Safari and Chrome*/ animation-iteration-count:1; animation-fill-mode: forwards; } @keyframes moveToLeft{ from {left:200px;} to {left:0px;} } @-webkit-keyframes moveToLeft /*Safari and Chrome*/{ from {left:200px;} to {left:0px;} } .toRightMove{ nimation:moveToRight 2s infinite; -webkit-animation:moveToRight 2s infinite; /*Safari and Chrome*/ animation-iteration-count:1; animation-fill-mode: forwards; } @keyframes moveToRight{ from {left:0px;} to {left:2000px;} } @-webkit-keyframes moveToRight /*Safari and Chrome*/{ from {left:0px;} to {left:2000px;} } `; /** 注册功能开关 */ registerMenuCommand() //================================================================================================================ // 显示脚本菜单 end //================================================================================================================ //================================================================================================================ // 功能函数,用于对付树洞版本更新带来的节点属性data-v-xxxx的改变 start //================================================================================================================ /** * 返回节点的 data-v-xxxx 属性名称 * @param {Node} Node * @return {String|undefined} attribute * 由于树洞在更新版本之后,会将节点属性data-v-xxxx作更新,对应的css属性也会更新。为了更好的兼容后续版本,需要将对应节点的data-v也作实时更新 */ function getDataVersionByNode(Node) { if (Node.hasAttributes()) { const attributes_names_array = Node.getAttributeNames(); for (var i = 0; i < attributes_names_array.length; ++i) { if (attributes_names_array[i].startsWith("data-v-")) { return attributes_names_array[i] } } } return undefined } /** 将cookie转换成对象 */ function _getCookieObj() { var cookieObj = {}; var cookieStr = document.cookie; var pairList = cookieStr.split(';'); for (var _i = 0, pairList_1 = pairList; _i < pairList_1.length; _i++) { var pair = pairList_1[_i]; var _a = pair.trim().split('='), key = _a[0], value = _a[1]; cookieObj[key] = value; } return cookieObj; } //================================================================================================================ // 功能函数,用于对付树洞版本更新带来的节点属性data-v-xxxx的改变 end //================================================================================================================ //================================================================================================================ // 取消最大高度限制 start //================================================================================================================ function cancelMaxHeight() { if (!Config.cancelMaxHeight.value) return console.log('取消最大高度限制') document.styleSheets[0].insertRule('.left-container .sidebar .box-content { max-height: none !important }', 0) } //================================================================================================================ // 取消最大高度限制 end //================================================================================================================ //================================================================================================================ // 添加“复制全文”标签 start //================================================================================================================ // 添加“复制全文”css图标 function addCopyFullTextButtonIcon() { if (!Config.copyFullText.value) return // 判断是否为夜晚模式 var dark_mode = document.querySelector("body").getAttribute("style").indexOf("--theme_bgc_color:rgba(31,31,31,0.8); --theme_font_color:#ededed;") > -1; GM_addStyle(`div.box-header.box-header-top-icon{ overflow: visible; }`) // 这一步是为了“复制全文”标签超出header框之后仍然能显示 GM_addStyle(` div:nth-child(3) > div.box-header.box-header-top-icon > code:hover:before { content: "复制全文"; position: relative; width: 5em; height: 1.3em; line-height: 1.3em; margin-bottom: -1.3em; border-radius: 3px; text-align: center; top: -1.5em; display: block; color: #fff; background-color: rgba(0,0,0,.6); pointer-events: none; } ` )// “复制全文”标签 GM_addStyle(` div:nth-child(3) > div.box-header.box-header-top-icon > code:hover{ text-decoration: underline; /* 下划线 */ color: ${dark_mode ? "#9BF" : "#00C"}; } `) } // 使用MutationObserver观察器观察dom子节点变动,判断是否进入树洞详情页,从而判断是否添加“复制全文”点击事件 function addCopyFullTextButton() { if (!Config.copyFullText.value) return console.log('新增复制全文按钮'); waitForKeyElements('#eagleMapContainer div.sidebar > div.sidebar-content.sidebar-content-show > div > div:nth-child(3) > div.box-header.box-header-top-icon > code', (codeNodes) => { let codeNode = codeNodes[0] var triggered = false; codeNode.addEventListener("click", async (event) => { if (triggered) { console.log("请耐心等待复制结果,不要多次点击复制全文"); return } triggered = true; // 防止重复触发 event.stopPropagation(); // 停止点击事件传播 const content = await _copy_content_now(codeNode.innerText.replace("#", "").trim()); GM_setClipboard(content); // 复制文本进入剪切板 if (Config.showCopyFullTextAlert.value) // 判断复制成功是否需要提醒 alert("复制成功!"); else console.log("复制成功!") triggered = false; // 重置触发状态 }, { capture: true }) } ) } // 获取树洞的内容,以及关注数、回复数 async function _getBoxIdDetail(box_id) { const content = await new Promise((resolve, reject) => { fetch("https://treehole.pku.edu.cn/api/pku/" + box_id, { "headers": { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9", "authorization": "Bearer " + _getCookieObj()["pku_token"], "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "uuid": localStorage.getItem("pku-uuid"), }, "referrer": "https://treehole.pku.edu.cn/web/", "referrerPolicy": "strict-origin-when-cross-origin", "body": null, "method": "GET", "mode": "cors", "credentials": "include" }).then(response => { resolve(response.json()) }).catch(err => { reject(err) }) }) return content; } async function _getReplys(box_id, pages, sort = "asc") { try { var fetch_list = [] var timeout = 0 // 每隔一段时间进行一次请求,避免被封,也减小服务器压力 for (let page = 1; page <= pages; ++page) { timeout = timeout + 200; fetch_list.push(new Promise((resolve, reject) => { setTimeout(() => { fetch( `https://treehole.pku.edu.cn/api/pku_comment_v3/${box_id}?page=${page}&limit=15&sort=${sort}`, { "headers": { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9", "authorization": "Bearer " + _getCookieObj()["pku_token"], "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "uuid": localStorage.getItem("pku-uuid"), }, "referrer": "https://treehole.pku.edu.cn/web/", "referrerPolicy": "strict-origin-when-cross-origin", "body": null, "method": "GET", "mode": "cors", "credentials": "include" } ).then((response) => resolve(response.json())).catch((err) => reject(err)) }, timeout); })) } const data_list = await Promise.all(fetch_list) return data_list } catch (error) { console.error(error); throw error; } } async function _copy_content_now(box_id) { const box_id_detail = await _getBoxIdDetail(box_id); var copy_content = "" if (box_id_detail.code === 20000) { const box_id_reply_num = box_id_detail.data.reply // 回复数 const box_id_like_num = box_id_detail.data.likenum // 关注数 const box_id_content = box_id_detail.data.text // 内容 const box_id_time = box_id_detail.data.timestamp // 时间 let box_id_replys = await _getReplys(box_id, Math.ceil(box_id_reply_num / 15)) // 回复列表 box_id_replys = box_id_replys.map((reply) => reply.data.data).flat(1) copy_content = `#${box_id} ${timeFormat(box_id_time)} 关注数:${box_id_like_num} 回复数:${box_id_reply_num}\n${box_id_content}\n` box_id_replys.forEach((reply) => { copy_content += `#${reply.cid} ${timeFormat(reply.timestamp)}\n[${reply.name}] ${reply.text}\n` }) } return copy_content; } //================================================================================================================ // 添加“复制全文”标签 end //================================================================================================================ //================================================================================================================ // 设置随机壁纸 start // =============================================================================================================== /** * 在页面的右下角插入一张风车svg图片,点击风车会切换壁纸 * */ var angle = 0; // 旋转角度 var speed_up_acceleration = 0.05; // 加速加速度 var speed_down_acceleration = 0.02; // 减速加速度 var speed = 0.5; // 初始旋转速度 var min_speed = 0.1; // 最小旋转速度,小于该速度风车停下 var max_speed = 5.4; // 最大旋转速度(一秒钟转一圈半),大于该速度风车不再加速 var is_rotating = false; // 是否正在旋转,防止多次点击风车 /** * Define a function to rotate the image with easing * @param {HTMLImageElement} img * @returns {number} setInterval's ID */ function rotateImage(img) { speed = 0.5; is_rotating = true; const rotateInterval = setInterval(() => { if (speed < max_speed) { angle = (angle - speed) % 360; speed += speed_up_acceleration; } else { angle = (angle - max_speed) % 360; } img.style.transform = `rotate(${angle}deg)`; }, 10); return rotateInterval; } /** * Define a function to stop the rotation with easing * @param {number} rotateInterval * @param {HTMLImageElement} img */ function stopRotation(rotateInterval, img) { clearInterval(rotateInterval); const stopRotateInterval = setInterval(() => { if (speed >= min_speed) { angle = (angle - speed) % 360; speed -= speed_down_acceleration; } else { speed = 0; clearInterval(stopRotateInterval); } img.style.transform = `rotate(${angle}deg)`; }, 10); is_rotating = false; } // 配置随机壁纸库 function configureWallPaperHub() { if (!Config.configureWallPaperHub.value) return // 添加右下角风车图标 var windMillNode = document.createElement("div") windMillNode.setAttribute("class", "windmill") var windMillImageNode = document.createElement("img") windMillImageNode.id = "windmill" windMillImageNode.src = "https://infinityicon.infinitynewtab.com/assets/windmill.svg" var windMillStickNode = document.createElement("span") windMillNode.appendChild(windMillImageNode) windMillNode.appendChild(windMillStickNode) document.querySelector("body").appendChild(windMillNode) // 给风车添加css样式 GM_addStyle(` .windmill { position: absolute; right: 60px; bottom: 0px; display: flex; flex-direction: column; } .windmill img { z-index: 1; cursor: pointer; width: 40px; height: 40px; } .windmill span { display: block; margin-top: -30px; margin-left: auto; margin-right: auto; width: 5px; height: 56px; background-color: rgb(254, 254, 254); } `) if (localStorage.getItem("pku_background_id") != "6") { localStorage.setItem("pku_background_id", "6") // 设置背景图片来自图片网址 } if (Config.wallPaperHubSite.value == 'Bing') { windMillImageNode.addEventListener("click", function () { if (!is_rotating) { const rotateInterval = rotateImage(windMillImageNode); getOneBingWallPaper(callback = () => { stopRotation(rotateInterval, windMillImageNode) }); } }) } else if (Config.wallPaperHubSite.value == 'WallHaven') { windMillImageNode.addEventListener("click", function () { if (!is_rotating) { const rotateInterval = rotateImage(windMillImageNode); getOneWallHavenPaper(callback = () => { stopRotation(rotateInterval, windMillImageNode) }); } }) } } /** 获取一张Bing随机壁纸 */ function getOneBingWallPaper(callback) { GM_xmlhttpRequest({ method: "GET", url: "https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=10", onload: function (res) { try { if (res.status == 200) { var text = res.responseText; var json = JSON.parse(text); // console.log(json); const img = json.images[Math.round(Math.random() * (8))]; const figure_href = `https://cn.bing.com${img.url.split("_1920x1080")[0]}_UHD.jpg`; // 先请求看图片网址是否有效,如果有效再将图片网址存入localStorage // 这一步利用了HTTP的缓存机制,网页不会重复发送请求 GM_xmlhttpRequest({ method: "GET", url: figure_href, onload: function (response) { if (response.status == 200) { let tempImgNode = document.createElement('img'); tempImgNode.src = figure_href; tempImgNode.onload = function () { localStorage.setItem("pku_background_data", figure_href); document.querySelector("#app > div.bg-img").style = `background: url("${figure_href}") center center / cover rgb(29, 71, 134);` console.log(`从Bing获取壁纸成功:网址:${figure_href}`); callback(); // 停止风车旋转 } } else { throw new Error(`wallhaven网址 ${figure_dic.href} 请求失败:状态码 + ${response.status}`); } } }); } else { throw new Error(`Bing网址 ${url} 请求失败:状态码 + ${res.status}`); } } catch (error) { console.error(error) console.log("无法从Bing获取最新壁纸!"); callback(); // 停止风车旋转 } } }); } /**获取一张WallHaven随机壁纸 */ function getOneWallHavenPaper(callback) { if (Config.WallHavenUrl.value) { var _url = Config.WallHavenUrl.value; var given_page = false; // 是否指定了页数 // 如果url以“&page=数字”结尾,则去直接在该页获取壁纸 if (_url.match(/&page=\d+$/)) { given_page = true; } // 根据url请求壁纸的第一页 GM_xmlhttpRequest({ method: "GET", url: _url, onload: async function (res) { try { if (res.status == 200) { var text = res.responseText; const figure_dic = await _getRandomOneHref(text, given_page); // 获取随机一页的随机一张图片 // 先请求看图片网址是否有效,如果有效再将图片网址存入localStorage // 这一步利用了HTTP的缓存机制,网页不会重复发送请求 GM_xmlhttpRequest({ method: "GET", url: figure_dic.href, onload: function (response) { if (response.status == 200) { // 使用img节点监听图片是否加载完成,完成了再显示壁纸 let tempImgNode = document.createElement('img'); tempImgNode.src = figure_dic.href; tempImgNode.onload = function () { localStorage.setItem("pku_background_data", figure_dic.href); document.querySelector("#app > div.bg-img").style = `background: url("${figure_dic.href}") center center / cover rgb(29, 71, 134);` console.log(`从wallhaven获取壁纸成功:第${figure_dic.page}页,第${figure_dic.index}张壁纸,网址:${figure_dic.href}`); callback(); // 停止风车旋转 } } else { throw new Error(`wallhaven网址 ${figure_dic.href} 请求失败:状态码 + ${response.status}`); } } }) } else { throw new Error(`wallhaven网址 ${figure_dic.href} 请求失败:状态码 + ${response.status}`); } } catch (error) { console.error(error) console.log("无法从wallhaven获取最新壁纸!请尝试手动访问Wallhaven网站,以确保访问正常!"); callback(); // 停止风车旋转 } } }) } else { console.log("请在功能开关里输入WallHaven分类地址!") } /** * 随机一张壁纸 * @param {string} text 从网页获取的html文本 * @param {boolean} given_page 是否指定了页数;若指定,则text为指定页数的网页html文本,否则text为主页的网页html文本 */ async function _getRandomOneHref(text, given_page = false) { /* 使用doc节点的加载方式解析第一页(主页) */ var WallHavenDoc = document.createElement('wallhaven') WallHavenDoc.innerHTML = text // 如果没有给定页数,则随机一个页数 if (!given_page) { // 获取总页数 var total_pages = 1 // 总页数 // 解析总页数所在json字符串 try { var pages_json_string = WallHavenDoc.querySelector("ul.pagination").getAttribute("data-pagination") let pages_json = JSON.parse(pages_json_string) total_pages = pages_json.total } catch (error) { console.log(error) console.log("解析总页数失败(可能是因为总页数为1),只返回第一页的壁纸。") } // 在总页数中随机一个页数(如果只有一页,这种计算方式下page=1) var page = Math.floor(Math.random() * total_pages) + 1 // 如果随机到第一页(主页),由于已经请求过了,就不再做请求 // 如果随机到的不是第一页(主页),需要再做一次请求 if (page != 1) { _url = _url + `&page=${page}` // 做一次fetch,请求随机的一页 async function fetchOnePage(_url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: _url, onload: function (response) { resolve(response); }, onerror: function (error) { reject(error); } }); }); } var res = await fetchOnePage(_url) if (res.status == 200) { WallHavenDoc.innerHTML = res.responseText // 重新加载doc节点为第page页的html文本 } } } // 随机获取该页的一张图片 var thumbs = WallHavenDoc.querySelectorAll("div.thumb-info") var arr = [] for (let thumb_info of thumbs) { let id = thumb_info.querySelector("a.jsAnchor.thumb-tags-toggle.tagged").getAttribute("data-href", "").replace('https://wallhaven.cc/wallpaper/tags/', '').trim() if (id && thumb_info.querySelector('span.png')) { arr.push(`https://w.wallhaven.cc/full/${id.substring(0, 2)}/wallhaven-${id}.png`) } else { arr.push(`https://w.wallhaven.cc/full/${id.substring(0, 2)}/wallhaven-${id}.jpg`) } } let index = Math.floor(Math.random() * arr.length) // 返回壁纸信息 const figure_dic = { index: index, page: page, href: arr[index], } return figure_dic } } //================================================================================================================ // 设置随机壁纸 end // =============================================================================================================== //================================================================================================================ // 屏蔽关键词 start // =============================================================================================================== // 屏蔽主页面树洞关键词 function blockChunkKeywords(item) { if (!Config.blockKeywords.value) return if (!Config.customBlockKeywords.value || Config.customBlockKeywords.value.length < 1) return _checkNode(item) } // 屏蔽侧边栏(详情页)关键词 function blockSidebarKeywords() { if (!Config.blockKeywords.value) return if (!Config.customBlockKeywords.value || Config.customBlockKeywords.value.length < 1) return const callback = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const item of mutation.addedNodes) { _checkNode(item) } } }; const sidebar_container_observer = new MutationObserver(callback) // 这个观察器是用来观察侧边栏的box waitForKeyElements("div.sidebar-content.sidebar-content-show > div", (sidebar_container) => { sidebar_container_observer.observe(sidebar_container[0], { childList: true }); for (let item of sidebar_container[0].childNodes) { _checkNode(item) } // 这个for循环是为了避免在加载评论时,观察器晚加载导致前面的节点没进行关键词检测 }) } // 检查节点中是否含有屏蔽词 function _checkNode(Node) { for (const keyword of Config.customBlockKeywords.value) { // 遍历关键词黑名单 let text = Node.content || Node.textContent; if (keyword != '' && text.toLowerCase().indexOf(keyword.toLowerCase()) > -1) { // 找到就删除该信息流 console.log('已屏蔽:' + text); Node.hidden = true; Node.style.display = 'none'; break; } } } //================================================================================================================ // 屏蔽关键词 end // =============================================================================================================== //================================================================================================================ // 记录别名 start // =============================================================================================================== /** * 这个函数的作用是在“账户”侧边栏里显示收藏的树洞别名 */ function recordAlias(params) { if (!Config.recordAlias.value) return if (!GM_getValue("Alias_json", "")) GM_setValue("Alias_json", "{}"); waitForKeyElements('#eagleMapContainer > div.title-bar > div.control-bar > div > div:nth-child(2) > div > a:nth-child(3)', (abouts) => { const callback = () => { // 在sidebar区添加文本框 var text_area_nodes = _create_text_area_nodes() // 等待侧边栏唤起 waitForKeyElements('#eagleMapContainer div.box.box-right', (nodes) => { var box_right_node = nodes[0] var hr = document.createElement('hr') // 空行 hr.setAttribute('style', 'margin: 16px 0') box_right_node.appendChild(hr) // 添加空行 box_right_node.appendChild(text_area_nodes) // 添加别名文本框节点 load_alias() let config_text_area_node = text_area_nodes.querySelector('.config-textarea') config_text_area_node.addEventListener("change", save_alias) } ) } var about = abouts[0] // “搜索”右侧的“账户”按钮的选择器 about.addEventListener('click', callback) // 点击“账户”按钮,唤起侧边栏时,给侧边栏添加别名文本框 function _create_text_area_nodes() { var text_area_nodes = document.createElement('div') text_area_nodes.innerHTML = `
洞号别名:

在这里记录别名和洞号(请用“别名+空格+洞号”的格式记录,以支持搜索提醒)

` return text_area_nodes } } ) } /** 将收藏的树洞加载到“账户”的侧边栏详情页中 */ function load_alias() { let alias_json_str = GM_getValue("Alias_json", "{}"); if (alias_json_str) { try { let alias_dic = JSON.parse(alias_json_str) let config_text_area_node = document.querySelector('.config-textarea') let alias = ""; for (let key in alias_dic) { alias = alias + key + " " + alias_dic[key] + "\n" } config_text_area_node.value = alias; } catch (error) { console.log("解析洞号别名错误!" + error) } } else { let config_text_area_node = document.querySelector('.config-textarea') let alias = ""; config_text_area_node.value = alias; } } /** 将“账户”侧边栏的收藏树洞保存 */ function save_alias() { let config_text_area_node = document.querySelector('.config-textarea'); let alias = config_text_area_node.value; let alias_arr = alias.split("\n") let alias_dic = {} for (let alia_str of alias_arr) { if (alia_str.length > 0) { // 不是空行 // alia_str = alia_str.trim() // let alia_arr = alia_str.split(" ").filter(d => d) // 去掉中间多余的空格 let alia_arr = alia_str.split(" ") if (alia_arr.length) { let key = "", value = "" if (alia_arr.length == 2) { key = alia_arr[0], value = alia_arr[1]; } // 刚好是key-value键值对 else if (alia_arr.length == 1) { key = alia_arr[0], value = ""; } // 这一行没有空格 else { key = alia_arr.slice(0, alia_arr.length - 1).join(" "), value = alia_arr[alia_arr.length - 1] } // 这一行有多个空格,以最后一个空格为分界 alias_dic[key] = value } } } try { let alias_json_str = JSON.stringify(alias_dic) GM_setValue("Alias_json", alias_json_str) } catch (error) { console("保存Alias_json发生错误!" + error) } } /** * 添加一条树洞别名 * @param {string} comment 树洞描述 * @param {string} hole_id 洞号 */ function addOneAlia(comment, hole_id) { let alias_json_str = GM_getValue("Alias_json", "{}"); if (!comment) { console.log("请输入树洞描述/别名!"); return } try { let alias_dic = JSON.parse(alias_json_str) if (typeof (comment) != 'string') throw "comment is not string" else if (comment in alias_dic) throw "现有描述(别名)已存在!" else { alias_dic[comment] = hole_id; try { alias_json_str = JSON.stringify(alias_dic) GM_setValue("Alias_json", alias_json_str) } catch (error) { alert("保存Alias_json发生错误:" + error) } } } catch (error) { alert("存储洞号别名失败:" + error) } } /** * 在树洞详情页添加“添加别名”按钮 */ function addAddAliaButton() { if (!Config.recordAlias.value) return let toolbar = document.querySelector("div.box.box-tip.sidebar-toolbar") if (toolbar) { let add_alia_button = document.createElement("span") add_alia_button.classList.add("sidebar-toolbar-item") add_alia_button.innerHTML = ` ` add_alia_button.addEventListener("click", () => { let newComment = prompt('请输入对该洞的描述(不需要输入洞号):', ''); if (newComment === '') { alert("未输入别名,无法储存") } else { var hole_id = document.querySelector("#eagleMapContainer > div:nth-child(3) > div > div:nth-child(2) > div.sidebar > div.sidebar-content.sidebar-content-show > div > div:nth-child(3) > div.box-header.box-header-top-icon > code") if (hole_id) addOneAlia(newComment, hole_id.innerText) else alert("洞号不存在,无法储存") } }) toolbar.insertBefore(add_alia_button, toolbar.lastChild) } } //================================================================================================================ // 记录别名 end // =============================================================================================================== //================================================================================================================ // 添加课程评估网站、bbs网站按钮 start // =============================================================================================================== function addSiteButton() { if (!Config.addSiteButton.value) return waitForKeyElements("#eagleMapContainer > div.title-bar > div.app-switcher", (app_switcher_list) => { let app_switcher = app_switcher_list[0] if (Config.addCourseEvaluationSiteButton.value) { let app_switcher_item = document.createElement("a") let app_switcher_right = document.querySelector("#eagleMapContainer > div.title-bar > div.app-switcher > span.app-switcher-desc.app-switcher-right") // 参考位置节点 app_switcher_item.setAttribute(`${getDataVersionByNode(app_switcher_right)}`, "") app_switcher_item.setAttribute("class", "app-switcher-item ") app_switcher_item.setAttribute("href", "https://courses.pinzhixiaoyuan.com/") // course_evalution_site_icon_base64 是课程网站的图标icon,这里直接用base64给出是为了减少跨域请求 let course_evalution_site_icon_base64 = "data:image/png;base64,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" app_switcher_item.innerHTML = ` 课程测评 ` app_switcher.insertBefore(app_switcher_item, app_switcher_right) } if (Config.addBBSSiteButton.value) { let app_switcher_item = document.createElement("a") let app_switcher_right = document.querySelector("#eagleMapContainer > div.title-bar > div.app-switcher > span.app-switcher-desc.app-switcher-right") // 参考位置节点 app_switcher_item.setAttribute(`${getDataVersionByNode(app_switcher_right)}`, "") app_switcher_item.setAttribute("class", "app-switcher-item ") app_switcher_item.setAttribute("href", "https://bbs.pku.edu.cn/v2/home.php") //未名bbs网站 let bbs_site_icon_src = "https://bbs.pku.edu.cn/v2/images/logo.jpg" app_switcher_item.innerHTML = ` 未名BBS ` app_switcher.insertBefore(app_switcher_item, app_switcher_right) } }) } //================================================================================================================ // 添加课程评估网站按钮 end // =============================================================================================================== //================================================================================================================ // 搜索建议功能(历史记录+收藏) begin // =============================================================================================================== function showSuggestionBlock() { var record_history = Config.recordHistory.value var show_history_suggestion = Config.showHistorySuggestions.value && record_history // 显示历史记录必须先打开保存历史记录功能 var show_alias_suggestion = Config.showAliasSuggestions.value if (!record_history) clearAllHistory(); if (!record_history && !show_history_suggestion && !show_alias_suggestion) { // 如果三个功能都没开启,那就直接返回 return } if (show_history_suggestion && (!localStorage.getItem("search_history"))) { // 如果没有历史记录,则创建一个空的历史记录 localStorage.setItem("search_history", "") } addSuggestionBlockCSS() var search_box; // 搜索框 waitForKeyElements("#eagleMapContainer > div.title-bar > div.control-bar > div > div:nth-child(1) > div > input", (search_boxs) => { search_box = search_boxs[0]; let search_box_cache = "" //
用来显示历史记录栏 var selectedId = document.createElement("div") selectedId.setAttribute("id", "selectedId") search_box.oninput = function refreshItem() { if (!document.querySelector("#selectedId")) search_box.parentNode.parentNode.appendChild(selectedId) // 添加历史记录栏 //删除ul var drop = document.getElementById("drop"); if (drop) selectedId.removeChild(drop); //把ul添加回来 var originalUl = document.createElement("ul"); originalUl.id = "drop"; selectedId.appendChild(originalUl); showList(search_box, show_alias_suggestion, show_history_suggestion); } // 添加获取焦点事件 search_box.onfocus = function () { // 初始下拉列表 if (!document.querySelector("#selectedId")) search_box.parentNode.parentNode.appendChild(selectedId) // 添加历史记录栏 var originalUl = document.createElement("ul"); originalUl.id = "drop"; selectedId.appendChild(originalUl); showList(search_box, show_alias_suggestion, show_history_suggestion); } //添加失去焦点事件 search_box.onblur = function () { // console.log("soutsout") var drop = document.getElementById("drop"); if (drop) { selectedId.removeChild(drop); search_box.parentNode.parentNode.removeChild(selectedId) } } // 添加键盘按键事件,按下按键时,下拉列表的选中框会移动,输入框的文本会填充 search_box.onkeydown = function (event) { var drop = document.getElementById("drop"); if (drop) { var lis = drop.getElementsByTagName("li"); var len = lis.length; var index = -1; // 查找当前上下键选中框 for (var i = 0; i < len; i++) { if (lis[i].style.backgroundColor == "darkgrey") { index = i; break; } } if (event.keyCode === 38) { // 上箭头 if (index == -1) { // 当前没有选中框 search_box_cache = search_box.value // 暂存当前值 search_box.value = lis[len - 1].querySelector("span").innerText; lis[len - 1].style.backgroundColor = "darkgrey"; } else if (index == 0) {// 当前选中框为第一个 search_box.value = search_box_cache lis[index].style.backgroundColor = ""; } else { search_box.value = lis[index - 1].querySelector("span").innerText; lis[index - 1].style.backgroundColor = "darkgrey"; lis[index].style.backgroundColor = "" } } else if (event.keyCode === 40) { // 下箭头 if (index == -1) { // 当前没有选中框 search_box_cache = search_box.value // 暂存当前值 search_box.value = lis[index + 1].querySelector("span").innerText; lis[index + 1].style.backgroundColor = "darkgrey"; } else if (index == len - 1) { search_box.value = search_box_cache lis[index].style.backgroundColor = ""; } else { search_box.value = lis[index + 1].querySelector("span").innerText; lis[index + 1].style.backgroundColor = "darkgrey"; lis[index].style.backgroundColor = ""; } } else if (event.keyCode === 13) { // 回车,真正选中(此时触发input事件,文本框的值真正被修改) search_box.dispatchEvent(new Event('input')) if (Config.recordHistory.value) addOneHistory(search_box.value); } } } // 给搜索按钮也绑定click事件,点击后存储搜索记录 waitForKeyElements("#eagleMapContainer > div.title-bar > div.control-bar > div > div:nth-child(2) > div > div > button", (buttons) => { let button = buttons[0]; button.addEventListener('mousedown', () => { search_box.dispatchEvent(new Event('input')) }) button.addEventListener('click', () => { if (Config.recordHistory.value) addOneHistory(search_box.value); }) }) }); } /** * 添加历史记录框的CSS */ function addSuggestionBlockCSS() { let historyBlockCSS = ` /* 搜索下拉框*/ ul#drop { position: relative; display: flex; flex-direction: column; min-width: 0; padding: 0; margin: 0; list-style-type: disc; margin-block-start: 0em; margin-block-end: 0em; margin-inline-start: 0px; margin-inline-end: 0px; } ul#drop li { display: flex; min-width: 0; max-height: none; padding: 0; flex-direction: column; border-radius: 4px; } ul#drop li:hover { background-color: darkgrey; border-radius: 4px; } ul#drop li div.li_block{ padding-left:10px; padding-right:10px; display: inline-block; float: left } ul#drop li div.li_content{ font-size:15px; display: block; } ul#drop li span{ float: left } ul#drop li span.alias{ } ul#drop li span.history.light_mode{ color: #52188c; /* 亮色主题下历史记录文字用紫色显示 */ } ul#drop li span.history.dark_mode{ color: #00CCFF; /* 暗色主题下历史记录文字用天蓝色显示 */ } ul#drop li div.delete_item{ display: block; float: right; } ul#drop li div.delete_button{ color: #70757a; font-size: 15px; cursor: pointer; align-self: center; opacity: 0 /*光标没有悬浮在“删除”按钮上时,透明化(相当于不显示) */ } ul#drop li div.delete_button:hover{ color: #1558d6; text-decoration: underline; text-decoration-line: underline; text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial; } ul#drop li:hover div.delete_button{ opacity: 1; } ul#drop li div.alias{ /* 别名显示框 */ display: inline-block; float: right; } ul#drop li div.alias span{ color: #D3D3D3; font-size: 12px; width: 100%; width: -moz-available; /* WebKit-based browsers will ignore this. */ width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */ width: fill-available; text-align: right } div#selectedId { position: relative; text-align: left; -webkit-user-select: none; width: 100%; padding-left: 144px; } @media screen and (max-width: 900px){ /* 页面缩小时自适应 */ div#selectedId{ padding-left: 80px; } } ` GM_addStyle(historyBlockCSS) } /** * 显示li标签(显示搜索时的下拉列表) * @param search_box {Node} 搜索文本框的dom节点 * @param show_alias_suggestion {bool} 是否显示别名/收藏建议 * @param show_history_suggestion {bool} 是否显示历史记录建议 */ function showList(search_box, show_alias_suggestion, show_history_suggestion) { if (show_alias_suggestion && search_box.value) { // 解析别名字典 var alias_json_str = GM_getValue("Alias_json", "{}"); if (alias_json_str) { try { var alias_dic = JSON.parse(alias_json_str) var alias = ""; } catch (error) { console.log("解析洞号别名错误!" + error) } } // alias_arr是别名数组 var alias_arr = Object.keys(alias_dic) var alias_res = searchByIndexOf(search_box.value, alias_arr); for (var i = 0; i < alias_res.length; i++) { var li = document.createElement("li"); li.innerHTML = `
${alias_dic[alias_res[i]]}
${alias_res[i]}
` li.addEventListener("click", (event) => { event.stopPropagation(); if (event.target.nodeName === "SPAN") // 如果直接点击的是文本span标签,则 search_box.value = event.target.innerText; else search_box.value = event.target.querySelector("span.suggestion").innerText; search_box.dispatchEvent(new Event('input')) $("#eagleMapContainer > div.title-bar > div.control-bar > div > div:nth-child(2) > div > div > button").click() }) // 添加mousedown事件,防止blur事件先于click发生 li.addEventListener("mousedown", (event) => { event.preventDefault() }) document.getElementById("drop").appendChild(li); } } if (show_history_suggestion) { // history_arr是历史记录 var history_arr = getALLHistory(); var history_res = searchByIndexOf(search_box.value, history_arr); var dark_mode = document.querySelector("body").getAttribute("style").indexOf("--theme_bgc_color:rgba(31,31,31,0.8); --theme_font_color:#ededed;") > -1; for (var i = 0; i < history_res.length && i < Config.maxShownHistorySize.value; i++) { let li = document.createElement("li"); li.innerHTML = `
${history_res[i]}
删除
` li.addEventListener("click", (event) => { event.stopPropagation(); if (event.target.nodeName === "SPAN") // 如果直接点击的是文本span标签,则直接输入文本 search_box.value = event.target.innerText; else // 如果是文本span标签的父标签,则查找span标签 search_box.value = event.target.querySelector("span.suggestion").innerText; search_box.dispatchEvent(new Event('input')) $("#eagleMapContainer > div.title-bar > div.control-bar > div > div:nth-child(2) > div > div > button").click() }) // 添加mousedown事件,防止blur事件先于click发生 li.addEventListener("mousedown", (event) => { event.preventDefault() }) var delete_button = li.querySelector(".delete_button"); delete_button.addEventListener("click", (event) => { // 点击删除按钮 event.stopPropagation() let parent_li = $(event.target).parents("li") deleteOneHistory(parent_li.find("span").text()) parent_li.remove() }) document.getElementById("drop").appendChild(li); } } } /** * 模糊查询:利用字符串的indexOf方法 */ function searchByIndexOf(keyWord, list) { if (!(list instanceof Array)) { return; } if (keyWord == "") { return list; } else { var len = list.length; var arr = []; for (var i = 0; i < len; i++) { //如果字符串中不包含目标字符会返回-1 if (list[i].toLowerCase().indexOf(keyWord.toLowerCase()) >= 0) { // 查找字符串方法不区分大小写 arr.push(list[i]); } } return arr; } } /** * * 获取搜索关键词设置历史记录 * 过滤一个结果的空记录添加,过滤空搜索 默认保存200条记录 可修改 * (这种记录方式有个小bug,就是搜索记录里不能有|,先用着吧) */ function addOneHistory(keyword) { keyword = keyword.trim(); // 过滤字符串左右的空格(不过滤字符串中间的空格) if (!keyword) { return false; // 字符串为空时禁止 } let historyIndexSearchItems = localStorage.getItem("search_history"); // 获取历史记录的字符串 if (!historyIndexSearchItems) { localStorage.setItem("search_history", keyword); } else { const onlyItem = historyIndexSearchItems.split('|').filter(e => e != keyword); if (onlyItem.length > 0) { historyIndexSearchItems = keyword + '|' + onlyItem.slice(0, Config.maxHistorySize.value).join('|'); } localStorage.setItem("search_history", historyIndexSearchItems) } } /** * 删除一条历史记录 */ function deleteOneHistory(keyword) { keyword = keyword.trim(); // 过滤字符串左右的空格(不过滤字符串中间的空格) if (!keyword) { return false; // 字符串为空时禁止 } let historyIndexSearchItems = localStorage.getItem("search_history"); // 获取历史记录的字符串 if (!historyIndexSearchItems) { return false; } else { const onlyItem = historyIndexSearchItems.split('|').filter(e => e); if (onlyItem.length > 0) { const index = onlyItem.indexOf(keyword); if (index > -1) { onlyItem.splice(index, 1); historyIndexSearchItems = onlyItem.slice(0, Config.maxHistorySize.value).join('|'); localStorage.setItem("search_history", historyIndexSearchItems); return true; } return false; } return false; } } /** * 获取所有历史记录 * @return: String Array */ function getALLHistory() { let historyIndexSearchItems = localStorage.getItem("search_history"); if (historyIndexSearchItems) return historyIndexSearchItems.split('|').filter(e => e) else return [] } /** * 清除所有历史记录 */ function clearAllHistory() { localStorage.removeItem('search_history'); } //================================================================================================================ // 搜索历史记录功能 end // =============================================================================================================== //================================================================================================================ // 显示最新回复时间 start // =============================================================================================================== /** * 该函数将一个时间戳转换为时间格式 * dateTimeStamp是时间戳,10位 * @param {string} dateTimeStamp * @returns 标准时间格式字符串 */ function timeFormat(dateTimeStamp) { let datetime = new Date(dateTimeStamp * 1000); let Nyear = datetime.getFullYear(); let Nmonth = datetime.getMonth() + 1 < 10 ? "0" + (datetime.getMonth() + 1) : datetime.getMonth() + 1; let Ndate = datetime.getDate() < 10 ? "0" + datetime.getDate() : datetime.getDate(); let Nhour = datetime.getHours() < 10 ? "0" + datetime.getHours() : datetime.getHours(); let Nminute = datetime.getMinutes() < 10 ? "0" + datetime.getMinutes() : datetime.getMinutes(); let Nsecond = datetime.getSeconds() < 10 ? "0" + datetime.getSeconds() : datetime.getSeconds(); result = Nyear + "-" + Nmonth + "-" + Ndate + " " + Nhour + ":" + Nminute return result; } /** * dateTimeStamp是评论的发送时间戳,10位 * @author https://blog.csdn.net/Aurora_____/article/details/110390353 * @param dateTimeStamp * @returns {string} */ function timeAgo(dateTimeStamp) { let result = ""; let minute = 1000 * 60; //把分,时,天,周,半个月,一个月用毫秒表示 let hour = minute * 60; let day = hour * 24; let week = day * 7; let halfamonth = day * 15; let month = day * 30; let now = new Date().getTime(); //获取当前时间毫秒 let diffValue = now - dateTimeStamp * 1000;//时间差 if (diffValue < 0) { return result; } let minC = Math.floor(diffValue / minute); //计算时间差的分,时,天,周,月 let hourC = Math.floor(diffValue / hour); let dayC = Math.floor(diffValue / day); let weekC = Math.floor(diffValue / week); let monthC = Math.floor(diffValue / month); if (monthC >= 1 && monthC <= 11) { result = " " + parseInt(monthC) + "月前" } else if (weekC >= 1 && weekC <= 3) { result = " " + parseInt(weekC) + "周前" } else if (dayC >= 1 && dayC <= 6) { result = " " + parseInt(dayC) + "天前" } else if (hourC >= 1 && hourC <= 23) { result = " " + parseInt(hourC) + "小时前" } else if (minC >= 1 && minC <= 59) { result = " " + parseInt(minC) + "分钟前" } else if (diffValue >= 0 && diffValue <= minute) { result = "刚刚" } else { result = timeFormat(dateTimeStamp) } return result; } // 最新回复时间记录字典 var last_reply_time_dic = {} /** 修改xhr的open方法,监听返回值,截取最近回复时间 */ function listenLastestReplyTime() { if (!Config.showLastestReplyTime.value) return const url_regexp = RegExp('/api/pku_comment_v3/(\\d+)\\?limit=10'); // 重写open方法,截取最近回复时间 const originOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (_, url) { const matched = url.match(url_regexp) if (matched) { this.addEventListener("readystatechange", function () { if (this.readyState === 4) { const res = JSON.parse(this.response); const box_id = matched[1] const total_reply = res.data.total // 总回复数 var last_page = res.data.last_page var lastest_reply_time if (last_page == 1) { // 如果总评论少于10条,可以直接提取出最近回复 lastest_reply_time = res.data.data[res.data.data.length - 1]['timestamp'] last_reply_time_dic[box_id] = lastest_reply_time // console.log(box_id + "最新回复时间:" + lastest_reply_time) } else { // 如果总评论多于10条,需要再次发请求 setTimeout(function () { GM_xmlhttpRequest({ method: "GET", url: "https://treehole.pku.edu.cn/api/pku_comment_v3/" + box_id + "?limit=50000", headers: { "accept": "application/json, text/plain, */*", "accept-language": "zh-CN,zh;q=0.9", "authorization": "Bearer " + _getCookieObj()["pku_token"], "sec-fetch-dest": "empty", "sec-fetch-mode": "cors", "sec-fetch-site": "same-origin", "uuid": localStorage.getItem("pku-uuid"), }, "referrer": "https://treehole.pku.edu.cn/web/", "referrerPolicy": "strict-origin-when-cross-origin", "body": null, "mode": "cors", "credentials": "include", onload: function (again_res) { if (again_res.status == 200) { try { const again_res_json = JSON.parse(again_res.responseText); lastest_reply_time = again_res_json.data.data[again_res_json.data.data.length - 1]['timestamp'] last_reply_time_dic[box_id] = lastest_reply_time // console.log(box_id + "最新回复时间:" + lastest_reply_time) } catch (error) { console.log(error) console.log("无法获取最新回复时间!" + box_id); } } else { console.log("请求失败:状态码" + again_res.status); } } }) }, Math.random() * 5000); // 平均延迟2.5秒,防止请求过于频繁 } this.response = JSON.stringify(res); } }); } originOpen.apply(this, arguments); }; } //================================================================================================================ // 显示最新回复时间 end // =============================================================================================================== //================================================================================================================ // 点击同意服务协议 start // =============================================================================================================== /** 点击登录页面的“同意北大树洞服务协议”,回车登录 */ function clickToAgreeToTheServiceAgreement() { if (Config.clickAgreeServiceAgreement.value) { waitForKeyElements('div.main-login > p.bottom-footer > input[type=checkbox]', (check_boxs) => { let check_box = check_boxs[0] if (!check_box.checked) check_box.click() // setTimeout(()=>{document.querySelector("#app button").click()}, 1000) document.querySelector("#app input[type=text]").focus() document.querySelector("#app input[type=text]").addEventListener("keydown", (event) => { if (event.keyCode === 13) { // console.log("监听到回车键,点击登录!") document.querySelector("#app button").click() } }, true) }, true) } } //================================================================================================================ // 点击同意服务协议 end // =============================================================================================================== //================================================================================================================ // 点击其它链接时,关闭提醒,自动跳转外链 start // =============================================================================================================== function clickToCloseAndJump() { if (Config.openTheFuckingURLRightNow.value) { waitForKeyElements('body > div.v-modal', (dialog) => { document.querySelector("body > div.el-dialog__wrapper > div > div.el-dialog__footer > span > button.el-button.el-button--primary").click() document.querySelector("body > div.v-modal").remove() document.querySelector("body > div.el-dialog__wrapper").remove() } ) } } //================================================================================================================ // 点击其它链接时,关闭提醒,自动跳转外链 end // =============================================================================================================== //================================================================================================================ // 滚动穿透:解除点击详情页时不能滚动外部树洞列表的问题 begin // =============================================================================================================== // !!!!!!!!!!!!!!!!!!开发中,尚未实现 function cancelScrollLimit(params) { const callback = (mutationsList, observer) => { for (const mutation of mutationsList) { for (const target of mutation.addedNodes) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0 && mutation.addedNodes && mutation.addedNodes[0].nodeName === 'DIV') var sidebar_shadow = document.querySelector(".sidebar-shadow").remove() break } } }; const observer = new MutationObserver(callback); observer.observe(document, { childList: true, subtree: true }); } //================================================================================================================ // 滚动穿透:解除点击详情页时不能滚动外部树洞列表的问题 end // =============================================================================================================== (function () { // 登录页面优化 if (location.pathname.indexOf("/web/login") > -1) clickToAgreeToTheServiceAgreement() // 登录之后页面优化 if (location.pathname.endsWith("/web/")) { addCopyFullTextButtonIcon() // 添加复制全文图标 cancelMaxHeight() // 取消全文最大长度限制 configureWallPaperHub() // 随机壁纸功能 recordAlias() // 别名记录 addSiteButton() // 添加课程测评按钮 showSuggestionBlock() // 显示搜索建议(包括历史记录和收藏) listenLastestReplyTime() // 监听最后一次回复的时间chunk // 使用MutationObserver持续监听是否跳出了外链警告弹窗 const dialog_observer = new MutationObserver((mutationsList, observer) => { if(document.querySelector("body > div.v-modal")) // 如果有弹窗,关闭弹窗 clickToCloseAndJump(); // 点击其它链接时,关闭提醒,自动跳转外链 }); dialog_observer.observe(document.body, { childList: true, subtree: false }); GM_addStyle(`.box-footer{opacity:0.5;}`) // 这个是监听网页插入事件,用来判断后续网页动态插入的元素(动态插入树洞) waitForKeyElements("div.flow-chunk", (chunk) => { var flow_chunk = chunk[0]; // 这个观察器是用来观察主页面的box流 let chunk_observer = new MutationObserver((mutationsList, observer) => { // console.log(`debug`) for (const mutation of mutationsList) { for (const item of mutation.addedNodes) { blockChunkKeywords(item) } if (mutation.addedNodes) { setTimeout(() => { // 懒得用查找了,直接延时 let flow_item_row_list = document.querySelectorAll("#table_list > div.flow-chunk > div > div") for (let flow_item_row of flow_item_row_list) { // console.log(flow_item_row) let exist_reply_icon = flow_item_row.querySelector("div.box-header > span:nth-child(3)") // 回复数图标 let exist_box_footer = flow_item_row.querySelector("div.flow-item > div.box > div.box-footer") // 最新回复时间文本 if (!exist_reply_icon || exist_box_footer) // 已经没有回复,或者已经有最新回复时间,跳过 continue; else { let box_id = flow_item_row.querySelector("div.flow-item > div.box > div.box-header > code.box-id.--box-id-copy-content").innerText.replace("#", "").trim() // 洞号 let box = flow_item_row.querySelector("div.flow-item > div.box") // console.log(box) if (last_reply_time_dic[box_id]) { let box_footer = document.createElement("div") box_footer.setAttribute(`${getDataVersionByNode(box.querySelector("div.box-header"))}`, "") box_footer.classList.add("box-footer") box_footer.innerText = "最新回复时间:" + timeAgo(last_reply_time_dic[box_id]) box.appendChild(box_footer) } } } }, 1000) } } } ); chunk_observer.observe(flow_chunk, { childList: true, subtree: true }); }) // 绑定侧边栏观察器 waitForKeyElements('.left-container', (value) => { var left_container = value[0]; let sidebar_rise_observer = new MutationObserver((mutationsList, observer) => { for (const mutation of mutationsList) { if (mutation.addedNodes.length && mutation.addedNodes[0].nodeName != "#comment" && document.querySelector("div.box.box-tip.sidebar-toolbar")) { // 确实唤起了详情页侧边栏(而不是关闭侧边栏,以及打开账户、发表树洞的侧边栏) addCopyFullTextButton(); blockSidebarKeywords(); addAddAliaButton() break } } }); sidebar_rise_observer.observe(left_container, { childList: true }); // 观察子节点的变动 }) } }()) /** * 这段代码来自于 https://gist.github.com/BrockA/2625891 ,将其复制下来是因为油猴脚本对github的跨域访问不太友好,不然可以直接在最前面require 此外还有一些安全性的考虑 */ /**--- waitForKeyElements(): A utility function, for Greasemonkey scripts, that detects and handles AJAXed content. Usage example: waitForKeyElements ( "div.comments" , commentCallbackFunction ); //--- Page-specific function to do what we want when the node is found. function commentCallbackFunction (jNode) { jNode.text ("This comment changed by waitForKeyElements()."); } IMPORTANT: This function requires your script to have loaded jQuery. */ function waitForKeyElements( selectorTxt, /* Required: The jQuery selector string that specifies the desired element(s). */ actionFunction, /* Required: The code to run when elements are found. It is passed a jNode to the matched element. */ bWaitOnce, /* Optional: If false, will continue to scan for new elements even after the first match is found. */ iframeSelector /* Optional: If set, identifies the iframe to search. */ ) { var targetNodes, btargetsFound; if (typeof iframeSelector == "undefined") targetNodes = $(selectorTxt); else targetNodes = $(iframeSelector).contents() .find(selectorTxt); if (targetNodes && targetNodes.length > 0) { btargetsFound = true; /*--- Found target node(s). Go through each and act if they are new. */ targetNodes.each(function () { var jThis = $(this); var alreadyFound = jThis.data('alreadyFound') || false; if (!alreadyFound) { //--- Call the payload function. var cancelFound = actionFunction(jThis); if (cancelFound) btargetsFound = false; else jThis.data('alreadyFound', true); } }); } else { btargetsFound = false; } //--- Get the timer-control variable for this selector. var controlObj = waitForKeyElements.controlObj || {}; var controlKey = selectorTxt.replace(/[^\w]/g, "_"); var timeControl = controlObj[controlKey]; //--- Now set or clear the timer as appropriate. if (btargetsFound && bWaitOnce && timeControl) { //--- The only condition where we need to clear the timer. clearInterval(timeControl); delete controlObj[controlKey] } else { //--- Set a timer, if needed. if (!timeControl) { timeControl = setInterval(function () { waitForKeyElements(selectorTxt, actionFunction, bWaitOnce, iframeSelector ); }, 300 ); controlObj[controlKey] = timeControl; } } waitForKeyElements.controlObj = controlObj; }